168 lines
6.9 KiB
Bash
168 lines
6.9 KiB
Bash
|
|
#!/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')"
|
||
|
|
}
|