Runner: Dev-Server Start/Abbruch-Korrekturen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
71f9243bf2
commit
6cd50bba9f
2 files changed
+103
-15
No files matched your search
+30
-9
@@ -8,6 +8,7 @@ import { readMemAvailable } from './server.js';
|
||||
const LOG_LINES = 300;
|
||||
const MAX_LINE = 2000;
|
||||
const POLL_MS = 1000;
|
||||
const RETRY_MS = 60e3;
|
||||
const UID = 1000;
|
||||
const PREFIX = 'preview-dev-';
|
||||
const cacheName = (repo) => repo.replace('/', '__');
|
||||
@@ -39,6 +40,7 @@ export function tcpConnect(host, port, timeoutMs = 2000) {
|
||||
export function createDev({
|
||||
config, docker, jobs, mem = readMemAvailable, now = Date.now, connect = tcpConnect,
|
||||
setTimer = (fn, ms) => setTimeout(fn, ms).unref?.() ?? 0, prepareDirs = defaultPrepareDirs,
|
||||
rmDir = (p) => nodeFs.rm(p, { recursive: true, force: true }),
|
||||
}) {
|
||||
const entries = new Map(); // sub ⇒ { sub, state, sha, lastUsed, log, port, job, release, promise, cancelled }
|
||||
const sleep = (ms) => new Promise((r) => setTimer(r, ms));
|
||||
@@ -56,8 +58,16 @@ export function createDev({
|
||||
async function shutdown(e, finalState) {
|
||||
e.cancelled = true;
|
||||
e.release?.(); e.release = null;
|
||||
if (finalState === 'off') entries.delete(e.sub); else { e.state = finalState; e.port = null; }
|
||||
try { await docker.rm(PREFIX + e.sub); } catch (err) { console.error('dev rm', e.sub, err); }
|
||||
if (finalState === 'off') { if (entries.get(e.sub) === e) entries.delete(e.sub); } else { e.state = finalState; e.port = null; }
|
||||
// Nur der Eintrag, der den Container angelegt hat, entfernt ihn (Name ist je sub geteilt). Läuft `docker run` noch,
|
||||
// räumt start() nach der Rückkehr selbst auf.
|
||||
if (!e.stopping) {
|
||||
e.stopping = (async () => {
|
||||
if (!e.created) return;
|
||||
try { const r = await docker.rm(PREFIX + e.sub); if (r && r.code) console.error('dev rm', e.sub, 'Exit-Code', r.code); } catch (err) { console.error('dev rm', e.sub, err); }
|
||||
})();
|
||||
}
|
||||
return e.stopping;
|
||||
}
|
||||
|
||||
async function start(e, job) {
|
||||
@@ -69,12 +79,14 @@ export function createDev({
|
||||
log(msg);
|
||||
e.release?.(); e.release = null;
|
||||
e.state = 'failed'; e.port = null;
|
||||
try { await docker.rm(name); } catch { /* egal */ }
|
||||
e.failedAt = now();
|
||||
if (e.created) { try { await docker.rm(name); } catch { /* egal */ } }
|
||||
};
|
||||
try {
|
||||
e.release = jobs.retainCache(job.repo);
|
||||
if (e.cancelled) return; // vor dem Start abgebrochen: nichts einreihen (ein laufendes prepareWorkspace ließe sich nicht abbrechen)
|
||||
const workDir = join(config.workRoot, 'dev', e.sub);
|
||||
const cacheDir = join(config.cacheRoot, cacheName(job.repo));
|
||||
// Bleibt ein Rest: ist prepareWorkspace schon gestartet, belegt es den Build-Slot bis zum Ende, auch nach Abbruch.
|
||||
const prep = await jobs.prepareWorkspace(job, { onLine: log, workDir });
|
||||
if (e.cancelled) return;
|
||||
const d = prep.detected;
|
||||
@@ -88,6 +100,7 @@ export function createDev({
|
||||
});
|
||||
// Feste Variable zuletzt, damit Projektvariablen sie nicht überschreiben; Werte laufen per --env-file (R10).
|
||||
const env = { ...job.env, __VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS: `.${config.baseHost}` };
|
||||
e.created = true;
|
||||
const r = await docker.run(args, { env, onLine: (l) => log(l) });
|
||||
if (e.cancelled) { await docker.rm(name).catch(() => {}); return; }
|
||||
if (r.code !== 0) return await fail(`Container konnte nicht gestartet werden (Exit-Code ${r.code})`);
|
||||
@@ -112,6 +125,7 @@ export function createDev({
|
||||
async function ensure(sub, job) {
|
||||
const cur = entries.get(sub);
|
||||
if (cur && cur.sha === job.sha) {
|
||||
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' }; }
|
||||
}
|
||||
@@ -119,17 +133,23 @@ export function createDev({
|
||||
try { free = mem(); } catch { /* Fehler = zu wenig Speicher */ }
|
||||
if (!(free >= config.minFreeBytes)) throw Object.assign(new Error('Server ausgelastet'), { status: 503 });
|
||||
const stops = [];
|
||||
if (cur) stops.push(shutdown(cur, 'sleeping')); // neuer sha oder failed/sleeping: frisch starten
|
||||
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: null, promise: null, cancelled: false };
|
||||
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 };
|
||||
entries.set(sub, e);
|
||||
e.promise = Promise.all(stops).then(() => start(e, job));
|
||||
e.promise = Promise.allSettled(stops).then(() => start(e, job));
|
||||
return { state: 'starting' };
|
||||
}
|
||||
|
||||
const touch = (sub) => { const e = entries.get(sub); if (e) e.lastUsed = now(); };
|
||||
async function stop(sub) { const e = entries.get(sub); if (e) await shutdown(e, 'off'); }
|
||||
async function stop(sub) {
|
||||
const e = entries.get(sub);
|
||||
if (!e) return;
|
||||
await shutdown(e, 'off');
|
||||
await e.promise.catch(() => {});
|
||||
if (!entries.has(sub)) await rmDir(join(config.workRoot, 'dev', sub)).catch(() => {}); // Arbeitsordner aufräumen
|
||||
}
|
||||
const list = () => [...entries.values()].map((e) => ({ sub: e.sub, state: e.state, lastUsed: e.lastUsed, sha: e.sha }));
|
||||
function status(sub) {
|
||||
const e = entries.get(sub);
|
||||
@@ -141,7 +161,8 @@ export function createDev({
|
||||
}
|
||||
// Zustand liegt nur im Speicher: nach Runner-Neustart übrig gebliebene Dev-Container entfernen.
|
||||
async function cleanupOrphans() {
|
||||
for (const n of await docker.ps('webklar.preview')) if (n.startsWith(PREFIX)) await docker.rm(n);
|
||||
const names = (await docker.ps('webklar.preview')).filter((n) => n.startsWith(PREFIX));
|
||||
for (const r of await Promise.allSettled(names.map((n) => docker.rm(n)))) if (r.status === 'rejected') console.error('orphan rm', r.reason);
|
||||
}
|
||||
const idle = () => Promise.all([...entries.values()].map((e) => e.promise)).then(() => {});
|
||||
|
||||
|
||||
Reference in new issue
Block a user