service_manager/
sc.rs

1use crate::utils::wrap_output;
2
3use super::{
4    RestartPolicy, ServiceInstallCtx, ServiceLevel, ServiceManager, ServiceStartCtx,
5    ServiceStopCtx, ServiceUninstallCtx,
6};
7use std::{
8    borrow::Cow,
9    ffi::{OsStr, OsString},
10    fmt, io,
11    process::{Command, Output, Stdio},
12};
13
14#[cfg(windows)]
15mod shell_escape;
16
17#[cfg(not(windows))]
18mod shell_escape {
19    use std::{borrow::Cow, ffi::OsStr};
20
21    /// When not on windows, this will do nothing but return the input str
22    pub fn escape(s: Cow<'_, OsStr>) -> Cow<'_, OsStr> {
23        s
24    }
25}
26
27static SC_EXE: &str = "sc.exe";
28
29/// Configuration settings tied to sc.exe services
30#[derive(Clone, Debug, Default, PartialEq, Eq)]
31pub struct ScConfig {
32    pub install: ScInstallConfig,
33}
34
35/// Configuration settings tied to sc.exe services during installation
36#[derive(Clone, Debug, Default, PartialEq, Eq)]
37pub struct ScInstallConfig {
38    /// Type of windows service for install
39    pub service_type: WindowsServiceType,
40
41    /// Start type for windows service for install
42    pub start_type: WindowsStartType,
43
44    /// Severity of the error if the windows service fails when the computer is started
45    pub error_severity: WindowsErrorSeverity,
46}
47
48#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
49pub enum WindowsServiceType {
50    /// Service runs in its own process. It does not share an executable file with other services
51    Own,
52
53    /// Service runs as a shared process. It shares an executable file with other services
54    Share,
55
56    /// Service is a driver
57    Kernel,
58
59    /// Service is a file-system driver
60    FileSys,
61
62    /// Server is a file system recognized driver (identifies file systems used on the computer)
63    Rec,
64}
65
66impl Default for WindowsServiceType {
67    fn default() -> Self {
68        Self::Own
69    }
70}
71
72impl fmt::Display for WindowsServiceType {
73    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74        match self {
75            Self::Own => write!(f, "own"),
76            Self::Share => write!(f, "share"),
77            Self::Kernel => write!(f, "kernel"),
78            Self::FileSys => write!(f, "filesys"),
79            Self::Rec => write!(f, "rec"),
80        }
81    }
82}
83
84#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
85pub enum WindowsStartType {
86    /// Specifies a device driver that is loaded by the boot loader
87    Boot,
88
89    /// Specifies a device driver that is started during kernel initialization
90    System,
91
92    /// Specifies a service that automatically starts each time the computer is restarted. Note
93    /// that the service runs even if no one logs on to the computer
94    Auto,
95
96    /// Specifies a service that must be started manually
97    Demand,
98
99    /// Specifies a service that cannot be started. To start a disabled service, change the start
100    /// type to some other value.
101    Disabled,
102}
103
104impl Default for WindowsStartType {
105    fn default() -> Self {
106        Self::Auto
107    }
108}
109
110impl fmt::Display for WindowsStartType {
111    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112        match self {
113            Self::Boot => write!(f, "boot"),
114            Self::System => write!(f, "system"),
115            Self::Auto => write!(f, "auto"),
116            Self::Demand => write!(f, "demand"),
117            Self::Disabled => write!(f, "disabled"),
118        }
119    }
120}
121
122#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
123pub enum WindowsErrorSeverity {
124    /// Specifies that the error is logged. A message box is displayed, informing the user that a service has failed to start. Startup will continue
125    Normal,
126
127    /// Specifies that the error is logged (if possible). The computer attempts to restart with the
128    /// last-known good configuration. This could result in the computer being able to restart, but
129    /// the service may still be unable to run
130    Severe,
131
132    /// Specifies that the error is logged (if possible). The computer attempts to restart with the
133    /// last-known good configuration. If the last-known good configuration fails, startup also
134    /// fails, and the boot process halts with a Stop error
135    Critical,
136
137    /// Specifies that the error is logged and startup continues. No notification is given to the
138    /// user beyond recording the error in the event log
139    Ignore,
140}
141
142impl Default for WindowsErrorSeverity {
143    fn default() -> Self {
144        Self::Normal
145    }
146}
147
148impl fmt::Display for WindowsErrorSeverity {
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        match self {
151            Self::Normal => write!(f, "normal"),
152            Self::Severe => write!(f, "severe"),
153            Self::Critical => write!(f, "critical"),
154            Self::Ignore => write!(f, "ignore"),
155        }
156    }
157}
158
159/// Implementation of [`ServiceManager`] for [Window Service](https://en.wikipedia.org/wiki/Windows_service)
160/// leveraging [`sc.exe`](https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2012-r2-and-2012/cc754599(v=ws.11))
161#[derive(Clone, Debug, Default, PartialEq, Eq)]
162pub struct ScServiceManager {
163    /// Configuration settings tied to rc.d services
164    pub config: ScConfig,
165}
166
167impl ScServiceManager {
168    /// Creates a new manager instance working with system services
169    pub fn system() -> Self {
170        Self::default()
171    }
172
173    /// Update manager to use the specified config
174    pub fn with_config(self, config: ScConfig) -> Self {
175        Self { config }
176    }
177}
178
179impl ServiceManager for ScServiceManager {
180    fn available(&self) -> io::Result<bool> {
181        match which::which(SC_EXE) {
182            Ok(_) => Ok(true),
183            Err(which::Error::CannotFindBinaryPath) => Ok(false),
184            Err(x) => Err(io::Error::new(io::ErrorKind::Other, x)),
185        }
186    }
187
188    fn install(&self, ctx: ServiceInstallCtx) -> io::Result<()> {
189        // sc.exe doesn't support restart policies through `sc create`.
190        // Log a warning if user requested anything other than `Never`.
191        match ctx.restart_policy {
192            RestartPolicy::Never => {
193                // This is fine, sc.exe services don't restart by default
194            }
195            RestartPolicy::Always { .. } | RestartPolicy::OnFailure { .. } | RestartPolicy::OnSuccess { .. } => {
196                log::warn!(
197                    "sc.exe does not support automatic restart policies through 'sc create'; service '{}' will not restart automatically. Use 'sc failure' to configure restart behavior manually.",
198                    ctx.label.to_qualified_name()
199                );
200            }
201        }
202
203        let service_name = ctx.label.to_qualified_name();
204
205        let service_type = OsString::from(self.config.install.service_type.to_string());
206        let error_severity = OsString::from(self.config.install.error_severity.to_string());
207        let start_type = if ctx.autostart {
208            OsString::from("Auto")
209        } else {
210            // TODO: Perhaps it could be useful to make `start_type` an `Option`? That way you
211            // could have `Auto`/`Demand` based on `autostart`, and if `start_type` is set, its
212            // special value will override `autostart`.
213            OsString::from(self.config.install.start_type.to_string())
214        };
215
216        // Build our binary including arguments, following similar approach as windows-service-rs
217        let mut binpath = OsString::new();
218        binpath.push(shell_escape::escape(Cow::Borrowed(ctx.program.as_ref())));
219        for arg in ctx.args_iter() {
220            binpath.push(" ");
221            binpath.push(shell_escape::escape(Cow::Borrowed(arg)));
222        }
223
224        let display_name = OsStr::new(&service_name);
225
226        wrap_output(sc_exe(
227            "create",
228            &service_name,
229            [
230                // type= {service_type}
231                OsStr::new("type="),
232                service_type.as_os_str(),
233                // start= {start_type}
234                OsStr::new("start="),
235                start_type.as_os_str(),
236                // error= {error_severity}
237                OsStr::new("error="),
238                error_severity.as_os_str(),
239                // binpath= "{program} {args}"
240                OsStr::new("binpath="),
241                binpath.as_os_str(),
242                // displayname= {display_name}
243                OsStr::new("displayname="),
244                display_name,
245            ],
246        )?)?;
247        Ok(())
248    }
249
250    fn uninstall(&self, ctx: ServiceUninstallCtx) -> io::Result<()> {
251        let service_name = ctx.label.to_qualified_name();
252        wrap_output(sc_exe("delete", &service_name, [])?)?;
253        Ok(())
254    }
255
256    fn start(&self, ctx: ServiceStartCtx) -> io::Result<()> {
257        let service_name = ctx.label.to_qualified_name();
258        wrap_output(sc_exe("start", &service_name, [])?)?;
259        Ok(())
260    }
261
262    fn stop(&self, ctx: ServiceStopCtx) -> io::Result<()> {
263        let service_name = ctx.label.to_qualified_name();
264        wrap_output(sc_exe("stop", &service_name, [])?)?;
265        Ok(())
266    }
267
268    fn level(&self) -> ServiceLevel {
269        ServiceLevel::System
270    }
271
272    fn set_level(&mut self, level: ServiceLevel) -> io::Result<()> {
273        match level {
274            ServiceLevel::System => Ok(()),
275            ServiceLevel::User => Err(io::Error::new(
276                io::ErrorKind::Unsupported,
277                "sc.exe does not support user-level services",
278            )),
279        }
280    }
281
282    fn status(&self, ctx: crate::ServiceStatusCtx) -> io::Result<crate::ServiceStatus> {
283        let service_name = ctx.label.to_qualified_name();
284        let output = sc_exe("query", &service_name, [])?;
285        if !output.status.success() {
286            if matches!(output.status.code(), Some(1060)) {
287                // 1060 = The specified service does not exist as an installed service.
288                return Ok(crate::ServiceStatus::NotInstalled);
289            }
290            return Err(io::Error::new(
291                io::ErrorKind::Other,
292                format!(
293                    "Command failed with exit code {}: {}",
294                    output.status.code().unwrap_or(-1),
295                    String::from_utf8_lossy(&output.stderr)
296                ),
297            ));
298        }
299
300        let stdout = String::from_utf8_lossy(&output.stdout);
301        let line = stdout.split('\n').find(|line| {
302            line.trim_matches(&['\r', ' '])
303                .to_lowercase()
304                .starts_with("state")
305        });
306        let status = match line {
307            Some(line) if line.contains("RUNNING") => crate::ServiceStatus::Running,
308            _ => crate::ServiceStatus::Stopped(None), // TODO: more statuses?
309        };
310        Ok(status)
311    }
312}
313
314fn sc_exe<'a>(
315    cmd: &str,
316    service_name: &str,
317    args: impl IntoIterator<Item = &'a OsStr>,
318) -> io::Result<Output> {
319    let mut command = Command::new(SC_EXE);
320
321    command
322        .stdin(Stdio::null())
323        .stdout(Stdio::piped())
324        .stderr(Stdio::piped());
325
326    command.arg(cmd).arg(service_name);
327
328    for arg in args {
329        command.arg(arg);
330    }
331
332    command.output()
333}