diff --git a/src/infra/gateway-lock.ts b/src/infra/gateway-lock.ts index ef89f42a101..a58f4f4e16c 100644 --- a/src/infra/gateway-lock.ts +++ b/src/infra/gateway-lock.ts @@ -139,7 +139,7 @@ function resolveGatewayOwnerStatus( return isGatewayArgv(args) ? "alive" : "dead"; } -async function readLockPayload(lockPath: string): Promise { +export async function readLockPayload(lockPath: string): Promise { try { const raw = await fs.readFile(lockPath, "utf8"); const parsed = JSON.parse(raw) as Partial; @@ -164,7 +164,7 @@ async function readLockPayload(lockPath: string): Promise { } } -function resolveGatewayLockPath(env: NodeJS.ProcessEnv) { +export function resolveGatewayLockPath(env: NodeJS.ProcessEnv) { const stateDir = resolveStateDir(env); const configPath = resolveConfigPath(env, stateDir); const hash = createHash("sha1").update(configPath).digest("hex").slice(0, 8); @@ -173,6 +173,54 @@ function resolveGatewayLockPath(env: NodeJS.ProcessEnv) { return { lockPath, configPath }; } +/** + * Attempt to stop a running gateway by reading its PID from the lock file + * and sending SIGTERM. + */ +export async function stopGatewayViaLock(opts: { + env: NodeJS.ProcessEnv; + stdout: NodeJS.WritableStream; + timeoutMs?: number; +}): Promise { + const { lockPath } = resolveGatewayLockPath(opts.env); + const payload = await readLockPayload(lockPath); + if (!payload || !payload.pid) { + return false; + } + + const pid = payload.pid; + if (!isAlive(pid)) { + return false; + } + + opts.stdout.write(`Stopping gateway (pid ${pid})...\n`); + try { + process.kill(pid, "SIGTERM"); + } catch (err) { + opts.stdout.write(`Failed to send SIGTERM to pid ${pid}: ${String(err)}\n`); + return false; + } + + // Wait for it to die + const timeoutMs = opts.timeoutMs ?? 5000; + const startedAt = Date.now(); + while (Date.now() - startedAt < timeoutMs) { + if (!isAlive(pid)) { + opts.stdout.write(`Gateway (pid ${pid}) stopped.\n`); + return true; + } + await new Promise((r) => setTimeout(r, 200)); + } + + opts.stdout.write(`Gateway (pid ${pid}) did not stop after ${timeoutMs}ms. Sending SIGKILL...\n`); + try { + process.kill(pid, "SIGKILL"); + return true; + } catch { + return false; + } +} + export async function acquireGatewayLock( opts: GatewayLockOptions = {}, ): Promise {