veilor-os/test/auto-install-keymap.sh

168 lines
6.9 KiB
Bash
Raw Normal View History

#!/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')"
}