qsu/
argp.rs

1//! Helpers for integrating clap into an application using _qsu_.
2//!
3//! The purpose of the argument parser is to allow a semi-standard command line
4//! interface for registering, deregistering and allowing services to run both
5//! in the foreground as well as run as actual services.  To accomplish this,
6//! the argument parser wraps around the runtime.  Specifically, this means
7//! that if the argument parser is used, the application does not need to
8//! launch the runtime explicitly; the argument parser will do this if the
9//! appropriate arguments have been passed to it.
10//!
11//! Assuming a service handler has been implemented already, a simple use-case
12//! for the argument parser in a server application involves:
13//! 1. Implement the [`ArgsProc`] trait on an application object, and implement
14//!    the [`ArgsProc::build_apprt()`] method.  This method should return a
15//!    [`SrvAppRt`] object, containing the service handler.
16//! 2. Create an instance of the [`ArgParser`], passing in the service name and
17//!    a reference to an [`ArgsProc`] object.
18//! 3. Call [`ArgParser::proc()`] to process the command line arguments.
19//!
20//! The argument parser will determine whether the caller requested to
21//! register/deregister the service, run the server application in foreground
22//! mode or as a system service.
23//!
24//! # Customization
25//! While the argument parser in _qsu_ is rather opinionated, it does allow
26//! for _some_ customization.  The names of the subcommands to (de)register or
27//! run the server application as a service can be modified by the
28//! application.  It is possible to register custom subcommands, and the
29//! command line parser specification can be modified by the [`ArgsProc`]
30//! callbacks.
31
32use clap::{builder::Str, Arg, ArgAction, ArgMatches, Args, Command};
33
34use crate::{
35  err::CbErr,
36  installer::{self, RegSvc},
37  lumberjack::LogLevel,
38  rt::{RunCtx, SrvAppRt}
39};
40
41
42/// Modify a `clap` [`Command`] instance to accept common service management
43/// subcommands.
44///
45/// If `inst_subcmd` is `Some()`, it should be the name of the subcommand used
46/// to register a service.  If `rm_subcmd` is `Some()` it should be the name of
47/// the subcommand used to deregister a service.  Similarly `run_subcmd` is
48/// used to add a subcommand for running the service under a service subsystem
49/// [where applicable].
50///
51/// It is recommended that applications use the higher-level `ArgParser`
52/// instead.
53#[must_use]
54pub fn add_subcommands(
55  cli: Command,
56  svcname: &str,
57  inst_subcmd: Option<&str>,
58  rm_subcmd: Option<&str>,
59  run_subcmd: Option<&str>
60) -> Command {
61  let cli = if let Some(subcmd) = inst_subcmd {
62    let sub = mk_inst_cmd(subcmd, svcname);
63    cli.subcommand(sub)
64  } else {
65    cli
66  };
67
68  let cli = if let Some(subcmd) = rm_subcmd {
69    let sub = mk_rm_cmd(subcmd, svcname);
70    cli.subcommand(sub)
71  } else {
72    cli
73  };
74
75  if let Some(subcmd) = run_subcmd {
76    let sub = mk_run_cmd(subcmd, svcname);
77    cli.subcommand(sub)
78  } else {
79    cli
80  }
81}
82
83/// Register service.
84#[derive(Debug, Args)]
85struct RegSvcArgs {
86  /// Autostart service at boot.
87  #[arg(short = 's', long)]
88  auto_start: bool,
89
90  /// Set an optional display name for the service.
91  ///
92  /// Only used on Windows.
93  #[arg(short = 'D', long, value_name = "DISPNAME")]
94  display_name: Option<String>,
95
96  /// Set an optional one-line service description.
97  ///
98  /// Only used on Windows and Linux with systemd.
99  #[arg(short, long, value_name = "DESC")]
100  description: Option<String>,
101
102  /// Add a command line argument to the service command line.
103  #[arg(short, long)]
104  arg: Vec<String>,
105
106  /// Add an environment variable to the service.
107  #[arg(short, long, num_args(2), value_names=["KEY", "VALUE"])]
108  env: Vec<String>,
109
110  /// Set an optional directory the service runtime should start in.
111  #[arg(short, long, value_name = "DIR")]
112  workdir: Option<String>,
113
114  #[arg(long, value_enum, value_name = "LEVEL")]
115  log_level: Option<LogLevel>,
116
117  #[arg(long, hide(true), value_name = "FILTER")]
118  trace_filter: Option<String>,
119
120  #[arg(long, value_enum, hide(true), value_name = "FNAME")]
121  trace_file: Option<String>,
122
123  /// Forcibly install service.
124  ///
125  /// On Windows, attempt to stop and uninstall service if it already exists.
126  ///
127  /// On macos/launchd and linux/systemd, overwrite the service specification
128  /// file if it already exists.
129  #[arg(long, short)]
130  force: bool
131}
132
133/// Create a `clap` [`Command`] object that accepts service registration
134/// arguments.
135///
136/// It is recommended that applications use the higher-level `ArgParser`
137/// instead, but this call exists in case applications need finer grained
138/// control.
139#[must_use]
140pub fn mk_inst_cmd(cmd: &str, svcname: &str) -> Command {
141  let namearg = Arg::new("svcname")
142    .short('n')
143    .long("name")
144    .action(ArgAction::Set)
145    .value_name("SVCNAME")
146    .default_value(Str::from(svcname.to_string()))
147    .help("Set service name");
148
149  //Command::new(cmd).arg(namearg).arg(autostartarg)
150  let cli = Command::new(cmd.to_string()).arg(namearg);
151
152  RegSvcArgs::augment_args(cli)
153}
154
155
156/// Deregister service.
157#[derive(Debug, Args)]
158struct DeregSvcArgs {}
159
160/// Create a `clap` [`Command`] object that accepts service deregistration
161/// arguments.
162///
163/// It is recommended that applications use the higher-level `ArgParser`
164/// instead, but this call exists in case applications need finer grained
165/// control.
166#[must_use]
167pub fn mk_rm_cmd(cmd: &str, svcname: &str) -> Command {
168  let namearg = Arg::new("svcname")
169    .short('n')
170    .long("name")
171    .action(ArgAction::Set)
172    .value_name("SVCNAME")
173    .default_value(svcname.to_string())
174    .help("Name of service to remove");
175
176  let cli = Command::new(cmd.to_string()).arg(namearg);
177
178  DeregSvcArgs::augment_args(cli)
179}
180
181
182/// Parsed service deregistration arguments.
183pub struct DeregSvc {
184  pub svcname: String
185}
186
187impl DeregSvc {
188  #[allow(
189    clippy::missing_panics_doc,
190    reason = "svcname should always have been set"
191  )]
192  #[must_use]
193  pub fn from_cmd_match(matches: &ArgMatches) -> Self {
194    // unwrap() should be okay here, because svcname should always be set.
195    let svcname = matches.get_one::<String>("svcname").unwrap().to_owned();
196    Self { svcname }
197  }
198}
199
200
201/// Run service.
202#[derive(Debug, Args)]
203struct RunSvcArgs {}
204
205
206/// Create a `clap` [`Command`] object that accepts service running arguments.
207///
208/// It is recommended that applications use the higher-level `ArgParser`
209/// instead, but this call exists in case applications need finer grained
210/// control.
211#[must_use]
212pub fn mk_run_cmd(cmd: &str, svcname: &str) -> Command {
213  let namearg = Arg::new("svcname")
214    .short('n')
215    .long("name")
216    .action(ArgAction::Set)
217    .value_name("SVCNAME")
218    .default_value(svcname.to_string())
219    .help("Service name");
220
221  let cli = Command::new(cmd.to_string()).arg(namearg);
222
223  RunSvcArgs::augment_args(cli)
224}
225
226
227pub(crate) enum ArgpRes<'cb, ApEr> {
228  /// Run server application.
229  RunApp(RunCtx, &'cb mut dyn ArgsProc<AppErr = ApEr>),
230
231  /// Nothing to do (service was probably registered/deregistred).
232  Quit
233}
234
235
236/// Parsed service running arguments.
237pub struct RunSvc {
238  pub svcname: String
239}
240
241impl RunSvc {
242  #[allow(
243    clippy::missing_panics_doc,
244    reason = "svcname should always have been set"
245  )]
246  #[must_use]
247  pub fn from_cmd_match(matches: &ArgMatches) -> Self {
248    let svcname = matches.get_one::<String>("svcname").unwrap().to_owned();
249    Self { svcname }
250  }
251}
252
253
254/// Used to differentiate between running without a subcommand, or the
255/// install/uninstall or run service subcommands.
256pub enum Cmd {
257  /// The root `Command`.
258  Root,
259
260  /// The service registration/installation `Command`.
261  Inst,
262
263  /// The service deregistration/uninstallation `Command`.
264  Rm,
265
266  /// The service run `Command`.
267  Run
268}
269
270/// Allow application to customise behavior of an [`ArgParser`] instance.
271pub trait ArgsProc {
272  type AppErr;
273
274  /// Give the application an opportunity to modify the root and subcommand
275  /// `Command`s.
276  ///
277  /// `cmdtype` indicates whether `cmd` is the root `Command` or one of the
278  /// subcommand `Command`s.
279  ///
280  /// # Errors
281  /// Application-defined error will be returned as `CbErr::Aop` to the
282  /// original caller.
283  #[allow(unused_variables)]
284  fn conf_cmd(
285    &mut self,
286    cmdtype: Cmd,
287    cmd: Command
288  ) -> Result<Command, Self::AppErr> {
289    Ok(cmd)
290  }
291
292  /// Callback allowing application to configure the service registration
293  /// context just before the service is registered.
294  ///
295  /// This trait method can, among other things, be used by an application to:
296  /// - Configure a service work directory.
297  /// - Add environment variables.
298  /// - Add command like arguments to the run command.
299  ///
300  /// The `sub_m` argument represents `clap`'s parsed subcommand context for
301  /// the service registration subcommand.  Applications that want to add
302  /// custom arguments to the parser should implement the
303  /// [`ArgsProc::conf_cmd()`] trait method and perform the subcommand
304  /// augmentation there.
305  ///
306  /// This callback is called after the system-defined command line arguments
307  /// have been parsed and populated into `regsvc`.  Implementation should be
308  /// careful not to overwrite settings that should be configurable via the
309  /// command line, and can inspect the current value of fields before setting
310  /// them if conditional reconfiguations are required.
311  ///
312  /// The default implementation does nothing but return `regsvc` unmodified.
313  ///
314  /// # Errors
315  /// Application-defined error will be returned as `CbErr::Aop` to the
316  /// original caller.
317  #[allow(unused_variables)]
318  fn proc_inst(
319    &mut self,
320    sub_m: &ArgMatches,
321    regsvc: RegSvc
322  ) -> Result<RegSvc, Self::AppErr> {
323    Ok(regsvc)
324  }
325
326  /// Callback allowing application to configure the service deregistration
327  /// context just before the service is deregistered.
328  ///
329  /// # Errors
330  /// Application-defined error will be returned as `CbErr::Aop` to the
331  /// original caller.
332  #[allow(unused_variables)]
333  fn proc_rm(
334    &mut self,
335    sub_m: &ArgMatches,
336    deregsvc: DeregSvc
337  ) -> Result<DeregSvc, Self::AppErr> {
338    Ok(deregsvc)
339  }
340
341  /// Callback allowing application to configure the run context before
342  /// launching the server application.
343  ///
344  /// qsu will have performed all its own initialization of the [`RunCtx`]
345  /// before calling this function.
346  ///
347  /// The application can differentiate between running in a service mode and
348  /// running as a foreground by calling [`RunCtx::is_service()`].
349  ///
350  /// # Errors
351  /// Application-defined error will be returned as `CbErr::Aop` to the
352  /// original caller.
353  #[allow(unused_variables)]
354  fn proc_run(
355    &mut self,
356    matches: &ArgMatches,
357    runctx: RunCtx
358  ) -> Result<RunCtx, Self::AppErr> {
359    Ok(runctx)
360  }
361
362  /// Called when a subcommand is encountered that is not one of the three
363  /// subcommands regognized by qsu.
364  ///
365  /// # Errors
366  /// Application-defined error will be returned as `CbErr::Aop` to the
367  /// original caller.
368  #[allow(unused_variables)]
369  fn proc_other(
370    &mut self,
371    subcmd: &str,
372    sub_m: &ArgMatches
373  ) -> Result<(), Self::AppErr> {
374    Ok(())
375  }
376
377  /// Construct an server application runtime.
378  ///
379  /// # Errors
380  /// Application-defined error will be returned as `CbErr::Aop` to the
381  /// original caller.
382  fn build_apprt(&mut self) -> Result<SrvAppRt<Self::AppErr>, Self::AppErr>;
383}
384
385
386/// High-level argument parser.
387///
388/// This is suitable for applications that follow a specific pattern:
389/// - It has subcommands for:
390///   - Registering a service
391///   - Deregistering a service
392///   - Running as a service
393/// - Running without any subcommands should run the server application as a
394///   foreground process.
395pub struct ArgParser<'cb, ApEr> {
396  svcname: String,
397  reg_subcmd: String,
398  dereg_subcmd: String,
399  run_subcmd: String,
400  cli: Command,
401  cb: &'cb mut dyn ArgsProc<AppErr = ApEr>,
402  regcb: Option<Box<dyn FnOnce(RegSvc) -> RegSvc>>
403}
404
405impl<'cb, ApEr> ArgParser<'cb, ApEr>
406where
407  ApEr: Send + 'static
408{
409  /// Create a new argument parser.
410  ///
411  /// `svcname` is the _default_ service name.  It may be overridden using
412  /// command line arguments.
413  pub fn new(svcname: &str, cb: &'cb mut dyn ArgsProc<AppErr = ApEr>) -> Self {
414    let cli = Command::new("");
415    Self {
416      svcname: svcname.to_string(),
417      reg_subcmd: "register-service".into(),
418      dereg_subcmd: "deregister-service".into(),
419      run_subcmd: "run-service".into(),
420      cli,
421      cb,
422      regcb: None
423    }
424  }
425
426
427  /// Create a new argument parser, basing the root command parser on an
428  /// application-supplied `Command`.
429  ///
430  /// `svcname` is the _default_ service name.  It may be overridden using
431  /// command line arguments.
432  pub fn with_cmd(
433    svcname: &str,
434    cli: Command,
435    cb: &'cb mut dyn ArgsProc<AppErr = ApEr>
436  ) -> Self {
437    Self {
438      svcname: svcname.to_string(),
439      reg_subcmd: "register-service".into(),
440      dereg_subcmd: "deregister-service".into(),
441      run_subcmd: "run-service".into(),
442      cli,
443      cb,
444      regcb: None
445    }
446  }
447
448  /// Rename the service registration subcommand.
449  #[must_use]
450  pub fn reg_subcmd(mut self, nm: &str) -> Self {
451    self.reg_subcmd = nm.to_string();
452    self
453  }
454
455  /// Rename the service deregistration subcommand.
456  #[must_use]
457  pub fn dereg_subcmd(mut self, nm: &str) -> Self {
458    self.dereg_subcmd = nm.to_string();
459    self
460  }
461
462  /// Rename the subcommand for running the service.
463  #[must_use]
464  pub fn run_subcmd(mut self, nm: &str) -> Self {
465    self.run_subcmd = nm.to_string();
466    self
467  }
468
469  fn inner_proc(self) -> Result<ArgpRes<'cb, ApEr>, CbErr<ApEr>> {
470    let matches = match self.cli.try_get_matches() {
471      Ok(m) => m,
472      Err(e) => match e.kind() {
473        clap::error::ErrorKind::DisplayHelp
474        | clap::error::ErrorKind::DisplayVersion => {
475          e.exit();
476        }
477        _ => {
478          // ToDo: Convert error to Error::ArgP, pass along the error type so
479          // that the Termination handler can output the specific error.
480          //Err(e)?;
481          e.exit();
482        }
483      }
484    };
485    match matches.subcommand() {
486      Some((subcmd, sub_m)) if subcmd == self.reg_subcmd => {
487        //println!("{:#?}", sub_m);
488
489        // Convert known register-service command line arguments to a RegSvc
490        // context.
491        let mut regsvc = RegSvc::from_cmd_match(sub_m);
492
493        // To trigger the server to run in service mode, run with the
494        // subcommand "run-service" (or whatever it has been changed to).
495        //
496        // We're making a pretty safe assumption that the service will
497        // be run though qsu's argument processor because it is being
498        // registered using it.
499        //
500        // If the service name is different that the name drived from the
501        // executable's name, then add "--name <svcname>" arguments.
502        let mut args = vec![String::from(&self.run_subcmd)];
503        if regsvc.svcname() != self.svcname {
504          args.push(String::from("--name"));
505          args.push(regsvc.svcname().to_string());
506        }
507        regsvc.args_ref(args);
508
509        // Call application call-back, to allow application-specific
510        // service configuration.
511        //
512        // This is a good place to stick custom environment, arguments,
513        // registry changes that may depend on command line arguments.
514        let regsvc = self
515          .cb
516          .proc_inst(sub_m, regsvc)
517          .map_err(|ae| CbErr::App(ae))?;
518
519        // Give the application a final chance to modify the service
520        // registration parameters.
521        //
522        // This can be used to set hardcoded parameters like service display
523        // name, description, etc.
524        let regsvc = if let Some(cb) = self.regcb {
525          cb(regsvc)
526        } else {
527          regsvc
528        };
529
530        // Register the service with the operating system's service subsystem.
531        regsvc.register()?;
532
533        Ok(ArgpRes::Quit)
534      }
535      Some((subcmd, sub_m)) if subcmd == self.dereg_subcmd => {
536        // Get arguments relating to service deregistration.
537        let args = DeregSvc::from_cmd_match(sub_m);
538
539        let args =
540          self.cb.proc_rm(sub_m, args).map_err(|ae| CbErr::App(ae))?;
541
542        installer::uninstall(&args.svcname)?;
543
544        Ok(ArgpRes::Quit)
545      }
546      Some((subcmd, sub_m)) if subcmd == self.run_subcmd => {
547        // Get arguments relating to running the service.
548        let args = RunSvc::from_cmd_match(sub_m);
549
550        // Create a RunCtx, mark it as a service, and allow application the
551        // opportunity to modify it based on the parsed command line.
552        let rctx = RunCtx::new(&args.svcname).service();
553        let rctx =
554          self.cb.proc_run(sub_m, rctx).map_err(|ae| CbErr::App(ae))?;
555
556        // Return a run context for a background service process.
557        Ok(ArgpRes::RunApp(rctx, self.cb))
558      }
559      Some((subcmd, sub_m)) => {
560        // Call application callback for processing "other" subcmd
561        self
562          .cb
563          .proc_other(subcmd, sub_m)
564          .map_err(|ae| CbErr::App(ae))?;
565
566        // Return a run context for a background service process.
567        Ok(ArgpRes::Quit)
568      }
569      _ => {
570        // Create a RunCtx, mark it as a service, and allow application the
571        // opportunity to modify it based on the parsed command line.
572        let rctx = RunCtx::new(&self.svcname);
573        let rctx = self
574          .cb
575          .proc_run(&matches, rctx)
576          .map_err(|ae| CbErr::App(ae))?;
577
578        // Return a run context for a foreground process.
579        Ok(ArgpRes::RunApp(rctx, self.cb))
580      }
581    }
582  }
583
584  /// Register a closure that will be called before service registration.
585  ///
586  /// This callback serves a different purpose than implementing the
587  /// [`ArgsProc::proc_inst()`] method, which is primarily meant to tranlate
588  /// command line arguments to service registration parameters.
589  ///
590  /// The `regsvc_proc()` callback on the other hand is called just before
591  /// actual registration and is intended to overwrite service registration
592  /// parametmer.  It is suitable to use this callback to set hard-coded
593  /// application-specific service parameters, like service display name,
594  /// description, dependencies, and other parameters that the user should
595  /// not need to specify manually.
596  ///
597  /// Just as with `ArgsProc::proc_inst()` this callback is called after the
598  /// system-defined command line arguments have been parsed and populated
599  /// into the [`RegSvc`] context.  Closures should be careful not to overwrite
600  /// settings that should be configurable via the command line, and can
601  /// inspect the current value of fields before setting them if conditional
602  /// reconfiguations are required.
603  #[must_use]
604  pub fn regsvc_proc(
605    mut self,
606    f: impl FnOnce(RegSvc) -> RegSvc + 'static
607  ) -> Self {
608    self.regcb = Some(Box::new(f));
609    self
610  }
611
612  /// Process command line arguments.
613  ///
614  /// Calling this method will initialize the command line parser, parse the
615  /// command line, using the associated [`ArgsProc`] as appropriate to modify
616  /// the argument parser, and then take the appropriate action:
617  ///
618  /// - If a service registration was requested, the service will be registered
619  ///   and then the function will return.
620  /// - If a service deregistration was requested, the service will be
621  ///   deregistered and then the function will return.
622  /// - If a service run was requested, then set up the service subsystem and
623  ///   launch the server application under it.
624  /// - If an application-defined subcommand was called, then process it using
625  ///   [`ArgsProc::proc_other()`] and then exit.
626  /// - If none of the above subcommands where issued, then run the server
627  ///   application as a foreground process.
628  ///
629  /// The `bldr` is a closure that will be called to yield the `SrvAppRt` in
630  /// case the service was requested to run.
631  ///
632  /// # Service registration behavior
633  /// The default service registration behavior in qsu will:
634  /// - Assume that the executable being used to register the service is the
635  ///   same one that will run the service.
636  /// - Add the "run service" subcommand to the service's command line
637  ///   arguments.
638  /// - If the specified service name is different than the default service
639  ///   name (determined by
640  ///   [`default_service_name()`](crate::default_service_name), then the
641  ///   aguments `--name <service name>` will be added.
642  ///
643  /// # Errors
644  /// Application-defined error will be returned as `CbErr::Aop` to the
645  /// original caller.
646  pub fn proc(mut self) -> Result<(), CbErr<ApEr>>
647  where
648    ApEr: std::fmt::Debug
649  {
650    // Give application the opportunity to modify root Command
651    self.cli = self
652      .cb
653      .conf_cmd(Cmd::Root, self.cli)
654      .map_err(|ae| CbErr::App(ae))?;
655
656    // Create registration subcommand and give application the opportunity to
657    // modify the subcommand's Command
658    let sub = mk_inst_cmd(&self.reg_subcmd, &self.svcname);
659    let sub = self
660      .cb
661      .conf_cmd(Cmd::Inst, sub)
662      .map_err(|ae| CbErr::App(ae))?;
663    self.cli = self.cli.subcommand(sub);
664
665    // Create deregistration subcommand
666    let sub = mk_rm_cmd(&self.dereg_subcmd, &self.svcname);
667    let sub = self
668      .cb
669      .conf_cmd(Cmd::Rm, sub)
670      .map_err(|ae| CbErr::App(ae))?;
671    self.cli = self.cli.subcommand(sub);
672
673    // Create run subcommand
674    let sub = mk_run_cmd(&self.run_subcmd, &self.svcname);
675    let sub = self
676      .cb
677      .conf_cmd(Cmd::Run, sub)
678      .map_err(|ae| CbErr::App(ae))?;
679    self.cli = self.cli.subcommand(sub);
680
681    // Parse command line arguments.  Run the service application if requiested
682    // to do so.
683    if let ArgpRes::RunApp(runctx, cb) = self.inner_proc()? {
684      // Argument parser asked us to run, so call the application to ask it to
685      // create the service handler, and then kick off the service runtime.
686      //let st = bldr(ctx)?;
687
688      let st = cb.build_apprt().map_err(|ae| CbErr::App(ae))?;
689
690      runctx.run(st)?;
691    }
692    Ok(())
693  }
694}
695
696// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 :