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