Skip to content

Commit 13723f2

Browse files
lukeedbartlomieju
andauthored
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 <[email protected]>
1 parent cf611fb commit 13723f2

File tree

20 files changed

+322
-24
lines changed

20 files changed

+322
-24
lines changed

cli/js/40_test.js

+18-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ const {
2828

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

31+
// Capture `Deno` global so that users deleting or mangling it, won't
32+
// have impact on our sanitizers.
33+
const DenoNs = globalThis.Deno;
34+
3135
/**
3236
* @typedef {{
3337
* id: number,
@@ -101,7 +105,20 @@ function assertExit(fn, isTest) {
101105

102106
try {
103107
const innerResult = await fn(...new SafeArrayIterator(params));
104-
if (innerResult) return innerResult;
108+
const exitCode = DenoNs.exitCode;
109+
if (exitCode !== 0) {
110+
// Reset the code to allow other tests to run...
111+
DenoNs.exitCode = 0;
112+
// ...and fail the current test.
113+
throw new Error(
114+
`${
115+
isTest ? "Test case" : "Bench"
116+
} finished with exit code set to ${exitCode}.`,
117+
);
118+
}
119+
if (innerResult) {
120+
return innerResult;
121+
}
105122
} finally {
106123
setExitHandler(null);
107124
}

cli/tests/unit/os_test.ts

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
2+
3+
import { assertEquals, assertThrows } from "../../testing/asserts.ts";
4+
5+
Deno.test("Deno.exitCode getter and setter", () => {
6+
// Initial value is 0
7+
assertEquals(Deno.exitCode, 0);
8+
9+
// Set a new value
10+
Deno.exitCode = 5;
11+
assertEquals(Deno.exitCode, 5);
12+
13+
// Reset to initial value
14+
Deno.exitCode = 0;
15+
assertEquals(Deno.exitCode, 0);
16+
});
17+
18+
Deno.test("Setting Deno.exitCode to NaN throws TypeError", () => {
19+
// @ts-expect-error;
20+
Deno.exitCode = "123";
21+
assertEquals(Deno.exitCode, 123);
22+
23+
// Reset
24+
Deno.exitCode = 0;
25+
assertEquals(Deno.exitCode, 0);
26+
27+
// Throws on non-number values
28+
assertThrows(
29+
() => {
30+
// @ts-expect-error Testing for runtime error
31+
Deno.exitCode = "not a number";
32+
},
33+
TypeError,
34+
"Exit code must be a number.",
35+
);
36+
});
37+
38+
Deno.test("Setting Deno.exitCode does not cause an immediate exit", () => {
39+
let exited = false;
40+
const originalExit = Deno.exit;
41+
42+
// @ts-expect-error; read-only
43+
Deno.exit = () => {
44+
exited = true;
45+
};
46+
47+
Deno.exitCode = 1;
48+
assertEquals(exited, false);
49+
50+
// @ts-expect-error; read-only
51+
Deno.exit = originalExit;
52+
});
53+
54+
Deno.test("Running Deno.exit(value) overrides Deno.exitCode", () => {
55+
let args: unknown[] | undefined;
56+
57+
const originalExit = Deno.exit;
58+
// @ts-expect-error; read-only
59+
Deno.exit = (...x) => {
60+
args = x;
61+
};
62+
63+
Deno.exitCode = 42;
64+
Deno.exit(0);
65+
66+
assertEquals(args, [0]);
67+
// @ts-expect-error; read-only
68+
Deno.exit = originalExit;
69+
});
70+
71+
Deno.test("Running Deno.exit() uses Deno.exitCode as fallback", () => {
72+
let args: unknown[] | undefined;
73+
74+
const originalExit = Deno.exit;
75+
// @ts-expect-error; read-only
76+
Deno.exit = (...x) => {
77+
args = x;
78+
};
79+
80+
Deno.exitCode = 42;
81+
Deno.exit();
82+
83+
assertEquals(args, [42]);
84+
// @ts-expect-error; read-only
85+
Deno.exit = originalExit;
86+
});
87+
88+
Deno.test("Retrieving the set exit code before process termination", () => {
89+
Deno.exitCode = 42;
90+
assertEquals(Deno.exitCode, 42);
91+
92+
// Reset to initial value
93+
Deno.exitCode = 0;
94+
});

cli/tsc/dts/lib.deno.ns.d.ts

+17
Original file line numberDiff line numberDiff line change
@@ -1466,6 +1466,23 @@ declare namespace Deno {
14661466
*/
14671467
export function exit(code?: number): never;
14681468

1469+
/** The exit code for the Deno process.
1470+
*
1471+
* If no exit code has been supplied, then Deno will assume a return code of `0`.
1472+
*
1473+
* When setting an exit code value, a number or non-NaN string must be provided,
1474+
* otherwise a TypeError will be thrown.
1475+
*
1476+
* ```ts
1477+
* console.log(Deno.exitCode); //-> 0
1478+
* Deno.exitCode = 1;
1479+
* console.log(Deno.exitCode); //-> 1
1480+
* ```
1481+
*
1482+
* @category Runtime
1483+
*/
1484+
export var exitCode: number;
1485+
14691486
/** An interface containing methods to interact with the process environment
14701487
* variables.
14711488
*

ext/node/polyfills/process.ts

+47-16
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import {
1010
op_geteuid,
1111
op_node_process_kill,
1212
op_process_abort,
13-
op_set_exit_code,
1413
} from "ext:core/ops";
1514

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

5353
export let argv0 = "";
5454

@@ -74,28 +74,31 @@ const notImplementedEvents = [
7474
];
7575

7676
export const argv: string[] = ["", ""];
77-
let globalProcessExitCode: number | undefined = undefined;
77+
78+
// In Node, `process.exitCode` is initially `undefined` until set.
79+
// And retains any value as long as it's nullish or number-ish.
80+
let ProcessExitCode: undefined | null | string | number;
7881

7982
/** https://nodejs.org/api/process.html#process_process_exit_code */
8083
export const exit = (code?: number | string) => {
8184
if (code || code === 0) {
82-
if (typeof code === "string") {
83-
const parsedCode = parseInt(code);
84-
globalProcessExitCode = isNaN(parsedCode) ? undefined : parsedCode;
85-
} else {
86-
globalProcessExitCode = code;
87-
}
85+
denoOs.setExitCode(code);
86+
} else if (Number.isNaN(code)) {
87+
denoOs.setExitCode(1);
8888
}
8989

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

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

101104
/** https://nodejs.org/api/process.html#processumaskmask */
@@ -433,14 +436,42 @@ Process.prototype._exiting = _exiting;
433436
/** https://nodejs.org/api/process.html#processexitcode_1 */
434437
Object.defineProperty(Process.prototype, "exitCode", {
435438
get() {
436-
return globalProcessExitCode;
439+
return ProcessExitCode;
437440
},
438-
set(code: number | undefined) {
439-
globalProcessExitCode = code;
440-
code = parseInt(code) || 0;
441-
if (!isNaN(code)) {
442-
op_set_exit_code(code);
441+
set(code: number | string | null | undefined) {
442+
let parsedCode;
443+
444+
if (typeof code === "number") {
445+
if (Number.isNaN(code)) {
446+
parsedCode = 1;
447+
denoOs.setExitCode(parsedCode);
448+
ProcessExitCode = parsedCode;
449+
return;
450+
}
451+
452+
// This is looser than `denoOs.setExitCode` which requires exit code
453+
// to be decimal or string of a decimal, but Node accept eg. 0x10.
454+
parsedCode = parseInt(code);
455+
denoOs.setExitCode(parsedCode);
456+
ProcessExitCode = parsedCode;
457+
return;
443458
}
459+
460+
if (typeof code === "string") {
461+
parsedCode = parseInt(code);
462+
if (Number.isNaN(parsedCode)) {
463+
throw new TypeError(
464+
`The "code" argument must be of type number. Received type ${typeof code} (${code})`,
465+
);
466+
}
467+
denoOs.setExitCode(parsedCode);
468+
ProcessExitCode = parsedCode;
469+
return;
470+
}
471+
472+
// TODO(bartlomieju): hope for the best here. This should be further tightened.
473+
denoOs.setExitCode(code);
474+
ProcessExitCode = code;
444475
},
445476
});
446477

runtime/js/30_os.js

+20-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
op_exec_path,
88
op_exit,
99
op_get_env,
10+
op_get_exit_code,
1011
op_gid,
1112
op_hostname,
1213
op_loadavg,
@@ -21,7 +22,9 @@ import {
2122
const {
2223
Error,
2324
FunctionPrototypeBind,
25+
NumberParseInt,
2426
SymbolFor,
27+
TypeError,
2528
} = primordials;
2629

2730
import { Event, EventTarget } from "ext:deno_web/02_event.js";
@@ -75,7 +78,7 @@ function exit(code) {
7578
if (typeof code === "number") {
7679
op_set_exit_code(code);
7780
} else {
78-
code = 0;
81+
code = op_get_exit_code();
7982
}
8083

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

100+
function getExitCode() {
101+
return op_get_exit_code();
102+
}
103+
104+
function setExitCode(value) {
105+
const code = NumberParseInt(value, 10);
106+
if (typeof code !== "number") {
107+
throw new TypeError(
108+
`Exit code must be a number, got: ${code} (${typeof code}).`,
109+
);
110+
}
111+
op_set_exit_code(code);
112+
}
113+
97114
function setEnv(key, value) {
98115
op_set_env(key, value);
99116
}
@@ -126,12 +143,14 @@ export {
126143
env,
127144
execPath,
128145
exit,
146+
getExitCode,
129147
gid,
130148
hostname,
131149
loadavg,
132150
networkInterfaces,
133151
osRelease,
134152
osUptime,
153+
setExitCode,
135154
setExitHandler,
136155
systemMemoryInfo,
137156
uid,

runtime/js/99_main.js

+8
Original file line numberDiff line numberDiff line change
@@ -674,6 +674,14 @@ ObjectDefineProperties(finalDenoNs, {
674674
return internals.future ? undefined : customInspect;
675675
},
676676
},
677+
exitCode: {
678+
get() {
679+
return os.getExitCode();
680+
},
681+
set(value) {
682+
os.setExitCode(value);
683+
},
684+
},
677685
});
678686

679687
const {

runtime/ops/os/mod.rs

+9-1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ deno_core::extension!(
3232
op_os_uptime,
3333
op_set_env,
3434
op_set_exit_code,
35+
op_get_exit_code,
3536
op_system_memory_info,
3637
op_uid,
3738
op_runtime_memory_usage,
@@ -60,12 +61,13 @@ deno_core::extension!(
6061
op_os_uptime,
6162
op_set_env,
6263
op_set_exit_code,
64+
op_get_exit_code,
6365
op_system_memory_info,
6466
op_uid,
6567
op_runtime_memory_usage,
6668
],
6769
middleware = |op| match op.name {
68-
"op_exit" | "op_set_exit_code" =>
70+
"op_exit" | "op_set_exit_code" | "op_get_exit_code" =>
6971
op.with_implementation_from(&deno_core::op_void_sync()),
7072
_ => op,
7173
},
@@ -164,6 +166,12 @@ fn op_set_exit_code(state: &mut OpState, #[smi] code: i32) {
164166
state.borrow_mut::<ExitCode>().set(code);
165167
}
166168

169+
#[op2(fast)]
170+
#[smi]
171+
fn op_get_exit_code(state: &mut OpState) -> i32 {
172+
state.borrow_mut::<ExitCode>().get()
173+
}
174+
167175
#[op2(fast)]
168176
fn op_exit(state: &mut OpState) {
169177
let code = state.borrow::<ExitCode>().get();
+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"args": "run main.js",
3+
"exitCode": 42,
4+
"output": "main.out"
5+
}

tests/specs/run/exit_code/main.js

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
if (Deno.exitCode != 0) {
2+
throw new Error("boom!");
3+
}
4+
5+
Deno.exitCode = 42;
6+
7+
console.log("Deno.exitCode", Deno.exitCode);

tests/specs/run/exit_code/main.out

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Deno.exitCode 42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"args": "test main.js",
3+
"exitCode": 1,
4+
"output": "main.out"
5+
}

tests/specs/test/exit_code/main.js

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Deno.test("Deno.exitCode", () => {
2+
Deno.exitCode = 42;
3+
});

0 commit comments

Comments
 (0)