Skip to content

feat(runtime): add Deno.addSignalListener API #12512

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

Merged
merged 5 commits into from
Oct 26, 2021
Merged
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
57 changes: 19 additions & 38 deletions cli/dts/lib.deno.unstable.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -566,56 +566,37 @@ declare namespace Deno {

/** **UNSTABLE**: new API, yet to be vetted.
*
* Represents the stream of signals, implements both `AsyncIterator` and
* `PromiseLike`. */
export class SignalStream
implements AsyncIterableIterator<void>, PromiseLike<void> {
constructor(signal: Signal);
then<T, S>(
f: (v: void) => T | Promise<T>,
g?: (v: void) => S | Promise<S>,
): Promise<T | S>;
next(): Promise<IteratorResult<void>>;
[Symbol.asyncIterator](): AsyncIterableIterator<void>;
dispose(): void;
}

/** **UNSTABLE**: new API, yet to be vetted.
*
* Returns the stream of the given signal number. You can use it as an async
* iterator.
* Registers the given function as a listener of the given signal event.
*
* ```ts
* for await (const _ of Deno.signal("SIGTERM")) {
* console.log("got SIGTERM!");
* }
* Deno.addSignalListener("SIGTERM", () => {
* console.log("SIGTERM!")
* });
* ```
*
* You can also use it as a promise. In this case you can only receive the
* first one.
*
* ```ts
* await Deno.signal("SIGTERM");
* console.log("SIGTERM received!")
* ```
* NOTE: This functionality is not yet implemented on Windows.
*/
export function addSignalListener(signal: Signal, handler: () => void): void;

/** **UNSTABLE**: new API, yet to be vetted.
*
* If you want to stop receiving the signals, you can use `.dispose()` method
* of the signal stream object.
* Removes the given signal listener that has been registered with
* Deno.addSignalListener.
*
* ```ts
* const sig = Deno.signal("SIGTERM");
* setTimeout(() => { sig.dispose(); }, 5000);
* for await (const _ of sig) {
* const listener = () => {
* console.log("SIGTERM!")
* }
* };
* Deno.addSignalListener("SIGTERM", listener);
* Deno.removeSignalListener("SIGTERM", listener);
* ```
*
* The above for-await loop exits after 5 seconds when `sig.dispose()` is
* called.
*
* NOTE: This functionality is not yet implemented on Windows.
*/
export function signal(sig: Signal): SignalStream;
export function removeSignalListener(
signal: Signal,
handler: () => void,
): void;

export type SetRawOptions = {
cbreak: boolean;
Expand Down
194 changes: 81 additions & 113 deletions cli/tests/unit/signal_test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
import {
assert,
assertEquals,
assertThrows,
deferred,
Expand All @@ -13,84 +12,84 @@ unitTest(
function signalsNotImplemented() {
assertThrows(
() => {
Deno.signal("SIGINT");
Deno.addSignalListener("SIGINT", () => {});
},
Error,
"not implemented",
);
assertThrows(
() => {
Deno.signal("SIGALRM");
Deno.addSignalListener("SIGALRM", () => {});
},
Error,
"not implemented",
);
assertThrows(
() => {
Deno.signal("SIGCHLD");
Deno.addSignalListener("SIGCHLD", () => {});
},
Error,
"not implemented",
);
assertThrows(
() => {
Deno.signal("SIGHUP");
Deno.addSignalListener("SIGHUP", () => {});
},
Error,
"not implemented",
);
assertThrows(
() => {
Deno.signal("SIGINT");
Deno.addSignalListener("SIGINT", () => {});
},
Error,
"not implemented",
);
assertThrows(
() => {
Deno.signal("SIGIO");
Deno.addSignalListener("SIGIO", () => {});
},
Error,
"not implemented",
);
assertThrows(
() => {
Deno.signal("SIGPIPE");
Deno.addSignalListener("SIGPIPE", () => {});
},
Error,
"not implemented",
);
assertThrows(
() => {
Deno.signal("SIGQUIT");
Deno.addSignalListener("SIGQUIT", () => {});
},
Error,
"not implemented",
);
assertThrows(
() => {
Deno.signal("SIGTERM");
Deno.addSignalListener("SIGTERM", () => {});
},
Error,
"not implemented",
);
assertThrows(
() => {
Deno.signal("SIGUSR1");
Deno.addSignalListener("SIGUSR1", () => {});
},
Error,
"not implemented",
);
assertThrows(
() => {
Deno.signal("SIGUSR2");
Deno.addSignalListener("SIGUSR2", () => {});
},
Error,
"not implemented",
);
assertThrows(
() => {
Deno.signal("SIGWINCH");
Deno.addSignalListener("SIGWINCH", () => {});
},
Error,
"not implemented",
Expand All @@ -101,34 +100,75 @@ unitTest(
unitTest(
{
ignore: Deno.build.os === "windows",
permissions: { run: true, net: true },
permissions: { run: true },
},
async function signalStreamTest() {
async function signalListenerTest() {
const resolvable = deferred();
// This prevents the program from exiting.
const t = setInterval(() => {}, 1000);

let c = 0;
const sig = Deno.signal("SIGUSR1");
const listener = () => {
c += 1;
};
Deno.addSignalListener("SIGUSR1", listener);
setTimeout(async () => {
await delay(20);
// Sends SIGUSR1 3 times.
for (const _ of Array(3)) {
// Sends SIGUSR1 3 times.
Deno.kill(Deno.pid, "SIGUSR1");
await delay(20);
Deno.kill(Deno.pid, "SIGUSR1");
}
sig.dispose();
await delay(20);
Deno.removeSignalListener("SIGUSR1", listener);
resolvable.resolve();
});

for await (const _ of sig) {
c += 1;
}

await resolvable;
assertEquals(c, 3);
},
);

unitTest(
{
ignore: Deno.build.os === "windows",
permissions: { run: true },
},
async function multipleSignalListenerTest() {
const resolvable = deferred();
let c = "";
const listener0 = () => {
c += "0";
};
const listener1 = () => {
c += "1";
};
Deno.addSignalListener("SIGUSR2", listener0);
Deno.addSignalListener("SIGUSR2", listener1);
setTimeout(async () => {
// Sends SIGUSR2 3 times.
for (const _ of Array(3)) {
await delay(20);
Deno.kill(Deno.pid, "SIGUSR2");
}
await delay(20);
Deno.removeSignalListener("SIGUSR2", listener1);
// Sends SIGUSR2 3 times.
for (const _ of Array(3)) {
await delay(20);
Deno.kill(Deno.pid, "SIGUSR2");
}
await delay(20);
// Sends SIGUSR1 (irrelevant signal) 3 times.
for (const _ of Array(3)) {
await delay(20);
Deno.kill(Deno.pid, "SIGUSR1");
}
await delay(20);
Deno.removeSignalListener("SIGUSR2", listener0);
resolvable.resolve();
});

clearInterval(t);
await resolvable;
// The first 3 events are handled by both handlers
// The last 3 events are handled only by handler0
assertEquals(c, "010101000");
},
);

Expand All @@ -138,13 +178,13 @@ unitTest(
ignore: Deno.build.os === "windows",
permissions: { run: true, read: true },
},
async function signalStreamExitTest() {
async function canExitWhileListeningToSignal() {
const p = Deno.run({
cmd: [
Deno.execPath(),
"eval",
"--unstable",
"(async () => { for await (const _ of Deno.signal('SIGIO')) {} })()",
"Deno.addSignalListener('SIGIO', () => {})",
],
});
const res = await p.status();
Expand All @@ -154,90 +194,18 @@ unitTest(
);

unitTest(
{ ignore: Deno.build.os === "windows", permissions: { run: true } },
async function signalPromiseTest() {
const resolvable = deferred();
// This prevents the program from exiting.
const t = setInterval(() => {}, 1000);

const sig = Deno.signal("SIGUSR1");
setTimeout(() => {
Deno.kill(Deno.pid, "SIGUSR1");
resolvable.resolve();
}, 20);
await sig;
sig.dispose();

clearInterval(t);
await resolvable;
{
ignore: Deno.build.os === "windows",
permissions: { run: true },
},
);

// https://github.com/denoland/deno/issues/9806
unitTest(
{ ignore: Deno.build.os === "windows", permissions: { run: true } },
async function signalPromiseTest2() {
const resolvable = deferred();
// This prevents the program from exiting.
const t = setInterval(() => {}, 1000);

let called = false;
const sig = Deno.signal("SIGUSR1");
sig.then(() => {
called = true;
function signalInvalidHandlerTest() {
assertThrows(() => {
// deno-lint-ignore no-explicit-any
Deno.addSignalListener("SIGINT", "handler" as any);
});
assertThrows(() => {
// deno-lint-ignore no-explicit-any
Deno.removeSignalListener("SIGINT", "handler" as any);
});
setTimeout(() => {
sig.dispose();
setTimeout(() => {
resolvable.resolve();
}, 10);
}, 10);

clearInterval(t);
await resolvable;

// Promise callback is not called because it didn't get
// the corresponding signal.
assert(!called);
},
);

unitTest(
{ ignore: Deno.build.os === "windows", permissions: { run: true } },
function signalShorthandsTest() {
let s: Deno.SignalStream;
s = Deno.signal("SIGALRM");
assert(s instanceof Deno.SignalStream);
s.dispose();
s = Deno.signal("SIGCHLD");
assert(s instanceof Deno.SignalStream);
s.dispose();
s = Deno.signal("SIGHUP");
assert(s instanceof Deno.SignalStream);
s.dispose();
s = Deno.signal("SIGINT");
assert(s instanceof Deno.SignalStream);
s.dispose();
s = Deno.signal("SIGIO");
assert(s instanceof Deno.SignalStream);
s.dispose();
s = Deno.signal("SIGPIPE");
assert(s instanceof Deno.SignalStream);
s.dispose();
s = Deno.signal("SIGQUIT");
assert(s instanceof Deno.SignalStream);
s.dispose();
s = Deno.signal("SIGTERM");
assert(s instanceof Deno.SignalStream);
s.dispose();
s = Deno.signal("SIGUSR1");
assert(s instanceof Deno.SignalStream);
s.dispose();
s = Deno.signal("SIGUSR2");
assert(s instanceof Deno.SignalStream);
s.dispose();
s = Deno.signal("SIGWINCH");
assert(s instanceof Deno.SignalStream);
s.dispose();
},
);
Loading