Fixed reconnect
Docker image / build-and-push (push) Successful in 3m3s

This commit is contained in:
ab
2026-07-22 17:35:23 +03:00
parent 571707c109
commit 33e01a4a8f
5 changed files with 93 additions and 2 deletions
+82
View File
@@ -7,11 +7,17 @@ const { EventEmitter } = require('events');
const SteamUser = require('steam-user'); const SteamUser = require('steam-user');
const protobuf = require('protobufjs'); const protobuf = require('protobufjs');
const APP_VERSION = require('./package.json').version;
const DOTA_APPID = 570; const DOTA_APPID = 570;
const DEFAULT_PORT = 3000; const DEFAULT_PORT = 3000;
const DEFAULT_REFRESH_MS = 3 * 60 * 1000; const DEFAULT_REFRESH_MS = 3 * 60 * 1000;
const READY_TIMEOUT_MS = 60 * 1000; const READY_TIMEOUT_MS = 60 * 1000;
const LOBBY_TIMEOUT_MS = 30 * 1000; const LOBBY_TIMEOUT_MS = 30 * 1000;
const WATCHDOG_INTERVAL_MS = 30 * 1000;
const GC_RELAUNCH_AFTER_MS = 2 * 60 * 1000;
const GC_RELOG_AFTER_MS = 10 * 60 * 1000;
const STEAM_STUCK_AFTER_MS = 10 * 60 * 1000;
const AUTH_FILE = process.env.STEAM_AUTH_FILE || path.join(__dirname, '.steam-auth.json'); const AUTH_FILE = process.env.STEAM_AUTH_FILE || path.join(__dirname, '.steam-auth.json');
const CREDENTIALS_FILE = process.env.STEAM_CREDENTIALS_FILE || path.join(path.dirname(AUTH_FILE), '.steam-credentials.json'); const CREDENTIALS_FILE = process.env.STEAM_CREDENTIALS_FILE || path.join(path.dirname(AUTH_FILE), '.steam-credentials.json');
const ADMIN_PASSWORD = process.env.AUTH_ADMIN_PASSWORD || process.env.ADMIN_PASSWORD || ''; const ADMIN_PASSWORD = process.env.AUTH_ADMIN_PASSWORD || process.env.ADMIN_PASSWORD || '';
@@ -263,6 +269,11 @@ class DotaLobbyClient extends EventEmitter {
this.pendingLobbyRequest = null; this.pendingLobbyRequest = null;
this.steamGuardPrompt = null; this.steamGuardPrompt = null;
this.steamGuardCallback = null; this.steamGuardCallback = null;
this.gcDownSince = null;
this.steamDownSince = null;
this.lastGcRelaunchAt = 0;
this.watchdogTimer = setInterval(() => this.watchdog(), WATCHDOG_INTERVAL_MS);
this.watchdogTimer.unref();
this.createSteamUser(); this.createSteamUser();
} }
@@ -482,6 +493,72 @@ class DotaLobbyClient extends EventEmitter {
this.emit('change'); this.emit('change');
} }
// Periodic self-healing: the GC can silently drop our session (e.g. GC
// maintenance restarts) while hello messages keep going into the void.
// Escalate: relaunch Dota after GC_RELAUNCH_AFTER_MS, then tear down and
// rebuild the whole Steam connection after GC_RELOG_AFTER_MS. Also rebuilds
// the connection if Steam itself stays offline longer than STEAM_STUCK_AFTER_MS.
watchdog() {
const now = Date.now();
if (this.steamGuardCallback) {
// Waiting on user input; nothing to heal automatically.
this.gcDownSince = null;
this.steamDownSince = null;
return;
}
if (!this.loggedOn) {
this.gcDownSince = null;
if (!this.steamDownSince) {
this.steamDownSince = now;
return;
}
if (now - this.steamDownSince >= STEAM_STUCK_AFTER_MS) {
this.steamDownSince = now;
console.warn('watchdog: Steam connection stuck offline, rebuilding session...');
this.hardReset();
}
return;
}
this.steamDownSince = null;
if (this.gcReady) {
this.gcDownSince = null;
return;
}
if (!this.gcDownSince) {
this.gcDownSince = now;
return;
}
if (now - this.gcDownSince >= GC_RELOG_AFTER_MS) {
this.gcDownSince = now;
console.warn('watchdog: GC unavailable despite relaunches, rebuilding Steam session...');
this.hardReset();
return;
}
if (now - this.gcDownSince >= GC_RELAUNCH_AFTER_MS && now - this.lastGcRelaunchAt >= GC_RELAUNCH_AFTER_MS) {
this.lastGcRelaunchAt = now;
console.warn('watchdog: GC not ready, relaunching Dota 2...');
this.clearHelloTimer();
this.launchDota(true);
}
}
hardReset() {
this.gcDownSince = null;
this.resetSteamUser();
try {
this.start();
} catch (err) {
console.error(`watchdog: could not restart Steam connection: ${err.message}`);
}
}
scheduleReconnect() { scheduleReconnect() {
if (this.reconnectTimer) return; if (this.reconnectTimer) return;
this.reconnectTimer = setTimeout(() => { this.reconnectTimer = setTimeout(() => {
@@ -569,6 +646,10 @@ class DotaLobbyClient extends EventEmitter {
this.readyWaiters.delete(waiter); this.readyWaiters.delete(waiter);
this.gcReady = false; this.gcReady = false;
this.setGcState('timeout'); this.setGcState('timeout');
if (this.loggedOn) {
this.clearHelloTimer();
this.launchDota(true);
}
reject(new Error('Dota 2 game coordinator did not become ready in time.')); reject(new Error('Dota 2 game coordinator did not become ready in time.'));
}, timeoutMs), }, timeoutMs),
}; };
@@ -741,6 +822,7 @@ let activeRefresh = null;
function publicState() { function publicState() {
return { return {
...state, ...state,
version: APP_VERSION,
connection: dota.status(), connection: dota.status(),
refreshIntervalMs, refreshIntervalMs,
regions: REGIONS, regions: REGIONS,
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "doka-lobby", "name": "doka-lobby",
"version": "1.0.0", "version": "1.1.0",
"private": true, "private": true,
"description": "List joinable Dota 2 custom game lobbies, filtered by game name / map", "description": "List joinable Dota 2 custom game lobbies, filtered by game name / map",
"main": "index.js", "main": "index.js",
+2
View File
@@ -1,5 +1,6 @@
const els = { const els = {
statusBadge: document.querySelector('#statusBadge'), statusBadge: document.querySelector('#statusBadge'),
appVersion: document.querySelector('#appVersion'),
serverFilter: document.querySelector('#serverFilter'), serverFilter: document.querySelector('#serverFilter'),
visibleCount: document.querySelector('#visibleCount'), visibleCount: document.querySelector('#visibleCount'),
totalCount: document.querySelector('#totalCount'), totalCount: document.querySelector('#totalCount'),
@@ -213,6 +214,7 @@ function render() {
const [label, klass] = statusInfo(data); const [label, klass] = statusInfo(data);
setBadge(label, klass); setBadge(label, klass);
els.appVersion.textContent = data?.version ? `v${data.version}` : '';
els.visibleCount.textContent = String(filtered.length); els.visibleCount.textContent = String(filtered.length);
els.totalCount.textContent = String(data?.totalLobbies || lobbies.length || 0); els.totalCount.textContent = String(data?.totalLobbies || lobbies.length || 0);
els.lastUpdated.textContent = formatDate(data?.lastUpdatedAt); els.lastUpdated.textContent = formatDate(data?.lastUpdatedAt);
+1 -1
View File
@@ -9,7 +9,7 @@
<body> <body>
<header class="topbar"> <header class="topbar">
<div class="title"> <div class="title">
<h1>Dota 2 lobbies</h1> <h1>Dota 2 lobbies <span id="appVersion" class="version"></span></h1>
<div class="subline"> <div class="subline">
<span id="statusBadge" class="badge badge-muted">starting</span> <span id="statusBadge" class="badge badge-muted">starting</span>
<span id="serverFilter">GC: Auto</span> <span id="serverFilter">GC: Auto</span>
+7
View File
@@ -55,6 +55,13 @@ select {
letter-spacing: 0; letter-spacing: 0;
} }
.title .version {
font-size: 12px;
font-weight: 400;
color: var(--muted);
vertical-align: middle;
}
.subline { .subline {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;