Files
claude-code-usage-bubble/src/cli.rs
T
tiennm99 607a3103d3 feat: parse the CLI flags, wait for the previous instance and clean up after updates
The argv parser accepts --diagnose, --wait-pid and --updated-to and ignores
anything else, including --apply-update, which no released version passes to
the downloaded binary. Arguments are read lossily, so a non-UTF-8 argument no
longer panics.

The wait for a parent pid polls kill(pid, 0) on Unix and waits on the process
handle on Windows, capped by the caller. The Windows platform module gains the
WebView2 runtime probe, an error message box and the theme read.

handoff::relaunch spawns the AppImage or the current executable detached with
--wait-pid and returns the spawn error to the caller. The startup cleanup of
<exe>.old.* siblings and staged updater executables moves out of the Win32
update module.
2026-10-21 13:19:15 +07:00

165 lines
4.8 KiB
Rust

//! Command-line flags. Parsing is pure so every accepted form is unit-tested.
//!
//! Accepted flags:
//! - `--diagnose` turns on the diagnostic log.
//! - `--wait-pid <pid>` waits for an exiting instance before starting.
//! - `--updated-to <version>` announces a finished update.
//!
//! Anything else, including a flag whose value is missing or malformed, is
//! ignored so an old or newer caller can never stop the app from starting.
use std::ffi::OsString;
/// Longest version string accepted for `--updated-to`.
const MAX_VERSION_LEN: usize = 64;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct CliArgs {
pub diagnose: bool,
pub wait_pid: Option<u32>,
pub updated_to: Option<String>,
}
/// The process arguments, parsed. Non-UTF-8 arguments are read lossily
/// instead of panicking, as `std::env::args` would.
pub fn from_env() -> CliArgs {
parse(std::env::args_os().skip(1))
}
/// Parse the arguments after the program name.
pub fn parse<I, S>(args: I) -> CliArgs
where
I: IntoIterator<Item = S>,
S: Into<OsString>,
{
let args: Vec<String> = args
.into_iter()
.map(|a| a.into().to_string_lossy().into_owned())
.collect();
let mut out = CliArgs::default();
let mut i = 0;
while i < args.len() {
match args[i].as_str() {
"--diagnose" => out.diagnose = true,
"--wait-pid" => {
if let Some(pid) = args.get(i + 1).and_then(|v| parse_pid(v)) {
out.wait_pid = Some(pid);
i += 1;
}
}
"--updated-to" => {
if let Some(version) = args.get(i + 1).filter(|v| is_version_like(v)) {
out.updated_to = Some(version.clone());
i += 1;
}
}
_ => {}
}
i += 1;
}
out
}
/// A positive process id. Zero is rejected: on Unix it names the whole
/// process group, never a single parent.
fn parse_pid(value: &str) -> Option<u32> {
value.parse::<u32>().ok().filter(|pid| *pid != 0)
}
/// A version as the updater passes it (`0.6.0`, `1.2.3-rc.1`). The text ends
/// up in a notification, so anything else is dropped.
fn is_version_like(value: &str) -> bool {
!value.is_empty()
&& value.len() <= MAX_VERSION_LEN
&& value.starts_with(|c: char| c.is_ascii_digit())
&& value
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '+'))
}
#[cfg(test)]
mod tests {
use super::*;
fn p(args: &[&str]) -> CliArgs {
parse(args.iter().copied())
}
#[test]
fn no_flags_is_the_default() {
assert_eq!(p(&[]), CliArgs::default());
}
#[test]
fn diagnose_flag() {
assert!(p(&["--diagnose"]).diagnose);
}
#[test]
fn wait_pid_takes_the_next_value() {
assert_eq!(p(&["--wait-pid", "4242"]).wait_pid, Some(4242));
}
#[test]
fn wait_pid_rejects_zero_garbage_and_a_missing_value() {
assert_eq!(p(&["--wait-pid", "0"]).wait_pid, None);
assert_eq!(p(&["--wait-pid", "-5"]).wait_pid, None);
assert_eq!(p(&["--wait-pid"]).wait_pid, None);
// A following flag is not swallowed as the value.
let args = p(&["--wait-pid", "--diagnose"]);
assert_eq!(args.wait_pid, None);
assert!(args.diagnose);
}
#[test]
fn updated_to_alone() {
// v0.3.13 to v0.4.0 relaunch with only this flag.
assert_eq!(
p(&["--updated-to", "0.6.0"]),
CliArgs {
updated_to: Some("0.6.0".into()),
..CliArgs::default()
}
);
}
#[test]
fn updated_to_combined_with_wait_pid() {
assert_eq!(
p(&[
"--wait-pid",
"17",
"--updated-to",
"1.2.3-rc.1",
"--diagnose"
]),
CliArgs {
diagnose: true,
wait_pid: Some(17),
updated_to: Some("1.2.3-rc.1".into()),
}
);
}
#[test]
fn updated_to_rejects_text_that_is_not_a_version() {
assert_eq!(p(&["--updated-to", "--diagnose"]).updated_to, None);
assert_eq!(p(&["--updated-to", "v0.6.0"]).updated_to, None);
assert_eq!(p(&["--updated-to", "0.6.0 <b>"]).updated_to, None);
let long = "1".repeat(MAX_VERSION_LEN + 1);
assert_eq!(p(&["--updated-to", &long]).updated_to, None);
assert_eq!(p(&["--updated-to"]).updated_to, None);
}
#[test]
fn unknown_flags_are_ignored() {
assert_eq!(
p(&["--apply-update", "x", "--wait-pid", "9", "--frobnicate"]),
CliArgs {
wait_pid: Some(9),
..CliArgs::default()
}
);
}
}