Index: Cargo.toml ================================================================== --- Cargo.toml +++ Cargo.toml @@ -1,8 +1,8 @@ [package] name = "qsu" -version = "0.0.3" +version = "0.0.4" edition = "2021" license = "0BSD" categories = [ "asynchronous" ] keywords = [ "service", "systemd", "winsvc" ] repository = "https://repos.qrnch.tech/pub/qsu" @@ -30,15 +30,15 @@ wait-for-debugger = ["dep:dbgtools-win"] [dependencies] async-trait = { version = "0.1.74" } chrono = { version = "0.4.24" } -clap = { version = "4.4.6", optional = true, features = [ +clap = { version = "4.4.7", optional = true, features = [ "derive", "env", "string", "wrap_help" ] } env_logger = { version = "0.10.0" } -futures = { version = "0.3.28" } +futures = { version = "0.3.29" } itertools = { version = "0.11.0", optional = true } killswitch = { version = "0.4.2" } log = { version = "0.4.20" } parking_lot = { version = "0.12.1" } rocket = { version = "0.5.0-rc.3", optional = true } Index: examples/argp/mod.rs ================================================================== --- examples/argp/mod.rs +++ examples/argp/mod.rs @@ -1,34 +1,51 @@ use clap::ArgMatches; -use qsu::installer::RegSvc; +use qsu::{installer::RegSvc, rt::SrvAppRt, AppErr}; + +use crate::err::Error; -pub(crate) struct AppArgsProc {} +pub(crate) struct AppArgsProc { + pub(crate) bldr: Box SrvAppRt> +} impl qsu::argp::ArgsProc for AppArgsProc { /// Process an `register-service` subcommand. fn proc_inst( - &self, + &mut self, _sub_m: &ArgMatches, regsvc: RegSvc - ) -> Result { - // Use current working directory as the service's workdir - let cwd = std::env::current_dir()?.to_str().unwrap().to_string(); - let regsvc = regsvc - .workdir(cwd) - .env("HOLY", "COW") - .env("Private", "Public") - .env("General", "Specific"); - - // Add a callback that will increase log and trace levels by deafault. - #[cfg(windows)] - let regsvc = regsvc.regconf(|_svcname, params| { - params.set_value("AppArgParser", &"SaysHello")?; - - Ok(()) - }); - - Ok(regsvc) - } + ) -> Result { + // This is split out into its own function because the orphan rule wouldn't + // allow the application to implement a std::io::Error -> qsu::AppErr + // conversion in one go, so we do it in two steps instead. + // proc_inst_inner()'s '?' converts "all" errors into 'Error`. + // The proc_inst() method's `?` converts from `Error` to `qsu::AppError` + Ok(proc_inst_inner(regsvc)?) + } + + fn build_apprt(&mut self) -> Result { + Ok((self.bldr)()) + } +} + +fn proc_inst_inner(regsvc: RegSvc) -> Result { + // Use current working directory as the service's workdir + let cwd = std::env::current_dir()?.to_str().unwrap().to_string(); + let regsvc = regsvc + .workdir(cwd) + .env("HOLY", "COW") + .env("Private", "Public") + .env("General", "Specific"); + + // Add a callback that will increase log and trace levels by deafault. + #[cfg(windows)] + let regsvc = regsvc.regconf(|_svcname, params| { + params.set_value("AppArgParser", &"SaysHello")?; + + Ok(()) + }); + + Ok(regsvc) } // vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : Index: examples/err/mod.rs ================================================================== --- examples/err/mod.rs +++ examples/err/mod.rs @@ -32,19 +32,21 @@ Error::Qsu(err.to_string()) } } /* -/// Convenience converter used to pass an application-defined errors from the -/// qsu inner runtime back out from the qsu runtime. +/// Convenience converter used to pass application-defined errors from the +/// inner callback back out from the qsu runtime. impl From for qsu::Error { fn from(err: Error) -> qsu::Error { qsu::Error::app(err) } } */ +/// Convenience converter for mapping application-specific errors to +/// `qsu::AppErr`. impl From for qsu::AppErr { fn from(err: Error) -> qsu::AppErr { qsu::AppErr::new(err) } } Index: examples/hellosvc-rocket.rs ================================================================== --- examples/hellosvc-rocket.rs +++ examples/hellosvc-rocket.rs @@ -94,21 +94,24 @@ ProcRes::into(main2().into()) } fn main2() -> Result<(), Error> { // Derive default service name from executable name. - // (This causes a memory leak). let svcname = qsu::default_service_name() .expect("Unable to determine default service name"); - // Parse, and process, command line arguments. - let mut argsproc = argp::AppArgsProc {}; - let ap = ArgParser::new(&svcname, &mut argsproc); - ap.proc(|| { + let creator = || { let handler = Box::new(MyService {}); SrvAppRt::Rocket(handler) - })?; + }; + + // Parse, and process, command line arguments. + let mut argsproc = argp::AppArgsProc { + bldr: Box::new(creator) + }; + let ap = ArgParser::new(&svcname, &mut argsproc); + ap.proc()?; Ok(()) } #[get("/")] Index: examples/hellosvc-tokio.rs ================================================================== --- examples/hellosvc-tokio.rs +++ examples/hellosvc-tokio.rs @@ -88,21 +88,24 @@ ProcRes::into(main2().into()) } fn main2() -> Result<(), Error> { // Derive default service name from executable name. - // (This causes a memory leak). let svcname = qsu::default_service_name() .expect("Unable to determine default service name"); - // Parse, and process, command line arguments. - let mut argsproc = argp::AppArgsProc {}; - let ap = ArgParser::new(&svcname, &mut argsproc); - ap.proc(|| { + let creator = || { let handler = Box::new(MyService {}); SrvAppRt::Tokio(None, handler) - })?; + }; + + // Parse, and process, command line arguments. + let mut argsproc = argp::AppArgsProc { + bldr: Box::new(creator) + }; + let ap = ArgParser::new(&svcname, &mut argsproc); + ap.proc()?; Ok(()) } // vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : Index: examples/hellosvc.rs ================================================================== --- examples/hellosvc.rs +++ examples/hellosvc.rs @@ -91,21 +91,24 @@ ProcRes::into(main2().into()) } fn main2() -> Result<(), Error> { // Derive default service name from executable name. - // (This causes a memory leak). let svcname = qsu::default_service_name() .expect("Unable to determine default service name"); - // Parse, and process, command line arguments. - let mut argsproc = argp::AppArgsProc {}; - let ap = ArgParser::new(&svcname, &mut argsproc); - ap.proc(|| { + let creator = || { let handler = Box::new(MyService {}); SrvAppRt::Sync(handler) - })?; + }; + + // Parse, and process, command line arguments. + let mut argsproc = argp::AppArgsProc { + bldr: Box::new(creator) + }; + let ap = ArgParser::new(&svcname, &mut argsproc); + ap.proc()?; Ok(()) } // vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : Index: src/argp.rs ================================================================== --- src/argp.rs +++ src/argp.rs @@ -1,14 +1,14 @@ //! Helpers for integrating clap into an application using _qsu_. use clap::{builder::Str, Arg, ArgAction, ArgMatches, Args, Command}; use crate::{ + err::{AppErr, Error}, installer::{self, RegSvc}, lumberjack::LogLevel, - rt::{RunCtx, SrvAppRt}, - Error + rt::{RunCtx, SrvAppRt} }; /// Modify a `clap` [`Command`] instance to accept common service management /// subcommands. @@ -102,18 +102,10 @@ .action(ArgAction::Set) .value_name("SVCNAME") .default_value(Str::from(svcname.to_string())) .help("Set service name"); - /* - let autostartarg = Arg::new("autostart") - .short('a') - .long("auto-start") - .action(ArgAction::SetTrue) - .help("Set service to auto-start on boot"); - */ - //Command::new(cmd).arg(namearg).arg(autostartarg) let cli = Command::new(cmd.to_string()).arg(namearg); RegSvcArgs::augment_args(cli) } @@ -180,13 +172,13 @@ RunSvcArgs::augment_args(cli) } -pub(crate) enum ArgpRes { +pub(crate) enum ArgpRes<'cb> { /// Run server application. - RunApp(RunCtx), + RunApp(RunCtx, &'cb mut dyn ArgsProc), /// Nothing to do (service was probably registered/deregistred). Quit } @@ -201,25 +193,32 @@ let svcname = matches.get_one::("svcname").unwrap().to_owned(); Self { svcname } } } + +pub enum Cmd { + Root, + Inst, + Rm, + Run +} /// Allow application to customise behavior of an [`ArgParser`] instance. pub trait ArgsProc { - /// Callback allowing application to configure service installation argument - /// parser. - fn inst_subcmd(&mut self) { - todo!() - } - - fn rm_subcmd(&mut self) { - todo!() - } - - fn run_subcmd(&mut self) { - todo!() + /// Give the application an opportunity to modify the root and subcommand + /// `Command`s. + /// + /// `cmdtype` indicates whether `cmd` is the root `Command` or one of the + /// subcommand `Command`s. + #[allow(unused_variables)] + fn conf_cmd( + &mut self, + cmdtype: Cmd, + cmd: Command + ) -> Result { + Ok(cmd) } /// Callback allowing application to configure the service registration /// context just before the service is registered. /// @@ -229,33 +228,62 @@ /// - Add command like arguments to the run command. /// /// The `sub_m` argument represents `clap`'s parsed subcommand context for /// the service registration subcommand. Applications that want to add /// custom arguments to the parser should implement the - /// [`ArgsProc::inst_subcmd()`] trait method and perform the subcommand + /// [`ArgsProc::conf_cmd()`] trait method and perform the subcommand /// augmentation there. /// /// The default implementation does nothing but return `regsvc` unmodified. #[allow(unused_variables)] fn proc_inst( - &self, + &mut self, sub_m: &ArgMatches, regsvc: RegSvc - ) -> Result { + ) -> Result { Ok(regsvc) } + + #[allow(unused_variables)] + fn proc_rm( + &mut self, + sub_m: &ArgMatches, + deregsvc: DeregSvc + ) -> Result { + Ok(deregsvc) + } + + /// Callback allowing application to configure the run context before + /// launching the server application. + /// + /// qsu will have performed all its own initialization of the [`RunCtx`] + /// before calling this function. + /// + /// The application can differentiate between running in a service mode and + /// running as a foreground by calling [`RunCtx::is_service()`]. + #[allow(unused_variables)] + fn proc_run( + &mut self, + matches: &ArgMatches, + runctx: RunCtx + ) -> Result { + Ok(runctx) + } /// Called when a subcommand is encountered that is not one of the three /// subcommands regognized by qsu. #[allow(unused_variables)] fn proc_other( &mut self, subcmd: &str, sub_m: &ArgMatches - ) -> Result<(), Error> { + ) -> Result<(), AppErr> { Ok(()) } + + /// Construct an server application runtime. + fn build_apprt(&mut self) -> Result; } /// High-level argument parser. /// @@ -274,10 +302,14 @@ cli: Command, cb: &'cb mut dyn ArgsProc } impl<'cb> ArgParser<'cb> { + /// Create a new argument parser. + /// + /// `svcname` is the _default_ service name. It may be overridden using + /// command line arguments. pub fn new(svcname: &str, cb: &'cb mut dyn ArgsProc) -> Self { let cli = Command::new(""); Self { svcname: svcname.to_string(), reg_subcmd: "register-service".into(), @@ -286,10 +318,16 @@ cli, cb } } + + /// Create a new argument parser, basing the root command parser on an + /// application-supplied `Command`. + /// + /// `svcname` is the _default_ service name. It may be overridden using + /// command line arguments. pub fn with_cmd( svcname: &str, cli: Command, cb: &'cb mut dyn ArgsProc ) -> Self { @@ -301,26 +339,29 @@ cli, cb } } + /// Rename the service registration subcommand. pub fn reg_subcmd(mut self, nm: &str) -> Self { self.reg_subcmd = nm.to_string(); self } + /// Rename the service deregistration subcommand. pub fn dereg_subcmd(mut self, nm: &str) -> Self { self.dereg_subcmd = nm.to_string(); self } + /// Rename the subcommand for running the service. pub fn run_subcmd(mut self, nm: &str) -> Self { self.run_subcmd = nm.to_string(); self } - fn inner_proc(self) -> Result { + fn inner_proc(self) -> Result, Error> { let matches = match self.cli.try_get_matches() { Ok(m) => m, Err(e) => match e.kind() { clap::error::ErrorKind::DisplayHelp => { e.exit(); @@ -368,31 +409,43 @@ } Some((subcmd, sub_m)) if subcmd == self.dereg_subcmd => { // Get arguments relating to service deregistration. let args = DeregSvc::from_cmd_match(sub_m); + let args = self.cb.proc_rm(sub_m, args)?; + installer::uninstall(&args.svcname)?; Ok(ArgpRes::Quit) } Some((subcmd, sub_m)) if subcmd == self.run_subcmd => { // Get arguments relating to running the service. let args = RunSvc::from_cmd_match(sub_m); + + // Create a RunCtx, mark it as a service, and allow application the + // opportunity to modify it based on the parsed command line. + let rctx = RunCtx::new(&args.svcname).service(); + let rctx = self.cb.proc_run(sub_m, rctx)?; // Return a run context for a background service process. - Ok(ArgpRes::RunApp(RunCtx::new(&args.svcname).service())) + Ok(ArgpRes::RunApp(rctx, self.cb)) } Some((subcmd, sub_m)) => { // Call application callback for processing "other" subcmd self.cb.proc_other(subcmd, sub_m)?; // Return a run context for a background service process. Ok(ArgpRes::Quit) } _ => { + // Create a RunCtx, mark it as a service, and allow application the + // opportunity to modify it based on the parsed command line. + let rctx = RunCtx::new(&self.svcname); + let rctx = self.cb.proc_run(&matches, rctx)?; + // Return a run context for a foreground process. - Ok(ArgpRes::RunApp(RunCtx::new(&self.svcname))) + Ok(ArgpRes::RunApp(rctx, self.cb)) } } } /// Process command line arguments. @@ -412,34 +465,52 @@ /// - If none of the above subcommands where issued, then run the server /// application as a foreground process. /// /// The `bldr` is a closure that will be called to yield the `SrvAppRt` in /// case the service was requested to run. - pub fn proc(mut self, bldr: F) -> Result<(), Error> - where - F: FnOnce() -> SrvAppRt - { - // Create registration subcommand + /// + /// # Service registration behavior + /// The default service registration behavior in qsu will: + /// - Assume that the executable being used to register the service is the + /// same one that will run the service. + /// - Add the "run service" subcommand to the service's command line + /// arguments. + /// - If the specified service name is different than the default service + /// name (determined by + /// [`default_service_name()`](crate::default_service_name), then the + /// aguments `--name ` will be added. + pub fn proc(mut self) -> Result<(), Error> { + // Give application the opportunity to modify root Command + self.cli = self.cb.conf_cmd(Cmd::Root, self.cli)?; + + // Create registration subcommand and give application the opportunity to + // modify the subcommand's Command let sub = mk_inst_cmd(&self.reg_subcmd, &self.svcname); + let sub = self.cb.conf_cmd(Cmd::Inst, sub)?; self.cli = self.cli.subcommand(sub); // Create deregistration subcommand let sub = mk_rm_cmd(&self.dereg_subcmd, &self.svcname); + let sub = self.cb.conf_cmd(Cmd::Rm, sub)?; self.cli = self.cli.subcommand(sub); // Create run subcommand let sub = mk_run_cmd(&self.run_subcmd, &self.svcname); + let sub = self.cb.conf_cmd(Cmd::Run, sub)?; self.cli = self.cli.subcommand(sub); // Parse command line arguments. Run the service application if requiested // to do so. - if let ArgpRes::RunApp(runctx) = self.inner_proc()? { + if let ArgpRes::RunApp(runctx, cb) = self.inner_proc()? { // Argument parser asked us to run, so call the application to ask it to // create the service handler, and then kick off the service runtime. - let st = bldr(); + //let st = bldr(ctx)?; + + let st = cb.build_apprt()?; + runctx.run(st)?; } Ok(()) } } // vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : Index: src/err.rs ================================================================== --- src/err.rs +++ src/err.rs @@ -26,23 +26,10 @@ } pub fn shutdown_failed(&self) -> bool { self.shutdown.is_some() } - - pub fn origin(&self) -> CbOrigin { - if self.init_failed() { - CbOrigin::Init - } else if self.run_failed() { - CbOrigin::Run - } else if self.shutdown_failed() { - CbOrigin::Shutdown - } else { - // Can't happen - unimplemented!() - } - } } /// Errors that qsu will return to application. #[derive(Debug)] @@ -49,11 +36,11 @@ pub enum Error { /// Application-defined error. /// /// Applications can use this variant to pass application-specific errors /// through the runtime back to itself. - App(CbOrigin, AppErr), + App(AppErr), ArgP(ArgsError), BadFormat(String), Internal(String), IO(String), @@ -85,43 +72,37 @@ } impl Error { pub fn is_apperr(&self) -> bool { - matches!(self, Error::App(_, _)) + matches!(self, Error::App(_)) } /// Attempt to convert [`Error`] into application-specific error. /// /// If it's not an `Error::App()` nor can be downcast to type `E`, the error /// will be returned back as an `Err()`. - pub fn try_into_apperr(self) -> Result<(CbOrigin, E), Error> { + pub fn try_into_apperr(self) -> Result { match self { - Error::App(origin, e) => match e.try_into_inner::() { - Ok(e) => Ok((origin, e)), - Err(e) => Err(Error::App(origin, AppErr::new(e))) + Error::App(e) => match e.try_into_inner::() { + Ok(e) => Ok(e), + Err(e) => Err(Error::App(AppErr::new(e))) }, e => Err(e) } } /// Unwrap application-specific error from an [`Error`]. /// /// # Panic /// Panics if `Error` variant is not `Error::App()`. - pub fn unwrap_apperr(self) -> (CbOrigin, E) { - let Ok((origin, e)) = self.try_into_apperr::() else { + pub fn unwrap_apperr(self) -> E { + let Ok(e) = self.try_into_apperr::() else { panic!("Unable to unwrap error E"); }; - (origin, e) - } - - /* - pub(crate) fn app(origin: CbOrigin, e: E) -> Self { - Error::App(origin, AppErr::new(e)) - } - */ + e + } pub fn bad_format(s: S) -> Self { Error::BadFormat(s.to_string()) } @@ -145,11 +126,11 @@ impl std::error::Error for Error {} impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - Error::App(_origin, _e) => { + Error::App(_e) => { write!(f, "Application-defined error") } Error::ArgP(s) => { // ToDo: Handle the ArgsError::Clap and ArgsError::Msg differently write!(f, "Argument parser; {:?}", s) @@ -271,10 +252,18 @@ where E: Send + 'static { Self(Box::new(e)) } + + /// Inspect error type wrapped by the `AppErr`. + pub fn is(&self) -> bool + where + T: Any + { + self.0.is::() + } /// Attempt to unpack and cast the inner error type. /// /// If it can't be downcast to `E`, `AppErr` will be returned in the `Err()` /// case. @@ -322,22 +311,13 @@ }; *e } } -/// Origin of an application callback error. -#[derive(Debug)] -pub enum CbOrigin { - /// The application error occurred in the service handler's `init()` - /// callback. - Init, - - /// The application error occurred in the service handler's `run()` - /// callback. - Run, - - /// The application error occurred in the service handler's `shutdown()`. - /// callback. - Shutdown +impl From for Error { + /// Wrap an [`AppErr`] in an [`Error`]. + fn from(err: AppErr) -> Self { + Error::App(err) + } } // vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : Index: src/installer/winsvc.rs ================================================================== --- src/installer/winsvc.rs +++ src/installer/winsvc.rs @@ -14,10 +14,15 @@ create_service_params, get_service_params_subkey, write_service_subkey } }; +/// Register a service in the system service's subsystem. +// ToDo: Make notes about Windows-specific semantics: +// - Uses registry +// - Installer +// - Windows Event Log pub fn install(ctx: super::RegSvc) -> Result<(), Error> { let svcname = &ctx.svcname; // Create a refrence cell used to keep track of whether to keep system // motifications (or not) when leaving function. Index: src/lib.rs ================================================================== --- src/lib.rs +++ src/lib.rs @@ -54,11 +54,11 @@ pub use async_trait::async_trait; pub use lumberjack::LumberJack; -pub use err::{AppErr, CbOrigin, Error}; +pub use err::{AppErr, Error}; #[cfg(feature = "tokio")] pub use tokio; #[cfg(feature = "rocket")] Index: src/rt.rs ================================================================== --- src/rt.rs +++ src/rt.rs @@ -477,10 +477,14 @@ /// one is available on this platform. pub fn service_ref(&mut self) -> &mut Self { self.service = true; self } + + pub fn is_service(&self) -> bool { + self.service + } /// Launch the application. /// /// If this `RunCtx` has been marked as a _service_ then it will perform the /// appropriate service subsystem integration before running the actual Index: www/changelog.md ================================================================== --- www/changelog.md +++ www/changelog.md @@ -6,10 +6,29 @@ ### Changed ### Removed +--- + +## [0.0.4] - 2023-10-29 + +### Added + +- Add the remaining `ArgsProc` callbacks in `ArgParser`. + +### Changed + +- Rather than pass a creation closure to the `ArgParser::proc()` for the run + case, add a `ArgsProc::build_apprt()` that'll be invoked to create the + runtime instead. +- More consistently use `AppErr` for callbacks. + +### Removed + +- Removed `err::CbOrigin`. + --- ## [0.0.3] - 2023-10-23 ### Added Index: www/index.md ================================================================== --- www/index.md +++ www/index.md @@ -88,10 +88,18 @@ Services subsystem. But mostly the benefits become apparent when targetting multiple service subsystems in the same project, and wanting to have a similar API when developing non-async and async services. +## General tips + +- The logging/tracing facilities aren't initialized until the server + application's runtime has been initialized, because the runtime type may + affect the logging/tracing backends. As an implication of this, services + that use the `ArgParser` should defer operations that need logging/tracking + until the service handler's `init()` trait method is called. + ## Feature labels in documentation The crate's documentation uses automatically generated feature labels, which currently requires nightly featuers. To build the documentation locally use: @@ -128,9 +136,12 @@ maintained [Change Log](./changelog.md). ## Project status -This crate is a work-in-progress, still in early prototyping stage. It works -for basic use-cases, but the API and some of the semantics are likely to -change. +This crate is a work-in-progress -- still in early prototyping stage. This +means potentially significant API instability between versions and incomplete, +or even incorrect, documentation. + +It is recommended that projects wanting to use _qsu_ at this point use the +tests and examples for up-to-date information on how to use the crate.