new destruction flow

This commit is contained in:
2026-09-17 16:57:08 -04:00
parent 5f8e03c693
commit a611850649
13 changed files with 349 additions and 96 deletions
+52 -14
View File
@@ -1,3 +1,4 @@
import { rconGetPlayerCount } from '../integrations/rcon.js';
import { beginDeployment, deploymentStage, finishDeployment } from './deployments.js';
import { mkdir, chmod, rm, writeFile } from 'node:fs/promises';
import { dirname, join } from 'node:path';
@@ -334,19 +335,56 @@ async function applyDeploymentTags(deployment: string, gameTagName: string, mapT
}
const destroying = new Set<string>();
export async function destroyServer(name: string): Promise<DestroyServerResult> {
if (creating || destroying.has(name)) return { ok: false, kind: 'komodo', message: 'A server operation is already in progress. Retry shortly.', warnings: [] };
// Persist the request before returning so navigation and client disconnects cannot lose it.
export function destroyServer(name: string): DestroyServerResult & { deploymentId?: string } {
const existing = db().prepare("SELECT id FROM server_deployments WHERE server = ? AND action = 'destroy' AND state IN ('waiting', 'running')").get(name) as { id: string } | undefined;
if (existing) return { ok: true, warnings: [], deploymentId: existing.id };
if (!getServer(name)) return { ok: false, kind: 'not-found', message: 'Server not found', warnings: [] };
const operation = beginDeployment(name, 'destroy');
destroying.add(name);
try {
const result = await destroyServerUnlocked(name, operation);
finishDeployment(operation, result);
return result;
} catch (err) {
finishDeployment(operation, { ok: false, message: (err as Error).message });
throw err;
} finally { destroying.delete(name); }
const operation = db().transaction(() => {
const id = beginDeployment(name, 'destroy');
db().prepare("UPDATE server_deployments SET state = 'waiting' WHERE id = ?").run(id);
deploymentStage(id, 'Waiting to verify the server is empty');
return id;
})();
return { ok: true, warnings: [], deploymentId: operation };
}
let destructionTick: Promise<void> | null = null;
export function processDestructions(): Promise<void> {
if (destructionTick) return destructionTick;
destructionTick = runDestructions().finally(() => { destructionTick = null; });
return destructionTick;
}
async function runDestructions(): Promise<void> {
const queued = db().prepare("SELECT id, server FROM server_deployments WHERE action = 'destroy' AND state = 'waiting' ORDER BY started_at").all() as { id: string; server: string }[];
for (const operation of queued) {
if (creating || destroying.has(operation.server)) continue;
destroying.add(operation.server);
try {
if (!getServer(operation.server)) {
finishDeployment(operation.id, { ok: false, message: 'Server is no longer registered' });
continue;
}
// A fresh successful zero is required; cached occupancy and timeouts are not evidence of emptiness.
const players = await rconGetPlayerCount(operation.server);
if (players !== 0) {
deploymentStage(operation.id, players === null
? 'Waiting for player count: server could not be reached'
: 'Waiting for server to empty: ' + players + ' player(s) online');
continue;
}
deploymentStage(operation.id, 'Server is empty (0 players verified)');
db().prepare("UPDATE server_deployments SET state = 'running' WHERE id = ?").run(operation.id);
const result = await destroyServerUnlocked(operation.server, operation.id);
finishDeployment(operation.id, result);
} catch (err) {
const row = db().prepare('SELECT state FROM server_deployments WHERE id = ?').get(operation.id) as { state: string };
if (row.state === 'waiting') deploymentStage(operation.id, 'Waiting for player count: ' + (err as Error).message);
else finishDeployment(operation.id, { ok: false, message: (err as Error).message });
} finally { destroying.delete(operation.server); }
}
}
async function destroyServerUnlocked(name: string, operation: string): Promise<DestroyServerResult> {
@@ -362,7 +400,7 @@ async function destroyServerUnlocked(name: string, operation: string): Promise<D
deploymentStage(operation, 'Stopping and removing container');
const destroyRes = await komodoDestroyDeployment(name);
if (!destroyRes.ok) {
warnings.push(`DestroyDeployment failed: ${destroyRes.message}`);
return { ok: false, kind: 'komodo', message: 'DestroyDeployment failed: ' + destroyRes.message, warnings };
}
// 2: wait for the container to actually disappear. Komodo refuses to
@@ -380,7 +418,7 @@ async function destroyServerUnlocked(name: string, operation: string): Promise<D
deploymentStage(operation, 'Deleting Komodo deployment');
const deleteRes = await komodoDeleteDeployment(name);
if (!deleteRes.ok) {
warnings.push(`DeleteDeployment failed: ${deleteRes.message}`);
return { ok: false, kind: 'komodo', message: 'DeleteDeployment failed: ' + deleteRes.message, warnings };
}
// 3: host directory. Fall back to derived path if host_dir wasn't set on