feat: init

This commit is contained in:
Moritz Böhme 2023-06-20 20:12:02 +02:00
commit 194e5112e5
No known key found for this signature in database
GPG key ID: 970C6E89EB0547A9
9 changed files with 817 additions and 0 deletions

286
src/main.rs Normal file
View file

@ -0,0 +1,286 @@
use crate::cli::{convert_command, send_command, Cli, Commands};
use crate::daemon::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)),
}
}