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}