Skip to main content

rootcx_platform/
service.rs

1use std::path::PathBuf;
2use crate::PlatformError;
3
4pub struct ServiceConfig {
5    pub name:        &'static str,
6    pub label:       &'static str, // reverse-DNS label (used on macOS)
7    pub description: &'static str,
8    pub binary:      PathBuf,
9    pub args:        &'static [&'static str],
10    pub log_file:    PathBuf,
11}
12
13#[derive(Debug, PartialEq)]
14pub enum ServiceStatus { Running, Stopped, NotInstalled }
15
16impl std::fmt::Display for ServiceStatus {
17    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18        f.write_str(match self {
19            Self::Running      => "running",
20            Self::Stopped      => "stopped",
21            Self::NotInstalled => "not installed",
22        })
23    }
24}
25
26pub fn install(c: &ServiceConfig)   -> Result<(), PlatformError> { imp::install(c) }
27pub fn uninstall(c: &ServiceConfig) -> Result<(), PlatformError> { imp::uninstall(c) }
28pub fn start(c: &ServiceConfig)     -> Result<(), PlatformError> { imp::start(c) }
29pub fn stop(c: &ServiceConfig)      -> Result<(), PlatformError> { imp::stop(c) }
30pub fn status(c: &ServiceConfig)    -> Result<ServiceStatus, PlatformError> { imp::status(c) }
31
32#[cfg(target_os = "macos")]
33mod imp {
34    use super::{PlatformError, ServiceConfig, ServiceStatus};
35    use std::path::PathBuf;
36
37    fn plist(c: &ServiceConfig) -> Result<PathBuf, PlatformError> {
38        Ok(crate::dirs::home_dir()?
39            .join(format!("Library/LaunchAgents/{}.plist", c.label)))
40    }
41
42    fn ctl(args: &[&str]) -> Result<(), PlatformError> {
43        std::process::Command::new("launchctl").args(args).status()
44            .map_err(|_| PlatformError("launchctl"))?
45            .success().then_some(()).ok_or(PlatformError("launchctl"))
46    }
47
48    pub fn install(c: &ServiceConfig) -> Result<(), PlatformError> {
49        let mut args_xml = format!("    <string>{}</string>\n", c.binary.display());
50        for a in c.args { args_xml.push_str(&format!("    <string>{a}</string>\n")); }
51        let p = plist(c)?;
52        std::fs::create_dir_all(p.parent().unwrap()).map_err(|_| PlatformError("LaunchAgents dir"))?;
53        std::fs::write(&p, format!(
54            "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
55             <!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \
56             \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n\
57             <plist version=\"1.0\"><dict>\n\
58             <key>Label</key><string>{label}</string>\n\
59             <key>ProgramArguments</key><array>\n{args}</array>\n\
60             <key>RunAtLoad</key><true/><key>KeepAlive</key><false/>\n\
61             <key>ProcessType</key><string>Background</string>\n\
62             <key>StandardOutPath</key><string>{log}</string>\n\
63             <key>StandardErrorPath</key><string>{log}</string>\n\
64             </dict></plist>\n",
65            label = c.label, args = args_xml, log = c.log_file.display()
66        )).map_err(|_| PlatformError("write plist"))?;
67        ctl(&["load", "-w", &p.to_string_lossy()])
68    }
69
70    pub fn uninstall(c: &ServiceConfig) -> Result<(), PlatformError> {
71        let p = plist(c)?;
72        if p.exists() {
73            let _ = ctl(&["unload", "-w", &p.to_string_lossy()]);
74            std::fs::remove_file(&p).map_err(|_| PlatformError("remove plist"))?;
75        }
76        Ok(())
77    }
78
79    pub fn start(c: &ServiceConfig)  -> Result<(), PlatformError> { ctl(&["start", c.label]) }
80    pub fn stop(c: &ServiceConfig)   -> Result<(), PlatformError> { ctl(&["stop",  c.label]) }
81
82    pub fn status(c: &ServiceConfig) -> Result<ServiceStatus, PlatformError> {
83        if !plist(c)?.exists() { return Ok(ServiceStatus::NotInstalled); }
84        let out = std::process::Command::new("launchctl")
85            .args(["list", c.label]).output()
86            .map_err(|_| PlatformError("launchctl list"))?;
87        if !out.status.success() { return Ok(ServiceStatus::Stopped); }
88        let s = String::from_utf8_lossy(&out.stdout);
89        // "PID" key is only present (and non-zero) when the daemon is running
90        Ok(if s.contains("\"PID\"") && !s.contains("\"PID\" = 0") {
91            ServiceStatus::Running
92        } else {
93            ServiceStatus::Stopped
94        })
95    }
96}
97
98#[cfg(target_os = "linux")]
99mod imp {
100    use super::{PlatformError, ServiceConfig, ServiceStatus};
101    use std::path::PathBuf;
102
103    fn unit(c: &ServiceConfig) -> Result<PathBuf, PlatformError> {
104        Ok(crate::dirs::home_dir()?
105            .join(format!(".config/systemd/user/{}.service", c.name)))
106    }
107
108    fn ctl(args: &[&str]) -> Result<(), PlatformError> {
109        std::process::Command::new("systemctl").arg("--user").args(args).status()
110            .map_err(|_| PlatformError("systemctl"))?
111            .success().then_some(()).ok_or(PlatformError("systemctl"))
112    }
113
114    pub fn install(c: &ServiceConfig) -> Result<(), PlatformError> {
115        let p = unit(c)?;
116        std::fs::create_dir_all(p.parent().unwrap()).map_err(|_| PlatformError("systemd user dir"))?;
117        let exec = std::iter::once(c.binary.to_string_lossy().into_owned())
118            .chain(c.args.iter().map(|s| s.to_string()))
119            .collect::<Vec<_>>().join(" ");
120        std::fs::write(&p, format!(
121            "[Unit]\nDescription={d}\nAfter=network.target\n\n\
122             [Service]\nType=simple\nExecStart={exec}\n\
123             Restart=on-failure\nRestartSec=5\n\
124             StandardOutput=append:{log}\nStandardError=append:{log}\n\n\
125             [Install]\nWantedBy=default.target\n",
126            d = c.description, log = c.log_file.display()
127        )).map_err(|_| PlatformError("write unit file"))?;
128        let _ = ctl(&["daemon-reload"]);
129        ctl(&["enable", "--now", c.name])
130    }
131
132    pub fn uninstall(c: &ServiceConfig) -> Result<(), PlatformError> {
133        let _ = ctl(&["disable", "--now", c.name]);
134        let p = unit(c)?;
135        if p.exists() { std::fs::remove_file(&p).map_err(|_| PlatformError("remove unit file"))?; }
136        let _ = ctl(&["daemon-reload"]);
137        Ok(())
138    }
139
140    pub fn start(c: &ServiceConfig)  -> Result<(), PlatformError> { ctl(&["start", c.name]) }
141    pub fn stop(c: &ServiceConfig)   -> Result<(), PlatformError> { ctl(&["stop",  c.name]) }
142
143    pub fn status(c: &ServiceConfig) -> Result<ServiceStatus, PlatformError> {
144        if !unit(c)?.exists() { return Ok(ServiceStatus::NotInstalled); }
145        Ok(if std::process::Command::new("systemctl")
146            .args(["--user", "is-active", c.name]).status()
147            .map(|s| s.success()).unwrap_or(false)
148        { ServiceStatus::Running } else { ServiceStatus::Stopped })
149    }
150}
151
152#[cfg(windows)]
153mod imp {
154    use super::{PlatformError, ServiceConfig, ServiceStatus};
155
156    fn task(c: &ServiceConfig) -> String { format!("RootCX\\{}", c.name) }
157
158    fn sch(args: &[&str]) -> Result<(), PlatformError> {
159        std::process::Command::new("schtasks").args(args).status()
160            .map_err(|_| PlatformError("schtasks"))?
161            .success().then_some(()).ok_or(PlatformError("schtasks"))
162    }
163
164    pub fn install(c: &ServiceConfig) -> Result<(), PlatformError> {
165        let mut tr = format!("\"{}\"", c.binary.display());
166        for a in c.args { tr.push(' '); tr.push_str(a); }
167        // ONLOGON + LIMITED: starts at user login with standard privileges
168        sch(&["/Create", "/TN", &task(c), "/TR", &tr, "/SC", "ONLOGON", "/RL", "LIMITED", "/F"])
169    }
170
171    pub fn uninstall(c: &ServiceConfig) -> Result<(), PlatformError> {
172        sch(&["/Delete", "/TN", &task(c), "/F"])
173    }
174
175    pub fn start(c: &ServiceConfig)  -> Result<(), PlatformError> { sch(&["/Run", "/TN", &task(c)]) }
176    pub fn stop(c: &ServiceConfig)   -> Result<(), PlatformError> { sch(&["/End", "/TN", &task(c)]) }
177
178    pub fn status(c: &ServiceConfig) -> Result<ServiceStatus, PlatformError> {
179        let out = std::process::Command::new("schtasks")
180            .args(["/Query", "/TN", &task(c), "/FO", "CSV", "/NH"])
181            .output().map_err(|_| PlatformError("schtasks /Query"))?;
182        if !out.status.success() { return Ok(ServiceStatus::NotInstalled); }
183        Ok(if String::from_utf8_lossy(&out.stdout).contains("Running") {
184            ServiceStatus::Running
185        } else {
186            ServiceStatus::Stopped
187        })
188    }
189}