diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..f18fe23 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,261 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "miniz_oxide" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "satellite" +version = "1.0.0" +dependencies = [ + "tokio", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +dependencies = [ + "libc", +] + +[[package]] +name = "syn" +version = "2.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tokio" +version = "1.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml index 9905e8d..3ec12be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,14 @@ [package] name = "satellite" -version = "0.0.0" +version = "1.0.0" edition = "2021" -description = "Rust challenge" +description = "Rust Challenge" repository = "/myrddin/satellite" -authors = ["CyberMages LLC ", "Jason Travis Smith "] +authors = ["Jason Travis Smith "] readme = "README.md" license-file = "LICENSE.md" [dependencies] +tokio = { version = "1.44.1", features = ["macros", "rt-multi-thread", "io-std", "io-util", "time", "signal", "sync"] } diff --git a/README.md b/README.md index e435a76..6300c15 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,88 @@ -# satellite +# Satellite Propulsion System -Rust challenge +## Approach +This program will be using standard I/O for getting data. + +Tackling this problem requires the use of asynchronous code. This is because +the user will constantly be giving input on the command line yet time needs +to keep moving forward to fire any propulsion commands. + +To handle this the program uses two separate tasks. One for handling +the input and one for running the commands given. The input, coming from +standard I/O, will run on a separate thread because it will use blocking +functions. The command processing can be handled in an asynchronous +task since it can use completely asynchronous functions. + +The two tasks are linked with message passing channels. One channel is for +passing commands from the input task to the command processing task. The +other channel is a watch channel on a boolean value that the input task +will flip from true to false. This will designate that the program should +no longer be running and needs to terminate. + +The input reading task is the main driver of the program. It will loop until +the program is told to stop running. This is done by the user typing "exit", +"stop", or "quit". It will then trigger termination through the watch +channel. If the user types a whole number between 0 and i32::MAX, it +will create and send a propulsion command with a delay time of the given +number, as seconds, and send it over the command channel. If the user types +a '-1', it will create a cancel command and send it over the command +channel. Cancel is handled as a separate command because it is a different +action and it is clearer to handle it separately. + +The command processing task will loop until the termination channel tells +it to end. It will handle two async jobs. The first is processing +commands from the command channel, either creating a propulsion interval +countdown or clearing any that are already running. The second job is +to run the current propulsion firing interval until it completes one firing. + + + +## Instructions +### Build +To build the program you must have rustc and cargo installed. +The instructions to do so can be found +[here](https://www.rust-lang.org/tools/install). + +Then to build the program navigate to the directory and run the +following command. +``` +cargo build --release +``` + +If you would like to include debugging symbols then you can run the +following command. +``` +cargo build +``` + +This will pull all the required dependencies from crates.io. +Below is the list of dependencies; this can be verified in Cargo.toml. +* tokio + + +### Run +To run the program type the following command. +``` +cargo run --release +``` + +If you want to run a version with all the debugging symbols then run the +following command. +``` +cargo run +``` + +You can also run the currently built version from the 'target' subdirectory. +Debug builds can be found in target/debug. +Release builds can be found in target/release. +The executable's name is satellite. + +### Operation +Once the program is running it will expect input commands from the user. +The commands can either be: +* A whole number - To specify the amount of delay, in seconds, before the + next time the satellite will fire its engine. +* The number '-1' - To specify that all current propulsion commands should + be canceled. +* The any one of the case sensitive words + "quit", "stop", or "exit" - To specify that the program should terminate. diff --git a/src/main.rs b/src/main.rs index c06f8ea..3e67a90 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,20 +1,283 @@ -//! Rust challenge +//! Rust Challenge +use std::future::Future; +use std::pin::Pin; +use tokio::io::AsyncWriteExt; +use tokio::sync::{mpsc, watch}; +use tokio::time::{Duration, Interval}; -mod project; +/// Just an easier to read type. +type IoResult = Result; - - -/// Print the version of the project. -fn print_version() +/// The commands available to send to the Satellite. +#[derive(Clone, Copy)] +pub enum Command { - println!("{} v{}", project::get_name(), project::get_version()); + /// Fire the engine in T minus x seconds! + Propulsion + { + /// The amount of seconds to wait before we fire the engines. + delay: Duration + }, + + /// Cancel any waiting commands. + Cancel } - -/// The usual starting point of your project. -fn main() +/// The state of the program. +#[derive(Clone, Copy)] +pub enum ProgramState { - print_version(); + /// The program is currently running. + Running, + + /// The program has switched to shutting down. + Terminated +} + +/// Reads input from the user until the termination signal is +/// received. It will send command messages every time it reads valid input. +fn read_user_input(command_sender: mpsc::Sender, + term_sender: watch::Sender) + -> IoResult<()> +{ + let mut running: bool = true; + let mut buffer = String::new(); + + // Loop and read the user input. + while running + { + // Clearing the last read line and read a new one. + buffer.clear(); + std::io::stdin().read_line(&mut buffer)?; + + let input = buffer.trim(); + + match input + { + // A termination method wasn't specified in the document, so + // this will terminate when either stop, exit, or quit is entered. + "exit" | "stop" | "quit" => + { + println!("Exiting program."); + running = false; + term_sender.send_replace(ProgramState::Terminated); + } + + _ => + { + // Check to see if we were given a number. + match input.parse::() + { + Ok(seconds) => + { + // Here we are just handling the different possible numbers + // passed in. -1 cancels commands, less than -1 is ignored + // with a message, and greated than -1 is turned into a + // propulsion command. + if seconds == -1 + { + match command_sender.blocking_send(Command::Cancel) + { + Ok(_) => + {} + Err(e) => eprintln!("Failed to send command: {}", e) + } + } + else if seconds < -1 + { + println!("All propulsion delay times are given in \ + seconds within the range of [{}, {}]", + 0, + i32::MAX); + println!("A value of -1 will cancel any current \ + propulsion commands."); + } + else + { + // Time to create the Propulsion command. + let delay_secs: u64 = seconds as u64; + let delay_duration: Duration = + Duration::from_secs(delay_secs); + match command_sender.try_send(Command::Propulsion { + delay: delay_duration, + }) { + Ok(_) => {} + Err(e) => eprintln!("Failed to send command: {}", e), + } + } + } + + Err(e) => + { + println!("Unable to parse the given input into seconds."); + println!("{:?}\n", e); + println!("Please specify only the desired whole seconds \ + until the time to fire the engines."); + } + } + } + } + } + + Ok(()) +} + +/// Determines if there is an interval that needs to be run or if +/// there is nothing to really do. +/// +/// This was the trickiest part due to the syntax. +fn maybe_tick<'a>(interval: Option<&'a mut Interval>) + -> Pin + Send + 'a>> +{ + match interval + { + Some(interval) => + { + Box::pin(async move { + interval.tick().await; + () + }) + } + + None => Box::pin(std::future::pending()) + } +} + +/// The command task. +/// +/// Processes the commands sent and fire the engine if a given +/// propulsion command completes. +async fn process_commands(mut command_receiver: mpsc::Receiver, + mut term_receiver: watch::Receiver) + -> IoResult<()> +{ + let mut stdout = tokio::io::stdout(); + let mut propulsion_interval: Option = None; + let mut running: bool = true; + + while running + { + tokio::select! { + // Process commands. + Some(command) = command_receiver.recv() => + { + match command + { + Command::Cancel => + { + // Dump any interval that is running. + propulsion_interval = None; + } + + Command::Propulsion { delay } => + { + // Overwrite any interval with this great new interval. + propulsion_interval = Some(tokio::time::interval(delay)); + + // Skip the first immediate tick. + if let Some(interval) = propulsion_interval.as_mut() + { + interval.tick().await; + } + } + } + } + + // Run the interval timer if there is one. + _ = maybe_tick(propulsion_interval.as_mut()) => + { + stdout.write_all(b"firing now!\n").await?; + stdout.flush().await?; + + // Clear the command out since it is done. + propulsion_interval = None; + } + + // Listen for termination. + _ = term_receiver.changed() => + { + running = match *term_receiver.borrow_and_update() + { + ProgramState::Running => { true } + ProgramState::Terminated => { false } + }; + } + } + } + + Ok(()) +} + +/// Program entry point. +#[tokio::main] +async fn main() -> IoResult<()> +{ + // The channel that will be used to send satellite commands between tasks. + let (command_sender, command_receiver) = mpsc::channel::(100); + + // The channel that will be used to signal termination of the program. + // + // This could be done as a satellite command since the program is driven by + // user interaction and it all happens from the input_task, but this is a + // program signal so I prefer to keep it seperate from the command messages. + // It also allows for moving termination control to another task, say from + // Ctrl+C, if the program was changed to use TCP or something else. + let (term_sender, term_receiver) = watch::channel(ProgramState::Running); + + // Spawn a task to handle reading input from the user. We use a thread here + // because tokio recommends it for the blocking read calls for interactive + // user input. + let input_task = tokio::task::spawn_blocking(move || { + read_user_input(command_sender, term_sender) + }); + + // Spawn a task to handle simulating the satellite. + let command_task = tokio::spawn(async move { + process_commands(command_receiver, term_receiver).await + }); + + // Run both tasks at the same time. + let (command_result, input_result) = tokio::join!(command_task, input_task); + + // Handle errors from the tasks. + match command_result + { + Ok(Ok(())) => + {} + + Ok(Err(e)) => + { + eprintln!("Command task failed: {}", e); + return Err(e); + } + + Err(e) => + { + eprintln!("Command task panicked or was aborted: {}", e); + return Err(std::io::Error::new(std::io::ErrorKind::Other, e)); + } + } + + match input_result + { + Ok(Ok(())) => + {} + + Ok(Err(e)) => + { + eprintln!("Input task failed: {}", e); + return Err(e); + } + + Err(e) => + { + eprintln!("Input task panicked or was aborted: {}", e); + return Err(std::io::Error::new(std::io::ErrorKind::Other, e)); + } + } + + println!("Shutdown complete."); + Ok(()) }