#!/usr/bin/env bash # auto-install-keymap.sh — sourced helper for QEMU-monitor-driven UI automation. # # Provides a minimal but complete US-layout keymap mapping every printable # ASCII character to a QEMU `sendkey` chord, plus convenience wrappers for # typing strings, sending special keys, taking screenshots, and waiting for # the monitor socket to appear. # # Usage: # source test/auto-install-keymap.sh # MONITOR_SOCK=/path/to/sock # km_wait_socket "$MONITOR_SOCK" 60 # km_send_str "$MONITOR_SOCK" "hello world" # km_send_key "$MONITOR_SOCK" ret # km_send_chord "$MONITOR_SOCK" ctrl alt f1 # km_screendump "$MONITOR_SOCK" /tmp/shot.ppm # # Why a separate file: other harnesses (regression suites, fuzzers) can # source this without dragging in the full installer test driver. # Guard against double-source. [[ -n "${__VEILOR_KEYMAP_LOADED:-}" ]] && return 0 __VEILOR_KEYMAP_LOADED=1 # ── Tool requirements ────────────────────────────────────────────────── # socat is the canonical way to talk to a unix-domain QEMU monitor. # nc-openbsd would also work but socat is what run-vm.sh already uses. km_require_tools() { local missing=() for t in socat qemu-img qemu-system-x86_64; do command -v "$t" >/dev/null 2>&1 || missing+=("$t") done if [[ ${#missing[@]} -gt 0 ]]; then echo "[ERR] missing required tools: ${missing[*]}" >&2 return 1 fi } # ── Low-level monitor I/O ────────────────────────────────────────────── # Send a single line of monitor input. Newlines are critical — QEMU's HMP # parses one command per line. Errors are swallowed: the most common cause # is the VM having shut down between two send_* calls, which we tolerate. km_monitor_send() { local sock=$1; shift printf '%s\n' "$*" | socat - "UNIX-CONNECT:$sock" 2>/dev/null || true } # Send a raw HMP command and capture any stdout response (e.g. for `info` # queries). Trims the QEMU monitor banner + prompt noise. km_monitor_query() { local sock=$1; shift printf '%s\n' "$*" | socat -t 1 - "UNIX-CONNECT:$sock" 2>/dev/null \ | sed -e 's/\r//g' -e '/^QEMU /d' -e '/^(qemu)/d' || true } # Wait until the monitor unix socket exists and accepts connections. # $2 = max wait in seconds (default 60). km_wait_socket() { local sock=$1 max=${2:-60} waited=0 while (( waited < max )); do if [[ -S $sock ]]; then # Try a no-op query — confirms the QEMU side is actually serving. if printf 'info status\n' | socat -t 1 - "UNIX-CONNECT:$sock" >/dev/null 2>&1; then return 0 fi fi sleep 1 waited=$((waited + 1)) done echo "[ERR] monitor socket $sock never became ready (waited ${max}s)" >&2 return 1 } # ── Screenshots ──────────────────────────────────────────────────────── # Ask QEMU to dump the current framebuffer. Output is PPM. Convert to PNG # with ImageMagick if available; otherwise leave PPM and warn. km_screendump() { local sock=$1 out=$2 local ppm="${out%.png}.ppm" km_monitor_send "$sock" "screendump $ppm" sleep 1 # give QEMU a moment to flush if [[ -f $ppm ]] && command -v convert >/dev/null 2>&1; then convert "$ppm" "$out" 2>/dev/null && rm -f "$ppm" fi } # ── Key tables ───────────────────────────────────────────────────────── # QEMU `sendkey` reference: docs/system/keys.html.in. The HMP names are # the X11 keysym lower-case, with a few exceptions for non-letter keys # (spc, ret, minus, etc.). What follows is the full US-layout printable # ASCII set. Everything outside this table is silently dropped — callers # are responsible for not feeding it characters the installer can't accept # anyway (passwords are validated to ASCII-printable in veilor-installer). declare -gA __KM_PLAIN=( [' ']=spc [a]=a [b]=b [c]=c [d]=d [e]=e [f]=f [g]=g [h]=h [i]=i [j]=j [k]=k [l]=l [m]=m [n]=n [o]=o [p]=p [q]=q [r]=r [s]=s [t]=t [u]=u [v]=v [w]=w [x]=x [y]=y [z]=z [0]=0 [1]=1 [2]=2 [3]=3 [4]=4 [5]=5 [6]=6 [7]=7 [8]=8 [9]=9 ['-']=minus ['=']=equal ['[']=bracket_left [']']=bracket_right [';']=semicolon ["'"]=apostrophe [',']=comma ['.']=dot ['/']=slash ['\\']=backslash ['`']=grave_accent ) # Shift-prefixed (US): all caps + shifted-symbol row. declare -gA __KM_SHIFT=( [A]=a [B]=b [C]=c [D]=d [E]=e [F]=f [G]=g [H]=h [I]=i [J]=j [K]=k [L]=l [M]=m [N]=n [O]=o [P]=p [Q]=q [R]=r [S]=s [T]=t [U]=u [V]=v [W]=w [X]=x [Y]=y [Z]=z ['!']=1 ['@']=2 ['#']=3 ['$']=4 ['%']=5 ['^']=6 ['&']=7 ['*']=8 ['(']=9 [')']=0 ['_']=minus ['+']=equal ['{']=bracket_left ['}']=bracket_right [':']=semicolon ['"']=apostrophe ['<']=comma ['>']=dot ['?']=slash ['|']=backslash ['~']=grave_accent ) # ── Public send wrappers ─────────────────────────────────────────────── # Send a single named key (e.g. ret, esc, up, tab, f1). km_send_key() { local sock=$1 key=$2 km_monitor_send "$sock" "sendkey $key" } # Send a chord — components are joined with `-` per QEMU HMP syntax. km_send_chord() { local sock=$1; shift local IFS='-' km_monitor_send "$sock" "sendkey $*" } # Type a string by encoding each character via the keymap. Unrecognised # characters are skipped with a warning to stderr — caller is expected to # stick to printable ASCII. km_send_str() { local sock=$1 s=$2 ch chord local i=0 while (( i < ${#s} )); do ch="${s:i:1}" if [[ -n "${__KM_PLAIN[$ch]:-}" ]]; then chord="${__KM_PLAIN[$ch]}" km_monitor_send "$sock" "sendkey $chord" elif [[ -n "${__KM_SHIFT[$ch]:-}" ]]; then chord="${__KM_SHIFT[$ch]}" km_monitor_send "$sock" "sendkey shift-$chord" else printf '[WARN] km_send_str: unencodable char %q skipped\n' "$ch" >&2 fi i=$((i + 1)) # Tiny gap so QEMU doesn't drop fast keypresses on busy hosts. # Empirically 5ms = the line between "100% reliable" and "loses ~1%". sleep 0.02 done } # Convenience: type a string then press Enter. km_send_line() { local sock=$1 s=$2 km_send_str "$sock" "$s" km_send_key "$sock" ret } # Visual indicator for log readability — prints a banner + a short pause so # the next monitor command has time to land on a stable UI frame. Used by # the harness between major steps; safe to skip in automated reuse. km_step_banner() { local label=$1 printf '\n──── %s @ %s ────\n' "$label" "$(date +'%H:%M:%S')" }