hopefully does not lose keyboard control any more for users

This commit is contained in:
jenz 2026-07-26 14:09:55 +01:00
parent a7b4f04118
commit d3baa7e050
2 changed files with 58 additions and 17 deletions

View File

@ -35,7 +35,7 @@ from gi.repository import Gst, GstWebRTC, GstSdp, GLib
import requests import requests
import websockets 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__) log = logging.getLogger(__name__)
# ── Configuration ───────────────────────────────────────────────────────────── # ── Configuration ─────────────────────────────────────────────────────────────
@ -345,16 +345,33 @@ class CSSStreamAgent:
def css_watchdog(): def css_watchdog():
# If cstrike_win64.exe crashes (e.g. Wine c0000409 stack overrun), the # If cstrike_win64.exe crashes (e.g. Wine c0000409 stack overrun), the
# stream keeps running but shows a black screen forever with no recovery. # stream keeps running but shows a black screen forever with no recovery.
# This watchdog detects the crash and relaunches CSS directly via revLoader, # This watchdog detects the crash and triggers a restart.
# the same way start.sh does on initial boot. gst_agent keeps running and #
# will simply start capturing the relaunched game once it renders. # 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 # 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 racing its initial startup. Require several consecutive misses to
# avoid false positives during a normal controller-triggered relaunch # avoid false positives during a normal controller-triggered handoff.
# (the signaling server kills+relaunches CSS on controller change). restart_url = SIG_URL.replace('ws://', 'http://').replace('wss://', 'https://')
home = os.path.expanduser('~') if not restart_url.endswith('/'):
revloader = os.path.join(home, 'Counter-Strike Source', 'revLoader.exe') restart_url += '/'
restart_url += 'webclient-force-restart'
seen_alive = False seen_alive = False
missing_count = 0 missing_count = 0
while True: while True:
@ -381,17 +398,14 @@ def css_watchdog():
# 4 consecutive misses (~20s) — long enough to not collide with the # 4 consecutive misses (~20s) — long enough to not collide with the
# signaling server's own kill+relaunch cycle on controller change. # signaling server's own kill+relaunch cycle on controller change.
if missing_count >= 4: if missing_count >= 4:
log.error('CSS appears crashed — relaunching revLoader') log.error('CSS appears crashed — requesting restart from signaling server')
try: try:
env = dict(os.environ, DISPLAY=DISPLAY) resp = requests.post(restart_url, timeout=5)
subprocess.Popen(['wine', revloader], env=env, log.info(f'restart request sent to {restart_url}, status={resp.status_code}')
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
log.info('revLoader relaunched by watchdog')
except Exception as e: except Exception as e:
log.error(f'watchdog relaunch failed: {e}') log.error(f'watchdog restart request failed: {e}')
# Reset so we give the relaunched game time to come up before # Reset so we give the restart time to complete before counting
# counting misses again. # misses again.
seen_alive = False seen_alive = False
missing_count = 0 missing_count = 0

View File

@ -10,6 +10,14 @@
'use strict'; '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 express = require('express');
const http = require('http'); const http = require('http');
const WebSocket = require('ws'); const WebSocket = require('ws');
@ -263,6 +271,25 @@ function performCssRestart(reason) {
} }
containerRestarting = false; containerRestarting = false;
broadcastQueueState(); 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); }, 2000);
} }