From 853e735fcd9233c54bb3021edf2f80067d75afa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20B=C3=B6hme?= Date: Fri, 28 Jul 2023 19:52:23 +0200 Subject: [PATCH] feat: split into modules --- Cargo.lock | 33 +++++- Cargo.toml | 1 + src/cli.rs | 44 ++++++++ src/daemon.rs | 139 +++++++++++++++++++++++ src/main.rs | 298 +++----------------------------------------------- src/timer.rs | 69 ++++++++++++ 6 files changed, 298 insertions(+), 286 deletions(-) create mode 100644 src/cli.rs create mode 100644 src/daemon.rs create mode 100644 src/timer.rs diff --git a/Cargo.lock b/Cargo.lock index 6a47f51..a745331 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -199,18 +199,18 @@ checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "proc-macro2" -version = "1.0.60" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec2b086b7a862cf4de201096214fa870344cf922b2b30c167badb3af3195406" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.28" +version = "1.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" +checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" dependencies = [ "proc-macro2", ] @@ -267,15 +267,35 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "2.0.18" +version = "2.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" +checksum = "b60f673f44a8255b9c8c657daf66a596d435f2da81a555b06dc644d080ba45e0" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "thiserror" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "timers" version = "0.1.0" @@ -284,6 +304,7 @@ dependencies = [ "clap", "serde", "serde_cbor", + "thiserror", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index cd9c0c4..923249b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,3 +10,4 @@ anyhow = "1.0.71" clap = { version = "4.3.4", features = ["derive"] } serde = { version = "1.0.164", features = ["derive"] } serde_cbor = "0.11.2" +thiserror = "1.0.44" diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..216cf7a --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,44 @@ +use crate::daemon::{Answer, Command as OtherCommand, AnswerErr}; +use anyhow::{Context, Result}; +use clap::{Parser, Subcommand}; +use std::net::Shutdown; +use std::os::unix::net::UnixStream; + +#[derive(Debug, Parser)] +#[command(name = "timers")] +#[command(about = "A advanced timer daemon/cli.", long_about = None)] +#[command(arg_required_else_help = true)] +pub struct Cli { + #[command(subcommand)] + pub command: Command, + #[arg(short, long)] + #[clap(default_value = "/tmp/timers.socket")] + pub socket: String, +} + +#[derive(Debug, Subcommand)] +pub enum Command { + Daemon, + Add { name: String, duration_seconds: u64 }, + List, + Remove { name: String }, +} + +fn get_stream(socket_path: &String) -> Result { + UnixStream::connect(socket_path) + .context(format!("Could not connect to socket {}!", socket_path)) +} + +pub fn send_command(socket_path: &String, command: OtherCommand) -> Result<()> { + let stream = get_stream(socket_path)?; + serde_cbor::to_writer(&stream, &command).context("Could not write command!")?; + stream + .shutdown(Shutdown::Write) + .context("Could not shutdown write!")?; + let answer: Result = serde_cbor::from_reader(&stream).context("Could not read answer!")?; + match answer { + Ok(answer) => println!("{}", answer), + Err(err) => println!("Error: {}", err), + } + Ok(()) +} diff --git a/src/daemon.rs b/src/daemon.rs new file mode 100644 index 0000000..5bba1a2 --- /dev/null +++ b/src/daemon.rs @@ -0,0 +1,139 @@ +pub use crate::timer::Timer; +use anyhow::Context; +use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; +use std::{ + io::Write, + os::unix::net::{UnixListener, UnixStream}, + thread::sleep, + time::Duration, +}; + +#[derive(Debug, Serialize, Deserialize)] +pub enum Command { + Add(String, Duration), + Remove(String), + List, +} + +#[derive(Debug, Serialize, Deserialize)] +pub enum Answer { + Ok, + Timers(Vec), +} + +impl Display for Answer { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + match self { + Answer::Ok => write!(f, "Ok"), + Answer::Timers(timers) => { + if timers.is_empty() { + write!(f, "No timers running.") + } else { + let strings: Vec = + timers.iter().map(|timer| timer.to_string()).collect(); + write!(f, "{}", strings.join("\n")) + } + } + } + } +} + +#[derive(Debug, thiserror::Error, Serialize, Deserialize)] +pub enum AnswerErr { + #[error("Timer with name '{}' already exists", .0)] + TimerAlreadyExist(String), + #[error("No timer with the name '{}' exists", .0)] + NoSuchTimer(String), +} + + +pub struct Daemon { + listener: UnixListener, + timers: Vec, +} + +impl Daemon { + pub fn new(socket_path: String) -> anyhow::Result { + let path = std::path::Path::new(&socket_path); + if path.exists() { + std::fs::remove_file(path) + .with_context(|| format!("Could not remove previous socket {}!", socket_path))?; + } + let listener = UnixListener::bind(&socket_path) + .context(format!("Cannot bind to socket {}!", socket_path))?; + Ok(Self { + listener, + timers: Vec::new(), + }) + } + + fn has_timer(&mut self, name: &String) -> bool { + self.timers.iter().any(|other| &other.name == name) + } + + fn handle_command(&mut self, command: Command) -> Result { + println!("Received command {:?}", command); + match command { + Command::List => Ok(Answer::Timers(self.timers.to_vec())), + Command::Add(name, duration) => { + if self.has_timer(&name) { + return Err(AnswerErr::TimerAlreadyExist(name)); + } + let timer = Timer::new(name, duration); + self.timers.push(timer); + Ok(Answer::Ok) + } + Command::Remove(name) => { + if !self.has_timer(&name) { + return Err(AnswerErr::NoSuchTimer(name)); + } + self.timers = self + .timers + .iter() + .cloned() + .filter(|other| other.name != name) + .collect(); + Ok(Answer::Ok) + } + } + } + + fn handle_stream(&mut self, mut stream: &UnixStream) -> anyhow::Result<()> { + let command = serde_cbor::from_reader(stream).context("Could not read command!")?; + let answer = self.handle_command(command); + serde_cbor::to_writer(stream, &answer).context("Could not write answer!")?; + stream.flush().context("Could not flush stream!")?; + Ok(()) + } + + fn check_timers(&mut self) { + self.timers = self + .timers + .iter() + .cloned() + .filter(|timer| { + let expired = timer.is_expired(); + if expired { + println!("Timer {} is expired!", timer.name); + } + !expired + }) + .collect(); + } + + pub fn run(&mut self) -> anyhow::Result<()> { + self.listener + .set_nonblocking(true) + .context("Could not set listener to non blocking!")?; + loop { + while let Ok((stream, _)) = self.listener.accept() { + if let Err(e) = self.handle_stream(&stream) { + println!("Error while handling stream: {}", e) + } + } + self.check_timers(); + sleep(Duration::from_millis(100)); + } + } +} diff --git a/src/main.rs b/src/main.rs index ed98523..aa44d10 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,286 +1,24 @@ -use crate::cli::{convert_command, send_command, Cli, Commands}; -use crate::daemon::Daemon; +pub mod cli; +pub mod daemon; +pub mod timer; + +use std::time::Duration; + +use crate::cli::{send_command, Cli, Command as CliCommand}; +use crate::daemon::{Command as DaemonCommand, Daemon}; use anyhow::Result; use clap::Parser; -mod timer { - use serde::{Deserialize, Serialize}; - use std::{ - fmt::{Display, Formatter}, - time::{Duration, Instant}, - }; - mod approx_instant { - use std::time::{Duration, Instant}; - - use serde::de::Error; - use serde::{Deserialize, Deserializer, Serialize, Serializer}; - - pub fn serialize(instant: &Instant, serializer: S) -> Result - where - S: Serializer, - { - let duration = instant.elapsed(); - duration.serialize(serializer) - } - - pub fn deserialize<'de, D>(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let duration = Duration::deserialize(deserializer)?; - let now = Instant::now(); - let instant = now - .checked_sub(duration) - .ok_or_else(|| Error::custom("Error deserializing instant!"))?; - Ok(instant) - } - } - - #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] - pub struct Timer { - pub name: String, - #[serde(with = "approx_instant")] - start: Instant, - duration: Duration, - } - - impl Display for Timer { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { - write!( - f, - "{} has {}s remaining.", - self.name, - self.remaining().as_secs() - ) - } - } - - impl Timer { - pub fn new(name: String, duration: Duration) -> Timer { - Timer { - name, - start: Instant::now(), - duration, - } - } - - pub fn is_expired(&self) -> bool { - return Instant::now() - self.start > self.duration; - } - - pub fn remaining(&self) -> Duration { - self.duration - (Instant::now() - self.start) - } - } -} - -mod daemon { - use crate::timer::Timer; - use anyhow::{Context, Ok, Result}; - use serde::{Deserialize, Serialize}; - use std::fmt::{Display, Formatter}; - use std::result::Result::Ok as ResultOk; - use std::{ - io::Write, - os::unix::net::{UnixListener, UnixStream}, - thread::sleep, - time::Duration, - }; - #[derive(Debug, Serialize, Deserialize)] - pub enum Command { - Add(String, Duration), - Remove(String), - List, - } - - #[derive(Debug, Serialize, Deserialize)] - pub enum Answer { - Ok, - Timers(Vec), - Err(String), - } - - impl Display for Answer { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { - match self { - Answer::Ok => write!(f, "Ok"), - Answer::Timers(timers) => { - if timers.is_empty() { - write!(f, "No timers running.") - } else { - let strings: Vec = - timers.iter().map(|timer| timer.to_string()).collect(); - write!(f, "{}", strings.join("\n")) - } - } - Answer::Err(msg) => write!(f, "Error: {}", msg), - } - } - } - - pub struct Daemon { - listener: UnixListener, - timers: Vec, - } - - impl Daemon { - pub fn new(socket_path: String) -> Result { - let path = std::path::Path::new(&socket_path); - if path.exists() { - std::fs::remove_file(path).with_context(|| { - format!("Could not remove previous socket {}!", socket_path) - })?; - } - let listener = UnixListener::bind(&socket_path) - .with_context(|| format!("Cannot bind to socket {}!", socket_path))?; - Ok(Self { - listener, - timers: Vec::new(), - }) - } - - fn has_timer(&mut self, name: &String) -> bool { - self.timers - .iter() - .find(|other| &other.name == name) - .is_some() - } - - fn handle_command(&mut self, command: Command) -> Answer { - println!("Received command {:?}", command); - match command { - Command::List => Answer::Timers(self.timers.to_vec()), - Command::Add(name, duration) => { - if self.has_timer(&name) { - return Answer::Err(format!("Timer with name {} already exists!", name)); - } - let timer = Timer::new(name, duration); - self.timers.push(timer); - Answer::Ok - } - Command::Remove(name) => { - if !self.has_timer(&name) { - return Answer::Err(format!("Timer with name {} does not exist!", name)); - } - self.timers = self - .timers - .to_vec() - .into_iter() - .filter(|other| other.name != name) - .collect(); - Answer::Ok - } - } - } - - fn handle_stream(&mut self, mut stream: &UnixStream) -> Result<()> { - let command = serde_cbor::from_reader(stream).context("Could not read command!")?; - let answer = self.handle_command(command); - serde_cbor::to_writer(stream, &answer).context("Could not write answer!")?; - stream.flush().context("Could not flush stream!")?; - Ok(()) - } - - fn check_timers(&mut self) { - self.timers = self - .timers - .to_vec() - .into_iter() - .filter(|timer| { - let expired = timer.is_expired(); - if expired { - println!("Timer {} is expired!", timer.name); - } - !expired - }) - .collect(); - } - - pub fn run(&mut self) -> Result<()> { - self.listener - .set_nonblocking(true) - .context("Could not set listener to non blocking!")?; - loop { - while let ResultOk((stream, _)) = self.listener.accept() { - if let Err(e) = self.handle_stream(&stream) { - println!("Error while handling stream: {}", e) - } - } - self.check_timers(); - sleep(Duration::from_millis(100)); - } - } - } -} - -mod cli { - use std::net::Shutdown; - use std::os::unix::net::UnixStream; - - use std::time::Duration; - - use clap::{Parser, Subcommand}; - - use anyhow::{Context, Result}; - - use crate::daemon::{Answer, Command}; - - #[derive(Debug, Parser)] - #[command(name = "timers")] - #[command(about = "A advanced timer daemon/cli.", long_about = None)] - pub struct Cli { - #[command(subcommand)] - pub command: Commands, - #[arg(short, long)] - #[clap(default_value = "/tmp/timers.socket")] - pub socket: String, - } - - #[derive(Debug, Subcommand)] - pub enum Commands { - Daemon, - Add { name: String, duration_seconds: u64 }, - List, - Remove { name: String }, - } - - fn get_stream(socket_path: &String) -> Result { - UnixStream::connect(socket_path) - .with_context(|| format!("Could not connect to socket {}!", socket_path)) - } - - pub fn convert_command(command: &Commands) -> Command { - match command { - Commands::Add { - name, - duration_seconds, - } => Command::Add(name.to_string(), Duration::from_secs(*duration_seconds)), - Commands::List => Command::List, - Commands::Remove { name } => Command::Remove(name.to_string()), - _ => panic!("Invalid command!"), - } - } - - pub fn send_command(socket_path: &String, command: Command) -> Result<()> { - let stream = get_stream(socket_path)?; - serde_cbor::to_writer(&stream, &command).with_context(|| "Could not write command!")?; - stream - .shutdown(Shutdown::Write) - .context("Could not shutdown write!")?; - let answer: Answer = - serde_cbor::from_reader(&stream).with_context(|| "Could not read answer!")?; - println!("{}", answer); - Ok(()) - } -} - fn main() -> Result<()> { let args = Cli::parse(); - match args.command { - Commands::Daemon => { - Daemon::new(args.socket)?.run()?; - Ok(()) - } - _ => send_command(&args.socket, convert_command(&args.command)), - } + let daemon_command = match args.command { + CliCommand::Daemon => return Daemon::new(args.socket)?.run(), + CliCommand::Add { + name, + duration_seconds, + } => DaemonCommand::Add(name, Duration::from_secs(duration_seconds)), + CliCommand::List => DaemonCommand::List, + CliCommand::Remove { name } => DaemonCommand::Remove(name), + }; + send_command(&args.socket, daemon_command) } diff --git a/src/timer.rs b/src/timer.rs new file mode 100644 index 0000000..262f41c --- /dev/null +++ b/src/timer.rs @@ -0,0 +1,69 @@ +use serde::{Deserialize, Serialize}; +use std::{ + fmt::{Display, Formatter}, + time::{Duration, Instant}, +}; + +mod approx_instant { + use std::time::{Duration, Instant}; + + use serde::de::Error; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + pub fn serialize(instant: &Instant, serializer: S) -> Result + where + S: Serializer, + { + let duration = instant.elapsed(); + duration.serialize(serializer) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let duration = Duration::deserialize(deserializer)?; + let now = Instant::now(); + let instant = now + .checked_sub(duration) + .ok_or_else(|| Error::custom("Error deserializing instant!"))?; + Ok(instant) + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct Timer { + pub name: String, + #[serde(with = "approx_instant")] + start: Instant, + duration: Duration, +} + +impl Display for Timer { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + write!( + f, + "{} has {}s remaining.", + self.name, + self.remaining().as_secs() + ) + } +} + +impl Timer { + pub fn new(name: String, duration: Duration) -> Timer { + Timer { + name, + start: Instant::now(), + duration, + } + } + + pub fn is_expired(&self) -> bool { + Instant::now() - self.start > self.duration + } + + pub fn remaining(&self) -> Duration { + self.duration - (Instant::now() - self.start) + } +}