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:
1 parent
cd9a6e0cf1
commit
96c26fb9b8
8 files changed
+139
-10
No files matched your search
+21
-5
@@ -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 };
|
||||
}
|
||||
Reference in new issue
Block a user