Skip to main content

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 windows_registry::Key) -> 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 `--as-service` is  passed to the service
165  /// 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  log_filter: Option<String>,
211
212  trace_filter: Option<String>,
213
214  trace_file: Option<String>,
215
216  runas: RunAs
217}
218
219pub enum Depend {
220  Network,
221  Custom(Vec<String>)
222}
223
224impl RegSvc {
225  #[must_use]
226  pub fn new(svcname: &str) -> Self {
227    Self {
228      force: false,
229
230      qsu_argp: false,
231
232      svcname: svcname.to_string(),
233
234      display_name: None,
235
236      description: None,
237
238      conf_reload: false,
239
240      netservice: false,
241
242      #[cfg(windows)]
243      regconf: None,
244
245      args: Vec::new(),
246
247      envs: Vec::new(),
248
249      autostart: false,
250
251      workdir: None,
252
253      deps: Vec::new(),
254
255      log_level: None,
256
257      log_filter: None,
258
259      trace_filter: None,
260
261      trace_file: None,
262
263      runas: RunAs::default()
264    }
265  }
266
267  #[cfg(feature = "clap")]
268  #[allow(clippy::missing_panics_doc)]
269  pub fn from_cmd_match(matches: &ArgMatches) -> Self {
270    let force = matches.get_flag("force");
271
272    // unwrap should be okay, because svcname is mandatory
273    let svcname = matches.get_one::<String>("svcname").unwrap().to_owned();
274    let autostart = matches.get_flag("auto_start");
275
276    let dispname = matches.get_one::<String>("display_name");
277
278    let descr = matches.get_one::<String>("description");
279    let args: Vec<String> = matches
280      .get_many::<String>("arg")
281      .map_or_else(Vec::new, |vr| vr.map(String::from).collect());
282
283    let envs: Vec<String> = matches
284      .get_many::<String>("env")
285      .map_or_else(Vec::new, |vr| vr.map(String::from).collect());
286
287    /*
288      if let Some(vr) = matches.get_many::<String>("env")
289    {
290      vr.map(String::from).collect()
291    } else {
292      Vec::new()
293    };
294    */
295    let workdir = matches.get_one::<String>("workdir");
296
297    let mut environ = Vec::new();
298    let mut it = envs.into_iter();
299    while let Some((key, value)) = it.next_tuple() {
300      environ.push((key, value));
301    }
302
303    let log_level = matches.get_one::<LogLevel>("log_level").copied();
304    let log_filter = matches.get_one::<String>("log_filter").cloned();
305    let trace_filter = matches.get_one::<String>("trace_filter").cloned();
306    let trace_file = matches.get_one::<String>("trace_file").cloned();
307
308    let runas = RunAs::default();
309
310    Self {
311      force,
312      qsu_argp: true,
313      svcname,
314      display_name: dispname.cloned(),
315      description: descr.cloned(),
316      conf_reload: false,
317      netservice: false,
318      #[cfg(windows)]
319      regconf: None,
320      args,
321      envs: environ,
322      autostart,
323      workdir: workdir.cloned(),
324      deps: Vec::new(),
325      log_level,
326      log_filter,
327      trace_filter,
328      trace_file,
329      runas
330    }
331  }
332
333  #[must_use]
334  pub fn svcname(&self) -> &str {
335    &self.svcname
336  }
337
338  /// Set the service's display name.
339  ///
340  /// This only has an effect on Windows.
341  #[must_use]
342  pub fn display_name(mut self, name: impl ToString) -> Self {
343    self.display_name_ref(name);
344    self
345  }
346
347  /// Set the service's _display name_.
348  ///
349  /// This only has an effect on Windows.
350  #[allow(clippy::needless_pass_by_value)]
351  pub fn display_name_ref(&mut self, name: impl ToString) -> &mut Self {
352    self.display_name = Some(name.to_string());
353    self
354  }
355
356  /// Set the service's description.
357  ///
358  /// This only has an effect on Windows and linux/systemd.
359  #[must_use]
360  pub fn description(mut self, text: impl ToString) -> Self {
361    self.description_ref(text);
362    self
363  }
364
365  /// Set the service's description.
366  ///
367  /// This only has an effect on Windows and linux/systemd.
368  #[allow(clippy::needless_pass_by_value)]
369  pub fn description_ref(&mut self, text: impl ToString) -> &mut Self {
370    self.description = Some(text.to_string());
371    self
372  }
373
374  /// Mark service as able to live reload its configuration.
375  #[must_use]
376  pub const fn conf_reload(mut self) -> Self {
377    self.conf_reload_ref();
378    self
379  }
380
381  /// Mark service as able to live reload its configuration.
382  pub const fn conf_reload_ref(&mut self) -> &mut Self {
383    self.conf_reload = true;
384    self
385  }
386
387  /// Mark service as a network application.
388  ///
389  /// # Windows
390  /// Calling this will implicitly add a `Tcpip` service dependency.
391  #[must_use]
392  pub fn netservice(mut self) -> Self {
393    self.netservice_ref();
394    self
395  }
396
397  /// Mark service as a network application.
398  ///
399  /// # Windows
400  /// Calling this will implicitly add a `Tcpip` service dependency.
401  #[cfg_attr(not(windows), allow(clippy::missing_const_for_fn))]
402  pub fn netservice_ref(&mut self) -> &mut Self {
403    self.netservice = true;
404
405    #[cfg(windows)]
406    self.deps.push(Depend::Network);
407
408    self
409  }
410
411  /// Register a callback that will be used to set service registry keys.
412  #[cfg(windows)]
413  #[cfg_attr(docsrs, doc(cfg(windows)))]
414  #[must_use]
415  pub fn regconf<F>(mut self, f: F) -> Self
416  where
417    F: FnOnce(&str, &mut windows_registry::Key) -> Result<(), Error> + 'static
418  {
419    self.regconf = Some(Box::new(f));
420    self
421  }
422
423  /// Register a callback that will be used to set service registry keys.
424  #[cfg(windows)]
425  #[cfg_attr(docsrs, doc(cfg(windows)))]
426  pub fn regconf_ref<F>(&mut self, f: F) -> &mut Self
427  where
428    F: FnOnce(&str, &mut windows_registry::Key) -> Result<(), Error> + 'static
429  {
430    self.regconf = Some(Box::new(f));
431    self
432  }
433
434  /// Append a service command line argument.
435  #[allow(clippy::needless_pass_by_value)]
436  #[must_use]
437  pub fn arg(mut self, arg: impl ToString) -> Self {
438    self.args.push(arg.to_string());
439    self
440  }
441
442  /// Append a service command line argument.
443  #[allow(clippy::needless_pass_by_value)]
444  pub fn arg_ref(&mut self, arg: impl ToString) -> &mut Self {
445    self.args.push(arg.to_string());
446    self
447  }
448
449  /// Append service command line arguments.
450  #[must_use]
451  pub fn args<I, S>(mut self, args: I) -> Self
452  where
453    I: IntoIterator<Item = S>,
454    S: ToString
455  {
456    for arg in args {
457      self.args.push(arg.to_string());
458    }
459    self
460  }
461
462  /// Append service command line arguments.
463  pub fn args_ref<I, S>(&mut self, args: I) -> &mut Self
464  where
465    I: IntoIterator<Item = S>,
466    S: ToString
467  {
468    for arg in args {
469      self.arg_ref(arg.to_string());
470    }
471    self
472  }
473
474  #[must_use]
475  pub const fn have_args(&self) -> bool {
476    !self.args.is_empty()
477  }
478
479  /// Add a service environment variable.
480  #[allow(clippy::needless_pass_by_value)]
481  #[must_use]
482  pub fn env<K, V>(mut self, key: K, val: V) -> Self
483  where
484    K: ToString,
485    V: ToString
486  {
487    self.envs.push((key.to_string(), val.to_string()));
488    self
489  }
490
491  /// Add a service environment variable.
492  #[allow(clippy::needless_pass_by_value)]
493  pub fn env_ref<K, V>(&mut self, key: K, val: V) -> &mut Self
494  where
495    K: ToString,
496    V: ToString
497  {
498    self.envs.push((key.to_string(), val.to_string()));
499    self
500  }
501
502  /// Add service environment variables.
503  #[must_use]
504  pub fn envs<I, K, V>(mut self, envs: I) -> Self
505  where
506    I: IntoIterator<Item = (K, V)>,
507    K: ToString,
508    V: ToString
509  {
510    for (key, val) in envs {
511      self.envs.push((key.to_string(), val.to_string()));
512    }
513    self
514  }
515
516  /// Add service environment variables.
517  pub fn envs_ref<I, K, V>(&mut self, args: I) -> &mut Self
518  where
519    I: IntoIterator<Item = (K, V)>,
520    K: ToString,
521    V: ToString
522  {
523    for (key, val) in args {
524      self.env_ref(key.to_string(), val.to_string());
525    }
526    self
527  }
528
529  #[must_use]
530  pub const fn have_envs(&self) -> bool {
531    !self.envs.is_empty()
532  }
533
534  /// Mark service to auto-start on boot.
535  #[must_use]
536  pub const fn autostart(mut self) -> Self {
537    self.autostart = true;
538    self
539  }
540
541  /// Mark service to auto-start on boot.
542  pub const fn autostart_ref(&mut self) -> &mut Self {
543    self.autostart = true;
544    self
545  }
546
547  /// Sets the work directory that the service should start in.
548  ///
549  /// This is a utf-8 string rather than a `Path` or `PathBuf` because the
550  /// directory tends to end up in places that have an utf-8 constraint.
551  #[allow(clippy::needless_pass_by_value)]
552  #[must_use]
553  pub fn workdir(mut self, workdir: impl ToString) -> Self {
554    self.workdir = Some(workdir.to_string());
555    self
556  }
557
558  /// In-place version of [`Self::workdir()`].
559  #[allow(clippy::needless_pass_by_value)]
560  pub fn workdir_ref(&mut self, workdir: impl ToString) -> &mut Self {
561    self.workdir = Some(workdir.to_string());
562    self
563  }
564
565  /// Add a service dependency.
566  ///
567  /// Has no effect on macos.
568  #[must_use]
569  pub fn depend(mut self, dep: Depend) -> Self {
570    self.deps.push(dep);
571    self
572  }
573
574  /// Add a service dependency.
575  ///
576  /// Has no effect on macos.
577  pub fn depend_ref(&mut self, dep: Depend) -> &mut Self {
578    self.deps.push(dep);
579    self
580  }
581
582  /// Perform the service registration.
583  ///
584  /// # Errors
585  /// The error may be system/service subsystem specific.
586  pub fn register(self) -> Result<(), Error> {
587    #[cfg(windows)]
588    winsvc::install(self)?;
589
590    #[cfg(target_os = "macos")]
591    launchd::install(self)?;
592
593    #[cfg(all(target_os = "linux", feature = "systemd"))]
594    systemd::install(self)?;
595
596    Ok(())
597  }
598}
599
600
601/// Deregister a service from a service subsystem.
602///
603/// # Errors
604/// The error may be system/service subsystem specific.
605#[allow(unreachable_code)]
606pub fn uninstall(svcname: &str) -> Result<(), Error> {
607  #[cfg(windows)]
608  {
609    winsvc::uninstall(svcname)?;
610    return Ok(());
611  }
612
613  #[cfg(target_os = "macos")]
614  {
615    launchd::uninstall(svcname)?;
616    return Ok(());
617  }
618
619  #[cfg(all(target_os = "linux", feature = "systemd"))]
620  {
621    systemd::uninstall(svcname)?;
622    return Ok(());
623  }
624
625  Err(Error::Unsupported)
626}
627
628// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 :