diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index fa323f9..61fef75 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -28,7 +28,7 @@ jobs: - uses: actions-rust-lang/setup-rust-toolchain@v1 with: toolchain: nightly - - run: cargo test + - run: cargo test --features all clippy: @@ -42,4 +42,4 @@ jobs: with: toolchain: nightly components: clippy - - run: cargo clippy --tests -- -Dclippy::all + - run: cargo clippy --features all --tests -- -Dclippy::all diff --git a/Cargo.toml b/Cargo.toml index 5e72391..860ed2c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,8 @@ tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } [features] default = ["text", "binary"] +# contains all the parsers +all = ["text", "binary"] text = ["dep:atoi_radix10"] binary = [] diff --git a/src/config.rs b/src/config.rs index 3318dea..230f9f6 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,6 +6,7 @@ pub const WEB_HOST: &str = "127.0.0.1:3000"; pub const IMAGE_SAVE_INTERVAL: Duration = Duration::from_secs(5); pub const JPEG_UPDATE_INTERVAL: Duration = Duration::from_millis(17); pub const WEB_UPDATE_INTERVAL: Duration = Duration::from_millis(50); +pub const STDOUT_STATISTICS_INTERVAL: Duration = Duration::from_millis(5000); pub const HELP_TEXT: &[u8] = b"Flurry is a pixelflut implementation, this means you can use commands to get and set pixels in the canvas SIZE returns the size of the canvas diff --git a/src/flutclient.rs b/src/flutclient.rs index ddd9f2f..69c27da 100644 --- a/src/flutclient.rs +++ b/src/flutclient.rs @@ -10,7 +10,7 @@ use crate::{ grid::{self, Flut}, increment_counter, protocols::{BinaryParser, IOProtocol, Parser, Responder, TextParser}, - set_pixel_rgba, Canvas, Color, Command, Coordinate, Protocol, Response, + set_pixel_rgba, Canvas, Color, Command, Coordinate, Protocol, ProtocolStatus, Response, }; macro_rules! build_parser_type_enum { @@ -26,16 +26,27 @@ macro_rules! build_parser_type_enum { impl std::default::Default for ParserTypes { // add code here + #[allow(unreachable_code)] fn default() -> Self { $( #[cfg(feature = $feat)] - #[allow(unreachable_code)] return ParserTypes::$name(<$t>::default()); )* } } impl ParserTypes { + pub fn get_status() -> Vec { + vec![ + $( + #[cfg(feature = $feat)] + ProtocolStatus::Enabled($feat), + #[cfg(not(feature = $feat))] + ProtocolStatus::Disabled($feat), + )* + ] + } + pub fn announce() { $( #[cfg(feature = $feat)] @@ -88,6 +99,14 @@ where Ok(()) } + async fn protocols_command(&mut self) -> io::Result<()> { + match_parser! { + parser: self.parser => parser.unparse(Response::Protocols(ParserTypes::get_status()), &mut self.writer).await? + }; + self.writer.flush().await?; + Ok(()) + } + async fn size_command(&mut self, canvas: Canvas) -> io::Result<()> { let (x, y) = self.grids[canvas as usize].get_size(); match_parser!(parser: self.parser => parser.unparse( @@ -166,6 +185,7 @@ where match parsed { Ok(Command::Help) => self.help_command().await?, Ok(Command::Size(canvas)) => self.size_command(canvas).await?, + Ok(Command::Protocols) => self.protocols_command().await?, Ok(Command::GetPixel(canvas, x, y)) => self.get_pixel_command(canvas, x, y).await?, Ok(Command::SetPixel(canvas, x, y, color)) => self.set_pixel_command(canvas, x, y, &color), Ok(Command::ChangeCanvas(canvas)) => { diff --git a/src/lib.rs b/src/lib.rs index 4b35c6f..ca49c1f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -53,6 +53,12 @@ fn increment_counter(amount: u64) { COUNTER.fetch_add(amount, std::sync::atomic::Ordering::Relaxed); } +#[derive(Debug, PartialEq)] +pub enum ProtocolStatus { + Enabled(&'static str), + Disabled(&'static str), +} + #[derive(Debug, PartialEq)] pub enum Protocol { Text, @@ -62,6 +68,7 @@ pub enum Protocol { #[derive(Debug, PartialEq)] pub enum Command { Help, + Protocols, Size(Canvas), GetPixel(Canvas, Coordinate, Coordinate), SetPixel(Canvas, Coordinate, Coordinate, Color), @@ -72,6 +79,7 @@ pub enum Command { #[derive(Debug, PartialEq)] pub enum Response { Help, + Protocols(Vec), Size(Coordinate, Coordinate), GetPixel(Coordinate, Coordinate, [u8; 3]), } diff --git a/src/main.rs b/src/main.rs index 74483e0..d59f101 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,9 @@ use std::{ }; use flurry::{ - config::{GRID_LENGTH, HOST, IMAGE_SAVE_INTERVAL, JPEG_UPDATE_INTERVAL}, + config::{ + GRID_LENGTH, HOST, IMAGE_SAVE_INTERVAL, JPEG_UPDATE_INTERVAL, STDOUT_STATISTICS_INTERVAL, + }, flutclient::{FlutClient, ParserTypes}, grid::{self, Flut}, webapi::WebApiContext, @@ -20,7 +22,7 @@ use tracing_subscriber::{layer::SubscriberExt as _, util::SubscriberInitExt as _ /// This function logs the current amount of changed pixels to stdout every second async fn pixel_change_stdout_log() -> AsyncResult { - let mut interval = tokio::time::interval(Duration::from_millis(1000)); + let mut interval = tokio::time::interval(STDOUT_STATISTICS_INTERVAL); loop { interval.tick().await; let cnt = COUNTER.load(std::sync::atomic::Ordering::Relaxed); diff --git a/src/protocols/binary_protocol.rs b/src/protocols/binary_protocol.rs index e505112..5bb0a13 100644 --- a/src/protocols/binary_protocol.rs +++ b/src/protocols/binary_protocol.rs @@ -7,6 +7,7 @@ use crate::{Canvas, Color, Command, Response}; use super::{IOProtocol, Parser, Responder}; const SIZE_BIN: u8 = 115; +const PROTOCOLS_BIN: u8 = 116; const HELP_BIN: u8 = 104; const GET_PX_BIN: u8 = 32; const SET_PX_RGB_BIN: u8 = 128; @@ -22,6 +23,7 @@ impl Parser for Binar match fst { Ok(command) => match command { HELP_BIN => Ok(Command::Help), + PROTOCOLS_BIN => Ok(Command::Protocols), SIZE_BIN => { let canvas = reader.read_u8().await?; Ok(Command::Size(canvas)) @@ -106,6 +108,23 @@ To set a pixel using RGB, use ({SET_PX_RGB_BIN:02X}) (u8 canvas) (x as u16_le) ( ); writer.write_all(help_text.as_bytes()).await } + Response::Protocols(protos) => { + for protocol in protos { + match protocol { + crate::ProtocolStatus::Enabled(proto) => { + writer + .write_all(format!("Enabled: {}\n", proto).as_bytes()) + .await?; + } + crate::ProtocolStatus::Disabled(proto) => { + writer + .write_all(format!("Disabled: {}\n", proto).as_bytes()) + .await?; + } + } + } + Ok(()) + } Response::Size(x, y) => { writer.write_u16(x).await?; writer.write_u16(y).await diff --git a/src/protocols/text_protocol.rs b/src/protocols/text_protocol.rs index 1c711a2..a63ff93 100644 --- a/src/protocols/text_protocol.rs +++ b/src/protocols/text_protocol.rs @@ -117,6 +117,8 @@ impl Parser for TextP if reader.read_line(&mut line).await.is_ok() { if line.starts_with("HELP") { return Ok(Command::Help); + } else if line.starts_with("PROTOCOLS") { + return Ok(Command::Protocols); } else if line.starts_with("SIZE") { return Ok(Command::Size(self.canvas)); } else if line.starts_with("PX ") { @@ -146,6 +148,23 @@ impl Responder for TextParser { async fn unparse(&self, response: Response, writer: &mut W) -> io::Result<()> { match response { Response::Help => writer.write_all(HELP_TEXT).await, + Response::Protocols(protos) => { + for protocol in protos { + match protocol { + crate::ProtocolStatus::Enabled(proto) => { + writer + .write_all(format!("Enabled: {}\n", proto).as_bytes()) + .await?; + } + crate::ProtocolStatus::Disabled(proto) => { + writer + .write_all(format!("Disabled: {}\n", proto).as_bytes()) + .await?; + } + } + } + Ok(()) + } Response::Size(x, y) => writer.write_all(format!("SIZE {x} {y}\n").as_bytes()).await, Response::GetPixel(x, y, color) => { writer @@ -209,6 +228,20 @@ mod tests { ); } + #[tokio::test] + async fn test_px_set_w_parse_caps() { + let parser = TextParser::default(); + let reader = tokio_test::io::Builder::new() + .read(b"PX 28283 29991 AB\n") + .build(); + let mut bufreader = BufReader::new(reader); + let thingy = parser.parse(&mut bufreader).await; + assert_eq!( + thingy.unwrap(), + Command::SetPixel(0, 28283, 29991, Color::W8(0xAB)) + ); + } + #[tokio::test] async fn test_px_set_rgb_parse() { let parser = TextParser::default(); @@ -223,6 +256,20 @@ mod tests { ); } + #[tokio::test] + async fn test_px_set_rgb_parse_caps() { + let parser = TextParser::default(); + let reader = tokio_test::io::Builder::new() + .read(b"PX 28283 29991 8800FA\n") + .build(); + let mut bufreader = BufReader::new(reader); + let thingy = parser.parse(&mut bufreader).await; + assert_eq!( + thingy.unwrap(), + Command::SetPixel(0, 28283, 29991, Color::RGB24(0x88, 0x00, 0xfa)) + ); + } + #[tokio::test] async fn test_px_set_rgba_parse() { let parser = TextParser::default(); @@ -237,6 +284,20 @@ mod tests { ); } + #[tokio::test] + async fn test_px_set_rgba_parse_caps() { + let parser = TextParser::default(); + let reader = tokio_test::io::Builder::new() + .read(b"PX 28283 29991 AB0c3F88\n") + .build(); + let mut bufreader = BufReader::new(reader); + let thingy = parser.parse(&mut bufreader).await; + assert_eq!( + thingy.unwrap(), + Command::SetPixel(0, 28283, 29991, Color::RGBA32(0xab, 0x0c, 0x3f, 0x88)) + ); + } + #[tokio::test] async fn test_px_get_parse() { let parser = TextParser::default(); diff --git a/src/stream.rs b/src/stream.rs index bb3f8d3..45b6686 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -28,6 +28,42 @@ impl Multipart { } } +impl Multipart { + fn write_multipart_frame( + obj: T, + boundary: Vec, + headers: HeaderMap, + first: bool, + ) -> Result, axum::Error> + where + T: IntoIterator, + { + let mut frame_vec = Vec::new(); + if first { + frame_vec.extend_from_slice(b"--"); + } else { + frame_vec.extend_from_slice(b"\r\n--"); + } + frame_vec.extend(boundary); + frame_vec.extend_from_slice(b"\r\n"); + for (header_name, header_value) in headers { + match header_name { + Some(header) => { + frame_vec.extend_from_slice(header.as_str().as_bytes()); + frame_vec.extend_from_slice(b": "); + frame_vec.extend_from_slice(header_value.as_bytes()); + frame_vec.extend_from_slice(b"\r\n"); + } + None => todo!(), + } + } + frame_vec.extend_from_slice(b"\r\n"); + frame_vec.extend(obj); + + Ok(frame_vec) + } +} + impl StreamingFormat for Multipart where T: Send + Sync + IntoIterator + 'static, @@ -37,40 +73,6 @@ where stream: futures::stream::BoxStream<'b, Result>, _options: &'a axum_streams::StreamBodyAsOptions, ) -> futures::stream::BoxStream<'b, Result> { - fn write_multipart_frame( - obj: T, - boundary: Vec, - headers: HeaderMap, - first: bool, - ) -> Result, axum::Error> - where - T: IntoIterator, - { - let mut frame_vec = Vec::new(); - if first { - frame_vec.extend_from_slice(b"--"); - } else { - frame_vec.extend_from_slice(b"\r\n--"); - } - frame_vec.extend(boundary); - frame_vec.extend_from_slice(b"\r\n"); - for (header_name, header_value) in headers { - match header_name { - Some(header) => { - frame_vec.extend_from_slice(header.as_str().as_bytes()); - frame_vec.extend_from_slice(b": "); - frame_vec.extend_from_slice(header_value.as_bytes()); - frame_vec.extend_from_slice(b"\r\n"); - } - None => todo!(), - } - } - frame_vec.extend_from_slice(b"\r\n"); - frame_vec.extend(obj); - - Ok(frame_vec) - } - let boundary = self.boundary.clone(); let headers = self.headers.clone(); let first = self.first; @@ -79,8 +81,12 @@ where stream.map(move |obj_res| match obj_res { Err(e) => Err(e), Ok(obj) => { - let picture_framed = - write_multipart_frame(obj, boundary.clone(), headers.clone(), first); + let picture_framed = Multipart::write_multipart_frame( + obj, + boundary.clone(), + headers.clone(), + first, + ); picture_framed.map(axum::body::Bytes::from) }