feat(core): implement service cli handler on macOS
This commit is contained in:
parent
d9f275895b
commit
59a405a21d
5
Cargo.lock
generated
5
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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" }
|
|
@ -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()?;
|
||||
|
|
|
@ -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 {
|
||||
|
|
166
espanso/src/cli/service/macos.rs
Normal file
166
espanso/src/cli/service/macos.rs
Normal file
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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),
|
||||
}
|
118
espanso/src/cli/service/mod.rs
Normal file
118
espanso/src/cli/service/mod.rs
Normal file
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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
|
||||
}
|
69
espanso/src/cli/service/stop.rs
Normal file
69
espanso/src/cli/service/stop.rs
Normal file
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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),
|
||||
}
|
85
espanso/src/cli/service/unix.rs
Normal file
85
espanso/src/cli/service/unix.rs
Normal file
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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,
|
||||
}
|
55
espanso/src/cli/util.rs
Normal file
55
espanso/src/cli/util.rs
Normal file
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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
|
||||
}
|
||||
}
|
|
@ -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<ExitMode>) -> 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<ExitMode>) {
|
||||
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;
|
||||
|
|
|
@ -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<ExitMode>,
|
||||
pub sequencer: &'a Sequencer,
|
||||
}
|
||||
|
||||
impl <'a> ExitSource<'a> {
|
||||
pub fn new(exit_signal: Receiver<()>, sequencer: &'a Sequencer) -> Self {
|
||||
pub fn new(exit_signal: Receiver<ExitMode>, 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),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -46,7 +46,7 @@ pub fn initialize_and_spawn(
|
|||
config_store: Box<dyn ConfigStore>,
|
||||
match_store: Box<dyn MatchStore>,
|
||||
ui_remote: Box<dyn UIRemote>,
|
||||
exit_signal: Receiver<()>,
|
||||
exit_signal: Receiver<ExitMode>,
|
||||
ui_event_receiver: Receiver<UIEvent>,
|
||||
secure_input_receiver: Receiver<SecureInputEvent>,
|
||||
) -> Result<JoinHandle<ExitMode>> {
|
||||
|
|
|
@ -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<ExitMode>) -> 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
|
||||
|
|
|
@ -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! {
|
||||
|
|
|
@ -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<impl IPCServer<IPCEvent>> {
|
||||
|
|
|
@ -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)*);
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
|
|
24
espanso/src/res/macos/com.federicoterzi.espanso.plist
Normal file
24
espanso/src/res/macos/com.federicoterzi.espanso.plist
Normal file
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.federicoterzi.espanso</string>
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>PATH</key>
|
||||
<string>{{{PATH}}}</string>
|
||||
</dict>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>{{{espanso_path}}}</string>
|
||||
<string>launcher</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>/tmp/espanso.err</string>
|
||||
<key>StandardOutPath</key>
|
||||
<string>/tmp/espanso.out</string>
|
||||
</dict>
|
||||
</plist>
|
Loading…
Reference in New Issue
Block a user