Skip to content
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ All notable changes to WASM-OJ are recorded here. Releases follow
self-delimiting archive or JSON compiler response is complete instead of waiting for
`Instance.wait()`. A guest that exits before completing its output still fails with its
stderr, about 2 s after it exits.
- Fix browser `Engine.interact`, which failed on every dialogue (#98). Each side now runs in its
own nested Worker as a standalone metered run, connected to the other through shared-memory pipes
that block with `Atomics.wait`. Browser interactive costs equal `run` costs for the same program.
Each pipe holds the writing side's whole output budget, and closing stdin or stdout signals the
peer at once, so browser and server give the same verdicts. The runner Worker also sends
`startupEntropyBytes` for interactive programs. The refreshed runtime identity changes cost
profiles.

## 0.2.3 - 2026-10-05

Expand Down
28 changes: 27 additions & 1 deletion crates/runtime-core/src/deterministic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use crate::{
},
types::DeterminismConfig,
};
use std::cell::Cell;
use std::sync::{Arc, Mutex};
use wasmer::{
AsStoreMut, Extern, Function, FunctionEnv, FunctionEnvMut, Imports, Memory, Memory32, Memory64,
Expand Down Expand Up @@ -239,6 +240,23 @@ struct PollEnv {
clock: VirtualClock,
}

thread_local! {
static STDIN_READINESS_DEFERRALS: Cell<u64> = const { Cell::new(0) };
}

/// Whether an empty stdin readiness check may return `Pending` instead of
/// blocking. A clock probe never blocks. A poll without a clock grants one
/// deferral per read subscription, so WASIX reports any other ready
/// subscription first and blocks on stdin only when it would wait anyway.
#[cfg(target_arch = "wasm32")]
pub(crate) fn defer_stdin_readiness() -> bool {
STDIN_READINESS_DEFERRALS.with(|deferrals| {
let remaining = deferrals.get();
deferrals.set(remaining.saturating_sub(1));
remaining > 0
})
}

#[derive(Clone, Copy)]
struct ClockSubscription {
subscription: Subscription,
Expand All @@ -251,6 +269,7 @@ fn call_original_poll<M: MemorySize>(
events: WasmPtr<Event, M>,
subscription_count: M::Offset,
event_count: WasmPtr<M::Offset, M>,
stdin_deferrals: u64,
) -> Result<i32, RuntimeError> {
let Some(original) = env.data().original.clone() else {
return Ok(Errno::Notsup as i32);
Expand All @@ -262,6 +281,7 @@ fn call_original_poll<M: MemorySize>(
Value::I32(offset as i32)
}
};
STDIN_READINESS_DEFERRALS.with(|deferrals| deferrals.set(stdin_deferrals));
let results = original.call(
env,
&[
Expand All @@ -270,7 +290,9 @@ fn call_original_poll<M: MemorySize>(
pointer(subscription_count.into()),
pointer(event_count.offset().into()),
],
)?;
);
STDIN_READINESS_DEFERRALS.with(|deferrals| deferrals.set(0));
let results = results?;
match results.as_ref() {
[Value::I32(errno)] => Ok(*errno),
_ => Err(RuntimeError::new(
Expand Down Expand Up @@ -302,6 +324,7 @@ fn deterministic_poll_oneoff<M: MemorySize>(
let mut originals = Vec::with_capacity(input.len() as usize);
let mut clock_subscriptions = Vec::new();
let mut has_non_clock = false;
let mut read_subscriptions = 0;
for index in 0..input.len() {
let subscription = input
.index(index)
Expand All @@ -320,6 +343,7 @@ fn deterministic_poll_oneoff<M: MemorySize>(
});
} else {
has_non_clock = true;
read_subscriptions += u64::from(subscription.type_ == Eventtype::FdRead);
}
}

Expand All @@ -330,6 +354,7 @@ fn deterministic_poll_oneoff<M: MemorySize>(
events,
subscription_count,
event_count,
read_subscriptions,
);
}

Expand All @@ -355,6 +380,7 @@ fn deterministic_poll_oneoff<M: MemorySize>(
events,
subscription_count,
event_count,
u64::MAX,
);
let restore_view = memory.view(&env);
let restore_input = subscriptions
Expand Down
60 changes: 60 additions & 0 deletions crates/runtime-core/src/interactive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -971,6 +971,66 @@ mod tests {
assert_eq!(result.interactor.metrics.logical_time_ns, 0);
}

#[test]
fn interactive_empty_stdin_poll_times_out_on_the_process_clock() {
let poller = wat::parse_str(
r#"(module
(import "wasi_snapshot_preview1" "poll_oneoff"
(func $poll (param i32 i32 i32 i32) (result i32)))
(import "wasi_snapshot_preview1" "fd_write"
(func $fd_write (param i32 i32 i32 i32) (result i32)))
(memory (export "memory") 1)
(func (export "_start")
i32.const 0 i64.const 1 i64.store
i32.const 8 i32.const 1 i32.store8
i32.const 16 i32.const 0 i32.store
i32.const 48 i64.const 2 i64.store
i32.const 56 i32.const 0 i32.store8
i32.const 64 i32.const 1 i32.store
i32.const 72 i64.const 5000000000 i64.store
i32.const 80 i64.const 1 i64.store
i32.const 88 i32.const 0 i32.store16
i32.const 0 i32.const 128 i32.const 2 i32.const 240 call $poll drop
i32.const 200 i32.const 138 i32.store
i32.const 204 i32.const 1 i32.store
i32.const 208 i32.const 240 i32.store
i32.const 212 i32.const 1 i32.store
i32.const 1 i32.const 200 i32.const 2 i32.const 216 call $fd_write drop))"#,
)
.unwrap();
let listener = wat::parse_str(
r#"(module
(import "wasi_snapshot_preview1" "fd_read"
(func $fd_read (param i32 i32 i32 i32) (result i32)))
(memory (export "memory") 1)
(func (export "_start")
(i32.store (i32.const 0) (i32.const 64))
(i32.store (i32.const 4) (i32.const 8))
(drop (call $fd_read (i32.const 0) (i32.const 0) (i32.const 1) (i32.const 8)))))"#,
)
.unwrap();
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let result = runtime
.block_on(interact(InteractiveRequest {
contestant: program(poller),
interactor: program(listener),
determinism: DeterminismConfig {
random_seed: 7,
realtime_epoch_ms: 946_684_800_000,
clock_step_ns: 1_000_000,
},
}))
.unwrap();

assert_eq!(result.contestant_to_interactor, [0, 1]);
assert_eq!(result.contestant.termination, ExecutionTermination::Exited);
assert_eq!(result.contestant.metrics.logical_time_ns, 5_000_000_000);
assert_eq!(result.interactor.termination, ExecutionTermination::Exited);
}

#[test]
fn interactive_contestant_is_metered_like_a_standalone_run() {
let looping = |ending: &str| {
Expand Down
2 changes: 2 additions & 0 deletions crates/runtime-core/src/run/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ mod native;
#[cfg(target_arch = "wasm32")]
mod web;
#[cfg(target_arch = "wasm32")]
pub(crate) mod web_interactive;
#[cfg(target_arch = "wasm32")]
pub(crate) mod web_runtime;

pub fn run(request: RunRequest) -> Result<RunResult, RunError> {
Expand Down
45 changes: 34 additions & 11 deletions crates/runtime-core/src/run/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,41 @@ use crate::filesystem::{read_files_bounded, runtime_project_files};
use crate::meter::{CostPoints, METER_MODEL, instrument_wasm, meter_state, remaining_points};
use crate::module_imports::attach_imported_memory;
use crate::module_policy::{DEFERRED_START_EXPORT, defer_start_section, enforce_memory_limit};
use crate::output::{OutputBudget, OutputCapture};
use crate::output::{CappedOutput, OutputBudget, OutputCapture};
use crate::{ExecutionMetrics, ExecutionTermination, RunError, RunRequest, RunResult};
use std::io::Write;
use std::sync::{Arc, Mutex};
use virtual_fs::VirtualFile;
use wasmer::{Instance, Memory, Module, Store};
use wasmer_wasix::{
Pipe, WasiEnv, WasiError, WasiModuleInstanceHandles, WasiModuleTreeHandles, wasmer_wasix_types,
};

pub fn run(
request: RunRequest,
mut on_execution: impl FnMut(bool) -> Result<(), RunError>,
on_execution: impl FnMut(bool) -> Result<(), RunError>,
) -> Result<RunResult, RunError> {
let (mut stdin_writer, stdin_reader) = Pipe::channel();
stdin_writer
.write_all(&request.stdin)
.map_err(|error| RunError::Io(error.to_string()))?;
drop(stdin_writer);
execute(request, Box::new(stdin_reader), None, on_execution).map(|execution| execution.result)
}

pub(super) type StdoutPeer = Box<dyn FnOnce(CappedOutput) -> Box<dyn VirtualFile + Send + Sync>>;

pub(super) struct Execution {
pub result: RunResult,
pub exit_code: Option<i32>,
}

pub(super) fn execute(
request: RunRequest,
stdin: Box<dyn VirtualFile + Send + Sync>,
stdout_peer: Option<StdoutPeer>,
mut on_execution: impl FnMut(bool) -> Result<(), RunError>,
) -> Result<Execution, RunError> {
let limited = enforce_memory_limit(&request.wasm, request.resources.memory_limit_bytes)
.map_err(RunError::Compile)?;
let metered = instrument_wasm(&limited, request.resources.instruction_budget)
Expand All @@ -29,11 +51,6 @@ pub fn run(
})?;
let runtime = runtime_with_engine(store.engine().clone());

let (mut stdin_writer, stdin_reader) = Pipe::channel();
stdin_writer
.write_all(&request.stdin)
.map_err(|error| RunError::Io(error.to_string()))?;
drop(stdin_writer);
let output_limit = usize::try_from(request.resources.output_limit_bytes)
.map_err(|_| RunError::InvalidRequest("output limit exceeds host range".to_string()))?;
let output_budget = OutputBudget::new(output_limit);
Expand All @@ -51,8 +68,11 @@ pub fn run(
.runtime(runtime)
.args(request.args.clone())
.envs(request.env.clone())
.stdin(Box::new(stdin_reader))
.stdout(Box::new(stdout_file))
.stdin(stdin)
.stdout(match stdout_peer {
Some(peer) => peer(stdout_file),
None => Box::new(stdout_file),
})
.stderr(Box::new(stderr_file))
.fs(filesystem.clone());
builder
Expand Down Expand Up @@ -138,12 +158,14 @@ pub fn run(
let logical_time_exceeded = clock.limit_exceeded()?;

let mut code = 0;
let mut exit_code = None;
let mut termination = ExecutionTermination::Exited;
let mut trap_message = None;
if let Err(error) = execution {
if let Some(wasi_error) = crate::wasi_error(&error) {
match wasi_error {
WasiError::Exit(exit) => {
exit_code = Some(exit.raw());
let errno: wasmer_wasix_types::wasi::Errno = (*exit).into();
if errno != wasmer_wasix_types::wasi::Errno::Success {
code = errno as i32;
Expand Down Expand Up @@ -211,7 +233,7 @@ pub fn run(
CostPoints::Exhausted => request.resources.instruction_budget,
};
let filesystem_metrics = project_filesystem.metrics();
Ok(RunResult {
let result = RunResult {
code,
metrics: ExecutionMetrics {
cost,
Expand All @@ -231,5 +253,6 @@ pub fn run(
trap_message,
determinism: request.determinism,
resources: request.resources,
})
};
Ok(Execution { result, exit_code })
}
Loading