Skip to main content

service_manager/
lib.rs

1#![doc = include_str!("../README.md")]
2
3#[doc = include_str!("../README.md")]
4#[cfg(doctest)]
5pub struct ReadmeDoctests;
6
7use std::{
8    ffi::{OsStr, OsString},
9    fmt, io,
10    path::PathBuf,
11    str::FromStr,
12};
13
14mod kind;
15mod launchd;
16mod openrc;
17mod rcd;
18mod sc;
19mod systemd;
20mod typed;
21mod utils;
22mod winsw;
23
24pub use kind::*;
25pub use launchd::*;
26pub use openrc::*;
27pub use rcd::*;
28pub use sc::*;
29pub use systemd::*;
30pub use typed::*;
31pub use winsw::*;
32
33/// Interface for a service manager
34pub trait ServiceManager {
35    /// Determines if the service manager exists (e.g. is `launchd` available on the system?) and
36    /// can be used
37    fn available(&self) -> io::Result<bool>;
38
39    /// Installs a new service using the manager
40    fn install(&self, ctx: ServiceInstallCtx) -> io::Result<()>;
41
42    /// Uninstalls an existing service using the manager
43    fn uninstall(&self, ctx: ServiceUninstallCtx) -> io::Result<()>;
44
45    /// Starts a service using the manager
46    fn start(&self, ctx: ServiceStartCtx) -> io::Result<()>;
47
48    /// Stops a running service using the manager
49    fn stop(&self, ctx: ServiceStopCtx) -> io::Result<()>;
50
51    /// Returns the current target level for the manager
52    fn level(&self) -> ServiceLevel;
53
54    /// Sets the target level for the manager
55    fn set_level(&mut self, level: ServiceLevel) -> io::Result<()>;
56
57    /// Return the service status info
58    fn status(&self, ctx: ServiceStatusCtx) -> io::Result<ServiceStatus>;
59}
60
61impl dyn ServiceManager {
62    /// Creates a new service using the specified type, falling back to selecting
63    /// based on native service manager for the current operating system if no type provided
64    pub fn target_or_native(
65        kind: impl Into<Option<ServiceManagerKind>>,
66    ) -> io::Result<Box<dyn ServiceManager>> {
67        Ok(TypedServiceManager::target_or_native(kind)?.into_box())
68    }
69
70    /// Creates a new service manager targeting the specific service manager kind using the
71    /// default service manager instance
72    pub fn target(kind: ServiceManagerKind) -> Box<dyn ServiceManager> {
73        TypedServiceManager::target(kind).into_box()
74    }
75
76    /// Attempts to select a native service manager for the current operating system
77    ///
78    /// * For MacOS, this will use [`LaunchdServiceManager`]
79    /// * For Windows, this will use [`ScServiceManager`]
80    /// * For BSD variants, this will use [`RcdServiceManager`]
81    /// * For Linux variants, this will use either [`SystemdServiceManager`] or [`OpenRcServiceManager`]
82    pub fn native() -> io::Result<Box<dyn ServiceManager>> {
83        native_service_manager()
84    }
85}
86
87/// Attempts to select a native service manager for the current operating system1
88///
89/// * For MacOS, this will use [`LaunchdServiceManager`]
90/// * For Windows, this will use [`ScServiceManager`]
91/// * For BSD variants, this will use [`RcdServiceManager`]
92/// * For Linux variants, this will use either [`SystemdServiceManager`] or [`OpenRcServiceManager`]
93#[inline]
94pub fn native_service_manager() -> io::Result<Box<dyn ServiceManager>> {
95    Ok(TypedServiceManager::native()?.into_box())
96}
97
98impl<'a, S> From<S> for Box<dyn ServiceManager + 'a>
99where
100    S: ServiceManager + 'a,
101{
102    fn from(service_manager: S) -> Self {
103        Box::new(service_manager)
104    }
105}
106
107/// Represents whether a service is system-wide or user-level
108#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
109pub enum ServiceLevel {
110    System,
111    User,
112}
113
114/// Represents the restart policy for a service.
115///
116/// This enum provides a cross-platform abstraction for service restart behavior with a set of
117/// simple options that cover most service managers.
118///
119/// For most service cases you likely want a restart-on-failure policy, so this is the default.
120///
121/// Each service manager supports different levels of granularity:
122///
123/// - **Systemd** (Linux): supports all variants natively
124/// - **Launchd** (macOS): supports Never, Always, OnFailure (approximated), and OnSuccess via KeepAlive dictionary
125/// - **WinSW** (Windows): supports Never, Always, and OnFailure; OnSuccess falls back to Always with a warning
126/// - **OpenRC/rc.d/sc.exe**: limited or no restart support as of yet
127///
128/// When a platform doesn't support a specific policy, the implementation will fall back
129/// to the closest approximation and log a warning.
130///
131/// In the case where you need a restart policy that is very specific to a particular service
132/// manager, you should instantiate that service manager directly, rather than using the generic
133/// `ServiceManager` trait.
134#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
135pub enum RestartPolicy {
136    /// Never restart the service
137    Never,
138
139    /// Always restart the service regardless of exit status.
140    ///
141    /// The optional delay specifies seconds to wait before restarting.
142    Always {
143        /// Delay in seconds before restarting
144        delay_secs: Option<u32>,
145    },
146
147    /// Restart the service only when it exits with a non-zero status.
148    ///
149    /// The optional delay specifies seconds to wait before restarting.
150    OnFailure {
151        /// Delay in seconds before restarting
152        delay_secs: Option<u32>,
153
154        /// Maximum number of restart attempts before giving up.
155        ///
156        /// If `None`, the service will restart indefinitely (platform default behavior).
157        /// If `Some(n)`, the service will be restarted at most `n` times before stopping.
158        ///
159        /// Platform mapping:
160        /// - **WinSW**: Generates `n` `<onfailure action="restart"/>` elements followed by
161        ///   `<onfailure action="none"/>`.
162        /// - **Systemd**: TODO — map to `StartLimitBurst`.
163        /// - **Launchd**: TODO — no direct equivalent.
164        max_retries: Option<u32>,
165
166        /// Duration in seconds after which the failure counter resets.
167        ///
168        /// If the service runs successfully for this many seconds, any previous failures
169        /// are forgotten and the retry counter starts fresh.
170        ///
171        /// If `None`, the platform default is used (e.g., WinSW defaults to 1 day).
172        ///
173        /// Platform mapping:
174        /// - **WinSW**: Maps to `<resetfailure>`.
175        /// - **Systemd**: TODO — map to `StartLimitIntervalSec`.
176        /// - **Launchd**: TODO — no direct equivalent.
177        reset_after_secs: Option<u32>,
178    },
179
180    /// Restart the service only when it exits with a zero status (success).
181    ///
182    /// The optional delay specifies seconds to wait before restarting.
183    OnSuccess {
184        /// Delay in seconds before restarting
185        delay_secs: Option<u32>,
186    },
187}
188
189impl Default for RestartPolicy {
190    fn default() -> Self {
191        RestartPolicy::OnFailure {
192            delay_secs: None,
193            max_retries: None,
194            reset_after_secs: None,
195        }
196    }
197}
198
199/// Represents the status of a service
200#[derive(Clone, Debug, PartialEq, Eq, Hash)]
201pub enum ServiceStatus {
202    NotInstalled,
203    Running,
204    Stopped(Option<String>), // Provide a reason if possible
205}
206
207/// Label describing the service (e.g. `org.example.my_application`
208#[derive(Clone, Debug, PartialEq, Eq, Hash)]
209pub struct ServiceLabel {
210    /// Qualifier used for services tied to management systems like `launchd`
211    ///
212    /// E.g. `org` or `com`
213    pub qualifier: Option<String>,
214
215    /// Organization associated with the service
216    ///
217    /// E.g. `example`
218    pub organization: Option<String>,
219
220    /// Application name associated with the service
221    ///
222    /// E.g. `my_application`
223    pub application: String,
224}
225
226impl ServiceLabel {
227    /// Produces a fully-qualified name in the form of `{qualifier}.{organization}.{application}`
228    pub fn to_qualified_name(&self) -> String {
229        let mut qualified_name = String::new();
230        if let Some(qualifier) = self.qualifier.as_ref() {
231            qualified_name.push_str(qualifier.as_str());
232            qualified_name.push('.');
233        }
234        if let Some(organization) = self.organization.as_ref() {
235            qualified_name.push_str(organization.as_str());
236            qualified_name.push('.');
237        }
238        qualified_name.push_str(self.application.as_str());
239        qualified_name
240    }
241
242    /// Produces a script name using the organization and application
243    /// in the form of `{organization}-{application}`
244    pub fn to_script_name(&self) -> String {
245        let mut script_name = String::new();
246        if let Some(organization) = self.organization.as_ref() {
247            script_name.push_str(organization.as_str());
248            script_name.push('-');
249        }
250        script_name.push_str(self.application.as_str());
251        script_name
252    }
253}
254
255impl fmt::Display for ServiceLabel {
256    /// Produces a fully-qualified name
257    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258        f.write_str(self.to_qualified_name().as_str())
259    }
260}
261
262impl FromStr for ServiceLabel {
263    type Err = io::Error;
264
265    /// Parses a fully-qualified name in the form of `{qualifier}.{organization}.{application}`
266    fn from_str(s: &str) -> Result<Self, Self::Err> {
267        let tokens = s.split('.').collect::<Vec<&str>>();
268
269        let label = match tokens.len() {
270            1 => Self {
271                qualifier: None,
272                organization: None,
273                application: tokens[0].to_string(),
274            },
275            2 => Self {
276                qualifier: None,
277                organization: Some(tokens[0].to_string()),
278                application: tokens[1].to_string(),
279            },
280            3 => Self {
281                qualifier: Some(tokens[0].to_string()),
282                organization: Some(tokens[1].to_string()),
283                application: tokens[2].to_string(),
284            },
285            _ => Self {
286                qualifier: Some(tokens[0].to_string()),
287                organization: Some(tokens[1].to_string()),
288                application: tokens[2..].join("."),
289            },
290        };
291
292        Ok(label)
293    }
294}
295
296/// Context provided to the install function of [`ServiceManager`]
297#[derive(Debug, Clone, PartialEq, Eq)]
298pub struct ServiceInstallCtx {
299    /// Label associated with the service
300    ///
301    /// E.g. `org.example.my_application`
302    pub label: ServiceLabel,
303
304    /// Path to the program to run
305    ///
306    /// E.g. `/usr/local/bin/my-program`
307    pub program: PathBuf,
308
309    /// Arguments to use for the program
310    ///
311    /// E.g. `--arg`, `value`, `--another-arg`
312    pub args: Vec<OsString>,
313
314    /// Optional contents of the service file for a given ServiceManager
315    /// to use instead of the default template.
316    pub contents: Option<String>,
317
318    /// Optionally supply the user the service will run as
319    ///
320    /// If not specified, the service will run as the root or Administrator user.
321    pub username: Option<String>,
322
323    /// Optionally specify a working directory for the process launched by the service
324    pub working_directory: Option<PathBuf>,
325
326    /// Optionally specify a list of environment variables to be passed to the process launched by
327    /// the service
328    pub environment: Option<Vec<(String, String)>>,
329
330    /// Specify whether the service should automatically start on reboot
331    pub autostart: bool,
332
333    /// Specify the restart policy for the service
334    ///
335    /// This controls when and how the service should be restarted if it exits.
336    /// Different platforms support different levels of granularity - see [`RestartPolicy`]
337    /// documentation for details.
338    ///
339    /// Defaults to [`RestartPolicy::OnFailure`] if not specified.
340    pub restart_policy: RestartPolicy,
341}
342
343impl ServiceInstallCtx {
344    /// Iterator over the program and its arguments
345    pub fn cmd_iter(&self) -> impl Iterator<Item = &OsStr> {
346        std::iter::once(self.program.as_os_str()).chain(self.args_iter())
347    }
348
349    /// Iterator over the program arguments
350    pub fn args_iter(&self) -> impl Iterator<Item = &OsStr> {
351        self.args.iter().map(OsString::as_os_str)
352    }
353}
354
355/// Context provided to the uninstall function of [`ServiceManager`]
356#[derive(Debug, Clone, PartialEq, Eq)]
357pub struct ServiceUninstallCtx {
358    /// Label associated with the service
359    ///
360    /// E.g. `rocks.distant.manager`
361    pub label: ServiceLabel,
362}
363
364/// Context provided to the start function of [`ServiceManager`]
365#[derive(Debug, Clone, PartialEq, Eq)]
366pub struct ServiceStartCtx {
367    /// Label associated with the service
368    ///
369    /// E.g. `rocks.distant.manager`
370    pub label: ServiceLabel,
371}
372
373/// Context provided to the stop function of [`ServiceManager`]
374#[derive(Debug, Clone, PartialEq, Eq)]
375pub struct ServiceStopCtx {
376    /// Label associated with the service
377    ///
378    /// E.g. `rocks.distant.manager`
379    pub label: ServiceLabel,
380}
381
382/// Context provided to the status function of [`ServiceManager`]
383#[derive(Debug, Clone, PartialEq, Eq)]
384pub struct ServiceStatusCtx {
385    /// Label associated with the service
386    ///
387    /// E.g. `rocks.distant.manager`
388    pub label: ServiceLabel,
389}
390
391#[cfg(test)]
392mod tests {
393    use super::*;
394
395    #[test]
396    fn test_service_label_parssing_1() {
397        let label = ServiceLabel::from_str("com.example.app123").unwrap();
398
399        assert_eq!(label.qualifier, Some("com".to_string()));
400        assert_eq!(label.organization, Some("example".to_string()));
401        assert_eq!(label.application, "app123".to_string());
402
403        assert_eq!(label.to_qualified_name(), "com.example.app123");
404        assert_eq!(label.to_script_name(), "example-app123");
405    }
406
407    #[test]
408    fn test_service_label_parssing_2() {
409        let label = ServiceLabel::from_str("example.app123").unwrap();
410
411        assert_eq!(label.qualifier, None);
412        assert_eq!(label.organization, Some("example".to_string()));
413        assert_eq!(label.application, "app123".to_string());
414
415        assert_eq!(label.to_qualified_name(), "example.app123");
416        assert_eq!(label.to_script_name(), "example-app123");
417    }
418
419    #[test]
420    fn test_service_label_parssing_3() {
421        let label = ServiceLabel::from_str("app123").unwrap();
422
423        assert_eq!(label.qualifier, None);
424        assert_eq!(label.organization, None);
425        assert_eq!(label.application, "app123".to_string());
426
427        assert_eq!(label.to_qualified_name(), "app123");
428        assert_eq!(label.to_script_name(), "app123");
429    }
430}