Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar

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
}