diff --git a/src/service.rs b/src/service.rs index 66b22eb..82478f7 100644 --- a/src/service.rs +++ b/src/service.rs @@ -1,30 +1,44 @@ //! User-level daemon lifecycle integration for Appa. +#[cfg(target_os = "linux")] +use std::process::Stdio; use std::{ env, fs, path::{Path, PathBuf}, - process::{Command, Stdio}, + process::Command, }; +#[cfg(target_os = "linux")] use anyhow::Context; use directories::BaseDirs; use crate::storage::AppPaths; +#[cfg(target_os = "linux")] const UNIT_NAME: &str = "appa.service"; #[cfg(target_os = "macos")] const LAUNCHD_LABEL: &str = "dev.appa.appad"; +#[cfg(target_os = "linux")] const JOURNAL_LOG_LINE_COUNT: &str = "100"; const RESTART_DELAY_SECONDS: u8 = 5; -#[allow(unreachable_code)] +#[cfg(target_os = "linux")] pub fn install() -> anyhow::Result { - #[cfg(target_os = "macos")] - { - return install_launch_agent(); - } - require_linux()?; + install_systemd_service() +} + +#[cfg(target_os = "macos")] +pub fn install() -> anyhow::Result { + install_launch_agent() +} + +#[cfg(not(any(target_os = "linux", target_os = "macos")))] +pub fn install() -> anyhow::Result { + anyhow::bail!("Appa services are supported on Linux and macOS") +} +#[cfg(target_os = "linux")] +fn install_systemd_service() -> anyhow::Result { let unit_file_path = unit_path()?; let executable = env::current_exe()?; let app_paths = AppPaths::discover()?; @@ -46,19 +60,27 @@ pub fn install() -> anyhow::Result { Ok(unit_file_path) } -#[allow(unreachable_code)] +#[cfg(target_os = "linux")] pub fn status() -> anyhow::Result<()> { - #[cfg(target_os = "macos")] - { - let status = Command::new("launchctl") - .args(["print", &launchd_service_target()?]) - .status()?; - if status.success() { - return Ok(()); - } - anyhow::bail!("launchctl reported that Appa is not running") - } - require_linux()?; + status_systemd_service() +} + +#[cfg(target_os = "macos")] +pub fn status() -> anyhow::Result<()> { + let target = launchd_service_target()?; + run_macos_command( + Command::new("launchctl").args(["print", &target]), + "launchctl reported that Appa is not running", + ) +} + +#[cfg(not(any(target_os = "linux", target_os = "macos")))] +pub fn status() -> anyhow::Result<()> { + anyhow::bail!("Appa services are supported on Linux and macOS") +} + +#[cfg(target_os = "linux")] +fn status_systemd_service() -> anyhow::Result<()> { let status = Command::new("systemctl") .args(["--user", "status", UNIT_NAME]) .stdin(Stdio::null()) @@ -69,43 +91,55 @@ pub fn status() -> anyhow::Result<()> { anyhow::bail!("systemctl reported that Appa is not running") } -#[allow(unreachable_code)] +#[cfg(target_os = "linux")] pub fn restart() -> anyhow::Result<()> { - #[cfg(target_os = "macos")] - { - let status = Command::new("launchctl") - .args(["kickstart", "-k", &launchd_service_target()?]) - .status()?; - if status.success() { - return Ok(()); - } - anyhow::bail!("launchctl could not restart Appa") - } - require_linux()?; run_systemctl(&["restart", UNIT_NAME]) } -#[allow(unreachable_code)] +#[cfg(target_os = "macos")] +pub fn restart() -> anyhow::Result<()> { + let target = launchd_service_target()?; + run_macos_command( + Command::new("launchctl").args(["kickstart", "-k", &target]), + "launchctl could not restart Appa", + ) +} + +#[cfg(not(any(target_os = "linux", target_os = "macos")))] +pub fn restart() -> anyhow::Result<()> { + anyhow::bail!("Appa services are supported on Linux and macOS") +} + +#[cfg(target_os = "linux")] pub fn logs() -> anyhow::Result<()> { - #[cfg(target_os = "macos")] - { - let status = Command::new("log") - .args([ - "show", - "--style", - "compact", - "--last", - "1h", - "--predicate", - &format!("process == '{LAUNCHD_LABEL}'"), - ]) - .status()?; - if status.success() { - return Ok(()); - } - anyhow::bail!("macOS log could not read Appa daemon logs") - } - require_linux()?; + logs_from_journal() +} + +#[cfg(target_os = "macos")] +pub fn logs() -> anyhow::Result<()> { + const MACOS_LOG_WINDOW: &str = "1h"; + let predicate = format!("process == '{LAUNCHD_LABEL}'"); + run_macos_command( + Command::new("log").args([ + "show", + "--style", + "compact", + "--last", + MACOS_LOG_WINDOW, + "--predicate", + &predicate, + ]), + "macOS log could not read Appa daemon logs", + ) +} + +#[cfg(not(any(target_os = "linux", target_os = "macos")))] +pub fn logs() -> anyhow::Result<()> { + anyhow::bail!("Appa services are supported on Linux and macOS") +} + +#[cfg(target_os = "linux")] +fn logs_from_journal() -> anyhow::Result<()> { let status = Command::new("journalctl") .args([ "--user-unit", @@ -121,14 +155,23 @@ pub fn logs() -> anyhow::Result<()> { anyhow::bail!("journalctl could not read Appa service logs") } -#[allow(unreachable_code)] +#[cfg(target_os = "linux")] pub fn uninstall() -> anyhow::Result { - #[cfg(target_os = "macos")] - { - return uninstall_launch_agent(); - } - require_linux()?; + uninstall_systemd_service() +} + +#[cfg(target_os = "macos")] +pub fn uninstall() -> anyhow::Result { + uninstall_launch_agent() +} +#[cfg(not(any(target_os = "linux", target_os = "macos")))] +pub fn uninstall() -> anyhow::Result { + anyhow::bail!("Appa services are supported on Linux and macOS") +} + +#[cfg(target_os = "linux")] +fn uninstall_systemd_service() -> anyhow::Result { let unit_path = unit_path()?; if !unit_path.exists() { return Ok(false); @@ -139,17 +182,7 @@ pub fn uninstall() -> anyhow::Result { Ok(true) } -fn require_linux() -> anyhow::Result<()> { - if is_linux() { - return Ok(()); - } - anyhow::bail!("Appa service commands are only supported on Linux with systemd") -} - -fn is_linux() -> bool { - cfg!(target_os = "linux") -} - +#[cfg(target_os = "linux")] fn unit_path() -> anyhow::Result { let config_directory = match env::var_os("XDG_CONFIG_HOME") { Some(path) => PathBuf::from(path), @@ -161,6 +194,7 @@ fn unit_path() -> anyhow::Result { Ok(config_directory.join("systemd/user").join(UNIT_NAME)) } +#[cfg(target_os = "linux")] fn run_systemctl(arguments: &[&str]) -> anyhow::Result<()> { let status = Command::new("systemctl") .arg("--user") @@ -172,11 +206,13 @@ fn run_systemctl(arguments: &[&str]) -> anyhow::Result<()> { anyhow::bail!("systemctl --user {} failed", arguments.join(" ")) } +#[cfg(target_os = "linux")] fn install_and_start_unit() -> anyhow::Result<()> { run_systemctl(&["daemon-reload"])?; run_systemctl(&["enable", "--now", UNIT_NAME]) } +#[cfg(target_os = "linux")] fn read_optional_file(path: &Path) -> anyhow::Result>> { match fs::read(path) { Ok(contents) => Ok(Some(contents)), @@ -185,6 +221,7 @@ fn read_optional_file(path: &Path) -> anyhow::Result>> { } } +#[cfg(target_os = "linux")] fn restore_unit_file(path: &Path, previous_contents: Option>) -> anyhow::Result<()> { match previous_contents { Some(contents) => fs::write(path, contents) @@ -199,6 +236,7 @@ fn restore_unit_file(path: &Path, previous_contents: Option>) -> anyhow: } } +#[cfg(target_os = "linux")] fn unit_contents(executable: &Path, data_directory: &Path) -> anyhow::Result { let executable = systemd_argument(executable)?; let data_directory = systemd_argument(data_directory)?; @@ -210,6 +248,7 @@ fn unit_contents(executable: &Path, data_directory: &Path) -> anyhow::Result anyhow::Result { let path = path .to_str() @@ -298,6 +337,15 @@ fn launchd_user_domain() -> anyhow::Result { Ok(format!("gui/{}", String::from_utf8(output.stdout)?.trim())) } +#[cfg(target_os = "macos")] +fn run_macos_command(command: &mut Command, failure_message: &str) -> anyhow::Result<()> { + let status = command.status()?; + if status.success() { + return Ok(()); + } + anyhow::bail!("{failure_message}") +} + #[cfg(target_os = "macos")] fn launch_agent_contents(executable: &Path, data_directory: &Path) -> anyhow::Result { let executable = xml_escape(executable)?; @@ -320,7 +368,7 @@ fn xml_escape(path: &Path) -> anyhow::Result { .replace('\'', "'")) } -#[cfg(test)] +#[cfg(all(test, target_os = "linux"))] mod tests { use std::path::Path;