diff --git a/streaming-agent/gst_agent.py b/streaming-agent/gst_agent.py index b326563..7ab7505 100755 --- a/streaming-agent/gst_agent.py +++ b/streaming-agent/gst_agent.py @@ -35,7 +35,7 @@ from gi.repository import Gst, GstWebRTC, GstSdp, GLib import requests import websockets -logging.basicConfig(level=logging.INFO, format='[gst_agent] %(message)s', stream=sys.stderr) +logging.basicConfig(level=logging.INFO, format='%(asctime)s [gst_agent] %(message)s', stream=sys.stderr) log = logging.getLogger(__name__) # ── Configuration ───────────────────────────────────────────────────────────── @@ -345,16 +345,33 @@ class CSSStreamAgent: def css_watchdog(): # If cstrike_win64.exe crashes (e.g. Wine c0000409 stack overrun), the # stream keeps running but shows a black screen forever with no recovery. - # This watchdog detects the crash and relaunches CSS directly via revLoader, - # the same way start.sh does on initial boot. gst_agent keeps running and - # will simply start capturing the relaunched game once it renders. + # This watchdog detects the crash and triggers a restart. + # + # IMPORTANT: this must NOT relaunch CSS directly itself. Doing so + # (a bare `wine revLoader.exe` Popen call) created a second, uncoordinated + # restart authority alongside the signaling server's own kill+relaunch + # logic (manual button / scheduled 5h restart). If both fired close + # together, two separate Wine processes could end up racing to launch + # CSS, producing duplicate/orphaned windows — xdotool's window search + # would then target an arbitrary one, causing keyboard input to land on + # a stale/backgrounded window while video (which captures the whole X + # display, not a specific window) kept working fine. That mismatch is + # exactly the "mouse/video fine, keyboard dead" symptom that was + # reported after a watchdog relaunch overlapped with a manual restart. + # + # So instead we just notify the signaling server, which has the single + # authoritative kill+relaunch implementation (performCssRestart) shared + # by the manual restart button, the scheduled 5-hour restart, and this + # watchdog — never more than one restart in flight at a time. # # We only start watching once CSS has been seen alive at least once, to # avoid racing its initial startup. Require several consecutive misses to - # avoid false positives during a normal controller-triggered relaunch - # (the signaling server kills+relaunches CSS on controller change). - home = os.path.expanduser('~') - revloader = os.path.join(home, 'Counter-Strike Source', 'revLoader.exe') + # avoid false positives during a normal controller-triggered handoff. + restart_url = SIG_URL.replace('ws://', 'http://').replace('wss://', 'https://') + if not restart_url.endswith('/'): + restart_url += '/' + restart_url += 'webclient-force-restart' + seen_alive = False missing_count = 0 while True: @@ -381,17 +398,14 @@ def css_watchdog(): # 4 consecutive misses (~20s) — long enough to not collide with the # signaling server's own kill+relaunch cycle on controller change. if missing_count >= 4: - log.error('CSS appears crashed — relaunching revLoader') + log.error('CSS appears crashed — requesting restart from signaling server') try: - env = dict(os.environ, DISPLAY=DISPLAY) - subprocess.Popen(['wine', revloader], env=env, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL) - log.info('revLoader relaunched by watchdog') + resp = requests.post(restart_url, timeout=5) + log.info(f'restart request sent to {restart_url}, status={resp.status_code}') except Exception as e: - log.error(f'watchdog relaunch failed: {e}') - # Reset so we give the relaunched game time to come up before - # counting misses again. + log.error(f'watchdog restart request failed: {e}') + # Reset so we give the restart time to complete before counting + # misses again. seen_alive = False missing_count = 0 diff --git a/streaming-agent/signaling-server.js b/streaming-agent/signaling-server.js index 046cbbe..72583ba 100644 --- a/streaming-agent/signaling-server.js +++ b/streaming-agent/signaling-server.js @@ -10,6 +10,14 @@ 'use strict'; +// Prefix every console.log / console.error line with an ISO timestamp. +// Wrapping here means every existing log call site automatically gets a +// timestamp without needing to be touched individually. +const _origLog = console.log; +const _origError = console.error; +console.log = (...args) => _origLog(`[${new Date().toISOString()}]`, ...args); +console.error = (...args) => _origError(`[${new Date().toISOString()}]`, ...args); + const express = require('express'); const http = require('http'); const WebSocket = require('ws'); @@ -263,6 +271,25 @@ function performCssRestart(reason) { } containerRestarting = false; broadcastQueueState(); + + // Explicitly give the newly-created CSS window keyboard focus once it + // has had time to map. Mouse clicks/movement always work after a + // restart because they target whatever window is physically under the + // cursor — but keyboard input goes to whichever window holds X11 input + // focus, which is separate state that does NOT automatically transfer + // to a freshly-relaunched window after a kill+relaunch (as opposed to a + // genuinely fresh X session on container boot). Without this, keyboard + // input can silently keep targeting a stale/dead window reference while + // everything else on the new window works fine — this was very likely + // 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`); + console.log('[sig] CSS restart: focused new window'); + } catch (e) { + console.error('[sig] CSS restart: failed to focus new window:', e.message); + } + }, 8000); }, 2000); }