From 3dc5b89ee59657fbe240cfdc5cacb15acd1f94fa Mon Sep 17 00:00:00 2001 From: hheik <4469778+hheik@users.noreply.github.com> Date: Fri, 29 Sep 2023 22:51:23 +0300 Subject: [PATCH] Added automatically starting server with local socket communication --- Cargo.toml | 1 + src/client.rs | 22 ++---- src/main.rs | 45 +++++++++++- src/server.rs | 200 +++++++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 251 insertions(+), 17 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8e81bc7..3dfec8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,4 +8,5 @@ edition = "2021" [dependencies] argh = "0.1.12" crossterm = "0.27.0" +interprocess = "1.2.1" ratatui = "0.23.0" diff --git a/src/client.rs b/src/client.rs index a8cdb8a..d6d5767 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,24 +1,16 @@ use argh::FromArgs; use std::{error::Error, time::Duration}; +use crate::CliArgs; + pub mod app; pub mod crossterm; pub mod ui; -/// Demo -#[derive(Debug, FromArgs)] -struct Cli { - /// time in ms between two ticks. - #[argh(option, default = "250")] - tick_rate: u64, - /// whether unicode symbols are used to improve the overall look of the app - #[argh(option, default = "true")] - enhanced_graphics: bool, -} - -pub fn run() -> Result<(), Box> { - let cli: Cli = argh::from_env(); - let tick_rate = Duration::from_millis(cli.tick_rate); - crossterm::run(tick_rate, cli.enhanced_graphics)?; +pub fn run(args: CliArgs) -> Result<(), Box> { + crossterm::run( + Duration::from_millis(args.tick_rate), + args.enhanced_graphics, + )?; Ok(()) } diff --git a/src/main.rs b/src/main.rs index da083c5..b30a5f1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,47 @@ +use argh::FromArgs; + pub mod client; pub mod server; -fn main() {} +/// rmp: Rust Music Player +#[derive(Debug, FromArgs)] +pub struct CliArgs { + /// run the server + #[argh(switch, short = 's')] + server: bool, + + /// kill server + #[argh(switch, short = 'x')] + exit: bool, + + /// don't start server even if it's not running + #[argh(switch, short = 'c')] + client_only: bool, + + /// time in ms between two ticks. + #[argh(option, default = "250")] + tick_rate: u64, + + /// whether unicode symbols are used to improve the overall look of the app + #[argh(option, default = "true")] + enhanced_graphics: bool, +} + +fn main() { + let args: CliArgs = argh::from_env(); + + if args.exit { + server::kill(); + return; + } + + if args.server { + server::run(args).unwrap(); + return; + } + + if !args.client_only && !server::is_running().unwrap() { + server::run_in_background(); + } + client::run(args).map(|_| server::kill()).unwrap(); +} diff --git a/src/server.rs b/src/server.rs index 74e6db3..7aff466 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,5 +1,203 @@ +use interprocess::local_socket::LocalSocketListener; +use std::{fmt::Debug, fs, path::PathBuf, process}; + +use crate::CliArgs; + pub mod audio_backend; -pub fn run() -> Result<(), ()> { +#[derive(Debug)] +pub enum ServerError { + Other(String), + Io(std::io::Error), + AlreadyStarted, + MissingRuntimeDir(PathBuf), +} + +impl ServerError { + fn from_debuggable(err: impl Debug) -> Self { + Self::Other(format!("Unexpected error: {err:?}").to_string()) + } +} + +pub struct Server { + pub socket: LocalSocketListener, +} + +impl Server { + pub fn new(socket_path: PathBuf) -> Result { + Ok(Self { + socket: LocalSocketListener::bind(socket_path).map_err(|err| ServerError::Io(err))?, + }) + } +} + +pub fn run(args: CliArgs) -> Result<(), ServerError> { + if let Err(err) = os::reserve_pid() { + let exit_code = handle_error(err); + process::exit(exit_code); + } + serve() +} + +pub fn kill() { + if let Err(err) = os::kill() { + let exit_code = handle_error(err); + process::exit(exit_code); + } +} + +pub fn is_running() -> Result { + match os::is_running() { + Ok(is_running) => Ok(is_running), + Err(err) => { + let exit_code = handle_error(err); + process::exit(exit_code); + } + } +} + +pub fn run_in_background() { + if let Err(err) = os::run_in_background() { + let exit_code = handle_error(err); + process::exit(exit_code); + } +} + +fn serve() -> Result<(), ServerError> { + let socket_path = os::get_socket_path()?; + if socket_path.exists() { + fs::remove_file(&socket_path).map_err(|err| ServerError::Io(err))?; + } + let server = Server::new(socket_path)?; + println!("Waiting for messages..."); + for message in server.socket.incoming() { + match message { + Ok(stream) => { + println!("\tstream: {:?}", stream); + } + Err(err) => { + let exit_code = handle_error(ServerError::Io(err)); + process::exit(exit_code); + } + } + } + println!("Reached the end of an infinite loop?"); Ok(()) } + +fn handle_error(err: ServerError) -> i32 { + match &err { + ServerError::Other(msg) => { + eprintln!("Unknown error: {msg}"); + 1 + } + ServerError::Io(err) => { + eprintln!("IO error: {err}"); + 2 + } + ServerError::AlreadyStarted => { + eprintln!("Server already running"); + 100 + } + ServerError::MissingRuntimeDir(path) => { + eprintln!("Missing runtime directory: {}", path.to_string_lossy()); + 101 + } + } +} + +#[cfg(target_family = "unix")] +mod os { + use std::{ + fs, + path::{Path, PathBuf}, + process::{id, Command, Stdio}, + }; + + use super::ServerError; + + pub fn reserve_pid() -> Result<(), ServerError> { + let pid_path = get_pid_path()?; + is_running()?; + + fs::write(&pid_path, id().to_string()).map_err(|err| ServerError::Io(err))?; + Command::new("chmod") + .args(&["600", &pid_path.to_string_lossy()]) + .output() + .map_err(|err| ServerError::Io(err))?; + Ok(()) + } + + pub fn is_running() -> Result { + let pid_path = get_pid_path()?; + + match fs::read(&pid_path) { + Ok(old_pid) => { + let old_pid = + String::from_utf8(old_pid).map_err(|err| ServerError::from_debuggable(err))?; + let old_pid = old_pid.trim(); + Ok(Command::new("ps") + .args(&["-p", old_pid]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .map_err(|err| ServerError::Io(err))? + .success()) + } + _ => Ok(false), + } + } + + pub fn run_in_background() -> Result<(), ServerError> { + let this = std::env::args().next().unwrap(); + Command::new(this) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .args(&["-s"]) + .spawn() + .map_err(|err| ServerError::Io(err))?; + Ok(()) + } + + pub fn kill() -> Result<(), ServerError> { + let pid_path = get_pid_path()?; + let socket_path = get_socket_path()?; + let pid = String::from_utf8(fs::read(&pid_path).map_err(|err| ServerError::Io(err))?) + .map_err(|err| ServerError::from_debuggable(err))?; + let pid = pid.trim(); + Command::new("kill") + .arg(pid) + .spawn() + .map_err(|err| ServerError::Io(err))?; + Command::new("rm") + .args(&[ + "-f", + &pid_path.to_string_lossy(), + &socket_path.to_string_lossy(), + ]) + .spawn() + .map_err(|err| ServerError::Io(err))?; + Ok(()) + } + + pub fn get_socket_path() -> Result { + Ok(get_runtime_dir()?.join("rmp.socket")) + } + + fn get_runtime_dir() -> Result { + let uid = String::from_utf8( + Command::new("id") + .arg("-u") + .output() + .map_err(|err| ServerError::Io(err))? + .stdout, + ) + .map_err(|err| ServerError::from_debuggable(err))?; + let dir = Path::new("/run/user").join(uid.trim().to_string()); + Ok(dir) + } + + fn get_pid_path() -> Result { + Ok(get_runtime_dir()?.join("rmp.pid")) + } +}