From ef3d3d71a02b8fc3747fc96fcf90ae7c7138a77b Mon Sep 17 00:00:00 2001 From: jacoobes Date: Mon, 3 Feb 2025 18:33:56 -0600 Subject: [PATCH] disposalfixe --- src/cleanup.ts | 111 +++++++++++++++++++++++++++++++++++++++++++++++++ src/sern.ts | 10 ++++- yarn.lock | 8 ++++ 3 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 src/cleanup.ts diff --git a/src/cleanup.ts b/src/cleanup.ts new file mode 100644 index 0000000..5abb10c --- /dev/null +++ b/src/cleanup.ts @@ -0,0 +1,111 @@ +// It's this package but without default console log / error https://github.com/trevorr/async-cleanup + +/** A possibly asynchronous function invoked with the process is about to exit. */ +export type CleanupListener = () => void | Promise; + +let cleanupListeners: Set | undefined; + +/** Registers a new cleanup listener. Adding the same listener more than once has no effect. */ +export function addCleanupListener(listener: CleanupListener): void { + // Install exit listeners on initial cleanup listener + if (!cleanupListeners) { + installExitListeners(); + cleanupListeners = new Set(); + } + + cleanupListeners.add(listener); +} + +/** Removes an existing cleanup listener, and returns whether the listener was registered. */ +export function removeCleanupListener(listener: CleanupListener): boolean { + return cleanupListeners != null && cleanupListeners.delete(listener); +} + +/** Executes all cleanup listeners and then exits the process. Call this instead of `process.exit` to ensure all listeners are fully executed. */ +export async function exitAfterCleanup(code = 0): Promise { + await executeCleanupListeners(code); + process.exit(code); +} + +/** Executes all cleanup listeners and then kills the process with the given signal. */ +export async function killAfterCleanup(signal: ExitSignal): Promise { + await executeCleanupListeners(); + process.kill(process.pid, signal); +} + +async function executeCleanupListeners(): Promise { + if (cleanupListeners) { + // Remove exit listeners to restore normal event handling + uninstallExitListeners(); + + // Clear cleanup listeners to reset state for testing + const listeners = cleanupListeners; + cleanupListeners = undefined; + + // Call listeners in order added with async listeners running concurrently + const promises: Promise[] = []; + for (const listener of listeners) { + try { + const promise = listener(); + if (promise) promises.push(promise); + } catch (err) { + // console.error("Uncaught exception during cleanup", err); + } + } + + // Wait for all listeners to complete and log any rejections + const results = await Promise.allSettled(promises); + for (const result of results) { + if (result.status === "rejected") { + console.error("Unhandled rejection during cleanup", result.reason); + } + } + } +} + +function beforeExitListener(code: number): void { + // console.log(`Exiting with code ${code} due to empty event loop`); + void exitAfterCleanup(code); +} + +function uncaughtExceptionListener(error: Error): void { + // console.error("Exiting with code 1 due to uncaught exception", error); + void exitAfterCleanup(1); +} + +function signalListener(signal: ExitSignal): void { + // console.log(`Exiting due to signal ${signal}`); + void killAfterCleanup(signal); +} + +// Listenable signals that terminate the process by default +// (except SIGQUIT, which generates a core dump and should not trigger cleanup) +// See https://nodejs.org/api/process.html#signal-events +const listenedSignals = [ + "SIGBREAK", // Ctrl-Break on Windows + "SIGHUP", // Parent terminal closed + "SIGINT", // Terminal interrupt, usually by Ctrl-C + "SIGTERM", // Graceful termination + "SIGUSR2", // Used by Nodemon +] as const; + +/** Signals that can terminate the process. */ +export type ExitSignal = + | typeof listenedSignals[number] + | "SIGKILL" + | "SIGQUIT" + | "SIGSTOP"; + +function installExitListeners(): void { + process.on("beforeExit", beforeExitListener); + process.on("uncaughtException", uncaughtExceptionListener); + listenedSignals.forEach((signal) => process.on(signal, signalListener)); +} + +function uninstallExitListeners(): void { + process.removeListener("beforeExit", beforeExitListener); + process.removeListener("uncaughtException", uncaughtExceptionListener); + listenedSignals.forEach((signal) => + process.removeListener(signal, signalListener) + ); +} diff --git a/src/sern.ts b/src/sern.ts index b0bfde4..39ff079 100644 --- a/src/sern.ts +++ b/src/sern.ts @@ -14,6 +14,7 @@ import { presenceHandler } from './handlers/presence'; import type { Payload, UnpackedDependencies, Wrapper } from './types/utility'; import type { Presence} from './core/presences'; import { registerTasks } from './handlers/tasks'; +import { addCleanupListener } from './cleanup'; /** @@ -76,5 +77,12 @@ export function init(maybeWrapper: Wrapper = { commands: "./dist/commands" }) { }) .catch(err => { throw err }); interactionHandler(deps, maybeWrapper.defaultPrefix); - messageHandler(deps, maybeWrapper.defaultPrefix) + messageHandler(deps, maybeWrapper.defaultPrefix); + + addCleanupListener(async () => { + const duration = ((performance.now() - startTime) / 1000).toFixed(2) + deps['@sern/logger']?.info({ 'message': 'sern is shutting down after '+duration +" seconds" }) + await useContainerRaw().disposeAll(); + }); + } diff --git a/yarn.lock b/yarn.lock index a2eae29..559e05c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -588,6 +588,7 @@ __metadata: "@types/node-cron": ^3.0.11 "@typescript-eslint/eslint-plugin": 5.58.0 "@typescript-eslint/parser": 5.59.1 + async-cleanup: ^1.0.0 callsites: ^3.1.0 cron: ^3.1.7 deepmerge: ^4.3.1 @@ -1012,6 +1013,13 @@ __metadata: languageName: node linkType: hard +"async-cleanup@npm:^1.0.0": + version: 1.0.0 + resolution: "async-cleanup@npm:1.0.0" + checksum: d2dea124db5f546716f9c5237714d78978de39e78f0bf9d9e884497249543e073203b1949c78262b2485e9ed827dc9a26d2392954be6e4025e9f06552c5164dc + languageName: node + linkType: hard + "balanced-match@npm:^1.0.0": version: 1.0.2 resolution: "balanced-match@npm:1.0.2"