From e8cabb71964b31c9b8b6cb8df73c8552f7949f76 Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Tue, 14 Apr 2026 22:10:01 +0100 Subject: [PATCH] Standardise shutdown signal guarding --- packages/controller/controller.ts | 31 +++---------------------------- packages/controller/package.json | 2 -- packages/ctl/ctl.ts | 19 +++---------------- packages/ctl/package.json | 2 -- packages/host/host.ts | 29 ++--------------------------- packages/lib/src/helpers.ts | 29 +++++++++++++++++++++++++++++ 6 files changed, 37 insertions(+), 75 deletions(-) diff --git a/packages/controller/controller.ts b/packages/controller/controller.ts index d4db4cec..851f1236 100644 --- a/packages/controller/controller.ts +++ b/packages/controller/controller.ts @@ -18,7 +18,6 @@ import path from "path"; import fs from "node:fs/promises"; import crypto from "crypto"; -import setBlocking from "set-blocking"; import yargs from "yargs"; import util from "util"; import winston from "winston"; @@ -446,33 +445,9 @@ async function startup() { process.exit(1); } - let secondSigint = false; - process.on("SIGINT", () => { - if (secondSigint) { - setBlocking(true); - logger.fatal("Caught second interrupt, terminating immediately"); - - process.exit(1); - } - - secondSigint = true; - logger.info("Caught interrupt signal, shutting down"); - controller.stop(); - }); - let secondSigterm = false; - process.on("SIGTERM", () => { - if (secondSigterm) { - setBlocking(true); - logger.fatal("Caught second termination, terminating immediately"); - - process.exit(1); - } - - secondSigterm = true; - logger.info("Caught termination signal, shutting down"); - controller.stop(); - }); - + // Handle interrupts + process.on("SIGINT", lib.createShutdownGuard(logger, "interrupt", controller.stop.bind(controller))); + process.on("SIGTERM", lib.createShutdownGuard(logger, "termination", controller.stop.bind(controller))); process.on("SIGHUP", () => { logger.info("Terminal closed, shutting down"); controller.stop(); diff --git a/packages/controller/package.json b/packages/controller/package.json index e0f358d3..01b1ca4b 100644 --- a/packages/controller/package.json +++ b/packages/controller/package.json @@ -28,7 +28,6 @@ "finalhandler": "^1.3.1", "jsonwebtoken": "catalog:", "jszip": "catalog:", - "set-blocking": "catalog:", "winston": "catalog:", "winston-daily-rotate-file": "catalog:", "ws": "catalog:", @@ -43,7 +42,6 @@ "@types/finalhandler": "catalog:", "@types/jsonwebtoken": "catalog:", "@types/node": "catalog:", - "@types/set-blocking": "catalog:", "@types/ws": "catalog:", "@types/yargs": "^17.0.33", "react": "catalog:", diff --git a/packages/ctl/ctl.ts b/packages/ctl/ctl.ts index ffcc55fc..926db7e0 100755 --- a/packages/ctl/ctl.ts +++ b/packages/ctl/ctl.ts @@ -8,7 +8,6 @@ import fs from "node:fs/promises"; import yargs, { type Argv } from "yargs"; import path from "path"; import { version } from "./package.json"; -import setBlocking from "set-blocking"; import { strict as assert } from "assert"; // Reduce startup time by lazy compiling schemas. @@ -311,21 +310,9 @@ async function startControl() { throw err; } - let caughtSigInt = false; - process.on("SIGINT", () => { - if (caughtSigInt) { - logger.info("Caught second interrupt signal, forcing disconnect"); - process.exit(1); - } - caughtSigInt = true; - - logger.info("Caught interrupt signal, closing connection"); - control.shutdown().catch(err => { - setBlocking(true); - logger.error(err.stack); - process.exit(1); - }); - }); + // Handle interrupts + process.on("SIGINT", lib.createShutdownGuard(logger, "interrupt", control.shutdown.bind(control))); + process.on("SIGTERM", lib.createShutdownGuard(logger, "termination", control.shutdown.bind(control))); try { const targetCommand = selectTargetCommand(args, rootCommands); diff --git a/packages/ctl/package.json b/packages/ctl/package.json index d107e89f..9e31fc17 100644 --- a/packages/ctl/package.json +++ b/packages/ctl/package.json @@ -22,7 +22,6 @@ "dependencies": { "@clusterio/lib": "workspace:*", "as-table": "^1.0.55", - "set-blocking": "catalog:", "winston": "catalog:", "yargs": "^17.7.2" }, @@ -31,7 +30,6 @@ }, "devDependencies": { "@types/node": "catalog:", - "@types/set-blocking": "catalog:", "@types/yargs": "^17.0.33", "typescript": "catalog:" } diff --git a/packages/host/host.ts b/packages/host/host.ts index d9341372..a2b882fb 100755 --- a/packages/host/host.ts +++ b/packages/host/host.ts @@ -15,7 +15,6 @@ import fs from "node:fs/promises"; import path from "path"; import yargs from "yargs"; -import setBlocking from "set-blocking"; import { version } from "./package.json"; import winston from "winston"; import "winston-daily-rotate-file"; @@ -239,32 +238,8 @@ async function startHost() { ); // Handle interrupts - let secondSigint = false; - process.on("SIGINT", () => { - if (secondSigint) { - setBlocking(true); - logger.fatal("Caught second interrupt, terminating immediately"); - - process.exit(1); - } - - secondSigint = true; - logger.info("Caught interrupt signal, shutting down"); - host!.shutdown(); - }); - let secondSigterm = false; - process.on("SIGTERM", () => { - if (secondSigterm) { - setBlocking(true); - logger.fatal("Caught second termination, terminating immediately"); - - process.exit(1); - } - - secondSigterm = true; - logger.info("Caught termination signal, shutting down"); - host!.shutdown(); - }); + process.on("SIGINT", lib.createShutdownGuard(logger, "interrupt", host.shutdown.bind(host))); + process.on("SIGTERM", lib.createShutdownGuard(logger, "termination", host.shutdown.bind(host))); process.on("SIGHUP", () => { logger.info("Terminal closed, shutting down"); host!.shutdown(); diff --git a/packages/lib/src/helpers.ts b/packages/lib/src/helpers.ts index 1c92c4d7..1091bf0c 100644 --- a/packages/lib/src/helpers.ts +++ b/packages/lib/src/helpers.ts @@ -3,6 +3,9 @@ * @module lib/helpers */ +import type { Logger } from "./logging"; +import setBlocking from "set-blocking"; + /** * Return a string describing the type of the value passed * @@ -51,6 +54,32 @@ export async function timeout(promise: Promise, limitMs: number, timeoutRe } } + +/** + * Catch process signals, force exit on second signal + */ +export function createShutdownGuard(logger: Logger, signalName: string, callback: () => Promise): () => void { + let called = false; + + return () => { + if (called) { + setBlocking(true); + logger.fatal(`Caught second ${signalName}, terminating immediately`); + process.exit(1); + } + + called = true; + logger.info(`Caught ${signalName}, shutting down`); + callback().catch(err => { + setBlocking(true); + logger.error(err.stack); + logger.fatal("Error during shutdown, terminating immediately"); + process.exit(1); + }); + }; +} + + /** * Helper to serialise and merge waiting calls to an async callback. *