Skip to content
This repository was archived by the owner on Dec 24, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ jobs:
- name: async_eval
run: cargo run --example async_eval

- name: os_exit
run: cargo run --example os_exit --features="os_exit"

## MEMO(ysh): Comment out due to upstream breaking changes
# - name: websocket
# run: cargo run --example websocket --features="web websocket"
195 changes: 184 additions & 11 deletions examples/os_exit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,37 +12,210 @@
use rustyscript::{Error, Module, Runtime, RuntimeOptions};

fn main() -> Result<(), Error> {
// First check if the feature is available
if !check_feature_available()? {
println!("The os_exit feature is not enabled.");
println!("Try running: cargo run --example os_exit --features=\"os_exit\"");
return Ok(());
}

println!("Success! The os_exit feature is working correctly.");
println!("JavaScript code now has access to Deno.exit() for script termination.");
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ランタイム自体が Deno と互換性がある状態なので Node 互換の os.exit ではなく Deno.exit と命名しています

Copy link

@k1LoW k1LoW Aug 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

質問です。Deno.exit という名前は露出はしないという認識ですがあっていますか?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ドキュメント上では露出しませんが、呼び出し自体は可能です。
スクリプトを終了させるだけなので、仮に呼び出し可能とわかってもセキュリティ的に問題はない認識です。

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Deno" という固有名詞が入っても問題ないかどうか気になったのでした。
仮にランタイムがNode.jsになった場合 "Node.exit" にする必要がでそうだと思ったのですが合っていますか?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deno_core に依存しているので && Node 互換の JS ランタイム ラッパーが rust に存在いないので、今後ランタイムが Node 互換になることはないです。


// Run each test with its own runtime for complete isolation
test_basic_exit()?;
test_runtime_survival()?;
test_infinite_loop()?;

Ok(())
}

fn check_feature_available() -> Result<bool, Error> {
let module = Module::new(
"test_exit.js",
r#"
// Check if Deno.exit is available
if (typeof Deno !== 'undefined' && typeof Deno.exit === 'function') {
console.log(" Deno.exit is available");
console.log("SUCCESS: Deno.exit is available");

// We can test the function exists but won't call it
// as that would terminate this example program
console.log(" Function signature:", Deno.exit.toString());
} else {
console.log(" Deno.exit is not available");
console.log("FAILURE: Deno.exit is not available");
console.log(" Make sure to compile with --features=\"os_exit\"");
}

export const hasExit = typeof Deno?.exit === 'function';
"#,
);

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

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

if has_exit {
println!("Success! The os_exit feature is working correctly.");
println!("JavaScript code now has access to Deno.exit() for process termination.");
} else {
println!("The os_exit feature is not enabled.");
println!("Try running: cargo run --example os_exit --features=\"os_exit\"");
// Create a fresh runtime for this test
let mut runtime = Runtime::new(RuntimeOptions::default())?;

let test_module = Module::new(
"test_exit.js",
r#"
console.log("Before Deno.exit(42)");

// This should throw an immediate exception - no further code should execute
Deno.exit(42);

// CRITICAL TEST: These lines should NEVER execute
console.log("FAILURE: This line executed after Deno.exit()!");
globalThis.POST_EXIT_EXECUTED = true;
throw new Error("Post-exit code executed - immediate termination failed!");
"#,
);

let result = runtime.load_module(&test_module);

let Err(e) = result else {
return Err(Error::Runtime(
"CRITICAL: Script completed without immediate exit!".to_string(),
));
};

let Some(code) = e.as_script_exit() else {
return Err(Error::Runtime(format!("ERROR: Unexpected error: {}", e)));
};

println!(
"SUCCESS: Basic test - Script exited immediately with code: {}",
code
);

// Verify no post-exit globals were set
match runtime.eval::<bool>("typeof globalThis.POST_EXIT_EXECUTED !== 'undefined'") {
Ok(false) => {
println!("SUCCESS: Immediate termination verified - No post-exit code executed")
}
Ok(true) => {
return Err(Error::Runtime(
"CRITICAL: Post-exit code executed!".to_string(),
))
}
Err(_) => {
println!("SUCCESS: Immediate termination verified - No post-exit globals accessible")
}
}

Ok(())
}

fn test_runtime_survival() -> Result<(), Error> {
// Create a fresh runtime for this test
let mut runtime = Runtime::new(RuntimeOptions::default())?;

// First exit the runtime
let exit_module = Module::new(
"exit_test.js",
r#"
console.log("About to exit...");
Deno.exit(0);
"#,
);

let _ = runtime.load_module(&exit_module); // Ignore the exit error

// Now verify the runtime still works
let result: String = runtime.eval("'Runtime still works after immediate termination!'")?;
println!("SUCCESS: Runtime survival test - {}", result);
Ok(())
}

fn test_infinite_loop() -> Result<(), Error> {
println!("\nTesting script exit from infinite loop...");

// Create a fresh runtime for this test
let mut runtime = Runtime::new(RuntimeOptions::default())?;

let infinite_loop_module = Module::new(
"infinite_loop_test.js",
r#"
console.log("Starting infinite loop test");
let count = 0;
while (true) {
count++;
if (count > 1000000) {
console.log("Calling Deno.exit from within infinite loop");
Deno.exit(99);

// CRITICAL TEST: These lines should NEVER execute due to immediate termination
console.log("FAILURE: Code executed after Deno.exit() in infinite loop!");
globalThis.INFINITE_LOOP_POST_EXIT_EXECUTED = true;
break; // This should never be reached
}
}
console.log("FAILURE: End of infinite loop reached!");
globalThis.INFINITE_LOOP_COMPLETED = true;
"#,
);

let result = runtime.load_module(&infinite_loop_module);

let Err(e) = result else {
return Err(Error::Runtime(
"ERROR: Unexpected - infinite loop script completed without exiting".to_string(),
));
};

let Some(code) = e.as_script_exit() else {
return Err(Error::Runtime(format!(
"ERROR: Unexpected error from infinite loop: {}",
e
)));
};

println!(
"SUCCESS: Infinite loop script exited cleanly with code: {}",
code
);

// CRITICAL: Verify no post-exit code executed in infinite loop
match runtime.eval::<bool>("typeof globalThis.INFINITE_LOOP_POST_EXIT_EXECUTED !== 'undefined'")
{
Ok(false) => {
println!("SUCCESS: Infinite loop immediate termination verified - No post-exit code executed")
}
Ok(true) => {
return Err(Error::Runtime(
"CRITICAL: Post-exit code executed in infinite loop!".to_string(),
))
}
Err(_) => println!(
"SUCCESS: Infinite loop immediate termination verified - No post-exit globals accessible"
),
}

// Also verify the loop didn't complete normally
match runtime.eval::<bool>("typeof globalThis.INFINITE_LOOP_COMPLETED !== 'undefined'") {
Ok(false) => {
println!("SUCCESS: Infinite loop properly terminated - Loop did not complete normally")
}
Ok(true) => {
return Err(Error::Runtime(
"CRITICAL: Infinite loop completed normally after exit!".to_string(),
))
}
Err(_) => {
println!("SUCCESS: Infinite loop properly terminated - No completion flags accessible")
}
}

println!("SUCCESS: Runtime survived the infinite loop!");

// Test that the runtime can still execute code after infinite loop
let result: String = runtime.eval("'Runtime still works after infinite loop!'")?;
println!("SUCCESS: Post-infinite-loop test - {}", result);

Ok(())
}
33 changes: 33 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,41 @@ pub enum Error {
/// Triggers when the heap (via `max_heap_size`) is exhausted during execution
#[error("Heap exhausted")]
HeapExhausted,

/// Indicates that a script has exited via Deno.exit() - this is not an error but a controlled termination
#[error("Script exited with code {0}")]
ScriptExit(i32),
}

impl Error {
/// Check if this error represents a script exit and return the exit code
///
/// # Returns
/// `Some(exit_code)` if this is a script exit, `None` otherwise
///
/// # Example
/// ```rust
/// use rustyscript::{Runtime, RuntimeOptions, Module};
///
/// let mut runtime = Runtime::new(RuntimeOptions::default()).unwrap();
/// let module = Module::new("test.js", "Deno.exit(42);");
///
/// match runtime.load_module(&module) {
/// Err(e) => {
/// if let Some(code) = e.as_script_exit() {
/// println!("Script exited with code: {}", code);
/// }
/// }
/// _ => {}
/// }
/// ```
pub fn as_script_exit(&self) -> Option<i32> {
match self {
Error::ScriptExit(code) => Some(*code),
_ => None,
}
}

/// Formats an error for display in a terminal
/// If the error is a `JsError`, it will attempt to highlight the source line
/// in this format:
Expand Down Expand Up @@ -263,6 +295,7 @@ impl deno_error::JsErrorClass for Error {
Error::JsError(_) => "Error".into(),
Error::Timeout(_) => "Error".into(),
Error::HeapExhausted => "RangeError".into(),
Error::ScriptExit(_) => "Error".into(),
}
}

Expand Down
20 changes: 17 additions & 3 deletions src/ext/os/init_os.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
const core = globalThis.Deno.core;

function exit(code = 0) {
function exit(code = 0, reason) {
if (typeof code !== "number" || !Number.isInteger(code) || code < 0) {
throw new TypeError("Exit code must be a non-negative integer");
}

core.ops.op_rustyscript_exit(code);

// Dispatch unload event before exit (similar to browser/Deno behavior)
if (typeof globalThis.dispatchEvent === "function" && typeof Event !== "undefined") {
try {
globalThis.dispatchEvent(new Event("unload"));
} catch (e) {
// Ignore errors in unload event dispatch
}
}

// Call the script exit operation - this terminates V8 execution immediately
// No JavaScript code can execute after this call due to immediate termination
core.ops.op_script_exit(code);

// This line will NEVER execute due to immediate exception from the operation above
throw new Error("Script execution should have been terminated immediately");
}

// Make exit available on the global Deno object
Expand Down
33 changes: 28 additions & 5 deletions src/ext/os/mod.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,39 @@
use super::ExtensionTrait;
use deno_core::{extension, op2, Extension};
use deno_core::{extension, op2, Extension, OpState};
use std::rc::Rc;

/// Exit the process with the given exit code
/// A structure to store exit code in OpState when script exit is requested
#[derive(Clone, Debug)]
pub struct ScriptExitRequest {
pub code: i32,
}

/// Wrapper for V8 isolate handle that can be stored in OpState
#[derive(Clone)]
pub struct V8IsolateHandle(pub Rc<deno_core::v8::IsolateHandle>);

/// Request script termination with the given exit code (replaces dangerous std::process::exit)
/// This terminates V8 execution immediately for zero-tolerance termination
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ここがメインです

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

std::process::exit(code); の利用から変更した。なるほど!

#[op2(fast)]
fn op_rustyscript_exit(#[smi] code: i32) {
std::process::exit(code);
fn op_script_exit(state: &mut OpState, #[smi] code: i32) -> Result<(), crate::Error> {
// Store the exit request in OpState for retrieval after termination
let exit_request = ScriptExitRequest { code };
state.put(exit_request);

// IMMEDIATE TERMINATION: Terminate V8 execution immediately
// This will stop ANY JavaScript execution, including infinite loops
if let Some(isolate_handle) = state.try_borrow::<V8IsolateHandle>() {
isolate_handle.0.terminate_execution();
}

// Return Ok - the V8 termination will handle immediate stopping
Ok(())
}

extension!(
init_os,
deps = [rustyscript],
ops = [op_rustyscript_exit],
ops = [op_script_exit],
esm_entry_point = "ext:init_os/init_os.js",
esm = [ dir "src/ext/os", "init_os.js" ],
);
Expand Down
Loading