#!/usr/bin/env bash set -Eeuo pipefail : "${SHUTDOWN:="Y"}" # Graceful ACPI shutdown : "${TIMEOUT:="105"}" # QEMU termination timeout # Configure QEMU for graceful shutdown SHUTDOWN_SKIP=0 SHUTDOWN_SIGNAL=0 QEMU_PTY="$QEMU_DIR/qemu.pty" QEMU_END="$QEMU_DIR/qemu.end" ACPI_SOCKET="$QEMU_DIR/acpi.sock" CONSOLE_PID="$QEMU_DIR/console.pid" CONSOLE_SOCKET="$QEMU_DIR/console.sock" QEMU_START_PID="$QEMU_DIR/qemu.start.pid" UEFI_SHELL_MESSAGE='UEFI Interactive Shell' LEGACY_BOOT_PATTERN='^Booting from (Hard Disk|DVD/CD)' UEFI_BOOT_PATTERN='BdsDxe: starting Boot[[:xdigit:]]{4} ' UEFI_NO_BOOT_MESSAGE='BdsDxe: No bootable option or device was found.' UEFI_DVD_BOOT_PATTERN='BdsDxe: starting Boot[[:xdigit:]]{4} "UEFI QEMU .*DVD-ROM' UEFI_WINDOWS_BOOT_PATTERN='BdsDxe: starting Boot[[:xdigit:]]{4} "Windows Boot Manager" from .*HD\(' bootStatus() { [ ! -s "$QEMU_PTY" ] && return 1 if [[ "${BOOT_MODE,,}" == "windows_legacy" ]]; then local line last recent # Only inspect output produced after the most recent BIOS boot attempt so # stale failures from an earlier device do not affect the current state. line=$(getBootMarker) [ -z "$line" ] && return 1 last="${line#*:}" recent=$(tail -n +"${line%%:*}" "$QEMU_PTY") grep -Fq "Loading FreeLoader..." <<< "$recent" && return 0 if grep -Fq \ -e "No bootable device." \ -e "BOOTMGR is missing" \ <<< "$recent"; then return 2 fi # These BIOS messages only describe the failed device attempt. QEMU may # immediately continue with another boot target, so clear pending success # instead of treating them as a terminal failure. if grep -Fq \ -e "Boot failed: not a bootable disk" \ -e "Boot failed: Could not read from CDROM" \ -e "Boot failed: could not read the boot disk" \ <<< "$recent"; then return 5 fi if [[ "$last" == "Booting from Hard Disk"* ]]; then return 3 fi if [[ "$last" == "Booting from DVD/CD"* ]]; then return 4 fi return 1 fi local line last recent # OVMF logs every boot option it tries. Track the newest attempt and only # evaluate messages emitted from that point onward. line=$(getBootMarker) [ -z "$line" ] && return 1 last="${line#*:}" recent=$(tail -n +"${line%%:*}" "$QEMU_PTY") grep -Eq \ 'BdsDxe: failed to start Boot[[:xdigit:]]{4} "UEFI QEMU .*DVD-ROM.*: Time out' \ <<< "$recent" && return 2 grep -Fq "$UEFI_NO_BOOT_MESSAGE" <<< "$recent" && return 2 grep -Fq "$UEFI_SHELL_MESSAGE" <<< "$recent" && return 2 # A failed device attempt is transitional because OVMF may immediately try # another boot target. Clear pending success and wait for the next attempt. grep -Eq \ 'BdsDxe: failed to start Boot[[:xdigit:]]{4} ' \ <<< "$recent" && return 5 grep -Eq "$UEFI_WINDOWS_BOOT_PATTERN" <<< "$last" && return 3 grep -Eq \ -e '"UEFI QEMU .*DVD-ROM' \ -e 'CDROM\(' \ -e 'USB\(' \ <<< "$last" && return 4 return 1 } waitForBoot() { local pid="$1" local timeout="${2:-30}" local keySent=0 local pendingType=0 local pendingLine="" local pendingDeadline=0 local keyDelay status marker local deadline=$((SECONDS + timeout)) local screen="visit http://127.0.0.1:$WEB_PORT/ to view the screen..." while isAlive "$pid"; do # Send the boot key once, either immediately after the prompt appears or # shortly after firmware starts the DVD when the prompt is not logged. if (( ! keySent )) && needsBootKey; then if keyDelay=$(bootKeyDelay); then if [[ "$keyDelay" == "0" ]]; then if sendKey spc 0 500; then keySent=1 fi else if sendKey spc "$keyDelay" 250 4 0.75; then keySent=1 fi fi fi fi if bootStatus; then status=0 else status=$? fi case "$status" in 0) echo if [[ "${DISPLAY,,}" == "web" ]] && ! disabled "${WEB:-Y}"; then info "$(app) started successfully, $screen" else info "$(app) started successfully." fi echo && return 0 ;; 2) echo error "$(app) could not boot, terminating..." terminateQemu return 0 ;; 3 | 4) marker=$(getBootMarker) # A firmware boot line alone is not proof that the guest started. Wait # briefly for a more definitive success or failure message, restarting # the grace period whenever firmware begins a different boot attempt. if [[ "$marker" != "$pendingLine" ]] || (( status != pendingType )); then pendingLine="$marker" pendingType=$status pendingDeadline=$((SECONDS + 6)) fi if (( pendingDeadline > 0 && SECONDS >= pendingDeadline )); then echo if [[ "${DISPLAY,,}" == "web" ]] && ! disabled "${WEB:-Y}"; then info "$(app) started successfully, $screen" else info "$(app) started successfully." fi echo && return 0 fi ;; 5) # A failed device attempt is transitional because firmware may continue # with another target. Discard any pending success decision. pendingType=0 pendingLine="" pendingDeadline=0 ;; *) pendingType=0 pendingLine="" pendingDeadline=0 ;; esac (( SECONDS >= deadline )) && break sleep 0.25 done isAlive "$pid" || return 0 [ -f "$QEMU_END" ] && return 0 error "Timeout while waiting for QEMU to boot the machine, terminating..." terminateQemu return 0 } legacyBootReady() { local line last recent local hard="Booting from Hard" local cdrom="Booting from DVD/CD" line=$(grep -n "^Booting.*" "$QEMU_PTY" | tail -1) [ -z "$line" ] && return 1 last="${line#*:}" recent=$(tail -n +"${line%%:*}" "$QEMU_PTY") # ACPI shutdown is safe once BIOS has handed control to the hard disk, unless # the same attempt already produced a known boot failure. [[ "${last,,}" != "${hard,,}"* ]] && return 1 grep -Fq "Loading FreeLoader..." <<< "$recent" && return 0 grep -Fq "No bootable device." <<< "$recent" && return 1 grep -Fq "BOOTMGR is missing" <<< "$recent" && return 1 grep -Fq "Boot failed: not a bootable disk" <<< "$recent" && return 1 grep -Fq "Boot failed: could not read the boot disk" <<< "$recent" && return 1 return 0 } ready() { # The marker means installation completed previously, so shutdown no longer # needs to infer guest readiness from firmware output. [ -f "$STORAGE/windows.boot" ] && return 0 [ ! -s "$QEMU_PTY" ] && return 1 if [[ "${BOOT_MODE,,}" == "windows_legacy" ]]; then legacyBootReady && return 0 return 1 fi local line last recent line=$(getBootMarker) [ -z "$line" ] && return 1 last="${line#*:}" recent=$(tail -n +"${line%%:*}" "$QEMU_PTY") # Only a Windows Boot Manager entry loaded from a hard disk proves that setup # has progressed far enough for an ACPI shutdown request to be appropriate. grep -Eq "$UEFI_WINDOWS_BOOT_PATTERN" <<< "$last" || return 1 # Reject failures emitted after this exact boot attempt. Without these checks, # a failed Windows Boot Manager entry remains classified as ready indefinitely. grep -Eq \ 'BdsDxe: failed to start Boot[[:xdigit:]]{4} "Windows Boot Manager"' \ <<< "$recent" && return 1 grep -Fq "$UEFI_NO_BOOT_MESSAGE" <<< "$recent" && return 1 grep -Fq "$UEFI_SHELL_MESSAGE" <<< "$recent" && return 1 return 0 } sendKey() { local key="$1" local delay="${2:-0}" local hold="${3:-100}" local repeat="${4:-1}" local interval="${5:-0}" local i output [ ! -S "$ACPI_SOCKET" ] && return 1 [[ "$delay" != "0" ]] && sleep "$delay" # Send all repeats through one monitor connection so timing remains stable # and QEMU receives the sequence as one operation. if ! output=$( { for ((i = 1; i <= repeat; i++)); do printf 'sendkey %s %s\n' "$key" "$hold" if (( i < repeat )); then sleep "$interval" fi done } | nc -q 1 -w 1 -U "$ACPI_SOCKET" 2>&1 ); then return 1 fi # The human monitor may return success at the transport level while reporting # a command error in its text response, so inspect that output explicitly. if grep -Eqi \ -e 'unknown command' \ -e 'unknown key' \ -e 'invalid parameter' \ -e 'invalid key' \ -e '^error:' \ <<< "$output"; then warn "failed to send boot key through QEMU monitor!" if enabled "${DEBUG:-}"; then echo "$output" fi return 1 fi return 0 } supportsBootKey() { local id="$1" [[ "${id,,}" == "win"* ]] } needsBootKey() { [ ! -s "$BOOT" ] && return 1 [[ "${BOOT,,}" != *".iso" ]] && return 1 [ -f "$STORAGE/windows.boot" ] && return 1 supportsBootKey "$DETECTED" } bootKeyDelay() { [ ! -s "$QEMU_PTY" ] && return 1 # A visible prompt is the safest trigger and should be answered immediately. if grep -Fq "Press any key to" "$QEMU_PTY"; then echo 0 return 0 fi # Some firmware or Windows versions do not log the prompt. In that case wait # briefly after the DVD boot attempt and send several short key presses. if [[ "${BOOT_MODE,,}" == "windows_legacy" ]]; then grep -Fq "Booting from DVD/CD" "$QEMU_PTY" || return 1 else grep -Eq "$UEFI_DVD_BOOT_PATTERN" "$QEMU_PTY" || return 1 fi echo 0.5 return 0 } getBootMarker() { if [[ "${BOOT_MODE,,}" == "windows_legacy" ]]; then grep -nE "$LEGACY_BOOT_PATTERN" "$QEMU_PTY" | tail -1 return 0 fi grep -nE "$UEFI_BOOT_PATTERN" "$QEMU_PTY" | tail -1 return 0 } markWindowsBooted() { local file="$STORAGE/windows.boot" if [ -f "$file" ] || [ ! -f "$BOOT" ]; then return 0 fi # Do not remove installation media until firmware output confirms Windows is # now booting from the installed disk rather than from setup media. ready || return 0 if ! touch "$file"; then warn "failed to create Windows installation marker!" return 0 fi if ! setOwner "$file"; then rm -f "$file" warn "failed to set the owner for \"$file\" !" return 0 fi if ! disabled "$REMOVE"; then rm -f "$BOOT" 2>/dev/null || true fi rm -f "$STORAGE/setup.img" 2>/dev/null || : return 0 } finish() { local reason=$1 failed=0 # QEMU_END distinguishes an expected shutdown path from an unexpected QEMU # exit carrying the same process status. if [ ! -f "$QEMU_END" ] && (( reason != 0 )); then failed=1 fi touch "$QEMU_END" forceKillQemu "$reason" if [ ! -f "$STORAGE/windows.boot" ]; then markWindowsBooted fi cleanupHelpers \ "${SMB_PID:-}" \ "${NMB_PID:-}" \ "${DDN_PID:-}" if ! waitQemuExit 10; then warn "Timed out while waiting for $(app) to exit!" fi echo if (( failed == 0 )); then echo "❯ Shutdown completed!" else error "QEMU exited unexpectedly!" fi exit "$reason" } abortDuringSetup() { local code="$1" # Before Windows boots from disk, ACPI may be ignored or interpreted by setup # itself. Terminate QEMU directly instead of waiting for a graceful shutdown. if [[ "${DETECTED,,}" != "reactos" ]] || [ -n "${CUSTOM:-}" ]; then info "Cannot send ACPI signal during $(app) setup, terminating..." else info "ReactOS LiveCD does not support ACPI shutdown, terminating..." fi terminateQemu if ! waitQemuExit 10; then warn "Timed out while waiting for $(app) to exit!" fi finish "$code" } gracefulShutdown() { local sig="$1" local pid code # Traps can run in subshells created by pipelines or command substitutions; # only the original shell may coordinate QEMU shutdown. [[ $BASHPID != "$TRAP_PID" ]] && return code=$(signalCode "$sig") if (( SHUTDOWN_SIGNAL != 0 )); then # A second Ctrl-C during an active shutdown skips the remaining grace period # and lets the shutdown loop force QEMU down immediately. if (( code == 130 && SHUTDOWN_SIGNAL == code )); then SHUTDOWN_SKIP=1 echo && info "Received SIGINT again, forcing shutdown..." return fi echo && info "Received $sig signal while already shutting down..." return fi SHUTDOWN_SIGNAL=$code # Signal handlers must complete their own error handling and cleanup without # errexit terminating the shell partway through the shutdown sequence. set +e echo && info "Received $sig signal, sending ACPI shutdown signal..." # Interactive startup may receive a signal before the PID file appears, so # briefly wait for it there; non-interactive operation fails immediately. if ! readQemuPid pid; then if ! interactive || ! waitQemuPid pid; then warn "QEMU PID file does not exist?" finish "$code" fi fi if [ -z "$pid" ] || ! isAlive "$pid"; then warn "QEMU process with PID $pid does not exist?" finish "$code" fi if ! ready; then abortDuringSetup "$code" fi normalizeTimeout 105 waitForShutdown "$pid" finish "$code" } enabled "$SHUTDOWN" || return 0 [ -n "${QEMU_TIMEOUT:-}" ] && TIMEOUT="$QEMU_TIMEOUT" # Keep Ctrl-C available to interactive users without installing an unnecessary # SIGINT handler for background/container execution. if interactive; then _trap gracefulShutdown SIGINT fi _trap gracefulShutdown SIGTERM SIGHUP SIGABRT SIGQUIT return 0