diff --git a/Cargo.lock b/Cargo.lock index 60799b2..a8eb92c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -221,6 +221,26 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28b9d6de7f49e22cf97ad17fc4036ece69300032f45f78f30b4a4482cdc3f4a6" +[[package]] +name = "const_format" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ea7d6aeb2ebd1ee24f7b7e1b23242ef5a56b3a693733b99bfbe5ef31d0306" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c36c619c422113552db4eb28cddba8faa757e33f758cc3415bd2885977b591" +dependencies = [ + "proc-macro2", + "quote 1.0.9", + "unicode-xid 0.2.1", +] + [[package]] name = "constant_time_eq" version = "0.1.5" @@ -452,6 +472,7 @@ dependencies = [ "caps", "clap", "colored", + "const_format", "crossbeam", "dialoguer", "dirs 3.0.1", @@ -481,6 +502,7 @@ dependencies = [ "markdown", "named_pipe", "notify", + "regex", "serde", "serde_json", "serde_yaml", diff --git a/espanso/Cargo.toml b/espanso/Cargo.toml index 751243b..b518282 100644 --- a/espanso/Cargo.toml +++ b/espanso/Cargo.toml @@ -69,4 +69,6 @@ libc = "0.2.98" espanso-mac-utils = { path = "../espanso-mac-utils" } [target.'cfg(target_os="linux")'.dependencies] -caps = "0.5.2" \ No newline at end of file +caps = "0.5.2" +const_format = "0.2.14" +regex = "1.4.3" \ No newline at end of file diff --git a/espanso/src/cli/service/linux.rs b/espanso/src/cli/service/linux.rs index f789000..c0195b5 100644 --- a/espanso/src/cli/service/linux.rs +++ b/espanso/src/cli/service/linux.rs @@ -18,40 +18,216 @@ */ use anyhow::Result; -use log::{info, warn}; -use std::process::Command; -use std::{fs::create_dir_all, process::ExitStatus}; +use const_format::formatcp; +use regex::Regex; +use std::path::PathBuf; +use std::process::{Command, Stdio}; +use std::{fs::create_dir_all}; use thiserror::Error; -// const SERVICE_PLIST_CONTENT: &str = include_str!("../../res/macos/com.federicoterzi.espanso.plist"); -// const SERVICE_PLIST_FILE_NAME: &str = "com.federicoterzi.espanso.plist"; +use crate::{error_eprintln, info_println, warn_eprintln}; + +const LINUX_SERVICE_NAME: &str = "espanso"; +const LINUX_SERVICE_CONTENT: &str = include_str!("../../res/linux/systemd.service"); +const LINUX_SERVICE_FILENAME: &str = &formatcp!("{}.service", LINUX_SERVICE_NAME); pub fn register() -> Result<()> { - todo!(); + let service_file = get_service_file_path()?; + + if service_file.exists() { + warn_eprintln!("service file already exists, this operation will overwrite it"); + } + + info_println!("creating service file in {:?}", service_file); + let espanso_path = std::env::current_exe().expect("unable to get espanso executable path"); + + let service_content = String::from(LINUX_SERVICE_CONTENT).replace( + "{{{espanso_path}}}", + &espanso_path.to_string_lossy().to_string(), + ); + + std::fs::write(service_file, service_content)?; + + info_println!("enabling systemd service"); + + match Command::new("systemctl") + .args(&["--user", "enable", LINUX_SERVICE_NAME]) + .status() + { + Ok(status) => { + if !status.success() { + return Err(RegisterError::SystemdEnableFailed.into()); + } + } + Err(err) => { + error_eprintln!("unable to call systemctl: {}", err); + return Err(RegisterError::SystemdCallFailed(err.into()).into()); + } + } + + Ok(()) } #[derive(Error, Debug)] pub enum RegisterError { - #[error("launchctl load failed")] - LaunchCtlLoadFailed, + #[error("systemctl command failed `{0}`")] + SystemdCallFailed(anyhow::Error), + + #[error("systemctl enable failed")] + SystemdEnableFailed, } pub fn unregister() -> Result<()> { - todo!(); + let service_file = get_service_file_path()?; + + if !service_file.exists() { + return Err(UnregisterError::ServiceFileNotFound.into()); + } + + info_println!("disabling espanso systemd service"); + + match Command::new("systemctl") + .args(&["--user", "disable", LINUX_SERVICE_NAME]) + .status() + { + Ok(status) => { + if !status.success() { + return Err(UnregisterError::SystemdDisableFailed.into()); + } + } + Err(err) => { + error_eprintln!("unable to call systemctl: {}", err); + return Err(UnregisterError::SystemdCallFailed(err.into()).into()); + } + } + + info_println!("deleting espanso systemd entry"); + std::fs::remove_file(service_file)?; + + Ok(()) } #[derive(Error, Debug)] pub enum UnregisterError { - #[error("plist entry not found")] - PlistNotFound, + #[error("service file not found")] + ServiceFileNotFound, + + #[error("failed to disable systemd service")] + SystemdDisableFailed, + + #[error("systemctl command failed `{0}`")] + SystemdCallFailed(anyhow::Error), } pub fn is_registered() -> bool { - todo!(); + let res = Command::new("systemctl") + .args(&["--user", "is-enabled", LINUX_SERVICE_NAME]) + .output(); + if let Ok(output) = res { + if !output.status.success() { + return false; + } + + // Make sure the systemd service points to the right binary + lazy_static! { + static ref EXEC_PATH_REGEX: Regex = Regex::new("ExecStart=(?P.*?)\\s").unwrap(); + } + + match Command::new("systemctl") + .args(&["--user", "cat", LINUX_SERVICE_NAME]) + .output() + { + Ok(cmd_output) => { + let output = String::from_utf8_lossy(cmd_output.stdout.as_slice()); + let output = output.trim(); + if cmd_output.status.success() { + let caps = EXEC_PATH_REGEX.captures(output).unwrap(); + let path = caps.get(1).map_or("", |m| m.as_str()); + let espanso_path = + std::env::current_exe().expect("unable to get espanso executable path"); + + if espanso_path.to_string_lossy() != path { + error_eprintln!("Espanso is registered as a systemd service, but it points to another binary location:"); + error_eprintln!(""); + error_eprintln!(" {}", path); + error_eprintln!(""); + error_eprintln!("This could have been caused by an update that changed its location."); + error_eprintln!("To solve the problem, please unregister and register espanso again with these commands:"); + error_eprintln!(""); + error_eprintln!(" espanso service unregister && espanso service register"); + error_eprintln!(""); + + false + } else { + true + } + } else { + error_eprintln!("systemctl command returned non-zero exit code"); + false + } + } + Err(err) => { + error_eprintln!("failed to execute systemctl: {}", err); + false + } + } + } else { + false + } } pub fn start_service() -> Result<()> { - todo!(); + // Check if systemd is available in the system + match Command::new("systemctl") + .args(&["--version"]) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .status() + { + Ok(status) => { + if !status.success() { + return Err(StartError::SystemctlNonZeroExitCode.into()); + } + } + Err(_) => { + error_eprintln!( + "Systemd was not found in this system, which means espanso can't run in managed mode" + ); + error_eprintln!("You can run it in unmanaged mode with `espanso service start --unmanaged`"); + error_eprintln!(""); + error_eprintln!("NOTE: unmanaged mode means espanso does not rely on the system service manager"); + error_eprintln!(" to run, but as a result, you are in charge of starting/stopping espanso"); + error_eprintln!(" when needed."); + return Err(StartError::SystemdNotFound.into()); + } + } + + if !is_registered() { + error_eprintln!("Unable to start espanso as a service as it's not been registered."); + error_eprintln!("You can either register it first with `espanso service register` or"); + error_eprintln!("you can run it in unmanaged mode with `espanso service start --unmanaged`"); + error_eprintln!(""); + error_eprintln!("NOTE: unmanaged mode means espanso does not rely on the system service manager"); + error_eprintln!(" to run, but as a result, you are in charge of starting/stopping espanso"); + error_eprintln!(" when needed."); + return Err(StartError::NotRegistered.into()); + } + + match Command::new("systemctl") + .args(&["--user", "start", LINUX_SERVICE_NAME]) + .status() + { + Ok(status) => { + if !status.success() { + return Err(StartError::SystemctlStartFailed.into()); + } + } + Err(err) => { + return Err(StartError::SystemctlFailed(err.into()).into()); + } + } + + Ok(()) } #[derive(Error, Debug)] @@ -59,9 +235,33 @@ pub enum StartError { #[error("not registered as a service")] NotRegistered, - #[error("launchctl failed to run")] - LaunchCtlFailure, + #[error("systemd not found")] + SystemdNotFound, - #[error("launchctl exited with non-zero code `{0}`")] - LaunchCtlNonZeroExit(ExitStatus), + #[error("failed to start systemctl: `{0}`")] + SystemctlFailed(anyhow::Error), + + #[error("systemctl non-zero exit code")] + SystemctlNonZeroExitCode, + + #[error("failed to launch espanso service through systemctl")] + SystemctlStartFailed, +} + +fn get_service_file_dir() -> Result { + // User level systemd services should be placed in this directory: + // $XDG_CONFIG_HOME/systemd/user/, usually: ~/.config/systemd/user/ + let config_dir = dirs::config_dir().expect("Could not get configuration directory"); + let systemd_dir = config_dir.join("systemd"); + let user_dir = systemd_dir.join("user"); + + if !user_dir.is_dir() { + create_dir_all(&user_dir)?; + } + + Ok(user_dir) +} + +fn get_service_file_path() -> Result { + Ok(get_service_file_dir()?.join(LINUX_SERVICE_FILENAME)) } diff --git a/espanso/src/res/linux/systemd.service b/espanso/src/res/linux/systemd.service new file mode 100644 index 0000000..79e9078 --- /dev/null +++ b/espanso/src/res/linux/systemd.service @@ -0,0 +1,11 @@ +[Unit] +Description=espanso + +[Service] +ExecStart={{{espanso_path}}} launcher +Restart=on-failure +RestartSec=3 + +[Install] +WantedBy=default.target +