This commit is contained in:
@@ -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
@@ -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",
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user