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}