Files
owui-tools/vm/host/owuivm-powerdown
T
2026-09-15 15:24:03 -04:00

58 lines
2.3 KiB
Bash
Executable File

#!/bin/bash
# =====================================================================
# Presses the virtual power button, from the unit's ExecStop=.
#
# WHY THIS EXISTS
#
# Stopping the unit used to SIGTERM qemu, which from inside the machine
# is indistinguishable from pulling the plug out of the wall. For a
# session that is fine - the overlay is thrown away either side of it -
# but a MAINTENANCE boot is the operator editing the base image that
# every future session runs from, and yanking power on that is how a
# frozen image acquires a dirty filesystem, or a half-written registry.
#
# ExecStop= runs while the main process is still alive, so this sends
# `system_powerdown` over QMP and returns. systemd then waits for qemu
# to exit by itself, up to TimeoutStopSec - and if the guest ignores the
# button, or has already hung, the SIGTERM still happens afterwards. So
# this is a courtesy with a deadline, not a new way to get stuck.
#
# Deliberately never fails: a stop path that can error is a stop path
# that leaves the unit in `failed` and the next start blocked.
# =====================================================================
set -u
SESSION="${1:-${OWUIVM_SESSION:-}}"
[[ "$SESSION" =~ ^[0-9a-f]{32}$ ]] || exit 0
SOCK="/run/owuivm/$SESSION/qmp.sock"
[ -S "$SOCK" ] || exit 0
/usr/bin/python3 - "$SOCK" <<'PY' 2>/dev/null || true
import json, socket, sys, time
path = sys.argv[1]
try:
s = socket.socket(socket.AF_UNIX); s.settimeout(5); s.connect(path)
f = s.makefile("rwb")
f.readline() # greeting
f.write(b'{"execute":"qmp_capabilities"}\n'); f.flush(); f.readline()
f.write(b'{"execute":"system_powerdown"}\n'); f.flush(); f.readline()
# Give the guest a moment to notice the button before systemd starts
# counting; an OS that has begun shutting down will not be helped by
# being asked twice.
deadline = time.monotonic() + 90
while time.monotonic() < deadline:
f.write(b'{"execute":"query-status"}\n'); f.flush()
line = f.readline()
if not line:
break # qemu has gone: done
try:
if not (json.loads(line).get("return") or {}).get("running", True):
break
except ValueError:
pass
time.sleep(1)
except Exception:
pass
PY
exit 0