qsu/
installer.rs

1//! Helpers for installing/uninstalling services.
2
3#[cfg(windows)]
4pub mod winsvc;
5
6#[cfg(target_os = "macos")]
7pub mod launchd;
8
9#[cfg(all(target_os = "linux", feature = "systemd"))]
10#[cfg_attr(
11  docsrs,
12  doc(cfg(all(all(target_os = "linux", feature = "installer"))))
13)]
14pub mod systemd;
15
16//use std::{fmt, path::PathBuf};
17
18#[cfg(feature = "clap")]
19use clap::ArgMatches;
20
21use itertools::Itertools;
22
23use crate::{err::Error, lumberjack::LogLevel};
24
25
26/*
27#[cfg(any(
28  target_os = "macos",
29  all(target_os = "linux", feature = "systemd")
30))]
31pub enum InstallDir {
32  #[cfg(target_os = "macos")]
33  UserAgent,
34
35  #[cfg(target_os = "macos")]
36  GlobalAgent,
37
38  #[cfg(target_os = "macos")]
39  GlobalDaemon,
40
41  #[cfg(all(target_os = "linux", feature = "systemd"))]
42  System,
43
44  #[cfg(all(target_os = "linux", feature = "systemd"))]
45  PublicUser,
46
47  #[cfg(all(target_os = "linux", feature = "systemd"))]
48  PrivateUser
49}
50
51#[cfg(any(
52  target_os = "macos",
53  all(target_os = "linux", feature = "systemd")
54))]
55impl InstallDir {
56  fn path(self) -> PathBuf {
57    PathBuf::from(self.to_string())
58  }
59
60  fn path_str(self) -> String {
61    self.to_string()
62  }
63}
64
65#[cfg(any(
66  target_os = "macos",
67  all(target_os = "linux", feature = "systemd")
68))]
69impl Default for InstallDir {
70  fn default() -> Self {
71    #[cfg(target_os = "macos")]
72    return InstallDir::GlobalDaemon;
73
74    #[cfg(all(target_os = "linux", feature = "systemd"))]
75    return InstallDir::System;
76  }
77}
78
79#[cfg(any(
80  target_os = "macos",
81  all(target_os = "linux", feature = "systemd")
82))]
83impl fmt::Display for InstallDir {
84  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85    let s = match self {
86      #[cfg(target_os = "macos")]
87      InstallDir::UserAgent => "~/Library/LaunchAgents",
88      #[cfg(target_os = "macos")]
89      InstallDir::GlobalAgent => "/Library/LaunchAgents",
90      #[cfg(target_os = "macos")]
91      InstallDir::GlobalDaemon => "/Library/LaunchDaemons",
92
93      #[cfg(all(target_os = "linux", feature = "systemd"))]
94      InstallDir::System => "/etc/systemd/system",
95      #[cfg(all(target_os = "linux", feature = "systemd"))]
96      InstallDir::PublicUser => "/etc/systemd/user",
97      #[cfg(all(target_os = "linux", feature = "systemd"))]
98      InstallDir::PrivateUser => "~/.config/systemd/user"
99    };
100    write!(f, "{}", s)
101  }
102}
103*/
104
105
106/// What account to run the service as.
107///
108/// # Windows
109#[derive(Default)]
110pub enum Account {
111  /// Run as the highest privileged user available on system.
112  ///
113  /// On unixy systems, this means `root`.  On Windows, this means the
114  /// [LocalSystem](https://learn.microsoft.com/en-us/windows/win32/services/localsystem-account) account.
115  #[default]
116  System,
117
118  /// On Windows systems, run the service as the [LocalService](https://learn.microsoft.com/en-us/windows/win32/services/localservice-account) account.
119  #[cfg(windows)]
120  #[cfg_attr(docsrs, doc(cfg(windows)))]
121  Service,
122
123  /// On Windows systems, run the service as the [NetworkService](https://learn.microsoft.com/en-us/windows/win32/services/networkservice-account) account.
124  #[cfg(windows)]
125  #[cfg_attr(docsrs, doc(cfg(windows)))]
126  Network,
127
128  #[cfg(unix)]
129  User(String),
130
131  #[cfg(windows)]
132  UserAndPass(String, String)
133}
134
135
136#[derive(Debug, Default)]
137pub struct RunAs {
138  user: Option<String>,
139  group: Option<String>,
140
141  #[cfg(target_os = "macos")]
142  initgroups: bool,
143
144  #[cfg(any(
145    target_os = "macos",
146    all(target_os = "linux", feature = "systemd")
147  ))]
148  umask: Option<String>
149}
150
151
152#[cfg(windows)]
153pub type BoxRegCb =
154  Box<dyn FnOnce(&str, &mut winreg::RegKey) -> Result<(), Error>>;
155
156
157#[allow(clippy::struct_excessive_bools)]
158pub struct RegSvc {
159  /// If `true`, then attempt to forcibly install service.
160  pub force: bool,
161
162  /// Set to `true` if this service uses the qsu service argument parser.
163  ///
164  /// This will ensure that `run-service` is the first argument passed to the
165  /// service executable.
166  pub qsu_argp: bool,
167
168  pub svcname: String,
169
170  /// Service's display name.
171  ///
172  /// Only used on Windows.
173  pub display_name: Option<String>,
174
175  /// Service's description.
176  ///
177  /// Only used on Windows and on linux/systemd.
178  pub description: Option<String>,
179
180  /// Set to `true` if service supports configuration reloading.
181  pub conf_reload: bool,
182
183  /// Set to `true` if this is a network service.
184  ///
185  /// Note that this does not magically solve startup dependencies.
186  pub netservice: bool,
187
188  #[cfg(windows)]
189  pub regconf: Option<BoxRegCb>,
190
191  /// Command line arguments.
192  pub args: Vec<String>,
193
194  /// Environment variables.
195  pub envs: Vec<(String, String)>,
196
197  /// Set service to auto-start.
198  ///
199  /// By default the service will be registered, but needs to be started
200  /// manually.
201  pub autostart: bool,
202
203  pub(crate) workdir: Option<String>,
204
205  /// List of service dependencies.
206  deps: Vec<Depend>,
207
208  log_level: Option<LogLevel>,
209
210  trace_filter: Option<String>,
211
212  trace_file: Option<String>,
213
214  runas: RunAs
215}
216
217pub enum Depend {
218  Network,
219  Custom(Vec<String>)
220}
221
222impl RegSvc {
223  #[must_use]
224  pub fn new(svcname: &str) -> Self {
225    Self {
226      force: false,
227
228      qsu_argp: false,
229
230      svcname: svcname.to_string(),
231
232      display_name: None,
233
234      description: None,
235
236      conf_reload: false,
237
238      netservice: false,
239
240      #[cfg(windows)]
241      regconf: None,
242
243      args: Vec::new(),
244
245      envs: Vec::new(),
246
247      autostart: false,
248
249      workdir: None,
250
251      deps: Vec::new(),
252
253      log_level: None,
254
255      trace_filter: None,
256
257      trace_file: None,
258
259      runas: RunAs::default()
260    }
261  }
262
263  #[cfg(feature = "clap")]
264  #[allow(clippy::missing_panics_doc)]
265  pub fn from_cmd_match(matches: &ArgMatches) -> Self {
266    let force = matches.get_flag("force");
267
268    // unwrap should be okay, because svcname is mandatory
269    let svcname = matches.get_one::<String>("svcname").unwrap().to_owned();
270    let autostart = matches.get_flag("auto_start");
271
272    let dispname = matches.get_one::<String>("display_name");
273
274    let descr = matches.get_one::<String>("description");
275    let args: Vec<String> = matches
276      .get_many::<String>("arg")
277      .map_or_else(Vec::new, |vr| vr.map(String::from).collect());
278
279    let envs: Vec<String> = matches
280      .get_many::<String>("env")
281      .map_or_else(Vec::new, |vr| vr.map(String::from).collect());
282
283    /*
284      if let Some(vr) = matches.get_many::<String>("env")
285    {
286      vr.map(String::from).collect()
287    } else {
288      Vec::new()
289    };
290    */
291    let workdir = matches.get_one::<String>("workdir");
292
293    let mut environ = Vec::new();
294    let mut it = envs.into_iter();
295    while let Some((key, value)) = it.next_tuple() {
296      environ.push((key, value));
297    }
298
299    let log_level = matches.get_one::<LogLevel>("log_level").copied();
300    let trace_filter = matches.get_one::<String>("trace_filter").cloned();
301    let trace_file = matches.get_one::<String>("trace_file").cloned();
302
303    let runas = RunAs::default();
304
305    Self {
306      force,
307      qsu_argp: true,
308      svcname,
309      display_name: dispname.cloned(),
310      description: descr.cloned(),
311      conf_reload: false,
312      netservice: false,
313      #[cfg(windows)]
314      regconf: None,
315      args,
316      envs: environ,
317      autostart,
318      workdir: workdir.cloned(),
319      deps: Vec::new(),
320      log_level,
321      trace_filter,
322      trace_file,
323      runas
324    }
325  }
326
327  #[must_use]
328  pub fn svcname(&self) -> &str {
329    &self.svcname
330  }
331
332  /// Set the service's display name.
333  ///
334  /// This only has an effect on Windows.
335  #[must_use]
336  pub fn display_name(mut self, name: impl ToString) -> Self {
337    self.display_name_ref(name);
338    self
339  }
340
341  /// Set the service's _display name_.
342  ///
343  /// This only has an effect on Windows.
344  #[allow(clippy::needless_pass_by_value)]
345  pub fn display_name_ref(&mut self, name: impl ToString) -> &mut Self {
346    self.display_name = Some(name.to_string());
347    self
348  }
349
350  /// Set the service's description.
351  ///
352  /// This only has an effect on Windows and linux/systemd.
353  #[must_use]
354  pub fn description(mut self, text: impl ToString) -> Self {
355    self.description_ref(text);
356    self
357  }
358
359  /// Set the service's description.
360  ///
361  /// This only has an effect on Windows and linux/systemd.
362  #[allow(clippy::needless_pass_by_value)]
363  pub fn description_ref(&mut self, text: impl ToString) -> &mut Self {
364    self.description = Some(text.to_string());
365    self
366  }
367
368  /// Mark service as able to live reload its configuration.
369  #[must_use]
370  pub fn conf_reload(mut self) -> Self {
371    self.conf_reload_ref();
372    self
373  }
374
375  /// Mark service as able to live reload its configuration.
376  pub fn conf_reload_ref(&mut self) -> &mut Self {
377    self.conf_reload = true;
378    self
379  }
380
381  /// Mark service as a network application.
382  ///
383  /// # Windows
384  /// Calling this will implicitly add a `Tcpip` service dependency.
385  #[must_use]
386  pub fn netservice(mut self) -> Self {
387    self.netservice_ref();
388    self
389  }
390
391  /// Mark service as a network application.
392  ///
393  /// # Windows
394  /// Calling this will implicitly add a `Tcpip` service dependency.
395  pub fn netservice_ref(&mut self) -> &mut Self {
396    self.netservice = true;
397
398    #[cfg(windows)]
399    self.deps.push(Depend::Network);
400
401    self
402  }
403
404  /// Register a callback that will be used to set service registry keys.
405  #[cfg(windows)]
406  #[cfg_attr(docsrs, doc(cfg(windows)))]
407  #[must_use]
408  pub fn regconf<F>(mut self, f: F) -> Self
409  where
410    F: FnOnce(&str, &mut winreg::RegKey) -> Result<(), Error> + 'static
411  {
412    self.regconf = Some(Box::new(f));
413    self
414  }
415
416  /// Register a callback that will be used to set service registry keys.
417  #[cfg(windows)]
418  #[cfg_attr(docsrs, doc(cfg(windows)))]
419  pub fn regconf_ref<F>(&mut self, f: F) -> &mut Self
420  where
421    F: FnOnce(&str, &mut winreg::RegKey) -> Result<(), Error> + 'static
422  {
423    self.regconf = Some(Box::new(f));
424    self
425  }
426
427  /// Append a service command line argument.
428  #[allow(clippy::needless_pass_by_value)]
429  #[must_use]
430  pub fn arg(mut self, arg: impl ToString) -> Self {
431    self.args.push(arg.to_string());
432    self
433  }
434
435  /// Append a service command line argument.
436  #[allow(clippy::needless_pass_by_value)]
437  pub fn arg_ref(&mut self, arg: impl ToString) -> &mut Self {
438    self.args.push(arg.to_string());
439    self
440  }
441
442  /// Append service command line arguments.
443  #[must_use]
444  pub fn args<I, S>(mut self, args: I) -> Self
445  where
446    I: IntoIterator<Item = S>,
447    S: ToString
448  {
449    for arg in args {
450      self.args.push(arg.to_string());
451    }
452    self
453  }
454
455  /// Append service command line arguments.
456  pub fn args_ref<I, S>(&mut self, args: I) -> &mut Self
457  where
458    I: IntoIterator<Item = S>,
459    S: ToString
460  {
461    for arg in args {
462      self.arg_ref(arg.to_string());
463    }
464    self
465  }
466
467  #[must_use]
468  pub fn have_args(&self) -> bool {
469    !self.args.is_empty()
470  }
471
472  /// Add a service environment variable.
473  #[allow(clippy::needless_pass_by_value)]
474  #[must_use]
475  pub fn env<K, V>(mut self, key: K, val: V) -> Self
476  where
477    K: ToString,
478    V: ToString
479  {
480    self.envs.push((key.to_string(), val.to_string()));
481    self
482  }
483
484  /// Add a service environment variable.
485  #[allow(clippy::needless_pass_by_value)]
486  pub fn env_ref<K, V>(&mut self, key: K, val: V) -> &mut Self
487  where
488    K: ToString,
489    V: ToString
490  {
491    self.envs.push((key.to_string(), val.to_string()));
492    self
493  }
494
495  /// Add service environment variables.
496  #[must_use]
497  pub fn envs<I, K, V>(mut self, envs: I) -> Self
498  where
499    I: IntoIterator<Item = (K, V)>,
500    K: ToString,
501    V: ToString
502  {
503    for (key, val) in envs {
504      self.envs.push((key.to_string(), val.to_string()));
505    }
506    self
507  }
508
509  /// Add service environment variables.
510  pub fn envs_ref<I, K, V>(&mut self, args: I) -> &mut Self
511  where
512    I: IntoIterator<Item = (K, V)>,
513    K: ToString,
514    V: ToString
515  {
516    for (key, val) in args {
517      self.env_ref(key.to_string(), val.to_string());
518    }
519    self
520  }
521
522  #[must_use]
523  pub fn have_envs(&self) -> bool {
524    !self.envs.is_empty()
525  }
526
527  /// Mark service to auto-start on boot.
528  #[must_use]
529  pub const fn autostart(mut self) -> Self {
530    self.autostart = true;
531    self
532  }
533
534  /// Mark service to auto-start on boot.
535  pub fn autostart_ref(&mut self) -> &mut Self {
536    self.autostart = true;
537    self
538  }
539
540  /// Sets the work directory that the service should start in.
541  ///
542  /// This is a utf-8 string rather than a `Path` or `PathBuf` because the
543  /// directory tends to end up in places that have an utf-8 constraint.
544  #[allow(clippy::needless_pass_by_value)]
545  #[must_use]
546  pub fn workdir(mut self, workdir: impl ToString) -> Self {
547    self.workdir = Some(workdir.to_string());
548    self
549  }
550
551  /// In-place version of [`Self::workdir()`].
552  #[allow(clippy::needless_pass_by_value)]
553  pub fn workdir_ref(&mut self, workdir: impl ToString) -> &mut Self {
554    self.workdir = Some(workdir.to_string());
555    self
556  }
557
558  /// Add a service dependency.
559  ///
560  /// Has no effect on macos.
561  #[must_use]
562  pub fn depend(mut self, dep: Depend) -> Self {
563    self.deps.push(dep);
564    self
565  }
566
567  /// Add a service dependency.
568  ///
569  /// Has no effect on macos.
570  pub fn depend_ref(&mut self, dep: Depend) -> &mut Self {
571    self.deps.push(dep);
572    self
573  }
574
575  /// Perform the service registration.
576  ///
577  /// # Errors
578  /// The error may be system/service subsystem specific.
579  pub fn register(self) -> Result<(), Error> {
580    #[cfg(windows)]
581    winsvc::install(self)?;
582
583    #[cfg(target_os = "macos")]
584    launchd::install(self)?;
585
586    #[cfg(all(target_os = "linux", feature = "systemd"))]
587    systemd::install(self)?;
588
589    Ok(())
590  }
591}
592
593
594/// Deregister a service from a service subsystem.
595///
596/// # Errors
597/// The error may be system/service subsystem specific.
598#[allow(unreachable_code)]
599pub fn uninstall(svcname: &str) -> Result<(), Error> {
600  #[cfg(windows)]
601  {
602    winsvc::uninstall(svcname)?;
603    return Ok(());
604  }
605
606  #[cfg(target_os = "macos")]
607  {
608    launchd::uninstall(svcname)?;
609    return Ok(());
610  }
611
612  #[cfg(all(target_os = "linux", feature = "systemd"))]
613  {
614    systemd::uninstall(svcname)?;
615    return Ok(());
616  }
617
618  Err(Error::Unsupported)
619}
620
621// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 :