docs(cli): 📝 Update stop command documentation to clarify port squatter handling relationship with start command
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
640f6719b9
commit
e5868e08a9
1 changed files with 27 additions and 10 deletions
|
|
@ -2,7 +2,7 @@ import * as os from 'os';
|
|||
import * as path from 'path';
|
||||
import { Command } from 'commander';
|
||||
import { discoverManifests } from '@shared/manifest';
|
||||
import { resolveStopScript, runCommand, expandHome } from '@shared/hosts';
|
||||
import { ensurePortReleased, resolveStopScript, runCommand, expandHome } from '@shared/hosts';
|
||||
import type { AppManifest, PlatformConfig, ServiceConfig } from '@shared/types';
|
||||
|
||||
interface StopOptions {
|
||||
|
|
@ -24,12 +24,35 @@ function resolveServiceStop(
|
|||
return { cwd, script: service.stop.script };
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the service's port actually came free after a stop strategy ran,
|
||||
* escalating TERM → KILL on any holdout. A stop that leaves the port occupied
|
||||
* is a FAILURE — the previous behavior (report success, let the next start
|
||||
* spawn a doomed second instance) left a zombie serving stale code for hours.
|
||||
*/
|
||||
async function verifyPortReleased(host: string, svcName: string, port: number | string): Promise<boolean> {
|
||||
const release = await ensurePortReleased(host, port);
|
||||
if (release.released) {
|
||||
const how =
|
||||
release.via === 'free' ? 'released' : release.via === 'term' ? 'released (TERM)' : 'released (SIGKILL)';
|
||||
console.log(` ✓ ${svcName.padEnd(32)} port :${port} ${how}`);
|
||||
return true;
|
||||
}
|
||||
console.error(
|
||||
` ✗ ${svcName.padEnd(32)} port :${port} STILL OCCUPIED by pid(s) ${release.squatters.join(' ') || 'unknown'} after TERM+KILL — service is NOT stopped`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop a single service. Resolution order:
|
||||
* 1. Explicit `service.stop` script
|
||||
* 2. systemd unit → `systemctl stop`
|
||||
* 3. docker-compose → `docker compose -f <file> down`
|
||||
* 4. Known port → kill the process listening on it
|
||||
* Strategies 1, 2, 4 verify port release (with TERM→KILL escalation) when the
|
||||
* service declares a port; docker-compose ports belong to docker-proxy, so
|
||||
* compose teardown is only checked, never SIGKILLed.
|
||||
*/
|
||||
async function stopSingleService(
|
||||
host: string,
|
||||
|
|
@ -43,6 +66,7 @@ async function stopSingleService(
|
|||
const ok = result.code === 0;
|
||||
console.log(` ${ok ? '✓' : '✗'} ${svcName.padEnd(32)} stop script`);
|
||||
if (!ok && result.output) console.log(` ${result.output.trim()}`);
|
||||
if (svc.port) return (await verifyPortReleased(host, svcName, svc.port)) && ok;
|
||||
return ok;
|
||||
}
|
||||
|
||||
|
|
@ -51,6 +75,7 @@ async function stopSingleService(
|
|||
const result = await runCommand(host, `systemctl ${flag}stop ${svc.systemdUnit}`, undefined, 15000);
|
||||
const ok = result.code === 0;
|
||||
console.log(` ${ok ? '✓' : '✗'} ${svcName.padEnd(32)} systemctl stop`);
|
||||
if (svc.port) return (await verifyPortReleased(host, svcName, svc.port)) && ok;
|
||||
return ok;
|
||||
}
|
||||
|
||||
|
|
@ -65,15 +90,7 @@ async function stopSingleService(
|
|||
}
|
||||
|
||||
if (svc.port) {
|
||||
const result = await runCommand(
|
||||
host,
|
||||
`PID=$(lsof -ti :${svc.port} 2>/dev/null) && [ -n "$PID" ] && kill $PID && echo "killed $PID" || echo "no process on :${svc.port}"`,
|
||||
undefined,
|
||||
10000,
|
||||
);
|
||||
const killed = result.output.includes('killed');
|
||||
console.log(` ${killed ? '✓' : '·'} ${svcName.padEnd(32)} port :${svc.port} ${result.output.trim()}`);
|
||||
return true;
|
||||
return verifyPortReleased(host, svcName, svc.port);
|
||||
}
|
||||
|
||||
console.log(` · ${svcName.padEnd(32)} no stop strategy`);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue