use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
const LABEL: &str = "ninja.kunobi.kache";
const PLIST_NAME: &str = "ninja.kunobi.kache.plist";
const LEGACY_LABEL: &str = "com.zondax.kache";
const LEGACY_PLIST_NAME: &str = "com.zondax.kache.plist";
const UNIT_NAME: &str = "kache.service";
const TASK_NAME: &str = "kache-daemon";
fn plist_path() -> PathBuf {
dirs::home_dir()
.unwrap_or_default()
.join("Library/LaunchAgents")
.join(PLIST_NAME)
}
fn legacy_plist_path() -> PathBuf {
dirs::home_dir()
.unwrap_or_default()
.join("Library/LaunchAgents")
.join(LEGACY_PLIST_NAME)
}
fn unit_path() -> PathBuf {
dirs::home_dir()
.unwrap_or_default()
.join(".config/systemd/user")
.join(UNIT_NAME)
}
fn task_xml_path() -> PathBuf {
dirs::data_local_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("kache")
.join("kache-task.xml")
}
pub fn service_file_path() -> Option<PathBuf> {
if cfg!(target_os = "macos") {
Some(plist_path())
} else if cfg!(target_os = "linux") {
Some(unit_path())
} else if cfg!(windows) {
Some(task_xml_path())
} else {
None
}
}
fn log_dir() -> PathBuf {
dirs::home_dir()
.unwrap_or_default()
.join("Library/Logs/kache")
}
fn stop_launchd_service(uid: u32, label: &str, plist: &std::path::Path) {
let bootout = std::process::Command::new("launchctl")
.args(["bootout", &format!("gui/{uid}/{label}")])
.output();
if !matches!(bootout, Ok(out) if out.status.success()) {
let _ = std::process::Command::new("launchctl")
.args(["unload", &plist.display().to_string()])
.output();
}
}
fn launchd_service_registered(uid: u32) -> bool {
std::process::Command::new("launchctl")
.args(["print", &format!("gui/{uid}/{LABEL}")])
.output()
.is_ok_and(|out| out.status.success())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ServiceExeMismatch {
pub installed: PathBuf,
pub current: PathBuf,
}
fn canonical_or_original(path: &Path) -> PathBuf {
path.canonicalize().unwrap_or_else(|_| path.to_path_buf())
}
pub(crate) fn service_exe_mismatch(path: &Path) -> Option<ServiceExeMismatch> {
let installed = parse_exe_from_service_file(path)?;
let current = std::env::current_exe()
.ok()
.map(|p| canonical_or_original(&p))?;
if canonical_or_original(&installed) == current {
None
} else {
Some(ServiceExeMismatch { installed, current })
}
}
pub fn install() -> Result<()> {
let exe = std::env::current_exe()
.context("resolving current executable")?
.canonicalize()
.context("canonicalizing executable path")?;
if cfg!(target_os = "macos") {
install_launchd(&exe)
} else if cfg!(target_os = "linux") {
install_systemd(&exe)
} else if cfg!(windows) {
install_task_scheduler(&exe)
} else {
anyhow::bail!("unsupported platform");
}
}
fn install_launchd(exe: &std::path::Path) -> Result<()> {
let plist = plist_path();
let legacy_plist = legacy_plist_path();
let uid = crate::platform::current_uid();
if plist.exists() || legacy_plist.exists() {
println!("Existing service found — upgrading in place...");
stop_launchd_service(uid, LABEL, &plist);
stop_launchd_service(uid, LEGACY_LABEL, &legacy_plist);
}
if legacy_plist.exists() {
std::fs::remove_file(&legacy_plist).context("removing legacy plist")?;
}
if let Some(parent) = plist.parent() {
std::fs::create_dir_all(parent).context("creating LaunchAgents directory")?;
}
let log_dir = log_dir();
std::fs::create_dir_all(&log_dir).context("creating log directory")?;
let exe_str = exe.display();
let stdout_log = log_dir.join("out.log");
let stderr_log = log_dir.join("err.log");
let content = format!(
r#"<?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>{LABEL}</string>
<key>ProgramArguments</key>
<array>
<string>{exe_str}</string>
<string>daemon</string>
<string>run</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key>
<false/>
</dict>
<key>StandardOutPath</key>
<string>{stdout}</string>
<key>StandardErrorPath</key>
<string>{stderr}</string>
<key>EnvironmentVariables</key>
<dict>
<key>KACHE_LOG</key>
<string>kache=info</string>
</dict>
<key>ThrottleInterval</key>
<integer>5</integer>
</dict>
</plist>
"#,
stdout = stdout_log.display(),
stderr = stderr_log.display(),
);
std::fs::write(&plist, &content).context("writing plist")?;
let bootstrap = std::process::Command::new("launchctl")
.args([
"bootstrap",
&format!("gui/{uid}"),
&plist.display().to_string(),
])
.output();
match bootstrap {
Ok(out) if out.status.success() => {}
bootstrap_result => {
let load = std::process::Command::new("launchctl")
.args(["load", "-w", &plist.display().to_string()])
.output()
.context("running launchctl load")?;
if !load.status.success() {
let bootstrap_stderr = bootstrap_result
.as_ref()
.ok()
.map(|out| String::from_utf8_lossy(&out.stderr).trim().to_string())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "launchctl bootstrap failed".to_string());
let stderr = String::from_utf8_lossy(&load.stderr);
anyhow::bail!(
"launchctl bootstrap failed: {bootstrap_stderr}; launchctl load failed: {stderr}"
);
}
}
}
if !launchd_service_registered(uid) {
anyhow::bail!(
"launchctl did not register {LABEL}; try running `launchctl bootstrap gui/{uid} {}`",
plist.display()
);
}
println!("Service installed and started.");
println!(" plist: {}", plist.display());
println!(" logs: {}", log_dir.display());
println!("\nThe daemon will now start automatically on login and restart on crash.");
println!("Use `kache daemon` to verify, `kache daemon log` to stream logs.");
Ok(())
}
fn install_systemd(exe: &std::path::Path) -> Result<()> {
let unit = unit_path();
if unit.exists() {
println!("Existing service found — upgrading in place...");
let _ = std::process::Command::new("systemctl")
.args(["--user", "stop", UNIT_NAME])
.output();
}
if let Some(parent) = unit.parent() {
std::fs::create_dir_all(parent).context("creating systemd user directory")?;
}
let content = format!(
r#"[Unit]
Description=kache build cache daemon
After=default.target
[Service]
Type=simple
ExecStart={exe} daemon run
Restart=on-failure
RestartSec=5s
Environment=KACHE_LOG=kache=info
[Install]
WantedBy=default.target
"#,
exe = exe.display(),
);
std::fs::write(&unit, &content).context("writing systemd unit")?;
let reload = std::process::Command::new("systemctl")
.args(["--user", "daemon-reload"])
.output()
.context("running systemctl daemon-reload")?;
if !reload.status.success() {
let stderr = String::from_utf8_lossy(&reload.stderr);
anyhow::bail!("systemctl daemon-reload failed: {stderr}");
}
let enable = std::process::Command::new("systemctl")
.args(["--user", "enable", "--now", UNIT_NAME])
.output()
.context("running systemctl enable")?;
if !enable.status.success() {
let stderr = String::from_utf8_lossy(&enable.stderr);
anyhow::bail!("systemctl enable --now failed: {stderr}");
}
let user = std::env::var("USER").unwrap_or_default();
if !user.is_empty() {
let _ = std::process::Command::new("loginctl")
.args(["enable-linger", &user])
.output();
}
println!("Service installed and started.");
println!(" unit: {}", unit.display());
println!(" logs: journalctl --user -u {UNIT_NAME}");
println!("\nThe daemon will now start automatically on login and restart on crash.");
println!("Use `kache daemon` to verify, `kache daemon log` to stream logs.");
Ok(())
}
fn install_task_scheduler(exe: &std::path::Path) -> Result<()> {
let xml_path = task_xml_path();
if task_scheduler_installed() {
println!("Existing task found — upgrading in place...");
let _ = std::process::Command::new("schtasks")
.args(["/delete", "/tn", TASK_NAME, "/f"])
.output();
}
if let Some(parent) = xml_path.parent() {
std::fs::create_dir_all(parent).context("creating kache data directory")?;
}
let username = std::env::var("USERNAME").unwrap_or_else(|_| "".into());
let exe_str = exe.display().to_string().replace('/', "\\");
let log_path = crate::config::Config::load()
.map(|c| c.socket_path().with_extension("log"))
.unwrap_or_else(|_| {
dirs::data_local_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("kache")
.join("daemon.log")
});
let content = format!(
r#"<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<RegistrationInfo>
<Description>kache build cache daemon — starts at login, restarts on crash</Description>
</RegistrationInfo>
<Triggers>
<LogonTrigger>
<Enabled>true</Enabled>
<UserId>{username}</UserId>
</LogonTrigger>
</Triggers>
<Principals>
<Principal id="Author">
<UserId>{username}</UserId>
<LogonType>InteractiveToken</LogonType>
<RunLevel>LeastPrivilege</RunLevel>
</Principal>
</Principals>
<Settings>
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
<RestartOnFailure>
<Interval>PT1M</Interval>
<Count>999</Count>
</RestartOnFailure>
<Hidden>false</Hidden>
</Settings>
<Actions Context="Author">
<Exec>
<Command>conhost.exe</Command>
<Arguments>--headless "{exe_str}" daemon run</Arguments>
</Exec>
</Actions>
</Task>
"#,
);
let utf16: Vec<u16> = content.encode_utf16().collect();
let mut bytes = vec![0xFF, 0xFE]; for word in &utf16 {
bytes.extend_from_slice(&word.to_le_bytes());
}
std::fs::write(&xml_path, &bytes).context("writing task XML")?;
let create = std::process::Command::new("schtasks")
.args([
"/create",
"/tn",
TASK_NAME,
"/xml",
&xml_path.display().to_string(),
"/f",
])
.output()
.context("running schtasks /create")?;
if !create.status.success() {
let stderr = String::from_utf8_lossy(&create.stderr);
if stderr.contains("Access") || stderr.contains("acceso") || stderr.contains("denied") {
anyhow::bail!(
"schtasks requires administrator privileges.\n\
Run this command from an elevated (admin) terminal:\n\n\
kache daemon install"
);
}
anyhow::bail!("schtasks /create failed: {stderr}");
}
let _ = std::process::Command::new("schtasks")
.args(["/run", "/tn", TASK_NAME])
.output();
println!("Service installed and started.");
println!(" task: {TASK_NAME}");
println!(" xml: {}", xml_path.display());
println!(" logs: {}", log_path.display());
println!("\nThe daemon will now start automatically on login and restart on crash.");
println!("Use `kache daemon` to verify, `kache daemon log` to stream logs.");
Ok(())
}
fn task_scheduler_installed() -> bool {
if !cfg!(windows) {
return false;
}
std::process::Command::new("schtasks")
.args(["/query", "/tn", TASK_NAME])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.is_ok_and(|s| s.success())
}
pub fn kickstart() -> Result<bool> {
if cfg!(target_os = "macos") {
let plist = plist_path();
if !plist.exists() {
return Ok(false);
}
let uid = crate::platform::current_uid();
let target = format!("gui/{uid}/{LABEL}");
let out = std::process::Command::new("launchctl")
.args(["kickstart", "-k", &target])
.output()
.context("running launchctl kickstart")?;
if !out.status.success() {
let stderr = String::from_utf8_lossy(&out.stderr);
anyhow::bail!("launchctl kickstart {target} failed: {stderr}");
}
Ok(true)
} else if cfg!(target_os = "linux") {
let unit = unit_path();
if !unit.exists() {
return Ok(false);
}
let out = std::process::Command::new("systemctl")
.args(["--user", "restart", UNIT_NAME])
.output()
.context("running systemctl --user restart")?;
if !out.status.success() {
let stderr = String::from_utf8_lossy(&out.stderr);
anyhow::bail!("systemctl --user restart {UNIT_NAME} failed: {stderr}");
}
Ok(true)
} else if cfg!(windows) {
if !task_scheduler_installed() {
return Ok(false);
}
let _ = std::process::Command::new("schtasks")
.args(["/end", "/tn", TASK_NAME])
.output();
std::thread::sleep(std::time::Duration::from_millis(500));
let out = std::process::Command::new("schtasks")
.args(["/run", "/tn", TASK_NAME])
.output()
.context("running schtasks /run")?;
if !out.status.success() {
let stderr = String::from_utf8_lossy(&out.stderr);
anyhow::bail!("schtasks /run {TASK_NAME} failed: {stderr}");
}
Ok(true)
} else {
Ok(false)
}
}
pub fn uninstall() -> Result<()> {
if cfg!(target_os = "macos") {
uninstall_launchd()
} else if cfg!(target_os = "linux") {
uninstall_systemd()
} else if cfg!(windows) {
uninstall_task_scheduler()
} else {
anyhow::bail!("unsupported platform");
}
}
fn uninstall_launchd() -> Result<()> {
let plist = plist_path();
let legacy_plist = legacy_plist_path();
let uid = crate::platform::current_uid();
let had_plist = plist.exists();
let had_legacy_plist = legacy_plist.exists();
if !had_plist && !had_legacy_plist {
println!("Service is not installed (no plist found).");
return Ok(());
}
stop_launchd_service(uid, LABEL, &plist);
stop_launchd_service(uid, LEGACY_LABEL, &legacy_plist);
if had_plist {
std::fs::remove_file(&plist).context("removing plist")?;
}
if had_legacy_plist {
std::fs::remove_file(&legacy_plist).context("removing legacy plist")?;
}
println!("Service stopped and removed.");
if had_plist {
println!(" removed: {}", plist.display());
}
if had_legacy_plist {
println!(" removed: {}", legacy_plist.display());
}
Ok(())
}
fn uninstall_systemd() -> Result<()> {
let unit = unit_path();
if !unit.exists() {
println!("Service is not installed (no unit file found).");
return Ok(());
}
let _ = std::process::Command::new("systemctl")
.args(["--user", "disable", "--now", UNIT_NAME])
.output();
std::fs::remove_file(&unit).context("removing unit file")?;
let _ = std::process::Command::new("systemctl")
.args(["--user", "daemon-reload"])
.output();
println!("Service stopped and removed.");
println!(" removed: {}", unit.display());
Ok(())
}
fn uninstall_task_scheduler() -> Result<()> {
if !task_scheduler_installed() {
println!("Service is not installed (no scheduled task found).");
return Ok(());
}
let _ = std::process::Command::new("schtasks")
.args(["/end", "/tn", TASK_NAME])
.output();
let delete = std::process::Command::new("schtasks")
.args(["/delete", "/tn", TASK_NAME, "/f"])
.output()
.context("running schtasks /delete")?;
if !delete.status.success() {
let stderr = String::from_utf8_lossy(&delete.stderr);
anyhow::bail!("schtasks /delete failed: {stderr}");
}
let xml_path = task_xml_path();
if xml_path.exists() {
let _ = std::fs::remove_file(&xml_path);
}
println!("Service stopped and removed.");
println!(" task: {TASK_NAME}");
Ok(())
}
pub fn status() -> Result<()> {
let config = crate::config::Config::load().ok();
let service_path = service_file_path();
let installed_service_path = service_path
.as_ref()
.and_then(|path| {
if cfg!(windows) {
task_scheduler_installed().then(|| path.clone())
} else {
path.exists().then(|| path.clone())
}
})
.or_else(|| {
if cfg!(target_os = "macos") {
let legacy_path = legacy_plist_path();
legacy_path.exists().then_some(legacy_path)
} else {
None
}
});
let legacy_service_installed = installed_service_path
.as_ref()
.and_then(|path| path.file_name().and_then(|name| name.to_str()))
== Some(LEGACY_PLIST_NAME);
println!(
" kache: v{} (epoch {})",
crate::VERSION,
crate::daemon::build_epoch(),
);
if let Some(ref path) = installed_service_path {
println!(" Service: \x1b[32minstalled\x1b[0m ({})", path.display());
if legacy_service_installed {
println!(
" \x1b[33mlegacy label detected — run `kache daemon install` to migrate to {LABEL}\x1b[0m"
);
}
} else if service_path.is_some() {
println!(" Service: \x1b[33mnot installed\x1b[0m");
println!(" run `kache daemon install` to set up");
} else {
println!(" Service: \x1b[33munsupported platform\x1b[0m");
}
let running = if let Some(ref cfg) = config {
crate::transport::is_reachable(&cfg.socket_path())
} else {
false
};
if running {
println!(" Daemon: \x1b[32mrunning\x1b[0m");
} else {
println!(" Daemon: \x1b[31mnot running\x1b[0m");
}
if let Some(ref cfg) = config {
println!(" Socket: {}", cfg.socket_path().display());
}
let diag = crate::diagnostic_log_path();
if diag.exists() {
println!(" Logs: {}", diag.display());
} else if cfg!(target_os = "macos") {
println!(" Logs: {}", log_dir().join("err.log").display());
} else if cfg!(target_os = "linux") {
println!(" Logs: journalctl --user -u {UNIT_NAME}");
}
if running
&& let Some(ref cfg) = config
&& let Ok(stats) = crate::daemon::send_stats_request(cfg, false, None, None)
{
let my_epoch = crate::daemon::build_epoch();
if !stats.version.is_empty() {
if stats.build_epoch == my_epoch {
println!(
" Version: \x1b[32mv{} (epoch {})\x1b[0m",
stats.version, stats.build_epoch
);
} else {
println!(
" Version: \x1b[33mv{} (epoch {}) — binary is v{} (epoch {})\x1b[0m",
stats.version,
stats.build_epoch,
crate::VERSION,
my_epoch
);
println!(" \x1b[33mauto-restart is pending\x1b[0m");
}
}
}
if let Some(ref path) = installed_service_path
&& let Some(mismatch) = service_exe_mismatch(path)
{
println!();
println!(" \x1b[33mWarning: installed exe differs from current exe\x1b[0m");
println!(" installed: {}", mismatch.installed.display());
println!(" current: {}", mismatch.current.display());
println!(" run `kache daemon install` to update");
}
println!();
Ok(())
}
pub(crate) fn parse_exe_from_service_file(path: &std::path::Path) -> Option<PathBuf> {
let content = std::fs::read_to_string(path).ok()?;
if cfg!(target_os = "macos") {
let after_prog = content.split("ProgramArguments").nth(1)?;
let start = after_prog.find("<string>")? + "<string>".len();
let end = after_prog[start..].find("</string>")? + start;
Some(PathBuf::from(after_prog[start..end].trim()))
} else if cfg!(windows) {
let args_start = content.find("<Arguments>")? + "<Arguments>".len();
let args_end = content[args_start..].find("</Arguments>")? + args_start;
let args = content[args_start..args_end].trim();
let exe = args
.strip_prefix("--headless ")?
.split("\" ")
.next()?
.trim_matches('"');
Some(PathBuf::from(exe))
} else {
for line in content.lines() {
if let Some(rest) = line.strip_prefix("ExecStart=") {
let exe = rest.split_whitespace().next()?;
return Some(PathBuf::from(exe));
}
}
None
}
}
pub fn log() -> Result<()> {
if cfg!(target_os = "macos") {
let diag_log = crate::diagnostic_log_path();
let err_log = log_dir().join("err.log");
let log_file = if diag_log.exists() {
diag_log
} else if err_log.exists() {
err_log
} else {
anyhow::bail!(
"no log files found in {}\nIs the service installed? Run `kache daemon install`",
log_dir().display()
);
};
eprintln!("Streaming {}", log_file.display());
let status = std::process::Command::new("tail")
.args(["-f", &log_file.display().to_string()])
.status()
.context("running tail -f")?;
std::process::exit(status.code().unwrap_or(1));
} else if cfg!(target_os = "linux") {
let status = std::process::Command::new("journalctl")
.args(["--user", "-u", UNIT_NAME, "-f"])
.status()
.context("running journalctl")?;
std::process::exit(status.code().unwrap_or(1));
} else if cfg!(windows) {
let diag_log = crate::diagnostic_log_path();
let fallback_log = crate::config::Config::load()
.map(|c| c.socket_path().with_extension("log"))
.ok();
let log_file = if diag_log.exists() {
diag_log
} else if let Some(ref fb) = fallback_log
&& fb.exists()
{
fb.clone()
} else {
anyhow::bail!("no log files found.\nIs the daemon running? Run `kache daemon start`");
};
eprintln!("Streaming {}", log_file.display());
let status = std::process::Command::new("powershell")
.args([
"-Command",
&format!("Get-Content -Wait -Tail 50 '{}'", log_file.display()),
])
.status()
.context("running powershell Get-Content -Wait")?;
std::process::exit(status.code().unwrap_or(1));
} else {
anyhow::bail!("unsupported platform");
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
fn write_service_file(path: &Path, exe: &Path) {
if cfg!(target_os = "macos") {
let content = format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
<key>ProgramArguments</key>
<array>
<string>{}</string>
<string>daemon</string>
<string>run</string>
</array>
</dict>
</plist>"#,
exe.display()
);
fs::write(path, content).unwrap();
} else if cfg!(target_os = "linux") {
fs::write(path, format!("ExecStart={} daemon run\n", exe.display())).unwrap();
}
}
#[test]
fn test_plist_path() {
let p = plist_path();
assert!(p.to_string_lossy().contains("LaunchAgents"));
assert!(p.to_string_lossy().contains(PLIST_NAME));
}
#[test]
fn test_unit_path() {
let p = unit_path();
assert!(p.to_string_lossy().contains("systemd/user"));
assert!(p.to_string_lossy().contains(UNIT_NAME));
}
#[test]
fn test_service_file_path_returns_some() {
let result = service_file_path();
if cfg!(target_os = "macos") || cfg!(target_os = "linux") {
assert!(result.is_some());
}
}
#[test]
fn test_log_dir() {
let d = log_dir();
assert!(d.to_string_lossy().contains("Logs/kache"));
}
#[test]
fn test_parse_exe_from_plist() {
let dir = tempfile::tempdir().unwrap();
let plist_file = dir.path().join("test.plist");
let content = r#"<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
<key>Label</key>
<string>ninja.kunobi.kache</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/kache</string>
<string>daemon</string>
<string>run</string>
</array>
</dict>
</plist>"#;
fs::write(&plist_file, content).unwrap();
if cfg!(target_os = "macos") {
let exe = parse_exe_from_service_file(&plist_file);
assert_eq!(exe, Some(PathBuf::from("/usr/local/bin/kache")));
}
}
#[test]
fn test_parse_exe_from_systemd_unit() {
let dir = tempfile::tempdir().unwrap();
let unit_file = dir.path().join("kache.service");
let content = r#"[Unit]
Description=kache build cache daemon
[Service]
Type=simple
ExecStart=/home/user/.cargo/bin/kache daemon run
Restart=on-failure
[Install]
WantedBy=default.target
"#;
fs::write(&unit_file, content).unwrap();
if cfg!(target_os = "linux") {
let exe = parse_exe_from_service_file(&unit_file);
assert_eq!(exe, Some(PathBuf::from("/home/user/.cargo/bin/kache")));
}
}
#[test]
fn test_parse_exe_from_nonexistent_file() {
let result = parse_exe_from_service_file(std::path::Path::new("/nonexistent/path"));
assert!(result.is_none());
}
#[test]
fn test_parse_exe_from_empty_file() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("empty");
fs::write(&file, "").unwrap();
let result = parse_exe_from_service_file(&file);
assert!(result.is_none());
}
#[test]
fn test_service_exe_mismatch_accepts_current_exe() {
if !(cfg!(target_os = "macos") || cfg!(target_os = "linux")) {
return;
}
let dir = tempfile::tempdir().unwrap();
let service_file = dir.path().join("service");
let current = std::env::current_exe().unwrap();
write_service_file(&service_file, ¤t);
assert_eq!(service_exe_mismatch(&service_file), None);
}
#[test]
fn test_service_exe_mismatch_detects_stale_exe() {
if !(cfg!(target_os = "macos") || cfg!(target_os = "linux")) {
return;
}
let dir = tempfile::tempdir().unwrap();
let service_file = dir.path().join("service");
let stale = dir.path().join("old-kache");
write_service_file(&service_file, &stale);
let mismatch = service_exe_mismatch(&service_file).unwrap();
assert_eq!(mismatch.installed, stale);
assert_eq!(
mismatch.current,
canonical_or_original(&std::env::current_exe().unwrap())
);
}
#[test]
fn test_label_constant() {
assert_eq!(LABEL, "ninja.kunobi.kache");
}
#[test]
fn test_plist_name_constant() {
assert_eq!(PLIST_NAME, "ninja.kunobi.kache.plist");
}
#[test]
fn test_unit_name_constant() {
assert_eq!(UNIT_NAME, "kache.service");
}
}