Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Difference From qsu-0.0.1 To qsu-0.0.2
2023-10-19
| ||
03:59 | Make a note about staticrocket. check-in: a54c7f6793 user: jan tags: trunk | |
03:32 | Category cleanup. check-in: 83c296e121 user: jan tags: qsu-0.0.2, trunk | |
03:29 | Cleanup. check-in: 00799d58af user: jan tags: trunk | |
2023-10-16
| ||
17:35 | Merge clap helpers. check-in: 397465de10 user: jan tags: trunk | |
00:20 | Start working on clap helpers for assisting in service registration/deregistration. check-in: 5f16ec8cf1 user: jan tags: clap-argp | |
2023-10-14
| ||
23:34 | Include examples when publishing. check-in: 429071c063 user: jan tags: qsu-0.0.1, trunk | |
23:32 | Happy clippy. check-in: 7235efb966 user: jan tags: trunk | |
Changes to .efiles.
︙ | ︙ | |||
16 17 18 19 20 21 22 23 24 25 26 | src/rttype/sync.rs src/rttype/tokio.rs src/rttype/rocket.rs src/installer.rs src/installer/winsvc.rs src/installer/launchd.rs src/installer/systemd.rs examples/hellosvc.rs examples/hellosvc-tokio.rs examples/hellosvc-rocket.rs examples/argp/mod.rs | > > > | 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | src/rttype/sync.rs src/rttype/tokio.rs 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 |
Changes to Cargo.toml.
1 2 | [package] name = "qsu" | | | > > | > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | [package] name = "qsu" version = "0.0.2" edition = "2021" license = "0BSD" 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] 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"] 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 } tokio = { version = "1.33.0", features = [ "macros", "rt-multi-thread", "signal", "sync" |
︙ | ︙ | |||
65 66 67 68 69 70 71 | ] } winreg = { version = "0.51.0" } [dev-dependencies] clap = { version = "4.4.6", features = ["derive", "env", "wrap_help"] } tokio = { version = "1.33.0", features = ["time"] } | < | < | | | | 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 | ] } winreg = { version = "0.51.0" } [dev-dependencies] clap = { version = "4.4.6", features = ["derive", "env", "wrap_help"] } tokio = { version = "1.33.0", features = ["time"] } [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs", "--generate-link-to-definition"] [[example]] name = "hellosvc" required-features = ["clap", "installer"] [[example]] name = "hellosvc-tokio" required-features = ["clap", "installer"] [[example]] name = "hellosvc-rocket" required-features = ["clap", "installer", "rocket"] |
Changes to Rocket.toml.
1 2 3 4 5 6 7 8 9 | #[global] #port = 8000 [debug] address = "127.0.0.1" port = 8080 keep_alive = 5 log_level = "normal" #secret_key = [randomly generated at launch] | > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | #[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 log_level = "normal" #secret_key = [randomly generated at launch] |
︙ | ︙ |
Added build_docs.sh.
> > > > > > | 1 2 3 4 5 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 : |
Changes to examples/argp/mod.rs.
|
| | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | < < < < | < < < | < < < < < | | < < | < < < | | | < | | < < < < | | < < < < < | | | | | | | | | < < | | < < | < | < < < < < < < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | 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<RegSvc, qsu::Error> { // 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.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 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<io::Error> for Error { fn from(err: io::Error) -> Self { Error::IO(err.to_string()) } } impl From<qsu::Error> 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 : |
Changes to examples/hellosvc-rocket.rs.
1 2 3 4 | #[macro_use] extern crate rocket; mod argp; | | < | < | | > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | #[macro_use] extern crate rocket; mod argp; mod err; mod procres; use qsu::{ argp::ArgParser, RocketServiceHandler, StartState, StopState, SvcEvt, SvcEvtReader, SvcType }; use rocket::{Build, Ignite, Rocket}; use err::Error; use procres::ProcRes; struct MyService {} #[qsu::async_trait] impl RocketServiceHandler for MyService { async fn init( |
︙ | ︙ | |||
42 43 44 45 46 47 48 | ) -> Result<(), qsu::Error> { for rocket in rockets { tokio::task::spawn(async { rocket.launch().await.unwrap(); }); } | < < | > > > > > > > | < > > > | < > | > | < > > | > > > | > | > | 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 | ) -> Result<(), qsu::Error> { for rocket in rockets { tokio::task::spawn(async { rocket.launch().await.unwrap(); }); } loop { tokio::select! { evt = set.arecv() => { match evt { Some(SvcEvt::Shutdown) => { 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"); } _ => { } } } } } Ok(()) } async fn shutdown(&mut self, _ss: StopState) -> Result<(), qsu::Error> { tracing::trace!("Running shutdown()"); Ok(()) } } 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"); log::warn!("warn"); log::info!("info"); |
︙ | ︙ |
Changes to examples/hellosvc-tokio.rs.
1 2 3 4 | //! Simple service that does nothing other than log/trace every N seconds. mod argp; | > > < < | < < < | > > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | //! Simple service that does nothing other than log/trace every N seconds. mod argp; mod err; mod procres; use std::time::{Duration, Instant}; use qsu::{ argp::ArgParser, StartState, StopState, SvcEvt, SvcEvtReader, SvcType, TokioServiceHandler }; use err::Error; use procres::ProcRes; struct MyService {} #[qsu::async_trait] impl TokioServiceHandler for MyService { async fn init(&mut self, _ss: StartState) -> Result<(), qsu::Error> { tracing::trace!("Running init()"); |
︙ | ︙ | |||
47 48 49 50 51 52 53 | tokio::select! { _ = tokio::time::sleep(std::time::Duration::from_secs(1)) => { continue; } evt = set.arecv() => { match evt { Some(SvcEvt::Shutdown) => { | | > > > > > > | 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | tokio::select! { _ = tokio::time::sleep(std::time::Duration::from_secs(1)) => { continue; } evt = set.arecv() => { match evt { Some(SvcEvt::Shutdown) => { 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"); } _ => { } } |
︙ | ︙ | |||
69 70 71 72 73 74 75 | async fn shutdown(&mut self, _ss: StopState) -> Result<(), qsu::Error> { tracing::trace!("Running shutdown()"); Ok(()) } } | | | > > | < > | > | < > > | > > > | > | > | 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | async fn shutdown(&mut self, _ss: StopState) -> Result<(), qsu::Error> { tracing::trace!("Running shutdown()"); Ok(()) } } 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 : |
Changes to examples/hellosvc.rs.
1 2 3 4 5 6 7 8 9 | //! Simple service that does nothing other than log/trace every N seconds. mod argp; use std::{ thread, time::{Duration, Instant} }; | > > < < | > > > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | //! 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 qsu::{ argp::ArgParser, ServiceHandler, StartState, StopState, SvcEvt, SvcEvtReader, SvcType }; use err::Error; use procres::ProcRes; struct MyService {} impl ServiceHandler for MyService { fn init(&mut self, _ss: StartState) -> Result<(), qsu::Error> { tracing::trace!("Running init()"); Ok(()) |
︙ | ︙ | |||
42 43 44 45 46 47 48 | last_dump = Instant::now(); } match set.try_recv() { Some(SvcEvt::Shutdown) => { tracing::info!( | | > > > > > > | | | > > | < > | > | < > > | > > > | > | > | 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | last_dump = Instant::now(); } match set.try_recv() { Some(SvcEvt::Shutdown) => { 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 the application reload its \ configuration" ); } _ => {} } thread::sleep(std::time::Duration::from_secs(1)); } Ok(()) } fn shutdown(&mut self, _ss: StopState) -> Result<(), qsu::Error> { tracing::trace!("Running shutdown()"); Ok(()) } } 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.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 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<T> From<Result<T, Error>> for ProcRes { fn from(res: Result<T, Error>) -> 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.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 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<String>, /// Set an optional one-line service description. #[cfg(any(all(target_os = "linux", feature = "systemd"), windows))] #[arg(short, long, value_name = "DESC")] description: Option<String>, /// Add a command line argument to the service command line. #[arg(short, long)] arg: Vec<String>, /// Add an environment variable to the service. #[arg(short, long, num_args(2), value_names=["KEY", "VALUE"])] env: Vec<String>, /// Set an optional directory the service runtime should start in. #[arg(short, long, value_name = "DIR")] workdir: Option<String>, #[arg(long, value_enum, value_name = "LEVEL")] log_level: Option<LogLevel>, #[arg(long, value_enum, hide(true), value_name = "LEVEL")] trace_level: Option<LogLevel>, #[arg(long, value_enum, hide(true), value_name = "FNAME")] trace_file: Option<String> } 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::<String>("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::<String>("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<RegSvc, Error> { 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<ArgpRes, Error> { 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 <svcname>" 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<F>(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 : |
Changes to src/err.rs.
1 2 3 4 5 6 7 8 9 10 11 | use std::{fmt, io}; /// Errors that qsu will return to application. #[derive(Debug)] pub enum Error { BadFormat(String), Internal(String), IO(String), LumberJack(String), #[cfg(feature = "rocket")] Rocket(String), | > > > > > > > > > > > > > > > | > > > > > > > > > > > > > > > > > > > > > > > > > | > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | 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), Unsupported } impl Error { pub fn bad_format<S: ToString>(s: S) -> Self { Error::BadFormat(s.to_string()) } pub fn internal<S: ToString>(s: S) -> Self { Error::Internal(s.to_string()) } pub fn io<S: ToString>(s: S) -> Self { Error::IO(s.to_string()) } pub fn lumberjack<S: ToString>(s: S) -> Self { Error::LumberJack(s.to_string()) } } 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) } Error::IO(s) => { write!(f, "I/O error; {}", s) } Error::LumberJack(s) => { write!(f, "LumberJack error; {}", s) } #[cfg(feature = "rocket")] Error::Rocket(s) => { 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<clap::error::Error> for Error { fn from(err: clap::error::Error) -> Self { Error::ArgP(err.to_string()) } } */ #[cfg(windows)] impl From<eventlog::InitError> for Error { /// Map eventlog initialization errors to `Error::LumberJack`. fn from(err: eventlog::InitError) -> Self { Error::LumberJack(err.to_string()) } } #[cfg(windows)] impl From<eventlog::Error> for Error { /// Map eventlog errors to `Error::LumberJack`. fn from(err: eventlog::Error) -> Self { Error::LumberJack(err.to_string()) } } impl From<io::Error> for Error { fn from(err: io::Error) -> Self { Error::IO(err.to_string()) } |
︙ | ︙ | |||
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 | #[cfg(feature = "rocket")] impl From<rocket::Error> for Error { fn from(err: rocket::Error) -> Self { Error::Rocket(err.to_string()) } } #[cfg(windows)] impl From<windows_service::Error> 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 : | > > > > > > > | 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 | #[cfg(feature = "rocket")] impl From<rocket::Error> for Error { fn from(err: rocket::Error) -> Self { Error::Rocket(err.to_string()) } } #[cfg(feature = "installer")] impl From<sidoc::Error> for Error { fn from(err: sidoc::Error) -> Self { Error::Internal(err.to_string()) } } #[cfg(windows)] impl From<windows_service::Error> 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 : |
Changes to src/installer.rs.
1 2 3 4 5 6 7 8 | //! Helpers for installing/uninstalling services. #[cfg(windows)] pub mod winsvc; #[cfg(target_os = "macos")] pub mod launchd; | > > > > > > > > > | > > > > > > > > > > > > > > > > > > > > > > | > > > > > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 | //! Helpers for installing/uninstalling services. #[cfg(windows)] pub mod winsvc; #[cfg(target_os = "macos")] pub mod launchd; #[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<String>, group: Option<String>, #[cfg(target_os = "macos")] initgroups: bool, #[cfg(any( target_os = "macos", all(target_os = "linux", feature = "systemd") ))] umask: Option<String> } pub struct RegSvc { pub svcname: String, #[cfg(windows)] pub display_name: Option<String>, #[cfg(any(windows, all(target_os = "linux", feature = "systemd")))] pub description: Option<String>, #[cfg(windows)] pub regconf: Option<Box<dyn FnOnce(&str, &mut winreg::RegKey) -> Result<(), Error>>>, /// Command line arguments. |
︙ | ︙ | |||
36 37 38 39 40 41 42 | /// By default the service will be registered, but needs to be started /// manually. pub autostart: bool, pub(crate) workdir: Option<String>, /// List of service dependencies. | | > > > > > > > > | | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | | | 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 | /// By default the service will be registered, but needs to be started /// manually. pub autostart: bool, pub(crate) workdir: Option<String>, /// List of service dependencies. deps: Vec<Depend>, log_level: Option<LogLevel>, trace_level: Option<LogLevel>, trace_file: Option<String>, runas: RunAs } pub enum Depend { Network, Custom(Vec<String>) } impl RegSvc { pub fn new(svcname: &str) -> Self { Self { svcname: svcname.to_string(), #[cfg(windows)] display_name: None, #[cfg(any(windows, all(target_os = "linux", feature = "systemd")))] description: None, #[cfg(windows)] regconf: None, args: Vec::new(), envs: Vec::new(), autostart: false, workdir: None, 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::<String>("svcname").unwrap().to_owned(); let autostart = matches.get_flag("auto_start"); #[cfg(windows)] let dispname = matches.get_one::<String>("display_name"); #[cfg(any(windows, all(target_os = "linux", feature = "systemd")))] let descr = matches.get_one::<String>("description"); let args: Vec<String> = if let Some(vr) = matches.get_many::<String>("arg") { vr.map(String::from).collect() } else { Vec::new() }; let envs: Vec<String> = if let Some(vr) = matches.get_many::<String>("env") { vr.map(String::from).collect() } else { Vec::new() }; let workdir = matches.get_one::<String>("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::<LogLevel>("log_level").copied(); let trace_level = matches.get_one::<LogLevel>("trace_level").copied(); let trace_file = matches.get_one::<String>("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 } #[cfg(windows)] pub fn display_name(mut self, name: impl ToString) -> Self { self.display_name = Some(name.to_string()); self } #[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, all(target_os = "linux", feature = "systemd")))] pub fn description_ref(&mut self, text: impl ToString) -> &mut Self { self.description = Some(text.to_string()); self } #[cfg(windows)] pub fn regconf<F>(mut self, f: F) -> Self |
︙ | ︙ | |||
237 238 239 240 241 242 243 244 245 246 247 248 | self } pub fn register(self) -> Result<(), Error> { #[cfg(windows)] winsvc::install(self)?; Ok(()) } } | > > > > > > | > | > | > > > > | > > > > > > > > > | 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 | self } 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(()) } } #[allow(unreachable_code)] pub fn uninstall(svcname: &str) -> Result<(), Error> { #[cfg(windows)] { 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(()); } Err(Error::Unsupported) } // vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : |
Changes to src/installer/launchd.rs.
|
| | | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | > | > | | > > > > > > > | > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 | use std::{fs, path::Path, sync::Arc}; use sidoc::{Builder, RenderContext}; use crate::err::Error; pub fn install(ctx: super::RegSvc) -> Result<(), Error> { let mut bldr = Builder::new(); bldr.line(r#"<?xml version="1.0" encoding="UTF-8"?>"#); bldr.line(r#"<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">"#); bldr.scope(r#"<plist version="1.0">"#, Some("</plist>")); bldr.scope("<dict>", Some("</dict")); // Use name "local.<svcname>" for now bldr.line(r#"<key>Label</key>"#); bldr.line(format!("<string>{}</string>", 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#"<key>UserName</key>"#); bldr.line(format!("<string>{}</string>", username)); } if let Some(ref groupname) = ctx.runas.group { bldr.line(r#"<key>GroupName</key>"#); bldr.line(format!("<string>{}</string>", groupname)); } if ctx.runas.initgroups { bldr.line(r#"<key>InitGroups</key>"#); bldr.line("<true/>"); } if let Some(ref umask) = ctx.runas.umask { bldr.line(r#"<key>Umask</key>"#); bldr.line(format!("<integer>{}</integer>", umask)); } if ctx.have_args() { bldr.line(r#"<key>ProgramArguments</key>"#); bldr.scope("<array>", Some("</array")); bldr.line(format!("<string>{}</string>", service_binary_path)); for arg in &ctx.args { bldr.line(format!("<string>{}</string>", arg)); } bldr.exit(); // <array> } else { bldr.line(r#"<key>Program</key>"#); bldr.line(format!("<string>{}</string>", 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#"<key>EnvironmentVariables</key>"#); bldr.scope("<dict>", Some("</dict")); for (key, value) in envs { bldr.line(format!("<key>{}</key>", key)); bldr.line(format!("<string>{}</string>", value)); } bldr.exit(); // <dict> } if let Some(wd) = ctx.workdir { bldr.line("<key>WorkingDirectory</key>"); bldr.line(format!("<string>{}</string>", wd)); } if ctx.autostart { bldr.line("<key>RunAtLoad</key>"); bldr.line("<true/>"); } bldr.exit(); // <dict> bldr.exit(); // <plist> 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")?; // 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 : |
Changes to src/installer/systemd.rs.
1 2 3 4 5 6 7 | use crate::err::Error; pub fn install(ctx: super::RegSvc) -> Result<(), Error> { let service_binary_path = ::std::env::current_exe()? .to_str() .ok_or(Error::bad_format("Executable pathname is not utf-8"))? .to_string(); | > > | 1 2 3 4 5 6 7 8 9 | 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() .ok_or(Error::bad_format("Executable pathname is not utf-8"))? .to_string(); |
︙ | ︙ | |||
17 18 19 20 21 22 23 24 25 26 27 28 29 30 | // // [Service] // let mut svc_lines: Vec<String> = vec![]; svc_lines.push("[Service]".into()); svc_lines.push("Type=notify".into()); 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)); } svc_lines.push(format!("ExecStart={}", service_binary_path)); | > > > > > > > > > > > > > > > > > > > > | 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | // // [Service] // let mut svc_lines: Vec<String> = 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)); } svc_lines.push(format!("ExecStart={}", service_binary_path)); |
︙ | ︙ | |||
40 41 42 43 44 45 46 | // Putting it all together // let mut blocks: Vec<String> = vec![]; blocks.push(unit_lines.join("\n")); blocks.push(svc_lines.join("\n")); blocks.push(inst_lines.join("\n")); | | > > > | > > > > > > > > > > > > > | 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | // Putting it all together // let mut blocks: Vec<String> = vec![]; blocks.push(unit_lines.join("\n")); blocks.push(svc_lines.join("\n")); blocks.push(inst_lines.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(()) } pub fn uninstall(_svcname: &str) -> Result<(), Error> { Ok(()) } // vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : |
Changes to src/installer/winsvc.rs.
1 2 3 4 5 6 7 8 9 10 11 12 | use std::{cell::RefCell, ffi::OsString, thread, time::Duration}; use windows_service::{ service::{ ServiceAccess, ServiceDependency, ServiceErrorControl, ServiceInfo, ServiceStartType, ServiceState, ServiceType }, service_manager::{ServiceManager, ServiceManagerAccess} }; use crate::{ err::Error, | | > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | use std::{cell::RefCell, ffi::OsString, thread, time::Duration}; use windows_service::{ service::{ ServiceAccess, ServiceDependency, ServiceErrorControl, ServiceInfo, ServiceStartType, ServiceState, ServiceType }, service_manager::{ServiceManager, ServiceManagerAccess} }; use crate::{ err::Error, 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"); } } }); |
︙ | ︙ | |||
101 102 103 104 105 106 107 | } }); if let Some(ref desc) = ctx.description { service.set_description(desc)?; } | < > > > > > > > > > > > | 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | } }); if let Some(ref desc) = ctx.description { service.set_description(desc)?; } if ctx.have_envs() { let key = write_service_subkey(svcname)?; let envs: Vec<String> = ctx .envs .iter() .map(|(k, v)| format!("{}={}", k, v)) .collect(); key.set_value("Environment", &envs)?; } //println!("==> Service installation successful"); 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)?; } // Mark status as success so the scopeguards won't attempt to reverse the |
︙ | ︙ |
Changes to src/lib.rs.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | //! _qsu_ is primarily a thin layer between the server application code and the //! operating system's service subsystem. When the application is not running //! under a service subsystem qsu may simulate parts of one so that the server //! application code does not need to diverge too far between the service and //! non-service cases. #![cfg_attr(docsrs, feature(doc_cfg))] mod err; mod lumberjack; mod nosvc; mod rttype; pub mod signals; #[cfg(feature = "installer")] #[cfg_attr(docsrs, doc(cfg(feature = "installer")))] pub mod installer; #[cfg(all(target_os = "linux", feature = "systemd"))] #[cfg_attr(docsrs, doc(cfg(feature = "systemd")))] mod systemd; #[cfg(windows)] pub mod winsvc; | > > > > | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | //! _qsu_ is primarily a thin layer between the server application code and the //! operating system's service subsystem. When the application is not running //! under a service subsystem qsu may simulate parts of one so that the server //! application code does not need to diverge too far between the service and //! non-service cases. #![cfg_attr(docsrs, feature(doc_cfg))] 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; #[cfg(all(target_os = "linux", feature = "systemd"))] #[cfg_attr(docsrs, doc(cfg(feature = "systemd")))] mod systemd; #[cfg(windows)] pub mod winsvc; use std::{ffi::OsStr, path::Path, sync::Arc}; use tokio::{runtime, sync::broadcast}; pub use async_trait::async_trait; pub use lumberjack::LumberJack; pub use crate::err::Error; |
︙ | ︙ | |||
85 86 87 88 89 90 91 | async fn run(&mut self, ser: SvcEvtReader) -> Result<(), Error>; async fn shutdown(&mut self, ss: StopState) -> Result<(), Error>; } | | > > > > > > > > > > > > > > > > > > > > > > | | | 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | async fn run(&mut self, ser: SvcEvtReader) -> Result<(), Error>; async fn shutdown(&mut self, ss: StopState) -> Result<(), Error>; } /// 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 handlers will /// be triggered on shutdown. async fn init( &mut self, ss: StartState ) -> Result<Vec<rocket::Rocket<rocket::Build>>, Error>; async fn run( &mut self, |
︙ | ︙ | |||
133 134 135 136 137 138 139 | /// /// On Unixy platforms this is triggered by SIGHUP, and is unsupported on /// Windows. ReloadConf, /// The service subsystem (or equivalent) has requested that the service /// shutdown. | | > > > > > > > > > > > > > > > > > | > > | | > | > | 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 | /// /// On Unixy platforms this is triggered by SIGHUP, and is unsupported on /// Windows. ReloadConf, /// The service subsystem (or equivalent) has requested that the service /// shutdown. Shutdown, Terminate } /// Channel end-point used to receive events from the service subsystem. pub struct SvcEvtReader { rx: broadcast::Receiver<SvcEvt> } 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<SvcEvt> { 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<SvcEvt> { 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<SvcEvt> { self.rx.recv().await.ok() } } /// The types of service types supported. pub enum SvcType { Sync(Box<dyn ServiceHandler + Send>), /// Initializa a tokio runtime. Tokio( Option<runtime::Builder>, Box<dyn TokioServiceHandler + Send> ), #[cfg(feature = "rocket")] #[cfg_attr(docsrs, doc(cfg(feature = "rocket")))] /// Rocket 0.5rc.3 insists on initializing tokio itself. Rocket(Box<dyn RocketServiceHandler + Send>) } /// Service configuration context. pub struct RunCtx { service: bool, svcname: String } 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()?; let reporter = Arc::new(systemd::ServiceReporter {}); let res = match st { SvcType::Sync(handler) => rttype::sync_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 } /// Run as a Windows service. |
︙ | ︙ | |||
208 209 210 211 212 213 214 | fn foreground(_svcname: &str, st: SvcType) -> Result<(), Error> { LumberJack::default().init()?; let reporter = Arc::new(nosvc::ServiceReporter {}); match st { SvcType::Sync(handler) => rttype::sync_main(handler, reporter, None), | > | > | 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 | fn foreground(_svcname: &str, st: SvcType) -> Result<(), Error> { LumberJack::default().init()?; let reporter = Arc::new(nosvc::ServiceReporter {}); match st { SvcType::Sync(handler) => rttype::sync_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) } } } |
︙ | ︙ | |||
272 273 274 275 276 277 278 | self, handler: Box<dyn ServiceHandler + Send> ) -> Result<(), Error> { self.run(SvcType::Sync(handler)) } /// Convenience method around [`Self::run()`] using [`SvcType::Tokio`]. | | > | | 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 | self, handler: Box<dyn ServiceHandler + Send> ) -> Result<(), Error> { self.run(SvcType::Sync(handler)) } /// Convenience method around [`Self::run()`] using [`SvcType::Tokio`]. //#[cfg(feature = "tokio")] pub fn run_tokio( self, rtbldr: Option<runtime::Builder>, handler: Box<dyn TokioServiceHandler + Send> ) -> Result<(), Error> { self.run(SvcType::Tokio(rtbldr, handler)) } /// Convenience method around [`Self::run()`] using [`SvcType::Rocket`]. #[cfg(feature = "rocket")] pub fn run_rocket( self, handler: Box<dyn RocketServiceHandler + Send> |
︙ | ︙ | |||
303 304 305 306 307 308 309 | pub fn default_service_name() -> Option<String> { let binary_path = ::std::env::current_exe().ok()?; let name = binary_path.file_name()?; let name = Path::new(name); let name = name.file_stem()?; | > > > > > | > > > > > > > > > > > | 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 | pub fn default_service_name() -> Option<String> { let binary_path = ::std::env::current_exe().ok()?; let name = binary_path.file_name()?; let name = Path::new(name); let name = name.file_stem()?; mkname(name) } #[cfg(not(target_os = "macos"))] fn mkname(nm: &OsStr) -> Option<String> { nm.to_str().map(String::from) } #[cfg(target_os = "macos")] fn mkname(nm: &OsStr) -> Option<String> { 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 : |
Changes to src/lumberjack.rs.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | use std::{ fmt, fs, io::Write, path::{Path, PathBuf}, str::FromStr }; use time::macros::format_description; use tracing_subscriber::{fmt::time::UtcTime, FmtSubscriber}; use crate::err::Error; #[derive(Default)] enum LogOut { #[default] | > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | use std::{ fmt, fs, io::Write, path::{Path, PathBuf}, str::FromStr }; use time::macros::format_description; use tracing_subscriber::{fmt::time::UtcTime, FmtSubscriber}; #[cfg(feature = "clap")] use clap::ValueEnum; use crate::err::Error; #[derive(Default)] enum LogOut { #[default] |
︙ | ︙ | |||
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | Ok(()) } } #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum LogLevel { Off, Error, Warn, Info, Debug, Trace } impl FromStr for LogLevel { type Err = Error; fn from_str(s: &str) -> Result<Self, Self::Err> { | > > > > > > > > > > > > > > > > > > | 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 | Ok(()) } } #[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; fn from_str(s: &str) -> Result<Self, Self::Err> { |
︙ | ︙ |
Changes to src/rttype/rocket.rs.
︙ | ︙ | |||
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | rx_svcevt } else { // Create channel used to signal events to application let (tx, rx) = broadcast::channel(16); let ks2 = ks.clone(); 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 )); // There doesn't seem to be anything equivalent to SIGHUP for Windows // (Services) #[cfg(unix)] | > > > > > > > > > > > > > > | 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | rx_svcevt } else { // 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 // (Services) #[cfg(unix)] |
︙ | ︙ | |||
119 120 121 122 123 124 125 126 127 128 129 130 131 132 | Ok(SvcEvt::Shutdown) => { 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; } Err(e) => { log::error!("Unable to receive broadcast SvcEvt message, {}", e); | > > > > > > > > | 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | Ok(SvcEvt::Shutdown) => { 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(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; } Err(e) => { log::error!("Unable to receive broadcast SvcEvt message, {}", e); |
︙ | ︙ | |||
141 142 143 144 145 146 147 | log::error!("Service application returned error; {}", e); } // Now that the main application has terminated kill off any remaining // auxiliary tasks (read: signal waiters) ks.trigger(); | > | > > > > > | 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 | log::error!("Service application returned error; {}", e); } // Now that the main application has terminated kill off any remaining // auxiliary tasks (read: signal waiters) ks.trigger(); // .. 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"); } // Call the application's shutdown() function. let ss = StopState { |
︙ | ︙ |
Changes to src/rttype/sync.rs.
︙ | ︙ | |||
9 10 11 12 13 14 15 | use nix::sys::signal::{SigSet, SigmaskHow, Signal}; use crate::{ err::Error, ServiceHandler, StartState, StateReporter, StopState, SvcEvt, SvcEvtReader }; | | | | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | use nix::sys::signal::{SigSet, SigmaskHow, Signal}; use crate::{ err::Error, ServiceHandler, StartState, StateReporter, StopState, SvcEvt, SvcEvtReader }; // 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<dyn ServiceHandler>, sr: Arc<dyn StateReporter + Send + Sync>, rx_svcevt: Option<broadcast::Receiver<SvcEvt>> ) -> Result<(), Error> { let rx_svcevt = if let Some(rx_svcevt) = rx_svcevt { rx_svcevt } else { let (tx, rx) = broadcast::channel(16); #[cfg(unix)] init_signals(tx)?; // 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)?; rx }; let set = Box::new(SvcEvtReader { rx: rx_svcevt }); // Call application's init() method. |
︙ | ︙ | |||
110 111 112 113 114 115 116 | loop { 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 { | | > > > > > > | 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | loop { 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 => { 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); } } _ => {} |
︙ | ︙ |
Changes to src/rttype/tokio.rs.
1 2 | use std::sync::Arc; | | > > > > | > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | use std::sync::Arc; 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<runtime::Builder>, handler: Box<dyn TokioServiceHandler>, sr: Arc<dyn StateReporter + Send + Sync>, rx_svcevt: Option<broadcast::Receiver<SvcEvt>> ) -> Result<(), Error> { 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. /// /// If `rx_svcevt` is `Some(_)` it means the channel was created elsewhere /// (implied: The transmitting endpoint lives somewhere else). If it is `None` |
︙ | ︙ | |||
38 39 40 41 42 43 44 45 46 47 48 | rx_svcevt } else { // Create channel used to signal events to application let (tx, rx) = broadcast::channel(16); let ks2 = ks.clone(); let txc = tx.clone(); task::spawn(signals::wait_shutdown( move || { if let Err(e) = txc.send(SvcEvt::Shutdown) { | > | > > > > > > > > > > > > > | 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | rx_svcevt } else { // 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::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 )); // There doesn't seem to be anything equivalent to SIGHUP for Windows // (Services) |
︙ | ︙ |
Changes to src/signals.rs.
1 2 3 4 5 6 7 8 9 | //! Signal monitoring. #[cfg(unix)] mod unix; #[cfg(windows)] mod win; #[cfg(unix)] | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | //! Signal monitoring. #[cfg(unix)] mod unix; #[cfg(windows)] mod win; #[cfg(unix)] pub use unix::{wait_reload, wait_shutdown, wait_term}; #[cfg(windows)] 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 : |
Changes to src/signals/unix.rs.
1 2 3 4 5 6 7 8 9 10 11 12 | use tokio::signal::unix::{signal, SignalKind}; use killswitch::KillSwitch; /// Async task used to wait for SIGINT/SIGTERM. /// /// Whenever a SIGINT or SIGTERM is signalled the closure in `f` is called and /// the task is terminated. pub async fn wait_shutdown<F>(f: F, ks: KillSwitch) where F: FnOnce() { | | < | < < | < | | < < < < > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | use tokio::signal::unix::{signal, SignalKind}; use killswitch::KillSwitch; /// Async task used to wait for SIGINT/SIGTERM. /// /// Whenever a SIGINT or SIGTERM is signalled the closure in `f` is called and /// the task is terminated. pub async fn wait_shutdown<F>(f: F, ks: KillSwitch) where F: FnOnce() { tracing::trace!("SIGINT task launched"); let Ok(mut sigint) = signal(SignalKind::interrupt()) else { log::error!("Unable to create SIGINT Future"); return; }; // Wait for SIGINT. tokio::select! { _ = sigint.recv() => { tracing::debug!("Received SIGINT -- running closure"); f(); }, _ = ks.wait() => { tracing::debug!("killswitch triggered"); } } tracing::trace!("wait_shutdown() terminating"); } pub async fn wait_term<F>(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: F, ks: KillSwitch) where F: Fn() |
︙ | ︙ |
Changes to src/signals/win.rs.
︙ | ︙ | |||
23 24 25 26 27 28 29 | /// the task is terminated. pub async fn wait_shutdown<F>(f: F, ks: KillSwitch) where F: FnOnce() { tracing::trace!("CTRL+C task launched"); | < > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | | > > > > > > > > > > > | 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | /// the task is terminated. pub async fn wait_shutdown<F>(f: F, ks: KillSwitch) where F: FnOnce() { tracing::trace!("CTRL+C task launched"); tokio::select! { _ = signal::ctrl_c() => { tracing::debug!("Received Ctrl+C"); // Once any process termination signal has been received post call the // callback. f(); }, _ = ks.wait() => { tracing::debug!("killswitch triggered"); } } tracing::trace!("wait_shutdown() terminating"); } pub async fn wait_term<F>(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<SvcEvt> ) -> Result<(), Error> { setup_sync_fg_kill_handler(move |ty| { match ty { CTRL_C_EVENT => { tracing::trace!( "Received some kind of event that should trigger a 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 } } _ => FALSE |
︙ | ︙ |
Changes to src/winsvc.rs.
︙ | ︙ | |||
110 111 112 113 114 115 116 | { debugger::wait_for_then_break(); debugger::output("Hello, debugger"); } // Create a one-shot channel used to receive a an initial handshake from the // service handler thread. | | | 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 | { debugger::wait_for_then_break(); debugger::output("Hello, debugger"); } // Create a one-shot channel used to receive a an initial handshake from the // service handler thread. 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(), tx_fromsvc }; |
︙ | ︙ | |||
162 163 164 165 166 167 168 | let Ok(HandshakeMsg { tx, rx }) = res else { panic!("Unable to receive handshake"); }; let reporter = Arc::new(ServiceReporter { tx: tx.clone() }); | | | | < | < | 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 | let Ok(HandshakeMsg { tx, rx }) = res else { panic!("Unable to receive handshake"); }; let reporter = Arc::new(ServiceReporter { tx: tx.clone() }); match st { SvcType::Sync(handler) => { crate::rttype::sync_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)) } } } // Generate the windows service boilerplate. The boilerplate contains the // low-level service entry function (ffi_service_main) that parses incoming // service arguments into Vec<OsString> and passes them to user defined service // entry (my_service_main). |
︙ | ︙ | |||
215 216 217 218 219 220 221 222 223 | match svcinit(&svcname) { Ok(InitRes { handshake_reply, rx_tosvc, status_handle }) => { // Return Ok() to main server app thread so it will kick off the main // server application. | > > | > > > > > > > | > > | | | | 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 | match svcinit(&svcname) { 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. 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) => { // 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"); } } } } fn svcinit(svcname: &str) -> Result<InitRes, Error> { // Set up logging *before* telling sending SvcRunning to caller LumberJack::from_winsvc(svcname)?.init()?; // If the service has a WorkDir configured under it's Parameters subkey, then // retreive it and attempt to change directory to it. // 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(wd) = svcparams.get_value::<String, &str>("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, rx_tosvc) = unbounded_channel(); // Create channel that will be used to send messages to the application. 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 { match control_event { ServiceControl::Interrogate => { |
︙ | ︙ | |||
346 347 348 349 350 351 352 | ToSvcMsg::Starting(checkpoint) => { log::debug!("app reported that it is running"); 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), | | | 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 | ToSvcMsg::Starting(checkpoint) => { log::debug!("app reported that it is running"); 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, wait_hint: SERVICE_STARTPENDING_TIME, process_id: None }) { log::error!( "Unable to set service status to 'start pending {}'; {}", checkpoint, e |
︙ | ︙ | |||
377 378 379 380 381 382 383 | ToSvcMsg::Stopping(checkpoint) => { log::debug!("app is shutting down"); 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), | | | 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 | ToSvcMsg::Stopping(checkpoint) => { log::debug!("app is shutting down"); 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, wait_hint: SERVICE_STOPPENDING_TIME, process_id: None }) { log::error!( "Unable to set service status to 'stop pending {}'; {}", checkpoint, e |
︙ | ︙ |
Changes to www/changelog.md.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | # Change log ## [Unreleased] ### Added ### Changed ### Removed --- ## [0.0.1] - 2023-10-15 | > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | # Change log ## [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 --- ## [0.0.1] - 2023-10-15 |
︙ | ︙ |
Changes to www/design-notes.md.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | # Design Notes _qsu_'s primary function is to provide a service runtime laywer that sits between the operating system's service subsystem and the application server code. It provides abstractions that allow a server to behave in a unified way, regardless of the actual service subsystem type (it even strives to support the 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. ## Service subsystem integration Traditional Unix server processes "daemonize", which involves steps to make it into an isolated background process. | > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | # Design Notes _qsu_'s primary function is to provide a service runtime laywer that sits between the operating system's service subsystem and the application server code. It provides abstractions that allow a server to behave in a unified way, regardless of the actual service subsystem type (it even strives to support the 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 it into an isolated background process. |
︙ | ︙ |
Changes to www/index.md.
︙ | ︙ | |||
40 41 42 43 44 45 46 47 48 49 50 51 52 53 | what the current version does. ### All platforms - Uses both the `log` crate and `tracing` crate. - `log` is intended for production logging. - `tracing` is intended for developer and debugging logging. ### 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` crate. - If `TRACE_FILE` is set tracing will be directed to a file instead of | > > | 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | what the current version does. ### 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` crate. - If `TRACE_FILE` is set tracing will be directed to a file instead of |
︙ | ︙ | |||
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | vairable. - If key `TraceFile` and `TraceLevel` correspond to the environment 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. ## 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 | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | | | | 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 | vairable. - If key `TraceFile` and `TraceLevel` correspond to the environment 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 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. |