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 :