Challenge finished.

This commit is contained in:
Myrddin Dundragon 2025-05-12 20:11:46 -04:00
parent b4735b5807
commit a9891242d1
4 changed files with 626 additions and 16 deletions

261
Cargo.lock generated Normal file
View File

@ -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"

View File

@ -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 <Software@CyberMagesLLC.com>", "Jason Travis Smith <Myrddin@CyberMages.tech>"]
authors = ["Jason Travis Smith <Myrddin@CyberMages.tech>"]
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"] }

View File

@ -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.

View File

@ -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<T> = Result<T, std::io::Error>;
/// 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<Command>,
term_sender: watch::Sender<ProgramState>)
-> 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::<i32>()
{
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<Box<dyn Future<Output = ()> + 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<Command>,
mut term_receiver: watch::Receiver<ProgramState>)
-> IoResult<()>
{
let mut stdout = tokio::io::stdout();
let mut propulsion_interval: Option<Interval> = 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::<Command>(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(())
}