kobako 0.18.0 → 0.20.0
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.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +31 -0
- data/Cargo.lock +6 -5
- data/README.md +4 -1
- data/crates/kobako-runtime/CHANGELOG.md +14 -0
- data/crates/kobako-runtime/Cargo.toml +1 -1
- data/crates/kobako-runtime/README.md +1 -1
- data/crates/kobako-wasmtime/CHANGELOG.md +14 -0
- data/crates/kobako-wasmtime/Cargo.toml +2 -2
- data/crates/kobako-wasmtime/README.md +1 -1
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +6 -2
- data/ext/kobako/src/runtime/bridge.rs +30 -24
- data/ext/kobako/src/runtime/gvl.rs +114 -0
- data/ext/kobako/src/runtime.rs +211 -190
- data/lib/kobako/catalog/extensions.rb +50 -43
- data/lib/kobako/catalog/handles.rb +7 -16
- data/lib/kobako/catalog/services.rb +22 -36
- data/lib/kobako/catalog/snippets.rb +11 -10
- data/lib/kobako/context.rb +202 -0
- data/lib/kobako/errors.rb +26 -4
- data/lib/kobako/execution.rb +52 -0
- data/lib/kobako/extension.rb +24 -10
- data/lib/kobako/pool.rb +1 -7
- data/lib/kobako/sandbox.rb +43 -182
- data/lib/kobako/sandbox_options.rb +23 -2
- data/lib/kobako/transport/dispatcher.rb +16 -15
- data/lib/kobako/transport/yielder.rb +1 -1
- data/lib/kobako/unresolved.rb +17 -0
- data/lib/kobako/usage.rb +6 -6
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +1 -0
- data/release-please-config.json +2 -1
- data/sig/kobako/catalog/extensions.rbs +4 -6
- data/sig/kobako/catalog/handles.rbs +0 -2
- data/sig/kobako/catalog/services.rbs +3 -3
- data/sig/kobako/catalog/snippets.rbs +2 -0
- data/sig/kobako/context.rbs +36 -0
- data/sig/kobako/errors.rbs +12 -3
- data/sig/kobako/execution.rbs +24 -0
- data/sig/kobako/extension.rbs +4 -2
- data/sig/kobako/runtime.rbs +37 -9
- data/sig/kobako/sandbox.rbs +5 -24
- data/sig/kobako/sandbox_options.rbs +9 -2
- data/sig/kobako/transport/dispatcher.rbs +3 -3
- data/sig/kobako/unresolved.rbs +4 -0
- metadata +8 -1
data/ext/kobako/src/runtime.rs
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
//! Host-side magnus shell over the extracted wasmtime driver.
|
|
2
2
|
//!
|
|
3
|
-
//! The
|
|
3
|
+
//! The Ruby-visible classes are
|
|
4
4
|
//!
|
|
5
|
-
//! Kobako::Runtime
|
|
5
|
+
//! Kobako::Runtime — wraps a `kobako_wasmtime::Driver`
|
|
6
|
+
//! Kobako::Runtime::Snapshot — one invocation's completion + captures + usage
|
|
6
7
|
//!
|
|
7
|
-
//! constructed via `Kobako::Runtime.from_path(path,
|
|
8
|
-
//! stdout_limit, stderr_limit, profile)`. Every
|
|
9
|
-
//!
|
|
10
|
-
//!
|
|
8
|
+
//! `Kobako::Runtime` is constructed via `Kobako::Runtime.from_path(path,
|
|
9
|
+
//! timeout, memory_limit, stdout_limit, stderr_limit, profile)`. Every
|
|
10
|
+
//! invocation (`#eval` / `#run`) takes that run's dispatch handler as a call
|
|
11
|
+
//! argument, instantiates a fresh instance, and returns a `Snapshot` — the
|
|
12
|
+
//! whole per-invocation result — so the Runtime holds no per-invocation
|
|
13
|
+
//! state and one Runtime is safe to drive concurrently. The run mechanics —
|
|
11
14
|
//! engine/module caches, caps, trap classification — live in the
|
|
12
15
|
//! `kobako-wasmtime` crate behind the `kobako_runtime` contract; no wasm
|
|
13
16
|
//! engine type reaches this crate or the Host App.
|
|
@@ -17,26 +20,24 @@
|
|
|
17
20
|
//! * `bridge` — the magnus dispatch bridge: `RubyDispatchHandler` plus the
|
|
18
21
|
//! frame-scoped `GuestYielder` Ruby class.
|
|
19
22
|
//! * `errors` — the single boundary mapping the neutral `Trap` /
|
|
20
|
-
//! `SetupError` channels onto the `Kobako::*` classes
|
|
21
|
-
//!
|
|
22
|
-
//! This file owns the `Kobako::Runtime` magnus class itself — the Ruby
|
|
23
|
-
//! init() that registers the class, the byte↔`RString` shuttling, the
|
|
24
|
-
//! dispatch-Proc GC root, and the per-invocation usage / capture readouts.
|
|
23
|
+
//! `SetupError` channels onto the `Kobako::*` classes for a failure that
|
|
24
|
+
//! never produced a `Snapshot` (a could-not-start fault).
|
|
25
25
|
|
|
26
26
|
mod bridge;
|
|
27
27
|
mod errors;
|
|
28
|
+
mod gvl;
|
|
28
29
|
|
|
29
30
|
use magnus::{
|
|
30
|
-
function,
|
|
31
|
-
Error as MagnusError,
|
|
31
|
+
function, method, prelude::*, typed_data::DataTypeFunctions, value::Opaque,
|
|
32
|
+
Error as MagnusError, RModule, RString, Ruby, Symbol, TypedData, Value,
|
|
32
33
|
};
|
|
33
34
|
|
|
34
|
-
use std::cell::{Cell, RefCell};
|
|
35
35
|
use std::path::Path;
|
|
36
36
|
use std::sync::Arc;
|
|
37
37
|
use std::time::Duration;
|
|
38
38
|
|
|
39
39
|
use kobako_runtime::dispatch::DispatchHandler;
|
|
40
|
+
use kobako_runtime::error::Trap;
|
|
40
41
|
use kobako_runtime::profile::Profile;
|
|
41
42
|
use kobako_runtime::runtime::{Entry, Frames, Runtime as ContractRuntime};
|
|
42
43
|
use kobako_runtime::snapshot::{Capture, Completion, Snapshot as RuntimeSnapshot, Usage};
|
|
@@ -63,73 +64,49 @@ pub fn init(ruby: &Ruby, kobako: RModule) -> Result<(), MagnusError> {
|
|
|
63
64
|
// in `runtime/errors.rs` — no intermediate hierarchy is registered.
|
|
64
65
|
|
|
65
66
|
let runtime = kobako.define_class("Runtime", ruby.class_object())?;
|
|
66
|
-
runtime.define_singleton_method("from_path", function!(Runtime::from_path,
|
|
67
|
-
runtime.define_method("
|
|
68
|
-
runtime.define_method("
|
|
69
|
-
runtime.define_method("run", method!(Runtime::run, 3))?;
|
|
70
|
-
runtime.define_method("usage", method!(Runtime::usage, 0))?;
|
|
71
|
-
runtime.define_method("captures", method!(Runtime::captures, 0))?;
|
|
67
|
+
runtime.define_singleton_method("from_path", function!(Runtime::from_path, 7))?;
|
|
68
|
+
runtime.define_method("eval", method!(Runtime::eval, 4))?;
|
|
69
|
+
runtime.define_method("run", method!(Runtime::run, 4))?;
|
|
72
70
|
runtime.define_method("profile", method!(Runtime::profile, 0))?;
|
|
73
71
|
// The guest re-enters for a block yield through a frame-scoped
|
|
74
72
|
// `Kobako::Runtime::GuestYielder` the dispatcher hands the Proc, not a
|
|
75
73
|
// method on Runtime.
|
|
76
74
|
bridge::register(runtime)?;
|
|
77
75
|
|
|
76
|
+
// Snapshot — the per-invocation result object each entry point returns.
|
|
77
|
+
let snapshot = runtime.define_class("Snapshot", ruby.class_object())?;
|
|
78
|
+
snapshot.define_method("outcome", method!(Snapshot::outcome, 0))?;
|
|
79
|
+
snapshot.define_method("trapped?", method!(Snapshot::trapped, 0))?;
|
|
80
|
+
snapshot.define_method("trap_kind", method!(Snapshot::trap_kind, 0))?;
|
|
81
|
+
snapshot.define_method("trap_message", method!(Snapshot::trap_message, 0))?;
|
|
82
|
+
snapshot.define_method("wall_time", method!(Snapshot::wall_time, 0))?;
|
|
83
|
+
snapshot.define_method("memory_peak", method!(Snapshot::memory_peak, 0))?;
|
|
84
|
+
snapshot.define_method("stdout", method!(Snapshot::stdout, 0))?;
|
|
85
|
+
snapshot.define_method("stdout_truncated?", method!(Snapshot::stdout_truncated, 0))?;
|
|
86
|
+
snapshot.define_method("stderr", method!(Snapshot::stderr, 0))?;
|
|
87
|
+
snapshot.define_method("stderr_truncated?", method!(Snapshot::stderr_truncated, 0))?;
|
|
88
|
+
|
|
78
89
|
Ok(())
|
|
79
90
|
}
|
|
80
91
|
|
|
81
92
|
#[derive(TypedData)]
|
|
82
|
-
#[magnus(class = "Kobako::Runtime", free_immediately, size
|
|
93
|
+
#[magnus(class = "Kobako::Runtime", free_immediately, size)]
|
|
83
94
|
struct Runtime {
|
|
84
95
|
// The magnus-free wasmtime driver that runs every invocation; the
|
|
85
|
-
// shell only shuttles Ruby values across its boundary.
|
|
96
|
+
// shell only shuttles Ruby values across its boundary. The Runtime
|
|
97
|
+
// holds no per-invocation state — each `#eval` / `#run` takes its
|
|
98
|
+
// dispatch handler as an argument and returns its whole result as a
|
|
99
|
+
// `Snapshot` — so `Driver`'s own `Send + Sync` carries the type with no
|
|
100
|
+
// interior mutability to guard.
|
|
86
101
|
driver: Driver,
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
// `Invocation`, where the `__kobako_dispatch` import calls it — both
|
|
93
|
-
// reference the one Proc this `Opaque` pins. `Cell` is sound under the
|
|
94
|
-
// GVL (see the `unsafe impl Sync` below).
|
|
95
|
-
on_dispatch: Cell<Option<Opaque<Value>>>,
|
|
96
|
-
// Usage of the most recent invocation, stashed here so `#usage` reads
|
|
97
|
-
// survive the per-invocation Store teardown and the trap path's
|
|
98
|
-
// raise. Zeroed before the first invocation.
|
|
99
|
-
last_usage: Cell<Usage>,
|
|
100
|
-
// Output captures of the most recent invocation, stashed for the same
|
|
101
|
-
// reason as `last_usage`: the trap path raises, and this readout is
|
|
102
|
-
// what keeps the guest's partial output readable after a rescue.
|
|
103
|
-
// `RefCell` (not `Cell`) because `Capture` owns its byte buffer; the
|
|
104
|
-
// same GVL single-thread discipline applies (see the `unsafe impl
|
|
105
|
-
// Sync` below). Empty before the first invocation.
|
|
106
|
-
last_captures: RefCell<(Capture, Capture)>,
|
|
102
|
+
// Whether each invocation releases Ruby's GVL for its guest span
|
|
103
|
+
// (`gvl: :release`) or holds it throughout (`gvl: :hold`). Fixed at
|
|
104
|
+
// construction; a `bool` carries no interior mutability, so the type
|
|
105
|
+
// stays `Send + Sync`.
|
|
106
|
+
release_gvl: bool,
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
impl DataTypeFunctions for Runtime {
|
|
110
|
-
/// Mark — and thereby pin — the host-side dispatch Proc so Ruby's GC
|
|
111
|
-
/// neither collects nor moves it while the ext holds a raw `Opaque`
|
|
112
|
-
/// copy on `Invocation` for the duration of a guest invocation.
|
|
113
|
-
/// `gc::Marker::mark` maps to `rb_gc_mark`, which pins: required because
|
|
114
|
-
/// the Invocation copy is a cached `VALUE` that compaction would
|
|
115
|
-
/// otherwise leave dangling. Without
|
|
116
|
-
/// this the Proc has no GC root at all — sweep collects it (SIGSEGV on
|
|
117
|
-
/// the next dispatch) and compaction relocates it (dispatch lands on
|
|
118
|
-
/// the wrong receiver).
|
|
119
|
-
fn mark(&self, marker: &gc::Marker) {
|
|
120
|
-
if let Some(on_dispatch) = self.on_dispatch.get() {
|
|
121
|
-
marker.mark(on_dispatch);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// SAFETY: magnus requires `Send + Sync` on TypedData types. The
|
|
127
|
-
// `on_dispatch` / `last_usage` `Cell`s and the `last_captures` `RefCell`
|
|
128
|
-
// make the auto-derived `Sync` unavailable, but every access to them
|
|
129
|
-
// happens under the GVL on a single thread at a time — Ruby method calls,
|
|
130
|
-
// and a GC `mark` pass that also holds the GVL. No cross-thread access to
|
|
131
|
-
// any of them can occur. `Send` stays auto-derived.
|
|
132
|
-
unsafe impl Sync for Runtime {}
|
|
109
|
+
impl DataTypeFunctions for Runtime {}
|
|
133
110
|
|
|
134
111
|
impl Runtime {
|
|
135
112
|
/// Construct a Runtime from a wasm file path, using the process-wide
|
|
@@ -142,10 +119,12 @@ impl Runtime {
|
|
|
142
119
|
/// bytes (`None` disables); `stdout_limit_bytes` / `stderr_limit_bytes`
|
|
143
120
|
/// are the per-channel output caps (`None`
|
|
144
121
|
/// disables); `profile` is the isolation rung the driver builds
|
|
145
|
-
/// (`:permissive` / `:hermetic`)
|
|
146
|
-
///
|
|
147
|
-
///
|
|
148
|
-
///
|
|
122
|
+
/// (`:permissive` / `:hermetic`); `gvl` is the scheduling mode
|
|
123
|
+
/// (`:hold` / `:release`) deciding whether each invocation releases
|
|
124
|
+
/// the GVL for its guest span. All six are validated by the caller
|
|
125
|
+
/// (`Kobako::Sandbox`); this method only refuses non-finite or
|
|
126
|
+
/// non-positive timeouts, off-ladder profiles, and unrecognized gvl
|
|
127
|
+
/// modes as a defence in depth.
|
|
149
128
|
fn from_path(
|
|
150
129
|
path: String,
|
|
151
130
|
timeout_seconds: Option<f64>,
|
|
@@ -153,6 +132,7 @@ impl Runtime {
|
|
|
153
132
|
stdout_limit_bytes: Option<usize>,
|
|
154
133
|
stderr_limit_bytes: Option<usize>,
|
|
155
134
|
profile: Symbol,
|
|
135
|
+
gvl: Symbol,
|
|
156
136
|
) -> Result<Self, MagnusError> {
|
|
157
137
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
158
138
|
let timeout = match timeout_seconds {
|
|
@@ -183,6 +163,19 @@ impl Runtime {
|
|
|
183
163
|
));
|
|
184
164
|
}
|
|
185
165
|
};
|
|
166
|
+
// Same fail-closed posture as `profile`: an unrecognized mode raises
|
|
167
|
+
// rather than defaulting. `SandboxOptions` is the primary validator;
|
|
168
|
+
// this guards direct `from_path` calls.
|
|
169
|
+
let release_gvl = match gvl.name()?.as_ref() {
|
|
170
|
+
"hold" => false,
|
|
171
|
+
"release" => true,
|
|
172
|
+
other => {
|
|
173
|
+
return Err(MagnusError::new(
|
|
174
|
+
ruby.exception_arg_error(),
|
|
175
|
+
format!("gvl must be :hold or :release, got :{other}"),
|
|
176
|
+
));
|
|
177
|
+
}
|
|
178
|
+
};
|
|
186
179
|
|
|
187
180
|
let driver = Driver::new(
|
|
188
181
|
Path::new(&path),
|
|
@@ -197,52 +190,39 @@ impl Runtime {
|
|
|
197
190
|
.map_err(|e| errors::setup_to_magnus(&ruby, e))?;
|
|
198
191
|
Ok(Self {
|
|
199
192
|
driver,
|
|
200
|
-
|
|
201
|
-
last_usage: Cell::new(Usage::default()),
|
|
202
|
-
last_captures: RefCell::new((Capture::default(), Capture::default())),
|
|
193
|
+
release_gvl,
|
|
203
194
|
})
|
|
204
195
|
}
|
|
205
196
|
|
|
206
|
-
/// Register the Ruby-side dispatch `Proc`.
|
|
207
|
-
/// Bound to Ruby as `Kobako::Runtime#on_dispatch=`. The handle is
|
|
208
|
-
/// pinned by `DataTypeFunctions::mark`; for each invocation
|
|
209
|
-
/// `build_handler` wraps a copy in a `RubyDispatchHandler` and the
|
|
210
|
-
/// driver's `invoke` binds it onto the per-invocation `Invocation`,
|
|
211
|
-
/// where the `__kobako_dispatch` import reads it through
|
|
212
|
-
/// `Caller<Invocation>`.
|
|
213
|
-
fn set_on_dispatch(&self, proc_value: Value) -> Result<(), MagnusError> {
|
|
214
|
-
self.on_dispatch.set(Some(Opaque::from(proc_value)));
|
|
215
|
-
Ok(())
|
|
216
|
-
}
|
|
217
|
-
|
|
218
197
|
// -----------------------------------------------------------------
|
|
219
|
-
// Run-path methods. Each
|
|
220
|
-
// `
|
|
221
|
-
//
|
|
222
|
-
//
|
|
198
|
+
// Run-path methods. Each takes the run's dispatch handler as its first
|
|
199
|
+
// argument and returns a `Snapshot` for any completed invocation —
|
|
200
|
+
// success or trap alike. Only a could-not-start fault (a missing export
|
|
201
|
+
// or a fault before the export call) raises a `Kobako::TrapError`
|
|
202
|
+
// directly, since it yields no `Snapshot`.
|
|
223
203
|
// -----------------------------------------------------------------
|
|
224
204
|
|
|
225
|
-
/// One-shot mruby source execution (`#eval`).
|
|
226
|
-
///
|
|
227
|
-
///
|
|
228
|
-
///
|
|
229
|
-
/// `finish_invocation` — or maps a could-not-start `Error` onto its
|
|
230
|
-
/// `Kobako::*` exception. The run mechanics — frames, caps, trap
|
|
231
|
-
/// classification — live in `kobako_wasmtime::Driver`.
|
|
205
|
+
/// One-shot mruby source execution (`#eval`). Builds the dispatch
|
|
206
|
+
/// handler from `dispatch` (the per-invocation Proc), hands the three
|
|
207
|
+
/// stdin frames (`preamble`, `source`, `snippets`) and the source to the
|
|
208
|
+
/// driver, and returns the run's `Snapshot`.
|
|
232
209
|
fn eval(
|
|
233
210
|
&self,
|
|
211
|
+
dispatch: Value,
|
|
234
212
|
preamble: RString,
|
|
235
213
|
source: RString,
|
|
236
214
|
snippets: RString,
|
|
237
|
-
) -> Result<
|
|
215
|
+
) -> Result<Snapshot, MagnusError> {
|
|
238
216
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
239
|
-
let handler =
|
|
217
|
+
let handler = build_handler(dispatch);
|
|
240
218
|
let preamble = rstring_to_vec(preamble);
|
|
241
219
|
let source = rstring_to_vec(source);
|
|
242
220
|
let snippets = rstring_to_vec(snippets);
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
221
|
+
// Release the GVL around the guest span iff this Sandbox asks for it;
|
|
222
|
+
// the closure touches no Ruby VALUE (the driver is magnus-free, and a
|
|
223
|
+
// guest→host dispatch re-acquires the GVL through the bridge).
|
|
224
|
+
let result = gvl::region(self.release_gvl, || {
|
|
225
|
+
self.driver.invoke(
|
|
246
226
|
Entry::Eval { source: &source },
|
|
247
227
|
Frames {
|
|
248
228
|
preamble: &preamble,
|
|
@@ -250,32 +230,34 @@ impl Runtime {
|
|
|
250
230
|
},
|
|
251
231
|
handler,
|
|
252
232
|
)
|
|
253
|
-
|
|
254
|
-
|
|
233
|
+
});
|
|
234
|
+
let snapshot = result.map_err(|e| errors::to_magnus(&ruby, e))?;
|
|
235
|
+
Ok(Snapshot::from(snapshot))
|
|
255
236
|
}
|
|
256
237
|
|
|
257
|
-
/// Execute one entrypoint dispatch (`__kobako_run`) and return
|
|
258
|
-
///
|
|
238
|
+
/// Execute one entrypoint dispatch (`__kobako_run`) and return its
|
|
239
|
+
/// `Snapshot`.
|
|
259
240
|
///
|
|
260
241
|
/// The two-frame stdin protocol (preamble + snippets; no user source
|
|
261
242
|
/// frame — docs/wire-codec.md § Invocation channels) plus the
|
|
262
243
|
/// `envelope` copied into guest linear memory; cap semantics match
|
|
263
|
-
/// `#eval`.
|
|
264
|
-
/// engine-vs-host-fault split inside the driver.
|
|
244
|
+
/// `#eval`.
|
|
265
245
|
fn run(
|
|
266
246
|
&self,
|
|
247
|
+
dispatch: Value,
|
|
267
248
|
preamble: RString,
|
|
268
249
|
snippets: RString,
|
|
269
250
|
envelope: RString,
|
|
270
|
-
) -> Result<
|
|
251
|
+
) -> Result<Snapshot, MagnusError> {
|
|
271
252
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
272
|
-
let handler =
|
|
253
|
+
let handler = build_handler(dispatch);
|
|
273
254
|
let preamble = rstring_to_vec(preamble);
|
|
274
255
|
let snippets = rstring_to_vec(snippets);
|
|
275
256
|
let envelope = rstring_to_vec(envelope);
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
257
|
+
// Release the GVL around the guest span iff this Sandbox asks for it;
|
|
258
|
+
// see the note in `#eval`.
|
|
259
|
+
let result = gvl::region(self.release_gvl, || {
|
|
260
|
+
self.driver.invoke(
|
|
279
261
|
Entry::Run {
|
|
280
262
|
envelope: &envelope,
|
|
281
263
|
},
|
|
@@ -285,103 +267,142 @@ impl Runtime {
|
|
|
285
267
|
},
|
|
286
268
|
handler,
|
|
287
269
|
)
|
|
288
|
-
|
|
289
|
-
|
|
270
|
+
});
|
|
271
|
+
let snapshot = result.map_err(|e| errors::to_magnus(&ruby, e))?;
|
|
272
|
+
Ok(Snapshot::from(snapshot))
|
|
290
273
|
}
|
|
291
274
|
|
|
292
|
-
///
|
|
293
|
-
///
|
|
294
|
-
///
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
275
|
+
/// Return the isolation profile the driver built, as a Symbol
|
|
276
|
+
/// (`:hermetic` / `:permissive`) — the declaration the Sandbox
|
|
277
|
+
/// compares against the posture its `profile:` option requested.
|
|
278
|
+
fn profile(&self) -> Symbol {
|
|
279
|
+
let ruby = Ruby::get().expect("Ruby thread");
|
|
280
|
+
match self.driver.profile() {
|
|
281
|
+
Profile::Hermetic => ruby.to_symbol("hermetic"),
|
|
282
|
+
Profile::Permissive => ruby.to_symbol("permissive"),
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/// Build the dispatch handler for one invocation from the per-call `dispatch`
|
|
288
|
+
/// Proc. A `nil` Proc yields no handler. The Proc stays GC-rooted for the
|
|
289
|
+
/// duration of the synchronous `#eval` / `#run` call as a live method
|
|
290
|
+
/// argument on the Ruby stack, so the driver only borrows it (the safety
|
|
291
|
+
/// contract on `kobako_runtime::runtime::Runtime`).
|
|
292
|
+
fn build_handler(dispatch: Value) -> Option<Arc<dyn DispatchHandler>> {
|
|
293
|
+
if dispatch.is_nil() {
|
|
294
|
+
return None;
|
|
295
|
+
}
|
|
296
|
+
Some(
|
|
297
|
+
Arc::new(bridge::RubyDispatchHandler::new(Opaque::from(dispatch)))
|
|
298
|
+
as Arc<dyn DispatchHandler>,
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/// One invocation's result at the Ruby boundary — the whole `Snapshot` the
|
|
303
|
+
/// driver produced, exposed as `Kobako::Runtime::Snapshot`. Usage and the
|
|
304
|
+
/// two output captures are present on every outcome, so the trap path
|
|
305
|
+
/// carries them just like the value path; the completion is read as either
|
|
306
|
+
/// the outcome bytes (`#outcome`) or a trap (`#trapped?` / `#trap_kind` /
|
|
307
|
+
/// `#trap_message`), and the Sandbox layer maps a trap onto its
|
|
308
|
+
/// `Kobako::TrapError` family.
|
|
309
|
+
#[derive(TypedData)]
|
|
310
|
+
#[magnus(class = "Kobako::Runtime::Snapshot", free_immediately, size)]
|
|
311
|
+
struct Snapshot {
|
|
312
|
+
completion: Completion,
|
|
313
|
+
stdout: Capture,
|
|
314
|
+
stderr: Capture,
|
|
315
|
+
usage: Usage,
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
impl DataTypeFunctions for Snapshot {}
|
|
319
|
+
|
|
320
|
+
impl From<RuntimeSnapshot> for Snapshot {
|
|
321
|
+
fn from(snapshot: RuntimeSnapshot) -> Self {
|
|
303
322
|
let RuntimeSnapshot {
|
|
304
323
|
completion,
|
|
305
324
|
stdout,
|
|
306
325
|
stderr,
|
|
307
326
|
usage,
|
|
308
327
|
} = snapshot;
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
328
|
+
Self {
|
|
329
|
+
completion,
|
|
330
|
+
stdout,
|
|
331
|
+
stderr,
|
|
332
|
+
usage,
|
|
314
333
|
}
|
|
315
334
|
}
|
|
335
|
+
}
|
|
316
336
|
|
|
317
|
-
|
|
318
|
-
///
|
|
319
|
-
///
|
|
320
|
-
///
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
self.
|
|
324
|
-
|
|
325
|
-
|
|
337
|
+
impl Snapshot {
|
|
338
|
+
/// The guest's raw outcome bytes on a completed run; empty on a trap,
|
|
339
|
+
/// where `#trapped?` is the authoritative discriminator and the bytes
|
|
340
|
+
/// are never read.
|
|
341
|
+
fn outcome(&self) -> RString {
|
|
342
|
+
let ruby = Ruby::get().expect("Ruby thread");
|
|
343
|
+
match &self.completion {
|
|
344
|
+
Completion::Outcome(bytes) => ruby.str_from_slice(bytes),
|
|
345
|
+
Completion::Trap(_) => ruby.str_from_slice(&[]),
|
|
346
|
+
}
|
|
326
347
|
}
|
|
327
348
|
|
|
328
|
-
///
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
///
|
|
334
|
-
///
|
|
335
|
-
///
|
|
336
|
-
|
|
337
|
-
/// excludes everything that runs after the guest export
|
|
338
|
-
/// returns. `0.0` before the first invocation.
|
|
339
|
-
/// * `memory_peak` (Integer bytes) — the high-water mark of the
|
|
340
|
-
/// per-invocation `memory.grow` delta past the linear-memory
|
|
341
|
-
/// size captured at invocation entry. `0` before the first
|
|
342
|
-
/// invocation.
|
|
343
|
-
///
|
|
344
|
-
/// Reads the `last_usage` Cell `finish_invocation` populated before
|
|
345
|
-
/// the per-invocation Store was discarded.
|
|
346
|
-
fn usage(&self) -> Result<RArray, MagnusError> {
|
|
349
|
+
/// `true` iff the invocation completed via an engine trap.
|
|
350
|
+
fn trapped(&self) -> bool {
|
|
351
|
+
matches!(self.completion, Completion::Trap(_))
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/// The trap's neutral kind as a Symbol (`:timeout` / `:memory_limit` /
|
|
355
|
+
/// `:trap`), or `nil` on a completed run. The Sandbox maps this onto the
|
|
356
|
+
/// named `Kobako::TrapError` subclass.
|
|
357
|
+
fn trap_kind(&self) -> Option<Symbol> {
|
|
347
358
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
359
|
+
match &self.completion {
|
|
360
|
+
Completion::Trap(Trap::Timeout(_)) => Some(ruby.to_symbol("timeout")),
|
|
361
|
+
Completion::Trap(Trap::MemoryLimit(_)) => Some(ruby.to_symbol("memory_limit")),
|
|
362
|
+
Completion::Trap(Trap::Other(_)) => Some(ruby.to_symbol("trap")),
|
|
363
|
+
Completion::Outcome(_) => None,
|
|
364
|
+
}
|
|
353
365
|
}
|
|
354
366
|
|
|
355
|
-
///
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
367
|
+
/// The trap's message, or `nil` on a completed run.
|
|
368
|
+
fn trap_message(&self) -> Option<String> {
|
|
369
|
+
match &self.completion {
|
|
370
|
+
Completion::Trap(Trap::Timeout(msg) | Trap::MemoryLimit(msg) | Trap::Other(msg)) => {
|
|
371
|
+
Some(msg.clone())
|
|
372
|
+
}
|
|
373
|
+
Completion::Outcome(_) => None,
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/// Wall-clock seconds the guest export call spent inside wasmtime.
|
|
378
|
+
fn wall_time(&self) -> f64 {
|
|
379
|
+
self.usage.wall_time
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/// High-water `memory.grow` delta in bytes past the entry-time baseline.
|
|
383
|
+
fn memory_peak(&self) -> usize {
|
|
384
|
+
self.usage.memory_peak
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/// Bytes captured on the guest's stdout channel, clipped to the cap.
|
|
388
|
+
fn stdout(&self) -> RString {
|
|
366
389
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
367
|
-
|
|
368
|
-
let (stdout, stderr) = &*captures;
|
|
369
|
-
let arr = ruby.ary_new_capa(4);
|
|
370
|
-
arr.push(ruby.str_from_slice(&stdout.bytes))?;
|
|
371
|
-
arr.push(stdout.truncated)?;
|
|
372
|
-
arr.push(ruby.str_from_slice(&stderr.bytes))?;
|
|
373
|
-
arr.push(stderr.truncated)?;
|
|
374
|
-
Ok(arr)
|
|
390
|
+
ruby.str_from_slice(&self.stdout.bytes)
|
|
375
391
|
}
|
|
376
392
|
|
|
377
|
-
///
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
393
|
+
/// `true` iff the stdout channel reached its cap during this run.
|
|
394
|
+
fn stdout_truncated(&self) -> bool {
|
|
395
|
+
self.stdout.truncated
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/// Bytes captured on the guest's stderr channel, clipped to the cap.
|
|
399
|
+
fn stderr(&self) -> RString {
|
|
381
400
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
401
|
+
ruby.str_from_slice(&self.stderr.bytes)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/// `true` iff the stderr channel reached its cap during this run.
|
|
405
|
+
fn stderr_truncated(&self) -> bool {
|
|
406
|
+
self.stderr.truncated
|
|
386
407
|
}
|
|
387
408
|
}
|