From 4a4d3625a2ce3d654266e2d5f2c31db55323839f Mon Sep 17 00:00:00 2001 From: Yoshiya Hinosawa Date: Sat, 7 Dec 2019 19:50:37 +0900 Subject: [PATCH] feat: signal handler --- cli/js/deno.ts | 1 + cli/js/dispatch.ts | 3 + cli/js/errors.ts | 4 +- cli/js/lib.deno_runtime.d.ts | 100 ++++++++++++++++++ cli/js/process.ts | 2 +- cli/js/signal_test.ts | 174 +++++++++++++++++++++++++++++++ cli/js/signals.ts | 196 +++++++++++++++++++++++++++++++++++ cli/js/unit_tests.ts | 1 + cli/lib.rs | 2 +- cli/ops/mod.rs | 1 + cli/ops/signal.rs | 139 +++++++++++++++++++++++++ cli/signal.rs | 12 +++ cli/worker.rs | 1 + std/manual.md | 33 ++++++ 14 files changed, 666 insertions(+), 3 deletions(-) create mode 100644 cli/js/signal_test.ts create mode 100644 cli/js/signals.ts create mode 100644 cli/ops/signal.rs diff --git a/cli/js/deno.ts b/cli/js/deno.ts index 513d9f28429f1e..f71fad2307121b 100644 --- a/cli/js/deno.ts +++ b/cli/js/deno.ts @@ -100,6 +100,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 f5049cca88f17a..42b03f7363e140 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_BIND_SIGNAL: number; +export let OP_UNBIND_SIGNAL: number; +export let OP_POLL_SIGNAL: number; /** **WARNING:** This is only available during the snapshotting process and is * unavailable at runtime. */ diff --git a/cli/js/errors.ts b/cli/js/errors.ts index f8210fb60ce664..41cd76e5ac3b90 100644 --- a/cli/js/errors.ts +++ b/cli/js/errors.ts @@ -79,5 +79,7 @@ export enum ErrorKind { TypeError = 51, /** TODO this is a DomException type, and should be moved out of here when possible */ - DataCloneError = 52 + DataCloneError = 52, + + StreamDisposed = 53 } diff --git a/cli/js/lib.deno_runtime.d.ts b/cli/js/lib.deno_runtime.d.ts index 6e3e2384a0d104..370e612e16c8ba 100644 --- a/cli/js/lib.deno_runtime.d.ts +++ b/cli/js/lib.deno_runtime.d.ts @@ -2106,6 +2106,106 @@ declare namespace Deno { */ export const args: string[]; + // @url js/signal.d.ts + 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 short hand for Deno.signal(Deno.Signal.SIGALRM). + */ + alarm: () => SignalStream; + /** + * Returns the stream of SIGCHLD signals. + * This method is the short hand for Deno.signal(Deno.Signal.SIGCHLD). + */ + child: () => SignalStream; + /** + * Returns the stream of SIGHUP signals. + * This method is the short hand for Deno.signal(Deno.Signal.SIGHUP). + */ + hungup: () => SignalStream; + /** + * Returns the stream of SIGINFO signals. + * This method is the short hand for Deno.signal(Deno.Signal.SIGINFO). + */ + info: () => SignalStream; + /** + * Returns the stream of SIGINT signals. + * This method is the short hand for Deno.signal(Deno.Signal.SIGINT). + */ + interrupt: () => SignalStream; + /** + * Returns the stream of SIGIO signals. + * This method is the short hand for Deno.signal(Deno.Signal.SIGIO). + */ + io: () => SignalStream; + /** + * Returns the stream of SIGPIPE signals. + * This method is the short hand for Deno.signal(Deno.Signal.SIGPIPE). + */ + pipe: () => SignalStream; + /** + * Returns the stream of SIGQUIT signals. + * This method is the short hand for Deno.signal(Deno.Signal.SIGQUIT). + */ + quit: () => SignalStream; + /** + * Returns the stream of SIGTERM signals. + * This method is the short hand for Deno.signal(Deno.Signal.SIGTERM). + */ + terminate: () => SignalStream; + /** + * Returns the stream of SIGUSR1 signals. + * This method is the short hand for Deno.signal(Deno.Signal.SIGUSR1). + */ + userDefined1: () => SignalStream; + /** + * Returns the stream of SIGUSR2 signals. + * This method is the short hand for Deno.signal(Deno.Signal.SIGUSR2). + */ + userDefined2: () => SignalStream; + /** + * Returns the stream of SIGWINCH signals. + * This method is the short hand 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 8ad6384b7414b8..5267763c1e7417 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 00000000000000..20ede294a92043 --- /dev/null +++ b/cli/js/signal_test.ts @@ -0,0 +1,174 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { 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") { + testPerm({ run: true }, 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.info(); // for SIGINFO + }, + 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 }, async function signalStreamTest(): Promise { + 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); + }); + + testPerm({ run: true }, async function signalPromiseTest(): Promise { + const sig = Deno.signal(Deno.Signal.SIGUSR1); + setTimeout(() => { + Deno.kill(Deno.pid, Deno.Signal.SIGUSR1); + }, 20); + await sig; + sig.dispose(); + }); + + 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.info(); // for SIGINFO + 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 00000000000000..c3ee965f57bc66 --- /dev/null +++ b/cli/js/signals.ts @@ -0,0 +1,196 @@ +// 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 { DenoError, ErrorKind } from "./errors.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 { + return new SignalStream(signo); +} + +export const signals = { + /** + * Returns the stream of SIGALRM signals. + * This method is the short hand for Deno.signal(Deno.Signal.SIGALRM). + */ + alarm(): SignalStream { + return createSignalStream(Signal.SIGALRM); + }, + /** + * Returns the stream of SIGCHLD signals. + * This method is the short hand for Deno.signal(Deno.Signal.SIGCHLD). + */ + child(): SignalStream { + return createSignalStream(Signal.SIGCHLD); + }, + /** + * Returns the stream of SIGHUP signals. + * This method is the short hand for Deno.signal(Deno.Signal.SIGHUP). + */ + hungup(): SignalStream { + return createSignalStream(Signal.SIGHUP); + }, + /** + * Returns the stream of SIGINFO signals. + * This method is the short hand for Deno.signal(Deno.Signal.SIGINFO). + */ + info(): SignalStream { + return createSignalStream(Signal.SIGINFO); + }, + /** + * Returns the stream of SIGINT signals. + * This method is the short hand for Deno.signal(Deno.Signal.SIGINT). + */ + interrupt(): SignalStream { + return createSignalStream(Signal.SIGINT); + }, + /** + * Returns the stream of SIGIO signals. + * This method is the short hand for Deno.signal(Deno.Signal.SIGIO). + */ + io(): SignalStream { + return createSignalStream(Signal.SIGIO); + }, + /** + * Returns the stream of SIGPIPE signals. + * This method is the short hand for Deno.signal(Deno.Signal.SIGPIPE). + */ + pipe(): SignalStream { + return createSignalStream(Signal.SIGPIPE); + }, + /** + * Returns the stream of SIGQUIT signals. + * This method is the short hand for Deno.signal(Deno.Signal.SIGQUIT). + */ + quit(): SignalStream { + return createSignalStream(Signal.SIGQUIT); + }, + /** + * Returns the stream of SIGTERM signals. + * This method is the short hand for Deno.signal(Deno.Signal.SIGTERM). + */ + terminate(): SignalStream { + return createSignalStream(Signal.SIGTERM); + }, + /** + * Returns the stream of SIGUSR1 signals. + * This method is the short hand for Deno.signal(Deno.Signal.SIGUSR1). + */ + userDefined1(): SignalStream { + return createSignalStream(Signal.SIGUSR1); + }, + /** + * Returns the stream of SIGUSR2 signals. + * This method is the short hand for Deno.signal(Deno.Signal.SIGUSR2). + */ + userDefined2(): SignalStream { + return createSignalStream(Signal.SIGUSR2); + }, + /** + * Returns the stream of SIGWINCH signals. + * This method is the short hand for Deno.signal(Deno.Signal.SIGWINCH). + */ + windowChange(): SignalStream { + return createSignalStream(Signal.SIGWINCH); + } +}; + +const createSignalStream = (signal: number): SignalStream => { + if (build.os === "win") { + throw new Error("not implemented!"); + } + return new SignalStream(signal); +}; + +const STREAM_DISPOSED_MESSAGE = + "No signal is available because signal stream is disposed"; + +export class SignalStream implements AsyncIterator, PromiseLike { + private rid: number; + private currentPromise: Promise = Promise.resolve(); + private disposed = false; + constructor(signo: number) { + this.rid = sendSync(dispatch.OP_BIND_SIGNAL, { signo }).rid; + this.loop(); + } + + private async pollSignal(): Promise { + const { done } = await sendAsync(dispatch.OP_POLL_SIGNAL, { + rid: this.rid + }); + + if (done) { + throw new DenoError(ErrorKind.StreamDisposed, STREAM_DISPOSED_MESSAGE); + } + } + + private async loop(): Promise { + while (!this.disposed) { + this.currentPromise = this.pollSignal(); + try { + await this.currentPromise; + } catch (e) { + if (e instanceof DenoError && e.kind === ErrorKind.StreamDisposed) { + // If the stream is disposed, then returns silently. + return; + } + // If it's not StreamDisposed error, it's an unexpected error. + throw e; + } + } + } + + then( + f: (v: void) => T | Promise, + g?: (v: void) => S | Promise + ): Promise { + return this.currentPromise.then(f, g); + } + + async next(): Promise> { + try { + await this.currentPromise; + return { done: false, value: undefined }; + } catch (e) { + if (e instanceof DenoError && e.kind === ErrorKind.StreamDisposed) { + return { done: true, value: undefined }; + } + throw e; + } + } + + [Symbol.asyncIterator](): AsyncIterator { + return this; + } + + dispose(): void { + this.disposed = true; + sendSync(dispatch.OP_UNBIND_SIGNAL, { rid: this.rid }); + } +} diff --git a/cli/js/unit_tests.ts b/cli/js/unit_tests.ts index 084661ab84dd4a..47ae06b19b060b 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 a57f224e207437..c900917c40308c 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 f93c5a0609999e..16fcf3ee53be8b 100644 --- a/cli/ops/mod.rs +++ b/cli/ops/mod.rs @@ -21,6 +21,7 @@ pub mod process; pub mod random; pub mod repl; pub mod resources; +pub mod signal; pub mod timers; pub mod tls; pub mod workers; diff --git a/cli/ops/signal.rs b/cli/ops/signal.rs new file mode 100644 index 00000000000000..ee96557cc6fc3a --- /dev/null +++ b/cli/ops/signal.rs @@ -0,0 +1,139 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +#[cfg(unix)] +use super::dispatch_json::Deserialize; +use super::dispatch_json::{JsonOp, Value}; +#[cfg(unix)] +use crate::deno_error::bad_resource; +use crate::ops::json_op; +#[cfg(unix)] +use crate::signal::SignalStreamResource; +use crate::state::ThreadSafeState; +use deno_core::*; +#[cfg(unix)] +use futures::future::{poll_fn, FutureExt}; +#[cfg(unix)] +use serde_json; +#[cfg(unix)] +use tokio::signal::unix::{signal, SignalKind}; + +pub fn init(i: &mut Isolate, s: &ThreadSafeState) { + i.register_op( + "bind_signal", + s.core_op(json_op(s.stateful_op(op_bind_signal))), + ); + i.register_op( + "unbind_signal", + s.core_op(json_op(s.stateful_op(op_unbind_signal))), + ); + i.register_op( + "poll_signal", + s.core_op(json_op(s.stateful_op(op_poll_signal))), + ); +} + +#[cfg(unix)] +#[derive(Deserialize)] +struct BindSignalArgs { + signo: i32, +} + +#[cfg(unix)] +#[derive(Deserialize)] +struct UnbindSignalArgs { + rid: i32, +} + +#[cfg(unix)] +#[derive(Deserialize)] +struct PollSignalArgs { + rid: i32, +} + +#[cfg(unix)] +fn op_bind_signal( + 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_poll_signal( + state: &ThreadSafeState, + args: Value, + _zero_copy: Option, +) -> Result { + let args: PollSignalArgs = 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::Async(future.boxed())) +} + +#[cfg(unix)] +pub fn op_unbind_signal( + state: &ThreadSafeState, + args: Value, + _zero_copy: Option, +) -> Result { + let args: UnbindSignalArgs = 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 { + waker.clone().wake(); + } + } + table.close(rid).ok_or_else(bad_resource)?; + Ok(JsonOp::Sync(json!({}))) +} + +#[cfg(not(unix))] +pub fn op_bind_signal( + _state: &ThreadSafeState, + _args: Value, + _zero_copy: Option, +) -> Result { + unimplemented!(); +} + +#[cfg(not(unix))] +fn op_unbind_signal( + _state: &ThreadSafeState, + _args: Value, + _zero_copy: Option, +) -> Result { + unimplemented!(); +} + +#[cfg(not(unix))] +fn op_poll_signal( + _state: &ThreadSafeState, + _args: Value, + _zero_copy: Option, +) -> Result { + unimplemented!(); +} diff --git a/cli/signal.rs b/cli/signal.rs index 57f2d0d3ddf2b9..e577ad5cd9d9e6 100644 --- a/cli/signal.rs +++ b/cli/signal.rs @@ -1,4 +1,10 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. use deno_core::ErrBox; +#[cfg(unix)] +use deno_core::Resource; + +#[cfg(unix)] +use tokio::signal::unix::Signal; #[cfg(unix)] pub fn kill(pid: i32, signo: i32) -> Result<(), ErrBox> { @@ -14,3 +20,9 @@ pub fn kill(_pid: i32, _signal: i32) -> Result<(), ErrBox> { // TODO: implement this for windows Ok(()) } + +#[cfg(unix)] +pub struct SignalStreamResource(pub Signal, pub Option); + +#[cfg(unix)] +impl Resource for SignalStreamResource {} diff --git a/cli/worker.rs b/cli/worker.rs index 7faf17e601299b..193223a2f56d7a 100644 --- a/cli/worker.rs +++ b/cli/worker.rs @@ -72,6 +72,7 @@ impl Worker { ops::random::init(&mut i, &state); ops::repl::init(&mut i, &state); ops::resources::init(&mut i, &state); + ops::signal::init(&mut i, &state); ops::timers::init(&mut i, &state); ops::workers::init(&mut i, &state); diff --git a/std/manual.md b/std/manual.md index 3f215c286b4509..37c7038d045bc8 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