Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: dispose #389

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions src/cleanup.ts
Original file line number Diff line number Diff line change
@@ -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<void>;

let cleanupListeners: Set<CleanupListener> | 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<never> {
await executeCleanupListeners();
process.exit(code);
}

/** Executes all cleanup listeners and then kills the process with the given signal. */
export async function killAfterCleanup(signal: ExitSignal): Promise<void> {
await executeCleanupListeners();
process.kill(process.pid, signal);
}

async function executeCleanupListeners(): Promise<void> {
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<void>[] = [];
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)
);
}
10 changes: 9 additions & 1 deletion src/sern.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';


/**
Expand Down Expand Up @@ -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();
});

}
Loading