From 5bf1e4de3b394eef203be3c393db57538e748be1 Mon Sep 17 00:00:00 2001 From: Chris Knight Date: Fri, 10 Apr 2020 15:05:56 +0100 Subject: [PATCH] feat(std/signal): add utility for listening to signal events (#4696) --- std/signal/mod.ts | 46 +++++++++++++++++++++++++++++++++++++++++++++- std/signal/test.ts | 36 +++++++++++++++++++++++++++++++++++- 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/std/signal/mod.ts b/std/signal/mod.ts index 0f2b475342..a463142e6e 100644 --- a/std/signal/mod.ts +++ b/std/signal/mod.ts @@ -1,8 +1,28 @@ import { MuxAsyncIterator } from "../util/async.ts"; +export type Disposable = { dispose: () => void }; + +/** + * Generates an AsyncIterable which can be awaited on for one or more signals. + * `dispose()` can be called when you are finished waiting on the events. + * + * Example: + * + * const sig = signal(Deno.Signal.SIGUSR1, Deno.Signal.SIGINT); + * setTimeout(() => {}, 5000); // Prevents exiting immediately + * + * for await (const _ of sig) { + * console.log("interrupt or usr1 signal received"); + * } + * + * // At some other point in your code when finished listening: + * sig.dispose(); + * + * @param signos - one or more `Deno.Signal`s to await on + */ export function signal( ...signos: [number, ...number[]] -): AsyncIterable & { dispose: () => void } { +): AsyncIterable & Disposable { const mux = new MuxAsyncIterator(); if (signos.length < 1) { @@ -26,3 +46,27 @@ export function signal( return Object.assign(mux, { dispose }); } + +/** + * Registers a callback function to be called on triggering of a signal event. + * + * const handle = onSignal(Deno.Signal.SIGINT, () => { + * console.log('Received SIGINT'); + * handle.dispose(); // de-register from receiving further events + * }); + * + * @param signo One of Deno.Signal (e.g. Deno.Signal.SIGINT) + * @param callback Callback function triggered upon signal event + */ +export function onSignal(signo: number, callback: () => void): Disposable { + const sig = signal(signo); + + //setTimeout allows `sig` to be returned before blocking on the await + setTimeout(async () => { + for await (const _ of sig) { + callback(); + } + }, 0); + + return sig; +} diff --git a/std/signal/test.ts b/std/signal/test.ts index 16d1458a23..e36a697a53 100644 --- a/std/signal/test.ts +++ b/std/signal/test.ts @@ -1,7 +1,7 @@ const { test } = Deno; import { assertEquals, assertThrows } from "../testing/asserts.ts"; import { delay } from "../util/async.ts"; -import { signal } from "./mod.ts"; +import { signal, onSignal } from "./mod.ts"; if (Deno.build.os !== "win") { test("signal() throws when called with empty signals", (): void => { @@ -58,4 +58,38 @@ if (Deno.build.os !== "win") { await delay(10); }, }); + + test({ + name: "onSignal() registers and disposes of event handler", + async fn() { + // This prevents the program from exiting. + const t = setInterval(() => {}, 1000); + + let calledCount = 0; + const handle = onSignal(Deno.Signal.SIGINT, () => { + calledCount++; + }); + + await delay(20); + Deno.kill(Deno.pid, Deno.Signal.SIGINT); + await delay(20); + Deno.kill(Deno.pid, Deno.Signal.SIGINT); + await delay(20); + Deno.kill(Deno.pid, Deno.Signal.SIGUSR2); + await delay(20); + handle.dispose(); // stop monitoring SIGINT + await delay(20); + Deno.kill(Deno.pid, Deno.Signal.SIGUSR1); + await delay(20); + Deno.kill(Deno.pid, Deno.Signal.SIGINT); + await delay(20); + assertEquals(calledCount, 2); + + clearTimeout(t); + // Clear timeout clears interval, but interval promise is not + // yet resolved, delay to next turn of event loop otherwise, + // we'll be leaking resources. + await delay(10); + }, + }); }