feat(std/node): Add support for process.on("exit") (#8940)

This commit adds support for process.on("exit") by appending a listener to
the unload event. Luckily, unload works pretty much the same as on("exit")
since it won't schedule any additional work in the even loop either.

This commit also solves an error in the Node implementation, since "process.argv"
didn't contained the main module route as it was supposed to.
This commit is contained in:
Steven Guerrero 2021-01-25 11:30:31 -05:00 committed by GitHub
parent e0eb111e3e
commit 1f8b83ba1a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 276 additions and 106 deletions

View file

@ -1,5 +1,5 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
import { process as processModule } from "./process.ts"; import processModule from "./process.ts";
import { Buffer as bufferModule } from "./buffer.ts"; import { Buffer as bufferModule } from "./buffer.ts";
import timers from "./timers.ts"; import timers from "./timers.ts";

View file

@ -1,6 +1,6 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
/// <reference path="./global.d.ts" /> /// <reference path="./global.d.ts" />
import { process as processModule } from "./process.ts"; import processModule from "./process.ts";
import { Buffer as bufferModule } from "./buffer.ts"; import { Buffer as bufferModule } from "./buffer.ts";
import timers from "./timers.ts"; import timers from "./timers.ts";

View file

@ -1,33 +1,110 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
import { notImplemented } from "./_utils.ts"; import { notImplemented } from "./_utils.ts";
import EventEmitter from "./events.ts";
import { fromFileUrl } from "../path/mod.ts";
const notImplementedEvents = [
"beforeExit",
"disconnect",
"message",
"multipleResolves",
"rejectionHandled",
"SIGBREAK",
"SIGBUS",
"SIGFPE",
"SIGHUP",
"SIGILL",
"SIGINT",
"SIGSEGV",
"SIGTERM",
"SIGWINCH",
"uncaughtException",
"uncaughtExceptionMonitor",
"unhandledRejection",
"warning",
];
/** https://nodejs.org/api/process.html#process_process_arch */ /** https://nodejs.org/api/process.html#process_process_arch */
export const arch = Deno.build.arch; export const arch = Deno.build.arch;
function getArguments() {
return [Deno.execPath(), fromFileUrl(Deno.mainModule), ...Deno.args];
}
//deno-lint-ignore ban-ts-comment
//@ts-ignore
const _argv: {
[Deno.customInspect]: () => string;
[key: number]: string;
} = [];
Object.defineProperty(_argv, Deno.customInspect, {
enumerable: false,
configurable: false,
get: function () {
return getArguments();
},
});
/**
* https://nodejs.org/api/process.html#process_process_argv
* Read permissions are required in order to get the executable route
* */
export const argv: Record<string, string> = new Proxy(_argv, {
get(target, prop) {
if (prop === Deno.customInspect) {
return target[Deno.customInspect];
}
return getArguments()[prop as number];
},
ownKeys() {
return Reflect.ownKeys(getArguments());
},
});
/** https://nodejs.org/api/process.html#process_process_chdir_directory */ /** https://nodejs.org/api/process.html#process_process_chdir_directory */
export const chdir = Deno.chdir; export const chdir = Deno.chdir;
/** https://nodejs.org/api/process.html#process_process_cwd */ /** https://nodejs.org/api/process.html#process_process_cwd */
export const cwd = Deno.cwd; export const cwd = Deno.cwd;
//deno-lint-ignore ban-ts-comment
//@ts-ignore
const _env: {
[Deno.customInspect]: () => string;
} = {};
Object.defineProperty(_env, Deno.customInspect, {
enumerable: false,
configurable: false,
get: function () {
return Deno.env.toObject();
},
});
/**
* https://nodejs.org/api/process.html#process_process_env
* Requires env permissions
* */
export const env: Record<string, string> = new Proxy(_env, {
get(target, prop) {
if (prop === Deno.customInspect) {
return target[Deno.customInspect];
}
return Deno.env.get(String(prop));
},
ownKeys() {
return Reflect.ownKeys(Deno.env.toObject());
},
set(_target, prop, value) {
Deno.env.set(String(prop), String(value));
return value;
},
});
/** https://nodejs.org/api/process.html#process_process_exit_code */ /** https://nodejs.org/api/process.html#process_process_exit_code */
export const exit = Deno.exit; export const exit = Deno.exit;
/** https://nodejs.org/api/process.html#process_process_pid */
export const pid = Deno.pid;
/** https://nodejs.org/api/process.html#process_process_platform */
export const platform = Deno.build.os === "windows" ? "win32" : Deno.build.os;
/** https://nodejs.org/api/process.html#process_process_version */
export const version = `v${Deno.version.deno}`;
/** https://nodejs.org/api/process.html#process_process_versions */
export const versions = {
node: Deno.version.deno,
...Deno.version,
};
/** https://nodejs.org/api/process.html#process_process_nexttick_callback_args */ /** https://nodejs.org/api/process.html#process_process_nexttick_callback_args */
export function nextTick(this: unknown, cb: () => void): void; export function nextTick(this: unknown, cb: () => void): void;
export function nextTick<T extends Array<unknown>>( export function nextTick<T extends Array<unknown>>(
@ -47,17 +124,103 @@ export function nextTick<T extends Array<unknown>>(
} }
} }
/** https://nodejs.org/api/process.html#process_process */ /** https://nodejs.org/api/process.html#process_process_pid */
// @deprecated `import { process } from 'process'` for backwards compatibility with old deno versions export const pid = Deno.pid;
export const process = {
arch, /** https://nodejs.org/api/process.html#process_process_platform */
chdir, export const platform = Deno.build.os === "windows" ? "win32" : Deno.build.os;
cwd,
exit, /** https://nodejs.org/api/process.html#process_process_version */
pid, export const version = `v${Deno.version.deno}`;
platform,
version, /** https://nodejs.org/api/process.html#process_process_versions */
versions, export const versions = {
node: Deno.version.deno,
...Deno.version,
};
class Process extends EventEmitter {
constructor() {
super();
//This causes the exit event to be binded to the unload event
window.addEventListener("unload", () => {
//TODO(Soremwar)
//Get the exit code from the unload event
super.emit("exit", 0);
});
}
/** https://nodejs.org/api/process.html#process_process_arch */
arch = arch;
/**
* https://nodejs.org/api/process.html#process_process_argv
* Read permissions are required in order to get the executable route
* */
argv = argv;
/** https://nodejs.org/api/process.html#process_process_chdir_directory */
chdir = chdir;
/** https://nodejs.org/api/process.html#process_process_cwd */
cwd = cwd;
/** https://nodejs.org/api/process.html#process_process_exit_code */
exit = exit;
/**
* https://nodejs.org/api/process.html#process_process_env
* Requires env permissions
* */
env = env;
/** https://nodejs.org/api/process.html#process_process_nexttick_callback_args */
nextTick = nextTick;
/** https://nodejs.org/api/process.html#process_process_events */
//deno-lint-ignore ban-types
on(event: typeof notImplementedEvents[number], listener: Function): never;
on(event: "exit", listener: (code: number) => void): this;
//deno-lint-ignore no-explicit-any
on(event: string, listener: (...args: any[]) => void): this {
if (notImplementedEvents.includes(event)) {
notImplemented();
}
super.on(event, listener);
return this;
}
/** https://nodejs.org/api/process.html#process_process_pid */
pid = pid;
/** https://nodejs.org/api/process.html#process_process_platform */
platform = platform;
removeAllListeners(_event: string): never {
notImplemented();
}
removeListener(
event: typeof notImplementedEvents[number],
//deno-lint-ignore ban-types
listener: Function,
): never;
removeListener(event: "exit", listener: (code: number) => void): this;
//deno-lint-ignore no-explicit-any
removeListener(event: string, listener: (...args: any[]) => void): this {
if (notImplementedEvents.includes(event)) {
notImplemented();
}
super.removeListener("exit", listener);
return this;
}
/** https://nodejs.org/api/process.html#process_process_stderr */
get stderr() { get stderr() {
return { return {
fd: Deno.stderr.rid, fd: Deno.stderr.rid,
@ -79,7 +242,9 @@ export const process = {
notImplemented(); notImplemented();
}, },
}; };
}, }
/** https://nodejs.org/api/process.html#process_process_stdin */
get stdin() { get stdin() {
return { return {
fd: Deno.stdin.rid, fd: Deno.stdin.rid,
@ -96,7 +261,9 @@ export const process = {
notImplemented(); notImplemented();
}, },
}; };
}, }
/** https://nodejs.org/api/process.html#process_process_stdout */
get stdout() { get stdout() {
return { return {
fd: Deno.stdout.rid, fd: Deno.stdout.rid,
@ -118,49 +285,17 @@ export const process = {
notImplemented(); notImplemented();
}, },
}; };
}, }
/** https://nodejs.org/api/process.html#process_process_events */ /** https://nodejs.org/api/process.html#process_process_version */
// on is not exported by node, it is only available within process: version = version;
// node --input-type=module -e "import { on } from 'process'; console.log(on)"
// deno-lint-ignore ban-types
on(_event: string, _callback: Function): void {
// TODO(rsp): to be implemented
notImplemented();
},
/** https://nodejs.org/api/process.html#process_process_argv */ /** https://nodejs.org/api/process.html#process_process_versions */
get argv(): string[] { versions = versions;
// Getter delegates --allow-env and --allow-read until request }
// Getter also allows the export Proxy instance to function as intended
return [Deno.execPath(), ...Deno.args];
},
/** https://nodejs.org/api/process.html#process_process_env */ /** https://nodejs.org/api/process.html#process_process */
get env(): { [index: string]: string } { const process = new Process();
// Getter delegates --allow-env and --allow-read until request
// Getter also allows the export Proxy instance to function as intended
return Deno.env.toObject();
},
nextTick,
};
/**
* https://nodejs.org/api/process.html#process_process_argv
* @example `import { argv } from './std/node/process.ts'; console.log(argv)`
*/
// Proxy delegates --allow-env and --allow-read to request time, even for exports
export const argv = new Proxy(process.argv, {});
/**
* https://nodejs.org/api/process.html#process_process_env
* @example `import { env } from './std/node/process.ts'; console.log(env)`
*/
// Proxy delegates --allow-env and --allow-read to request time, even for exports
export const env = new Proxy(process.env, {});
// import process from './std/node/process.ts'
export default process;
Object.defineProperty(process, Symbol.toStringTag, { Object.defineProperty(process, Symbol.toStringTag, {
enumerable: false, enumerable: false,
@ -168,3 +303,16 @@ Object.defineProperty(process, Symbol.toStringTag, {
configurable: false, configurable: false,
value: "process", value: "process",
}); });
export const removeListener = process.removeListener;
export const removeAllListeners = process.removeAllListeners;
export const stderr = process.stderr;
export const stdin = process.stdin;
export const stdout = process.stdout;
export default process;
//TODO(Soremwar)
//Remove on 1.0
//Kept for backwars compatibility with std
export { process };

View file

@ -0,0 +1,19 @@
import "./global.ts";
//deno-lint-ignore no-undef
process.on("exit", () => {
console.log(1);
});
function unexpected() {
console.log(null);
}
//deno-lint-ignore no-undef
process.on("exit", unexpected);
//deno-lint-ignore no-undef
process.removeListener("exit", unexpected);
//deno-lint-ignore no-undef
process.on("exit", () => {
console.log(2);
});

View file

@ -1,37 +1,12 @@
// deno-lint-ignore-file no-undef // deno-lint-ignore-file no-undef
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
import { assert, assertEquals, assertThrows } from "../testing/asserts.ts";
import * as path from "../path/mod.ts";
import * as all from "./process.ts";
import { argv, env } from "./process.ts";
import { delay } from "../async/delay.ts";
import "./global.ts"; import "./global.ts";
import { assert, assertEquals, assertThrows } from "../testing/asserts.ts";
// NOTE: Deno.execPath() (and thus process.argv) currently requires --allow-env import { stripColor } from "../fmt/colors.ts";
// (Also Deno.env.toObject() (and process.env) requires --allow-env but it's more obvious) import * as path from "../path/mod.ts";
import { delay } from "../async/delay.ts";
Deno.test({ import { env } from "./process.ts";
name: "process exports are as they should be",
fn() {
// * should be the same as process, default, and globalThis.process
// without the export aliases, and with properties that are not standalone
const allKeys = new Set<string>(Object.keys(all));
// without { process } for deno b/c
allKeys.delete("process");
// without esm default
allKeys.delete("default");
// with on, stdin, stderr, and stdout, which is not exported via *
allKeys.add("on");
allKeys.add("stdin");
allKeys.add("stderr");
allKeys.add("stdout");
const allStr = Array.from(allKeys).sort().join(" ");
assertEquals(Object.keys(all.default).sort().join(" "), allStr);
assertEquals(Object.keys(all.process).sort().join(" "), allStr);
assertEquals(Object.keys(process).sort().join(" "), allStr);
},
});
Deno.test({ Deno.test({
name: "process.cwd and process.chdir success", name: "process.cwd and process.chdir success",
@ -103,7 +78,7 @@ Deno.test({
Deno.test({ Deno.test({
name: "process.on", name: "process.on",
fn() { async fn() {
assertEquals(typeof process.on, "function"); assertEquals(typeof process.on, "function");
assertThrows( assertThrows(
() => { () => {
@ -112,6 +87,33 @@ Deno.test({
Error, Error,
"implemented", "implemented",
); );
let triggered = false;
process.on("exit", () => {
triggered = true;
});
process.emit("exit");
assert(triggered);
const cwd = path.dirname(path.fromFileUrl(import.meta.url));
const p = Deno.run({
cmd: [
Deno.execPath(),
"run",
"./process_exit_test.ts",
],
cwd,
stdout: "piped",
});
const decoder = new TextDecoder();
const rawOutput = await p.output();
assertEquals(
stripColor(decoder.decode(rawOutput).trim()),
"1\n2",
);
p.close();
}, },
}); });
@ -119,12 +121,14 @@ Deno.test({
name: "process.argv", name: "process.argv",
fn() { fn() {
assert(Array.isArray(process.argv)); assert(Array.isArray(process.argv));
assert(Array.isArray(argv));
assert( assert(
process.argv[0].match(/[^/\\]*deno[^/\\]*$/), process.argv[0].match(/[^/\\]*deno[^/\\]*$/),
"deno included in the file name of argv[0]", "deno included in the file name of argv[0]",
); );
// we cannot test for anything else (we see test runner arguments here) assertEquals(
process.argv[1],
path.fromFileUrl(Deno.mainModule),
);
}, },
}); });
@ -136,9 +140,8 @@ Deno.test({
assertEquals(typeof (process.env.HELLO), "string"); assertEquals(typeof (process.env.HELLO), "string");
assertEquals(process.env.HELLO, "WORLD"); assertEquals(process.env.HELLO, "WORLD");
// TODO(caspervonb) test the globals in a different setting (they're broken) assertEquals(typeof env.HELLO, "string");
// assertEquals(typeof env.HELLO, "string"); assertEquals(env.HELLO, "WORLD");
// assertEquals(env.HELLO, "WORLD");
}, },
}); });