scripts/gui-session.sh: bring the headless GUI up without clicking blind

The relaunch sequence was an ad-hoc pile of docker exec one-liners, and it
had a real bug: it dismissed the first-run dialogs by computing the
titlebar close box from `xdotool getwindowgeometry --shell` and clicking
it. When the dialog had already closed, that eval left the geometry
variables stale or empty, the click landed at a garbage coordinate, and it
kept hitting the Sketch button in the toolbar underneath — so the app came
up in sketch mode with a stray Sketch feature that then had to be
cancelled by hand. Three times in one session.

The fix is not a different mechanism. `xdotool windowclose` looks cleaner
and KILLS THE APP: it destroys the GdkWindow out from under the dialog and
the process dies with "GdkWindow unexpectedly destroyed", three
GLib-GObject criticals and a segfault. Measured, not guessed — that is
what the first version of this script did. Escape does not close the Setup
Wizard either, which is why it needs handling at all. So the titlebar
click stays, and what changes is that it refuses to click geometry it has
not validated: the window id is re-resolved immediately before, all four
geometry variables are unset first and must come back numeric, and the
computed point must be inside the screen. Any of those failing logs why
and clicks nothing.

Two further honesty fixes in the status output, both caught by reading it
rather than by it failing: app_pid skipped nothing, so with a container
full of <defunct> instances it printed a dead pid as though the session
were healthy — it now walks /proc/<pid>/stat and ignores zombies. And a
container without x11vnc reported "vnc: DOWN" as if something had broken,
when nothing was ever installed; it now says so, and does not try to start
what is not there.

Also replaces the fixed post-launch sleep with a wait for the main window,
because cold starts under software GL vary by a lot, and adds --status for
diagnosis: "no windows but the desktop is up" means the app died, "cannot
connect at all" means the desktop did. That distinction cost real time to
work out by hand.

Verified on both containers: one run each, wizard closed on validated
geometry, no stray sketch mode, live pid reported, and the app still up.

snaporca-e1p adjacent (tooling, not the tab itself).
This commit is contained in:
Tommaso Bianchi
2026-07-30 14:35:35 +02:00
parent 3f8f46f93f
commit f14d31d956
+125
View File
@@ -0,0 +1,125 @@
#!/usr/bin/env bash
# Bring the headless GUI up on a VNC-served X display, ready to drive or to attach Remmina to.
#
# Runs INSIDE the long-lived GUI container (see the header of scripts/docker-iter-build.sh for how
# that container is created). Idempotent: safe to re-run to recover a session whose app died.
#
# docker exec <container> /OrcaSlicer/scripts/gui-session.sh # launch + settle
# docker exec <container> /OrcaSlicer/scripts/gui-session.sh --status # report, change nothing
#
# WHY THIS EXISTS. Dismissing the first-run dialogs by computing the titlebar close box from
# `xdotool getwindowgeometry --shell` and clicking it went wrong whenever the dialog had already
# closed: the eval left the geometry variables stale or empty, the click landed at a garbage
# coordinate, and it repeatedly hit the Sketch button in the toolbar underneath, so the app came up
# in sketch mode with a stray Sketch feature. Three of those in one session.
#
# The titlebar click is nevertheless the RIGHT mechanism and is kept. `xdotool windowclose` looks
# cleaner but kills the app: it destroys the GdkWindow out from under the dialog and the process
# dies with "GdkWindow unexpectedly destroyed", three GLib-GObject criticals and a segfault
# (measured 2026-07-30). Escape does not close the Setup Wizard either. So the fix is not a
# different mechanism, it is refusing to click on geometry we have not validated.
set -euo pipefail
DISP="${DISP:-:11}"
GEOM="${GEOM:-1920x1080}"
BIN="${BIN:-/OrcaSlicer/build/src/Release/orca-slicer}"
# Packaging cannot bundle python (the deps python layer carries a doubled-DESTDIR RUNPATH), so the
# build-tree binary needs the deps libpython on the path. Packaging-only issue; the app runs fine.
LIBPY="/OrcaSlicer/deps/build/destdir/usr/local/libpython/lib"
LIBPY2="/OrcaSlicer/build/src/Release/python/lib"
LOG="${LOG:-/tmp/gui-session.log}"
export DISPLAY="$DISP" HOME=/root
export LIBGL_ALWAYS_SOFTWARE=1 GALLIUM_DRIVER=llvmpipe
export LD_LIBRARY_PATH="$LIBPY:$LIBPY2:${LD_LIBRARY_PATH:-}"
mkdir -p /root/.config # startup dies in boost::filesystem::create_directory without this
# Skip zombies. This container accumulates <defunct> instances of the app across runs, and pgrep
# matches them, so the naive "first match" reported a dead pid as if the session were healthy.
app_pid() {
local p
for p in $(pgrep -f "$(basename "$BIN")" 2>/dev/null); do
[ "$(awk "{print \$3}" "/proc/$p/stat" 2>/dev/null)" = "Z" ] && continue
echo "$p"; return 0
done
return 1
}
status() {
echo "display : $(pgrep -f "Xvfb $DISP" >/dev/null && echo up || echo DOWN)"
echo "wm : $(pgrep -x openbox >/dev/null && echo up || echo DOWN)"
if pgrep -x x11vnc >/dev/null; then echo "vnc : up on :5900"
elif ! command -v x11vnc >/dev/null; then echo "vnc : n/a (x11vnc not installed here)"
else echo "vnc : DOWN"; fi
local p; p="$(app_pid || true)"
echo "app : ${p:-DOWN}"
[ -n "${p:-}" ] && echo "windows : $(xdotool search --name . getwindowname %@ 2>/dev/null | paste -sd'|' -)"
return 0
}
[ "${1:-}" = "--status" ] && { status; exit 0; }
# --- desktop: Xvfb, a window manager, and the VNC server ------------------------------------
# openbox is REQUIRED: without it xdotool windowactivate aborts with "windowmanager claims not to
# support _NET_ACTIVE_WINDOW" and dialogs never take focus.
pgrep -f "Xvfb $DISP" >/dev/null || { nohup Xvfb "$DISP" -screen 0 "${GEOM}x24" -nolisten tcp >/tmp/xvfb.log 2>&1 & sleep 3; }
pgrep -x openbox >/dev/null || { nohup openbox >/tmp/openbox.log 2>&1 & sleep 1; }
if ! pgrep -x x11vnc >/dev/null && command -v x11vnc >/dev/null; then
AUTH=()
[ -f /root/.vnc/passwd ] && AUTH=(-rfbauth /root/.vnc/passwd)
nohup x11vnc -display "$DISP" -rfbport 5900 "${AUTH[@]}" -forever -shared -noxdamage \
>/tmp/x11vnc.log 2>&1 &
sleep 2
fi
# --- app ------------------------------------------------------------------------------------
pkill -9 -f "$BIN" 2>/dev/null || true
sleep 2
nohup "$BIN" >"$LOG" 2>&1 &
echo "launched $(basename "$BIN") pid $!"
# Wait for the main window rather than sleeping a fixed amount: cold starts vary a lot under
# software GL, and a fixed sleep either wastes time or races.
for _ in $(seq 1 40); do
xdotool search --name "Untitled" >/dev/null 2>&1 && break
sleep 1
done
# --- first-run dialogs ----------------------------------------------------------------------
# Click the titlebar close box, but only on geometry we have just read for a window that still
# exists, and only if the resulting point is inside the screen. Every variable is unset first so a
# failed read cannot leave the previous dialog's numbers behind — that is the whole bug.
screen_w="${GEOM%x*}"; screen_h="${GEOM#*x}"
close_dialog() {
local name="$1" id X Y WIDTH HEIGHT cx cy
id="$(xdotool search --name "$name" 2>/dev/null | head -1 || true)"
[ -z "$id" ] && return 1
unset X Y WIDTH HEIGHT
eval "$(xdotool getwindowgeometry --shell "$id" 2>/dev/null || true)"
# All four must be present and numeric: an empty or stale read is how the stray click happened.
for v in "${X:-}" "${Y:-}" "${WIDTH:-}" "${HEIGHT:-}"; do
[[ "$v" =~ ^-?[0-9]+$ ]] || { echo " $name: unreadable geometry, not clicking"; return 1; }
done
cx=$((X + WIDTH - 11)); cy=$((Y - 31)) # openbox decoration: close box above the frame
if [ "$cx" -lt 0 ] || [ "$cy" -lt 0 ] || [ "$cx" -ge "$screen_w" ] || [ "$cy" -ge "$screen_h" ]; then
echo " $name: close box at ${cx},${cy} is off-screen, not clicking"; return 1
fi
echo " $name: closing via titlebar at ${cx},${cy}"
xdotool mousemove "$cx" "$cy" click 1
sleep 2
return 0
}
for name in "Setup Wizard" "New version"; do
for _ in 1 2 3; do close_dialog "$name" || break; done
done
# --- main window ----------------------------------------------------------------------------
main="$(xdotool search --name "Untitled" 2>/dev/null | head -1 || true)"
if [ -n "$main" ]; then
xdotool windowmove "$main" 0 0 windowsize "$main" ${GEOM/x/ } 2>/dev/null || true
xdotool windowactivate "$main" 2>/dev/null || true
sleep 2
fi
echo "--- session ---"
status