1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-25 15:29:32 -05:00

feat: Add Deno.exitCode API (#23609)

This commits adds the ability to set a would-be exit code 
for the Deno process without forcing an immediate exit, 
through the new `Deno.exitCode` API.

- **Implements `Deno.exitCode` getter and setter**: Adds support for
setting
and retrieving a would-be exit code via `Deno.exitCode`.
This allows for asynchronous cleanup before process termination 
without immediately exiting.
- **Ensures type safety**: The setter for `Deno.exitCode` validates that
the provided value is a number, throwing a TypeError if not, to ensure
that
only valid exit codes are set.

Closes to #23605

---------

Co-authored-by: Bartek Iwańczuk <biwanczuk@gmail.com>
This commit is contained in:
Luke Edwards 2024-05-29 16:16:27 -07:00 committed by GitHub
parent cf611fbf54
commit 13723f267e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 322 additions and 24 deletions

View file

@ -28,6 +28,10 @@ const {
import { setExitHandler } from "ext:runtime/30_os.js"; 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 {{ * @typedef {{
* id: number, * id: number,
@ -101,7 +105,20 @@ function assertExit(fn, isTest) {
try { try {
const innerResult = await fn(...new SafeArrayIterator(params)); 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 { } finally {
setExitHandler(null); setExitHandler(null);
} }

94
cli/tests/unit/os_test.ts Normal file
View file

@ -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;
});

View file

@ -1466,6 +1466,23 @@ declare namespace Deno {
*/ */
export function exit(code?: number): never; 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 /** An interface containing methods to interact with the process environment
* variables. * variables.
* *

View file

@ -10,7 +10,6 @@ import {
op_geteuid, op_geteuid,
op_node_process_kill, op_node_process_kill,
op_process_abort, op_process_abort,
op_set_exit_code,
} from "ext:core/ops"; } from "ext:core/ops";
import { warnNotImplemented } from "ext:deno_node/_utils.ts"; import { warnNotImplemented } from "ext:deno_node/_utils.ts";
@ -49,6 +48,7 @@ import {
} from "ext:deno_node/_next_tick.ts"; } from "ext:deno_node/_next_tick.ts";
import { isWindows } from "ext:deno_node/_util/os.ts"; import { isWindows } from "ext:deno_node/_util/os.ts";
import * as io from "ext:deno_io/12_io.js"; import * as io from "ext:deno_io/12_io.js";
import * as denoOs from "ext:runtime/30_os.js";
export let argv0 = ""; export let argv0 = "";
@ -74,28 +74,31 @@ const notImplementedEvents = [
]; ];
export const argv: string[] = ["", ""]; 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 */ /** https://nodejs.org/api/process.html#process_process_exit_code */
export const exit = (code?: number | string) => { export const exit = (code?: number | string) => {
if (code || code === 0) { if (code || code === 0) {
if (typeof code === "string") { denoOs.setExitCode(code);
const parsedCode = parseInt(code); } else if (Number.isNaN(code)) {
globalProcessExitCode = isNaN(parsedCode) ? undefined : parsedCode; denoOs.setExitCode(1);
} else {
globalProcessExitCode = code;
}
} }
ProcessExitCode = denoOs.getExitCode();
if (!process._exiting) { if (!process._exiting) {
process._exiting = true; process._exiting = true;
// FIXME(bartlomieju): this is wrong, we won't be using syscall to exit // 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" // and thus the `unload` event will not be emitted to properly trigger "emit"
// event on `process`. // 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 */ /** https://nodejs.org/api/process.html#processumaskmask */
@ -433,14 +436,42 @@ Process.prototype._exiting = _exiting;
/** https://nodejs.org/api/process.html#processexitcode_1 */ /** https://nodejs.org/api/process.html#processexitcode_1 */
Object.defineProperty(Process.prototype, "exitCode", { Object.defineProperty(Process.prototype, "exitCode", {
get() { get() {
return globalProcessExitCode; return ProcessExitCode;
}, },
set(code: number | undefined) { set(code: number | string | null | undefined) {
globalProcessExitCode = code; let parsedCode;
code = parseInt(code) || 0;
if (!isNaN(code)) { if (typeof code === "number") {
op_set_exit_code(code); 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;
}, },
}); });

View file

@ -7,6 +7,7 @@ import {
op_exec_path, op_exec_path,
op_exit, op_exit,
op_get_env, op_get_env,
op_get_exit_code,
op_gid, op_gid,
op_hostname, op_hostname,
op_loadavg, op_loadavg,
@ -21,7 +22,9 @@ import {
const { const {
Error, Error,
FunctionPrototypeBind, FunctionPrototypeBind,
NumberParseInt,
SymbolFor, SymbolFor,
TypeError,
} = primordials; } = primordials;
import { Event, EventTarget } from "ext:deno_web/02_event.js"; import { Event, EventTarget } from "ext:deno_web/02_event.js";
@ -75,7 +78,7 @@ function exit(code) {
if (typeof code === "number") { if (typeof code === "number") {
op_set_exit_code(code); op_set_exit_code(code);
} else { } else {
code = 0; code = op_get_exit_code();
} }
// Dispatches `unload` only when it's not dispatched yet. // Dispatches `unload` only when it's not dispatched yet.
@ -94,6 +97,20 @@ function exit(code) {
throw new Error("Code not reachable"); 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) { function setEnv(key, value) {
op_set_env(key, value); op_set_env(key, value);
} }
@ -126,12 +143,14 @@ export {
env, env,
execPath, execPath,
exit, exit,
getExitCode,
gid, gid,
hostname, hostname,
loadavg, loadavg,
networkInterfaces, networkInterfaces,
osRelease, osRelease,
osUptime, osUptime,
setExitCode,
setExitHandler, setExitHandler,
systemMemoryInfo, systemMemoryInfo,
uid, uid,

View file

@ -674,6 +674,14 @@ ObjectDefineProperties(finalDenoNs, {
return internals.future ? undefined : customInspect; return internals.future ? undefined : customInspect;
}, },
}, },
exitCode: {
get() {
return os.getExitCode();
},
set(value) {
os.setExitCode(value);
},
},
}); });
const { const {

View file

@ -32,6 +32,7 @@ deno_core::extension!(
op_os_uptime, op_os_uptime,
op_set_env, op_set_env,
op_set_exit_code, op_set_exit_code,
op_get_exit_code,
op_system_memory_info, op_system_memory_info,
op_uid, op_uid,
op_runtime_memory_usage, op_runtime_memory_usage,
@ -60,12 +61,13 @@ deno_core::extension!(
op_os_uptime, op_os_uptime,
op_set_env, op_set_env,
op_set_exit_code, op_set_exit_code,
op_get_exit_code,
op_system_memory_info, op_system_memory_info,
op_uid, op_uid,
op_runtime_memory_usage, op_runtime_memory_usage,
], ],
middleware = |op| match op.name { 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.with_implementation_from(&deno_core::op_void_sync()),
_ => op, _ => op,
}, },
@ -164,6 +166,12 @@ fn op_set_exit_code(state: &mut OpState, #[smi] code: i32) {
state.borrow_mut::<ExitCode>().set(code); 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)] #[op2(fast)]
fn op_exit(state: &mut OpState) { fn op_exit(state: &mut OpState) {
let code = state.borrow::<ExitCode>().get(); let code = state.borrow::<ExitCode>().get();

View file

@ -0,0 +1,5 @@
{
"args": "run main.js",
"exitCode": 42,
"output": "main.out"
}

View file

@ -0,0 +1,7 @@
if (Deno.exitCode != 0) {
throw new Error("boom!");
}
Deno.exitCode = 42;
console.log("Deno.exitCode", Deno.exitCode);

View file

@ -0,0 +1 @@
Deno.exitCode 42

View file

@ -0,0 +1,5 @@
{
"args": "test main.js",
"exitCode": 1,
"output": "main.out"
}

View file

@ -0,0 +1,3 @@
Deno.test("Deno.exitCode", () => {
Deno.exitCode = 42;
});

View file

@ -0,0 +1,17 @@
running 1 test from ./main.js
Deno.exitCode ... FAILED ([WILDCARD])
ERRORS
Deno.exitCode => ./main.js:1:6
error: Error: Test case finished with exit code set to 42.
at exitSanitizer (ext:cli/40_test.js:113:15)
at async outerWrapped (ext:cli/40_test.js:134:14)
FAILURES
Deno.exitCode => ./main.js:1:6
FAILED | 0 passed | 1 failed ([WILDCARD])
error: Test failed

View file

@ -0,0 +1,5 @@
{
"args": "test main.js",
"exitCode": 1,
"output": "main.out"
}

View file

@ -0,0 +1,7 @@
Deno.test("Deno.exitCode", () => {
Deno.exitCode = 5;
throw new Error("");
});
Deno.test("success", () => {
});

View file

@ -0,0 +1,25 @@
running 2 tests from ./main.js
Deno.exitCode ... FAILED ([WILDCARD])
success ... FAILED ([WILDCARD])
ERRORS
Deno.exitCode => ./main.js:1:6
error: Error
throw new Error("");
^
at [WILDCARD]/exit_code2/main.js:3:9
success => ./main.js:6:6
error: Error: Test case finished with exit code set to 5.
at exitSanitizer (ext:cli/40_test.js:113:15)
at async outerWrapped (ext:cli/40_test.js:134:14)
FAILURES
Deno.exitCode => ./main.js:1:6
success => ./main.js:6:6
FAILED | 0 passed | 2 failed ([WILDCARD])
error: Test failed

View file

@ -0,0 +1,5 @@
{
"args": "test main.js",
"exitCode": 1,
"output": "main.out"
}

View file

@ -0,0 +1,6 @@
Deno.test("Deno.exitCode", () => {
Deno.exitCode = 42;
});
Deno.test("success", () => {
});

View file

@ -0,0 +1,18 @@
running 2 tests from ./main.js
Deno.exitCode ... FAILED ([WILDCARD])
success ... ok ([WILDCARD])
ERRORS
Deno.exitCode => ./main.js:1:6
error: Error: Test case finished with exit code set to 42.
at exitSanitizer (ext:cli/40_test.js:113:15)
at async outerWrapped (ext:cli/40_test.js:134:14)
FAILURES
Deno.exitCode => ./main.js:1:6
FAILED | 1 passed | 1 failed ([WILDCARD])
error: Test failed

View file

@ -787,10 +787,10 @@ Deno.test("process.exitCode", () => {
assertEquals(process.exitCode, undefined); assertEquals(process.exitCode, undefined);
process.exitCode = 127; process.exitCode = 127;
assertEquals(process.exitCode, 127); assertEquals(process.exitCode, 127);
assertThrows(() => {
// deno-lint-ignore no-explicit-any // deno-lint-ignore no-explicit-any
(process.exitCode as any) = "asdf"; (process.exitCode as any) = "asdf";
// deno-lint-ignore no-explicit-any });
assertEquals(process.exitCode as any, "asdf");
// deno-lint-ignore no-explicit-any // deno-lint-ignore no-explicit-any
(process.exitCode as any) = "10"; (process.exitCode as any) = "10";
process.exitCode = undefined; // reset process.exitCode = undefined; // reset
@ -827,7 +827,7 @@ Deno.test("process.exitCode in should change exit code", async () => {
); );
await exitCodeTest( await exitCodeTest(
"import process from 'node:process'; process.exitCode = NaN;", "import process from 'node:process'; process.exitCode = NaN;",
0, 1,
); );
}); });