diff --git a/start.sh b/start.sh index f70d591..eee674c 100755 --- a/start.sh +++ b/start.sh @@ -1,5 +1,4 @@ #!/bin/bash -set -euo pipefail # ── Clean up stale sockets from previous run ────────────────────────────────── rm -f /tmp/.X99-lock @@ -117,6 +116,7 @@ echo "socat mic listener started on TCP :12345" # gst_agent exits with code 1 on ICE failure or other fatal errors. # Without a restart loop, the container sits dead until manually restarted. # 3 second delay before each restart to avoid a tight crash loop. +# while true; do python3 ~/streaming-agent/gst_agent.py echo "[gst_agent] exited with code $?, restarting in 3s..." diff --git a/streaming-agent/signaling-server.js b/streaming-agent/signaling-server.js index c4cc981..7d5996f 100644 --- a/streaming-agent/signaling-server.js +++ b/streaming-agent/signaling-server.js @@ -252,17 +252,6 @@ function performCssRestart(reason) { containerRestarting = true; broadcastQueueState(); - // Use Wine's own proper teardown instead of pkill + a fixed sleep guess. - // `wineserver -k` signals every Wine process under this prefix to exit; - // `wineserver -w` BLOCKS until wineserver has actually fully released all - // its shared memory / locks / sockets, rather than gambling on a fixed - // delay being long enough. This is what previously let a fresh CSS - // instance start before the old one had genuinely finished tearing down, - // inheriting a wedged state and landing right back on the same stuck - // "Connecting to server..." screen even after repeated restarts. - // Wrapped in `timeout` so a manual restart can never hang indefinitely if - // wineserver somehow never exits — after 15s we give up waiting and - // proceed to relaunch anyway. try { execSync(`docker exec ${CONTAINER} bash -c "wineserver -k; timeout 15 wineserver -w || true"`); console.log('[sig] CSS restart: wineserver fully torn down'); @@ -296,7 +285,14 @@ function performCssRestart(reason) { // the actual cause of "keyboard stuck, mouse/video fine" after restarts. setTimeout(() => { try { - execSync(`docker exec ${CONTAINER} bash -c "DISPLAY=:99 WIN=\$(xdotool search --name '' | head -1) && xdotool windowfocus --sync \$WIN" || true`); + // A fresh `docker exec` session gets a NEW environment built from + // the image's defaults, not the runtime environment of the + // container's PID 1 (start.sh) — unlike gst_agent.py, which + // inherits start.sh's environment directly as a child process and + // has always found xdotool fine. Explicitly ensure common binary + // paths are present here too, since this exact command was + // silently failing with "xdotool: not found" every time. + execSync(`docker exec ${CONTAINER} bash -c "export PATH=\$PATH:/usr/bin:/usr/local/bin:/bin; DISPLAY=:99 WIN=\$(xdotool search --name '' | head -1) && xdotool windowfocus --sync \$WIN" || true`); console.log('[sig] CSS restart: focused new window'); } catch (e) { console.error('[sig] CSS restart: failed to focus new window:', e.message); @@ -313,6 +309,49 @@ function performCssRestart(reason) { const CSS_RESTART_INTERVAL = 5 * 60 * 60 * 1000; // 5 hours setInterval(() => performCssRestart('scheduled 5-hour restart'), CSS_RESTART_INTERVAL); +// ── Container liveness check ────────────────────────────────────────────────── +// Everything above (performCssRestart, the watchdog, the manual restart +// button) assumes the DOCKER CONTAINER itself is alive and only needs its +// internal Wine/CSS process cycled. If the container's own main process +// (start.sh, PID 1) dies for any reason, the whole container exits — at +// which point every docker exec call above fails outright with "container +// is not running", +// +const CONTAINER_LIVENESS_INTERVAL = 30 * 1000; // 30 seconds + +function checkContainerLiveness() { + let running = false; + try { + const out = execSync(`docker inspect -f "{{.State.Running}}" ${CONTAINER}`).toString().trim(); + running = out === 'true'; + } catch (e) { + running = false; + } + + if (!running) { + console.error(`[sig] CONTAINER ${CONTAINER} IS NOT RUNNING — attempting docker start`); + try { + execSync(`docker start ${CONTAINER}`); + console.log(`[sig] docker start ${CONTAINER} succeeded — container should reboot via start.sh`); + } catch (e) { + console.error(`[sig] docker start ${CONTAINER} failed:`, e.message); + } + + // The old agent registration and controller state are now stale — the + // container is rebooting from scratch (fresh gst_agent, fresh CSS). + // Clear them so the browser doesn't sit on a dead session waiting for + // an agent that will never re-register under the old WebSocket. + agentSessions.clear(); + if (currentController) { + releaseControl(currentController, 'container-restart'); + } + containerRestarting = true; + broadcastQueueState(); + } +} + +setInterval(checkContainerLiveness, CONTAINER_LIVENESS_INTERVAL); + function releaseControl(entry, reason) { if (idleTimer) { clearTimeout(idleTimer); idleTimer = null; } if (currentController !== entry) return; @@ -441,6 +480,18 @@ wss.on('connection', (ws, req) => { agentWs: ws, }); console.log(`[sig] agent registered — sessionId=${agentSessionId}`); + + // A fresh agent registering is the actual completion signal for a + // full container reboot (checkContainerLiveness sets + // containerRestarting=true immediately after `docker start`, but that + // only kicks off the boot — the full start.sh sequence, Weston, + // Xwayland, PipeWire, CSS launch, then gst_agent itself starting and + // registering, takes much longer than a same-container CSS restart). + // Clear it here so the browser's loading overlay doesn't stay stuck. + if (containerRestarting) { + containerRestarting = false; + broadcastQueueState(); + } } else if (msg.type === 'request_control') {