use clap::{Arg, ArgAction, ArgMatches, Args, Command, builder::Str};
use crate::{
err::CbErr,
installer::{self, RegSvc},
lumberjack::LogLevel,
rt::{RunCtx, SrvAppRt}
};
#[derive(Debug, Args)]
struct RegSvcArgs {
#[arg(short = 's', long)]
auto_start: bool,
#[arg(short = 'D', long, value_name = "DISPNAME")]
display_name: Option<String>,
#[arg(short, long, value_name = "DESC")]
description: Option<String>,
#[arg(short, long)]
arg: Vec<String>,
#[arg(short, long, num_args(2), value_names=["KEY", "VALUE"])]
env: Vec<String>,
#[arg(short, long, value_name = "DIR")]
workdir: Option<String>,
#[arg(long, value_enum, value_name = "LEVEL")]
log_level: Option<LogLevel>,
#[arg(long, value_enum, value_name = "FILTER")]
log_filter: Option<String>,
#[arg(long, hide(true), value_name = "FILTER")]
trace_filter: Option<String>,
#[arg(long, value_enum, hide(true), value_name = "FNAME")]
trace_file: Option<String>,
#[arg(long, short)]
force: bool
}
#[must_use]
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 cli = Command::new(cmd.to_string()).arg(namearg);
RegSvcArgs::augment_args(cli)
}
#[derive(Debug, Args)]
struct DeregSvcArgs {}
#[must_use]
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 {
#[allow(
clippy::missing_panics_doc,
reason = "svcname should always have been set"
)]
#[must_use]
pub fn from_cmd_match(matches: &ArgMatches) -> Self {
let svcname = matches.get_one::<String>("svcname").unwrap().to_owned();
Self { svcname }
}
}
pub(crate) enum ArgpRes<'cb, ApEr> {
RunApp(RunCtx, &'cb mut dyn ArgsProc<AppErr = ApEr>),
Quit
}
pub struct RunSvc {
pub as_service: bool,
pub svcname: String
}
impl RunSvc {
#[allow(
clippy::missing_panics_doc,
reason = "svcname should always have been set"
)]
#[must_use]
pub fn from_cmd_match(matches: &ArgMatches) -> Self {
let as_service = matches.get_one::<bool>("as_service").unwrap().to_owned();
let svcname = matches
.get_one::<String>("service_name")
.unwrap()
.to_owned();
Self {
as_service,
svcname
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum Cmd {
Root,
Inst,
Rm,
Run
}
pub trait ArgsProc {
type AppErr;
#[allow(unused_variables)]
fn conf_cmd(
&mut self,
cmdtype: Cmd,
cmd: Command
) -> Result<Command, Self::AppErr> {
Ok(cmd)
}
#[allow(unused_variables)]
fn proc_inst(
&mut self,
sub_m: &ArgMatches,
regsvc: RegSvc
) -> Result<RegSvc, Self::AppErr> {
Ok(regsvc)
}
#[allow(unused_variables)]
fn proc_rm(
&mut self,
sub_m: &ArgMatches,
deregsvc: DeregSvc
) -> Result<DeregSvc, Self::AppErr> {
Ok(deregsvc)
}
#[allow(unused_variables)]
fn proc_run(
&mut self,
matches: &ArgMatches,
runctx: RunCtx
) -> Result<RunCtx, Self::AppErr> {
Ok(runctx)
}
#[allow(unused_variables)]
fn proc_other(
&mut self,
subcmd: &str,
sub_m: &ArgMatches
) -> Result<(), Self::AppErr> {
Ok(())
}
fn build_apprt(
&mut self,
runctx: &mut RunCtx
) -> Result<SrvAppRt<Self::AppErr>, Self::AppErr>;
}
#[macro_export]
macro_rules! init_command {
($cmd:expr) => {
$cmd
.name(env!("CARGO_PKG_NAME"))
.about(env!("CARGO_PKG_DESCRIPTION"))
.version(env!("CARGO_PKG_VERSION"))
};
}
pub struct ArgParser<'cb, ApEr> {
svcname: String,
reg_subcmd: String,
dereg_subcmd: String,
cli: Command,
cb: &'cb mut dyn ArgsProc<AppErr = ApEr>,
runcb: Option<Box<dyn FnOnce(RunCtx) -> RunCtx>>,
regcb: Option<Box<dyn FnOnce(RegSvc) -> RegSvc>>
}
impl<'cb, ApEr> ArgParser<'cb, ApEr>
where
ApEr: Send + 'static
{
fn setup_run_options(mut cli: Command, svcname: &str) -> Command {
let as_service = Arg::new("as_service")
.short('S')
.long("as-service")
.action(ArgAction::SetTrue)
.help("Run as a system service");
cli = cli.arg(as_service);
let svcnamearg = Arg::new("service_name")
.short('N')
.long("service-name")
.action(ArgAction::Set)
.value_name("SVCNAME")
.default_value(Str::from(svcname.to_string()))
.help("Set service name");
cli = cli.arg(svcnamearg);
cli
}
pub fn new(svcname: &str, cb: &'cb mut dyn ArgsProc<AppErr = ApEr>) -> Self {
let mut cli = Command::new("");
cli = Self::setup_run_options(cli, svcname);
Self {
svcname: svcname.to_string(),
reg_subcmd: "register-service".into(),
dereg_subcmd: "deregister-service".into(),
cli,
cb,
runcb: None,
regcb: None
}
}
pub fn with_cmd(
svcname: &str,
mut cli: Command,
cb: &'cb mut dyn ArgsProc<AppErr = ApEr>
) -> Self {
cli = Self::setup_run_options(cli, svcname);
Self {
svcname: svcname.to_string(),
reg_subcmd: "register-service".into(),
dereg_subcmd: "deregister-service".into(),
cli,
cb,
runcb: None,
regcb: None
}
}
#[must_use]
pub fn setup_cmd<F>(mut self, f: F) -> Self
where
F: FnOnce(Command) -> Command
{
self.cli = f(self.cli);
self
}
#[must_use]
pub fn reg_subcmd(mut self, nm: &str) -> Self {
self.reg_subcmd = nm.to_string();
self
}
#[must_use]
pub fn dereg_subcmd(mut self, nm: &str) -> Self {
self.dereg_subcmd = nm.to_string();
self
}
fn inner_proc(self) -> Result<ArgpRes<'cb, ApEr>, CbErr<ApEr>> {
let matches = match self.cli.try_get_matches() {
Ok(m) => m,
Err(e) => match e.kind() {
clap::error::ErrorKind::DisplayHelp
| clap::error::ErrorKind::DisplayVersion => {
e.exit();
}
_ => {
e.exit();
}
}
};
match matches.subcommand() {
Some((subcmd, sub_m)) if subcmd == self.reg_subcmd => {
let mut regsvc = RegSvc::from_cmd_match(sub_m);
let mut args = vec![String::from("--as-service")];
if regsvc.svcname() != self.svcname {
args.push(String::from("--service-name"));
args.push(regsvc.svcname().to_string());
}
regsvc.args_ref(args);
let regsvc = self
.cb
.proc_inst(sub_m, regsvc)
.map_err(|ae| CbErr::App(ae))?;
let regsvc = if let Some(cb) = self.regcb {
cb(regsvc)
} else {
regsvc
};
regsvc.register()?;
Ok(ArgpRes::Quit)
}
Some((subcmd, sub_m)) if subcmd == self.dereg_subcmd => {
let args = DeregSvc::from_cmd_match(sub_m);
let args =
self.cb.proc_rm(sub_m, args).map_err(|ae| CbErr::App(ae))?;
installer::uninstall(&args.svcname)?;
Ok(ArgpRes::Quit)
}
Some((subcmd, sub_m)) => {
self
.cb
.proc_other(subcmd, sub_m)
.map_err(|ae| CbErr::App(ae))?;
Ok(ArgpRes::Quit)
}
_ => {
let args = RunSvc::from_cmd_match(&matches);
let mut rctx = RunCtx::new(&self.svcname);
if args.as_service {
rctx = rctx.service();
}
rctx = self
.cb
.proc_run(&matches, rctx)
.map_err(|ae| CbErr::App(ae))?;
Ok(ArgpRes::RunApp(rctx, self.cb))
}
}
}
#[must_use]
pub fn runsvc_proc(
mut self,
f: impl FnOnce(RunCtx) -> RunCtx + 'static
) -> Self {
self.runcb = Some(Box::new(f));
self
}
#[must_use]
pub fn regsvc_proc(
mut self,
f: impl FnOnce(RegSvc) -> RegSvc + 'static
) -> Self {
self.regcb = Some(Box::new(f));
self
}
pub fn proc(mut self) -> Result<(), CbErr<ApEr>>
where
ApEr: std::fmt::Debug
{
self.cli = self
.cb
.conf_cmd(Cmd::Root, self.cli)
.map_err(|ae| CbErr::App(ae))?;
let runcb = self.runcb.take();
let sub = mk_inst_cmd(&self.reg_subcmd, &self.svcname);
let sub = self
.cb
.conf_cmd(Cmd::Inst, sub)
.map_err(|ae| CbErr::App(ae))?;
self.cli = self.cli.subcommand(sub);
let sub = mk_rm_cmd(&self.dereg_subcmd, &self.svcname);
let sub = self
.cb
.conf_cmd(Cmd::Rm, sub)
.map_err(|ae| CbErr::App(ae))?;
self.cli = self.cli.subcommand(sub);
if let ArgpRes::RunApp(mut runctx, cb) = self.inner_proc()? {
let st = cb.build_apprt(&mut runctx).map_err(|ae| CbErr::App(ae))?;
if let Some(runcb) = runcb {
runctx = runcb(runctx);
}
runctx.run(st)?;
}
Ok(())
}
}