@wasm-oj/server 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -111,7 +111,7 @@ fn run_in_runtime(request: RunRequest) -> Result<RunResult, RunError> {
111
111
 
112
112
  let instance = Instance::new(&mut store, &module, &imports)
113
113
  .map_err(|error| RunError::Compile(format!("failed to instantiate module: {error}")))?;
114
- let meter = meter_state(&mut store, &instance).map_err(RunError::Runtime)?;
114
+ let meter = meter_state(&instance).map_err(RunError::Runtime)?;
115
115
  let guest_memory = instance
116
116
  .exports
117
117
  .get_memory("memory")
@@ -5,10 +5,11 @@ use crate::filesystem::{read_files_bounded, runtime_project_files};
5
5
  use crate::meter::{CostPoints, METER_MODEL, instrument_wasm, meter_state, remaining_points};
6
6
  use crate::module_imports::attach_imported_memory;
7
7
  use crate::module_policy::{DEFERRED_START_EXPORT, defer_start_section, enforce_memory_limit};
8
- use crate::output::{OutputBudget, OutputCapture};
8
+ use crate::output::{CappedOutput, OutputBudget, OutputCapture};
9
9
  use crate::{ExecutionMetrics, ExecutionTermination, RunError, RunRequest, RunResult};
10
10
  use std::io::Write;
11
11
  use std::sync::{Arc, Mutex};
12
+ use virtual_fs::VirtualFile;
12
13
  use wasmer::{Instance, Memory, Module, Store};
13
14
  use wasmer_wasix::{
14
15
  Pipe, WasiEnv, WasiError, WasiModuleInstanceHandles, WasiModuleTreeHandles, wasmer_wasix_types,
@@ -16,8 +17,29 @@ use wasmer_wasix::{
16
17
 
17
18
  pub fn run(
18
19
  request: RunRequest,
19
- mut on_execution: impl FnMut(bool) -> Result<(), RunError>,
20
+ on_execution: impl FnMut(bool) -> Result<(), RunError>,
20
21
  ) -> Result<RunResult, RunError> {
22
+ let (mut stdin_writer, stdin_reader) = Pipe::channel();
23
+ stdin_writer
24
+ .write_all(&request.stdin)
25
+ .map_err(|error| RunError::Io(error.to_string()))?;
26
+ drop(stdin_writer);
27
+ execute(request, Box::new(stdin_reader), None, on_execution).map(|execution| execution.result)
28
+ }
29
+
30
+ pub(super) type StdoutPeer = Box<dyn FnOnce(CappedOutput) -> Box<dyn VirtualFile + Send + Sync>>;
31
+
32
+ pub(super) struct Execution {
33
+ pub result: RunResult,
34
+ pub exit_code: Option<i32>,
35
+ }
36
+
37
+ pub(super) fn execute(
38
+ request: RunRequest,
39
+ stdin: Box<dyn VirtualFile + Send + Sync>,
40
+ stdout_peer: Option<StdoutPeer>,
41
+ mut on_execution: impl FnMut(bool) -> Result<(), RunError>,
42
+ ) -> Result<Execution, RunError> {
21
43
  let limited = enforce_memory_limit(&request.wasm, request.resources.memory_limit_bytes)
22
44
  .map_err(RunError::Compile)?;
23
45
  let metered = instrument_wasm(&limited, request.resources.instruction_budget)
@@ -29,11 +51,6 @@ pub fn run(
29
51
  })?;
30
52
  let runtime = runtime_with_engine(store.engine().clone());
31
53
 
32
- let (mut stdin_writer, stdin_reader) = Pipe::channel();
33
- stdin_writer
34
- .write_all(&request.stdin)
35
- .map_err(|error| RunError::Io(error.to_string()))?;
36
- drop(stdin_writer);
37
54
  let output_limit = usize::try_from(request.resources.output_limit_bytes)
38
55
  .map_err(|_| RunError::InvalidRequest("output limit exceeds host range".to_string()))?;
39
56
  let output_budget = OutputBudget::new(output_limit);
@@ -51,8 +68,11 @@ pub fn run(
51
68
  .runtime(runtime)
52
69
  .args(request.args.clone())
53
70
  .envs(request.env.clone())
54
- .stdin(Box::new(stdin_reader))
55
- .stdout(Box::new(stdout_file))
71
+ .stdin(stdin)
72
+ .stdout(match stdout_peer {
73
+ Some(peer) => peer(stdout_file),
74
+ None => Box::new(stdout_file),
75
+ })
56
76
  .stderr(Box::new(stderr_file))
57
77
  .fs(filesystem.clone());
58
78
  builder
@@ -87,7 +107,7 @@ pub fn run(
87
107
  attach_imported_memory(&mut store, &module, &mut imports).map_err(RunError::Compile)?;
88
108
  let instance = Instance::new(&mut store, &module, &imports)
89
109
  .map_err(|error| RunError::Compile(format!("failed to instantiate module: {error}")))?;
90
- let meter = meter_state(&mut store, &instance).map_err(RunError::Runtime)?;
110
+ let meter = meter_state(&instance).map_err(RunError::Runtime)?;
91
111
  let guest_memory = instance
92
112
  .exports
93
113
  .get_memory("memory")
@@ -138,12 +158,14 @@ pub fn run(
138
158
  let logical_time_exceeded = clock.limit_exceeded()?;
139
159
 
140
160
  let mut code = 0;
161
+ let mut exit_code = None;
141
162
  let mut termination = ExecutionTermination::Exited;
142
163
  let mut trap_message = None;
143
164
  if let Err(error) = execution {
144
165
  if let Some(wasi_error) = crate::wasi_error(&error) {
145
166
  match wasi_error {
146
167
  WasiError::Exit(exit) => {
168
+ exit_code = Some(exit.raw());
147
169
  let errno: wasmer_wasix_types::wasi::Errno = (*exit).into();
148
170
  if errno != wasmer_wasix_types::wasi::Errno::Success {
149
171
  code = errno as i32;
@@ -211,7 +233,7 @@ pub fn run(
211
233
  CostPoints::Exhausted => request.resources.instruction_budget,
212
234
  };
213
235
  let filesystem_metrics = project_filesystem.metrics();
214
- Ok(RunResult {
236
+ let result = RunResult {
215
237
  code,
216
238
  metrics: ExecutionMetrics {
217
239
  cost,
@@ -231,5 +253,6 @@ pub fn run(
231
253
  trap_message,
232
254
  determinism: request.determinism,
233
255
  resources: request.resources,
234
- })
256
+ };
257
+ Ok(Execution { result, exit_code })
235
258
  }
@@ -0,0 +1,370 @@
1
+ use super::web::{Execution, execute};
2
+ use crate::output::CappedOutput;
3
+ use crate::{
4
+ DeterminismConfig, ExecutionTermination, InteractiveMetrics, InteractiveProcessResult,
5
+ InteractiveProgram, RunError, RunRequest,
6
+ };
7
+ use serde::{Deserialize, Serialize};
8
+ use std::io;
9
+ use std::pin::Pin;
10
+ use std::sync::Arc;
11
+ use std::task::{Context, Poll};
12
+ use tokio::io::{AsyncRead, AsyncSeek, AsyncWrite, ReadBuf};
13
+ use virtual_fs::{FsError, VirtualFile};
14
+ use wasm_bindgen::JsValue;
15
+
16
+ #[derive(Clone, Debug, Deserialize, Serialize)]
17
+ #[serde(deny_unknown_fields, rename_all = "camelCase")]
18
+ pub struct InteractiveSideRequest {
19
+ pub program: InteractiveProgram,
20
+ pub determinism: DeterminismConfig,
21
+ }
22
+
23
+ #[derive(Clone, Debug, Serialize)]
24
+ #[serde(rename_all = "camelCase")]
25
+ pub struct InteractiveSideResult {
26
+ pub process: InteractiveProcessResult,
27
+ #[serde(with = "serde_bytes")]
28
+ pub protocol: Vec<u8>,
29
+ }
30
+
31
+ /// Stream callbacks supplied by the side Worker. Reads and writes wait with
32
+ /// `Atomics.wait`, so they never return `Pending` to WASIX.
33
+ pub struct HostStreams {
34
+ read: js_sys::Function,
35
+ poll: js_sys::Function,
36
+ wait: js_sys::Function,
37
+ write: js_sys::Function,
38
+ close: js_sys::Function,
39
+ }
40
+
41
+ // SAFETY: the web build of runtime-core has no threads; these JS handles never
42
+ // leave the Worker thread that created them.
43
+ unsafe impl Send for HostStreams {}
44
+ // SAFETY: see `Send`.
45
+ unsafe impl Sync for HostStreams {}
46
+
47
+ impl HostStreams {
48
+ pub fn new(
49
+ read: js_sys::Function,
50
+ poll: js_sys::Function,
51
+ wait: js_sys::Function,
52
+ write: js_sys::Function,
53
+ close: js_sys::Function,
54
+ ) -> Self {
55
+ Self {
56
+ read,
57
+ poll,
58
+ wait,
59
+ write,
60
+ close,
61
+ }
62
+ }
63
+
64
+ fn read(&self, maximum: usize) -> io::Result<Vec<u8>> {
65
+ let chunk = self
66
+ .read
67
+ .call1(&JsValue::UNDEFINED, &JsValue::from(maximum as u32))
68
+ .map_err(host_error)?;
69
+ Ok(js_sys::Uint8Array::new(&chunk).to_vec())
70
+ }
71
+
72
+ fn poll(&self) -> io::Result<Option<usize>> {
73
+ let available = self
74
+ .poll
75
+ .call0(&JsValue::UNDEFINED)
76
+ .map_err(host_error)?
77
+ .as_f64()
78
+ .ok_or_else(|| io::Error::other("interactive input poll returned no size"))?;
79
+ Ok((available >= 0.0).then_some(available as usize))
80
+ }
81
+
82
+ fn wait(&self) -> io::Result<usize> {
83
+ let available = self
84
+ .wait
85
+ .call0(&JsValue::UNDEFINED)
86
+ .map_err(host_error)?
87
+ .as_f64()
88
+ .ok_or_else(|| io::Error::other("interactive input wait returned no size"))?;
89
+ Ok(available as usize)
90
+ }
91
+
92
+ fn write(&self, bytes: &[u8]) -> io::Result<usize> {
93
+ let written = self
94
+ .write
95
+ .call1(&JsValue::UNDEFINED, &js_sys::Uint8Array::from(bytes))
96
+ .map_err(host_error)?
97
+ .as_f64()
98
+ .ok_or_else(|| io::Error::other("interactive output write returned no size"))?;
99
+ if written < 0.0 {
100
+ return Err(io::ErrorKind::BrokenPipe.into());
101
+ }
102
+ Ok(written as usize)
103
+ }
104
+
105
+ /// Closes the input (fd 0) or output (fd 1) pipe end, so the peer sees
106
+ /// EOF or a broken pipe at once, as when native drops its pipe end.
107
+ fn close(&self, fd: u32) {
108
+ let _ = self.close.call1(&JsValue::UNDEFINED, &JsValue::from(fd));
109
+ }
110
+ }
111
+
112
+ fn host_error(error: JsValue) -> io::Error {
113
+ io::Error::other(format!("interactive stream failed: {error:?}"))
114
+ }
115
+
116
+ pub fn run_side(
117
+ request: InteractiveSideRequest,
118
+ streams: HostStreams,
119
+ on_execution: impl FnMut(bool) -> Result<(), RunError>,
120
+ ) -> Result<InteractiveSideResult, RunError> {
121
+ let program = request.program;
122
+ if program.wasm.is_empty() {
123
+ return Err(RunError::InvalidRequest(
124
+ "interactive Wasm must not be empty".to_string(),
125
+ ));
126
+ }
127
+ let run = RunRequest {
128
+ wasm: program.wasm,
129
+ args: program.args,
130
+ env: program.env,
131
+ stdin: Vec::new(),
132
+ files: program.files,
133
+ output_paths: Vec::new(),
134
+ cwd: program.cwd,
135
+ startup_entropy_bytes: program.startup_entropy_bytes,
136
+ determinism: request.determinism,
137
+ resources: program.resources,
138
+ };
139
+ super::validate(&run)?;
140
+ let streams = Arc::new(streams);
141
+ let input = StreamInput {
142
+ streams: streams.clone(),
143
+ };
144
+ let Execution { result, exit_code } = execute(
145
+ run,
146
+ Box::new(input),
147
+ Some(Box::new(move |capture| {
148
+ Box::new(StreamOutput { streams, capture })
149
+ })),
150
+ on_execution,
151
+ )?;
152
+ let code = match exit_code {
153
+ Some(code) if result.termination == ExecutionTermination::Exited => code,
154
+ _ => result.code,
155
+ };
156
+ Ok(InteractiveSideResult {
157
+ process: InteractiveProcessResult {
158
+ code,
159
+ stderr: result.stderr,
160
+ termination: result.termination,
161
+ metrics: InteractiveMetrics {
162
+ cost: result.metrics.cost,
163
+ operations: result.metrics.operations,
164
+ logical_time_ns: result.metrics.logical_time_ns,
165
+ filesystem_bytes: result.metrics.filesystem_bytes,
166
+ filesystem_entries: result.metrics.filesystem_entries,
167
+ protocol_bytes: result.metrics.stdout_bytes,
168
+ stderr_bytes: result.metrics.stderr_bytes,
169
+ },
170
+ },
171
+ protocol: result.stdout,
172
+ })
173
+ }
174
+
175
+ struct StreamInput {
176
+ streams: Arc<HostStreams>,
177
+ }
178
+
179
+ // WASIX drops the stdio handle when the last descriptor that refers to it is
180
+ // closed, which is when native drops its `PipeRx` or `PipeTx`.
181
+ impl Drop for StreamInput {
182
+ fn drop(&mut self) {
183
+ self.streams.close(0);
184
+ }
185
+ }
186
+
187
+ impl std::fmt::Debug for StreamInput {
188
+ fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189
+ formatter.write_str("StreamInput")
190
+ }
191
+ }
192
+
193
+ impl AsyncRead for StreamInput {
194
+ fn poll_read(
195
+ self: Pin<&mut Self>,
196
+ _context: &mut Context<'_>,
197
+ buffer: &mut ReadBuf<'_>,
198
+ ) -> Poll<io::Result<()>> {
199
+ Poll::Ready(self.streams.read(buffer.remaining()).map(|chunk| {
200
+ buffer.put_slice(&chunk);
201
+ }))
202
+ }
203
+ }
204
+
205
+ impl AsyncWrite for StreamInput {
206
+ fn poll_write(
207
+ self: Pin<&mut Self>,
208
+ _context: &mut Context<'_>,
209
+ _buffer: &[u8],
210
+ ) -> Poll<io::Result<usize>> {
211
+ Poll::Ready(Err(io::ErrorKind::Unsupported.into()))
212
+ }
213
+
214
+ fn poll_flush(self: Pin<&mut Self>, _context: &mut Context<'_>) -> Poll<io::Result<()>> {
215
+ Poll::Ready(Ok(()))
216
+ }
217
+
218
+ fn poll_shutdown(self: Pin<&mut Self>, _context: &mut Context<'_>) -> Poll<io::Result<()>> {
219
+ Poll::Ready(Ok(()))
220
+ }
221
+ }
222
+
223
+ impl AsyncSeek for StreamInput {
224
+ fn start_seek(self: Pin<&mut Self>, _position: io::SeekFrom) -> io::Result<()> {
225
+ Ok(())
226
+ }
227
+
228
+ fn poll_complete(self: Pin<&mut Self>, _context: &mut Context<'_>) -> Poll<io::Result<u64>> {
229
+ Poll::Ready(Ok(0))
230
+ }
231
+ }
232
+
233
+ impl VirtualFile for StreamInput {
234
+ fn last_accessed(&self) -> u64 {
235
+ 0
236
+ }
237
+ fn last_modified(&self) -> u64 {
238
+ 0
239
+ }
240
+ fn created_time(&self) -> u64 {
241
+ 0
242
+ }
243
+ fn size(&self) -> u64 {
244
+ 0
245
+ }
246
+ fn set_len(&mut self, _new_size: u64) -> Result<(), FsError> {
247
+ Ok(())
248
+ }
249
+ fn unlink(&mut self) -> Result<(), FsError> {
250
+ Ok(())
251
+ }
252
+ fn get_special_fd(&self) -> Option<u32> {
253
+ Some(0)
254
+ }
255
+
256
+ fn poll_read_ready(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<io::Result<usize>> {
257
+ match self.streams.poll() {
258
+ Ok(Some(available)) => Poll::Ready(Ok(available)),
259
+ Ok(None) if crate::deterministic::defer_stdin_readiness() => {
260
+ context.waker().wake_by_ref();
261
+ Poll::Pending
262
+ }
263
+ Ok(None) => Poll::Ready(self.streams.wait()),
264
+ Err(error) => Poll::Ready(Err(error)),
265
+ }
266
+ }
267
+
268
+ fn poll_write_ready(
269
+ self: Pin<&mut Self>,
270
+ _context: &mut Context<'_>,
271
+ ) -> Poll<io::Result<usize>> {
272
+ Poll::Ready(Err(io::ErrorKind::Unsupported.into()))
273
+ }
274
+ }
275
+
276
+ struct StreamOutput {
277
+ streams: Arc<HostStreams>,
278
+ capture: CappedOutput,
279
+ }
280
+
281
+ impl Drop for StreamOutput {
282
+ fn drop(&mut self) {
283
+ self.streams.close(1);
284
+ }
285
+ }
286
+
287
+ impl std::fmt::Debug for StreamOutput {
288
+ fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
289
+ formatter.write_str("StreamOutput")
290
+ }
291
+ }
292
+
293
+ impl AsyncWrite for StreamOutput {
294
+ fn poll_write(
295
+ mut self: Pin<&mut Self>,
296
+ context: &mut Context<'_>,
297
+ buffer: &[u8],
298
+ ) -> Poll<io::Result<usize>> {
299
+ match Pin::new(&mut self.capture).poll_write(context, buffer) {
300
+ Poll::Ready(Ok(written)) => Poll::Ready(self.streams.write(&buffer[..written])),
301
+ result => result,
302
+ }
303
+ }
304
+
305
+ fn poll_flush(self: Pin<&mut Self>, _context: &mut Context<'_>) -> Poll<io::Result<()>> {
306
+ Poll::Ready(Ok(()))
307
+ }
308
+
309
+ fn poll_shutdown(self: Pin<&mut Self>, _context: &mut Context<'_>) -> Poll<io::Result<()>> {
310
+ Poll::Ready(Ok(()))
311
+ }
312
+ }
313
+
314
+ impl AsyncRead for StreamOutput {
315
+ fn poll_read(
316
+ self: Pin<&mut Self>,
317
+ _context: &mut Context<'_>,
318
+ _buffer: &mut ReadBuf<'_>,
319
+ ) -> Poll<io::Result<()>> {
320
+ Poll::Ready(Ok(()))
321
+ }
322
+ }
323
+
324
+ impl AsyncSeek for StreamOutput {
325
+ fn start_seek(self: Pin<&mut Self>, _position: io::SeekFrom) -> io::Result<()> {
326
+ Ok(())
327
+ }
328
+
329
+ fn poll_complete(self: Pin<&mut Self>, _context: &mut Context<'_>) -> Poll<io::Result<u64>> {
330
+ Poll::Ready(Ok(0))
331
+ }
332
+ }
333
+
334
+ impl VirtualFile for StreamOutput {
335
+ fn last_accessed(&self) -> u64 {
336
+ 0
337
+ }
338
+ fn last_modified(&self) -> u64 {
339
+ 0
340
+ }
341
+ fn created_time(&self) -> u64 {
342
+ 0
343
+ }
344
+ fn size(&self) -> u64 {
345
+ 0
346
+ }
347
+ fn set_len(&mut self, _new_size: u64) -> Result<(), FsError> {
348
+ Ok(())
349
+ }
350
+ fn unlink(&mut self) -> Result<(), FsError> {
351
+ Ok(())
352
+ }
353
+ fn get_special_fd(&self) -> Option<u32> {
354
+ Some(1)
355
+ }
356
+
357
+ fn poll_read_ready(
358
+ self: Pin<&mut Self>,
359
+ _context: &mut Context<'_>,
360
+ ) -> Poll<io::Result<usize>> {
361
+ Poll::Ready(Ok(0))
362
+ }
363
+
364
+ fn poll_write_ready(
365
+ mut self: Pin<&mut Self>,
366
+ context: &mut Context<'_>,
367
+ ) -> Poll<io::Result<usize>> {
368
+ Pin::new(&mut self.capture).poll_write_ready(context)
369
+ }
370
+ }
@@ -1,6 +1,7 @@
1
+ use crate::run::web_interactive::{HostStreams, InteractiveSideRequest, run_side};
1
2
  use crate::{
2
3
  GoCompilerSession as CoreGoCompilerSession, GoCompilerSessionConfig, GoCompilerSessionRequest,
3
- InteractiveRequest, RunError, RunRequest, interactive_response, run_response_from_result,
4
+ RunError, RunFailure, RunRequest, run_response_from_result,
4
5
  };
5
6
  use serde::Serialize;
6
7
  use wasm_bindgen::prelude::*;
@@ -72,17 +73,62 @@ impl WebGoCompilerSession {
72
73
  }
73
74
  }
74
75
 
76
+ /// Runs one side of an interactive session in the calling Worker. `read`,
77
+ /// `wait` and `write` block on the session's shared ring buffers; `poll`
78
+ /// checks the input without blocking; `close(fd)` closes the input (0) or
79
+ /// output (1) end when the guest drops it.
75
80
  #[wasm_bindgen]
76
- pub async fn interact_wasm_oj(request: JsValue) -> Result<JsValue, JsValue> {
81
+ pub fn run_interactive_side(
82
+ request: JsValue,
83
+ read: js_sys::Function,
84
+ poll: js_sys::Function,
85
+ wait: js_sys::Function,
86
+ write: js_sys::Function,
87
+ close: js_sys::Function,
88
+ on_execution: js_sys::Function,
89
+ ) -> Result<JsValue, JsValue> {
77
90
  console_error_panic_hook::set_once();
78
- let request: InteractiveRequest = serde_wasm_bindgen::from_value(request)
79
- .map_err(|error| JsValue::from_str(&format!("invalid interactive request: {error}")))?;
80
- let response = interactive_response(request).await;
91
+ let request: InteractiveSideRequest =
92
+ serde_wasm_bindgen::from_value(request).map_err(|error| {
93
+ JsValue::from_str(&format!("invalid interactive side request: {error}"))
94
+ })?;
95
+ let response = match run_side(
96
+ request,
97
+ HostStreams::new(read, poll, wait, write, close),
98
+ |running| {
99
+ on_execution
100
+ .call1(&JsValue::UNDEFINED, &JsValue::from_bool(running))
101
+ .map(|_| ())
102
+ .map_err(|error| RunError::Runtime(format!("execution observer failed: {error:?}")))
103
+ },
104
+ ) {
105
+ Ok(result) => InteractiveSideResponse {
106
+ ok: true,
107
+ result: Some(result),
108
+ error: None,
109
+ },
110
+ Err(error) => InteractiveSideResponse {
111
+ ok: false,
112
+ result: None,
113
+ error: Some(RunFailure {
114
+ code: error.code(),
115
+ message: error.to_string(),
116
+ }),
117
+ },
118
+ };
81
119
  response
82
120
  .serialize(&serde_wasm_bindgen::Serializer::new().serialize_maps_as_objects(true))
83
121
  .map_err(|error| {
84
122
  JsValue::from_str(&format!(
85
- "failed to serialize interactive response: {error}"
123
+ "failed to serialize interactive side response: {error}"
86
124
  ))
87
125
  })
88
126
  }
127
+
128
+ #[derive(Serialize)]
129
+ #[serde(rename_all = "camelCase")]
130
+ struct InteractiveSideResponse {
131
+ ok: bool,
132
+ result: Option<crate::run::web_interactive::InteractiveSideResult>,
133
+ error: Option<RunFailure>,
134
+ }