1use std::path::PathBuf;
2use crate::PlatformError;
3
4pub struct ServiceConfig {
5 pub name: &'static str,
6 pub label: &'static str, 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 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 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}