Skip to content
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

feat: Add Deno.exitCode API #23609

Merged
merged 31 commits into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
870e805
Add `Deno.exit.code` support
lukeed Apr 29, 2024
0798c5f
Update lib.deno.ns.d.ts
lukeed Apr 30, 2024
9b7c5fb
fix: use `_exitCode` as `Deno.exit` fallback value
lukeed Apr 30, 2024
9cc8188
chore: apply style format to test file
lukeed Apr 30, 2024
f81d833
chore: apply fmt to `lib.deno.ns.d.ts` file
lukeed Apr 30, 2024
f5b6c83
fix(lint): use primordials
lukeed Apr 30, 2024
2435c75
chore: add addl tests
lukeed Apr 30, 2024
89cf058
Merge branch 'main' into add-exit-code
lukeed Apr 30, 2024
39f72f5
Merge branch 'main' into add-exit-code
lukeed May 8, 2024
581abcc
chore: revert to getter/setter & incorporate node process
lukeed May 8, 2024
fa4fdc0
fix: revert `Deno.exit` dts changes
lukeed May 8, 2024
2c1481f
Merge branch 'main' into add-exit-code
bartlomieju May 13, 2024
9b30d68
updates to match Node 21.5.0
bartlomieju May 13, 2024
7839cc1
Merge branch 'main' into add-exit-code
bartlomieju May 23, 2024
21930ef
improve error a bit
bartlomieju May 23, 2024
8661a99
Merge branch 'main' into add-exit-code
bartlomieju May 23, 2024
974c06b
lint
bartlomieju May 23, 2024
fb2ad14
remove stray files
bartlomieju May 23, 2024
35c78d4
maybe fix?
bartlomieju May 23, 2024
54831ae
Try again
bartlomieju May 23, 2024
703f1b1
Merge branch 'main' into add-exit-code
bartlomieju May 28, 2024
7fb4d91
node tests finally passing, will Deno tests pass too?
bartlomieju May 28, 2024
6cf53f5
update type declarations
bartlomieju May 28, 2024
ba9a252
add spec test
bartlomieju May 28, 2024
bf77c66
Merge branch 'main' into add-exit-code
bartlomieju May 28, 2024
eb03dff
move to 99_main, need to fix exit sanitizer
bartlomieju May 28, 2024
f228e1d
fix test
bartlomieju May 28, 2024
40d35ec
Merge branch 'main' into add-exit-code
bartlomieju May 29, 2024
6c2f234
exit sanitizer
bartlomieju May 29, 2024
0b91e19
Wording
bartlomieju May 29, 2024
393f7b9
update tests and working
bartlomieju May 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
19 changes: 18 additions & 1 deletion cli/js/40_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ const {

import { setExitHandler } from "ext:runtime/30_os.js";

// Capture `Deno` global so that users deleting or mangling it, won't
// have impact on our sanitizers.
const DenoNs = globalThis.Deno;

/**
* @typedef {{
* id: number,
Expand Down Expand Up @@ -101,7 +105,20 @@ function assertExit(fn, isTest) {

try {
const innerResult = await fn(...new SafeArrayIterator(params));
if (innerResult) return innerResult;
const exitCode = DenoNs.exitCode;
if (exitCode !== 0) {
// Reset the code to allow other tests to run...
DenoNs.exitCode = 0;
// ...and fail the current test.
throw new Error(
`${
isTest ? "Test case" : "Bench"
} finished with exit code set to ${exitCode}.`,
);
}
if (innerResult) {
return innerResult;
}
} finally {
setExitHandler(null);
}
Expand Down
94 changes: 94 additions & 0 deletions cli/tests/unit/os_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.

import { assertEquals, assertThrows } from "../../testing/asserts.ts";

Deno.test("Deno.exitCode getter and setter", () => {
// Initial value is 0
assertEquals(Deno.exitCode, 0);

// Set a new value
Deno.exitCode = 5;
assertEquals(Deno.exitCode, 5);

// Reset to initial value
Deno.exitCode = 0;
assertEquals(Deno.exitCode, 0);
});

Deno.test("Setting Deno.exitCode to NaN throws TypeError", () => {
// @ts-expect-error;
Deno.exitCode = "123";
assertEquals(Deno.exitCode, 123);

// Reset
Deno.exitCode = 0;
assertEquals(Deno.exitCode, 0);

// Throws on non-number values
assertThrows(
() => {
// @ts-expect-error Testing for runtime error
Deno.exitCode = "not a number";
},
TypeError,
"Exit code must be a number.",
);
});

Deno.test("Setting Deno.exitCode does not cause an immediate exit", () => {
let exited = false;
const originalExit = Deno.exit;

// @ts-expect-error; read-only
Deno.exit = () => {
exited = true;
};

Deno.exitCode = 1;
assertEquals(exited, false);

// @ts-expect-error; read-only
Deno.exit = originalExit;
});

Deno.test("Running Deno.exit(value) overrides Deno.exitCode", () => {
let args: unknown[] | undefined;

const originalExit = Deno.exit;
// @ts-expect-error; read-only
Deno.exit = (...x) => {
args = x;
};

Deno.exitCode = 42;
Deno.exit(0);

assertEquals(args, [0]);
// @ts-expect-error; read-only
Deno.exit = originalExit;
});

Deno.test("Running Deno.exit() uses Deno.exitCode as fallback", () => {
let args: unknown[] | undefined;

const originalExit = Deno.exit;
// @ts-expect-error; read-only
Deno.exit = (...x) => {
args = x;
};

Deno.exitCode = 42;
Deno.exit();

assertEquals(args, [42]);
// @ts-expect-error; read-only
Deno.exit = originalExit;
});

Deno.test("Retrieving the set exit code before process termination", () => {
Deno.exitCode = 42;
assertEquals(Deno.exitCode, 42);

// Reset to initial value
Deno.exitCode = 0;
});
17 changes: 17 additions & 0 deletions cli/tsc/dts/lib.deno.ns.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1466,6 +1466,23 @@ declare namespace Deno {
*/
export function exit(code?: number): never;

/** The exit code for the Deno process.
*
* If no exit code has been supplied, then Deno will assume a return code of `0`.
*
* When setting an exit code value, a number or non-NaN string must be provided,
* otherwise a TypeError will be thrown.
*
* ```ts
* console.log(Deno.exitCode); //-> 0
* Deno.exitCode = 1;
* console.log(Deno.exitCode); //-> 1
* ```
*
* @category Runtime
*/
export var exitCode: number;

/** An interface containing methods to interact with the process environment
* variables.
*
Expand Down
63 changes: 47 additions & 16 deletions ext/node/polyfills/process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
op_geteuid,
op_node_process_kill,
op_process_abort,
op_set_exit_code,
} from "ext:core/ops";

import { warnNotImplemented } from "ext:deno_node/_utils.ts";
Expand Down Expand Up @@ -49,6 +48,7 @@ import {
} from "ext:deno_node/_next_tick.ts";
import { isWindows } from "ext:deno_node/_util/os.ts";
import * as io from "ext:deno_io/12_io.js";
import * as denoOs from "ext:runtime/30_os.js";

export let argv0 = "";

Expand All @@ -74,28 +74,31 @@ const notImplementedEvents = [
];

export const argv: string[] = ["", ""];
let globalProcessExitCode: number | undefined = undefined;

// In Node, `process.exitCode` is initially `undefined` until set.
// And retains any value as long as it's nullish or number-ish.
let ProcessExitCode: undefined | null | string | number;

/** https://nodejs.org/api/process.html#process_process_exit_code */
export const exit = (code?: number | string) => {
if (code || code === 0) {
if (typeof code === "string") {
const parsedCode = parseInt(code);
globalProcessExitCode = isNaN(parsedCode) ? undefined : parsedCode;
} else {
globalProcessExitCode = code;
}
denoOs.setExitCode(code);
} else if (Number.isNaN(code)) {
denoOs.setExitCode(1);
}

ProcessExitCode = denoOs.getExitCode();
if (!process._exiting) {
process._exiting = true;
// FIXME(bartlomieju): this is wrong, we won't be using syscall to exit
// and thus the `unload` event will not be emitted to properly trigger "emit"
// event on `process`.
process.emit("exit", process.exitCode || 0);
process.emit("exit", ProcessExitCode);
}

process.reallyExit(process.exitCode || 0);
// Any valid thing `process.exitCode` set is already held in Deno.exitCode.
// At this point, we don't have to pass around Node's raw/string exit value.
process.reallyExit(ProcessExitCode);
};

/** https://nodejs.org/api/process.html#processumaskmask */
Expand Down Expand Up @@ -433,14 +436,42 @@ Process.prototype._exiting = _exiting;
/** https://nodejs.org/api/process.html#processexitcode_1 */
Object.defineProperty(Process.prototype, "exitCode", {
get() {
return globalProcessExitCode;
return ProcessExitCode;
},
set(code: number | undefined) {
globalProcessExitCode = code;
code = parseInt(code) || 0;
if (!isNaN(code)) {
op_set_exit_code(code);
set(code: number | string | null | undefined) {
let parsedCode;

if (typeof code === "number") {
if (Number.isNaN(code)) {
parsedCode = 1;
denoOs.setExitCode(parsedCode);
ProcessExitCode = parsedCode;
return;
}

// This is looser than `denoOs.setExitCode` which requires exit code
// to be decimal or string of a decimal, but Node accept eg. 0x10.
parsedCode = parseInt(code);
denoOs.setExitCode(parsedCode);
ProcessExitCode = parsedCode;
return;
}

if (typeof code === "string") {
parsedCode = parseInt(code);
if (Number.isNaN(parsedCode)) {
throw new TypeError(
`The "code" argument must be of type number. Received type ${typeof code} (${code})`,
);
}
denoOs.setExitCode(parsedCode);
ProcessExitCode = parsedCode;
return;
}

// TODO(bartlomieju): hope for the best here. This should be further tightened.
denoOs.setExitCode(code);
ProcessExitCode = code;
},
});

Expand Down
21 changes: 20 additions & 1 deletion runtime/js/30_os.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
op_exec_path,
op_exit,
op_get_env,
op_get_exit_code,
op_gid,
op_hostname,
op_loadavg,
Expand All @@ -21,7 +22,9 @@ import {
const {
Error,
FunctionPrototypeBind,
NumberParseInt,
SymbolFor,
TypeError,
} = primordials;

import { Event, EventTarget } from "ext:deno_web/02_event.js";
Expand Down Expand Up @@ -75,7 +78,7 @@ function exit(code) {
if (typeof code === "number") {
op_set_exit_code(code);
} else {
code = 0;
code = op_get_exit_code();
}

// Dispatches `unload` only when it's not dispatched yet.
Expand All @@ -94,6 +97,20 @@ function exit(code) {
throw new Error("Code not reachable");
}

function getExitCode() {
return op_get_exit_code();
}

function setExitCode(value) {
const code = NumberParseInt(value, 10);
if (typeof code !== "number") {
throw new TypeError(
`Exit code must be a number, got: ${code} (${typeof code}).`,
);
}
op_set_exit_code(code);
}

function setEnv(key, value) {
op_set_env(key, value);
}
Expand Down Expand Up @@ -126,12 +143,14 @@ export {
env,
execPath,
exit,
getExitCode,
gid,
hostname,
loadavg,
networkInterfaces,
osRelease,
osUptime,
setExitCode,
setExitHandler,
systemMemoryInfo,
uid,
Expand Down
8 changes: 8 additions & 0 deletions runtime/js/99_main.js
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,14 @@ ObjectDefineProperties(finalDenoNs, {
return internals.future ? undefined : customInspect;
},
},
exitCode: {
get() {
return os.getExitCode();
},
set(value) {
os.setExitCode(value);
},
},
});

const {
Expand Down
10 changes: 9 additions & 1 deletion runtime/ops/os/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ deno_core::extension!(
op_os_uptime,
op_set_env,
op_set_exit_code,
op_get_exit_code,
op_system_memory_info,
op_uid,
op_runtime_memory_usage,
Expand Down Expand Up @@ -60,12 +61,13 @@ deno_core::extension!(
op_os_uptime,
op_set_env,
op_set_exit_code,
op_get_exit_code,
op_system_memory_info,
op_uid,
op_runtime_memory_usage,
],
middleware = |op| match op.name {
"op_exit" | "op_set_exit_code" =>
"op_exit" | "op_set_exit_code" | "op_get_exit_code" =>
op.with_implementation_from(&deno_core::op_void_sync()),
_ => op,
},
Expand Down Expand Up @@ -164,6 +166,12 @@ fn op_set_exit_code(state: &mut OpState, #[smi] code: i32) {
state.borrow_mut::<ExitCode>().set(code);
}

#[op2(fast)]
#[smi]
fn op_get_exit_code(state: &mut OpState) -> i32 {
state.borrow_mut::<ExitCode>().get()
}

#[op2(fast)]
fn op_exit(state: &mut OpState) {
let code = state.borrow::<ExitCode>().get();
Expand Down
5 changes: 5 additions & 0 deletions tests/specs/run/exit_code/__test__.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"args": "run main.js",
"exitCode": 42,
"output": "main.out"
}
7 changes: 7 additions & 0 deletions tests/specs/run/exit_code/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
if (Deno.exitCode != 0) {
throw new Error("boom!");
}

Deno.exitCode = 42;

console.log("Deno.exitCode", Deno.exitCode);
1 change: 1 addition & 0 deletions tests/specs/run/exit_code/main.out
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Deno.exitCode 42
5 changes: 5 additions & 0 deletions tests/specs/test/exit_code/__test__.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"args": "test main.js",
"exitCode": 1,
"output": "main.out"
}
3 changes: 3 additions & 0 deletions tests/specs/test/exit_code/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Deno.test("Deno.exitCode", () => {
Deno.exitCode = 42;
});