new destruction flow
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user