diff --git a/Cargo.lock b/Cargo.lock index 3a0c167..41bab16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -441,6 +441,7 @@ dependencies = [ "fs_extra", "html2text", "lazy_static", + "libc", "log", "log-panics", "maplit", @@ -936,9 +937,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.94" +version = "0.2.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18794a8ad5b29321f790b55d93dfba91e125cb1a9edbd4f8e3150acc771c1a5e" +checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790" [[package]] name = "libdbus-sys" diff --git a/Makefile.toml b/Makefile.toml index 4754cc6..fc6286a 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -121,7 +121,7 @@ cp -f $EXEC_PATH $TARGET_DIR/Contents/MacOS/espanso ''' dependencies=["build-binary"] -[tasks.run-debug-bundle] +[tasks.run-bundle] command="target/mac/Espanso.app/Contents/MacOS/espanso" args=["${@}"] dependencies=["create-bundle"] diff --git a/espanso/Cargo.toml b/espanso/Cargo.toml index ca7b2b2..30d123a 100644 --- a/espanso/Cargo.toml +++ b/espanso/Cargo.toml @@ -62,5 +62,8 @@ winapi = { version = "0.3.9", features = ["wincon"] } winreg = "0.9.0" widestring = "0.4.3" +[target.'cfg(unix)'.dependencies] +libc = "0.2.98" + [target.'cfg(target_os="macos")'.dependencies] espanso-mac-utils = { path = "../espanso-mac-utils" } \ No newline at end of file diff --git a/espanso/src/cli/launcher/daemon.rs b/espanso/src/cli/launcher/daemon.rs index 27cff0b..a4fc895 100644 --- a/espanso/src/cli/launcher/daemon.rs +++ b/espanso/src/cli/launcher/daemon.rs @@ -23,35 +23,13 @@ use anyhow::Result; use thiserror::Error; use crate::cli::PathsOverrides; +use crate::cli::util::CommandExt; pub fn launch_daemon(paths_overrides: &PathsOverrides) -> Result<()> { let espanso_exe_path = std::env::current_exe()?; let mut command = Command::new(&espanso_exe_path.to_string_lossy().to_string()); command.args(&["daemon", "--show-welcome"]); - - // We only inject the paths that were explicitly overrided because otherwise - // the migration process might create some incompatibilities. - // For example, after the migration the "packages" dir could have been - // moved to a different one, and that might cause the daemon to crash - // if we inject the old packages dir that was automatically resolved. - if let Some(config_override) = &paths_overrides.config { - command.env( - "ESPANSO_CONFIG_DIR", - config_override.to_string_lossy().to_string(), - ); - } - if let Some(packages_override) = &paths_overrides.packages { - command.env( - "ESPANSO_PACKAGE_DIR", - packages_override.to_string_lossy().to_string(), - ); - } - if let Some(runtime_override) = &paths_overrides.runtime { - command.env( - "ESPANSO_RUNTIME_DIR", - runtime_override.to_string_lossy().to_string(), - ); - } + command.with_paths_overrides(paths_overrides); let mut child = command.spawn()?; let result = child.wait()?; diff --git a/espanso/src/cli/mod.rs b/espanso/src/cli/mod.rs index 7166f04..a0ce61d 100644 --- a/espanso/src/cli/mod.rs +++ b/espanso/src/cli/mod.rs @@ -30,6 +30,8 @@ pub mod log; pub mod migrate; pub mod modulo; pub mod path; +pub mod service; +pub mod util; pub mod worker; pub struct CliModule { diff --git a/espanso/src/cli/service/macos.rs b/espanso/src/cli/service/macos.rs new file mode 100644 index 0000000..c65e120 --- /dev/null +++ b/espanso/src/cli/service/macos.rs @@ -0,0 +1,166 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019-2021 Federico Terzi + * + * espanso is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * espanso is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with espanso. If not, see . + */ + +use anyhow::Result; +use log::{info, warn}; +use std::process::Command; +use std::{fs::create_dir_all, process::ExitStatus}; +use thiserror::Error; + +#[cfg(target_os = "macos")] +const SERVICE_PLIST_CONTENT: &str = include_str!("../../res/macos/com.federicoterzi.espanso.plist"); +#[cfg(target_os = "macos")] +const SERVICE_PLIST_FILE_NAME: &str = "com.federicoterzi.espanso.plist"; + +pub fn register() -> Result<()> { + let home_dir = dirs::home_dir().expect("could not get user home directory"); + let library_dir = home_dir.join("Library"); + let agents_dir = library_dir.join("LaunchAgents"); + + // Make sure agents directory exists + if !agents_dir.exists() { + create_dir_all(agents_dir.clone())?; + } + + let plist_file = agents_dir.join(SERVICE_PLIST_FILE_NAME); + if !plist_file.exists() { + info!( + "creating LaunchAgents entry: {}", + plist_file.to_str().unwrap_or_default() + ); + + let espanso_path = std::env::current_exe()?; + info!( + "entry will point to: {}", + espanso_path.to_str().unwrap_or_default() + ); + + let plist_content = String::from(SERVICE_PLIST_CONTENT).replace( + "{{{espanso_path}}}", + espanso_path.to_str().unwrap_or_default(), + ); + + // Copy the user PATH variable and inject it in the Plist file so that + // it gets loaded by Launchd. + // To see why this is necessary: https://github.com/federico-terzi/espanso/issues/233 + let user_path = std::env::var("PATH").unwrap_or("".to_owned()); + let plist_content = plist_content.replace("{{{PATH}}}", &user_path); + + std::fs::write(plist_file.clone(), plist_content).expect("Unable to write plist file"); + } + + info!("reloading espanso launchctl entry"); + + if let Err(err) = Command::new("launchctl") + .args(&["unload", "-w", plist_file.to_str().unwrap_or_default()]) + .output() + { + warn!("unload command failed: {}", err); + } + + let res = Command::new("launchctl") + .args(&["load", "-w", plist_file.to_str().unwrap_or_default()]) + .status(); + + if let Ok(status) = res { + if status.success() { + return Ok(()); + } + } + + Err(RegisterError::LaunchCtlLoadFailed.into()) +} + +#[derive(Error, Debug)] +pub enum RegisterError { + #[error("launchctl load failed")] + LaunchCtlLoadFailed, +} + +pub fn unregister() -> Result<()> { + let home_dir = dirs::home_dir().expect("could not get user home directory"); + let library_dir = home_dir.join("Library"); + let agents_dir = library_dir.join("LaunchAgents"); + + let plist_file = agents_dir.join(SERVICE_PLIST_FILE_NAME); + if plist_file.exists() { + let _res = Command::new("launchctl") + .args(&["unload", "-w", plist_file.to_str().unwrap_or_default()]) + .output(); + + std::fs::remove_file(&plist_file)?; + + Ok(()) + } else { + Err(UnregisterError::PlistNotFound.into()) + } +} + +#[derive(Error, Debug)] +pub enum UnregisterError { + #[error("plist entry not found")] + PlistNotFound, +} + +pub fn is_registered() -> bool { + let home_dir = dirs::home_dir().expect("could not get user home directory"); + let library_dir = home_dir.join("Library"); + let agents_dir = library_dir.join("LaunchAgents"); + let plist_file = agents_dir.join(SERVICE_PLIST_FILE_NAME); + plist_file.is_file() +} + +pub fn start_service() -> Result<()> { + if !is_registered() { + eprintln!("Unable to start espanso as a service as it's not been registered."); + eprintln!("You can either register it first with `espanso service register` or"); + eprintln!("you can run it in unmanaged mode with `espanso service start --unmanaged`"); + eprintln!(""); + eprintln!("NOTE: unmanaged mode means espanso does not rely on the system service manager"); + eprintln!(" to run, but as a result, you are in charge of starting/stopping espanso"); + eprintln!(" when needed."); + return Err(StartError::NotRegistered.into()); + } + + let res = Command::new("launchctl") + .args(&["start", "com.federicoterzi.espanso"]) + .status(); + + if let Ok(status) = res { + if status.success() { + Ok(()) + } else { + Err(StartError::LaunchCtlNonZeroExit(status).into()) + } + } else { + Err(StartError::LaunchCtlFailure.into()) + } +} + +#[derive(Error, Debug)] +pub enum StartError { + #[error("not registered as a service")] + NotRegistered, + + #[error("launchctl failed to run")] + LaunchCtlFailure, + + #[error("launchctl exited with non-zero code `{0}`")] + LaunchCtlNonZeroExit(ExitStatus), +} diff --git a/espanso/src/cli/service/mod.rs b/espanso/src/cli/service/mod.rs new file mode 100644 index 0000000..4c8b567 --- /dev/null +++ b/espanso/src/cli/service/mod.rs @@ -0,0 +1,118 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019-2021 Federico Terzi + * + * espanso is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * espanso is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with espanso. If not, see . + */ + +use super::{CliModule, CliModuleArgs}; +use crate::{error_eprintln, exit_code::{SERVICE_ALREADY_RUNNING, SERVICE_FAILURE, SERVICE_NOT_REGISTERED, SERVICE_NOT_RUNNING, SERVICE_SUCCESS}, info_println, lock::acquire_worker_lock}; + +#[cfg(target_os = "macos")] +mod macos; +#[cfg(target_os = "macos")] +use macos::*; + +#[cfg(not(target_os = "windows"))] +mod unix; +#[cfg(not(target_os = "windows"))] +use unix::*; + +mod stop; + +pub fn new() -> CliModule { + CliModule { + enable_logs: true, + disable_logs_terminal_output: true, + requires_paths: true, + subcommand: "service".to_string(), + log_mode: super::LogMode::AppendOnly, + entry: service_main, + ..Default::default() + } +} + +fn service_main(args: CliModuleArgs) -> i32 { + let paths = args.paths.expect("missing paths argument"); + let cli_args = args.cli_args.expect("missing cli_args"); + let paths_overrides = args.paths_overrides.expect("missing paths_overrides"); + + if cli_args.subcommand_matches("register").is_some() { + if let Err(err) = register() { + error_eprintln!("unable to register service: {}", err); + return SERVICE_FAILURE; + } else { + info_println!("service registered correctly!"); + } + } else if cli_args.subcommand_matches("unregister").is_some() { + if let Err(err) = unregister() { + error_eprintln!("unable to unregister service: {}", err); + return SERVICE_FAILURE; + } else { + info_println!("service unregistered correctly!"); + } + } else if cli_args.subcommand_matches("check").is_some() { + if is_registered() { + info_println!("registered as a service"); + } else { + error_eprintln!("not registered as a service"); + return SERVICE_NOT_REGISTERED; + } + } else if let Some(sub_args) = cli_args.subcommand_matches("start") { + let lock_file = acquire_worker_lock(&paths.runtime); + if lock_file.is_none() { + error_eprintln!("espanso is already running!"); + return SERVICE_ALREADY_RUNNING; + } + drop(lock_file); + + if sub_args.is_present("unmanaged") && !cfg!(target_os = "windows") { + // Unmanaged service + #[cfg(unix)] + { + if let Err(err) = fork_daemon(&paths_overrides) { + error_eprintln!("unable to start service (unmanaged): {}", err); + return SERVICE_FAILURE; + } + } + #[cfg(windows)] + { + unreachable!(); + } + } else { + // Managed service + if let Err(err) = start_service() { + error_eprintln!("unable to start service: {}", err); + return SERVICE_FAILURE; + } else { + info_println!("espanso started correctly!"); + } + } + } else if cli_args.subcommand_matches("stop").is_some() { + let lock_file = acquire_worker_lock(&paths.runtime); + if lock_file.is_some() { + error_eprintln!("espanso is not running!"); + return SERVICE_NOT_RUNNING; + } + drop(lock_file); + + if let Err(err) = stop::terminate_worker(&paths.runtime) { + error_eprintln!("unable to stop espanso: {}", err); + return SERVICE_FAILURE; + } + } + + SERVICE_SUCCESS +} diff --git a/espanso/src/cli/service/stop.rs b/espanso/src/cli/service/stop.rs new file mode 100644 index 0000000..4a39efa --- /dev/null +++ b/espanso/src/cli/service/stop.rs @@ -0,0 +1,69 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019-2021 Federico Terzi + * + * espanso is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * espanso is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with espanso. If not, see . + */ + +use anyhow::{Error, Result}; +use log::error; +use std::{path::Path, time::Instant}; +use thiserror::Error; + +use espanso_ipc::IPCClient; + +use crate::{ + ipc::{create_ipc_client_to_worker, IPCEvent}, + lock::acquire_worker_lock, +}; + +pub fn terminate_worker(runtime_dir: &Path) -> Result<()> { + match create_ipc_client_to_worker(runtime_dir) { + Ok(mut worker_ipc) => { + if let Err(err) = worker_ipc.send_async(IPCEvent::ExitAllProcesses) { + error!( + "unable to send termination signal to worker process: {}", + err + ); + return Err(StopError::IPCError(err).into()); + } + } + Err(err) => { + error!("could not establish IPC connection with worker: {}", err); + return Err(StopError::IPCError(err).into()); + } + } + + let now = Instant::now(); + while now.elapsed() < std::time::Duration::from_secs(3) { + let lock_file = acquire_worker_lock(runtime_dir); + if lock_file.is_some() { + return Ok(()); + } + + std::thread::sleep(std::time::Duration::from_millis(200)); + } + + Err(StopError::WorkerTimedOut.into()) +} + +#[derive(Error, Debug)] +pub enum StopError { + #[error("worker timed out")] + WorkerTimedOut, + + #[error("ipc error: `{0}`")] + IPCError(Error), +} diff --git a/espanso/src/cli/service/unix.rs b/espanso/src/cli/service/unix.rs new file mode 100644 index 0000000..6c3ffb5 --- /dev/null +++ b/espanso/src/cli/service/unix.rs @@ -0,0 +1,85 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019-2021 Federico Terzi + * + * espanso is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * espanso is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with espanso. If not, see . + */ + +use anyhow::Result; +use thiserror::Error; +use crate::cli::util::CommandExt; + +use crate::cli::PathsOverrides; + +pub fn fork_daemon(paths_overrides: &PathsOverrides) -> Result<()> { + let pid = unsafe { libc::fork() }; + if pid < 0 { + return Err(ForkError::ForkFailed.into()); + } + + if pid > 0 { + // Parent process + return Ok(()); + } + + // Spawned process + + // Create a new SID for the child process + let sid = unsafe { libc::setsid() }; + if sid < 0 { + return Err(ForkError::SetSidFailed.into()); + } + + // Detach stdout and stderr + let null_path = std::ffi::CString::new("/dev/null").expect("CString unwrap failed"); + unsafe { + let fd = libc::open(null_path.as_ptr(), libc::O_RDWR, 0); + if fd != -1 { + libc::dup2(fd, libc::STDIN_FILENO); + libc::dup2(fd, libc::STDOUT_FILENO); + libc::dup2(fd, libc::STDERR_FILENO); + } + }; + + spawn_launcher(paths_overrides) +} + +pub fn spawn_launcher(paths_overrides: &PathsOverrides) -> Result<()> { + let espanso_exe_path = std::env::current_exe()?; + let mut command = std::process::Command::new(&espanso_exe_path.to_string_lossy().to_string()); + command.args(&["launcher"]); + command.with_paths_overrides(&paths_overrides); + + let mut child = command.spawn()?; + let result = child.wait()?; + + if result.success() { + Ok(()) + } else { + Err(ForkError::LauncherSpawnFailure.into()) + } +} + +#[derive(Error, Debug)] +pub enum ForkError { + #[error("unable to fork")] + ForkFailed, + + #[error("setsid failed")] + SetSidFailed, + + #[error("launcher spawn failure")] + LauncherSpawnFailure, +} diff --git a/espanso/src/cli/util.rs b/espanso/src/cli/util.rs new file mode 100644 index 0000000..e029a74 --- /dev/null +++ b/espanso/src/cli/util.rs @@ -0,0 +1,55 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019-2021 Federico Terzi + * + * espanso is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * espanso is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with espanso. If not, see . + */ + +use super::PathsOverrides; +use std::process::Command; + +pub trait CommandExt { + fn with_paths_overrides(&mut self, paths_overrides: &PathsOverrides) -> &mut Self; +} + +impl CommandExt for Command { + fn with_paths_overrides(&mut self, paths_overrides: &PathsOverrides) -> &mut Self { + // We only inject the paths that were explicitly overrided because otherwise + // the migration process might create some incompatibilities. + // For example, after the migration the "packages" dir could have been + // moved to a different one, and that might cause the daemon to crash + // if we inject the old packages dir that was automatically resolved. + if let Some(config_override) = &paths_overrides.config { + self.env( + "ESPANSO_CONFIG_DIR", + config_override.to_string_lossy().to_string(), + ); + } + if let Some(packages_override) = &paths_overrides.packages { + self.env( + "ESPANSO_PACKAGE_DIR", + packages_override.to_string_lossy().to_string(), + ); + } + if let Some(runtime_override) = &paths_overrides.runtime { + self.env( + "ESPANSO_RUNTIME_DIR", + runtime_override.to_string_lossy().to_string(), + ); + } + + self + } +} diff --git a/espanso/src/cli/worker/daemon_monitor.rs b/espanso/src/cli/worker/daemon_monitor.rs index da6ca9c..8e1d4a3 100644 --- a/espanso/src/cli/worker/daemon_monitor.rs +++ b/espanso/src/cli/worker/daemon_monitor.rs @@ -23,11 +23,11 @@ use anyhow::Result; use crossbeam::channel::Sender; use log::{error, info, warn}; -use crate::lock::acquire_daemon_lock; +use crate::{engine::event::ExitMode, lock::acquire_daemon_lock}; const DAEMON_STATUS_CHECK_INTERVAL: u64 = 1000; -pub fn initialize_and_spawn(runtime_dir: &Path, exit_notify: Sender<()>) -> Result<()> { +pub fn initialize_and_spawn(runtime_dir: &Path, exit_notify: Sender) -> Result<()> { let runtime_dir_clone = runtime_dir.to_path_buf(); std::thread::Builder::new() .name("daemon-monitor".to_string()) @@ -38,7 +38,7 @@ pub fn initialize_and_spawn(runtime_dir: &Path, exit_notify: Sender<()>) -> Resu Ok(()) } -fn daemon_monitor_main(runtime_dir: &Path, exit_notify: Sender<()>) { +fn daemon_monitor_main(runtime_dir: &Path, exit_notify: Sender) { info!("monitoring the status of the daemon process"); loop { @@ -49,7 +49,7 @@ fn daemon_monitor_main(runtime_dir: &Path, exit_notify: Sender<()>) { if is_daemon_lock_free { warn!("detected unexpected daemon termination, sending exit signal to the engine"); - if let Err(error) = exit_notify.send(()) { + if let Err(error) = exit_notify.send(ExitMode::Exit) { error!("unable to send daemon exit signal: {}", error); } break; diff --git a/espanso/src/cli/worker/engine/funnel/exit.rs b/espanso/src/cli/worker/engine/funnel/exit.rs index 2a50011..74da016 100644 --- a/espanso/src/cli/worker/engine/funnel/exit.rs +++ b/espanso/src/cli/worker/engine/funnel/exit.rs @@ -24,12 +24,12 @@ use crate::engine::{event::{Event, EventType, ExitMode}, funnel}; use super::sequencer::Sequencer; pub struct ExitSource<'a> { - pub exit_signal: Receiver<()>, + pub exit_signal: Receiver, pub sequencer: &'a Sequencer, } impl <'a> ExitSource<'a> { - pub fn new(exit_signal: Receiver<()>, sequencer: &'a Sequencer) -> Self { + pub fn new(exit_signal: Receiver, sequencer: &'a Sequencer) -> Self { ExitSource { exit_signal, sequencer, @@ -43,12 +43,12 @@ impl<'a> funnel::Source<'a> for ExitSource<'a> { } fn receive(&self, op: SelectedOperation) -> Event { - op + let mode = op .recv(&self.exit_signal) .expect("unable to select data from ExitSource receiver"); Event { source_id: self.sequencer.next_id(), - etype: EventType::ExitRequested(ExitMode::Exit), + etype: EventType::ExitRequested(mode), } } } \ No newline at end of file diff --git a/espanso/src/cli/worker/engine/mod.rs b/espanso/src/cli/worker/engine/mod.rs index 5cb0002..fa5fd1e 100644 --- a/espanso/src/cli/worker/engine/mod.rs +++ b/espanso/src/cli/worker/engine/mod.rs @@ -46,7 +46,7 @@ pub fn initialize_and_spawn( config_store: Box, match_store: Box, ui_remote: Box, - exit_signal: Receiver<()>, + exit_signal: Receiver, ui_event_receiver: Receiver, secure_input_receiver: Receiver, ) -> Result> { diff --git a/espanso/src/cli/worker/ipc.rs b/espanso/src/cli/worker/ipc.rs index cbdfa57..5384bac 100644 --- a/espanso/src/cli/worker/ipc.rs +++ b/espanso/src/cli/worker/ipc.rs @@ -24,9 +24,9 @@ use crossbeam::channel::Sender; use espanso_ipc::{EventHandlerResponse, IPCServer}; use log::{error, warn}; -use crate::ipc::IPCEvent; +use crate::{engine::event::ExitMode, ipc::IPCEvent}; -pub fn initialize_and_spawn(runtime_dir: &Path, exit_notify: Sender<()>) -> Result<()> { +pub fn initialize_and_spawn(runtime_dir: &Path, exit_notify: Sender) -> Result<()> { let server = crate::ipc::create_worker_ipc_server(runtime_dir)?; std::thread::Builder::new() @@ -35,7 +35,17 @@ pub fn initialize_and_spawn(runtime_dir: &Path, exit_notify: Sender<()>) -> Resu server.run(Box::new(move |event| { match event { IPCEvent::Exit => { - if let Err(err) = exit_notify.send(()) { + if let Err(err) = exit_notify.send(ExitMode::Exit) { + error!( + "experienced error while sending exit signal from worker ipc handler: {}", + err + ); + } + + EventHandlerResponse::NoResponse + } + IPCEvent::ExitAllProcesses => { + if let Err(err) = exit_notify.send(ExitMode::ExitAllProcesses) { error!( "experienced error while sending exit signal from worker ipc handler: {}", err diff --git a/espanso/src/exit_code.rs b/espanso/src/exit_code.rs index 58ccae7..b310c76 100644 --- a/espanso/src/exit_code.rs +++ b/espanso/src/exit_code.rs @@ -43,6 +43,12 @@ pub const ADD_TO_PATH_FAILURE: i32 = 1; pub const LAUNCHER_SUCCESS: i32 = 0; pub const LAUNCHER_CONFIG_DIR_POPULATION_FAILURE: i32 = 1; +pub const SERVICE_SUCCESS: i32 = 0; +pub const SERVICE_FAILURE: i32 = 1; +pub const SERVICE_NOT_REGISTERED: i32 = 2; +pub const SERVICE_ALREADY_RUNNING: i32 = 3; +pub const SERVICE_NOT_RUNNING: i32 = 4; + use std::sync::Mutex; lazy_static! { diff --git a/espanso/src/ipc.rs b/espanso/src/ipc.rs index d405013..418ec36 100644 --- a/espanso/src/ipc.rs +++ b/espanso/src/ipc.rs @@ -25,6 +25,7 @@ use serde::{Serialize, Deserialize}; #[derive(Debug, Serialize, Deserialize)] pub enum IPCEvent { Exit, + ExitAllProcesses, } pub fn create_daemon_ipc_server(runtime_dir: &Path) -> Result> { diff --git a/espanso/src/logging/mod.rs b/espanso/src/logging/mod.rs index bbab677..69ea0ff 100644 --- a/espanso/src/logging/mod.rs +++ b/espanso/src/logging/mod.rs @@ -112,3 +112,19 @@ impl Write for FileProxy { } } } + +#[macro_export] +macro_rules! info_println { + ($($tts:tt)*) => { + println!($($tts)*); + log::info!($($tts)*); + } +} + +#[macro_export] +macro_rules! error_eprintln { + ($($tts:tt)*) => { + eprintln!($($tts)*); + log::error!($($tts)*); + } +} \ No newline at end of file diff --git a/espanso/src/main.rs b/espanso/src/main.rs index e708277..93d28c4 100644 --- a/espanso/src/main.rs +++ b/espanso/src/main.rs @@ -43,6 +43,7 @@ mod gui; mod icon; mod ipc; mod lock; +#[macro_use] mod logging; mod path; mod preferences; @@ -61,6 +62,7 @@ lazy_static! { cli::modulo::new(), cli::migrate::new(), cli::env_path::new(), + cli::service::new(), ]; } @@ -168,7 +170,7 @@ fn main() { Arg::with_name("show-welcome") .long("show-welcome") .required(false) - .takes_value(false) + .takes_value(false), ) .about("Start the daemon without spawning a new process."), ) @@ -215,10 +217,7 @@ fn main() { .help("Interpret the input data as JSON"), ), ) - .subcommand( - SubCommand::with_name("welcome") - .about("Display the welcome screen") - ), + .subcommand(SubCommand::with_name("welcome").about("Display the welcome screen")), ) // .subcommand(SubCommand::with_name("start") // .about("Start the daemon spawning a new process in the background.")) @@ -254,6 +253,30 @@ fn main() { .arg(Arg::with_name("noconfirm").long("noconfirm")) .help("Migrate the configuration without asking for confirmation"), ) + .subcommand( + SubCommand::with_name("service") + .subcommand(SubCommand::with_name("register").about("Register espanso as a system service")) + .subcommand( + SubCommand::with_name("unregister").about("Unregister espanso from system services"), + ) + .subcommand( + SubCommand::with_name("check") + .about("Check if espanso is registered as a system service"), + ) + .subcommand( + SubCommand::with_name("start") + .about("Start espanso as a service") + .arg( + Arg::with_name("unmanaged") + .long("unmanaged") + .required(false) + .takes_value(false) + .help("Run espanso as an unmanaged service (avoid system manager)"), + ), + ) + .subcommand(SubCommand::with_name("stop").about("Stop espanso service")) + .about("Register and manage 'espanso' as a system service."), + ) // .subcommand(SubCommand::with_name("match") // .about("List and execute matches from the CLI") // .subcommand(SubCommand::with_name("list") diff --git a/espanso/src/res/macos/com.federicoterzi.espanso.plist b/espanso/src/res/macos/com.federicoterzi.espanso.plist new file mode 100644 index 0000000..cacf47b --- /dev/null +++ b/espanso/src/res/macos/com.federicoterzi.espanso.plist @@ -0,0 +1,24 @@ + + + + + Label + com.federicoterzi.espanso + EnvironmentVariables + + PATH + {{{PATH}}} + + ProgramArguments + + {{{espanso_path}}} + launcher + + RunAtLoad + + StandardErrorPath + /tmp/espanso.err + StandardOutPath + /tmp/espanso.out + + \ No newline at end of file