homegames-common 1.5.2 → 1.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/game-session-manager.js +78 -2
- package/package.json +1 -1
package/game-session-manager.js
CHANGED
|
@@ -84,7 +84,7 @@ class GameSessionManager {
|
|
|
84
84
|
this.dockerImageName = opts.dockerImageName || 'homegames-runner';
|
|
85
85
|
this.childServerPath = opts.childServerPath || null;
|
|
86
86
|
this.gracePeriodMs = opts.gracePeriodMs || 30000;
|
|
87
|
-
this.lifecycleCheckMs = opts.lifecycleCheckMs ||
|
|
87
|
+
this.lifecycleCheckMs = opts.lifecycleCheckMs || 3000;
|
|
88
88
|
this.forgejo = opts.forgejo || {};
|
|
89
89
|
this.saveDataRoot = opts.saveDataRoot || path.join(os.tmpdir(), 'hg-save-data');
|
|
90
90
|
this.assetCachePath = opts.assetCachePath || null;
|
|
@@ -368,6 +368,8 @@ class GameSessionManager {
|
|
|
368
368
|
execArgv: ['--max-old-space-size=64'],
|
|
369
369
|
});
|
|
370
370
|
|
|
371
|
+
this._attachForkLogForwarding(child);
|
|
372
|
+
|
|
371
373
|
child.send(JSON.stringify({
|
|
372
374
|
key: input.gameKey || path.basename(gamePath, '.js'),
|
|
373
375
|
squishVersion,
|
|
@@ -433,6 +435,38 @@ class GameSessionManager {
|
|
|
433
435
|
});
|
|
434
436
|
}
|
|
435
437
|
|
|
438
|
+
// -----------------------------------------------------------------------
|
|
439
|
+
// Forward forked child stdout/stderr to the parent terminal.
|
|
440
|
+
// Docker sessions are excluded — use GET /sessions/:id/logs or docker logs.
|
|
441
|
+
// -----------------------------------------------------------------------
|
|
442
|
+
_attachForkLogForwarding(child) {
|
|
443
|
+
const emitLine = (line) => {
|
|
444
|
+
if (!line) return;
|
|
445
|
+
console.log(`session log: ${line}`);
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
const attachStream = (stream) => {
|
|
449
|
+
if (!stream) return;
|
|
450
|
+
let buffer = '';
|
|
451
|
+
stream.on('data', (chunk) => {
|
|
452
|
+
buffer += chunk.toString();
|
|
453
|
+
const lines = buffer.split('\n');
|
|
454
|
+
buffer = lines.pop() || '';
|
|
455
|
+
for (const line of lines) {
|
|
456
|
+
emitLine(line);
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
stream.on('end', () => {
|
|
460
|
+
if (buffer.length > 0) {
|
|
461
|
+
emitLine(buffer);
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
attachStream(child.stdout);
|
|
467
|
+
attachStream(child.stderr);
|
|
468
|
+
}
|
|
469
|
+
|
|
436
470
|
// -----------------------------------------------------------------------
|
|
437
471
|
// Find the project root (nearest ancestor with node_modules or package.json)
|
|
438
472
|
// so we mount enough of the tree for relative requires to work.
|
|
@@ -525,10 +559,30 @@ class GameSessionManager {
|
|
|
525
559
|
|
|
526
560
|
if (s.type === 'docker') {
|
|
527
561
|
const running = await isContainerRunning(s.containerId);
|
|
528
|
-
this.log.info(`Session ${sessionId} lifecycle check: container running = ${running}`);
|
|
529
562
|
if (!running) {
|
|
530
563
|
this.log.info(`Session ${sessionId} container exited`);
|
|
531
564
|
this._cleanupSession(sessionId);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Check player count — stop container if empty past grace period
|
|
569
|
+
try {
|
|
570
|
+
const healthData = await this._querySessionHealth(s.port);
|
|
571
|
+
const playerCount = healthData && healthData.playerCount !== undefined ? healthData.playerCount : -1;
|
|
572
|
+
if (playerCount === 0) {
|
|
573
|
+
s._emptyTicks++;
|
|
574
|
+
const emptyMs = s._emptyTicks * this.lifecycleCheckMs;
|
|
575
|
+
if (emptyMs >= this.gracePeriodMs) {
|
|
576
|
+
this.log.info(`Session ${sessionId} empty for ${emptyMs}ms, stopping`);
|
|
577
|
+
await stopContainer(s.containerId);
|
|
578
|
+
this._cleanupSession(sessionId);
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
} else if (playerCount > 0) {
|
|
582
|
+
s._emptyTicks = 0;
|
|
583
|
+
}
|
|
584
|
+
} catch (e) {
|
|
585
|
+
// Health check failed — container might be starting up, ignore
|
|
532
586
|
}
|
|
533
587
|
} else if (s.type === 'fork') {
|
|
534
588
|
// For fork sessions, send heartbeat. The child_game_server.js
|
|
@@ -559,6 +613,28 @@ class GameSessionManager {
|
|
|
559
613
|
this._cleanupSession(sessionId);
|
|
560
614
|
}
|
|
561
615
|
|
|
616
|
+
_querySessionHealth(port) {
|
|
617
|
+
return new Promise((resolve) => {
|
|
618
|
+
const req = http.request({
|
|
619
|
+
hostname: 'localhost',
|
|
620
|
+
port,
|
|
621
|
+
path: '/health',
|
|
622
|
+
method: 'GET',
|
|
623
|
+
timeout: 2000,
|
|
624
|
+
}, (res) => {
|
|
625
|
+
let data = '';
|
|
626
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
627
|
+
res.on('end', () => {
|
|
628
|
+
try { resolve(JSON.parse(data)); }
|
|
629
|
+
catch (e) { resolve(null); }
|
|
630
|
+
});
|
|
631
|
+
});
|
|
632
|
+
req.on('error', () => resolve(null));
|
|
633
|
+
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
634
|
+
req.end();
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
|
|
562
638
|
_cleanupSession(sessionId) {
|
|
563
639
|
const session = this.sessions[sessionId];
|
|
564
640
|
if (!session) return;
|