// 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 ` 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 ` 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, } impl ParsedArgs { pub fn from_argv>(argv: I) -> Result { let args: Vec = 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 = 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 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::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()); } }