From fc744833696f33bfef14c77035e255dd140f969b Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Fri, 11 Oct 2019 23:35:17 +0200 Subject: [PATCH] Add systemd integration on Linux. Fix #80 --- native/liblinuxbridge/bridge.cpp | 12 +++- native/liblinuxbridge/bridge.h | 5 ++ src/bridge/linux.rs | 1 + src/context/linux.rs | 13 ++++- src/main.rs | 53 ++++++++++++++++-- src/res/linux/systemd.service | 11 ++++ src/sysdaemon.rs | 96 ++++++++++++++++++++++++++++++-- 7 files changed, 180 insertions(+), 11 deletions(-) create mode 100644 src/res/linux/systemd.service diff --git a/native/liblinuxbridge/bridge.cpp b/native/liblinuxbridge/bridge.cpp index 3f6369e..c5b3c42 100644 --- a/native/liblinuxbridge/bridge.cpp +++ b/native/liblinuxbridge/bridge.cpp @@ -84,6 +84,16 @@ void register_keypress_callback(KeypressCallback callback) { keypress_callback = callback; } +int32_t check_x11() { + Display *check_disp = XOpenDisplay(NULL); + + if (!check_disp) { + return -1; + } + + XCloseDisplay(check_disp); + return 1; +} int32_t initialize(void * _context_instance) { setlocale(LC_ALL, ""); @@ -395,4 +405,4 @@ int32_t is_current_window_terminal() { } return 0; -} +} \ No newline at end of file diff --git a/native/liblinuxbridge/bridge.h b/native/liblinuxbridge/bridge.h index 89d60aa..25c78e7 100644 --- a/native/liblinuxbridge/bridge.h +++ b/native/liblinuxbridge/bridge.h @@ -24,6 +24,11 @@ extern void * context_instance; +/* + * Check if the X11 context is available + */ +extern "C" int32_t check_x11(); + /* * Initialize the X11 context and parameters */ diff --git a/src/bridge/linux.rs b/src/bridge/linux.rs index a7f40e3..d85156b 100644 --- a/src/bridge/linux.rs +++ b/src/bridge/linux.rs @@ -22,6 +22,7 @@ use std::os::raw::{c_void, c_char}; #[allow(improper_ctypes)] #[link(name="linuxbridge", kind="static")] extern { + pub fn check_x11() -> i32; pub fn initialize(s: *const c_void) -> i32; pub fn eventloop(); pub fn cleanup(); diff --git a/src/context/linux.rs b/src/context/linux.rs index f0ccab7..2595d1b 100644 --- a/src/context/linux.rs +++ b/src/context/linux.rs @@ -23,8 +23,9 @@ use crate::event::*; use crate::event::KeyModifier::*; use crate::bridge::linux::*; use std::process::exit; -use log::error; +use log::{error, info}; use std::ffi::CStr; +use std::{thread, time}; #[repr(C)] pub struct LinuxContext { @@ -33,6 +34,16 @@ pub struct LinuxContext { impl LinuxContext { pub fn new(send_channel: Sender) -> Box { + // Check if the X11 context is available + let x11_available = unsafe { + check_x11() + }; + + if x11_available < 0 { + error!("Error, can't connect to X11 context"); + std::process::exit(100); + } + let context = Box::new(LinuxContext { send_channel, }); diff --git a/src/main.rs b/src/main.rs index 8b8da15..328e638 100644 --- a/src/main.rs +++ b/src/main.rs @@ -102,9 +102,9 @@ fn main() { .subcommand(SubCommand::with_name("daemon") .about("Start the daemon without spawning a new process.")) .subcommand(SubCommand::with_name("register") - .about("MacOS only. Register espanso in the system daemon manager.")) + .about("MacOS and Linux only. Register espanso in the system daemon manager.")) .subcommand(SubCommand::with_name("unregister") - .about("MacOS only. Unregister espanso from the system daemon manager.")) + .about("MacOS and Linux only. Unregister espanso from the system daemon manager.")) .subcommand(SubCommand::with_name("log") .about("Print the latest daemon logs.")) .subcommand(SubCommand::with_name("start") @@ -382,10 +382,10 @@ fn start_daemon(config_set: ConfigSet) { if status.success() { println!("Daemon started correctly!") }else{ - println!("Error starting launchd daemon with status: {}", status); + eprintln!("Error starting launchd daemon with status: {}", status); } }else{ - println!("Error starting launchd daemon: {}", res.unwrap_err()); + eprintln!("Error starting launchd daemon: {}", res.unwrap_err()); } }else{ fork_daemon(config_set); @@ -394,7 +394,50 @@ fn start_daemon(config_set: ConfigSet) { #[cfg(target_os = "linux")] fn start_daemon(config_set: ConfigSet) { - fork_daemon(config_set); + if config_set.default.use_system_agent { + use std::process::Command; + + // Make sure espanso is currently registered in systemd + let res = Command::new("systemctl") + .args(&["--user", "is-enabled", "espanso.service"]) + .status(); + if !res.unwrap().success() { + use std::io::{self, BufRead}; + eprintln!("espanso must be registered to systemd (user level) first."); + eprint!("Do you want to proceed? [Y/n]: "); + + let mut line = String::new(); + let stdin = io::stdin(); + stdin.lock().read_line(&mut line).unwrap(); + let answer = line.trim().to_lowercase(); + if answer != "n" { + register_main(config_set); + }else{ + eprintln!("Please register espanso to systemd with this command:"); + eprintln!(" espanso register"); + // TODO: enable flag to use non-managed daemon mode + + std::process::exit(4); + } + } + + // Start the espanso service + let res = Command::new("systemctl") + .args(&["--user", "start", "espanso.service"]) + .status(); + + if let Ok(status) = res { + if status.success() { + println!("Daemon started correctly!") + }else{ + eprintln!("Error starting systemd daemon with status: {}", status); + } + }else{ + eprintln!("Error starting systemd daemon: {}", res.unwrap_err()); + } + }else{ + fork_daemon(config_set); + } } #[cfg(not(target_os = "windows"))] diff --git a/src/res/linux/systemd.service b/src/res/linux/systemd.service new file mode 100644 index 0000000..4fdd26e --- /dev/null +++ b/src/res/linux/systemd.service @@ -0,0 +1,11 @@ +[Unit] +Description=espanso daemon + +[Service] +ExecStart={{{espanso_path}}} daemon +Restart=on-failure +RestartSec=3 + +[Install] +WantedBy=default.target + diff --git a/src/sysdaemon.rs b/src/sysdaemon.rs index 23443fd..4f932d3 100644 --- a/src/sysdaemon.rs +++ b/src/sysdaemon.rs @@ -102,13 +102,101 @@ pub fn unregister(_config_set: ConfigSet) { // LINUX #[cfg(target_os = "linux")] -pub fn register(_config_set: ConfigSet) { - println!("Linux does not support automatic system daemon integration."); +const LINUX_SERVICE_CONTENT : &str = include_str!("res/linux/systemd.service"); +#[cfg(target_os = "linux")] +const LINUX_SERVICE_FILENAME : &str = "espanso.service"; + +#[cfg(target_os = "linux")] +pub fn register(config_set: ConfigSet) { + use std::fs::create_dir_all; + use std::process::{Command, ExitStatus}; + + // Check if espanso service is already registered + let res = Command::new("systemctl") + .args(&["--user", "is-enabled", "espanso"]) + .output(); + if let Ok(res) = res { + let output = String::from_utf8_lossy(res.stdout.as_slice()); + let output = output.trim(); + if res.status.success() && output == "enabled" { + eprintln!("espanso service is already registered to systemd"); + eprintln!("If you want to register it again, please uninstall it first with:"); + eprintln!(" espanso unregister"); + std::process::exit(5); + } + } + + // 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"); + + // Make sure the directory exists + if !user_dir.exists() { + create_dir_all(user_dir.clone()).expect("Could not create systemd user directory"); + } + + let service_file = user_dir.join(LINUX_SERVICE_FILENAME); + if !service_file.exists() { + println!("Creating service entry: {}", service_file.to_str().unwrap_or_default()); + + let espanso_path = std::env::current_exe().expect("Could not get espanso executable path"); + println!("Entry will point to: {}", espanso_path.to_str().unwrap_or_default()); + + let service_content = String::from(LINUX_SERVICE_CONTENT) + .replace("{{{espanso_path}}}", espanso_path.to_str().unwrap_or_default()); + + std::fs::write(service_file.clone(), service_content).expect("Unable to write service file"); + + println!("Service file created correctly!") + } + + println!("Enabling espanso for systemd..."); + + let res = Command::new("systemctl") + .args(&["--user", "enable", "espanso"]) + .status(); + + if let Ok(status) = res { + if status.success() { + println!("Service registered correctly!") + } + }else{ + println!("Error loading espanso service"); + } } #[cfg(target_os = "linux")] -pub fn unregister(_config_set: ConfigSet) { - println!("Linux does not support automatic system daemon integration."); +pub fn unregister(config_set: ConfigSet) { + use std::process::{Command, ExitStatus}; + + // Disable the service first + let res = Command::new("systemctl") + .args(&["--user", "disable", "espanso"]) + .status(); + + // Then delete the espanso.service entry + 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"); + let service_file = user_dir.join(LINUX_SERVICE_FILENAME); + + if service_file.exists() { + let res = std::fs::remove_file(&service_file); + match res { + Ok(_) => { + println!("Deleted entry at {}", service_file.to_string_lossy()); + println!("Service unregistered successfully!"); + }, + Err(e) => { + println!("Error, could not delete service entry at {} with error {}", + service_file.to_string_lossy(), e); + }, + } + }else{ + eprintln!("Error, could not find espanso service file"); + } } // WINDOWS