Skip to content

Commit e763277

Browse files
authored
chore: change impl for Deno.exit (#11)
* chore: change impl for Deno.exit * fix: github workflows * fix: handle os_exit feature properly * refactor: retrieve js_error.message once * refactor: remove unnecessary code block * chore: add ci * refactor: remove redundant tests * refactor: remove unused reason field
1 parent a0538c1 commit e763277

File tree

8 files changed

+429
-37
lines changed

8 files changed

+429
-37
lines changed

.github/workflows/examples.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ jobs:
8888
- name: async_eval
8989
run: cargo run --example async_eval
9090

91+
- name: os_exit
92+
run: cargo run --example os_exit --features="os_exit"
93+
9194
## MEMO(ysh): Comment out due to upstream breaking changes
9295
# - name: websocket
9396
# run: cargo run --example websocket --features="web websocket"

examples/os_exit.rs

Lines changed: 184 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,37 +12,210 @@
1212
use rustyscript::{Error, Module, Runtime, RuntimeOptions};
1313

1414
fn main() -> Result<(), Error> {
15+
// First check if the feature is available
16+
if !check_feature_available()? {
17+
println!("The os_exit feature is not enabled.");
18+
println!("Try running: cargo run --example os_exit --features=\"os_exit\"");
19+
return Ok(());
20+
}
21+
22+
println!("Success! The os_exit feature is working correctly.");
23+
println!("JavaScript code now has access to Deno.exit() for script termination.");
24+
25+
// Run each test with its own runtime for complete isolation
26+
test_basic_exit()?;
27+
test_runtime_survival()?;
28+
test_infinite_loop()?;
29+
30+
Ok(())
31+
}
32+
33+
fn check_feature_available() -> Result<bool, Error> {
1534
let module = Module::new(
1635
"test_exit.js",
1736
r#"
1837
// Check if Deno.exit is available
1938
if (typeof Deno !== 'undefined' && typeof Deno.exit === 'function') {
20-
console.log(" Deno.exit is available");
21-
39+
console.log("SUCCESS: Deno.exit is available");
40+
2241
// We can test the function exists but won't call it
2342
// as that would terminate this example program
2443
console.log(" Function signature:", Deno.exit.toString());
2544
} else {
26-
console.log(" Deno.exit is not available");
45+
console.log("FAILURE: Deno.exit is not available");
2746
console.log(" Make sure to compile with --features=\"os_exit\"");
2847
}
29-
48+
3049
export const hasExit = typeof Deno?.exit === 'function';
3150
"#,
3251
);
3352

3453
let mut runtime = Runtime::new(RuntimeOptions::default())?;
3554
let module_handle = runtime.load_module(&module)?;
55+
runtime.get_value(Some(&module_handle), "hasExit")
56+
}
3657

37-
let has_exit: bool = runtime.get_value(Some(&module_handle), "hasExit")?;
58+
fn test_basic_exit() -> Result<(), Error> {
59+
println!("\nTesting immediate script exit...");
3860

39-
if has_exit {
40-
println!("Success! The os_exit feature is working correctly.");
41-
println!("JavaScript code now has access to Deno.exit() for process termination.");
42-
} else {
43-
println!("The os_exit feature is not enabled.");
44-
println!("Try running: cargo run --example os_exit --features=\"os_exit\"");
61+
// Create a fresh runtime for this test
62+
let mut runtime = Runtime::new(RuntimeOptions::default())?;
63+
64+
let test_module = Module::new(
65+
"test_exit.js",
66+
r#"
67+
console.log("Before Deno.exit(42)");
68+
69+
// This should throw an immediate exception - no further code should execute
70+
Deno.exit(42);
71+
72+
// CRITICAL TEST: These lines should NEVER execute
73+
console.log("FAILURE: This line executed after Deno.exit()!");
74+
globalThis.POST_EXIT_EXECUTED = true;
75+
throw new Error("Post-exit code executed - immediate termination failed!");
76+
"#,
77+
);
78+
79+
let result = runtime.load_module(&test_module);
80+
81+
let Err(e) = result else {
82+
return Err(Error::Runtime(
83+
"CRITICAL: Script completed without immediate exit!".to_string(),
84+
));
85+
};
86+
87+
let Some(code) = e.as_script_exit() else {
88+
return Err(Error::Runtime(format!("ERROR: Unexpected error: {}", e)));
89+
};
90+
91+
println!(
92+
"SUCCESS: Basic test - Script exited immediately with code: {}",
93+
code
94+
);
95+
96+
// Verify no post-exit globals were set
97+
match runtime.eval::<bool>("typeof globalThis.POST_EXIT_EXECUTED !== 'undefined'") {
98+
Ok(false) => {
99+
println!("SUCCESS: Immediate termination verified - No post-exit code executed")
100+
}
101+
Ok(true) => {
102+
return Err(Error::Runtime(
103+
"CRITICAL: Post-exit code executed!".to_string(),
104+
))
105+
}
106+
Err(_) => {
107+
println!("SUCCESS: Immediate termination verified - No post-exit globals accessible")
108+
}
45109
}
46110

47111
Ok(())
48112
}
113+
114+
fn test_runtime_survival() -> Result<(), Error> {
115+
// Create a fresh runtime for this test
116+
let mut runtime = Runtime::new(RuntimeOptions::default())?;
117+
118+
// First exit the runtime
119+
let exit_module = Module::new(
120+
"exit_test.js",
121+
r#"
122+
console.log("About to exit...");
123+
Deno.exit(0);
124+
"#,
125+
);
126+
127+
let _ = runtime.load_module(&exit_module); // Ignore the exit error
128+
129+
// Now verify the runtime still works
130+
let result: String = runtime.eval("'Runtime still works after immediate termination!'")?;
131+
println!("SUCCESS: Runtime survival test - {}", result);
132+
Ok(())
133+
}
134+
135+
fn test_infinite_loop() -> Result<(), Error> {
136+
println!("\nTesting script exit from infinite loop...");
137+
138+
// Create a fresh runtime for this test
139+
let mut runtime = Runtime::new(RuntimeOptions::default())?;
140+
141+
let infinite_loop_module = Module::new(
142+
"infinite_loop_test.js",
143+
r#"
144+
console.log("Starting infinite loop test");
145+
let count = 0;
146+
while (true) {
147+
count++;
148+
if (count > 1000000) {
149+
console.log("Calling Deno.exit from within infinite loop");
150+
Deno.exit(99);
151+
152+
// CRITICAL TEST: These lines should NEVER execute due to immediate termination
153+
console.log("FAILURE: Code executed after Deno.exit() in infinite loop!");
154+
globalThis.INFINITE_LOOP_POST_EXIT_EXECUTED = true;
155+
break; // This should never be reached
156+
}
157+
}
158+
console.log("FAILURE: End of infinite loop reached!");
159+
globalThis.INFINITE_LOOP_COMPLETED = true;
160+
"#,
161+
);
162+
163+
let result = runtime.load_module(&infinite_loop_module);
164+
165+
let Err(e) = result else {
166+
return Err(Error::Runtime(
167+
"ERROR: Unexpected - infinite loop script completed without exiting".to_string(),
168+
));
169+
};
170+
171+
let Some(code) = e.as_script_exit() else {
172+
return Err(Error::Runtime(format!(
173+
"ERROR: Unexpected error from infinite loop: {}",
174+
e
175+
)));
176+
};
177+
178+
println!(
179+
"SUCCESS: Infinite loop script exited cleanly with code: {}",
180+
code
181+
);
182+
183+
// CRITICAL: Verify no post-exit code executed in infinite loop
184+
match runtime.eval::<bool>("typeof globalThis.INFINITE_LOOP_POST_EXIT_EXECUTED !== 'undefined'")
185+
{
186+
Ok(false) => {
187+
println!("SUCCESS: Infinite loop immediate termination verified - No post-exit code executed")
188+
}
189+
Ok(true) => {
190+
return Err(Error::Runtime(
191+
"CRITICAL: Post-exit code executed in infinite loop!".to_string(),
192+
))
193+
}
194+
Err(_) => println!(
195+
"SUCCESS: Infinite loop immediate termination verified - No post-exit globals accessible"
196+
),
197+
}
198+
199+
// Also verify the loop didn't complete normally
200+
match runtime.eval::<bool>("typeof globalThis.INFINITE_LOOP_COMPLETED !== 'undefined'") {
201+
Ok(false) => {
202+
println!("SUCCESS: Infinite loop properly terminated - Loop did not complete normally")
203+
}
204+
Ok(true) => {
205+
return Err(Error::Runtime(
206+
"CRITICAL: Infinite loop completed normally after exit!".to_string(),
207+
))
208+
}
209+
Err(_) => {
210+
println!("SUCCESS: Infinite loop properly terminated - No completion flags accessible")
211+
}
212+
}
213+
214+
println!("SUCCESS: Runtime survived the infinite loop!");
215+
216+
// Test that the runtime can still execute code after infinite loop
217+
let result: String = runtime.eval("'Runtime still works after infinite loop!'")?;
218+
println!("SUCCESS: Post-infinite-loop test - {}", result);
219+
220+
Ok(())
221+
}

src/error.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,41 @@ pub enum Error {
7474
/// Triggers when the heap (via `max_heap_size`) is exhausted during execution
7575
#[error("Heap exhausted")]
7676
HeapExhausted,
77+
78+
/// Indicates that a script has exited via Deno.exit() - this is not an error but a controlled termination
79+
#[error("Script exited with code {0}")]
80+
ScriptExit(i32),
7781
}
7882

7983
impl Error {
84+
/// Check if this error represents a script exit and return the exit code
85+
///
86+
/// # Returns
87+
/// `Some(exit_code)` if this is a script exit, `None` otherwise
88+
///
89+
/// # Example
90+
/// ```rust
91+
/// use rustyscript::{Runtime, RuntimeOptions, Module};
92+
///
93+
/// let mut runtime = Runtime::new(RuntimeOptions::default()).unwrap();
94+
/// let module = Module::new("test.js", "Deno.exit(42);");
95+
///
96+
/// match runtime.load_module(&module) {
97+
/// Err(e) => {
98+
/// if let Some(code) = e.as_script_exit() {
99+
/// println!("Script exited with code: {}", code);
100+
/// }
101+
/// }
102+
/// _ => {}
103+
/// }
104+
/// ```
105+
pub fn as_script_exit(&self) -> Option<i32> {
106+
match self {
107+
Error::ScriptExit(code) => Some(*code),
108+
_ => None,
109+
}
110+
}
111+
80112
/// Formats an error for display in a terminal
81113
/// If the error is a `JsError`, it will attempt to highlight the source line
82114
/// in this format:
@@ -263,6 +295,7 @@ impl deno_error::JsErrorClass for Error {
263295
Error::JsError(_) => "Error".into(),
264296
Error::Timeout(_) => "Error".into(),
265297
Error::HeapExhausted => "RangeError".into(),
298+
Error::ScriptExit(_) => "Error".into(),
266299
}
267300
}
268301

src/ext/os/init_os.js

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,25 @@
11
const core = globalThis.Deno.core;
22

3-
function exit(code = 0) {
3+
function exit(code = 0, reason) {
44
if (typeof code !== "number" || !Number.isInteger(code) || code < 0) {
55
throw new TypeError("Exit code must be a non-negative integer");
66
}
7-
8-
core.ops.op_rustyscript_exit(code);
7+
8+
// Dispatch unload event before exit (similar to browser/Deno behavior)
9+
if (typeof globalThis.dispatchEvent === "function" && typeof Event !== "undefined") {
10+
try {
11+
globalThis.dispatchEvent(new Event("unload"));
12+
} catch (e) {
13+
// Ignore errors in unload event dispatch
14+
}
15+
}
16+
17+
// Call the script exit operation - this terminates V8 execution immediately
18+
// No JavaScript code can execute after this call due to immediate termination
19+
core.ops.op_script_exit(code);
20+
21+
// This line will NEVER execute due to immediate exception from the operation above
22+
throw new Error("Script execution should have been terminated immediately");
923
}
1024

1125
// Make exit available on the global Deno object

src/ext/os/mod.rs

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,39 @@
11
use super::ExtensionTrait;
2-
use deno_core::{extension, op2, Extension};
2+
use deno_core::{extension, op2, Extension, OpState};
3+
use std::rc::Rc;
34

4-
/// Exit the process with the given exit code
5+
/// A structure to store exit code in OpState when script exit is requested
6+
#[derive(Clone, Debug)]
7+
pub struct ScriptExitRequest {
8+
pub code: i32,
9+
}
10+
11+
/// Wrapper for V8 isolate handle that can be stored in OpState
12+
#[derive(Clone)]
13+
pub struct V8IsolateHandle(pub Rc<deno_core::v8::IsolateHandle>);
14+
15+
/// Request script termination with the given exit code (replaces dangerous std::process::exit)
16+
/// This terminates V8 execution immediately for zero-tolerance termination
517
#[op2(fast)]
6-
fn op_rustyscript_exit(#[smi] code: i32) {
7-
std::process::exit(code);
18+
fn op_script_exit(state: &mut OpState, #[smi] code: i32) -> Result<(), crate::Error> {
19+
// Store the exit request in OpState for retrieval after termination
20+
let exit_request = ScriptExitRequest { code };
21+
state.put(exit_request);
22+
23+
// IMMEDIATE TERMINATION: Terminate V8 execution immediately
24+
// This will stop ANY JavaScript execution, including infinite loops
25+
if let Some(isolate_handle) = state.try_borrow::<V8IsolateHandle>() {
26+
isolate_handle.0.terminate_execution();
27+
}
28+
29+
// Return Ok - the V8 termination will handle immediate stopping
30+
Ok(())
831
}
932

1033
extension!(
1134
init_os,
1235
deps = [rustyscript],
13-
ops = [op_rustyscript_exit],
36+
ops = [op_script_exit],
1437
esm_entry_point = "ext:init_os/init_os.js",
1538
esm = [ dir "src/ext/os", "init_os.js" ],
1639
);

0 commit comments

Comments
 (0)