qsu

Check-in Differences
Login

Check-in Differences

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
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
[package]
name = "qsu"
version = "0.0.1"
edition = "2021"
license = "0BSD"
categories = [ "concurrency", "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",

  "Rocket.toml",
  "rustfmt.toml"
]

[features]

full = ["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" }



env_logger = { version = "0.10.0" }
futures = { version = "0.3.28" }

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"


|


|









>





>
|










>
>
>


>







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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
] }
winreg = { version = "0.51.0" }

[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"]

[[example]]
name = "hellosvc"
required-features = ["installer"]

[[example]]
name = "hellosvc-tokio"
required-features = ["installer"]

[[example]]
name = "hellosvc-rocket"
required-features = ["installer", "rocket"]








<


|
<



|



|



|

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
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
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<Commands>
}

#[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<InstCmd> 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<bool, ()> {
  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)
}

// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 :
|

<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|
<
<
<
<
|
<
<
<
|
<
<
<
<
<
|
|
<
<
|
<
<
<
|
|
|
<
|
|
<
<
<
<
|
|
<
<
<
<
<
|
|
|
|
|

|
|
|
|
<
<

|
|

<
<
|
<
|
<
<
<
<
<
<
<
<
<
<
<
<
<



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
5
6
7
8
9
10
11
12
13
14
15
16

17
18
19
20
21
22
23
24
#[macro_use]
extern crate rocket;

mod argp;

use std::time::{Duration, Instant};

use clap::Parser;

use qsu::{
  RocketServiceHandler, RunCtx, StartState, StopState, SvcEvt, SvcEvtReader,
  SvcType
};

use rocket::{Build, Ignite, Rocket};


const SVCNAME: &str = "hellosvc-rocket";


struct MyService {}

#[qsu::async_trait]
impl RocketServiceHandler for MyService {
  async fn init(




|
<
|
<


|
|




>
|







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
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
  ) -> Result<(), qsu::Error> {
    for rocket in rockets {
      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");






              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() {
  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));

  }

}

#[get("/")]
fn index() -> &'static str {
  log::error!("error");
  log::warn!("warn");
  log::info!("info");







<
<





|
>
>
>
>
>
>




















>
|
<
>
>
>
|
<

>
|
>
|
<
>

>
|
>
>
>
|
>
|
>







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
5
6
7
8
9
10
11
12
13
14
15
16


17
18
19
20
21
22
23
24
//! Simple service that does nothing other than log/trace every N seconds.

mod argp;



use std::{
  thread,
  time::{Duration, Instant}
};

use clap::{ArgAction, Args, Parser, Subcommand};

use qsu::{
  installer, RunCtx, StartState, StopState, SvcEvt, SvcEvtReader, SvcType,
  TokioServiceHandler
};



const SVCNAME: &str = "hellosvc-tokio";

struct MyService {}

#[qsu::async_trait]
impl TokioServiceHandler for MyService {
  async fn init(&mut self, _ss: StartState) -> Result<(), qsu::Error> {
    tracing::trace!("Running init()");



>
>

<
<
|
<

<
<

|



>
>
|







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
54






55
56
57
58
59
60
61
      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 application terminate");






              break;
            }
            Some(SvcEvt::ReloadConf) => {
              tracing::info!("The service subsystem requested that application reload configuration");
            }
            _ => { }
          }







|
>
>
>
>
>
>







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
76
77


78
79
80

81

82
83

84

85



86

87

88
89
90
  async fn shutdown(&mut self, _ss: StopState) -> Result<(), qsu::Error> {
    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::Tokio(handler));

  }

}

// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 :







|
|
>
>
|
<

>
|
>
|
<
>

>
|
>
>
>
|
>
|
>



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
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;



use std::{
  thread,
  time::{Duration, Instant}
};

use clap::{ArgAction, Args, Parser, Subcommand};

use qsu::{
  RunCtx, ServiceHandler, StartState, StopState, SvcEvt, SvcEvtReader, SvcType

};



const SVCNAME: &str = "hellosvc";

struct MyService {}

impl ServiceHandler for MyService {
  fn init(&mut self, _ss: StartState) -> Result<(), qsu::Error> {
    tracing::trace!("Running init()");
    Ok(())



>
>






<
<

|
>


>
>
|







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
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

        last_dump = Instant::now();
      }

      match set.try_recv() {
        Some(SvcEvt::Shutdown) => {
          tracing::info!(
            "The service subsystem requested that application terminate"






          );
          break;
        }
        Some(SvcEvt::ReloadConf) => {
          tracing::info!(
            "The service subsystem requested that application reload \
             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() {
  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));

  }

}

// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 :







|
>
>
>
>
>
>





|



















|
|
>
>
|
<

>
|
>
|
<
>

>
|
>
>
>
|
>
|
>



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
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
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),
  SubSystem(String)

}

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 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::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)
      }



    }
  }
}











#[cfg(windows)]
impl From<eventlog::InitError> for Error {

  fn from(err: eventlog::InitError) -> Self {
    Error::SubSystem(err.to_string())
  }
}

#[cfg(windows)]
impl From<eventlog::Error> for Error {

  fn from(err: eventlog::Error) -> Self {
    Error::SubSystem(err.to_string())
  }
}

impl From<io::Error> for Error {
  fn from(err: io::Error) -> Self {
    Error::IO(err.to_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









9






















10
11


12



13



































































































14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//! Helpers for installing/uninstalling services.

#[cfg(windows)]
pub mod winsvc;

#[cfg(target_os = "macos")]
pub mod launchd;










#[cfg(feature = "systemd")]






















pub mod systemd;







use crate::err::Error;




































































































pub struct RegSvc {
  pub svcname: String,

  #[cfg(windows)]
  pub display_name: Option<String>,

  #[cfg(any(windows, feature = "systemd"))]
  pub description: Option<String>,

  #[cfg(windows)]
  pub regconf:
    Option<Box<dyn FnOnce(&str, &mut winreg::RegKey) -> Result<(), Error>>>,

  /// Command line arguments.








>
>
>
>
>
>
>
>
>
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|

>
>

>
>
>
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







|







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
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
  /// 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>








}

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, feature = "systemd"))]
      description: None,

      #[cfg(windows)]
      regconf: None,

      args: Vec::new(),

      envs: Vec::new(),

      autostart: false,

      workdir: None,

      deps: Vec::new()































































    }
  }

  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, feature = "systemd"))]
  pub fn description(mut self, text: impl ToString) -> Self {
    self.description = Some(text.to_string());
    self
  }

  #[cfg(any(windows, 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







|
>
>
>
>
>
>
>
>















|













|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>













|





|







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
249
250
251

252

253




254









255
256
257
    self
  }

  pub fn register(self) -> Result<(), Error> {
    #[cfg(windows)]
    winsvc::install(self)?;







    Ok(())
  }
}


// ToDo: Return Error::NotImplemented for unsupported platforms
pub fn uninstall(svcname: &str) -> Result<(), Error> {
  #[cfg(windows)]

  winsvc::uninstall(svcname)?;






  Ok(())









}

// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 :







>
>
>
>
>
>





|


>
|
>
|
>
>
>
>
|
>
>
>
>
>
>
>
>
>



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
use std::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>local.{}</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 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));
  }












  if ctx.have_envs() {






    bldr.line(r#"<key>EnvironmentVariables</key>"#);
    bldr.scope("<dict>", Some("</dict"));

    for (key, value) in &ctx.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().unwrap();


  // 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, "<!DOCTYPE html>\n<html>\n</html>\n");
















  Ok(())
}

// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 :
|














|






>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>

















>
>
>
>
>
>
>
>
>
>

>
>
>
>
>
>



|






>















|
>






|
|
>
>
>

>
>
>
>
|
>
>
>
>
>
>
>




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
47
48



49













50
51
52
  // 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");




  unimplemented!()













}

// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 :







|

>
>
>
|
>
>
>
>
>
>
>
>
>
>
>
>
>



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
13
14
15
16
17
18
19


20
21

22





23
24
25
26
27
28
29
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, read_service_subkey, write_service_subkey}
};


pub fn install(ctx: super::RegSvc) -> Result<(), Error> {
  let svcname = &ctx.svcname;



  let status = RefCell::new(false);


  eventlog::register(&svcname)?;





  let _status = scopeguard::guard(&status, |st| {
    if !*st.borrow() {
      if eventlog::deregister(svcname).is_err() {
        eprintln!("!!> Unable to deregister event source");
      }
    }
  });












|






>
>


>

>
>
>
>
>







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
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
    }
  });

  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<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)?;
  }












  // 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







<

















>
>
>
>
>
>
>
>
>
>
>







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
26
27
28
29
30
31
32
33
34
35
//! _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;

use std::{path::Path, sync::Arc};

use tokio::sync::broadcast;

pub use async_trait::async_trait;

pub use lumberjack::LumberJack;

pub 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
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
92





















93

94
95
96
97
98
99
100
101
102
103
104
105
106

  async fn run(&mut self, ser: SvcEvtReader) -> Result<(), Error>;

  async fn shutdown(&mut self, ss: StopState) -> Result<(), Error>;
}


/// Rocket server application.





















#[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/
  async fn init(
    &mut self,
    ss: StartState
  ) -> Result<Vec<rocket::Rocket<rocket::Build>>, Error>;

  async fn run(
    &mut self,







|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>

>




|
|







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
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
  ///
  /// 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


}



pub struct SvcEvtReader {
  rx: broadcast::Receiver<SvcEvt>
}

impl SvcEvtReader {




  pub fn recv(&mut self) -> Option<SvcEvt> {
    self.rx.blocking_recv().ok()
  }





  pub fn try_recv(&mut self) -> Option<SvcEvt> {
    self.rx.try_recv().ok()
  }





  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(Box<dyn TokioServiceHandler + Send>),


  #[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(handler) => rttype::tokio_main(handler, reporter, None),

      SvcType::Rocket(handler) => rttype::rocket_main(handler, reporter, None)
    };

    res
  }

  /// Run as a Windows service.







|
>
>



>





>
>
>
>




>
>
>
>




>
>
>
>











>
>
|
>


>














|
|





>
|
>







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

215

216
217
218
219
220
221
222
  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(handler) => rttype::tokio_main(handler, reporter, None),


      #[cfg(feature = "rocket")]
      SvcType::Rocket(handler) => rttype::rocket_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
279
280
281

282
283
284
285
286
287
288
289
290
291
    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,

    handler: Box<dyn TokioServiceHandler + Send>
  ) -> Result<(), Error> {
    self.run(SvcType::Tokio(handler))
  }

  /// Convenience method around [`Self::run()`] using [`SvcType::Rocket`].
  #[cfg(feature = "rocket")]
  pub fn run_rocket(
    self,
    handler: Box<dyn RocketServiceHandler + Send>







|


>


|







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





310











311
312
313
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()?;






  name.to_str().map(String::from)











}

// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 :







>
>
>
>
>
|
>
>
>
>
>
>
>
>
>
>
>



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

148





149
150
151
152
153
154
155
    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();


  jh_graceful_landing.await;






  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 {







>
|
>
>
>
>
>







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
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 cat 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.







|


















|







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
117
118
119
120
121






122
123
124
125
126
127
128

      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 | Signal::SIGTERM => {
              if let Err(e) = tx_svcevt.send(SvcEvt::Shutdown) {
                log::error!("Unable to send SvcEvt::Shutdown event; {}", e);
              }
              break;






            }
            Signal::SIGHUP => {
              if let Err(e) = tx_svcevt.send(SvcEvt::ReloadConf) {
                log::error!("Unable to send SvcEvt::ReloadConf event; {}", e);
              }
            }
            _ => {}







|




>
>
>
>
>
>







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
3
4
5
6
7
8
9
10
11
12

13
14
15
16



17

18

19
20
21
22
23
24
25
use std::sync::Arc;

use tokio::{sync::broadcast, task};

use crate::{
  err::Error, signals, StartState, StateReporter, StopState, SvcEvt,
  SvcEvtReader, TokioServiceHandler
};

use killswitch::KillSwitch;

pub(crate) fn tokio_main(

  handler: Box<dyn TokioServiceHandler>,
  sr: Arc<dyn StateReporter + Send + Sync>,
  rx_svcevt: Option<broadcast::Receiver<SvcEvt>>
) -> Result<(), Error> {



  let rt = tokio::runtime::Runtime::new().unwrap();

  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`


|









>




>
>
>
|
>

>







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
49













50
51
52
53
54
55
56
    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 EvcEvt::ReloadConf event; {}", e);













        }
      },
      ks2
    ));

    // There doesn't seem to be anything equivalent to SIGHUP for Windows
    // (Services)







>




|
>
>
>
>
>
>
>
>
>
>
>
>
>







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
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};

#[cfg(windows)]
pub use win::wait_shutdown;

#[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 :









|


|





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
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
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/SIGTERM task launched");

  let (Ok(mut sigint), Ok(mut sigterm)) = (
    signal(SignalKind::interrupt()),
    signal(SignalKind::terminate())
  ) else {
    log::error!("Unable to create SIGINT/SIGTERM Futures");
    return;
  };


  // Wait for either SIGINT or SIGTERM.
  tokio::select! {
    _ = sigint.recv() => {
      tracing::debug!("Received SIGINT");
      f();
    },
    _ = sigterm.recv() => {
      tracing::debug!("Received SIGTERM");
      f();
    }
    _ = ks.wait() => {
      tracing::debug!("killswitch triggered");
    }
  }

  tracing::trace!("wait_shutdown() 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()












|

<
|
<
<
|



<
|


|


<
<
<
<








>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







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
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
/// the task is terminated.
pub async fn wait_shutdown<F>(f: F, ks: KillSwitch)
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.
      f();
    },
    _ = ks.wait() => {
      tracing::debug!("killswitch triggered");
    }
  }

  tracing::trace!("wait_shutdown() 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 | CTRL_BREAK_EVENT | CTRL_CLOSE_EVENT => {
        tracing::trace!(
          "Received some kind of event that should trigger a shutdown."
        );
        if let Ok(_) = tx.send(SvcEvt::Shutdown) {











          // We handled this event
          TRUE
        } else {
          FALSE
        }
      }
      _ => FALSE







<















>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>






|



|
>
>
>
>
>
>
>
>
>
>
>







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
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, mut 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
  };







|







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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189

  let Ok(HandshakeMsg { tx, rx }) = res else {
    panic!("Unable to receive handshake");
  };

  let reporter = Arc::new(ServiceReporter { tx: tx.clone() });

  let res = match st {
    SvcType::Sync(handler) => {
      crate::rttype::sync_main(handler, reporter, Some(rx))
    }
    SvcType::Tokio(handler) => {
      crate::rttype::tokio_main(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
// service arguments into Vec<OsString> and passes them to user defined service
// entry (my_service_main).







|



|
|





<
|
<







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
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

  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.
      tx_fromsvc.send(Ok(handshake_reply));




      // 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));


    }
  }
}


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, mut 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);

  //
  // Define system service event handler that will be receiving service events.
  //
  let event_handler = move |control_event| -> ServiceControlHandlerResult {
    match control_event {
      ServiceControl::Interrogate => {







>
>


|
>
>
>







>
>
>
>
|
>
>

















|








|


|







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
353
354
355
356
357
358
359
360
          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: checkpoint,
              wait_hint: SERVICE_STARTPENDING_TIME,
              process_id: None
            }) {
              log::error!(
                "Unable to set service status to 'start pending {}'; {}",
                checkpoint,
                e







|







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
384
385
386
387
388
389
390
391
          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: checkpoint,
              wait_hint: SERVICE_STOPPENDING_TIME,
              process_id: None
            }) {
              log::error!(
                "Unable to set service status to 'stop pending {}'; {}",
                checkpoint,
                e







|







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
90
91
92
93
    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

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.








>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







>



|
|
|

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.