237 lines
7.9 KiB
Rust
237 lines
7.9 KiB
Rust
// Hand-rolled CLI parser. Matches the upstream rustdesk style (see
|
|
// `src/core_main.rs` in the rustdesk crate) — clap is not pulled into the
|
|
// main path. Only a handful of flags are supported on purpose: the surface
|
|
// area is the user-facing contract.
|
|
|
|
use anyhow::{bail, Result};
|
|
|
|
#[derive(Debug, PartialEq, Eq)]
|
|
pub enum Action {
|
|
/// `--install`. Optionally combined with `--config <BLOB>` for MDM
|
|
/// one-liner deployment.
|
|
Install,
|
|
/// `--uninstall`. Stops the service, deletes it, removes config dir.
|
|
Uninstall,
|
|
/// `--service`. SCM entry point; user code should never invoke this
|
|
/// manually except via the service dispatcher.
|
|
Service,
|
|
/// `--server`. Worker mode launched into the active console session by
|
|
/// the service shell.
|
|
Server,
|
|
/// `--config <BLOB>` without `--install`. Persist config and exit.
|
|
ConfigOnly,
|
|
/// No flags. Foreground dev mode.
|
|
None,
|
|
/// `--cm`. Connection-manager process. Spawned as a USER-token child of
|
|
/// the SYSTEM-token `--server` worker (either pre-emptively by hello-agent's
|
|
/// own `spawn_cm_into_user_desktop`, or as a fallback by librustdesk's
|
|
/// `run_as_user` when its first `ipc::connect("_cm")` fails). Binds the
|
|
/// `_cm` named pipe, runs upstream's `IpcTaskRunner` for each incoming
|
|
/// `--server` connection, and lives for as long as the user session does
|
|
/// — every `Data::FS(...)` frame the server sends is executed here, in
|
|
/// the user's security context.
|
|
Cm,
|
|
/// `--update`. Self-replacement entry point launched as an elevated child
|
|
/// by the running service's updater (see `librustdesk::updater`) after it
|
|
/// has downloaded and SHA256-verified a new hello-agent.exe from the
|
|
/// Gitea releases page. `current_exe()` here points at the staged new
|
|
/// binary in `%TEMP%`; it copies itself over the installed location and
|
|
/// restarts the service via `librustdesk::platform::update_me`.
|
|
Update,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct ParsedArgs {
|
|
pub action: Action,
|
|
pub config_blob: Option<String>,
|
|
}
|
|
|
|
impl ParsedArgs {
|
|
pub fn from_argv<I: IntoIterator<Item = String>>(argv: I) -> Result<Self> {
|
|
let args: Vec<String> = argv.into_iter().collect();
|
|
let mut install = false;
|
|
let mut uninstall = false;
|
|
let mut service = false;
|
|
let mut server = false;
|
|
let mut cm = false;
|
|
let mut update = false;
|
|
let mut config_blob: Option<String> = None;
|
|
|
|
let mut i = 0;
|
|
while i < args.len() {
|
|
match args[i].as_str() {
|
|
"--install" => install = true,
|
|
"--uninstall" => uninstall = true,
|
|
"--service" => service = true,
|
|
"--server" => server = true,
|
|
"--update" => update = true,
|
|
// Connection-manager popup mode. Treat `--cm-no-ui` (the
|
|
// Linux-headless variant librustdesk also tries) as a
|
|
// synonym; either way we run cm_popup.
|
|
"--cm" | "--cm-no-ui" => cm = true,
|
|
"--config" => {
|
|
let next = args.get(i + 1).cloned().ok_or_else(|| {
|
|
anyhow::anyhow!("--config requires a value")
|
|
})?;
|
|
config_blob = Some(next);
|
|
i += 1;
|
|
}
|
|
"--help" | "-h" => {
|
|
print_usage();
|
|
std::process::exit(0);
|
|
}
|
|
"--version" | "-V" => {
|
|
println!("hello-agent {}", env!("CARGO_PKG_VERSION"));
|
|
std::process::exit(0);
|
|
}
|
|
other => bail!("unknown argument: {}", other),
|
|
}
|
|
i += 1;
|
|
}
|
|
|
|
// Mutual-exclusion rules. --install + --config is the MDM one-liner;
|
|
// everything else is one-action-at-a-time. --update is launched by
|
|
// the updater as a standalone elevated child, never combined.
|
|
let exclusive = [uninstall, service, server, cm, update]
|
|
.iter()
|
|
.filter(|x| **x)
|
|
.count();
|
|
if exclusive > 1 {
|
|
bail!("--uninstall, --service, --server, --cm, --update are mutually exclusive");
|
|
}
|
|
if uninstall && (install || config_blob.is_some()) {
|
|
bail!("--uninstall cannot be combined with other flags");
|
|
}
|
|
if update && (install || config_blob.is_some()) {
|
|
bail!("--update cannot be combined with other flags");
|
|
}
|
|
|
|
let action = if uninstall {
|
|
Action::Uninstall
|
|
} else if install {
|
|
Action::Install
|
|
} else if service {
|
|
Action::Service
|
|
} else if server {
|
|
Action::Server
|
|
} else if cm {
|
|
Action::Cm
|
|
} else if update {
|
|
Action::Update
|
|
} else if config_blob.is_some() {
|
|
Action::ConfigOnly
|
|
} else {
|
|
Action::None
|
|
};
|
|
|
|
Ok(ParsedArgs {
|
|
action,
|
|
config_blob,
|
|
})
|
|
}
|
|
}
|
|
|
|
pub fn print_usage() {
|
|
eprintln!(
|
|
"hello-agent — headless RustDesk-protocol-compatible support agent
|
|
|
|
USAGE:
|
|
hello-agent [OPTIONS]
|
|
|
|
OPTIONS:
|
|
--install Register and start the Windows service.
|
|
--uninstall Stop, delete, and clean up the Windows service.
|
|
--config <BLOB> Import an admin-UI deploy blob. Accepts either the
|
|
reversed-base64 string emitted by the rustdesk-server
|
|
admin UI or the `host=...,key=...,api=...,relay=...`
|
|
filename form. May be combined with --install for
|
|
one-line MDM deployment.
|
|
--service SCM entry point. Do not invoke manually.
|
|
--server Worker mode (launched by the service shell into
|
|
the active console session).
|
|
--update Self-replacement entry point. Launched by the
|
|
running service's updater after downloading and
|
|
SHA256-verifying a new release from Gitea. Do
|
|
not invoke manually.
|
|
-h, --help Show this help.
|
|
-V, --version Show version.
|
|
|
|
EXAMPLES:
|
|
hello-agent.exe --install --config 0nI900VsFHZ...
|
|
hello-agent.exe --uninstall
|
|
"
|
|
);
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn parse(s: &[&str]) -> Result<ParsedArgs> {
|
|
ParsedArgs::from_argv(s.iter().map(|s| s.to_string()))
|
|
}
|
|
|
|
#[test]
|
|
fn no_args_is_none() {
|
|
assert_eq!(parse(&[]).unwrap().action, Action::None);
|
|
}
|
|
|
|
#[test]
|
|
fn install_with_config() {
|
|
let p = parse(&["--install", "--config", "BLOB"]).unwrap();
|
|
assert_eq!(p.action, Action::Install);
|
|
assert_eq!(p.config_blob.as_deref(), Some("BLOB"));
|
|
}
|
|
|
|
#[test]
|
|
fn config_only() {
|
|
let p = parse(&["--config", "BLOB"]).unwrap();
|
|
assert_eq!(p.action, Action::ConfigOnly);
|
|
}
|
|
|
|
#[test]
|
|
fn uninstall_alone() {
|
|
assert_eq!(parse(&["--uninstall"]).unwrap().action, Action::Uninstall);
|
|
}
|
|
|
|
#[test]
|
|
fn install_uninstall_conflict() {
|
|
assert!(parse(&["--install", "--uninstall"]).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn service_server_conflict() {
|
|
assert!(parse(&["--service", "--server"]).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn config_missing_value() {
|
|
assert!(parse(&["--config"]).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn unknown_arg() {
|
|
assert!(parse(&["--no-such-flag"]).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn update_alone() {
|
|
assert_eq!(parse(&["--update"]).unwrap().action, Action::Update);
|
|
}
|
|
|
|
#[test]
|
|
fn update_install_conflict() {
|
|
assert!(parse(&["--update", "--install"]).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn update_service_conflict() {
|
|
assert!(parse(&["--update", "--service"]).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn update_config_conflict() {
|
|
assert!(parse(&["--update", "--config", "BLOB"]).is_err());
|
|
}
|
|
}
|