diff --git a/cli/js/deno.ts b/cli/js/deno.ts index e53f9a63af..2d20ae8114 100644 --- a/cli/js/deno.ts +++ b/cli/js/deno.ts @@ -101,6 +101,7 @@ export { } from "./process.ts"; export { transpileOnly, compile, bundle } from "./compiler_api.ts"; export { inspect } from "./console.ts"; +export { signal, signals, SignalStream } from "./signals.ts"; export { build, OperatingSystem, Arch } from "./build.ts"; export { version } from "./version.ts"; export const args: string[] = []; diff --git a/cli/js/dispatch.ts b/cli/js/dispatch.ts index f5049cca88..aa6696fa2c 100644 --- a/cli/js/dispatch.ts +++ b/cli/js/dispatch.ts @@ -74,6 +74,9 @@ export let OP_HOSTNAME: number; export let OP_OPEN_PLUGIN: number; export let OP_COMPILE: number; export let OP_TRANSPILE: number; +export let OP_SIGNAL_BIND: number; +export let OP_SIGNAL_UNBIND: number; +export let OP_SIGNAL_POLL: number; /** **WARNING:** This is only available during the snapshotting process and is * unavailable at runtime. */ diff --git a/cli/js/lib.deno_runtime.d.ts b/cli/js/lib.deno_runtime.d.ts index efdf06347a..1dfa3209f8 100644 --- a/cli/js/lib.deno_runtime.d.ts +++ b/cli/js/lib.deno_runtime.d.ts @@ -2130,6 +2130,80 @@ declare namespace Deno { */ export const args: string[]; + /** SignalStream represents the stream of signals, implements both + * AsyncIterator and PromiseLike */ + export class SignalStream implements AsyncIterator, PromiseLike { + constructor(signal: typeof Deno.Signal); + then( + f: (v: void) => T | Promise, + g?: (v: void) => S | Promise + ): Promise; + next(): Promise>; + [Symbol.asyncIterator](): AsyncIterator; + dispose(): void; + } + /** + * Returns the stream of the given signal number. You can use it as an async + * iterator. + * + * for await (const _ of Deno.signal(Deno.Signal.SIGTERM)) { + * console.log("got SIGTERM!"); + * } + * + * You can also use it as a promise. In this case you can only receive the + * first one. + * + * await Deno.signal(Deno.Signal.SIGTERM); + * console.log("SIGTERM received!") + * + * If you want to stop receiving the signals, you can use .dispose() method + * of the signal stream object. + * + * const sig = Deno.signal(Deno.Signal.SIGTERM); + * setTimeout(() => { sig.dispose(); }, 5000); + * for await (const _ of sig) { + * console.log("SIGTERM!") + * } + * + * The above for-await loop exits after 5 seconds when sig.dispose() is called. + */ + export function signal(signo: number): SignalStream; + export const signals: { + /** Returns the stream of SIGALRM signals. + * This method is the shorthand for Deno.signal(Deno.Signal.SIGALRM). */ + alarm: () => SignalStream; + /** Returns the stream of SIGCHLD signals. + * This method is the shorthand for Deno.signal(Deno.Signal.SIGCHLD). */ + child: () => SignalStream; + /** Returns the stream of SIGHUP signals. + * This method is the shorthand for Deno.signal(Deno.Signal.SIGHUP). */ + hungup: () => SignalStream; + /** Returns the stream of SIGINT signals. + * This method is the shorthand for Deno.signal(Deno.Signal.SIGINT). */ + interrupt: () => SignalStream; + /** Returns the stream of SIGIO signals. + * This method is the shorthand for Deno.signal(Deno.Signal.SIGIO). */ + io: () => SignalStream; + /** Returns the stream of SIGPIPE signals. + * This method is the shorthand for Deno.signal(Deno.Signal.SIGPIPE). */ + pipe: () => SignalStream; + /** Returns the stream of SIGQUIT signals. + * This method is the shorthand for Deno.signal(Deno.Signal.SIGQUIT). */ + quit: () => SignalStream; + /** Returns the stream of SIGTERM signals. + * This method is the shorthand for Deno.signal(Deno.Signal.SIGTERM). */ + terminate: () => SignalStream; + /** Returns the stream of SIGUSR1 signals. + * This method is the shorthand for Deno.signal(Deno.Signal.SIGUSR1). */ + userDefined1: () => SignalStream; + /** Returns the stream of SIGUSR2 signals. + * This method is the shorthand for Deno.signal(Deno.Signal.SIGUSR2). */ + userDefined2: () => SignalStream; + /** Returns the stream of SIGWINCH signals. + * This method is the shorthand for Deno.signal(Deno.Signal.SIGWINCH). */ + windowChange: () => SignalStream; + }; + /** UNSTABLE: new API. Maybe move EOF here. * * Special Deno related symbols. diff --git a/cli/js/process.ts b/cli/js/process.ts index 8ad6384b74..5267763c1e 100644 --- a/cli/js/process.ts +++ b/cli/js/process.ts @@ -296,7 +296,7 @@ enum MacOSSignal { /** Signals numbers. This is platform dependent. */ -export const Signal = {}; +export const Signal: { [key: string]: number } = {}; export function setSignals(): void { if (build.os === "mac") { diff --git a/cli/js/signal_test.ts b/cli/js/signal_test.ts new file mode 100644 index 0000000000..06457314c8 --- /dev/null +++ b/cli/js/signal_test.ts @@ -0,0 +1,185 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { + test, + testPerm, + assert, + assertEquals, + assertThrows +} from "./test_util.ts"; + +function defer(n: number): Promise { + return new Promise((resolve, _) => { + setTimeout(resolve, n); + }); +} + +if (Deno.build.os === "win") { + test(async function signalsNotImplemented(): Promise { + assertThrows( + () => { + Deno.signal(1); + }, + Error, + "not implemented" + ); + assertThrows( + () => { + Deno.signals.alarm(); // for SIGALRM + }, + Error, + "not implemented" + ); + assertThrows( + () => { + Deno.signals.child(); // for SIGCHLD + }, + Error, + "not implemented" + ); + assertThrows( + () => { + Deno.signals.hungup(); // for SIGHUP + }, + Error, + "not implemented" + ); + assertThrows( + () => { + Deno.signals.interrupt(); // for SIGINT + }, + Error, + "not implemented" + ); + assertThrows( + () => { + Deno.signals.io(); // for SIGIO + }, + Error, + "not implemented" + ); + assertThrows( + () => { + Deno.signals.pipe(); // for SIGPIPE + }, + Error, + "not implemented" + ); + assertThrows( + () => { + Deno.signals.quit(); // for SIGQUIT + }, + Error, + "not implemented" + ); + assertThrows( + () => { + Deno.signals.terminate(); // for SIGTERM + }, + Error, + "not implemented" + ); + assertThrows( + () => { + Deno.signals.userDefined1(); // for SIGUSR1 + }, + Error, + "not implemented" + ); + assertThrows( + () => { + Deno.signals.userDefined2(); // for SIGURS2 + }, + Error, + "not implemented" + ); + assertThrows( + () => { + Deno.signals.windowChange(); // for SIGWINCH + }, + Error, + "not implemented" + ); + }); +} else { + testPerm({ run: true, net: true }, async function signalStreamTest(): Promise< + void + > { + // This prevents the program from exiting. + const t = setInterval(() => {}, 1000); + + let c = 0; + const sig = Deno.signal(Deno.Signal.SIGUSR1); + + setTimeout(async () => { + await defer(20); + for (const _ of Array(3)) { + // Sends SIGUSR1 3 times. + Deno.kill(Deno.pid, Deno.Signal.SIGUSR1); + await defer(20); + } + sig.dispose(); + }); + + for await (const _ of sig) { + c += 1; + } + + assertEquals(c, 3); + + clearTimeout(t); + }); + + testPerm( + { run: true, net: true }, + async function signalPromiseTest(): Promise { + // This prevents the program from exiting. + const t = setInterval(() => {}, 1000); + + const sig = Deno.signal(Deno.Signal.SIGUSR1); + setTimeout(() => { + Deno.kill(Deno.pid, Deno.Signal.SIGUSR1); + }, 20); + await sig; + sig.dispose(); + + clearTimeout(t); + } + ); + + testPerm({ run: true }, async function signalShorthandsTest(): Promise { + let s: Deno.SignalStream; + s = Deno.signals.alarm(); // for SIGALRM + assert(s instanceof Deno.SignalStream); + s.dispose(); + s = Deno.signals.child(); // for SIGCHLD + assert(s instanceof Deno.SignalStream); + s.dispose(); + s = Deno.signals.hungup(); // for SIGHUP + assert(s instanceof Deno.SignalStream); + s.dispose(); + s = Deno.signals.interrupt(); // for SIGINT + assert(s instanceof Deno.SignalStream); + s.dispose(); + s = Deno.signals.io(); // for SIGIO + assert(s instanceof Deno.SignalStream); + s.dispose(); + s = Deno.signals.pipe(); // for SIGPIPE + assert(s instanceof Deno.SignalStream); + s.dispose(); + s = Deno.signals.quit(); // for SIGQUIT + assert(s instanceof Deno.SignalStream); + s.dispose(); + s = Deno.signals.terminate(); // for SIGTERM + assert(s instanceof Deno.SignalStream); + s.dispose(); + s = Deno.signals.userDefined1(); // for SIGUSR1 + assert(s instanceof Deno.SignalStream); + s.dispose(); + s = Deno.signals.userDefined2(); // for SIGURS2 + assert(s instanceof Deno.SignalStream); + s.dispose(); + s = Deno.signals.windowChange(); // for SIGWINCH + assert(s instanceof Deno.SignalStream); + s.dispose(); + }); +} diff --git a/cli/js/signals.ts b/cli/js/signals.ts new file mode 100644 index 0000000000..02d52bc2f1 --- /dev/null +++ b/cli/js/signals.ts @@ -0,0 +1,148 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { Signal } from "./process.ts"; +import * as dispatch from "./dispatch.ts"; +import { sendSync, sendAsync } from "./dispatch_json.ts"; +import { build } from "./build.ts"; + +/** + * Returns the stream of the given signal number. You can use it as an async + * iterator. + * + * for await (const _ of Deno.signal(Deno.Signal.SIGTERM)) { + * console.log("got SIGTERM!"); + * } + * + * You can also use it as a promise. In this case you can only receive the + * first one. + * + * await Deno.signal(Deno.Signal.SIGTERM); + * console.log("SIGTERM received!") + * + * If you want to stop receiving the signals, you can use .dispose() method + * of the signal stream object. + * + * const sig = Deno.signal(Deno.Signal.SIGTERM); + * setTimeout(() => { sig.dispose(); }, 5000); + * for await (const _ of sig) { + * console.log("SIGTERM!") + * } + * + * The above for-await loop exits after 5 seconds when sig.dispose() is called. + */ +export function signal(signo: number): SignalStream { + if (build.os === "win") { + throw new Error("not implemented!"); + } + return new SignalStream(signo); +} + +export const signals = { + /** Returns the stream of SIGALRM signals. + * This method is the shorthand for Deno.signal(Deno.Signal.SIGALRM). */ + alarm(): SignalStream { + return signal(Signal.SIGALRM); + }, + /** Returns the stream of SIGCHLD signals. + * This method is the shorthand for Deno.signal(Deno.Signal.SIGCHLD). */ + child(): SignalStream { + return signal(Signal.SIGCHLD); + }, + /** Returns the stream of SIGHUP signals. + * This method is the shorthand for Deno.signal(Deno.Signal.SIGHUP). */ + hungup(): SignalStream { + return signal(Signal.SIGHUP); + }, + /** Returns the stream of SIGINT signals. + * This method is the shorthand for Deno.signal(Deno.Signal.SIGINT). */ + interrupt(): SignalStream { + return signal(Signal.SIGINT); + }, + /** Returns the stream of SIGIO signals. + * This method is the shorthand for Deno.signal(Deno.Signal.SIGIO). */ + io(): SignalStream { + return signal(Signal.SIGIO); + }, + /** Returns the stream of SIGPIPE signals. + * This method is the shorthand for Deno.signal(Deno.Signal.SIGPIPE). */ + pipe(): SignalStream { + return signal(Signal.SIGPIPE); + }, + /** Returns the stream of SIGQUIT signals. + * This method is the shorthand for Deno.signal(Deno.Signal.SIGQUIT). */ + quit(): SignalStream { + return signal(Signal.SIGQUIT); + }, + /** Returns the stream of SIGTERM signals. + * This method is the shorthand for Deno.signal(Deno.Signal.SIGTERM). */ + terminate(): SignalStream { + return signal(Signal.SIGTERM); + }, + /** Returns the stream of SIGUSR1 signals. + * This method is the shorthand for Deno.signal(Deno.Signal.SIGUSR1). */ + userDefined1(): SignalStream { + return signal(Signal.SIGUSR1); + }, + /** Returns the stream of SIGUSR2 signals. + * This method is the shorthand for Deno.signal(Deno.Signal.SIGUSR2). */ + userDefined2(): SignalStream { + return signal(Signal.SIGUSR2); + }, + /** Returns the stream of SIGWINCH signals. + * This method is the shorthand for Deno.signal(Deno.Signal.SIGWINCH). */ + windowChange(): SignalStream { + return signal(Signal.SIGWINCH); + } +}; + +/** SignalStream represents the stream of signals, implements both + * AsyncIterator and PromiseLike */ +export class SignalStream implements AsyncIterator, PromiseLike { + private rid: number; + /** The promise of polling the signal, + * resolves with false when it receives signal, + * Resolves with true when the signal stream is disposed. */ + private pollingPromise: Promise = Promise.resolve(false); + /** The flag, which is true when the stream is disposed. */ + private disposed = false; + constructor(signo: number) { + this.rid = sendSync(dispatch.OP_SIGNAL_BIND, { signo }).rid; + this.loop(); + } + + private async pollSignal(): Promise { + return ( + await sendAsync(dispatch.OP_SIGNAL_POLL, { + rid: this.rid + }) + ).done; + } + + private async loop(): Promise { + do { + this.pollingPromise = this.pollSignal(); + } while (!(await this.pollingPromise) && !this.disposed); + } + + then( + f: (v: void) => T | Promise, + g?: (v: Error) => S | Promise + ): Promise { + return this.pollingPromise.then((_): void => {}).then(f, g); + } + + async next(): Promise> { + return { done: await this.pollingPromise, value: undefined }; + } + + [Symbol.asyncIterator](): AsyncIterator { + return this; + } + + dispose(): void { + if (this.disposed) { + throw new Error("The stream has already been disposed."); + } + this.disposed = true; + sendSync(dispatch.OP_SIGNAL_UNBIND, { rid: this.rid }); + } +} diff --git a/cli/js/unit_tests.ts b/cli/js/unit_tests.ts index 084661ab84..47ae06b19b 100644 --- a/cli/js/unit_tests.ts +++ b/cli/js/unit_tests.ts @@ -42,6 +42,7 @@ import "./read_link_test.ts"; import "./rename_test.ts"; import "./request_test.ts"; import "./resources_test.ts"; +import "./signal_test.ts"; import "./stat_test.ts"; import "./symbols_test.ts"; import "./symlink_test.ts"; diff --git a/cli/lib.rs b/cli/lib.rs index 9753e4c4c6..4a08f40c60 100644 --- a/cli/lib.rs +++ b/cli/lib.rs @@ -44,7 +44,7 @@ mod progress; mod repl; pub mod resolve_addr; mod shell; -mod signal; +pub mod signal; pub mod source_maps; mod startup_data; pub mod state; diff --git a/cli/ops/mod.rs b/cli/ops/mod.rs index 81f95ffb9c..4306e25e24 100644 --- a/cli/ops/mod.rs +++ b/cli/ops/mod.rs @@ -22,6 +22,7 @@ pub mod random; pub mod repl; pub mod resources; pub mod runtime_compiler; +pub mod signal; pub mod timers; pub mod tls; pub mod web_worker; diff --git a/cli/ops/signal.rs b/cli/ops/signal.rs new file mode 100644 index 0000000000..5fef020131 --- /dev/null +++ b/cli/ops/signal.rs @@ -0,0 +1,146 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +use super::dispatch_json::{JsonOp, Value}; +use crate::ops::json_op; +use crate::state::ThreadSafeState; +use deno_core::*; + +#[cfg(unix)] +use super::dispatch_json::Deserialize; +#[cfg(unix)] +use crate::deno_error::bad_resource; +#[cfg(unix)] +use deno_core::Resource; +#[cfg(unix)] +use futures::future::{poll_fn, FutureExt}; +#[cfg(unix)] +use serde_json; +#[cfg(unix)] +use std::task::Waker; +#[cfg(unix)] +use tokio::signal::unix::{signal, Signal, SignalKind}; + +pub fn init(i: &mut Isolate, s: &ThreadSafeState) { + i.register_op( + "signal_bind", + s.core_op(json_op(s.stateful_op(op_signal_bind))), + ); + i.register_op( + "signal_unbind", + s.core_op(json_op(s.stateful_op(op_signal_unbind))), + ); + i.register_op( + "signal_poll", + s.core_op(json_op(s.stateful_op(op_signal_poll))), + ); +} + +#[cfg(unix)] +/// The resource for signal stream. +/// The second element is the waker of polling future. +pub struct SignalStreamResource(pub Signal, pub Option); + +#[cfg(unix)] +impl Resource for SignalStreamResource {} + +#[cfg(unix)] +#[derive(Deserialize)] +struct BindSignalArgs { + signo: i32, +} + +#[cfg(unix)] +#[derive(Deserialize)] +struct SignalArgs { + rid: i32, +} + +#[cfg(unix)] +fn op_signal_bind( + state: &ThreadSafeState, + args: Value, + _zero_copy: Option, +) -> Result { + let args: BindSignalArgs = serde_json::from_value(args)?; + let mut table = state.lock_resource_table(); + let rid = table.add( + "signal", + Box::new(SignalStreamResource( + signal(SignalKind::from_raw(args.signo)).expect(""), + None, + )), + ); + Ok(JsonOp::Sync(json!({ + "rid": rid, + }))) +} + +#[cfg(unix)] +fn op_signal_poll( + state: &ThreadSafeState, + args: Value, + _zero_copy: Option, +) -> Result { + let args: SignalArgs = serde_json::from_value(args)?; + let rid = args.rid as u32; + let state_ = state.clone(); + + let future = poll_fn(move |cx| { + let mut table = state_.lock_resource_table(); + if let Some(mut signal) = table.get_mut::(rid) { + signal.1 = Some(cx.waker().clone()); + return signal.0.poll_recv(cx); + } + std::task::Poll::Ready(None) + }) + .then(|result| async move { Ok(json!({ "done": result.is_none() })) }); + + Ok(JsonOp::AsyncUnref(future.boxed())) +} + +#[cfg(unix)] +pub fn op_signal_unbind( + state: &ThreadSafeState, + args: Value, + _zero_copy: Option, +) -> Result { + let args: SignalArgs = serde_json::from_value(args)?; + let rid = args.rid as u32; + let mut table = state.lock_resource_table(); + let resource = table.get::(rid); + if let Some(signal) = resource { + if let Some(waker) = &signal.1 { + // Wakes up the pending poll if exists. + // This prevents the poll future from getting stuck forever. + waker.clone().wake(); + } + } + table.close(rid).ok_or_else(bad_resource)?; + Ok(JsonOp::Sync(json!({}))) +} + +#[cfg(not(unix))] +pub fn op_signal_bind( + _state: &ThreadSafeState, + _args: Value, + _zero_copy: Option, +) -> Result { + unimplemented!(); +} + +#[cfg(not(unix))] +fn op_signal_unbind( + _state: &ThreadSafeState, + _args: Value, + _zero_copy: Option, +) -> Result { + unimplemented!(); +} + +#[cfg(not(unix))] +fn op_signal_poll( + _state: &ThreadSafeState, + _args: Value, + _zero_copy: Option, +) -> Result { + unimplemented!(); +} diff --git a/cli/signal.rs b/cli/signal.rs index 57f2d0d3dd..6f150aeab5 100644 --- a/cli/signal.rs +++ b/cli/signal.rs @@ -1,3 +1,4 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. use deno_core::ErrBox; #[cfg(unix)] diff --git a/cli/worker.rs b/cli/worker.rs index ef72602d41..9fd70eedc2 100644 --- a/cli/worker.rs +++ b/cli/worker.rs @@ -200,6 +200,7 @@ impl MainWorker { ops::random::init(&mut isolate, &state); ops::repl::init(&mut isolate, &state); ops::resources::init(&mut isolate, &state); + ops::signal::init(&mut isolate, &state); ops::timers::init(&mut isolate, &state); ops::worker_host::init(&mut isolate, &state); ops::web_worker::init(&mut isolate, &state); diff --git a/std/manual.md b/std/manual.md index ece5a755b9..204701566f 100644 --- a/std/manual.md +++ b/std/manual.md @@ -428,6 +428,39 @@ Uncaught NotFound: No such file or directory (os error 2) at handleAsyncMsgFromRust (deno/js/dispatch.ts:27:17) ``` +### Handle OS Signals + +[API Reference](https://deno.land/typedoc/index.html#signal) + +You can use `Deno.signal()` function for handling OS signals. + +``` +for await (const _ of Deno.signal(Deno.Signal.SIGINT)) { + console.log("interrupted!"); +} +``` + +`Deno.signal()` also works as a promise. + +``` +await Deno.signal(Deno.Singal.SIGINT); +console.log("interrupted!"); +``` + +If you want to stop watching the signal, you can use `dispose()` method of the +signal object. + +``` +const sig = Deno.signal(Deno.Signal.SIGINT); +setTimeout(() => { sig.dispose(); }, 5000); + +for await (const _ of sig) { + console.log("interrupted"); +} +``` + +The above for-await loop exits after 5 seconds when sig.dispose() is called. + ### Linking to third party code In the above examples, we saw that Deno could execute scripts from URLs. Like