feat: split into modules

This commit is contained in:
Moritz Böhme 2023-07-28 19:52:23 +02:00
parent a5cac154f4
commit 853e735fcd
Signed by: moritz
GPG key ID: 970C6E89EB0547A9
6 changed files with 298 additions and 286 deletions

33
Cargo.lock generated
View file

@ -199,18 +199,18 @@ checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.60" version = "1.0.66"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dec2b086b7a862cf4de201096214fa870344cf922b2b30c167badb3af3195406" checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.28" version = "1.0.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@ -267,15 +267,35 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.18" version = "2.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" checksum = "b60f673f44a8255b9c8c657daf66a596d435f2da81a555b06dc644d080ba45e0"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"unicode-ident", "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]] [[package]]
name = "timers" name = "timers"
version = "0.1.0" version = "0.1.0"
@ -284,6 +304,7 @@ dependencies = [
"clap", "clap",
"serde", "serde",
"serde_cbor", "serde_cbor",
"thiserror",
] ]
[[package]] [[package]]

View file

@ -10,3 +10,4 @@ anyhow = "1.0.71"
clap = { version = "4.3.4", features = ["derive"] } clap = { version = "4.3.4", features = ["derive"] }
serde = { version = "1.0.164", features = ["derive"] } serde = { version = "1.0.164", features = ["derive"] }
serde_cbor = "0.11.2" serde_cbor = "0.11.2"
thiserror = "1.0.44"

44
src/cli.rs Normal file
View file

@ -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> {
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<Answer, AnswerErr> = serde_cbor::from_reader(&stream).context("Could not read answer!")?;
match answer {
Ok(answer) => println!("{}", answer),
Err(err) => println!("Error: {}", err),
}
Ok(())
}

139
src/daemon.rs Normal file
View file

@ -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<Timer>),
}
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<String> =
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<Timer>,
}
impl Daemon {
pub fn new(socket_path: String) -> anyhow::Result<Self> {
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<Answer, AnswerErr> {
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));
}
}
}

View file

@ -1,286 +1,24 @@
use crate::cli::{convert_command, send_command, Cli, Commands}; pub mod cli;
use crate::daemon::Daemon; pub mod daemon;
use anyhow::Result; pub mod timer;
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<S>(instant: &Instant, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let duration = instant.elapsed();
duration.serialize(serializer)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Instant, D::Error>
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<Timer>),
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<String> =
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<Timer>,
}
impl Daemon {
pub fn new(socket_path: String) -> Result<Self> {
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 std::time::Duration;
use clap::{Parser, Subcommand}; use crate::cli::{send_command, Cli, Command as CliCommand};
use crate::daemon::{Command as DaemonCommand, Daemon};
use anyhow::{Context, Result}; use anyhow::Result;
use clap::Parser;
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> {
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<()> { fn main() -> Result<()> {
let args = Cli::parse(); let args = Cli::parse();
match args.command { let daemon_command = match args.command {
Commands::Daemon => { CliCommand::Daemon => return Daemon::new(args.socket)?.run(),
Daemon::new(args.socket)?.run()?; CliCommand::Add {
Ok(()) name,
} duration_seconds,
_ => send_command(&args.socket, convert_command(&args.command)), } => DaemonCommand::Add(name, Duration::from_secs(duration_seconds)),
} CliCommand::List => DaemonCommand::List,
CliCommand::Remove { name } => DaemonCommand::Remove(name),
};
send_command(&args.socket, daemon_command)
} }

69
src/timer.rs Normal file
View file

@ -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<S>(instant: &Instant, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let duration = instant.elapsed();
duration.serialize(serializer)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Instant, D::Error>
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)
}
}