From 08f040a819c4b742b08cd103ccfefdd5f4b4a989 Mon Sep 17 00:00:00 2001 From: Kroese Date: Tue, 23 Jan 2024 22:38:17 +0100 Subject: [PATCH] feat: Implement graceful shutdown (#81) --- docker-compose.yml | 2 +- readme.md | 6 +- src/entry.sh | 16 +++-- src/install.sh | 17 +---- src/power.sh | 151 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 169 insertions(+), 23 deletions(-) create mode 100644 src/power.sh diff --git a/docker-compose.yml b/docker-compose.yml index 9315465..0f8bf97 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,4 +12,4 @@ services: - 3389:3389/tcp - 3389:3389/udp stop_grace_period: 2m - restart: unless-stopped + restart: on-failure diff --git a/readme.md b/readme.md index 86c2187..3c479dd 100644 --- a/readme.md +++ b/readme.md @@ -39,13 +39,13 @@ services: - 3389:3389/tcp - 3389:3389/udp stop_grace_period: 2m - restart: unless-stopped + restart: on-failure ``` Via `docker run` ```bash -docker run -it --rm -p 8006:8006 --device=/dev/kvm --cap-add NET_ADMIN dockurr/windows +docker run -it --rm -p 8006:8006 --device=/dev/kvm --cap-add NET_ADMIN --stop-timeout 120 dockurr/windows ``` ## FAQ @@ -152,7 +152,7 @@ docker run -it --rm -p 8006:8006 --device=/dev/kvm --cap-add NET_ADMIN dockurr/w VERSION: "https://example.com/win.iso" ``` - Alternatively, you can also place a file called `custom.iso` in an empty `/storage` folder to skip the download. + Alternatively, you can also rename a local file to `custom.iso` and place it in an empty `/storage` folder to skip the download. * ### How do I pass-through a disk? diff --git a/src/entry.sh b/src/entry.sh index 17d3009..c1cf0d6 100644 --- a/src/entry.sh +++ b/src/entry.sh @@ -2,23 +2,31 @@ set -Eeuo pipefail APP="Windows" -export BOOT_MODE=windows +BOOT_MODE="windows" SUPPORT="https://github.com/dockur/windows" cd /run . reset.sh # Initialize system -. install.sh # Get bootdisk +. install.sh # Run installation . disk.sh # Initialize disks . display.sh # Initialize graphics . network.sh # Initialize network . boot.sh # Configure boot . proc.sh # Initialize processor +. power.sh # Configure shutdown . config.sh # Configure arguments trap - ERR info "Booting $APP using $VERS..." +[[ "$DEBUG" == [Yy1]* ]] && echo "Arguments: $ARGS" && echo -[[ "$DEBUG" == [Yy1]* ]] && set -x -exec qemu-system-x86_64 ${ARGS:+ $ARGS} +{ qemu-system-x86_64 ${ARGS:+ $ARGS} >"$QEMU_OUT" 2>"$QEMU_LOG"; rc=$?; } || : +(( rc != 0 )) && error "$(<"$QEMU_LOG")" && exit 15 + +terminal +tail -fn +0 "$QEMU_LOG" 2>/dev/null & +cat "$QEMU_TERM" 2>/dev/null & wait $! || : + +sleep 1 && finish 0 diff --git a/src/install.sh b/src/install.sh index 4188e30..18fe266 100644 --- a/src/install.sh +++ b/src/install.sh @@ -31,14 +31,6 @@ set -Eeuo pipefail [[ "${VERSION,,}" == "win16" ]] && VERSION="win2016-eval" [[ "${VERSION,,}" == "win2016" ]] && VERSION="win2016-eval" -if [[ "${VERSION,,}" == "tiny10" ]]; then - VERSION="https://archive.org/download/tiny-10-23-h2/tiny10%20x64%2023h2.iso" -fi - -if [[ "${VERSION,,}" == "tiny11" ]]; then - VERSION="https://archive.org/download/tiny-11-core-x-64-beta-1/tiny11%20core%20x64%20beta%201.iso" -fi - CUSTOM="custom.iso" [ ! -f "$STORAGE/$CUSTOM" ] && CUSTOM="Custom.iso" @@ -91,14 +83,9 @@ else fi html "$MSG" + TMP="$STORAGE/tmp" - -if [ -z "$MANUAL" ]; then - - MANUAL="N" - [[ "${BASE,,}" == "tiny10"* ]] && MANUAL="Y" - -fi +[ -z "$MANUAL" ] && MANUAL="N" if [ -f "$STORAGE/$BASE" ]; then diff --git a/src/power.sh b/src/power.sh new file mode 100644 index 0000000..75e244b --- /dev/null +++ b/src/power.sh @@ -0,0 +1,151 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +# Configure QEMU for graceful shutdown + +QEMU_TERM="" +QEMU_PORT=7100 +QEMU_TIMEOUT=110 +QEMU_PID="/run/shm/qemu.pid" +QEMU_LOG="/run/shm/qemu.log" +QEMU_OUT="/run/shm/qemu.out" +QEMU_END="/run/shm/qemu.end" + +rm -f /run/shm/qemu.* +touch "$QEMU_LOG" + +_trap() { + func="$1" ; shift + for sig ; do + trap "$func $sig" "$sig" + done +} + +finish() { + + local pid + local reason=$1 + + if [ -f "$QEMU_PID" ]; then + + pid=$(<"$QEMU_PID") + echo && error "Forcefully terminating Windows, reason: $reason..." + { kill -15 "$pid" || true; } 2>/dev/null + + while isAlive "$pid"; do + sleep 1 + # Workaround for zombie pid + [ ! -f "$QEMU_PID" ] && break + done + fi + + pid="/var/run/tpm.pid" + [ -f "$pid" ] && pKill "$(<"$pid")" + + closeNetwork + + sleep 1 + echo && echo "❯ Shutdown completed!" + + exit "$reason" +} + +terminal() { + + local dev="" + + if [ -f "$QEMU_OUT" ]; then + + local msg + msg=$(<"$QEMU_OUT") + + if [ -n "$msg" ]; then + + if [[ "${msg,,}" != "char"* || "$msg" != *"serial0)" ]]; then + echo "$msg" + fi + + dev="${msg#*/dev/p}" + dev="/dev/p${dev%% *}" + + fi + fi + + if [ ! -c "$dev" ]; then + dev=$(echo 'info chardev' | nc -q 1 -w 1 localhost "$QEMU_PORT" | tr -d '\000') + dev="${dev#*serial0}" + dev="${dev#*pty:}" + dev="${dev%%$'\n'*}" + dev="${dev%%$'\r'*}" + fi + + if [ ! -c "$dev" ]; then + error "Device '$dev' not found!" + finish 34 && return 34 + fi + + QEMU_TERM="$dev" + return 0 +} + +_graceful_shutdown() { + + local code=$? + + set +e + + if [ -f "$QEMU_END" ]; then + echo && info "Received $1 while already shutting down..." + return + fi + + touch "$QEMU_END" + echo && info "Received $1, sending ACPI shutdown signal..." + + if [ ! -f "$QEMU_PID" ]; then + echo && error "QEMU PID file does not exist?" + finish "$code" && return "$code" + fi + + local pid="" + pid=$(<"$QEMU_PID") + + if ! isAlive "$pid"; then + echo && error "QEMU process does not exist?" + finish "$code" && return "$code" + fi + + # Send ACPI shutdown signal + echo 'system_powerdown' | nc -q 1 -w 1 localhost "${QEMU_PORT}" > /dev/null + + local cnt=0 + while [ "$cnt" -lt "$QEMU_TIMEOUT" ]; do + + sleep 1 + cnt=$((cnt+1)) + + ! isAlive "$pid" && break + # Workaround for zombie pid + [ ! -f "$QEMU_PID" ] && break + + info "Waiting for Windows to shutdown... ($cnt/$QEMU_TIMEOUT)" + + # Send ACPI shutdown signal + echo 'system_powerdown' | nc -q 1 -w 1 localhost "${QEMU_PORT}" > /dev/null + + done + + if [ "$cnt" -ge "$QEMU_TIMEOUT" ]; then + echo && error "Shutdown timeout reached, aborting..." + fi + + finish "$code" && return "$code" +} + +SERIAL="pty" +MONITOR="telnet:localhost:$QEMU_PORT,server,nowait,nodelay" +MONITOR="$MONITOR -daemonize -D $QEMU_LOG -pidfile $QEMU_PID" + +_trap _graceful_shutdown SIGTERM SIGHUP SIGINT SIGABRT SIGQUIT + +return 0