service_manager/
sc.rs

1use crate::utils::wrap_output;
2
3use super::{
4    ServiceInstallCtx, ServiceLevel, ServiceManager, ServiceStartCtx, ServiceStopCtx,
5    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        let service_name = ctx.label.to_qualified_name();
190
191        let service_type = OsString::from(self.config.install.service_type.to_string());
192        let error_severity = OsString::from(self.config.install.error_severity.to_string());
193        let start_type = if ctx.autostart {
194            OsString::from("Auto")
195        } else {
196            // TODO: Perhaps it could be useful to make `start_type` an `Option`? That way you
197            // could have `Auto`/`Demand` based on `autostart`, and if `start_type` is set, its
198            // special value will override `autostart`.
199            OsString::from(self.config.install.start_type.to_string())
200        };
201
202        // Build our binary including arguments, following similar approach as windows-service-rs
203        let mut binpath = OsString::new();
204        binpath.push(shell_escape::escape(Cow::Borrowed(ctx.program.as_ref())));
205        for arg in ctx.args_iter() {
206            binpath.push(" ");
207            binpath.push(shell_escape::escape(Cow::Borrowed(arg)));
208        }
209
210        let display_name = OsStr::new(&service_name);
211
212        wrap_output(sc_exe(
213            "create",
214            &service_name,
215            [
216                // type= {service_type}
217                OsStr::new("type="),
218                service_type.as_os_str(),
219                // start= {start_type}
220                OsStr::new("start="),
221                start_type.as_os_str(),
222                // error= {error_severity}
223                OsStr::new("error="),
224                error_severity.as_os_str(),
225                // binpath= "{program} {args}"
226                OsStr::new("binpath="),
227                binpath.as_os_str(),
228                // displayname= {display_name}
229                OsStr::new("displayname="),
230                display_name,
231            ],
232        )?)?;
233        Ok(())
234    }
235
236    fn uninstall(&self, ctx: ServiceUninstallCtx) -> io::Result<()> {
237        let service_name = ctx.label.to_qualified_name();
238        wrap_output(sc_exe("delete", &service_name, [])?)?;
239        Ok(())
240    }
241
242    fn start(&self, ctx: ServiceStartCtx) -> io::Result<()> {
243        let service_name = ctx.label.to_qualified_name();
244        wrap_output(sc_exe("start", &service_name, [])?)?;
245        Ok(())
246    }
247
248    fn stop(&self, ctx: ServiceStopCtx) -> io::Result<()> {
249        let service_name = ctx.label.to_qualified_name();
250        wrap_output(sc_exe("stop", &service_name, [])?)?;
251        Ok(())
252    }
253
254    fn level(&self) -> ServiceLevel {
255        ServiceLevel::System
256    }
257
258    fn set_level(&mut self, level: ServiceLevel) -> io::Result<()> {
259        match level {
260            ServiceLevel::System => Ok(()),
261            ServiceLevel::User => Err(io::Error::new(
262                io::ErrorKind::Unsupported,
263                "sc.exe does not support user-level services",
264            )),
265        }
266    }
267
268    fn status(&self, ctx: crate::ServiceStatusCtx) -> io::Result<crate::ServiceStatus> {
269        let service_name = ctx.label.to_qualified_name();
270        let output = sc_exe("query", &service_name, [])?;
271        if !output.status.success() {
272            if matches!(output.status.code(), Some(1060)) {
273                // 1060 = The specified service does not exist as an installed service.
274                return Ok(crate::ServiceStatus::NotInstalled);
275            }
276            return Err(io::Error::new(
277                io::ErrorKind::Other,
278                format!(
279                    "Command failed with exit code {}: {}",
280                    output.status.code().unwrap_or(-1),
281                    String::from_utf8_lossy(&output.stderr)
282                ),
283            ));
284        }
285
286        let stdout = String::from_utf8_lossy(&output.stdout);
287        let line = stdout.split('\n').find(|line| {
288            line.trim_matches(&['\r', ' '])
289                .to_lowercase()
290                .starts_with("state")
291        });
292        let status = match line {
293            Some(line) if line.contains("RUNNING") => crate::ServiceStatus::Running,
294            _ => crate::ServiceStatus::Stopped(None), // TODO: more statuses?
295        };
296        Ok(status)
297    }
298}
299
300fn sc_exe<'a>(
301    cmd: &str,
302    service_name: &str,
303    args: impl IntoIterator<Item = &'a OsStr>,
304) -> io::Result<Output> {
305    let mut command = Command::new(SC_EXE);
306
307    command
308        .stdin(Stdio::null())
309        .stdout(Stdio::piped())
310        .stderr(Stdio::piped());
311
312    command.arg(cmd).arg(service_name);
313
314    for arg in args {
315        command.arg(arg);
316    }
317
318    command.output()
319}