feat: split into modules

This commit is contained in:
Moritz Böhme 2023-07-28 19:52:23 +02:00
parent a5cac154f4
commit 853e735fcd
No known key found for this signature in database
GPG key ID: 970C6E89EB0547A9
6 changed files with 298 additions and 286 deletions

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};
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<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 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> {
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)
}

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