From 0a3edeb4a8aca12dda64b18bbab21780f4f7529d Mon Sep 17 00:00:00 2001 From: Myrddin Date: Fri, 9 May 2025 17:41:54 -0400 Subject: [PATCH] All done! --- Cargo.toml | 2 +- LICENSE.md | 69 ----------- README.md | 89 +++++++++++++- docs/problem_statement.md | 64 ----------- src/commands.rs | 18 --- src/main.rs | 236 ++++++++++++++++++++++---------------- src/satellite.rs | 3 - 7 files changed, 226 insertions(+), 255 deletions(-) delete mode 100644 LICENSE.md delete mode 100644 docs/problem_statement.md delete mode 100644 src/commands.rs delete mode 100644 src/satellite.rs diff --git a/Cargo.toml b/Cargo.toml index a7f3b38..7e9f4ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "0.0.0" edition = "2021" description = "Rust Challenge" repository = "/myrddin/satellite" -authors = ["CyberMages LLC ", "Jason Travis Smith "] +authors = ["Jason Travis Smith "] readme = "README.md" license-file = "LICENSE.md" diff --git a/LICENSE.md b/LICENSE.md deleted file mode 100644 index 0a2e8d7..0000000 --- a/LICENSE.md +++ /dev/null @@ -1,69 +0,0 @@ -Copyright (c) <2025> CyberMages LLC - -Redistribution and use in source and binary forms, with or -without modification, are permitted provided that the -following conditions are met: - -1. Redistributions of source code must retain the above - copyright notice, this list of conditions and the - following disclaimer. - -2. Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the - following disclaimer in the documentation and/or other - materials provided with the distribution. - -Subject to the terms and conditions of this license, each -copyright holder and contributor hereby grants to those -receiving rights under this license a perpetual, -worldwide, non-exclusive, no-charge, royalty-free, -irrevocable (except for failure to satisfy the conditions -of this license) patent license to make, have made, use, -offer to sell, sell, import, and otherwise transfer this -software, where such license applies only to those patent -claims, already acquired or hereafter acquired, licensable -by such copyright holder or contributor that are -necessarily infringed by: - - (a) their Contribution(s) (the licensed copyrights of - copyright holders and non-copyrightable additions - of contributors, in source or binary form) alone; - - -- or -- - - (b) combination of their Contribution(s) with the work - of authorship to which such Contribution(s) was - added by such copyright holder or contributor, if, - at the time the Contribution is added, such - addition causes such combination to be necessarily - infringed. The patent license shall not apply to - any other combinations which include the - Contribution. - -Except as expressly stated above, no rights or licenses -from any copyright holder or contributor is granted under -this license, whether expressly, by implication, estoppel -or otherwise. - - -DISCLAIMER - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND -CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED -WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, -INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN -IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -YOU ACKNOWLEDGE THAT THIS SOFTWARE IS NOT DESIGNED, -LICENSED OR INTENDED FOR USE IN THE DESIGN, CONSTRUCTION, -OPERATION OR MAINTENANCE OF ANY MILITARY FACILITY OR -RELIGIOUS INSTITUTION. diff --git a/README.md b/README.md index 2d00916..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/docs/problem_statement.md b/docs/problem_statement.md deleted file mode 100644 index ce70786..0000000 --- a/docs/problem_statement.md +++ /dev/null @@ -1,64 +0,0 @@ -# Problem Statement -You are a member of the flight software team at Umbra and are responsible -for writing code that manages the satellite’s propulsion system. - -Firing the propulsion system involves waiting for a certain period of time before ignition. - -The following is an example usage of this system: - -* At absolute time t = 0, send a command to the computer to fire the propulsion in 15 seconds -* At absolute time t = 2, send a command to the computer to fire the propulsion in 30 seconds -* At absolute time t = 32, the computer begins firing the propulsion - -The flight computer should accept a command with a relative time of when to fire -the propulsion - once that time has elapsed, the program should print out “firing now!”. -If another command is received before the propulsion is fired then the most recently -received relative time should overwrite any existing commands. - -More formally, if a command _A_ is waiting to fire and another command _B_ is received -before A has fired, then B should replace _A_ as the pending command and _A_ should never fire. - -If a time of -1 is given, any outstanding commands to fire the propulsion are cancelled. - -Note that the flight computer should be able to fire the thruster multiple times in a -single execution of the program. - -You may use exactly one of the following interfaces for getting data into and out of your program: -* Standard input/standard output -* TCP - -You can use whichever is more convenient - we note that some languages make asynchronous -IO with standard input/standard output cumbersome and have thus included TCP. - -If you do choose to use TCP, please have your server listen on port `8124`. - -A sample TCP client, `propulsion_tcp_client.py`, is provided that plumbs standard input -to TCP writes and likewise plumbs TCP reads to standard output. - -Commands should be delineated by newlines. - -A sample _complete_ execution of your program using standard input and output is shown below - -``` -./your_program -15 -30 -firing now! -``` - -# Submitting your code -You may complete this assignment in any programming language that you wish. -We're most familiar with Rust and Python at Umbra, but we always enjoy learning -new languages and seeing solutions with different tools! - -Your final submission for this exercise should be a zip file including: -* The source code of your program. -* A brief README with a description of your approach and instructions on how to - build and run the program. -* Any libraries or frameworks needed to run your program, or instructions for installing them. - -Please ensure that your code is clear and legible before submitting. - -While we don't expect you to write production-quality code for this exercise, -readability goes a long way in helping us review your submission. - diff --git a/src/commands.rs b/src/commands.rs deleted file mode 100644 index 5067e5a..0000000 --- a/src/commands.rs +++ /dev/null @@ -1,18 +0,0 @@ -use tokio::time::Duration; - - - -/// The commands available to send to the Satellite. -#[derive(Clone, Copy)] -pub enum Command -{ - /// 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 -} diff --git a/src/main.rs b/src/main.rs index a70969c..db463a3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,104 +1,106 @@ //! Rust Challenge -mod commands; - - -use std::io::Write; -use std::pin::Pin; use std::future::Future; +use std::pin::Pin; use tokio::io::AsyncWriteExt; use tokio::sync::{mpsc, watch}; use tokio::time::{Duration, Interval}; -use crate::commands::Command; - - - +/// Just an easier to read type. type IoResult = Result; +/// The commands available to send to the Satellite. +#[derive(Clone, Copy)] +pub enum Command { + /// 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 state of the program. +#[derive(Clone, Copy)] +pub enum ProgramState { + /// 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<()> -{ +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 - { + while running { + // Clearing the last read line and read a new one. buffer.clear(); std::io::stdin().read_line(&mut buffer)?; - std::io::stdout().flush().unwrap(); let input = buffer.trim(); - match input - { + 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 application."); + "exit" | "stop" | "quit" => { + println!("Exiting program."); running = false; - term_sender.send_replace(running); + term_sender.send_replace(ProgramState::Terminated); } - _ => - { + _ => { // Check to see if we were given a number. - match input.parse::() - { - Ok(seconds) => - { + 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(_) => - { - println!("Cancelling any outstanding commands.") - } - Err(e) => println!("Failed to send command: {}", e) + 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, {}]", - u32::MAX); - println!("A value of -1 will cancel any current \ - propulsion commands."); - } - else - { - let larger_seconds: u64 = seconds as u64; + } 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(larger_seconds); - match command_sender.try_send(Command::Propulsion { delay: delay_duration }) - { - Ok(_) => - { - println!("Firing the engines in {} seconds.", - seconds) - } - Err(e) => println!("Failed to send command: {}", e) + 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) => - { + Err(e) => { println!("Unable to parse the given input into seconds."); - println!("Please specify only seconds until the time to \ + println!("{:?}\n", e); + println!("Please specify only the desired whole seconds until the time to \ fire the engines."); } } @@ -109,69 +111,80 @@ fn read_user_input(command_sender: mpsc::Sender, Ok(()) } +/// Determines if there is an interval that needs to be run or if +/// there is nothing to really do. /// -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()) +/// 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()), } } -async fn process_commands(mut command_receiver: mpsc::Receiver, - mut term_receiver: watch::Receiver) - -> IoResult<()> -{ - let mut propulsion_interval: Option = None; +/// 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 - { + + while running { tokio::select! { + // Process commands. Some(command) = command_receiver.recv() => { match command { Command::Cancel => { - stdout.write_all(b"Received: Cancel\n").await?; - stdout.flush().await?; + // Dump any interval that is running. propulsion_interval = None; } Command::Propulsion { delay } => { - let mut buffer: Vec = Vec::new(); - writeln!(&mut buffer, "Received: {:?} delay", delay); - stdout.write_all(&buffer).await?; - stdout.flush().await?; - + // Overwrite any interval with this great new interval. propulsion_interval = Some(tokio::time::interval(delay)); - // Skip the first immediate tick + // Skip the first immediate tick. if let Some(interval) = propulsion_interval.as_mut() { - interval.tick().await; // skip first tick + 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() => { - stdout.write_all(b"Communication task received shutdown message.").await?; - stdout.flush().await?; - running = *term_receiver.borrow_and_update(); + running = match *term_receiver.borrow_and_update() + { + ProgramState::Running => { true } + ProgramState::Terminated => { false } + }; } } } @@ -181,20 +194,18 @@ async fn process_commands(mut command_receiver: mpsc::Receiver, /// Program entry point. #[tokio::main] -async fn main() -> IoResult<()> -{ +async fn main() -> IoResult<()> { // The channel that will be used to send satellite commands between tasks. - let (command_sender, mut command_receiver) = mpsc::channel::(100); + let (command_sender, command_receiver) = mpsc::channel::(100); // The channel that will be used to signal termination of the program. - // True means the program is running. False means it has been terminated. // // 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, mut term_receiver) = watch::channel(true); + 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 @@ -204,13 +215,42 @@ async fn main() -> IoResult<()> }); // Spawn a task to handle simulating the satellite. - let sim_task = tokio::spawn(async move { + let command_task = tokio::spawn(async move { process_commands(command_receiver, term_receiver).await }); - let (sim_result, input_result) = tokio::join!(sim_task, input_task); - sim_result?; - input_result??; + // 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(()) } diff --git a/src/satellite.rs b/src/satellite.rs deleted file mode 100644 index dcc5021..0000000 --- a/src/satellite.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub struct Satellite -{ -}