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 status of a service
115#[derive(Clone, Debug, PartialEq, Eq, Hash)]
116pub enum ServiceStatus {
117    NotInstalled,
118    Running,
119    Stopped(Option<String>), // Provide a reason if possible
120}
121
122/// Label describing the service (e.g. `org.example.my_application`
123#[derive(Clone, Debug, PartialEq, Eq, Hash)]
124pub struct ServiceLabel {
125    /// Qualifier used for services tied to management systems like `launchd`
126    ///
127    /// E.g. `org` or `com`
128    pub qualifier: Option<String>,
129
130    /// Organization associated with the service
131    ///
132    /// E.g. `example`
133    pub organization: Option<String>,
134
135    /// Application name associated with the service
136    ///
137    /// E.g. `my_application`
138    pub application: String,
139}
140
141impl ServiceLabel {
142    /// Produces a fully-qualified name in the form of `{qualifier}.{organization}.{application}`
143    pub fn to_qualified_name(&self) -> String {
144        let mut qualified_name = String::new();
145        if let Some(qualifier) = self.qualifier.as_ref() {
146            qualified_name.push_str(qualifier.as_str());
147            qualified_name.push('.');
148        }
149        if let Some(organization) = self.organization.as_ref() {
150            qualified_name.push_str(organization.as_str());
151            qualified_name.push('.');
152        }
153        qualified_name.push_str(self.application.as_str());
154        qualified_name
155    }
156
157    /// Produces a script name using the organization and application
158    /// in the form of `{organization}-{application}`
159    pub fn to_script_name(&self) -> String {
160        let mut script_name = String::new();
161        if let Some(organization) = self.organization.as_ref() {
162            script_name.push_str(organization.as_str());
163            script_name.push('-');
164        }
165        script_name.push_str(self.application.as_str());
166        script_name
167    }
168}
169
170impl fmt::Display for ServiceLabel {
171    /// Produces a fully-qualified name
172    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173        f.write_str(self.to_qualified_name().as_str())
174    }
175}
176
177impl FromStr for ServiceLabel {
178    type Err = io::Error;
179
180    /// Parses a fully-qualified name in the form of `{qualifier}.{organization}.{application}`
181    fn from_str(s: &str) -> Result<Self, Self::Err> {
182        let tokens = s.split('.').collect::<Vec<&str>>();
183
184        let label = match tokens.len() {
185            1 => Self {
186                qualifier: None,
187                organization: None,
188                application: tokens[0].to_string(),
189            },
190            2 => Self {
191                qualifier: None,
192                organization: Some(tokens[0].to_string()),
193                application: tokens[1].to_string(),
194            },
195            3 => Self {
196                qualifier: Some(tokens[0].to_string()),
197                organization: Some(tokens[1].to_string()),
198                application: tokens[2].to_string(),
199            },
200            _ => Self {
201                qualifier: Some(tokens[0].to_string()),
202                organization: Some(tokens[1].to_string()),
203                application: tokens[2..].join("."),
204            },
205        };
206
207        Ok(label)
208    }
209}
210
211/// Context provided to the install function of [`ServiceManager`]
212#[derive(Debug, Clone, PartialEq, Eq)]
213pub struct ServiceInstallCtx {
214    /// Label associated with the service
215    ///
216    /// E.g. `org.example.my_application`
217    pub label: ServiceLabel,
218
219    /// Path to the program to run
220    ///
221    /// E.g. `/usr/local/bin/my-program`
222    pub program: PathBuf,
223
224    /// Arguments to use for the program
225    ///
226    /// E.g. `--arg`, `value`, `--another-arg`
227    pub args: Vec<OsString>,
228
229    /// Optional contents of the service file for a given ServiceManager
230    /// to use instead of the default template.
231    pub contents: Option<String>,
232
233    /// Optionally supply the user the service will run as
234    ///
235    /// If not specified, the service will run as the root or Administrator user.
236    pub username: Option<String>,
237
238    /// Optionally specify a working directory for the process launched by the service
239    pub working_directory: Option<PathBuf>,
240
241    /// Optionally specify a list of environment variables to be passed to the process launched by
242    /// the service
243    pub environment: Option<Vec<(String, String)>>,
244
245    /// Specify whether the service should automatically start on reboot
246    pub autostart: bool,
247
248    /// Optionally disable a service from restarting when it exits with a failure
249    ///
250    /// This could overwrite the platform specific service manager config.
251    pub disable_restart_on_failure: bool,
252}
253
254impl ServiceInstallCtx {
255    /// Iterator over the program and its arguments
256    pub fn cmd_iter(&self) -> impl Iterator<Item = &OsStr> {
257        std::iter::once(self.program.as_os_str()).chain(self.args_iter())
258    }
259
260    /// Iterator over the program arguments
261    pub fn args_iter(&self) -> impl Iterator<Item = &OsStr> {
262        self.args.iter().map(OsString::as_os_str)
263    }
264}
265
266/// Context provided to the uninstall function of [`ServiceManager`]
267#[derive(Debug, Clone, PartialEq, Eq)]
268pub struct ServiceUninstallCtx {
269    /// Label associated with the service
270    ///
271    /// E.g. `rocks.distant.manager`
272    pub label: ServiceLabel,
273}
274
275/// Context provided to the start function of [`ServiceManager`]
276#[derive(Debug, Clone, PartialEq, Eq)]
277pub struct ServiceStartCtx {
278    /// Label associated with the service
279    ///
280    /// E.g. `rocks.distant.manager`
281    pub label: ServiceLabel,
282}
283
284/// Context provided to the stop function of [`ServiceManager`]
285#[derive(Debug, Clone, PartialEq, Eq)]
286pub struct ServiceStopCtx {
287    /// Label associated with the service
288    ///
289    /// E.g. `rocks.distant.manager`
290    pub label: ServiceLabel,
291}
292
293/// Context provided to the status function of [`ServiceManager`]
294#[derive(Debug, Clone, PartialEq, Eq)]
295pub struct ServiceStatusCtx {
296    /// Label associated with the service
297    ///
298    /// E.g. `rocks.distant.manager`
299    pub label: ServiceLabel,
300}
301
302#[cfg(test)]
303mod tests {
304    use super::*;
305
306    #[test]
307    fn test_service_label_parssing_1() {
308        let label = ServiceLabel::from_str("com.example.app123").unwrap();
309
310        assert_eq!(label.qualifier, Some("com".to_string()));
311        assert_eq!(label.organization, Some("example".to_string()));
312        assert_eq!(label.application, "app123".to_string());
313
314        assert_eq!(label.to_qualified_name(), "com.example.app123");
315        assert_eq!(label.to_script_name(), "example-app123");
316    }
317
318    #[test]
319    fn test_service_label_parssing_2() {
320        let label = ServiceLabel::from_str("example.app123").unwrap();
321
322        assert_eq!(label.qualifier, None);
323        assert_eq!(label.organization, Some("example".to_string()));
324        assert_eq!(label.application, "app123".to_string());
325
326        assert_eq!(label.to_qualified_name(), "example.app123");
327        assert_eq!(label.to_script_name(), "example-app123");
328    }
329
330    #[test]
331    fn test_service_label_parssing_3() {
332        let label = ServiceLabel::from_str("app123").unwrap();
333
334        assert_eq!(label.qualifier, None);
335        assert_eq!(label.organization, None);
336        assert_eq!(label.application, "app123".to_string());
337
338        assert_eq!(label.to_qualified_name(), "app123");
339        assert_eq!(label.to_script_name(), "app123");
340    }
341}