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
155    /// Restart the service only when it exits with a zero status (success).
156    ///
157    /// The optional delay specifies seconds to wait before restarting.
158    OnSuccess {
159        /// Delay in seconds before restarting
160        delay_secs: Option<u32>,
161    },
162}
163
164impl Default for RestartPolicy {
165    fn default() -> Self {
166        RestartPolicy::OnFailure { delay_secs: None }
167    }
168}
169
170/// Represents the status of a service
171#[derive(Clone, Debug, PartialEq, Eq, Hash)]
172pub enum ServiceStatus {
173    NotInstalled,
174    Running,
175    Stopped(Option<String>), // Provide a reason if possible
176}
177
178/// Label describing the service (e.g. `org.example.my_application`
179#[derive(Clone, Debug, PartialEq, Eq, Hash)]
180pub struct ServiceLabel {
181    /// Qualifier used for services tied to management systems like `launchd`
182    ///
183    /// E.g. `org` or `com`
184    pub qualifier: Option<String>,
185
186    /// Organization associated with the service
187    ///
188    /// E.g. `example`
189    pub organization: Option<String>,
190
191    /// Application name associated with the service
192    ///
193    /// E.g. `my_application`
194    pub application: String,
195}
196
197impl ServiceLabel {
198    /// Produces a fully-qualified name in the form of `{qualifier}.{organization}.{application}`
199    pub fn to_qualified_name(&self) -> String {
200        let mut qualified_name = String::new();
201        if let Some(qualifier) = self.qualifier.as_ref() {
202            qualified_name.push_str(qualifier.as_str());
203            qualified_name.push('.');
204        }
205        if let Some(organization) = self.organization.as_ref() {
206            qualified_name.push_str(organization.as_str());
207            qualified_name.push('.');
208        }
209        qualified_name.push_str(self.application.as_str());
210        qualified_name
211    }
212
213    /// Produces a script name using the organization and application
214    /// in the form of `{organization}-{application}`
215    pub fn to_script_name(&self) -> String {
216        let mut script_name = String::new();
217        if let Some(organization) = self.organization.as_ref() {
218            script_name.push_str(organization.as_str());
219            script_name.push('-');
220        }
221        script_name.push_str(self.application.as_str());
222        script_name
223    }
224}
225
226impl fmt::Display for ServiceLabel {
227    /// Produces a fully-qualified name
228    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229        f.write_str(self.to_qualified_name().as_str())
230    }
231}
232
233impl FromStr for ServiceLabel {
234    type Err = io::Error;
235
236    /// Parses a fully-qualified name in the form of `{qualifier}.{organization}.{application}`
237    fn from_str(s: &str) -> Result<Self, Self::Err> {
238        let tokens = s.split('.').collect::<Vec<&str>>();
239
240        let label = match tokens.len() {
241            1 => Self {
242                qualifier: None,
243                organization: None,
244                application: tokens[0].to_string(),
245            },
246            2 => Self {
247                qualifier: None,
248                organization: Some(tokens[0].to_string()),
249                application: tokens[1].to_string(),
250            },
251            3 => Self {
252                qualifier: Some(tokens[0].to_string()),
253                organization: Some(tokens[1].to_string()),
254                application: tokens[2].to_string(),
255            },
256            _ => Self {
257                qualifier: Some(tokens[0].to_string()),
258                organization: Some(tokens[1].to_string()),
259                application: tokens[2..].join("."),
260            },
261        };
262
263        Ok(label)
264    }
265}
266
267/// Context provided to the install function of [`ServiceManager`]
268#[derive(Debug, Clone, PartialEq, Eq)]
269pub struct ServiceInstallCtx {
270    /// Label associated with the service
271    ///
272    /// E.g. `org.example.my_application`
273    pub label: ServiceLabel,
274
275    /// Path to the program to run
276    ///
277    /// E.g. `/usr/local/bin/my-program`
278    pub program: PathBuf,
279
280    /// Arguments to use for the program
281    ///
282    /// E.g. `--arg`, `value`, `--another-arg`
283    pub args: Vec<OsString>,
284
285    /// Optional contents of the service file for a given ServiceManager
286    /// to use instead of the default template.
287    pub contents: Option<String>,
288
289    /// Optionally supply the user the service will run as
290    ///
291    /// If not specified, the service will run as the root or Administrator user.
292    pub username: Option<String>,
293
294    /// Optionally specify a working directory for the process launched by the service
295    pub working_directory: Option<PathBuf>,
296
297    /// Optionally specify a list of environment variables to be passed to the process launched by
298    /// the service
299    pub environment: Option<Vec<(String, String)>>,
300
301    /// Specify whether the service should automatically start on reboot
302    pub autostart: bool,
303
304    /// Specify the restart policy for the service
305    ///
306    /// This controls when and how the service should be restarted if it exits.
307    /// Different platforms support different levels of granularity - see [`RestartPolicy`]
308    /// documentation for details.
309    ///
310    /// Defaults to [`RestartPolicy::OnFailure`] if not specified.
311    pub restart_policy: RestartPolicy,
312}
313
314impl ServiceInstallCtx {
315    /// Iterator over the program and its arguments
316    pub fn cmd_iter(&self) -> impl Iterator<Item = &OsStr> {
317        std::iter::once(self.program.as_os_str()).chain(self.args_iter())
318    }
319
320    /// Iterator over the program arguments
321    pub fn args_iter(&self) -> impl Iterator<Item = &OsStr> {
322        self.args.iter().map(OsString::as_os_str)
323    }
324}
325
326/// Context provided to the uninstall function of [`ServiceManager`]
327#[derive(Debug, Clone, PartialEq, Eq)]
328pub struct ServiceUninstallCtx {
329    /// Label associated with the service
330    ///
331    /// E.g. `rocks.distant.manager`
332    pub label: ServiceLabel,
333}
334
335/// Context provided to the start function of [`ServiceManager`]
336#[derive(Debug, Clone, PartialEq, Eq)]
337pub struct ServiceStartCtx {
338    /// Label associated with the service
339    ///
340    /// E.g. `rocks.distant.manager`
341    pub label: ServiceLabel,
342}
343
344/// Context provided to the stop function of [`ServiceManager`]
345#[derive(Debug, Clone, PartialEq, Eq)]
346pub struct ServiceStopCtx {
347    /// Label associated with the service
348    ///
349    /// E.g. `rocks.distant.manager`
350    pub label: ServiceLabel,
351}
352
353/// Context provided to the status function of [`ServiceManager`]
354#[derive(Debug, Clone, PartialEq, Eq)]
355pub struct ServiceStatusCtx {
356    /// Label associated with the service
357    ///
358    /// E.g. `rocks.distant.manager`
359    pub label: ServiceLabel,
360}
361
362#[cfg(test)]
363mod tests {
364    use super::*;
365
366    #[test]
367    fn test_service_label_parssing_1() {
368        let label = ServiceLabel::from_str("com.example.app123").unwrap();
369
370        assert_eq!(label.qualifier, Some("com".to_string()));
371        assert_eq!(label.organization, Some("example".to_string()));
372        assert_eq!(label.application, "app123".to_string());
373
374        assert_eq!(label.to_qualified_name(), "com.example.app123");
375        assert_eq!(label.to_script_name(), "example-app123");
376    }
377
378    #[test]
379    fn test_service_label_parssing_2() {
380        let label = ServiceLabel::from_str("example.app123").unwrap();
381
382        assert_eq!(label.qualifier, None);
383        assert_eq!(label.organization, Some("example".to_string()));
384        assert_eq!(label.application, "app123".to_string());
385
386        assert_eq!(label.to_qualified_name(), "example.app123");
387        assert_eq!(label.to_script_name(), "example-app123");
388    }
389
390    #[test]
391    fn test_service_label_parssing_3() {
392        let label = ServiceLabel::from_str("app123").unwrap();
393
394        assert_eq!(label.qualifier, None);
395        assert_eq!(label.organization, None);
396        assert_eq!(label.application, "app123".to_string());
397
398        assert_eq!(label.to_qualified_name(), "app123");
399        assert_eq!(label.to_script_name(), "app123");
400    }
401}