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 };
|
||||
}
|
||||
@@ -177,10 +177,16 @@ export function createDocker({ exec = spawnDocker, baseEnv = process.env, envDir
|
||||
return {
|
||||
run,
|
||||
async ps(label) {
|
||||
const { code, lines } = await capture(['ps', '--filter', `label=${label}`, '--format', '{{.Names}}']);
|
||||
const { code, lines } = await capture(['ps', '-a', '--filter', `label=${label}`, '--format', '{{.Names}}']);
|
||||
if (code !== 0) throw new Error(`docker ps fehlgeschlagen (${code})`);
|
||||
return lines.map((l) => l.trim()).filter((l) => NAME_RE.test(l));
|
||||
},
|
||||
// Laeuft der Container? (inspect; fehlt er oder ist beendet, dann false)
|
||||
async running(name) {
|
||||
checkName(name);
|
||||
const { code, lines } = await capture(['inspect', '-f', '{{.State.Running}}', name]);
|
||||
return code === 0 && lines.some((l) => l.trim() === 'true');
|
||||
},
|
||||
async rm(name) { checkName(name); return capture(['rm', '-f', name]); },
|
||||
async stop(name) { checkName(name); return capture(['rm', '-f', name]); },
|
||||
};
|
||||
|
||||
+6
-1
@@ -139,7 +139,12 @@ export function createJobs({ config, docker, fs: fsOver = {}, git, mem = readMem
|
||||
await gitFn({ repo: job.repo, branch: job.branch, dir: workDir, onLine, timeoutMs: Math.max(1000, deadline - now()) });
|
||||
const detected = detectFromDir(workDir, { mode: job.mode, index: job.index });
|
||||
if (detected.error || detected.kind === 'static' || !detected.install?.length) return { workDir, detected };
|
||||
const r = await containerStep({ job, id, step: 'install', cmd: detected.install, pm: detected.pm, workDir, cacheDir, onLine, deadline });
|
||||
let r = await containerStep({ job, id, step: 'install', cmd: detected.install, pm: detected.pm, workDir, cacheDir, onLine, deadline });
|
||||
// I8: `npm ci` scheitert bei nicht passender Lock-Datei; wie der bisherige Build-Weg auf `npm install` ausweichen.
|
||||
if (r.code !== 0 && detected.pm === 'npm' && detected.install[1] === 'ci') {
|
||||
onLine('npm ci fehlgeschlagen – Wiederholung mit npm install');
|
||||
r = await containerStep({ job, id, step: 'install2', cmd: ['npm', 'install', '--include=dev', '--no-audit', '--no-fund'], pm: detected.pm, workDir, cacheDir, onLine, deadline });
|
||||
}
|
||||
if (r.code !== 0) return { workDir, detected, error: failText('Installation', r.code, config.buildTimeoutMs) };
|
||||
return { workDir, detected };
|
||||
}
|
||||
|
||||
+2
-1
@@ -83,7 +83,8 @@ export function createProxy({ config, dev, request = http.request, upstreamTimeo
|
||||
});
|
||||
let timedOut = false;
|
||||
up.setTimeout(upstreamTimeoutMs, () => { timedOut = true; up.destroy(); });
|
||||
up.on('error', () => {
|
||||
up.on('error', (err) => {
|
||||
if (['ECONNREFUSED', 'ENOTFOUND', 'EHOSTUNREACH', 'EAI_AGAIN'].includes(err?.code)) dev.markDead?.(sub)?.catch?.(() => {});
|
||||
if (!res.headersSent) {
|
||||
const body = timedOut ? 'Die Preview antwortet nicht rechtzeitig (504).' : 'Die Preview ist gerade nicht erreichbar (502).';
|
||||
res.writeHead(timedOut ? 504 : 502, { 'Content-Type': 'text/plain; charset=utf-8', 'Content-Length': Buffer.byteLength(body) });
|
||||
|
||||
Reference in new issue
Block a user