All done!

This commit is contained in:
Myrddin Dundragon 2025-05-09 17:41:54 -04:00
parent 43a093afd1
commit 0a3edeb4a8
Signed by: myrddin
GPG Key ID: 1183D7329E01CF15
7 changed files with 226 additions and 255 deletions

View File

@ -4,7 +4,7 @@ version = "0.0.0"
edition = "2021"
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"

View File

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

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,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 satellites 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.

View File

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

View File

@ -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<T> = Result<T, std::io::Error>;
/// 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<Command>,
term_sender: watch::Sender<bool>)
-> IoResult<()>
{
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
{
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::<i32>()
{
Ok(seconds) =>
{
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(_) =>
{
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<Command>,
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<Box<dyn Future<Output = ()> + 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<Box<dyn Future<Output = ()> + 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<Command>,
mut term_receiver: watch::Receiver<bool>)
-> IoResult<()>
{
let mut propulsion_interval: Option<Interval> = 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<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
{
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<u8> = 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<Command>,
/// 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::<Command>(100);
let (command_sender, command_receiver) = mpsc::channel::<Command>(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(())
}

View File

@ -1,3 +0,0 @@
pub struct Satellite
{
}