feat(core): implement linux service methods

This commit is contained in:
Federico Terzi 2021-07-14 22:35:45 +02:00
parent 40a307c38d
commit fd2f385858
4 changed files with 253 additions and 18 deletions

22
Cargo.lock generated
View File

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

View File

@ -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"
caps = "0.5.2"
const_format = "0.2.14"
regex = "1.4.3"

View File

@ -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<path>.*?)\\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<PathBuf> {
// 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<PathBuf> {
Ok(get_service_file_dir()?.join(LINUX_SERVICE_FILENAME))
}

View File

@ -0,0 +1,11 @@
[Unit]
Description=espanso
[Service]
ExecStart={{{espanso_path}}} launcher
Restart=on-failure
RestartSec=3
[Install]
WantedBy=default.target