uni_service_manager/
manager.rs

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