Files
hello-agent/src/cli.rs
T
mike e45abbe64d
build-windows / build-hello-agent-x64 (push) Successful in 5m5s
build-windows / sign-hello-agent-x64 (push) Successful in 5s
build-windows / validate-hello-agent-x64 (push) Successful in 6s
Implement auto-update routine
2026-05-21 22:52:39 +02:00

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