deploy/upcloud/src/bin/vnc-capture.rs
//! `gunnar-vnc-capture <host:port> <password> <out-prefix> [count] [gap_secs]`
//!
//! Takes N screenshots of the appliance's QEMU console, `gap_secs` apart, and
//! writes them as `<out-prefix>-1.png` … so that a *boot loop shows as motion*
//! rather than being guessed from one frame. Prints one line per capture with
//! the framebuffer size, the desktop name the server reported, and a colour
//! summary including an FNV-1a fingerprint of the pixels — identical
//! fingerprints across captures mean the screen is **static**, which is itself
//! the finding when a box is suspected of crash-looping.
//!
//! A separate `[[bin]]` rather than a verb on `gunnar-upcloud`: that binary is
//! `reinstall`, a destructive procedure behind `--confirm-destroy`, and this is
//! a read-only observation that must stay reachable without going anywhere near
//! that argument parser.
//!
//! **The port changes on every stop/start and the API reports a stale one.**
//! This binary takes an address, it does not discover one — get a live address
//! from `gunnar-upcloud reinstall --console`, i.e. [`gunnar_upcloud::console`],
//! which toggles `remote_access_enabled` and proves the `RFB 003.008` greeting
//! before returning.
use std::process::ExitCode;
use std::time::Duration;
use gunnar_upcloud::vnc;
fn main() -> ExitCode {
let args: Vec<String> = std::env::args().skip(1).collect();
if args.len() < 3 || args.iter().any(|a| a == "-h" || a == "--help") {
eprintln!(
"usage: gunnar-vnc-capture <host:port> <password> <out-prefix> [count] [gap_secs]"
);
return ExitCode::from(2);
}
let (addr, password, prefix) = (&args[0], &args[1], &args[2]);
let count: usize = args.get(3).and_then(|s| s.parse().ok()).unwrap_or(3);
let gap: u64 = args.get(4).and_then(|s| s.parse().ok()).unwrap_or(10);
let mut ok = 0usize;
for i in 1..=count {
eprintln!("[capture {i}/{count}] {addr}");
match vnc::capture(addr, password) {
Ok(shot) => {
let path = format!("{prefix}-{i}.png");
match vnc::write_png(&path, shot.w as u32, shot.h as u32, &shot.rgb) {
Ok(()) => {
println!(
"OK {path} {}x{} name={:?} {}",
shot.w,
shot.h,
shot.name,
vnc::describe(&shot.rgb)
);
ok += 1;
}
Err(e) => println!("FAIL write {path}: {e}"),
}
}
// Not fatal on its own: the console can drop between frames, and a
// capture that got 2 of 3 is still evidence.
Err(e) => println!("FAIL capture {i}: {e}"),
}
if i < count {
std::thread::sleep(Duration::from_secs(gap));
}
}
if ok == 0 {
eprintln!("vnc-capture: no frame captured");
return ExitCode::FAILURE;
}
ExitCode::SUCCESS
}