Index: .efiles ================================================================== --- .efiles +++ .efiles @@ -18,9 +18,12 @@ src/rttype/rocket.rs src/installer.rs src/installer/winsvc.rs src/installer/launchd.rs src/installer/systemd.rs +src/argp.rs examples/hellosvc.rs examples/hellosvc-tokio.rs examples/hellosvc-rocket.rs +examples/err/mod.rs +examples/procres/mod.rs examples/argp/mod.rs Index: Cargo.toml ================================================================== --- Cargo.toml +++ Cargo.toml @@ -1,26 +1,28 @@ [package] name = "qsu" -version = "0.0.1" +version = "0.0.2" edition = "2021" license = "0BSD" -categories = [ "concurrency", "asynchronous" ] +categories = [ "asynchronous" ] keywords = [ "service", "systemd", "winsvc" ] repository = "https://repos.qrnch.tech/pub/qsu" description = "Service subsystem wrapper." rust-version = "1.56" exclude = [ ".fossil-settings", ".efiles", ".fslckout", "www", + "build_docs.sh", "Rocket.toml", "rustfmt.toml" ] [features] -full = ["installer", "rocket", "systemd"] +clap = ["dep:clap", "dep:itertools"] +full = ["clap", "installer", "rocket", "systemd"] installer = ["dep:sidoc"] systemd = ["dep:sd-notify"] #tokio = ["dep:tokio"] #rocket = ["dep:rocket", "dep:tokio"] rocket = ["dep:rocket"] @@ -27,12 +29,16 @@ wait-for-debugger = ["dep:dbgtools-win"] [dependencies] async-trait = { version = "0.1.73" } chrono = { version = "0.4.24" } +clap = { version = "4.4.6", optional = true, features = [ + "derive", "env", "string", "wrap_help" +] } env_logger = { version = "0.10.0" } futures = { version = "0.3.28" } +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 } sidoc = { version = "0.1.0", optional = true } @@ -67,23 +73,21 @@ [dev-dependencies] clap = { version = "4.4.6", features = ["derive", "env", "wrap_help"] } tokio = { version = "1.33.0", features = ["time"] } -# Building with --cfg docsrs causes doc generation to fail in tokio-stream [package.metadata.docs.rs] all-features = true -#rustdoc-args = ["--cfg", "docsrs", "--generate-link-to-definition"] -rustdoc-args = ["--generate-link-to-definition"] +rustdoc-args = ["--cfg", "docsrs", "--generate-link-to-definition"] [[example]] name = "hellosvc" -required-features = ["installer"] +required-features = ["clap", "installer"] [[example]] name = "hellosvc-tokio" -required-features = ["installer"] +required-features = ["clap", "installer"] [[example]] name = "hellosvc-rocket" -required-features = ["installer", "rocket"] +required-features = ["clap", "installer", "rocket"] Index: Rocket.toml ================================================================== --- Rocket.toml +++ Rocket.toml @@ -1,7 +1,15 @@ #[global] #port = 8000 + +# It is recommended that catching SIGINT,SIGTERM,Ctrl+C is left to qsu. +[default.shutdown] +ctrlc = false +force = true +signals = [] +grace = 2 +mercy = 3 [debug] address = "127.0.0.1" port = 8080 keep_alive = 5 ADDED build_docs.sh Index: build_docs.sh ================================================================== --- /dev/null +++ build_docs.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +RUSTFLAGS="--cfg docsrs" RUSTDOCFLAGS="--cfg docsrs" \ +cargo +nightly doc --all-features + +# vim: set ft=sh et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : Index: examples/argp/mod.rs ================================================================== --- examples/argp/mod.rs +++ examples/argp/mod.rs @@ -1,117 +1,34 @@ -use clap::{Args, Parser, Subcommand}; - -use qsu::{installer, RunCtx}; - - -#[derive(Debug, Parser)] -#[command(name = "hellosvc")] -#[command(about = "Hello Service")] -pub struct Cli { - #[command(subcommand)] - pub cmd: Option -} - -#[derive(Debug, Subcommand)] -#[command(name = "ident")] -pub enum Commands { - /// Install service. - #[command(arg_required_else_help = true)] - InstallService(InstCmd), - - /// Remove service. - #[command(arg_required_else_help = true)] - RemoveService(RmCmd), - - /// Run service. - #[command(arg_required_else_help = true)] - RunService(RunCmd) -} - -#[derive(Debug, Args)] -pub struct InstCmd { - /// Tell the service subsystem to auto-start the service on boot. - #[arg(long, short)] - pub auto_start: bool, - - /// Service name. - #[arg(value_name = "SVCNAME")] - pub name: String -} - -impl From for installer::RegSvc { - fn from(input: InstCmd) -> Self { - let mut ctx = installer::RegSvc::new(&input.name); - if input.auto_start { - ctx.autostart_ref(); - } - ctx - } -} - -#[derive(Debug, Args)] -pub struct RmCmd { - /// Service name. - #[arg(value_name = "NAME")] - pub name: String -} - -#[derive(Debug, Args)] -pub struct RunCmd { - /// Service name. - #[arg(value_name = "SVCNAME")] - pub name: String -} - - -pub(crate) fn proc_subcmd( - svcrunctx: &mut RunCtx, - args: Cli -) -> Result { - let mut run_svc = false; - if let Some(cmd) = args.cmd { - match cmd { - Commands::InstallService(ctx) => { - // Use current directory as the work directory - let cwd = std::env::current_dir() - .unwrap() - .to_str() - .unwrap() - .to_string(); - - let mut regsvc = installer::RegSvc::from(ctx) - .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)] - regsvc.regconf_ref(|svcname, params| { - params.set_value("LogLevel", &"trace")?; - params.set_value("TraceLevel", &"trace")?; - params.set_value("TraceFile", &"C:\\Temp\\hellosvc-trace.log")?; - - Ok(()) - }); - - let svcname = regsvc.svcname().to_string(); - regsvc.args_ref(["run-service", &svcname]); - - regsvc.register().unwrap(); - } - Commands::RemoveService(ctx) => { - installer::uninstall(&ctx.name).unwrap(); - } - Commands::RunService(ctx) => { - svcrunctx.service_ref(); - run_svc = true; - } - } - } else { - run_svc = true; - } - - Ok(run_svc) +use clap::ArgMatches; + +use qsu::installer::RegSvc; + +pub(crate) struct AppArgsProc {} + +impl qsu::argp::ArgsProc for AppArgsProc { + /// Process an `register-service` subcommand. + fn proc_inst( + &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) + } } // vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : ADDED examples/err/mod.rs Index: examples/err/mod.rs ================================================================== --- /dev/null +++ examples/err/mod.rs @@ -0,0 +1,36 @@ +use std::{fmt, io}; + +#[derive(Debug)] +pub enum Error { + IO(String), + Qsu(String) +} + +impl std::error::Error for Error {} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Error::IO(s) => { + write!(f, "I/O error; {}", s) + } + Error::Qsu(s) => { + write!(f, "qsu error; {}", s) + } + } + } +} + +impl From for Error { + fn from(err: io::Error) -> Self { + Error::IO(err.to_string()) + } +} + +impl From for Error { + fn from(err: qsu::Error) -> Self { + Error::Qsu(err.to_string()) + } +} + +// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : Index: examples/hellosvc-rocket.rs ================================================================== --- examples/hellosvc-rocket.rs +++ examples/hellosvc-rocket.rs @@ -1,22 +1,21 @@ #[macro_use] extern crate rocket; mod argp; - -use std::time::{Duration, Instant}; - -use clap::Parser; +mod err; +mod procres; use qsu::{ - RocketServiceHandler, RunCtx, StartState, StopState, SvcEvt, SvcEvtReader, - SvcType + argp::ArgParser, RocketServiceHandler, StartState, StopState, SvcEvt, + SvcEvtReader, SvcType }; use rocket::{Build, Ignite, Rocket}; -const SVCNAME: &str = "hellosvc-rocket"; +use err::Error; +use procres::ProcRes; struct MyService {} #[qsu::async_trait] @@ -44,18 +43,22 @@ tokio::task::spawn(async { rocket.launch().await.unwrap(); }); } - const SECS: u64 = 30; - let mut last_dump = Instant::now() - Duration::from_secs(SECS); loop { tokio::select! { evt = set.arecv() => { match evt { Some(SvcEvt::Shutdown) => { - tracing::info!("The service subsystem requested that application terminate"); + tracing::info!("The service subsystem requested that the application shut down"); + break; + } + Some(SvcEvt::Terminate) => { + tracing::info!( + "The service subsystem requested that the application terminate" + ); break; } Some(SvcEvt::ReloadConf) => { tracing::info!("The service subsystem requested that application reload configuration"); } @@ -72,22 +75,32 @@ tracing::trace!("Running shutdown()"); Ok(()) } } -fn main() { - let args = argp::Cli::parse(); - - let handler = Box::new(MyService {}); - - // ToDo: service name should be overridable - let mut svcrunctx = RunCtx::new(SVCNAME); - let run_svc = argp::proc_subcmd(&mut svcrunctx, args).unwrap(); - - if run_svc { - svcrunctx.run(SvcType::Rocket(handler)); - } + +fn main() -> ProcRes { + // In the future we'll be able to use Try to implement support for implicit + // conversion to ProcRes from a Result using `?`, but for now use this hack. + 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 handler = Box::new(MyService {}); + SvcType::Rocket(handler) + })?; + + Ok(()) } #[get("/")] fn index() -> &'static str { log::error!("error"); Index: examples/hellosvc-tokio.rs ================================================================== --- examples/hellosvc-tokio.rs +++ examples/hellosvc-tokio.rs @@ -1,22 +1,21 @@ //! Simple service that does nothing other than log/trace every N seconds. mod argp; +mod err; +mod procres; -use std::{ - thread, - time::{Duration, Instant} -}; - -use clap::{ArgAction, Args, Parser, Subcommand}; +use std::time::{Duration, Instant}; use qsu::{ - installer, RunCtx, StartState, StopState, SvcEvt, SvcEvtReader, SvcType, + argp::ArgParser, StartState, StopState, SvcEvt, SvcEvtReader, SvcType, TokioServiceHandler }; -const SVCNAME: &str = "hellosvc-tokio"; +use err::Error; +use procres::ProcRes; + struct MyService {} #[qsu::async_trait] impl TokioServiceHandler for MyService { @@ -49,11 +48,17 @@ continue; } evt = set.arecv() => { match evt { Some(SvcEvt::Shutdown) => { - tracing::info!("The service subsystem requested that application terminate"); + tracing::info!("The service subsystem requested that the application shut down"); + break; + } + Some(SvcEvt::Terminate) => { + tracing::info!( + "The service subsystem requested that the application terminate" + ); break; } Some(SvcEvt::ReloadConf) => { tracing::info!("The service subsystem requested that application reload configuration"); } @@ -71,20 +76,29 @@ Ok(()) } } -fn main() { - let args = argp::Cli::parse(); - - let handler = Box::new(MyService {}); - - // ToDo: service name should be overridable - let mut svcrunctx = RunCtx::new(SVCNAME); - let run_svc = argp::proc_subcmd(&mut svcrunctx, args).unwrap(); - - if run_svc { - svcrunctx.run(SvcType::Tokio(handler)); - } +fn main() -> ProcRes { + // In the future we'll be able to use Try to implement support for implicit + // conversion to ProcRes from a Result using `?`, but for now use this hack. + 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 handler = Box::new(MyService {}); + SvcType::Tokio(None, handler) + })?; + + 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 @@ -1,21 +1,24 @@ //! Simple service that does nothing other than log/trace every N seconds. mod argp; +mod err; +mod procres; use std::{ thread, time::{Duration, Instant} }; -use clap::{ArgAction, Args, Parser, Subcommand}; - use qsu::{ - RunCtx, ServiceHandler, StartState, StopState, SvcEvt, SvcEvtReader, SvcType + argp::ArgParser, ServiceHandler, StartState, StopState, SvcEvt, + SvcEvtReader, SvcType }; -const SVCNAME: &str = "hellosvc"; +use err::Error; +use procres::ProcRes; + struct MyService {} impl ServiceHandler for MyService { fn init(&mut self, _ss: StartState) -> Result<(), qsu::Error> { @@ -44,17 +47,23 @@ } match set.try_recv() { Some(SvcEvt::Shutdown) => { tracing::info!( - "The service subsystem requested that application terminate" + "The service subsystem requested that the application shut down" + ); + break; + } + Some(SvcEvt::Terminate) => { + tracing::info!( + "The service subsystem requested that the application terminate" ); break; } Some(SvcEvt::ReloadConf) => { tracing::info!( - "The service subsystem requested that application reload \ + "The service subsystem requested that the application reload its \ configuration" ); } _ => {} } @@ -70,20 +79,29 @@ Ok(()) } } -fn main() { - let args = argp::Cli::parse(); - - let handler = Box::new(MyService {}); - - // ToDo: service name should be overridable - let mut svcrunctx = RunCtx::new(SVCNAME); - let run_svc = argp::proc_subcmd(&mut svcrunctx, args).unwrap(); - - if run_svc { - svcrunctx.run(SvcType::Sync(handler)); - } +fn main() -> ProcRes { + // In the future we'll be able to use Try to implement support for implicit + // conversion to ProcRes from a Result using `?`, but for now use this hack. + 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 handler = Box::new(MyService {}); + SvcType::Sync(handler) + })?; + + Ok(()) } // vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : ADDED examples/procres/mod.rs Index: examples/procres/mod.rs ================================================================== --- /dev/null +++ examples/procres/mod.rs @@ -0,0 +1,35 @@ +use std::process::{ExitCode, Termination}; + +use crate::err::Error; + +#[repr(u8)] +pub enum ProcRes { + Success, + Error(Error) +} + +impl Termination for ProcRes { + fn report(self) -> ExitCode { + match self { + ProcRes::Success => { + //eprintln!("Process terminated successfully"); + ExitCode::from(0) + } + ProcRes::Error(e) => { + eprintln!("Abnormal termination: {}", e); + ExitCode::from(1) + } + } + } +} + +impl From> for ProcRes { + fn from(res: Result) -> ProcRes { + match res { + Ok(_) => ProcRes::Success, + Err(e) => ProcRes::Error(e) + } + } +} + +// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : ADDED src/argp.rs Index: src/argp.rs ================================================================== --- /dev/null +++ src/argp.rs @@ -0,0 +1,382 @@ +//! Helpers for integrating clap into an application using _qsu_. + +use clap::{builder::Str, Arg, ArgAction, ArgMatches, Args, Command}; + +use crate::{ + installer::{self, RegSvc}, + lumberjack::LogLevel, + Error, RunCtx, SvcType +}; + + +pub fn add_subcommands( + cli: Command, + svcname: &str, + inst_subcmd: Option<&str>, + rm_subcmd: Option<&str>, + run_subcmd: Option<&str> +) -> Command { + let cli = if let Some(subcmd) = inst_subcmd { + let sub = mk_inst_cmd(subcmd, svcname); + cli.subcommand(sub) + } else { + cli + }; + + let cli = if let Some(subcmd) = rm_subcmd { + let sub = mk_rm_cmd(subcmd, svcname); + cli.subcommand(sub) + } else { + cli + }; + + let cli = if let Some(subcmd) = run_subcmd { + let sub = mk_run_cmd(subcmd, svcname); + cli.subcommand(sub) + } else { + cli + }; + + cli +} + +/// Register service. +#[derive(Debug, Args)] +struct RegSvcArgs { + /// Autostart service at boot. + #[arg(short = 's', long)] + auto_start: bool, + + /// Set an optional display name for the service. + #[cfg(windows)] + #[arg(short = 'D', long, value_name = "DISPNAME")] + display_name: Option, + + /// Set an optional one-line service description. + #[cfg(any(all(target_os = "linux", feature = "systemd"), windows))] + #[arg(short, long, value_name = "DESC")] + description: Option, + + /// Add a command line argument to the service command line. + #[arg(short, long)] + arg: Vec, + + /// Add an environment variable to the service. + #[arg(short, long, num_args(2), value_names=["KEY", "VALUE"])] + env: Vec, + + /// Set an optional directory the service runtime should start in. + #[arg(short, long, value_name = "DIR")] + workdir: Option, + + #[arg(long, value_enum, value_name = "LEVEL")] + log_level: Option, + + #[arg(long, value_enum, hide(true), value_name = "LEVEL")] + trace_level: Option, + + #[arg(long, value_enum, hide(true), value_name = "FNAME")] + trace_file: Option +} + +pub fn mk_inst_cmd(cmd: &str, svcname: &str) -> Command { + let namearg = Arg::new("svcname") + .short('n') + .long("name") + .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) +} + + +/// Deregister service. +#[derive(Debug, Args)] +struct DeregSvcArgs {} + +pub fn mk_rm_cmd(cmd: &str, svcname: &str) -> Command { + let namearg = Arg::new("svcname") + .short('n') + .long("name") + .action(ArgAction::Set) + .value_name("SVCNAME") + .default_value(svcname.to_string()) + .help("Name of service to remove"); + + let cli = Command::new(cmd.to_string()).arg(namearg); + + DeregSvcArgs::augment_args(cli) +} + + +pub struct DeregSvc { + pub svcname: String +} + +impl DeregSvc { + pub fn from_cmd_match(matches: &ArgMatches) -> Self { + let svcname = matches.get_one::("svcname").unwrap().to_owned(); + Self { svcname } + } +} + + +/// Run service. +#[derive(Debug, Args)] +struct RunSvcArgs {} + +pub fn mk_run_cmd(cmd: &str, svcname: &str) -> Command { + let namearg = Arg::new("svcname") + .short('n') + .long("name") + .action(ArgAction::Set) + .value_name("SVCNAME") + .default_value(svcname.to_string()) + .help("Service name"); + + let cli = Command::new(cmd.to_string()).arg(namearg); + + RunSvcArgs::augment_args(cli) +} + + +pub(crate) enum ArgpRes { + /// Run server application. + RunApp(RunCtx), + + /// Nothing to do (service was probably registered/deregistred). + Quit +} + + +pub struct RunSvc { + pub svcname: String +} + +impl RunSvc { + pub fn from_cmd_match(matches: &ArgMatches) -> Self { + let svcname = matches.get_one::("svcname").unwrap().to_owned(); + Self { svcname } + } +} + + +pub trait ArgsProc { + /// Callback allowing application to configure service installation argument + /// parser. + fn inst_subcmd(&mut self) {} + + fn rm_subcmd(&mut self) {} + + fn run_subcmd(&mut self) {} + + /// Callback allowing application to configure the service registry context + /// before the service is registered. + /// + /// The default implementation does nothing but return `regsvc` unmodified. + #[allow(unused_variables)] + fn proc_inst( + &self, + sub_m: &ArgMatches, + regsvc: RegSvc + ) -> Result { + Ok(regsvc) + } + + /// 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> { + Ok(()) + } +} + + +/// High-level argument parser. +/// +/// This is suitable for applications that follow a specific pattern: +/// - It has subcommands for: +/// - Registering a service +/// - Deregistering a service +/// - Running as a service +/// - Running without any subcommands should run the server application as a +/// foreground process. +pub struct ArgParser<'cb> { + svcname: String, + reg_subcmd: String, + dereg_subcmd: String, + run_subcmd: String, + cli: Command, + cb: &'cb mut dyn ArgsProc +} + +impl<'cb> ArgParser<'cb> { + 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(), + dereg_subcmd: "deregister-service".into(), + run_subcmd: "run-service".into(), + cli, + cb + } + } + + pub fn with_cmd( + svcname: &str, + cli: Command, + cb: &'cb mut dyn ArgsProc + ) -> Self { + Self { + svcname: svcname.to_string(), + reg_subcmd: "register-service".into(), + dereg_subcmd: "deregister-service".into(), + run_subcmd: "run-service".into(), + cli, + cb + } + } + + pub fn reg_subcmd(mut self, nm: &str) -> Self { + self.reg_subcmd = nm.to_string(); + self + } + + pub fn dereg_subcmd(mut self, nm: &str) -> Self { + self.dereg_subcmd = nm.to_string(); + self + } + + pub fn run_subcmd(mut self, nm: &str) -> Self { + self.run_subcmd = nm.to_string(); + self + } + + fn inner_proc(self) -> Result { + let matches = match self.cli.try_get_matches() { + Ok(m) => m, + Err(e) => match e.kind() { + clap::error::ErrorKind::DisplayHelp => { + e.exit(); + //return Ok(ArgpRes::Quit); + } + clap::error::ErrorKind::DisplayVersion => { + e.exit(); + //return Ok(ArgpRes::Quit); + } + _ => { + // ToDo: Convert error to Error::ArgP, pass along the error type so + // that the Termination handler can output the specific error. + //Err(e)?; + e.exit(); + } + } + }; + match matches.subcommand() { + Some((subcmd, sub_m)) if subcmd == self.reg_subcmd => { + //println!("{:#?}", sub_m); + + let mut regsvc = RegSvc::from_cmd_match(sub_m); + + // To trigger the server to run in service mode, run with the + // subcommand "run-service". + // If the service name is different that the name drived from the + // executable's name, then add "--name " arguments. + let mut args = vec![String::from(&self.run_subcmd)]; + if regsvc.svcname() != self.svcname { + args.push(String::from("--name")); + args.push(regsvc.svcname().to_string()); + } + regsvc.args_ref(args); + + // Call application call-back, to allow application-specific + // service configuration. + // This is a good place to stick custom environment, arguments, + // registry changes. + let regsvc = self.cb.proc_inst(sub_m, regsvc)?; + + // Register the service with the operating system's service subsystem. + regsvc.register()?; + + Ok(ArgpRes::Quit) + } + Some((subcmd, sub_m)) if subcmd == self.dereg_subcmd => { + // Get arguments relating to service deregistration. + let args = DeregSvc::from_cmd_match(sub_m); + + 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); + + // Return a run context for a background service process. + Ok(ArgpRes::RunApp(RunCtx::new(&args.svcname).service())) + } + 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) + } + _ => { + // Return a run context for a foreground process. + Ok(ArgpRes::RunApp(RunCtx::new(&self.svcname))) + } + } + } + + /// Process command line arguments. + /// + /// The `bldr` is a closure that will be called to yield the `SvcType` in + /// case the service was requested to run. + pub fn proc(mut self, bldr: F) -> Result<(), Error> + where + F: FnOnce() -> SvcType + { + // Create registration subcommand + let sub = mk_inst_cmd(&self.reg_subcmd, &self.svcname); + self.cli = self.cli.subcommand(sub); + + // Create deregistration subcommand + let sub = mk_rm_cmd(&self.dereg_subcmd, &self.svcname); + self.cli = self.cli.subcommand(sub); + + // Create run subcommand + let sub = mk_run_cmd(&self.run_subcmd, &self.svcname); + 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()? { + // 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(); + 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 @@ -1,26 +1,48 @@ use std::{fmt, io}; + +#[derive(Debug)] +pub enum ArgsError { + #[cfg(feature = "clap")] + Clap(clap::Error), + Msg(String) +} /// Errors that qsu will return to application. #[derive(Debug)] pub enum Error { + ArgP(ArgsError), BadFormat(String), Internal(String), IO(String), + + /// An error related to logging occurred. + /// + /// This includes both initialization and actual logging. + /// + /// On Windows errors such as failure to register an event source will be + /// treated as this error variant as well. LumberJack(String), #[cfg(feature = "rocket")] Rocket(String), - SubSystem(String) + SubSystem(String), + Unsupported } impl Error { pub fn bad_format(s: S) -> Self { Error::BadFormat(s.to_string()) } + pub fn internal(s: S) -> Self { Error::Internal(s.to_string()) } + + pub fn io(s: S) -> Self { + Error::IO(s.to_string()) + } + pub fn lumberjack(s: S) -> Self { Error::LumberJack(s.to_string()) } } @@ -27,10 +49,14 @@ impl std::error::Error for Error {} impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { + Error::ArgP(s) => { + // ToDo: Handle the ArgsError::Clap and ArgsError::Msg differently + write!(f, "Argument parser; {:?}", s) + } Error::BadFormat(s) => { write!(f, "Bad format error; {}", s) } Error::Internal(s) => { write!(f, "Internal error; {}", s) @@ -46,25 +72,40 @@ write!(f, "Rocket error; {}", s) } Error::SubSystem(s) => { write!(f, "Service subsystem error; {}", s) } + Error::Unsupported => { + write!(f, "Operation is unsupported [on this platform]") + } } } } + +/* +#[cfg(feature = "clap")] +impl From for Error { + fn from(err: clap::error::Error) -> Self { + Error::ArgP(err.to_string()) + } +} +*/ + #[cfg(windows)] impl From for Error { + /// Map eventlog initialization errors to `Error::LumberJack`. fn from(err: eventlog::InitError) -> Self { - Error::SubSystem(err.to_string()) + Error::LumberJack(err.to_string()) } } #[cfg(windows)] impl From for Error { + /// Map eventlog errors to `Error::LumberJack`. fn from(err: eventlog::Error) -> Self { - Error::SubSystem(err.to_string()) + Error::LumberJack(err.to_string()) } } impl From for Error { fn from(err: io::Error) -> Self { @@ -83,14 +124,21 @@ impl From for Error { fn from(err: rocket::Error) -> Self { Error::Rocket(err.to_string()) } } + +#[cfg(feature = "installer")] +impl From for Error { + fn from(err: sidoc::Error) -> Self { + Error::Internal(err.to_string()) + } +} #[cfg(windows)] impl From for Error { fn from(err: windows_service::Error) -> Self { Error::SubSystem(err.to_string()) } } // vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : Index: src/installer.rs ================================================================== --- src/installer.rs +++ src/installer.rs @@ -4,23 +4,158 @@ pub mod winsvc; #[cfg(target_os = "macos")] pub mod launchd; -#[cfg(feature = "systemd")] +#[cfg(all(target_os = "linux", feature = "systemd"))] +#[cfg_attr( + docsrs, + doc(cfg(all(all(target_os = "linux", feature = "installer")))) +)] pub mod systemd; +//use std::{fmt, path::PathBuf}; + +#[cfg(feature = "clap")] +use clap::ArgMatches; + +use itertools::Itertools; + +use crate::{err::Error, lumberjack::LogLevel}; + +/* +#[cfg(any( + target_os = "macos", + all(target_os = "linux", feature = "systemd") +))] +pub enum InstallDir { + #[cfg(target_os = "macos")] + UserAgent, + + #[cfg(target_os = "macos")] + GlobalAgent, + + #[cfg(target_os = "macos")] + GlobalDaemon, + + #[cfg(all(target_os = "linux", feature = "systemd"))] + System, + + #[cfg(all(target_os = "linux", feature = "systemd"))] + PublicUser, + + #[cfg(all(target_os = "linux", feature = "systemd"))] + PrivateUser +} + +#[cfg(any( + target_os = "macos", + all(target_os = "linux", feature = "systemd") +))] +impl InstallDir { + fn path(self) -> PathBuf { + PathBuf::from(self.to_string()) + } + + fn path_str(self) -> String { + self.to_string() + } +} + +#[cfg(any( + target_os = "macos", + all(target_os = "linux", feature = "systemd") +))] +impl Default for InstallDir { + fn default() -> Self { + #[cfg(target_os = "macos")] + return InstallDir::GlobalDaemon; + + #[cfg(all(target_os = "linux", feature = "systemd"))] + return InstallDir::System; + } +} + +#[cfg(any( + target_os = "macos", + all(target_os = "linux", feature = "systemd") +))] +impl fmt::Display for InstallDir { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + #[cfg(target_os = "macos")] + InstallDir::UserAgent => "~/Library/LaunchAgents", + #[cfg(target_os = "macos")] + InstallDir::GlobalAgent => "/Library/LaunchAgents", + #[cfg(target_os = "macos")] + InstallDir::GlobalDaemon => "/Library/LaunchDaemons", + + #[cfg(all(target_os = "linux", feature = "systemd"))] + InstallDir::System => "/etc/systemd/system", + #[cfg(all(target_os = "linux", feature = "systemd"))] + InstallDir::PublicUser => "/etc/systemd/user", + #[cfg(all(target_os = "linux", feature = "systemd"))] + InstallDir::PrivateUser => "~/.config/systemd/user" + }; + write!(f, "{}", s) + } +} +*/ + + +/// What account to run the service as. +/// +/// # Windows +#[derive(Default)] +pub enum Account { + /// Run as the highest privileged user available on system. + /// + /// On unixy systems, this means `root`. On Windows, this means the + /// [LocalSystem](https://learn.microsoft.com/en-us/windows/win32/services/localsystem-account) account. + #[default] + System, + + /// On Windows systems, run the service as the [LocalService](https://learn.microsoft.com/en-us/windows/win32/services/localservice-account) account. + #[cfg(windows)] + #[cfg_attr(docsrs, doc(cfg(windows)))] + Service, + + /// On Windows systems, run the service as the [NetworkService](https://learn.microsoft.com/en-us/windows/win32/services/networkservice-account) account. + #[cfg(windows)] + #[cfg_attr(docsrs, doc(cfg(windows)))] + Network, + + #[cfg(unix)] + User(String), + + #[cfg(windows)] + UserAndPass(String, String) +} + + +#[derive(Debug, Default)] +pub struct RunAs { + user: Option, + group: Option, + + #[cfg(target_os = "macos")] + initgroups: bool, -use crate::err::Error; + #[cfg(any( + target_os = "macos", + all(target_os = "linux", feature = "systemd") + ))] + umask: Option +} pub struct RegSvc { pub svcname: String, #[cfg(windows)] pub display_name: Option, - #[cfg(any(windows, feature = "systemd"))] + #[cfg(any(windows, all(target_os = "linux", feature = "systemd")))] pub description: Option, #[cfg(windows)] pub regconf: Option Result<(), Error>>>, @@ -38,11 +173,19 @@ pub autostart: bool, pub(crate) workdir: Option, /// List of service dependencies. - deps: Vec + deps: Vec, + + log_level: Option, + + trace_level: Option, + + trace_file: Option, + + runas: RunAs } pub enum Depend { Network, Custom(Vec) @@ -54,11 +197,11 @@ svcname: svcname.to_string(), #[cfg(windows)] display_name: None, - #[cfg(any(windows, feature = "systemd"))] + #[cfg(any(windows, all(target_os = "linux", feature = "systemd")))] description: None, #[cfg(windows)] regconf: None, @@ -68,11 +211,74 @@ autostart: false, workdir: None, - deps: Vec::new() + deps: Vec::new(), + + log_level: None, + + trace_level: None, + + trace_file: None, + + runas: RunAs::default() + } + } + + #[cfg(feature = "clap")] + pub fn from_cmd_match(matches: &ArgMatches) -> Self { + let svcname = matches.get_one::("svcname").unwrap().to_owned(); + let autostart = matches.get_flag("auto_start"); + #[cfg(windows)] + let dispname = matches.get_one::("display_name"); + + #[cfg(any(windows, all(target_os = "linux", feature = "systemd")))] + let descr = matches.get_one::("description"); + let args: Vec = if let Some(vr) = matches.get_many::("arg") + { + vr.map(String::from).collect() + } else { + Vec::new() + }; + let envs: Vec = if let Some(vr) = matches.get_many::("env") + { + vr.map(String::from).collect() + } else { + Vec::new() + }; + let workdir = matches.get_one::("workdir"); + + let mut environ = Vec::new(); + let mut it = envs.into_iter(); + while let Some((key, value)) = it.next_tuple() { + environ.push((key, value)); + } + + let log_level = matches.get_one::("log_level").copied(); + let trace_level = matches.get_one::("trace_level").copied(); + let trace_file = matches.get_one::("trace_file").cloned(); + + let runas = RunAs::default(); + + Self { + svcname, + #[cfg(windows)] + display_name: dispname.cloned(), + #[cfg(any(windows, all(target_os = "linux", feature = "systemd")))] + description: descr.cloned(), + #[cfg(windows)] + regconf: None, + args: args.to_vec(), + envs: environ, + autostart, + workdir: workdir.cloned(), + deps: Vec::new(), + log_level, + trace_level, + trace_file, + runas } } pub fn svcname(&self) -> &str { &self.svcname @@ -82,17 +288,17 @@ pub fn display_name(mut self, name: impl ToString) -> Self { self.display_name = Some(name.to_string()); self } - #[cfg(any(windows, feature = "systemd"))] + #[cfg(any(windows, all(target_os = "linux", feature = "systemd")))] pub fn description(mut self, text: impl ToString) -> Self { self.description = Some(text.to_string()); self } - #[cfg(any(windows, feature = "systemd"))] + #[cfg(any(windows, all(target_os = "linux", feature = "systemd")))] pub fn description_ref(&mut self, text: impl ToString) -> &mut Self { self.description = Some(text.to_string()); self } @@ -239,19 +445,40 @@ pub fn register(self) -> Result<(), Error> { #[cfg(windows)] winsvc::install(self)?; + #[cfg(target_os = "macos")] + launchd::install(self)?; + + #[cfg(all(target_os = "linux", feature = "systemd"))] + systemd::install(self)?; + Ok(()) } } -// ToDo: Return Error::NotImplemented for unsupported platforms +#[allow(unreachable_code)] pub fn uninstall(svcname: &str) -> Result<(), Error> { #[cfg(windows)] - winsvc::uninstall(svcname)?; + { + winsvc::uninstall(svcname)?; + return Ok(()); + } + + #[cfg(target_os = "macos")] + { + launchd::uninstall(svcname)?; + return Ok(()); + } + + #[cfg(all(target_os = "linux", feature = "systemd"))] + { + systemd::uninstall(svcname)?; + return Ok(()); + } - Ok(()) + Err(Error::Unsupported) } // vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : Index: src/installer/launchd.rs ================================================================== --- src/installer/launchd.rs +++ src/installer/launchd.rs @@ -1,6 +1,6 @@ -use std::sync::Arc; +use std::{fs, path::Path, sync::Arc}; use sidoc::{Builder, RenderContext}; use crate::err::Error; @@ -11,17 +11,34 @@ bldr.scope(r#""#, Some("")); bldr.scope("", Some("" for now bldr.line(r#"Label"#); - bldr.line(format!("local.{}", ctx.svcname())); + bldr.line(format!("{}", ctx.svcname())); let service_binary_path = ::std::env::current_exe()? .to_str() .ok_or(Error::bad_format("Executable pathname is not utf-8"))? .to_string(); + + if let Some(ref username) = ctx.runas.user { + bldr.line(r#"UserName"#); + bldr.line(format!("{}", username)); + } + if let Some(ref groupname) = ctx.runas.group { + bldr.line(r#"GroupName"#); + bldr.line(format!("{}", groupname)); + } + if ctx.runas.initgroups { + bldr.line(r#"InitGroups"#); + bldr.line(""); + } + if let Some(ref umask) = ctx.runas.umask { + bldr.line(r#"Umask"#); + bldr.line(format!("{}", umask)); + } if ctx.have_args() { bldr.line(r#"ProgramArguments"#); bldr.scope("", Some("{}", service_binary_path)); @@ -35,21 +52,38 @@ bldr.line(r#"Program"#); bldr.line(format!("{}", service_binary_path)); } + let mut envs = Vec::new(); + if let Some(ll) = ctx.log_level { + envs.push((String::from("LOG_LEVEL"), ll.to_string())); + } + if let Some(ll) = ctx.trace_level { + envs.push((String::from("TRACE_LEVEL"), ll.to_string())); + } + if let Some(ref fname) = ctx.trace_file { + envs.push((String::from("TRACE_FILE"), fname.to_string())); + } if ctx.have_envs() { + for (key, value) in &ctx.envs { + envs.push((key.to_string(), value.to_string())); + } + } + + if !envs.is_empty() { bldr.line(r#"EnvironmentVariables"#); bldr.scope("", Some("{}", key)); bldr.line(format!("{}", value)); } bldr.exit(); // } + if let Some(wd) = ctx.workdir { bldr.line("WorkingDirectory"); bldr.line(format!("{}", wd)); } @@ -61,20 +95,35 @@ bldr.exit(); // bldr.exit(); // - let doc = bldr.build().unwrap(); + let doc = bldr.build()?; + // Create a render context, add document to it let mut r = RenderContext::new(); r.doc("root", Arc::new(doc)); // Render the output - let buf = r.render("root").unwrap(); - assert_eq!(buf, "\n\n\n"); + let buf = r.render("root")?; + + // ToDo: Set proper path + let fname = format!("{}.plist", ctx.svcname); + let fname = Path::new(&fname); + + // ToDo: If plist file already exist then fail -- unless force flag was + // specified. + if fname.exists() { + Err(Error::io("File already exists."))?; + } + + fs::write(fname, buf)?; + + Ok(()) +} - +pub fn uninstall(_svcname: &str) -> Result<(), Error> { Ok(()) } // vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : Index: src/installer/systemd.rs ================================================================== --- src/installer/systemd.rs +++ src/installer/systemd.rs @@ -1,5 +1,7 @@ +use std::{fs, path::Path}; + use crate::err::Error; pub fn install(ctx: super::RegSvc) -> Result<(), Error> { let service_binary_path = ::std::env::current_exe()? .to_str() @@ -19,10 +21,30 @@ // [Service] // let mut svc_lines: Vec = vec![]; svc_lines.push("[Service]".into()); svc_lines.push("Type=notify".into()); + + if let Some(ref username) = ctx.runas.user { + svc_lines.push(format!(r#"User="{}""#, username)); + } + if let Some(ref groupname) = ctx.runas.group { + svc_lines.push(format!(r#"Group="{}""#, groupname)); + } + if let Some(ref umask) = ctx.runas.umask { + svc_lines.push(format!(r#"UMask="{}""#, umask)); + } + + if let Some(ll) = ctx.log_level { + svc_lines.push(format!(r#"Environment="LOG_LEVEL={}""#, ll.to_string())); + } + if let Some(ll) = ctx.trace_level { + svc_lines.push(format!(r#"Environment="TRACE_LEVEL={}""#, ll.to_string())); + } + if let Some(fname) = ctx.trace_file { + svc_lines.push(format!(r#"Environment="TRACE_FILE={}""#, fname)); + } for (key, value) in &ctx.envs { svc_lines.push(format!(r#"Environment="{}={}""#, key, value)); } if let Some(wd) = ctx.workdir { svc_lines.push(format!("WorkingDirectory={}", wd)); @@ -42,11 +64,27 @@ let mut blocks: Vec = vec![]; blocks.push(unit_lines.join("\n")); blocks.push(svc_lines.join("\n")); blocks.push(inst_lines.join("\n")); - let filebuf = blocks.join("\n"); + let filebuf = blocks.join("\n\n"); + + // ToDo: Set proper path + let fname = format!("{}.service", ctx.svcname); + let fname = Path::new(&fname); + + // ToDo: If plist file already exist then fail -- unless force flag was + // specified. + if fname.exists() { + Err(Error::io("File already exists."))?; + } + + fs::write(fname, filebuf)?; + + Ok(()) +} - unimplemented!() +pub fn uninstall(_svcname: &str) -> Result<(), Error> { + Ok(()) } // 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 @@ -8,20 +8,28 @@ service_manager::{ServiceManager, ServiceManagerAccess} }; use crate::{ err::Error, - winsvc::{create_service_params, read_service_subkey, write_service_subkey} + winsvc::{create_service_params, write_service_subkey} }; 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. let status = RefCell::new(false); + // Register an event source named by the service name. eventlog::register(&svcname)?; + + // The event source registration was successful and is a persistent change. + // If this function returns early due to an error we want to roll back the + // changes it made up to that point. This scope guard is used to deregister + // the event source of the function returns early. let _status = scopeguard::guard(&status, |st| { if !*st.borrow() { if eventlog::deregister(svcname).is_err() { eprintln!("!!> Unable to deregister event source"); } @@ -103,11 +111,10 @@ if let Some(ref desc) = ctx.description { service.set_description(desc)?; } - // ToDo: Fix this. if ctx.have_envs() { let key = write_service_subkey(svcname)?; let envs: Vec = ctx .envs .iter() @@ -121,10 +128,21 @@ let mut params = create_service_params(svcname)?; if let Some(wd) = ctx.workdir { params.set_value("WorkDir", &wd)?; } + + if let Some(ll) = ctx.log_level { + params.set_value("LogLevel", &ll.to_string())?; + } + if let Some(ll) = ctx.trace_level { + params.set_value("TraceLevel", &ll.to_string())?; + } + if let Some(fname) = ctx.trace_file { + params.set_value("TraceFile", &fname.to_string())?; + } + // Give application the opportunity to create registry keys. if let Some(cb) = ctx.regconf { cb(svcname, &mut params)?; } Index: src/lib.rs ================================================================== --- src/lib.rs +++ src/lib.rs @@ -9,10 +9,14 @@ mod err; mod lumberjack; mod nosvc; mod rttype; pub mod signals; + +#[cfg(feature = "clap")] +#[cfg_attr(docsrs, doc(cfg(feature = "clap")))] +pub mod argp; #[cfg(feature = "installer")] #[cfg_attr(docsrs, doc(cfg(feature = "installer")))] pub mod installer; @@ -21,13 +25,13 @@ mod systemd; #[cfg(windows)] pub mod winsvc; -use std::{path::Path, sync::Arc}; +use std::{ffi::OsStr, path::Path, sync::Arc}; -use tokio::sync::broadcast; +use tokio::{runtime, sync::broadcast}; pub use async_trait::async_trait; pub use lumberjack::LumberJack; @@ -87,18 +91,40 @@ async fn shutdown(&mut self, ss: StopState) -> Result<(), Error>; } -/// Rocket server application. +/// Rocket server application handler. +/// +/// While Rocket is built on top of tokio, it \[Rocket\] wants to initialize +/// tokio itself. +/// +/// There are two major ways to write Rocket services using qsu; either the +/// application can let qsu be aware of the server applications' `Rocket` +/// instances. It does this by creating the `Rocket` instances in +/// `RocketServiceHandler::init()` and returns them. _qsu_ will ignite these +/// rockets and pass them to `RocketServiceHandler::run()`. The application is +/// responsible for launching the rockets at this point. +/// +/// The other way to do it is to completely manage the `Rocket` instances in +/// application code (by not returning rocket instances from `init()`). +/// +/// Allowing _qsu_ to manage the `Rocket` instances will cause _qsu_ to request +/// graceful shutdown of all `Rocket` instances once a `SvcEvt::Shutdown` is +/// sent by the runtime. +/// +/// It is recommended that `ctrlc` shutdown and termination signals are +/// disabled in each `Rocket` instance's configuration, and allow the _qsu_ +/// runtime to be responsible for initiating the `Rocket` shutdown. #[cfg(feature = "rocket")] +#[cfg_attr(docsrs, doc(cfg(feature = "rocket")))] #[async_trait] pub trait RocketServiceHandler { /// Rocket service initialization. /// - /// The returned `Rocket`s will be ignited and their shutdown handles will be - /// triggered on shutdown/ + /// The returned `Rocket`s will be ignited and their shutdown handlers will + /// be triggered on shutdown. async fn init( &mut self, ss: StartState ) -> Result>, Error>; @@ -135,27 +161,42 @@ /// Windows. ReloadConf, /// The service subsystem (or equivalent) has requested that the service /// shutdown. - Shutdown + Shutdown, + + Terminate } +/// Channel end-point used to receive events from the service subsystem. pub struct SvcEvtReader { rx: broadcast::Receiver } impl SvcEvtReader { + /// Block and wait for an event. + /// + /// Once `SvcEvt::Shutdown` or `SvcEvt::Terminate` has been received, this + /// method should not be called again. pub fn recv(&mut self) -> Option { self.rx.blocking_recv().ok() } + /// Attemt to get next event. + /// + /// Once `SvcEvt::Shutdown` or `SvcEvt::Terminate` has been received, this + /// method should not be called again. pub fn try_recv(&mut self) -> Option { self.rx.try_recv().ok() } + /// Async wait for an event. + /// + /// Once `SvcEvt::Shutdown` or `SvcEvt::Terminate` has been received, this + /// method should not be called again. pub async fn arecv(&mut self) -> Option { self.rx.recv().await.ok() } } @@ -163,13 +204,17 @@ /// The types of service types supported. pub enum SvcType { Sync(Box), /// Initializa a tokio runtime. - Tokio(Box), + Tokio( + Option, + Box + ), #[cfg(feature = "rocket")] + #[cfg_attr(docsrs, doc(cfg(feature = "rocket")))] /// Rocket 0.5rc.3 insists on initializing tokio itself. Rocket(Box) } @@ -180,18 +225,20 @@ } impl RunCtx { /// Run as a systemd service. #[cfg(all(target_os = "linux", feature = "systemd"))] - fn systemd(svcname: &str, st: SvcType) -> Result<(), Error> { - LumberJack::default().init(); + fn systemd(_svcname: &str, st: SvcType) -> Result<(), Error> { + LumberJack::default().init()?; let reporter = Arc::new(systemd::ServiceReporter {}); let res = match st { SvcType::Sync(handler) => rttype::sync_main(handler, reporter, None), - SvcType::Tokio(handler) => rttype::tokio_main(handler, reporter, None), + SvcType::Tokio(rtbldr, handler) => { + rttype::tokio_main(rtbldr, handler, reporter, None) + } SvcType::Rocket(handler) => rttype::rocket_main(handler, reporter, None) }; res } @@ -210,11 +257,13 @@ let reporter = Arc::new(nosvc::ServiceReporter {}); match st { SvcType::Sync(handler) => rttype::sync_main(handler, reporter, None), - SvcType::Tokio(handler) => rttype::tokio_main(handler, reporter, None), + SvcType::Tokio(rtbldr, handler) => { + rttype::tokio_main(rtbldr, handler, reporter, None) + } #[cfg(feature = "rocket")] SvcType::Rocket(handler) => rttype::rocket_main(handler, reporter, None) } } @@ -274,16 +323,17 @@ ) -> Result<(), Error> { self.run(SvcType::Sync(handler)) } /// Convenience method around [`Self::run()`] using [`SvcType::Tokio`]. - #[cfg(feature = "tokio")] + //#[cfg(feature = "tokio")] pub fn run_tokio( self, + rtbldr: Option, handler: Box ) -> Result<(), Error> { - self.run(SvcType::Tokio(handler)) + self.run(SvcType::Tokio(rtbldr, handler)) } /// Convenience method around [`Self::run()`] using [`SvcType::Rocket`]. #[cfg(feature = "rocket")] pub fn run_rocket( @@ -305,9 +355,25 @@ let name = binary_path.file_name()?; let name = Path::new(name); let name = name.file_stem()?; - name.to_str().map(String::from) + mkname(name) +} + +#[cfg(not(target_os = "macos"))] +fn mkname(nm: &OsStr) -> Option { + nm.to_str().map(String::from) +} + +#[cfg(target_os = "macos")] +fn mkname(nm: &OsStr) -> Option { + nm.to_str().map(|x| format!("local.{}", x)) +} + + +pub fn leak_default_service_name() -> Option<&'static str> { + let svcname = default_service_name()?; + Some(svcname.leak()) } // vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : Index: src/lumberjack.rs ================================================================== --- src/lumberjack.rs +++ src/lumberjack.rs @@ -6,10 +6,13 @@ }; use time::macros::format_description; use tracing_subscriber::{fmt::time::UtcTime, FmtSubscriber}; + +#[cfg(feature = "clap")] +use clap::ValueEnum; use crate::err::Error; #[derive(Default)] @@ -140,16 +143,34 @@ } } #[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "clap", derive(ValueEnum))] pub enum LogLevel { + /// No logging. + #[cfg_attr(feature = "clap", clap(name = "off"))] Off, + + /// Log errors. + #[cfg_attr(feature = "clap", clap(name = "error"))] Error, + + /// Log warnings and errors. + #[cfg_attr(feature = "clap", clap(name = "warn"))] Warn, + + /// Log info, warnings and errors. + #[cfg_attr(feature = "clap", clap(name = "info"))] Info, + + /// Log debug, info, warnings and errors. + #[cfg_attr(feature = "clap", clap(name = "debug"))] Debug, + + /// Log trace, debug, info, warninga and errors. + #[cfg_attr(feature = "clap", clap(name = "trace"))] Trace } impl FromStr for LogLevel { type Err = Error; Index: src/rttype/rocket.rs ================================================================== --- src/rttype/rocket.rs +++ src/rttype/rocket.rs @@ -52,16 +52,30 @@ // Create channel used to signal events to application let (tx, rx) = broadcast::channel(16); let ks2 = ks.clone(); + // SIGINT (on unix) and Ctrl+C on Windows should trigger a Shutdown event. let txc = tx.clone(); task::spawn(signals::wait_shutdown( move || { if let Err(e) = txc.send(SvcEvt::Shutdown) { log::error!("Unable to send SvcEvt::Shutdown event; {}", e); } + }, + ks2 + )); + + // SIGTERM (on unix) and Ctrl+Break/Close on Windows should trigger a + // Terminate event. + let txc = tx.clone(); + let ks2 = ks.clone(); + task::spawn(signals::wait_term( + move || { + if let Err(e) = txc.send(SvcEvt::Terminate) { + log::error!("Unable to send SvcEvt::Terminate event; {}", e); + } }, ks2 )); // There doesn't seem to be anything equivalent to SIGHUP for Windows @@ -121,10 +135,18 @@ for shutdown in rocket_shutdowns { // Tell this rocket instance to shut down gracefully. shutdown.notify(); } break; + } + Ok(SvcEvt::Terminate) => { + tracing::trace!("Ask rocket instances to shut down gracefully"); + for shutdown in rocket_shutdowns { + // Tell this rocket instance to shut down gracefully. + shutdown.notify(); + } + break; } Ok(_) => { tracing::trace!("Ignored message in wask waiting for shutdown"); continue; } @@ -143,11 +165,17 @@ // Now that the main application has terminated kill off any remaining // auxiliary tasks (read: signal waiters) ks.trigger(); - jh_graceful_landing.await; + // .. and wait for + if let Err(e) = jh_graceful_landing.await { + log::warn!( + "An error was returned from the graceful landing task; {}", + e + ); + } if let Err(_) = ks.finalize().await { log::warn!("Attempted to finalize KillSwitch that wasn't triggered yet"); } Index: src/rttype/sync.rs ================================================================== --- src/rttype/sync.rs +++ src/rttype/sync.rs @@ -11,11 +11,11 @@ use crate::{ err::Error, ServiceHandler, StartState, StateReporter, StopState, SvcEvt, SvcEvtReader }; -// ToDo: Set up a signal handling so we can cat SIGINT, SIGTERM and SIGHUP in +// ToDo: Set up a signal handling so we can catch SIGINT, SIGTERM and SIGHUP in // sync/blocking land as well. pub(crate) fn sync_main( mut handler: Box, sr: Arc, rx_svcevt: Option> @@ -30,11 +30,11 @@ // On Windows, if rx_svcevt is None, means we're not running under the // service subsystem (i.e. we're running as a foreground process), so // register a Ctrl+C handler. #[cfg(windows)] - crate::signals::sync_kill_to_event(tx); + crate::signals::sync_kill_to_event(tx)?; rx }; let set = Box::new(SvcEvtReader { rx: rx_svcevt }); @@ -112,15 +112,21 @@ let mut sig: libc::c_int = 0; let ret = unsafe { libc::sigwait(&mask, &mut sig) }; if ret == 0 { let signal = Signal::try_from(sig).unwrap(); match signal { - Signal::SIGINT | Signal::SIGTERM => { + Signal::SIGINT => { if let Err(e) = tx_svcevt.send(SvcEvt::Shutdown) { log::error!("Unable to send SvcEvt::Shutdown event; {}", e); } break; + } + Signal::SIGTERM => { + if let Err(e) = tx_svcevt.send(SvcEvt::Terminate) { + log::error!("Unable to send SvcEvt::Terminate event; {}", e); + } + break; } Signal::SIGHUP => { if let Err(e) = tx_svcevt.send(SvcEvt::ReloadConf) { log::error!("Unable to send SvcEvt::ReloadConf event; {}", e); } Index: src/rttype/tokio.rs ================================================================== --- src/rttype/tokio.rs +++ src/rttype/tokio.rs @@ -1,23 +1,29 @@ use std::sync::Arc; -use tokio::{sync::broadcast, task}; +use tokio::{runtime, sync::broadcast, task}; use crate::{ err::Error, signals, StartState, StateReporter, StopState, SvcEvt, SvcEvtReader, TokioServiceHandler }; use killswitch::KillSwitch; pub(crate) fn tokio_main( + rtbldr: Option, handler: Box, sr: Arc, rx_svcevt: Option> ) -> Result<(), Error> { - let rt = tokio::runtime::Runtime::new().unwrap(); + let rt = if let Some(mut bldr) = rtbldr { + bldr.build()? + } else { + tokio::runtime::Runtime::new()? + }; rt.block_on(tokio_async_main(handler, sr, rx_svcevt))?; + Ok(()) } /// The `async` main function for tokio servers. /// @@ -40,15 +46,29 @@ // Create channel used to signal events to application let (tx, rx) = broadcast::channel(16); let ks2 = ks.clone(); + // SIGINT (on unix) and Ctrl+C on Windows should trigger a Shutdown event. let txc = tx.clone(); task::spawn(signals::wait_shutdown( move || { if let Err(e) = txc.send(SvcEvt::Shutdown) { - log::error!("Unable to send EvcEvt::ReloadConf event; {}", e); + log::error!("Unable to send SvcEvt::ReloadConf event; {}", e); + } + }, + ks2 + )); + + // SIGTERM (on unix) and Ctrl+Break/Close on Windows should trigger a + // Terminate event. + let txc = tx.clone(); + let ks2 = ks.clone(); + task::spawn(signals::wait_term( + move || { + if let Err(e) = txc.send(SvcEvt::Terminate) { + log::error!("Unable to send SvcEvt::Terminate event; {}", e); } }, ks2 )); Index: src/signals.rs ================================================================== --- src/signals.rs +++ src/signals.rs @@ -5,14 +5,14 @@ #[cfg(windows)] mod win; #[cfg(unix)] -pub use unix::{wait_reload, wait_shutdown}; +pub use unix::{wait_reload, wait_shutdown, wait_term}; #[cfg(windows)] -pub use win::wait_shutdown; +pub use win::{wait_shutdown, wait_term}; #[cfg(windows)] pub(crate) use win::sync_kill_to_event; // vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : Index: src/signals/unix.rs ================================================================== --- src/signals/unix.rs +++ src/signals/unix.rs @@ -8,39 +8,55 @@ /// the task is terminated. pub async fn wait_shutdown(f: F, ks: KillSwitch) where F: FnOnce() { - tracing::trace!("SIGINT/SIGTERM task launched"); + tracing::trace!("SIGINT task launched"); - let (Ok(mut sigint), Ok(mut sigterm)) = ( - signal(SignalKind::interrupt()), - signal(SignalKind::terminate()) - ) else { - log::error!("Unable to create SIGINT/SIGTERM Futures"); + let Ok(mut sigint) = signal(SignalKind::interrupt()) else { + log::error!("Unable to create SIGINT Future"); return; }; - - // Wait for either SIGINT or SIGTERM. + // Wait for SIGINT. tokio::select! { _ = sigint.recv() => { - tracing::debug!("Received SIGINT"); + tracing::debug!("Received SIGINT -- running closure"); f(); }, - _ = sigterm.recv() => { - tracing::debug!("Received SIGTERM"); - f(); - } _ = ks.wait() => { tracing::debug!("killswitch triggered"); } } tracing::trace!("wait_shutdown() terminating"); } +pub async fn wait_term(f: F, ks: KillSwitch) +where + F: FnOnce() +{ + tracing::trace!("SIGTERM task launched"); + + let Ok(mut sigterm) = signal(SignalKind::terminate()) else { + log::error!("Unable to create SIGTERM Future"); + return; + }; + + // Wait for either SIGTERM. + tokio::select! { + _ = sigterm.recv() => { + tracing::debug!("Received SIGTERM -- running closure"); + f(); + } + _ = ks.wait() => { + tracing::debug!("killswitch triggered"); + } + } + + tracing::trace!("wait_term() terminating"); +} /// Async task used to wait for SIGHUP /// /// Whenever a SIGHUP is signalled the closure in `f` is called. pub async fn wait_reload(f: F, ks: KillSwitch) Index: src/signals/win.rs ================================================================== --- src/signals/win.rs +++ src/signals/win.rs @@ -25,11 +25,10 @@ where F: FnOnce() { tracing::trace!("CTRL+C task launched"); - // ToDo: handle ctrl_break and ctrl_close as well tokio::select! { _ = signal::ctrl_c() => { tracing::debug!("Received Ctrl+C"); // Once any process termination signal has been received post call the // callback. @@ -41,21 +40,69 @@ } tracing::trace!("wait_shutdown() terminating"); } +pub async fn wait_term(f: F, ks: KillSwitch) +where + F: FnOnce() +{ + tracing::trace!("CTRL+Break/Close task launched"); + + let Ok(mut cbreak) = signal::windows::ctrl_break() else { + log::error!("Unable to create Ctrl+Break monitor"); + return; + }; + + let Ok(mut cclose) = signal::windows::ctrl_close() else { + log::error!("Unable to create Close monitor"); + return; + }; + + tokio::select! { + _ = cbreak.recv() => { + tracing::debug!("Received Ctrl+Break"); + // Once any process termination signal has been received post call the + // callback. + f(); + }, + _ = cclose.recv() => { + tracing::debug!("Received Close"); + // Once any process termination signal has been received post call the + // callback. + f(); + }, + _ = ks.wait() => { + tracing::debug!("killswitch triggered"); + } + } + + tracing::trace!("wait_term() terminating"); +} + pub(crate) fn sync_kill_to_event( tx: broadcast::Sender ) -> Result<(), Error> { setup_sync_fg_kill_handler(move |ty| { match ty { - CTRL_C_EVENT | CTRL_BREAK_EVENT | CTRL_CLOSE_EVENT => { + CTRL_C_EVENT => { tracing::trace!( "Received some kind of event that should trigger a shutdown." ); - if let Ok(_) = tx.send(SvcEvt::Shutdown) { + if tx.send(SvcEvt::Shutdown).is_ok() { + // We handled this event + TRUE + } else { + FALSE + } + } + CTRL_BREAK_EVENT | CTRL_CLOSE_EVENT => { + tracing::trace!( + "Received some kind of event that should trigger a termination." + ); + if tx.send(SvcEvt::Terminate).is_ok() { // We handled this event TRUE } else { FALSE } Index: src/winsvc.rs ================================================================== --- src/winsvc.rs +++ src/winsvc.rs @@ -112,11 +112,11 @@ debugger::output("Hello, debugger"); } // Create a one-shot channel used to receive a an initial handshake from the // service handler thread. - let (tx_fromsvc, mut rx_fromsvc) = oneshot::channel(); + let (tx_fromsvc, rx_fromsvc) = oneshot::channel(); // Create a buffer that will be used to transfer data to the service // subsystem's callback function. let xfer = Xfer { svcname: svcname.into(), @@ -164,24 +164,22 @@ panic!("Unable to receive handshake"); }; let reporter = Arc::new(ServiceReporter { tx: tx.clone() }); - let res = match st { + match st { SvcType::Sync(handler) => { crate::rttype::sync_main(handler, reporter, Some(rx)) } - SvcType::Tokio(handler) => { - crate::rttype::tokio_main(handler, reporter, Some(rx)) + SvcType::Tokio(rtbldr, handler) => { + crate::rttype::tokio_main(rtbldr, handler, reporter, Some(rx)) } #[cfg(feature = "rocket")] SvcType::Rocket(handler) => { crate::rttype::rocket_main(handler, reporter, Some(rx)) } - }; - - res + } } // Generate the windows service boilerplate. The boilerplate contains the // low-level service entry function (ffi_service_main) that parses incoming @@ -217,21 +215,32 @@ Ok(InitRes { handshake_reply, rx_tosvc, status_handle }) => { + // If svcinit() returned Ok(), it should have initialized logging. + // Return Ok() to main server app thread so it will kick off the main // server application. - tx_fromsvc.send(Ok(handshake_reply)); + if tx_fromsvc.send(Ok(handshake_reply)).is_err() { + log::error!("Unable to send handshake message"); + return; + } // Enter a loop that waits to receive a service termination event. if let Err(e) = svcloop(rx_tosvc, status_handle) { log::error!("The service loop failed; {}", e); } } Err(e) => { - tx_fromsvc.send(Err(e)); + // If svcinit() returns Err() we don't actually know if logging has been + // enabled yet -- but we can't do much other than hope that it is and try + // to output an error log. + // ToDo: If dbgtools-win is used, then we should output to the debugger. + if tx_fromsvc.send(Err(e)).is_err() { + log::error!("Unable to send handshake message"); + } } } } @@ -245,23 +254,23 @@ // This must be done _before_ sending the HandskageMsg back to the service // main thread. // ToDo: Need proper error handling: // - If the Paramters subkey can not be loaded, do we abort? // - If the cwd can not be changed to the WorkDir we should abort. - if let Ok(svcparams) = get_service_params_subkey(&svcname) { + if let Ok(svcparams) = get_service_params_subkey(svcname) { if let Ok(wd) = svcparams.get_value::("WorkDir") { std::env::set_current_dir(wd).map_err(|e| { Error::internal(format!("Unable to switch to WorkDir; {}", e)) })?; } } // Create channel that will be used to receive messages from the application. - let (tx_tosvc, mut rx_tosvc) = unbounded_channel(); + let (tx_tosvc, rx_tosvc) = unbounded_channel(); // Create channel that will be used to send messages to the application. - let (tx_svcevt, mut rx_svcevt) = broadcast::channel(16); + let (tx_svcevt, rx_svcevt) = broadcast::channel(16); // // Define system service event handler that will be receiving service events. // let event_handler = move |control_event| -> ServiceControlHandlerResult { @@ -348,11 +357,11 @@ if let Err(e) = status_handle.set_service_status(ServiceStatus { service_type: SERVICE_TYPE, current_state: ServiceState::StartPending, controls_accepted: ServiceControlAccept::empty(), exit_code: ServiceExitCode::Win32(0), - checkpoint: checkpoint, + checkpoint, wait_hint: SERVICE_STARTPENDING_TIME, process_id: None }) { log::error!( "Unable to set service status to 'start pending {}'; {}", @@ -379,11 +388,11 @@ if let Err(e) = status_handle.set_service_status(ServiceStatus { service_type: SERVICE_TYPE, current_state: ServiceState::StopPending, controls_accepted: ServiceControlAccept::empty(), exit_code: ServiceExitCode::Win32(0), - checkpoint: checkpoint, + checkpoint, wait_hint: SERVICE_STOPPENDING_TIME, process_id: None }) { log::error!( "Unable to set service status to 'stop pending {}'; {}", Index: www/changelog.md ================================================================== --- www/changelog.md +++ www/changelog.md @@ -2,11 +2,24 @@ ## [Unreleased] ### Added +- Added some optional clap integration convenience functionality, that can be + enabled using the 'clap' feature. +- Added `SvcEvt::Terminate`. +- Argument parser allows setting default service logging/tracing settings when + registering service. +- High-level argument parser that wraps service registration, deregistration, + and running has been integrated into the qsu core library. + ### Changed + +- SIGTERM/Ctrl+Break/Close sends `SvcEvt::Terminate` rather than + `SvcEvt::Shutdown`. +- `eventlog` errors are mapped to `Error::LumberJack` (instead of + `Error::SubSystem`). ### Removed --- Index: www/design-notes.md ================================================================== --- www/design-notes.md +++ www/design-notes.md @@ -7,10 +7,12 @@ same interface and semantics when running as a foreground process). In addition to the service wrapper runtime, _qsu_ includes: - Initialization of the `log` and `tracing` crates. - System service installer/uninstaller wrappers. +- A command line argument parser, based on clap, which defines some common + semantics for registering, deregistering and running services. ## Service subsystem integration Traditional Unix server processes "daemonize", which involves steps to make Index: www/index.md ================================================================== --- www/index.md +++ www/index.md @@ -42,10 +42,12 @@ ### All platforms - Uses both the `log` crate and `tracing` crate. - `log` is intended for production logging. - `tracing` is intended for developer and debugging logging. +- The optional built-in command line parser assumes that there are at least + three subcommands (used to register, deregister and run service). ### Unixy platforms and running as a foreground process in Windows - The following environment variables control logging/tracing: - `LOG_LEVEL` is used to control the logging level for the `log` crate. - `TRACE_LEVEL` is used to control the tracing level for the `tracing` @@ -75,19 +77,48 @@ variables `TRACE_FILE` and `TRACE_LEVEL`. Both these must be configured in the registry to enable tracing. - Logging through `log` will log to the Windows Events Log. - Logging using `trace` will write trace logs to a file. + +## Feature labels in documentation + +The crate's documentation uses automatically generated feature labels, which +currently requires nightly featuers. To build the documentation locally use: + +``` +RUSTFLAGS="--cfg docsrs" RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features +``` + + +## Known limitations + +- There are several assumptions made in _qsu_ about paths being utf-8. + Even if a public interface takes a `Path` or `PathBuf`, the function may + return an error if the path isn't utf-8 compliant. + + +## Examples + +- The repository contains three different in-tree examples: + - `hellosvc` is a "sync" (read: non-`async`) server application which dumps + logs and traces every 30 seconds until the service is terminated. + - `hellosvc-tokio` is the same as `hellosvc`, but is an `async` server that + runs on top of tokio. + - `hellosvc-rocket` is a Rocket server that writes logs and traces each time + a request it made to the index page. + ## Change log The details of changes can always be found in the timeline, but for a high-level view of changes between released versions there's a manually maintained [Change Log](./changelog.md). + ## Project status -This crate is still in early prototyping stage. It works for basic use-cases, -but the API and some of the semantics are likely to change. The error handling -needs work. +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. The error handling needs work.