feat(core): implement service cli handler on macOS

This commit is contained in:
Federico Terzi 2021-07-11 22:26:40 +02:00
parent d9f275895b
commit 59a405a21d
19 changed files with 601 additions and 44 deletions

5
Cargo.lock generated
View File

@ -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"

View File

@ -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"]

View File

@ -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" }

View File

@ -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()?;

View File

@ -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 {

View 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),
}

View 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
}

View 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),
}

View 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
View 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
}
}

View File

@ -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;

View File

@ -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),
}
}
}

View File

@ -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>> {

View File

@ -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

View File

@ -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! {

View File

@ -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>> {

View File

@ -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)*);
}
}

View File

@ -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")

View 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>