From 058ce50245157a3b8417bc118f0cbc4cd73e0d9d Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Mon, 14 Sep 2026 15:04:40 +1000 Subject: [PATCH] tests: the quit tests wait for the GUI's window before sending SIGTERM The launcher took the NML task being reachable as the GUI being up, and sent its SIGTERM a fixed few seconds later. On a loaded runner the GUI is still importing or constructing then, so the test was killing a startup, not quitting a GUI, and failed one of two ways: the process did not exist yet, or a SIGTERM inside construction got lost. Wait for the process, then for a viewable window with its pid; offscreen Qt has no window, so ask its shim for the self-grab it saves only once a top-level is visible. --- tests/ui-smoke/_lib/quit-launch.sh | 95 +++++++++++++++++++++++------- 1 file changed, 75 insertions(+), 20 deletions(-) diff --git a/tests/ui-smoke/_lib/quit-launch.sh b/tests/ui-smoke/_lib/quit-launch.sh index c374c22e83d..ebada34e249 100755 --- a/tests/ui-smoke/_lib/quit-launch.sh +++ b/tests/ui-smoke/_lib/quit-launch.sh @@ -3,9 +3,9 @@ # Usage: quit-launch.sh # # Boots linuxcnc + GUI under xvfb-run exactly like launch.sh, waits for -# the NML task to come up (via drive.py), then sends SIGTERM to the GUI -# process *alone* and asserts the GUI exits on its own within a short -# grace. This is the regression guard for the SIGTERM clean-shutdown +# the NML task to come up (via drive.py) and for the GUI to put a window +# on the screen, then sends SIGTERM to the GUI process *alone* and +# asserts the GUI exits on its own within a short grace. This is the regression guard for the SIGTERM clean-shutdown # handlers: a GUI that absorbs SIGTERM and has to be SIGKILLed fails. # # is a pgrep -f pattern identifying the GUI process @@ -14,7 +14,7 @@ # # Markers (consumed by checkresult-quit.sh): # UI_SMOKE_QUIT_OK GUI exited on SIGTERM within QUIT_GRACE -# UI_SMOKE_QUIT_FAIL GUI never started, was not found, or ignored TERM +# UI_SMOKE_QUIT_FAIL GUI never started, showed no window, or ignored TERM set -u @@ -24,7 +24,7 @@ TEST_DIR="${TEST_DIR:-$(cd "$(dirname "$0")" && pwd)}" LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$TEST_DIR" || exit 1 -rm -f ui-smoke.out ui-smoke.err linuxcnc.pid +rm -f ui-smoke.out ui-smoke.err ui-smoke-qt.png linuxcnc.pid bash "$LIB_DIR/cleanup-runtime.sh" @@ -34,6 +34,9 @@ DRIVER_TIMEOUT=90 # stuck. A GUI honouring SIGTERM exits in well under a second; the # margin covers Cleanup of task/motion on slow CI. QUIT_GRACE=15 +# Seconds to wait, after task is reachable, for the GUI to show a window +# before declaring it never came up. +GUI_UP_TIMEOUT=60 # Shared headless environment (software GL + audio silencing), kept in # launch-env.sh so launch.sh and quit-launch.sh cannot drift apart. @@ -47,7 +50,7 @@ crashdump_arm # shell's cwd); see launch.sh for why a relative name would miss. export UI_SMOKE_QT_SHOT="$PWD/ui-smoke-qt.png" -export CONFIG_INI LIB_DIR DRIVER_TIMEOUT GUI_MATCH QUIT_GRACE +export CONFIG_INI LIB_DIR DRIVER_TIMEOUT GUI_MATCH QUIT_GRACE GUI_UP_TIMEOUT # shellcheck disable=SC2016 xvfb-run -a --server-args="-screen 0 $UI_SMOKE_XVFB_SCREEN" \ @@ -57,8 +60,8 @@ xvfb-run -a --server-args="-screen 0 $UI_SMOKE_XVFB_SCREEN" \ LINUXCNC_PID=$! echo "$LINUXCNC_PID" >linuxcnc.pid - # Wait until the task is reachable (GUI has constructed and the - # NML round-trip works). Reuse the phase-1 driver for readiness. + # Wait until the task is reachable (the NML round-trip works). + # Reuse the phase-1 driver for readiness. timeout "$DRIVER_TIMEOUT" python3 "$LIB_DIR/drive.py" >ui-smoke.out 2>ui-smoke.err if ! grep -q "^UI_SMOKE_OK$" ui-smoke.out; then echo "UI_SMOKE_QUIT_FAIL: GUI did not come up; cannot test quit" @@ -67,19 +70,62 @@ xvfb-run -a --server-args="-screen 0 $UI_SMOKE_XVFB_SCREEN" \ exit 1 fi - # Identify the GUI process. pgrep -f matches against the whole - # command line, so wrapper processes (the linuxcnc launcher, the - # xvfb-run shell, this bash -c) also match because the GUI name - # appears in the config path or the embedded script text. Every - # such wrapper has a shell or xvfb-run as argv[0]; the real GUI - # is a python interpreter. Pick the first match whose argv[0] - # basename is a python binary. + # Task being reachable says nothing about the GUI: the launcher + # starts it after task, and on a loaded runner it can still be + # importing, or constructing, seconds later. A SIGTERM there + # tests startup, not quit. Wait for the process, then for a + # window of its own on the screen: that is a GUI running. + # + # pgrep -f matches against the whole command line, so wrapper + # processes (the linuxcnc launcher, the xvfb-run shell, this + # bash -c) also match because the GUI name appears in the config + # path or the embedded script text. Every such wrapper has a + # shell or xvfb-run as argv[0]; the real GUI is a python + # interpreter. Pick the first match whose argv[0] basename is a + # python binary. + find_gui_pid() { + for p in $(pgrep -f "$GUI_MATCH"); do + arg0=$(tr "\0" "\n" <"/proc/$p/cmdline" 2>/dev/null | head -1) + case "$(basename "$arg0" 2>/dev/null)" in + python*) echo "$p"; return 0 ;; + esac + done + return 1 + } + # A viewable top-level window whose _NET_WM_PID is the GUI. GTK + # and Qt both set the property; xvfb runs no window manager, so + # top-levels are children of the root window. + # + # Offscreen Qt never draws to the X server. Its shim saves a + # self-grab on SIGUSR1, and only once a top-level is visible, so + # the grab file appearing is the same signal. The shim installs + # the handler at interpreter startup; until then SIGUSR1 would + # kill the process, so ask only once /proc says it is caught. + gui_window_up() { + if [ "${QT_QPA_PLATFORM:-}" = "offscreen" ]; then + [ -s "$UI_SMOKE_QT_SHOT" ] && return 0 + caught=$(awk "/^SigCgt:/{print \$2}" "/proc/$1/status" 2>/dev/null) + if [ -n "$caught" ] && [ $(( 0x$caught >> 9 & 1 )) -eq 1 ]; then + kill -USR1 "$1" 2>/dev/null || true + fi + return 1 + fi + for w in $(xwininfo -root -children 2>/dev/null | grep -oE "^ +0x[0-9a-f]+"); do + xprop -id "$w" _NET_WM_PID 2>/dev/null | grep -q "= $1\$" || continue + xwininfo -id "$w" 2>/dev/null | grep -q "Map State: IsViewable" && return 0 + done + return 1 + } GUI_PID="" - for p in $(pgrep -f "$GUI_MATCH"); do - arg0=$(tr "\0" "\n" <"/proc/$p/cmdline" 2>/dev/null | head -1) - case "$(basename "$arg0" 2>/dev/null)" in - python*) GUI_PID="$p"; break ;; - esac + waited=0 + while [ "$waited" -lt "$GUI_UP_TIMEOUT" ]; do + [ -n "$GUI_PID" ] || GUI_PID=$(find_gui_pid) + if [ -n "$GUI_PID" ] && gui_window_up "$GUI_PID"; then + break + fi + kill -0 "$LINUXCNC_PID" 2>/dev/null || break + sleep 1 + waited=$((waited + 1)) done if [ -z "$GUI_PID" ]; then echo "UI_SMOKE_QUIT_FAIL: GUI process matching \"$GUI_MATCH\" not found" @@ -87,6 +133,15 @@ xvfb-run -a --server-args="-screen 0 $UI_SMOKE_XVFB_SCREEN" \ bash "$LIB_DIR/cleanup-runtime.sh" exit 1 fi + if [ "$waited" -ge "$GUI_UP_TIMEOUT" ]; then + echo "UI_SMOKE_QUIT_FAIL: GUI (pid $GUI_PID) showed no window within ${GUI_UP_TIMEOUT}s" + . "$LIB_DIR/screenshot.sh" + screenshot_grab screenshot.png + kill -KILL -- -"$LINUXCNC_PID" 2>/dev/null || true + bash "$LIB_DIR/cleanup-runtime.sh" + exit 1 + fi + echo "GUI (pid $GUI_PID) up with a window after ${waited}s" # Send SIGTERM to the GUI alone and time how long it takes to go. kill -TERM "$GUI_PID" 2>/dev/null || true