uni_service_manager/
manager.rs

1use std::{
2    borrow::Cow,
3    ffi::{OsStr, OsString},
4    path::PathBuf,
5    thread,
6    time::{Duration, Instant},
7};
8
9use bitflags::bitflags;
10use uni_error::{ErrorContext as _, UniError, UniKind, UniResult};
11
12#[cfg(target_os = "macos")]
13use crate::launchd::capabilities;
14#[cfg(windows)]
15use crate::sc::capabilities;
16#[cfg(target_os = "linux")]
17use crate::systemd::capabilities;
18
19// *** make_service_manager ***
20
21#[cfg(target_os = "macos")]
22use crate::launchd::make_service_manager;
23#[cfg(windows)]
24use crate::sc::make_service_manager;
25#[cfg(target_os = "linux")]
26use crate::systemd::make_service_manager;
27#[cfg(not(target_os = "windows"))]
28use crate::util;
29
30#[cfg(all(
31    not(target_os = "windows"),
32    not(target_os = "linux"),
33    not(target_os = "macos")
34))]
35fn make_service_manager(
36    _name: OsString,
37    _prefix: OsString,
38    _user: bool,
39) -> UniResult<Box<dyn ServiceManager>, ServiceErrKind> {
40    Err(ServiceErrKind::ServiceManagementNotAvailable.into_error())
41}
42
43// *** Status ***
44
45/// The status of a service. Windows services can be in any of these states.
46/// Linux/macOS services will only ever be `NotInstalled`, `Running` or `Stopped`.
47#[derive(Copy, Clone, Debug, PartialEq)]
48pub enum ServiceStatus {
49    /// The specified service is not installed.
50    NotInstalled,
51    /// The specified service is stopped.
52    Stopped,
53    /// The specified service is starting.
54    StartPending,
55    /// The specified service is stopping.
56    StopPending,
57    /// The specified service is running.
58    Running,
59    /// The specified service is continuing.
60    ContinuePending,
61    /// The specified service is pausing.
62    PausePending,
63    /// The specified service is paused.
64    Paused,
65}
66
67// *** Service Spec ***
68
69/// A specification of a service to be installed.
70pub struct ServiceSpec {
71    /// The path to the executable to run when the service starts.
72    pub path: PathBuf,
73    /// The arguments to pass to the executable.
74    pub args: Vec<OsString>,
75    /// The display name of the service.
76    pub display_name: Option<OsString>,
77    /// The description of the service.
78    pub description: Option<OsString>,
79    /// Whether the service should start automatically when the system boots or user logs in.
80    pub autostart: bool,
81    /// Whether the service should be restarted if it fails.
82    pub restart_on_failure: bool,
83    /// User to run the service as.
84    pub user: Option<OsString>,
85    /// Password to use for the user.
86    pub password: Option<OsString>,
87    /// Group to run the service as.
88    pub group: Option<OsString>,
89}
90
91impl ServiceSpec {
92    /// Creates a new service specification with the given path to the executable.
93    pub fn new(path: impl Into<PathBuf>) -> Self {
94        Self {
95            path: path.into(),
96            args: vec![],
97            display_name: None,
98            description: None,
99            autostart: false,
100            restart_on_failure: false,
101            user: None,
102            password: None,
103            group: None,
104        }
105    }
106
107    fn validate(field: OsString) -> UniResult<OsString, ServiceErrKind> {
108        if field.is_empty() {
109            return Err(UniError::from_kind_context(
110                ServiceErrKind::BadServiceSpec,
111                "Field cannot be empty",
112            ));
113        }
114        Ok(field)
115    }
116
117    /// Adds an argument to the executable.
118    pub fn arg(mut self, arg: impl Into<OsString>) -> UniResult<Self, ServiceErrKind> {
119        self.args.push(Self::validate(arg.into())?);
120        Ok(self)
121    }
122
123    /// Sets the display name of the service.
124    pub fn display_name(
125        mut self,
126        display_name: impl Into<OsString>,
127    ) -> UniResult<Self, ServiceErrKind> {
128        self.display_name = Some(Self::validate(display_name.into())?);
129        Ok(self)
130    }
131
132    /// Sets the description of the service.
133    pub fn description(mut self, desc: impl Into<OsString>) -> UniResult<Self, ServiceErrKind> {
134        self.description = Some(Self::validate(desc.into())?);
135        Ok(self)
136    }
137
138    /// Sets whether the service should start automatically when the system boots or user logs in.
139    pub fn set_autostart(mut self) -> Self {
140        self.autostart = true;
141        self
142    }
143
144    /// Sets whether the service should be restarted if it fails.
145    pub fn set_restart_on_failure(mut self) -> Self {
146        self.restart_on_failure = true;
147        self
148    }
149
150    /// Sets the user to run the service as.
151    pub fn set_user(mut self, user: impl Into<OsString>) -> UniResult<Self, ServiceErrKind> {
152        self.user = Some(Self::validate(user.into())?);
153        Ok(self)
154    }
155
156    /// Sets the password to use for the user.
157    pub fn set_password(
158        mut self,
159        password: impl Into<OsString>,
160    ) -> UniResult<Self, ServiceErrKind> {
161        self.password = Some(Self::validate(password.into())?);
162        Ok(self)
163    }
164
165    /// Sets the group to run the service as.
166    pub fn set_group(mut self, group: impl Into<OsString>) -> UniResult<Self, ServiceErrKind> {
167        self.group = Some(Self::validate(group.into())?);
168        Ok(self)
169    }
170
171    pub(crate) fn path_and_args(&self) -> Vec<&OsStr> {
172        let mut result = vec![self.path.as_ref()];
173        let args = self
174            .args
175            .iter()
176            .map(|arg| <OsString as AsRef<OsStr>>::as_ref(arg));
177        result.extend(args);
178        result
179    }
180
181    #[cfg(not(target_os = "windows"))]
182    pub(crate) fn path_and_args_string(&self) -> UniResult<Vec<String>, ServiceErrKind> {
183        let combined = self.path_and_args();
184        combined
185            .iter()
186            .map(|arg| util::os_string_to_string(arg))
187            .collect()
188    }
189
190    #[cfg(target_os = "linux")]
191    pub(crate) fn description_string(&self) -> UniResult<Option<String>, ServiceErrKind> {
192        self.description
193            .as_ref()
194            .map(|desc| util::os_string_to_string(desc))
195            .transpose()
196    }
197
198    #[cfg(not(target_os = "windows"))]
199    pub(crate) fn user_string(&self) -> UniResult<Option<String>, ServiceErrKind> {
200        self.user
201            .as_ref()
202            .map(|user| util::os_string_to_string(user))
203            .transpose()
204    }
205
206    #[cfg(not(target_os = "windows"))]
207    pub(crate) fn group_string(&self) -> UniResult<Option<String>, ServiceErrKind> {
208        self.group
209            .as_ref()
210            .map(|group| util::os_string_to_string(group))
211            .transpose()
212    }
213}
214
215// *** Service Capabilities ***
216
217bitflags! {
218    /// The capabilities and limitations of the underlying platform service manager.
219    pub struct ServiceCapabilities: u32 {
220        /// The service requires a password when a custom user is used.
221        const CUSTOM_USER_REQUIRES_PASSWORD = 1 << 0;
222        /// The service supports running as a custom group.
223        const SUPPORTS_CUSTOM_GROUP = 1 << 1;
224        /// User services require a new logon before they can be started.
225        const USER_SERVICES_REQUIRE_NEW_LOGON = 1 << 2;
226        /// The service requires autostart to be enabled when restarting on failure is enabled.
227        const RESTART_ON_FAILURE_REQUIRES_AUTOSTART = 1 << 3;
228        /// The service uses a name prefix.
229        const USES_NAME_PREFIX = 1 << 4;
230        /// User services require elevated privileges to be installed.
231        const USER_SERVICES_REQ_ELEVATED_PRIV_FOR_INSTALL = 1 << 5;
232        /// The service supports pending and pause states.
233        const SUPPORTS_PENDING_PAUSED_STATES = 1 << 6;
234        /// Fully qualified user service names are dynamic change between sessions. They should not be stored.
235        const USER_SERVICE_NAME_IS_DYNAMIC = 1 << 7;
236        /// The service supports a custom description.
237        const SUPPORTS_DESCRIPTION = 1 << 8;
238        /// The service supports a custom display name.
239        const SUPPORTS_DISPLAY_NAME = 1 << 9;
240        /// The service starts immediately after install when autostart is enabled.
241        const STARTS_IMMEDIATELY_WITH_AUTOSTART = 1 << 10;
242    }
243}
244
245// *** Service Manager ***
246
247pub(crate) trait ServiceManager {
248    fn fully_qualified_name(&self) -> Cow<'_, OsStr>;
249
250    fn is_user_service(&self) -> bool;
251
252    fn install(&self, spec: &ServiceSpec) -> UniResult<(), ServiceErrKind>;
253
254    fn uninstall(&self) -> UniResult<(), ServiceErrKind>;
255
256    fn start(&self) -> UniResult<(), ServiceErrKind>;
257
258    fn stop(&self) -> UniResult<(), ServiceErrKind>;
259
260    fn status(&self) -> UniResult<ServiceStatus, ServiceErrKind>;
261}
262
263/// The error type for service management operations.
264#[derive(Clone, Debug)]
265pub enum ServiceErrKind {
266    /// Service management is not available on this platform either because it's not
267    /// supported or because the service manager is not detected.
268    ServiceManagementNotAvailable,
269    /// The service is already installed.
270    AlreadyInstalled,
271    /// The service is not installed.
272    NotInstalled,
273    /// The service name or prefix is invalid.
274    InvalidNameOrPrefix,
275    /// The service is in the wrong state for the requested operation.
276    WrongState(ServiceStatus),
277    /// The status operation timed out. Last status is returned.
278    Timeout(ServiceStatus),
279    /// The operation timed out. Last error is returned.
280    TimeoutError(Box<ServiceErrKind>),
281    /// The operation failed because an OS string wasn't valid UTF-8.
282    BadUtf8,
283    /// The operation failed because a child process exited with a non-zero status.
284    BadExitStatus(Option<i32>, String),
285    /// The service path was not found.
286    ServicePathNotFound,
287    /// The operation failed due to insufficient permissions.
288    AccessDenied,
289    /// The operation failed because a directory was not found.
290    DirectoryNotFound,
291    /// The operation failed because the service specification is invalid.
292    BadServiceSpec,
293    /// The operation failed because of an I/O error.
294    IoError,
295    /// The operation failed because the SID could not be extracted.
296    BadSid,
297    /// The operation failed because of a platform-specific error.
298    PlatformError(Option<i64>),
299
300    /// The operation failed because of an unknown error.
301    Unknown,
302}
303
304impl UniKind for ServiceErrKind {
305    fn context(&self) -> Option<Cow<'static, str>> {
306        Some(match self {
307            ServiceErrKind::ServiceManagementNotAvailable => {
308                "Service management is not available on this platform".into()
309            }
310            ServiceErrKind::AlreadyInstalled => "Service is already installed".into(),
311            ServiceErrKind::NotInstalled => "Service is not installed".into(),
312            ServiceErrKind::InvalidNameOrPrefix => "Service name or prefix is invalid".into(),
313            ServiceErrKind::WrongState(status) => format!(
314                "Service is in the wrong state for the requested operation. Current status: {:?}",
315                status
316            )
317            .into(),
318            ServiceErrKind::Timeout(status) => format!(
319                "Timeout waiting for service status. Last status: {:?}",
320                status
321            )
322            .into(),
323            ServiceErrKind::TimeoutError(kind) => {
324                format!("Timeout waiting for service status. Last error: {:?}", kind).into()
325            }
326            ServiceErrKind::BadUtf8 => "Bad UTF-8 encoding".into(),
327            ServiceErrKind::BadExitStatus(code, msg) => format!(
328                "Bad child process exit status. Code: {:?}. Stderr: {}",
329                code, msg
330            )
331            .into(),
332            ServiceErrKind::ServicePathNotFound => "The service path was not found".into(),
333            ServiceErrKind::AccessDenied => "Access denied".into(),
334            ServiceErrKind::DirectoryNotFound => "Unable to locate the directory".into(),
335            ServiceErrKind::BadServiceSpec => "The service specification is invalid".into(),
336            ServiceErrKind::IoError => "An I/O error occurred".into(),
337            ServiceErrKind::BadSid => "The SID could not be extracted".into(),
338            ServiceErrKind::PlatformError(code) => {
339                format!("A platform-specific error occurred. Code: {:?}", code).into()
340            }
341            ServiceErrKind::Unknown => "Unknown error".into(),
342        })
343    }
344}
345
346// *** UniServiceManager ***
347
348/// A service manager to manage services on the current system. It uses platform-specific implementations
349/// behind the scenes to perform the actual service management, but provides a unified interface regardless
350/// of the platform.
351pub struct UniServiceManager {
352    manager: Box<dyn ServiceManager>,
353}
354
355impl UniServiceManager {
356    /// Creates a new service manager for the given service name. The `prefix` is a java-style
357    /// reverse domain name prefix (e.g. `com.example.`) and is only used on macOS (ignored on other
358    /// platforms). If `user` is `true`, the service applies directly to the current user only.
359    /// On Windows, user level services require administrator privileges to manage and won't start
360    /// until the first logon.
361    pub fn new(
362        name: impl Into<OsString>,
363        prefix: impl Into<OsString>,
364        user: bool,
365    ) -> UniResult<Self, ServiceErrKind> {
366        let name = name.into();
367        if name.is_empty() {
368            return Err(UniError::from_kind_context(
369                ServiceErrKind::InvalidNameOrPrefix,
370                "The service name cannot be empty",
371            ));
372        }
373        make_service_manager(name, prefix.into(), user).map(|manager| Self { manager })
374    }
375
376    /// Gets the capabilities of the underlying platform service manager.
377    pub fn capabilities() -> ServiceCapabilities {
378        capabilities()
379    }
380
381    /// Gets the fully qualified name of the service. Note that Windows user services have a dynamic name that changes between sessions.
382    pub fn fully_qualified_name(&self) -> Cow<'_, OsStr> {
383        self.manager.fully_qualified_name()
384    }
385
386    /// `true` if the service is a user service, `false` if it is a system service.
387    pub fn is_user_service(&self) -> bool {
388        self.manager.is_user_service()
389    }
390
391    /// Installs the service. The `program` is the path to the executable to run when the service starts.
392    /// The `args` are the arguments to pass to the executable. The `display_name` is the name to display
393    /// to the user. The `desc` is the description of the service. After the method returns successfully, the
394    /// service may or may not be installed yet, as this is platform-dependent. An error is returned if the
395    /// service is already installed or if the installation fails.
396    pub fn install(&self, spec: &ServiceSpec) -> UniResult<(), ServiceErrKind> {
397        match self.status() {
398            Ok(ServiceStatus::NotInstalled) => {
399                if self.is_user_service()
400                    && (spec.user.is_some() || spec.group.is_some() || spec.password.is_some())
401                {
402                    return Err(UniError::from_kind_context(
403                        ServiceErrKind::BadServiceSpec,
404                        "User services cannot be installed with a custom user, group, or password",
405                    ));
406                }
407
408                let capabilities = Self::capabilities();
409
410                if capabilities.contains(ServiceCapabilities::RESTART_ON_FAILURE_REQUIRES_AUTOSTART)
411                    && spec.restart_on_failure
412                    && !spec.autostart
413                {
414                    return Err(UniError::from_kind_context(
415                        ServiceErrKind::BadServiceSpec,
416                        "Restarting on failure without autostart is not supported on this platform",
417                    ));
418                }
419
420                if capabilities.contains(ServiceCapabilities::CUSTOM_USER_REQUIRES_PASSWORD)
421                    && spec.user.is_some()
422                    && spec.password.is_none()
423                {
424                    return Err(UniError::from_kind_context(
425                        ServiceErrKind::BadServiceSpec,
426                        "A password is required when a custom username is specified",
427                    ));
428                }
429
430                if !capabilities.contains(ServiceCapabilities::SUPPORTS_CUSTOM_GROUP)
431                    && spec.group.is_some()
432                {
433                    return Err(UniError::from_kind_context(
434                        ServiceErrKind::BadServiceSpec,
435                        "Custom groups are not supported",
436                    ));
437                }
438
439                self.manager.install(spec)
440            }
441            Ok(_) => Err(ServiceErrKind::AlreadyInstalled.into_error()),
442            Err(e) => Err(e),
443        }
444    }
445
446    /// Installs the service and waits for it to reach the expected status. The `timeout` is the maximum time
447    /// to wait for the service to reach that status. The expected status is `Running` if autostart is enabled and the
448    /// service starts immediately after install, otherwise `Stopped`. The current status is returned when successful.
449    pub fn install_and_wait(
450        &self,
451        spec: &ServiceSpec,
452        timeout: Duration,
453    ) -> UniResult<ServiceStatus, ServiceErrKind> {
454        self.install(spec)?;
455
456        let status = if spec.autostart
457            && Self::capabilities().contains(ServiceCapabilities::STARTS_IMMEDIATELY_WITH_AUTOSTART)
458        {
459            ServiceStatus::Running
460        } else {
461            ServiceStatus::Stopped
462        };
463
464        self.wait_for_status(status, timeout)?;
465        Ok(status)
466    }
467
468    /// Installs the service if it is not already installed and starts it. The `timeout` is the maximum time
469    /// to wait for the service to reach the expected status. The expected status is `Running` if autostart is enabled and the
470    /// service starts immediately after install, otherwise `Stopped`. The current status is returned when successful.
471    pub fn install_if_needed_and_start(
472        &self,
473        spec: &ServiceSpec,
474        timeout: Duration,
475    ) -> UniResult<(), ServiceErrKind> {
476        match self.install_and_wait(spec, timeout) {
477            // Wasn't installed, but now it is
478            Ok(_) => self.start_and_wait(timeout),
479            // Already installed
480            Err(err) if matches!(err.kind_ref(), ServiceErrKind::AlreadyInstalled) => {
481                self.start_and_wait(timeout)
482            }
483            Err(e) => Err(e),
484        }
485    }
486
487    /// Uninstalls the service. After the method returns successfully, the service may or may not be uninstalled yet,
488    /// as this is platform-dependent. An error is returned if the service is not installed, if the service
489    /// is not stopped, or if the uninstallation fails.
490    pub fn uninstall(&self) -> UniResult<(), ServiceErrKind> {
491        match self.status() {
492            Ok(ServiceStatus::Stopped) => self.manager.uninstall(),
493            Ok(status) => Err(ServiceErrKind::WrongState(status).into_error()),
494            Err(e) => Err(e),
495        }
496    }
497
498    /// Uninstalls the service and waits for it to reach the expected status of `NotInstalled`.
499    /// The `timeout` is the maximum time to wait for the service to reach that status.
500    pub fn uninstall_and_wait(&self, timeout: Duration) -> UniResult<(), ServiceErrKind> {
501        self.uninstall()?;
502        self.wait_for_status(ServiceStatus::NotInstalled, timeout)
503    }
504
505    /// Stops the service if it is running and uninstalls it. If the service is already stopped, it will be uninstalled
506    /// without further action. The `timeout` is the maximum time to wait for the service to reach each expected status.
507    pub fn stop_if_needed_and_uninstall(&self, timeout: Duration) -> UniResult<(), ServiceErrKind> {
508        match self.stop_and_wait(timeout) {
509            // Stopped
510            Ok(_) => self.uninstall_and_wait(timeout),
511            // Already stopped
512            Err(err)
513                if matches!(
514                    err.kind_ref(),
515                    ServiceErrKind::WrongState(ServiceStatus::Stopped)
516                ) =>
517            {
518                self.uninstall_and_wait(timeout)
519            }
520            Err(e) => Err(e),
521        }
522    }
523
524    /// Starts the service. After the method returns successfully, the service may or may not be started yet,
525    /// as this is platform-dependent. An error is returned if the service is not stopped or if the starting
526    /// fails.
527    pub fn start(&self) -> UniResult<(), ServiceErrKind> {
528        match self.status() {
529            Ok(ServiceStatus::Stopped) => self.manager.start(),
530            Ok(status) => Err(ServiceErrKind::WrongState(status).into_error()),
531            Err(e) => Err(e),
532        }
533    }
534
535    /// Starts the service and waits for it to reach the expected status of `Running`.
536    /// The `timeout` is the maximum time to wait for the service to reach that status.
537    pub fn start_and_wait(&self, timeout: Duration) -> UniResult<(), ServiceErrKind> {
538        self.start()?;
539        self.wait_for_status(ServiceStatus::Running, timeout)
540    }
541
542    /// Stops the service. After the method returns successfully, the service may or may not be stopped yet,
543    /// as this is platform-dependent. An error is returned if the service is not running or if the stopping
544    /// fails.
545    pub fn stop(&self) -> UniResult<(), ServiceErrKind> {
546        match self.status() {
547            Ok(ServiceStatus::Running) => self.manager.stop(),
548            Ok(status) => Err(ServiceErrKind::WrongState(status).into_error()),
549            Err(e) => Err(e),
550        }
551    }
552
553    /// Stops the service and waits for it to reach the expected status of `Stopped`.
554    /// The `timeout` is the maximum time to wait for the service to reach that status.
555    pub fn stop_and_wait(&self, timeout: Duration) -> UniResult<(), ServiceErrKind> {
556        self.stop()?;
557        self.wait_for_status(ServiceStatus::Stopped, timeout)
558    }
559
560    /// Gets the current status of the service. It returns an error if the service is not installed
561    /// or if the status cannot be determined.
562    pub fn status(&self) -> UniResult<ServiceStatus, ServiceErrKind> {
563        self.manager.status()
564    }
565
566    /// Waits for the service to reach the desired status. It returns an error if the service is not installed
567    /// the status cannot be determined, or if the service does not reach the desired status before the timeout.
568    pub fn wait_for_status(
569        &self,
570        desired_status: ServiceStatus,
571        timeout: Duration,
572    ) -> UniResult<(), ServiceErrKind> {
573        let start_time = Instant::now();
574
575        loop {
576            let (last_status, last_error) = match self.status() {
577                Ok(s) => {
578                    if s == desired_status {
579                        return Ok(());
580                    }
581
582                    (Some(s), None)
583                }
584                Err(e) => (None, Some(e)),
585            };
586
587            if start_time.elapsed() > timeout {
588                match (last_status, last_error) {
589                    (None, Some(err)) => {
590                        let kind = err.kind_clone();
591                        return Err(err.kind(ServiceErrKind::TimeoutError(Box::new(kind))));
592                    }
593                    (Some(s), None) => {
594                        return Err(ServiceErrKind::Timeout(s).into_error());
595                    }
596                    _ => unreachable!(),
597                }
598            } else {
599                thread::sleep(Duration::from_millis(50));
600            }
601        }
602    }
603}