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#[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#[derive(Copy, Clone, Debug, PartialEq)]
48pub enum ServiceStatus {
49 NotInstalled,
51 Stopped,
53 StartPending,
55 StopPending,
57 Running,
59 ContinuePending,
61 PausePending,
63 Paused,
65}
66
67pub struct ServiceSpec {
71 pub path: PathBuf,
73 pub args: Vec<OsString>,
75 pub display_name: Option<OsString>,
77 pub description: Option<OsString>,
79 pub autostart: bool,
81 pub restart_on_failure: bool,
83 pub user: Option<OsString>,
85 pub password: Option<OsString>,
87 pub group: Option<OsString>,
89}
90
91impl ServiceSpec {
92 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 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 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 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 pub fn set_autostart(mut self) -> Self {
140 self.autostart = true;
141 self
142 }
143
144 pub fn set_restart_on_failure(mut self) -> Self {
146 self.restart_on_failure = true;
147 self
148 }
149
150 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 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 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
215bitflags! {
218 pub struct ServiceCapabilities: u32 {
220 const CUSTOM_USER_REQUIRES_PASSWORD = 1 << 0;
222 const SUPPORTS_CUSTOM_GROUP = 1 << 1;
224 const USER_SERVICES_REQUIRE_NEW_LOGON = 1 << 2;
226 const RESTART_ON_FAILURE_REQUIRES_AUTOSTART = 1 << 3;
228 const USES_NAME_PREFIX = 1 << 4;
230 const USER_SERVICES_REQ_ELEVATED_PRIV_FOR_INSTALL = 1 << 5;
232 const SUPPORTS_PENDING_PAUSED_STATES = 1 << 6;
234 const USER_SERVICE_NAME_IS_DYNAMIC = 1 << 7;
236 const SUPPORTS_DESCRIPTION = 1 << 8;
238 const SUPPORTS_DISPLAY_NAME = 1 << 9;
240 const STARTS_IMMEDIATELY_WITH_AUTOSTART = 1 << 10;
242 }
243}
244
245pub(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#[derive(Clone, Debug)]
265pub enum ServiceErrKind {
266 ServiceManagementNotAvailable,
269 AlreadyInstalled,
271 NotInstalled,
273 InvalidNameOrPrefix,
275 WrongState(ServiceStatus),
277 Timeout(ServiceStatus),
279 TimeoutError(Box<ServiceErrKind>),
281 BadUtf8,
283 BadExitStatus(Option<i32>, String),
285 ServicePathNotFound,
287 AccessDenied,
289 DirectoryNotFound,
291 BadServiceSpec,
293 IoError,
295 BadSid,
297 PlatformError(Option<i64>),
299
300 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
346pub struct UniServiceManager {
352 manager: Box<dyn ServiceManager>,
353}
354
355impl UniServiceManager {
356 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 pub fn capabilities() -> ServiceCapabilities {
378 capabilities()
379 }
380
381 pub fn fully_qualified_name(&self) -> Cow<'_, OsStr> {
383 self.manager.fully_qualified_name()
384 }
385
386 pub fn is_user_service(&self) -> bool {
388 self.manager.is_user_service()
389 }
390
391 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 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 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 Ok(_) => self.start_and_wait(timeout),
479 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 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 pub fn uninstall_and_wait(&self, timeout: Duration) -> UniResult<(), ServiceErrKind> {
501 self.uninstall()?;
502 self.wait_for_status(ServiceStatus::NotInstalled, timeout)
503 }
504
505 pub fn stop_if_needed_and_uninstall(&self, timeout: Duration) -> UniResult<(), ServiceErrKind> {
508 match self.stop_and_wait(timeout) {
509 Ok(_) => self.uninstall_and_wait(timeout),
511 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 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 pub fn start_and_wait(&self, timeout: Duration) -> UniResult<(), ServiceErrKind> {
538 self.start()?;
539 self.wait_for_status(ServiceStatus::Running, timeout)
540 }
541
542 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 pub fn stop_and_wait(&self, timeout: Duration) -> UniResult<(), ServiceErrKind> {
556 self.stop()?;
557 self.wait_for_status(ServiceStatus::Stopped, timeout)
558 }
559
560 pub fn status(&self) -> UniResult<ServiceStatus, ServiceErrKind> {
563 self.manager.status()
564 }
565
566 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}