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#[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#[derive(Copy, Clone, Debug, PartialEq)]
49pub enum ServiceStatus {
50 NotInstalled,
52 Stopped,
54 StartPending,
56 StopPending,
58 Running,
60 ContinuePending,
62 PausePending,
64 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
84pub struct ServiceSpec {
88 pub path: PathBuf,
90 pub args: Vec<OsString>,
92 pub display_name: Option<OsString>,
94 pub description: Option<OsString>,
96 pub autostart: bool,
98 pub restart_on_failure: bool,
100 pub user: Option<OsString>,
102 pub password: Option<OsString>,
104 pub group: Option<OsString>,
106}
107
108impl ServiceSpec {
109 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 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 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 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 pub fn set_autostart(mut self) -> Self {
157 self.autostart = true;
158 self
159 }
160
161 pub fn set_restart_on_failure(mut self) -> Self {
163 self.restart_on_failure = true;
164 self
165 }
166
167 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 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 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
232bitflags! {
235 pub struct ServiceCapabilities: u32 {
237 const CUSTOM_USER_REQUIRES_PASSWORD = 1 << 0;
239 const SUPPORTS_CUSTOM_GROUP = 1 << 1;
241 const USER_SERVICES_REQUIRE_NEW_LOGON = 1 << 2;
243 const RESTART_ON_FAILURE_REQUIRES_AUTOSTART = 1 << 3;
245 const USES_NAME_PREFIX = 1 << 4;
247 const USER_SERVICES_REQ_ELEVATED_PRIV_FOR_INSTALL = 1 << 5;
249 const SUPPORTS_PENDING_PAUSED_STATES = 1 << 6;
251 const USER_SERVICE_NAME_IS_DYNAMIC = 1 << 7;
253 const SUPPORTS_DESCRIPTION = 1 << 8;
255 const SUPPORTS_DISPLAY_NAME = 1 << 9;
257 const STARTS_IMMEDIATELY_WITH_AUTOSTART = 1 << 10;
259 }
260}
261
262pub(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#[derive(Clone, Debug)]
282pub enum ServiceErrKind {
283 ServiceManagementNotAvailable,
286 AlreadyInstalled,
288 NotInstalled,
290 InvalidNameOrPrefix,
292 WrongState(ServiceStatus),
294 Timeout(ServiceStatus),
296 TimeoutError(Box<ServiceErrKind>),
298 BadUtf8,
300 BadExitStatus(Option<i32>, String),
302 ServicePathNotFound,
304 AccessDenied,
306 DirectoryNotFound,
308 BadServiceSpec,
310 IoError,
312 BadSid,
314 PlatformError(Option<i64>),
316
317 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
363pub struct UniServiceManager {
369 manager: Box<dyn ServiceManager>,
370}
371
372impl UniServiceManager {
373 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 pub fn capabilities() -> ServiceCapabilities {
395 capabilities()
396 }
397
398 pub fn fully_qualified_name(&self) -> Cow<'_, OsStr> {
400 self.manager.fully_qualified_name()
401 }
402
403 pub fn is_user_service(&self) -> bool {
405 self.manager.is_user_service()
406 }
407
408 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 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 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 Ok(_) => self.start_and_wait(timeout),
496 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 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 pub fn uninstall_and_wait(&self, timeout: Duration) -> UniResult<(), ServiceErrKind> {
518 self.uninstall()?;
519 self.wait_for_status(ServiceStatus::NotInstalled, timeout)
520 }
521
522 pub fn stop_if_needed_and_uninstall(&self, timeout: Duration) -> UniResult<(), ServiceErrKind> {
525 match self.stop_and_wait(timeout) {
526 Ok(_) => self.uninstall_and_wait(timeout),
528 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 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 pub fn start_and_wait(&self, timeout: Duration) -> UniResult<(), ServiceErrKind> {
555 self.start()?;
556 self.wait_for_status(ServiceStatus::Running, timeout)
557 }
558
559 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 pub fn stop_and_wait(&self, timeout: Duration) -> UniResult<(), ServiceErrKind> {
573 self.stop()?;
574 self.wait_for_status(ServiceStatus::Stopped, timeout)
575 }
576
577 pub fn restart(&self, timeout: Duration) -> UniResult<(), ServiceErrKind> {
581 match self.stop_and_wait(timeout) {
582 Ok(_) => self.start_and_wait(timeout),
584 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 pub fn status(&self) -> UniResult<ServiceStatus, ServiceErrKind> {
600 self.manager.status()
601 }
602
603 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}