diff --git a/Cargo.lock b/Cargo.lock index c453d28..9d297ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2023,6 +2023,7 @@ dependencies = [ "lan-mouse-ipc", "libadwaita", "log", + "thiserror 2.0.0", ] [[package]] diff --git a/lan-mouse-cli/src/lib.rs b/lan-mouse-cli/src/lib.rs index b8c4a0d..dda74a8 100644 --- a/lan-mouse-cli/src/lib.rs +++ b/lan-mouse-cli/src/lib.rs @@ -18,7 +18,7 @@ pub enum CliError { Ipc(#[from] IpcError), } -#[derive(Parser, Debug, PartialEq, Eq)] +#[derive(Parser, Clone, Debug, PartialEq, Eq)] #[command(name = "lan-mouse-cli", about = "LanMouse CLI interface")] pub struct CliArgs { #[command(subcommand)] @@ -37,7 +37,7 @@ struct Client { enter_hook: Option, } -#[derive(Subcommand, Debug, PartialEq, Eq)] +#[derive(Clone, Subcommand, Debug, PartialEq, Eq)] enum CliSubcommand { /// add a new client AddClient(Client), diff --git a/lan-mouse-gtk/Cargo.toml b/lan-mouse-gtk/Cargo.toml index 98e4490..4a1a202 100644 --- a/lan-mouse-gtk/Cargo.toml +++ b/lan-mouse-gtk/Cargo.toml @@ -13,6 +13,7 @@ async-channel = { version = "2.1.1" } hostname = "0.4.0" log = "0.4.20" lan-mouse-ipc = { path = "../lan-mouse-ipc", version = "0.2.0" } +thiserror = "2.0.0" [build-dependencies] glib-build-tools = { version = "0.20.0" } diff --git a/lan-mouse-gtk/src/lib.rs b/lan-mouse-gtk/src/lib.rs index f5e72b7..edc2220 100644 --- a/lan-mouse-gtk/src/lib.rs +++ b/lan-mouse-gtk/src/lib.rs @@ -18,7 +18,15 @@ use gtk::{gio, glib, prelude::ApplicationExt}; use self::client_object::ClientObject; use self::key_object::KeyObject; -pub fn run() -> glib::ExitCode { +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum GtkError { + #[error("gtk frontend exited with non zero exit code: {0}")] + NonZeroExitCode(i32), +} + +pub fn run() -> Result<(), GtkError> { log::debug!("running gtk frontend"); #[cfg(windows)] let ret = std::thread::Builder::new() @@ -31,13 +39,10 @@ pub fn run() -> glib::ExitCode { #[cfg(not(windows))] let ret = gtk_main(); - if ret == glib::ExitCode::FAILURE { - log::error!("frontend exited with failure"); - } else { - log::info!("frontend exited successfully"); + match ret { + glib::ExitCode::SUCCESS => Ok(()), + e => Err(GtkError::NonZeroExitCode(e.value())), } - - ret } fn gtk_main() -> glib::ExitCode { diff --git a/lan-mouse-gtk/src/window.rs b/lan-mouse-gtk/src/window.rs index 3eef58b..d11395d 100644 --- a/lan-mouse-gtk/src/window.rs +++ b/lan-mouse-gtk/src/window.rs @@ -126,7 +126,7 @@ impl Window { #[strong] window, move |row: ClientRow, hostname: String| { - log::info!("request-hostname-change"); + log::debug!("request-hostname-change"); if let Some(client) = window.client_by_idx(row.index() as u32) { let hostname = Some(hostname).filter(|s| !s.is_empty()); /* changed in response to FrontendEvent @@ -163,7 +163,7 @@ impl Window { window, move |row: ClientRow, active: bool| { if let Some(client) = window.client_by_idx(row.index() as u32) { - log::info!( + log::debug!( "request: {} client", if active { "activating" } else { "deactivating" } ); diff --git a/src/capture_test.rs b/src/capture_test.rs index 9654ab8..5af2218 100644 --- a/src/capture_test.rs +++ b/src/capture_test.rs @@ -4,13 +4,13 @@ use futures::StreamExt; use input_capture::{self, CaptureError, CaptureEvent, InputCapture, InputCaptureError, Position}; use input_event::{Event, KeyboardEvent}; -#[derive(Args, Debug, Eq, PartialEq)] +#[derive(Args, Clone, Debug, Eq, PartialEq)] pub struct TestCaptureArgs {} pub async fn run(config: Config, _args: TestCaptureArgs) -> Result<(), InputCaptureError> { log::info!("running input capture test"); log::info!("creating input capture"); - let backend = config.capture_backend.map(|b| b.into()); + let backend = config.capture_backend().map(|b| b.into()); loop { let mut input_capture = InputCapture::new(backend).await?; log::info!("creating clients"); diff --git a/src/config.rs b/src/config.rs index b681e58..e156781 100644 --- a/src/config.rs +++ b/src/config.rs @@ -24,33 +24,50 @@ use shadow_rs::shadow; shadow!(build); -#[derive(Serialize, Deserialize, Debug)] -pub struct ConfigToml { - pub capture_backend: Option, - pub emulation_backend: Option, - pub port: Option, - pub frontend: Option, - pub release_bind: Option>, - pub cert_path: Option, - pub left: Option, - pub right: Option, - pub top: Option, - pub bottom: Option, - pub authorized_fingerprints: Option>, +const CONFIG_FILE_NAME: &str = "config.toml"; +const CERT_FILE_NAME: &str = "lan-mouse.pem"; + +fn default_path() -> Result { + #[cfg(unix)] + let default_path = { + let xdg_config_home = + env::var("XDG_CONFIG_HOME").unwrap_or(format!("{}/.config", env::var("HOME")?)); + format!("{xdg_config_home}/lan-mouse/") + }; + + #[cfg(not(unix))] + let default_path = { + let app_data = + env::var("LOCALAPPDATA").unwrap_or(format!("{}/.config", env::var("USERPROFILE")?)); + format!("{app_data}\\lan-mouse\\") + }; + Ok(PathBuf::from(default_path)) } -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -pub struct TomlClient { - pub hostname: Option, - pub host_name: Option, - pub ips: Option>, - pub port: Option, - pub activate_on_startup: Option, - pub enter_hook: Option, +#[derive(Serialize, Deserialize, Debug)] +struct ConfigToml { + capture_backend: Option, + emulation_backend: Option, + port: Option, + release_bind: Option>, + cert_path: Option, + clients: Vec, + authorized_fingerprints: Option>, +} + +#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)] +struct TomlClient { + hostname: Option, + host_name: Option, + ips: Option>, + port: Option, + pos: Option, + activate_on_startup: Option, + enter_hook: Option, } impl ConfigToml { - pub fn new(path: &Path) -> Result { + fn new(path: &Path) -> Result { let config = fs::read_to_string(path)?; Ok(toml::from_str::<_>(&config)?) } @@ -58,36 +75,33 @@ impl ConfigToml { #[derive(Parser, Debug)] #[command(author, version=build::CLAP_LONG_VERSION, about, long_about = None)] -pub struct Args { +struct Args { /// the listen port for lan-mouse #[arg(short, long)] port: Option, - /// the frontend to use [cli | gtk] - #[arg(short, long)] - frontend: Option, - /// non-default config file location #[arg(short, long)] - pub config: Option, - - #[command(subcommand)] - pub command: Option, + config: Option, /// capture backend override #[arg(long)] - pub capture_backend: Option, + capture_backend: Option, /// emulation backend override #[arg(long)] - pub emulation_backend: Option, + emulation_backend: Option, /// path to non-default certificate location #[arg(long)] - pub cert_path: Option, + cert_path: Option, + + /// subcommands + #[command(subcommand)] + command: Option, } -#[derive(Subcommand, Debug, Eq, PartialEq)] +#[derive(Subcommand, Clone, Debug, Eq, PartialEq)] pub enum Command { /// test input emulation TestEmulation(TestEmulationArgs), @@ -220,48 +234,16 @@ impl Display for EmulationBackend { } } -#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize, ValueEnum)] -pub enum Frontend { - #[serde(rename = "gtk")] - Gtk, - #[serde(rename = "none")] - None, -} - -impl Default for Frontend { - fn default() -> Self { - if cfg!(feature = "gtk") { - Self::Gtk - } else { - Self::None - } - } -} - #[derive(Debug)] pub struct Config { - /// the path to the configuration file used - pub path: PathBuf, - /// public key fingerprints authorized for connection - pub authorized_fingerprints: HashMap, - /// optional input-capture backend override - pub capture_backend: Option, - /// optional input-emulation backend override - pub emulation_backend: Option, - /// the frontend to use - pub frontend: Frontend, - /// the port to use (initially) - pub port: u16, - /// list of clients - pub clients: Vec<(TomlClient, Position)>, - /// configured release bind - pub release_bind: Vec, - /// test capture instead of running the app - pub test_capture: bool, - /// test emulation instead of running the app - pub test_emulation: bool, - /// path to the tls certificate to use - pub cert_path: PathBuf, + /// command line arguments + args: Args, + /// path to the certificate file used + cert_path: PathBuf, + /// path to the config file used + config_path: PathBuf, + /// the (optional) toml config and it's path + config_toml: Option, } pub struct ConfigClient { @@ -273,6 +255,25 @@ pub struct ConfigClient { pub enter_hook: Option, } +impl From for ConfigClient { + fn from(toml: TomlClient) -> Self { + let active = toml.activate_on_startup.unwrap_or(false); + let enter_hook = toml.enter_hook; + let hostname = toml.hostname; + let ips = HashSet::from_iter(toml.ips.into_iter().flatten()); + let port = toml.port.unwrap_or(DEFAULT_PORT); + let pos = toml.pos.unwrap_or_default(); + Self { + ips, + hostname, + port, + pos, + active, + enter_hook, + } + } +} + #[derive(Debug, Error)] pub enum ConfigError { #[error(transparent)] @@ -287,133 +288,99 @@ const DEFAULT_RELEASE_KEYS: [scancode::Linux; 4] = [KeyLeftCtrl, KeyLeftShift, KeyLeftMeta, KeyLeftAlt]; impl Config { - pub fn new(args: &Args) -> Result { - const CONFIG_FILE_NAME: &str = "config.toml"; - const CERT_FILE_NAME: &str = "lan-mouse.pem"; - - #[cfg(unix)] - let config_path = { - let xdg_config_home = - env::var("XDG_CONFIG_HOME").unwrap_or(format!("{}/.config", env::var("HOME")?)); - format!("{xdg_config_home}/lan-mouse/") - }; - - #[cfg(not(unix))] - let config_path = { - let app_data = - env::var("LOCALAPPDATA").unwrap_or(format!("{}/.config", env::var("USERPROFILE")?)); - format!("{app_data}\\lan-mouse\\") - }; - - let config_path = PathBuf::from(config_path); - let config_file = config_path.join(CONFIG_FILE_NAME); + pub fn new() -> Result { + let args = Args::parse(); // --config overrules default location - let config_file = args.config.clone().unwrap_or(config_file); + let config_path = args + .config + .clone() + .unwrap_or(default_path()?.join(CONFIG_FILE_NAME)); - let mut config_toml = match ConfigToml::new(&config_file) { + let config_toml = match ConfigToml::new(&config_path) { Err(e) => { - log::warn!("{config_file:?}: {e}"); + log::warn!("{config_path:?}: {e}"); log::warn!("Continuing without config file ..."); None } Ok(c) => Some(c), }; - let frontend_arg = args.frontend; - let frontend_cfg = config_toml.as_ref().and_then(|c| c.frontend); - let frontend = frontend_arg.or(frontend_cfg).unwrap_or_default(); - - let port = args - .port - .or(config_toml.as_ref().and_then(|c| c.port)) - .unwrap_or(DEFAULT_PORT); - - log::debug!("{config_toml:?}"); - let release_bind = config_toml - .as_ref() - .and_then(|c| c.release_bind.clone()) - .unwrap_or(Vec::from_iter(DEFAULT_RELEASE_KEYS.iter().cloned())); - - let capture_backend = args - .capture_backend - .or(config_toml.as_ref().and_then(|c| c.capture_backend)); - - let emulation_backend = args - .emulation_backend - .or(config_toml.as_ref().and_then(|c| c.emulation_backend)); - + // --cert-path overrules default location let cert_path = args .cert_path .clone() .or(config_toml.as_ref().and_then(|c| c.cert_path.clone())) - .unwrap_or(config_path.join(CERT_FILE_NAME)); - - let authorized_fingerprints = config_toml - .as_mut() - .and_then(|c| std::mem::take(&mut c.authorized_fingerprints)) - .unwrap_or_default(); - - let mut clients: Vec<(TomlClient, Position)> = vec![]; - - if let Some(config_toml) = config_toml { - if let Some(c) = config_toml.right { - clients.push((c, Position::Right)) - } - if let Some(c) = config_toml.left { - clients.push((c, Position::Left)) - } - if let Some(c) = config_toml.top { - clients.push((c, Position::Top)) - } - if let Some(c) = config_toml.bottom { - clients.push((c, Position::Bottom)) - } - } - - let test_capture = matches!(args.command, Some(Command::TestCapture(_))); - let test_emulation = matches!(args.command, Some(Command::TestEmulation(_))); + .unwrap_or(default_path()?.join(CERT_FILE_NAME)); Ok(Config { - path: config_path, - authorized_fingerprints, - capture_backend, - emulation_backend, - frontend, - clients, - port, - release_bind, - test_capture, - test_emulation, + args, cert_path, + config_path, + config_toml, }) } - pub fn get_clients(&self) -> Vec { - self.clients - .iter() - .map(|(c, pos)| { - let port = c.port.unwrap_or(DEFAULT_PORT); - let ips: HashSet = if let Some(ips) = c.ips.as_ref() { - HashSet::from_iter(ips.iter().cloned()) - } else { - HashSet::new() - }; - let hostname = match &c.hostname { - Some(h) => Some(h.clone()), - None => c.host_name.clone(), - }; - let active = c.activate_on_startup.unwrap_or(false); - let enter_hook = c.enter_hook.clone(); - ConfigClient { - ips, - hostname, - port, - pos: *pos, - active, - enter_hook, - } - }) + /// the command to run + pub fn command(&self) -> Option { + self.args.command.clone() + } + + pub fn config_path(&self) -> &Path { + &self.config_path + } + + /// public key fingerprints authorized for connection + pub fn authorized_fingerprints(&self) -> HashMap { + self.config_toml + .as_ref() + .and_then(|c| c.authorized_fingerprints.clone()) + .unwrap_or_default() + } + + /// path to certificate + pub fn cert_path(&self) -> &Path { + &self.cert_path + } + + /// optional input-capture backend override + pub fn capture_backend(&self) -> Option { + self.args + .capture_backend + .or(self.config_toml.as_ref().and_then(|c| c.capture_backend)) + } + + /// optional input-emulation backend override + pub fn emulation_backend(&self) -> Option { + self.args + .emulation_backend + .or(self.config_toml.as_ref().and_then(|c| c.emulation_backend)) + } + + /// the port to use (initially) + pub fn port(&self) -> u16 { + self.args + .port + .or(self.config_toml.as_ref().and_then(|c| c.port)) + .unwrap_or(DEFAULT_PORT) + } + + /// list of configured clients + pub fn clients(&self) -> Vec { + self.config_toml + .as_ref() + .map(|c| c.clients.clone()) + .into_iter() + .flatten() + .map(From::::from) .collect() } + + /// release bind for returning control to the host + pub fn release_bind(&self) -> Vec { + self.config_toml + .as_ref() + .and_then(|c| c.release_bind.clone()) + .unwrap_or(Vec::from_iter(DEFAULT_RELEASE_KEYS.iter().cloned())) + } } diff --git a/src/emulation_test.rs b/src/emulation_test.rs index 5b2d649..b3183f1 100644 --- a/src/emulation_test.rs +++ b/src/emulation_test.rs @@ -8,7 +8,7 @@ use std::time::{Duration, Instant}; const FREQUENCY_HZ: f64 = 1.0; const RADIUS: f64 = 100.0; -#[derive(Args, Debug, Eq, PartialEq)] +#[derive(Args, Clone, Debug, Eq, PartialEq)] pub struct TestEmulationArgs { #[arg(long)] mouse: bool, @@ -21,7 +21,7 @@ pub struct TestEmulationArgs { pub async fn run(config: Config, _args: TestEmulationArgs) -> Result<(), InputEmulationError> { log::info!("running input emulation test"); - let backend = config.emulation_backend.map(|b| b.into()); + let backend = config.emulation_backend().map(|b| b.into()); let mut emulation = InputEmulation::new(backend).await?; emulation.create(0).await; diff --git a/src/main.rs b/src/main.rs index 33672ed..0ca54c9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,20 @@ -use clap::Parser; use env_logger::Env; use input_capture::InputCaptureError; use input_emulation::InputEmulationError; use lan_mouse::{ capture_test, - config::{self, Config, ConfigError, Frontend}, + config::{self, Command, Config, ConfigError}, emulation_test, service::{Service, ServiceError}, }; use lan_mouse_cli::CliError; +#[cfg(feature = "gtk")] +use lan_mouse_gtk::GtkError; use lan_mouse_ipc::{IpcError, IpcListenerCreationError}; use std::{ future::Future, io, - process::{self, Child, Command}, + process::{self, Child}, }; use thiserror::Error; use tokio::task::LocalSet; @@ -32,6 +33,9 @@ enum LanMouseError { Capture(#[from] InputCaptureError), #[error(transparent)] Emulation(#[from] InputEmulationError), + #[cfg(feature = "gtk")] + #[error(transparent)] + Gtk(#[from] GtkError), #[error(transparent)] Cli(#[from] CliError), } @@ -48,15 +52,13 @@ fn main() { } fn run() -> Result<(), LanMouseError> { - // parse config file + cli args - let args = config::Args::parse(); - let config = config::Config::new(&args)?; - match args.command { + let config = config::Config::new()?; + match config.command() { Some(command) => match command { - config::Command::TestEmulation(args) => run_async(emulation_test::run(config, args))?, - config::Command::TestCapture(args) => run_async(capture_test::run(config, args))?, - config::Command::Cli(cli_args) => run_async(lan_mouse_cli::run(cli_args))?, - config::Command::Daemon => { + Command::TestEmulation(args) => run_async(emulation_test::run(config, args))?, + Command::TestCapture(args) => run_async(capture_test::run(config, args))?, + Command::Cli(cli_args) => run_async(lan_mouse_cli::run(cli_args))?, + Command::Daemon => { // if daemon is specified we run the service match run_async(run_service(config)) { Err(LanMouseError::Service(ServiceError::IpcListen( @@ -69,18 +71,32 @@ fn run() -> Result<(), LanMouseError> { None => { // otherwise start the service as a child process and // run a frontend - let mut service = start_service()?; - run_frontend(&config)?; - #[cfg(unix)] + #[cfg(feature = "gtk")] { - // on unix we give the service a chance to terminate gracefully - let pid = service.id() as libc::pid_t; - unsafe { - libc::kill(pid, libc::SIGINT); + let mut service = start_service()?; + let res = lan_mouse_gtk::run(); + #[cfg(unix)] + { + // on unix we give the service a chance to terminate gracefully + let pid = service.id() as libc::pid_t; + unsafe { + libc::kill(pid, libc::SIGINT); + } + service.wait()?; + } + service.kill()?; + res?; + } + #[cfg(not(feature = "gtk"))] + { + // run daemon if gtk is diabled + match run_async(run_service(config)) { + Err(LanMouseError::Service(ServiceError::IpcListen( + IpcListenerCreationError::AlreadyRunning, + ))) => log::info!("service already running!"), + r => r?, } - service.wait()?; } - service.kill()?; } } @@ -103,7 +119,7 @@ where } fn start_service() -> Result { - let child = Command::new(std::env::current_exe()?) + let child = process::Command::new(std::env::current_exe()?) .args(std::env::args().skip(1)) .arg("daemon") .spawn()?; @@ -111,25 +127,12 @@ fn start_service() -> Result { } async fn run_service(config: Config) -> Result<(), ServiceError> { - log::info!("using config: {:?}", config.path); - log::info!("Press {:?} to release the mouse", config.release_bind); + let release_bind = config.release_bind(); + let config_path = config.config_path().to_owned(); let mut service = Service::new(config).await?; + log::info!("using config: {config_path:?}"); + log::info!("Press {release_bind:?} to release the mouse"); service.run().await?; log::info!("service exited!"); Ok(()) } - -fn run_frontend(config: &Config) -> Result<(), IpcError> { - match config.frontend { - #[cfg(feature = "gtk")] - Frontend::Gtk => { - lan_mouse_gtk::run(); - } - #[cfg(not(feature = "gtk"))] - Frontend::Gtk => panic!("gtk frontend requested but feature not enabled!"), - Frontend::None => { - log::warn!("no frontend available!"); - } - }; - Ok(()) -} diff --git a/src/service.rs b/src/service.rs index 516c571..a1bb4de 100644 --- a/src/service.rs +++ b/src/service.rs @@ -80,7 +80,7 @@ struct Incoming { impl Service { pub async fn new(config: Config) -> Result { let client_manager = ClientManager::default(); - for client in config.get_clients() { + for client in config.clients() { let config = ClientConfig { hostname: client.hostname, fix_ips: client.ips.into_iter().collect(), @@ -99,28 +99,28 @@ impl Service { } // load certificate - let cert = crypto::load_or_generate_key_and_cert(&config.cert_path)?; + let cert = crypto::load_or_generate_key_and_cert(config.cert_path())?; let public_key_fingerprint = crypto::certificate_fingerprint(&cert); // create frontend communication adapter, exit if already running let frontend_listener = AsyncFrontendListener::new().await?; - let authorized_keys = Arc::new(RwLock::new(config.authorized_fingerprints.clone())); + let authorized_keys = Arc::new(RwLock::new(config.authorized_fingerprints())); // listener + connection let listener = - LanMouseListener::new(config.port, cert.clone(), authorized_keys.clone()).await?; + LanMouseListener::new(config.port(), cert.clone(), authorized_keys.clone()).await?; let conn = LanMouseConnection::new(cert.clone(), client_manager.clone()); // input capture + emulation - let capture_backend = config.capture_backend.map(|b| b.into()); - let capture = Capture::new(capture_backend, conn, config.release_bind.clone()); - let emulation_backend = config.emulation_backend.map(|b| b.into()); + let capture_backend = config.capture_backend().map(|b| b.into()); + let capture = Capture::new(capture_backend, conn, config.release_bind()); + let emulation_backend = config.emulation_backend().map(|b| b.into()); let emulation = Emulation::new(emulation_backend, listener); // create dns resolver let resolver = DnsResolver::new()?; - let port = config.port; + let port = config.port(); let service = Self { capture, emulation, @@ -142,11 +142,15 @@ impl Service { } pub async fn run(&mut self) -> Result<(), ServiceError> { - for handle in self.client_manager.active_clients() { + let active = self.client_manager.active_clients(); + for handle in active.iter() { // small hack: `activate_client()` checks, if the client // is already active in client_manager and does not create a // capture barrier in that case so we have to deactivate it first - self.client_manager.deactivate_client(handle); + self.client_manager.deactivate_client(*handle); + } + + for handle in active { self.activate_client(handle); }