fix(runner): Dev-Neustart bei geänderten Variablen, Liveness-Prüfung, npm-ci-Fallback

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-09-30 21:54:01 +00:00
1 parent cd9a6e0cf1
commit 96c26fb9b8
8 files changed
+139 -10

No files matched your search

+21 -5
View File
@@ -1,5 +1,6 @@
// Dev-Server bei Bedarf: Container mit `npm run dev` starten, bei Leerlauf stoppen, max. devMax gleichzeitig (LRU-Verdrängung).
import net from 'node:net';
import { createHash } from 'node:crypto';
import { join } from 'node:path';
import * as nodeFs from 'node:fs/promises';
import { devRunArgs } from './docker.js';
@@ -11,6 +12,9 @@ const POLL_MS = 1000;
const RETRY_MS = 60e3;
const UID = 1000;
const PREFIX = 'preview-dev-';
const LIVE_CHECK_MS = 10e3;
// I2: Stand der Startparameter (Variablen, Branch, Modus): Aenderung => Neustart
const cfgHash = (job) => createHash('sha256').update(JSON.stringify([job.branch, job.mode, Object.entries(job.env || {}).sort(([a], [b]) => (a < b ? -1 : 1))])).digest('hex');
const cacheName = (repo) => repo.replace('/', '__');
async function chownR(p) {
@@ -126,7 +130,14 @@ export function createDev({
async function ensure(sub, job) {
specs.set(sub, job);
const cur = entries.get(sub);
if (cur && cur.sha === job.sha) {
// I7: ein abgestuerzter/beendeter Container darf nicht als online gelten
if (cur && cur.state === 'online' && docker.running && now() - (cur.checkedAt || 0) >= LIVE_CHECK_MS) {
let alive = true;
try { alive = await docker.running(PREFIX + sub); } catch { /* unklar: als lebendig behandeln */ }
cur.checkedAt = now();
if (!alive && entries.get(sub) === cur && cur.state === 'online') { console.error('dev', sub, 'Container laeuft nicht mehr - Neustart'); await shutdown(cur, 'sleeping'); }
}
if (cur && cur.sha === job.sha && cur.cfg === cfgHash(job)) {
if (cur.state === 'failed' && now() - cur.failedAt < RETRY_MS) return { state: 'failed' }; // nicht öfter als alle 60 s neu versuchen
if (cur.state === 'online') { cur.lastUsed = now(); return { state: 'online', target: `http://${PREFIX}${sub}:${cur.port}` }; }
if (cur.state === 'starting') { cur.lastUsed = now(); return { state: 'starting' }; }
@@ -138,7 +149,7 @@ export function createDev({
if (cur) stops.push(shutdown(cur, 'sleeping'), cur.promise); // neuer sha oder failed/sleeping: frisch starten, erst nach dem alten Eintrag
const others = [...entries.values()].filter((e) => e !== cur && isRunning(e)).sort((a, b) => a.lastUsed - b.lastUsed);
while (others.length >= config.devMax) stops.push(shutdown(others.shift(), 'sleeping')); // LRU
const e = { sub, state: 'starting', sha: job.sha, lastUsed: now(), log: [], port: null, release: jobs.retainCache(job.repo), promise: null, cancelled: false, created: false, stopping: null, failedAt: 0 };
const e = { sub, state: 'starting', sha: job.sha, cfg: cfgHash(job), checkedAt: now(), lastUsed: now(), log: [], port: null, release: jobs.retainCache(job.repo), promise: null, cancelled: false, created: false, stopping: null, failedAt: 0 };
entries.set(sub, e);
e.promise = Promise.allSettled(stops).then(() => start(e, job));
return { state: 'starting' };
@@ -147,9 +158,9 @@ export function createDev({
// R12: Spezifikation merken, ohne zu starten. Läuft der sub mit anderem sha, wird nach den üblichen Regeln neu gestartet.
async function register(sub, job) {
const cur = entries.get(sub);
if (cur && isRunning(cur) && cur.sha !== job.sha) return ensure(sub, job);
if (cur && isRunning(cur) && (cur.sha !== job.sha || cur.cfg !== cfgHash(job))) return ensure(sub, job);
specs.set(sub, job);
if (cur && cur.sha === job.sha && (cur.state === 'online' || cur.state === 'starting' || cur.state === 'failed')) return { state: cur.state };
if (cur && cur.sha === job.sha && cur.cfg === cfgHash(job) && (cur.state === 'online' || cur.state === 'starting' || cur.state === 'failed')) return { state: cur.state };
return { state: 'sleeping' };
}
// R12: Start mit der gemerkten Spezifikation (Proxy kennt nur die Subdomain).
@@ -159,6 +170,11 @@ export function createDev({
return ensure(sub, job);
}
// I7: Proxy meldet einen nicht erreichbaren Container: als schlafend markieren und entfernen (naechster Aufruf startet neu).
async function markDead(sub) {
const e = entries.get(sub);
if (e && e.state === 'online') await shutdown(e, 'sleeping');
}
const touch = (sub) => { const e = entries.get(sub); if (e) e.lastUsed = now(); };
async function stop(sub) {
specs.delete(sub); // 'off': Spezifikation vergessen, bis das Portal neu registriert
@@ -184,5 +200,5 @@ export function createDev({
}
const idle = () => Promise.all([...entries.values()].map((e) => e.promise)).then(() => {});
return { ensure, register, wake, touch, stop, list, status, sweep, cleanupOrphans, idle };
return { ensure, register, wake, touch, markDead, stop, list, status, sweep, cleanupOrphans, idle };
}