kobako 0.19.0 → 0.21.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 +127 -0
- data/Cargo.lock +15 -5
- data/README.md +96 -44
- data/ROADMAP.md +1 -1
- data/crates/kobako-runtime/CHANGELOG.md +44 -0
- data/crates/kobako-runtime/Cargo.toml +8 -1
- data/crates/kobako-runtime/README.md +1 -1
- data/crates/kobako-runtime/src/dispatch.rs +17 -11
- data/crates/kobako-runtime/src/error.rs +38 -6
- data/crates/kobako-runtime/src/lib.rs +13 -2
- data/crates/kobako-runtime/src/profile.rs +8 -0
- data/crates/kobako-runtime/src/runtime.rs +7 -2
- data/crates/kobako-runtime/src/snapshot.rs +13 -4
- data/crates/kobako-runtime/src/yielder.rs +4 -4
- data/crates/kobako-transport/CHANGELOG.md +31 -0
- data/crates/kobako-transport/Cargo.toml +27 -0
- data/crates/kobako-transport/README.md +43 -0
- data/crates/kobako-transport/src/abi.rs +89 -0
- data/crates/kobako-transport/src/envelope/bytes.rs +234 -0
- data/crates/kobako-transport/src/envelope/call.rs +222 -0
- data/crates/kobako-transport/src/envelope/error_record.rs +101 -0
- data/crates/kobako-transport/src/envelope/fault.rs +171 -0
- data/crates/kobako-transport/src/envelope/invocation_frames.rs +232 -0
- data/crates/kobako-transport/src/envelope/outcome.rs +261 -0
- data/crates/kobako-transport/src/envelope/reply.rs +240 -0
- data/crates/kobako-transport/src/envelope/run.rs +85 -0
- data/crates/kobako-transport/src/envelope.rs +73 -0
- data/crates/kobako-transport/src/lib.rs +21 -0
- data/crates/kobako-wasmtime/CHANGELOG.md +30 -0
- data/crates/kobako-wasmtime/Cargo.toml +5 -2
- data/crates/kobako-wasmtime/README.md +1 -1
- data/crates/kobako-wasmtime/src/config.rs +19 -12
- data/crates/kobako-wasmtime/src/dispatch.rs +15 -8
- data/crates/kobako-wasmtime/src/driver.rs +8 -27
- data/crates/kobako-wasmtime/src/frames.rs +11 -9
- data/crates/kobako-wasmtime/src/guest_mem.rs +7 -15
- data/crates/kobako-wasmtime/src/instance_pre.rs +2 -2
- data/crates/kobako-wasmtime/src/invocation.rs +1 -1
- data/crates/kobako-wasmtime/src/lib.rs +1 -1
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +7 -2
- data/ext/kobako/src/runtime/bridge.rs +103 -40
- data/ext/kobako/src/runtime/errors.rs +13 -5
- data/ext/kobako/src/runtime/gvl.rs +114 -0
- data/ext/kobako/src/runtime.rs +307 -207
- data/lib/kobako/catalog/extensions.rb +50 -43
- data/lib/kobako/catalog/handles.rb +7 -16
- data/lib/kobako/catalog/services.rb +24 -46
- data/lib/kobako/catalog/snippets.rb +16 -33
- data/lib/kobako/codec/decoder.rb +10 -13
- data/lib/kobako/codec/encoder.rb +7 -7
- data/lib/kobako/codec/error.rb +6 -6
- data/lib/kobako/codec/ext_types.rb +12 -72
- data/lib/kobako/codec/handle_walk.rb +6 -6
- data/lib/kobako/codec/state.rb +4 -56
- data/lib/kobako/codec/utils.rb +4 -4
- data/lib/kobako/codec.rb +11 -21
- data/lib/kobako/context.rb +208 -0
- data/lib/kobako/errors.rb +59 -16
- data/lib/kobako/execution.rb +52 -0
- data/lib/kobako/extension.rb +24 -10
- data/lib/kobako/outcome.rb +71 -128
- data/lib/kobako/payload/arguments.rb +59 -0
- data/lib/kobako/payload.rb +19 -0
- data/lib/kobako/pool.rb +1 -7
- data/lib/kobako/sandbox.rb +51 -191
- data/lib/kobako/sandbox_options.rb +24 -3
- data/lib/kobako/snippet/binary.rb +3 -4
- data/lib/kobako/snippet/source.rb +5 -6
- data/lib/kobako/snippet.rb +2 -2
- data/lib/kobako/transport/call.rb +21 -0
- data/lib/kobako/transport/dispatcher.rb +94 -142
- data/lib/kobako/transport/error.rb +21 -5
- data/lib/kobako/transport/reflection.rb +82 -0
- data/lib/kobako/transport/run.rb +25 -33
- data/lib/kobako/transport/yielder.rb +27 -36
- data/lib/kobako/transport.rb +11 -8
- data/lib/kobako/unresolved.rb +17 -0
- data/lib/kobako/usage.rb +7 -7
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +1 -0
- data/release-please-config.json +47 -2
- data/sig/kobako/catalog/extensions.rbs +4 -6
- data/sig/kobako/catalog/handles.rbs +0 -2
- data/sig/kobako/catalog/services.rbs +4 -4
- data/sig/kobako/catalog/snippets.rbs +4 -2
- data/sig/kobako/codec/error.rbs +4 -4
- data/sig/kobako/codec/ext_types.rbs +0 -7
- data/sig/kobako/codec/state.rbs +0 -4
- data/sig/kobako/codec.rbs +4 -5
- data/sig/kobako/context.rbs +36 -0
- data/sig/kobako/errors.rbs +27 -6
- data/sig/kobako/execution.rbs +24 -0
- data/sig/kobako/extension.rbs +4 -2
- data/sig/kobako/outcome.rbs +12 -13
- data/sig/kobako/payload/arguments.rbs +26 -0
- data/sig/kobako/payload.rbs +4 -0
- data/sig/kobako/pool.rbs +0 -2
- data/sig/kobako/runtime.rbs +44 -14
- data/sig/kobako/sandbox.rbs +5 -24
- data/sig/kobako/sandbox_options.rbs +9 -2
- data/sig/kobako/snippet/binary.rbs +1 -1
- data/sig/kobako/snippet/source.rbs +1 -1
- data/sig/kobako/transport/call.rbs +21 -0
- data/sig/kobako/transport/dispatcher.rbs +9 -11
- data/sig/kobako/transport/error.rbs +11 -0
- data/sig/kobako/transport/reflection.rbs +15 -0
- data/sig/kobako/transport/run.rbs +1 -1
- data/sig/kobako/transport/yielder.rbs +2 -2
- data/sig/kobako/transport.rbs +8 -2
- data/sig/kobako/unresolved.rbs +4 -0
- metadata +30 -11
- data/lib/kobako/fault.rb +0 -38
- data/lib/kobako/outcome/panic.rb +0 -42
- data/lib/kobako/transport/request.rb +0 -76
- data/lib/kobako/transport/response.rb +0 -83
- data/lib/kobako/transport/yield.rb +0 -94
- data/sig/kobako/fault.rbs +0 -19
- data/sig/kobako/outcome/panic.rbs +0 -34
- data/sig/kobako/transport/request.rbs +0 -29
- data/sig/kobako/transport/response.rbs +0 -33
- data/sig/kobako/transport/yield.rbs +0 -34
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,31 +20,36 @@
|
|
|
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
|
+
function, method, prelude::*, typed_data::DataTypeFunctions, value::Opaque,
|
|
31
32
|
Error as MagnusError, RArray, 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};
|
|
44
|
+
use kobako_transport::envelope::{Bindings, Outcome, Run, Snippet, Snippets};
|
|
43
45
|
use kobako_wasmtime::{Config, Driver};
|
|
44
46
|
|
|
47
|
+
/// A Panic's fields as they cross to Ruby: origin, class, message,
|
|
48
|
+
/// backtrace, and the names the invocation could have used in place of the
|
|
49
|
+
/// one it named. Named so the Ruby side reads them positionally against one
|
|
50
|
+
/// shape, matching the `Kobako::Outcome::panic_fields` alias.
|
|
51
|
+
type PanicFields = (String, String, String, Vec<String>, Vec<String>);
|
|
52
|
+
|
|
45
53
|
/// Copy the bytes of `s` into a fresh `Vec<u8>`. Single safe entry to
|
|
46
54
|
/// what would otherwise be an inline `unsafe { rstring.as_slice() }
|
|
47
55
|
/// .to_vec()` duplicated at every host-↔-guest boundary. The borrow
|
|
@@ -53,6 +61,47 @@ fn rstring_to_vec(s: RString) -> Vec<u8> {
|
|
|
53
61
|
unsafe { s.as_slice() }.to_vec()
|
|
54
62
|
}
|
|
55
63
|
|
|
64
|
+
/// Frame the Frame 1 preamble from the Service registry's bind paths.
|
|
65
|
+
/// The core envelope's byte layout lives on this side of the boundary,
|
|
66
|
+
/// so the registry stays a registry and never holds a wire image.
|
|
67
|
+
fn frame_preamble(paths: RArray) -> Result<Vec<u8>, MagnusError> {
|
|
68
|
+
Ok(Bindings {
|
|
69
|
+
paths: paths.to_vec()?,
|
|
70
|
+
}
|
|
71
|
+
.encode())
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// Frame the Frame 3 snippet table from the registry's entries — one
|
|
75
|
+
/// `[kind, name, body]` triple each, `kind` a Symbol naming the form so
|
|
76
|
+
/// the wire's discriminant byte stays here. An off-ladder kind raises
|
|
77
|
+
/// rather than defaulting, the same fail-closed posture `from_path`
|
|
78
|
+
/// takes on its Symbol options.
|
|
79
|
+
fn frame_snippets(ruby: &Ruby, entries: RArray) -> Result<Vec<u8>, MagnusError> {
|
|
80
|
+
let mut frame = Snippets {
|
|
81
|
+
entries: Vec::with_capacity(entries.len()),
|
|
82
|
+
};
|
|
83
|
+
for index in 0..entries.len() as isize {
|
|
84
|
+
let entry: RArray = entries.entry(index)?;
|
|
85
|
+
let kind: Symbol = entry.entry(0)?;
|
|
86
|
+
frame.entries.push(match kind.name()?.as_ref() {
|
|
87
|
+
"source" => Snippet::Source {
|
|
88
|
+
name: entry.entry(1)?,
|
|
89
|
+
body: entry.entry(2)?,
|
|
90
|
+
},
|
|
91
|
+
"bytecode" => Snippet::Bytecode {
|
|
92
|
+
body: rstring_to_vec(entry.entry(2)?),
|
|
93
|
+
},
|
|
94
|
+
other => {
|
|
95
|
+
return Err(MagnusError::new(
|
|
96
|
+
ruby.exception_arg_error(),
|
|
97
|
+
format!("snippet kind must be :source or :bytecode, got :{other}"),
|
|
98
|
+
))
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
Ok(frame.encode())
|
|
103
|
+
}
|
|
104
|
+
|
|
56
105
|
// ---------------------------------------------------------------------------
|
|
57
106
|
// Ruby init
|
|
58
107
|
// ---------------------------------------------------------------------------
|
|
@@ -63,73 +112,49 @@ pub fn init(ruby: &Ruby, kobako: RModule) -> Result<(), MagnusError> {
|
|
|
63
112
|
// in `runtime/errors.rs` — no intermediate hierarchy is registered.
|
|
64
113
|
|
|
65
114
|
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))?;
|
|
115
|
+
runtime.define_singleton_method("from_path", function!(Runtime::from_path, 7))?;
|
|
116
|
+
runtime.define_method("eval", method!(Runtime::eval, 4))?;
|
|
117
|
+
runtime.define_method("run", method!(Runtime::run, 5))?;
|
|
72
118
|
runtime.define_method("profile", method!(Runtime::profile, 0))?;
|
|
73
119
|
// The guest re-enters for a block yield through a frame-scoped
|
|
74
120
|
// `Kobako::Runtime::GuestYielder` the dispatcher hands the Proc, not a
|
|
75
121
|
// method on Runtime.
|
|
76
122
|
bridge::register(runtime)?;
|
|
77
123
|
|
|
124
|
+
// Snapshot — the per-invocation result object each entry point returns.
|
|
125
|
+
let snapshot = runtime.define_class("Snapshot", ruby.class_object())?;
|
|
126
|
+
snapshot.define_method("outcome", method!(Snapshot::outcome, 0))?;
|
|
127
|
+
snapshot.define_method("trapped?", method!(Snapshot::trapped, 0))?;
|
|
128
|
+
snapshot.define_method("trap_kind", method!(Snapshot::trap_kind, 0))?;
|
|
129
|
+
snapshot.define_method("trap_message", method!(Snapshot::trap_message, 0))?;
|
|
130
|
+
snapshot.define_method("wall_time", method!(Snapshot::wall_time, 0))?;
|
|
131
|
+
snapshot.define_method("memory_peak", method!(Snapshot::memory_peak, 0))?;
|
|
132
|
+
snapshot.define_method("stdout", method!(Snapshot::stdout, 0))?;
|
|
133
|
+
snapshot.define_method("stdout_truncated?", method!(Snapshot::stdout_truncated, 0))?;
|
|
134
|
+
snapshot.define_method("stderr", method!(Snapshot::stderr, 0))?;
|
|
135
|
+
snapshot.define_method("stderr_truncated?", method!(Snapshot::stderr_truncated, 0))?;
|
|
136
|
+
|
|
78
137
|
Ok(())
|
|
79
138
|
}
|
|
80
139
|
|
|
81
140
|
#[derive(TypedData)]
|
|
82
|
-
#[magnus(class = "Kobako::Runtime", free_immediately, size
|
|
141
|
+
#[magnus(class = "Kobako::Runtime", free_immediately, size)]
|
|
83
142
|
struct Runtime {
|
|
84
143
|
// The magnus-free wasmtime driver that runs every invocation; the
|
|
85
|
-
// shell only shuttles Ruby values across its boundary.
|
|
144
|
+
// shell only shuttles Ruby values across its boundary. The Runtime
|
|
145
|
+
// holds no per-invocation state — each `#eval` / `#run` takes its
|
|
146
|
+
// dispatch handler as an argument and returns its whole result as a
|
|
147
|
+
// `Snapshot` — so `Driver`'s own `Send + Sync` carries the type with no
|
|
148
|
+
// interior mutability to guard.
|
|
86
149
|
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)>,
|
|
107
|
-
}
|
|
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
|
-
}
|
|
150
|
+
// Whether each invocation releases Ruby's GVL for its guest span
|
|
151
|
+
// (`gvl: :release`) or holds it throughout (`gvl: :hold`). Fixed at
|
|
152
|
+
// construction; a `bool` carries no interior mutability, so the type
|
|
153
|
+
// stays `Send + Sync`.
|
|
154
|
+
release_gvl: bool,
|
|
124
155
|
}
|
|
125
156
|
|
|
126
|
-
|
|
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 {}
|
|
157
|
+
impl DataTypeFunctions for Runtime {}
|
|
133
158
|
|
|
134
159
|
impl Runtime {
|
|
135
160
|
/// Construct a Runtime from a wasm file path, using the process-wide
|
|
@@ -139,20 +164,23 @@ impl Runtime {
|
|
|
139
164
|
///
|
|
140
165
|
/// `timeout_seconds` is the wall-clock cap in seconds
|
|
141
166
|
/// (`None` disables); `memory_limit` is the linear-memory cap in
|
|
142
|
-
/// bytes (`None` disables); `
|
|
167
|
+
/// bytes (`None` disables); `stdout_limit` / `stderr_limit`
|
|
143
168
|
/// are the per-channel output caps (`None`
|
|
144
169
|
/// disables); `profile` is the isolation rung the driver builds
|
|
145
|
-
/// (`:permissive` / `:hermetic`)
|
|
146
|
-
///
|
|
147
|
-
///
|
|
148
|
-
///
|
|
170
|
+
/// (`:permissive` / `:hermetic`); `gvl` is the scheduling mode
|
|
171
|
+
/// (`:hold` / `:release`) deciding whether each invocation releases
|
|
172
|
+
/// the GVL for its guest span. All six are validated by the caller
|
|
173
|
+
/// (`Kobako::Sandbox`); this method only refuses non-finite or
|
|
174
|
+
/// non-positive timeouts, off-ladder profiles, and unrecognized gvl
|
|
175
|
+
/// modes as a defence in depth.
|
|
149
176
|
fn from_path(
|
|
150
177
|
path: String,
|
|
151
178
|
timeout_seconds: Option<f64>,
|
|
152
179
|
memory_limit: Option<usize>,
|
|
153
|
-
|
|
154
|
-
|
|
180
|
+
stdout_limit: Option<usize>,
|
|
181
|
+
stderr_limit: Option<usize>,
|
|
155
182
|
profile: Symbol,
|
|
183
|
+
gvl: Symbol,
|
|
156
184
|
) -> Result<Self, MagnusError> {
|
|
157
185
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
158
186
|
let timeout = match timeout_seconds {
|
|
@@ -183,66 +211,67 @@ impl Runtime {
|
|
|
183
211
|
));
|
|
184
212
|
}
|
|
185
213
|
};
|
|
214
|
+
// Same fail-closed posture as `profile`: an unrecognized mode raises
|
|
215
|
+
// rather than defaulting. `SandboxOptions` is the primary validator;
|
|
216
|
+
// this guards direct `from_path` calls.
|
|
217
|
+
let release_gvl = match gvl.name()?.as_ref() {
|
|
218
|
+
"hold" => false,
|
|
219
|
+
"release" => true,
|
|
220
|
+
other => {
|
|
221
|
+
return Err(MagnusError::new(
|
|
222
|
+
ruby.exception_arg_error(),
|
|
223
|
+
format!("gvl must be :hold or :release, got :{other}"),
|
|
224
|
+
));
|
|
225
|
+
}
|
|
226
|
+
};
|
|
186
227
|
|
|
187
228
|
let driver = Driver::new(
|
|
188
229
|
Path::new(&path),
|
|
189
|
-
memory_limit,
|
|
190
230
|
Config {
|
|
191
231
|
timeout,
|
|
192
|
-
|
|
193
|
-
|
|
232
|
+
memory_limit,
|
|
233
|
+
stdout_limit,
|
|
234
|
+
stderr_limit,
|
|
194
235
|
profile,
|
|
195
236
|
},
|
|
196
237
|
)
|
|
197
238
|
.map_err(|e| errors::setup_to_magnus(&ruby, e))?;
|
|
198
239
|
Ok(Self {
|
|
199
240
|
driver,
|
|
200
|
-
|
|
201
|
-
last_usage: Cell::new(Usage::default()),
|
|
202
|
-
last_captures: RefCell::new((Capture::default(), Capture::default())),
|
|
241
|
+
release_gvl,
|
|
203
242
|
})
|
|
204
243
|
}
|
|
205
244
|
|
|
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
245
|
// -----------------------------------------------------------------
|
|
219
|
-
// Run-path methods. Each
|
|
220
|
-
// `
|
|
221
|
-
//
|
|
222
|
-
//
|
|
246
|
+
// Run-path methods. Each takes the run's dispatch handler as its first
|
|
247
|
+
// argument and returns a `Snapshot` for any completed invocation —
|
|
248
|
+
// success or trap alike. Only a could-not-start fault (a missing export
|
|
249
|
+
// or a fault before the export call) raises a `Kobako::TrapError`
|
|
250
|
+
// directly, since it yields no `Snapshot`.
|
|
223
251
|
// -----------------------------------------------------------------
|
|
224
252
|
|
|
225
|
-
/// One-shot mruby source execution (`#eval`).
|
|
226
|
-
///
|
|
227
|
-
///
|
|
228
|
-
/// to the driver, and
|
|
229
|
-
///
|
|
230
|
-
/// `Kobako::*` exception. The run mechanics — frames, caps, trap
|
|
231
|
-
/// classification — live in `kobako_wasmtime::Driver`.
|
|
253
|
+
/// One-shot mruby source execution (`#eval`). Builds the dispatch
|
|
254
|
+
/// handler from `dispatch` (the per-invocation Proc), frames the two
|
|
255
|
+
/// stdin invocation frames from the registry state (`paths`,
|
|
256
|
+
/// `snippets`), hands them and the source to the driver, and returns
|
|
257
|
+
/// the run's `Snapshot`.
|
|
232
258
|
fn eval(
|
|
233
259
|
&self,
|
|
234
|
-
|
|
260
|
+
dispatch: Value,
|
|
261
|
+
paths: RArray,
|
|
235
262
|
source: RString,
|
|
236
|
-
snippets:
|
|
237
|
-
) -> Result<
|
|
263
|
+
snippets: RArray,
|
|
264
|
+
) -> Result<Snapshot, MagnusError> {
|
|
238
265
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
239
|
-
let handler =
|
|
240
|
-
let preamble =
|
|
266
|
+
let handler = build_handler(dispatch);
|
|
267
|
+
let preamble = frame_preamble(paths)?;
|
|
241
268
|
let source = rstring_to_vec(source);
|
|
242
|
-
let snippets =
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
269
|
+
let snippets = frame_snippets(&ruby, snippets)?;
|
|
270
|
+
// Release the GVL around the guest span iff this Sandbox asks for it;
|
|
271
|
+
// the closure touches no Ruby VALUE (the driver is magnus-free, and a
|
|
272
|
+
// guest→host dispatch re-acquires the GVL through the bridge).
|
|
273
|
+
let result = gvl::region(self.release_gvl, || {
|
|
274
|
+
self.driver.invoke(
|
|
246
275
|
Entry::Eval { source: &source },
|
|
247
276
|
Frames {
|
|
248
277
|
preamble: &preamble,
|
|
@@ -250,32 +279,40 @@ impl Runtime {
|
|
|
250
279
|
},
|
|
251
280
|
handler,
|
|
252
281
|
)
|
|
253
|
-
|
|
254
|
-
|
|
282
|
+
});
|
|
283
|
+
let snapshot = result.map_err(|e| errors::to_magnus(&ruby, e))?;
|
|
284
|
+
Ok(Snapshot::from(snapshot))
|
|
255
285
|
}
|
|
256
286
|
|
|
257
|
-
/// Execute one entrypoint dispatch (`__kobako_run`) and return
|
|
258
|
-
///
|
|
287
|
+
/// Execute one entrypoint dispatch (`__kobako_run`) and return its
|
|
288
|
+
/// `Snapshot`.
|
|
259
289
|
///
|
|
260
290
|
/// The two-frame stdin protocol (preamble + snippets; no user source
|
|
261
|
-
/// frame — docs/wire-codec.md § Invocation channels) plus the
|
|
262
|
-
///
|
|
263
|
-
///
|
|
264
|
-
///
|
|
291
|
+
/// frame — docs/wire-codec.md § Invocation channels) plus the Run
|
|
292
|
+
/// envelope copied into guest linear memory — `entrypoint` routes it,
|
|
293
|
+
/// `payload` feeds it, and this side frames the two together. Cap
|
|
294
|
+
/// semantics match `#eval`.
|
|
265
295
|
fn run(
|
|
266
296
|
&self,
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
297
|
+
dispatch: Value,
|
|
298
|
+
paths: RArray,
|
|
299
|
+
snippets: RArray,
|
|
300
|
+
entrypoint: String,
|
|
301
|
+
payload: RString,
|
|
302
|
+
) -> Result<Snapshot, MagnusError> {
|
|
271
303
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
272
|
-
let handler =
|
|
273
|
-
let preamble =
|
|
274
|
-
let snippets =
|
|
275
|
-
let envelope =
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
304
|
+
let handler = build_handler(dispatch);
|
|
305
|
+
let preamble = frame_preamble(paths)?;
|
|
306
|
+
let snippets = frame_snippets(&ruby, snippets)?;
|
|
307
|
+
let envelope = Run {
|
|
308
|
+
entrypoint,
|
|
309
|
+
payload: rstring_to_vec(payload),
|
|
310
|
+
}
|
|
311
|
+
.encode();
|
|
312
|
+
// Release the GVL around the guest span iff this Sandbox asks for it;
|
|
313
|
+
// see the note in `#eval`.
|
|
314
|
+
let result = gvl::region(self.release_gvl, || {
|
|
315
|
+
self.driver.invoke(
|
|
279
316
|
Entry::Run {
|
|
280
317
|
envelope: &envelope,
|
|
281
318
|
},
|
|
@@ -285,103 +322,166 @@ impl Runtime {
|
|
|
285
322
|
},
|
|
286
323
|
handler,
|
|
287
324
|
)
|
|
288
|
-
|
|
289
|
-
|
|
325
|
+
});
|
|
326
|
+
let snapshot = result.map_err(|e| errors::to_magnus(&ruby, e))?;
|
|
327
|
+
Ok(Snapshot::from(snapshot))
|
|
290
328
|
}
|
|
291
329
|
|
|
292
|
-
///
|
|
293
|
-
///
|
|
294
|
-
///
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
330
|
+
/// Return the isolation profile the driver built, as a Symbol
|
|
331
|
+
/// (`:hermetic` / `:permissive`) — the declaration the Sandbox
|
|
332
|
+
/// compares against the posture its `profile:` option requested.
|
|
333
|
+
fn profile(&self) -> Symbol {
|
|
334
|
+
let ruby = Ruby::get().expect("Ruby thread");
|
|
335
|
+
match self.driver.profile() {
|
|
336
|
+
Profile::Hermetic => ruby.to_symbol("hermetic"),
|
|
337
|
+
Profile::Permissive => ruby.to_symbol("permissive"),
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/// Build the dispatch handler for one invocation from the per-call `dispatch`
|
|
343
|
+
/// Proc. A `nil` Proc yields no handler. The Proc stays GC-rooted for the
|
|
344
|
+
/// duration of the synchronous `#eval` / `#run` call as a live method
|
|
345
|
+
/// argument on the Ruby stack, so the driver only borrows it (the safety
|
|
346
|
+
/// contract on `kobako_runtime::runtime::Runtime`).
|
|
347
|
+
fn build_handler(dispatch: Value) -> Option<Arc<dyn DispatchHandler>> {
|
|
348
|
+
if dispatch.is_nil() {
|
|
349
|
+
return None;
|
|
350
|
+
}
|
|
351
|
+
Some(
|
|
352
|
+
Arc::new(bridge::RubyDispatchHandler::new(Opaque::from(dispatch)))
|
|
353
|
+
as Arc<dyn DispatchHandler>,
|
|
354
|
+
)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/// One invocation's result at the Ruby boundary — the whole `Snapshot` the
|
|
358
|
+
/// driver produced, exposed as `Kobako::Runtime::Snapshot`. Usage and the
|
|
359
|
+
/// two output captures are present on every outcome, so the trap path
|
|
360
|
+
/// carries them just like the value path; the completion is read as either
|
|
361
|
+
/// the outcome bytes (`#outcome`) or a trap (`#trapped?` / `#trap_kind` /
|
|
362
|
+
/// `#trap_message`), and the Sandbox layer maps a trap onto its
|
|
363
|
+
/// `Kobako::TrapError` family.
|
|
364
|
+
#[derive(TypedData)]
|
|
365
|
+
#[magnus(class = "Kobako::Runtime::Snapshot", free_immediately, size)]
|
|
366
|
+
struct Snapshot {
|
|
367
|
+
completion: Completion,
|
|
368
|
+
stdout: Capture,
|
|
369
|
+
stderr: Capture,
|
|
370
|
+
usage: Usage,
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
impl DataTypeFunctions for Snapshot {}
|
|
374
|
+
|
|
375
|
+
impl From<RuntimeSnapshot> for Snapshot {
|
|
376
|
+
fn from(snapshot: RuntimeSnapshot) -> Self {
|
|
303
377
|
let RuntimeSnapshot {
|
|
304
378
|
completion,
|
|
305
379
|
stdout,
|
|
306
380
|
stderr,
|
|
307
381
|
usage,
|
|
308
382
|
} = snapshot;
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
383
|
+
Self {
|
|
384
|
+
completion,
|
|
385
|
+
stdout,
|
|
386
|
+
stderr,
|
|
387
|
+
usage,
|
|
314
388
|
}
|
|
315
389
|
}
|
|
390
|
+
}
|
|
316
391
|
|
|
317
|
-
|
|
318
|
-
///
|
|
319
|
-
///
|
|
320
|
-
///
|
|
321
|
-
/// `
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
392
|
+
impl Snapshot {
|
|
393
|
+
/// One completed run's outcome, already split off the core envelope:
|
|
394
|
+
/// `[kind, payload, panic]`. `kind` names the arm — `:ok`,
|
|
395
|
+
/// `:panic`, `:absent` (nothing written), or `:malformed` (bytes the
|
|
396
|
+
/// envelope cannot frame). `payload` is the invocation's
|
|
397
|
+
/// codec-encoded value, carried only by `:ok`; every other arm
|
|
398
|
+
/// answers empty. `panic` carries the Panic's own fields on `:panic`
|
|
399
|
+
/// and is `nil` otherwise, so the Ruby side maps a failure onto its
|
|
400
|
+
/// error taxonomy without decoding a payload byte.
|
|
401
|
+
///
|
|
402
|
+
/// A trap answers `:absent` — `#trapped?` is the authoritative
|
|
403
|
+
/// discriminator there and this is never read.
|
|
404
|
+
fn outcome(&self) -> (Symbol, RString, Option<PanicFields>) {
|
|
405
|
+
let ruby = Ruby::get().expect("Ruby thread");
|
|
406
|
+
let empty = || ruby.str_from_slice(&[]);
|
|
407
|
+
let Completion::Outcome(bytes) = &self.completion else {
|
|
408
|
+
return (ruby.to_symbol("absent"), empty(), None);
|
|
409
|
+
};
|
|
410
|
+
match Outcome::decode(bytes) {
|
|
411
|
+
Ok(Outcome::Ok(value)) => (ruby.to_symbol("ok"), ruby.str_from_slice(&value), None),
|
|
412
|
+
Ok(Outcome::Panic(panic)) => (
|
|
413
|
+
ruby.to_symbol("panic"),
|
|
414
|
+
empty(),
|
|
415
|
+
Some((
|
|
416
|
+
panic.origin.name().to_owned(),
|
|
417
|
+
panic.error.name,
|
|
418
|
+
panic.error.message,
|
|
419
|
+
panic.error.backtrace,
|
|
420
|
+
panic.available,
|
|
421
|
+
)),
|
|
422
|
+
),
|
|
423
|
+
Err(_) if bytes.is_empty() => (ruby.to_symbol("absent"), empty(), None),
|
|
424
|
+
Err(_) => (ruby.to_symbol("malformed"), empty(), None),
|
|
425
|
+
}
|
|
326
426
|
}
|
|
327
427
|
|
|
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> {
|
|
428
|
+
/// `true` iff the invocation completed via an engine trap.
|
|
429
|
+
fn trapped(&self) -> bool {
|
|
430
|
+
matches!(self.completion, Completion::Trap(_))
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/// The trap's neutral kind as a Symbol (`:timeout` / `:memory_limit` /
|
|
434
|
+
/// `:trap`), or `nil` on a completed run. The Sandbox maps this onto the
|
|
435
|
+
/// named `Kobako::TrapError` subclass.
|
|
436
|
+
fn trap_kind(&self) -> Option<Symbol> {
|
|
347
437
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
438
|
+
match &self.completion {
|
|
439
|
+
Completion::Trap(Trap::Timeout(_)) => Some(ruby.to_symbol("timeout")),
|
|
440
|
+
Completion::Trap(Trap::MemoryLimit(_)) => Some(ruby.to_symbol("memory_limit")),
|
|
441
|
+
// A cap this frontend has no named subclass for is still a
|
|
442
|
+
// trap; the base class is what the taxonomy owes it.
|
|
443
|
+
Completion::Trap(_) => Some(ruby.to_symbol("trap")),
|
|
444
|
+
Completion::Outcome(_) => None,
|
|
445
|
+
}
|
|
353
446
|
}
|
|
354
447
|
|
|
355
|
-
///
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
///
|
|
364
|
-
|
|
365
|
-
|
|
448
|
+
/// The trap's message, or `nil` on a completed run.
|
|
449
|
+
fn trap_message(&self) -> Option<String> {
|
|
450
|
+
match &self.completion {
|
|
451
|
+
Completion::Trap(trap) => Some(trap.to_string()),
|
|
452
|
+
Completion::Outcome(_) => None,
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/// Wall-clock seconds the guest export call spent inside wasmtime.
|
|
457
|
+
fn wall_time(&self) -> f64 {
|
|
458
|
+
self.usage.wall_time
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/// High-water `memory.grow` delta in bytes past the entry-time baseline.
|
|
462
|
+
fn memory_peak(&self) -> usize {
|
|
463
|
+
self.usage.memory_peak
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/// Bytes captured on the guest's stdout channel, clipped to the cap.
|
|
467
|
+
fn stdout(&self) -> RString {
|
|
366
468
|
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)
|
|
469
|
+
ruby.str_from_slice(&self.stdout.bytes)
|
|
375
470
|
}
|
|
376
471
|
|
|
377
|
-
///
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
472
|
+
/// `true` iff the stdout channel reached its cap during this run.
|
|
473
|
+
fn stdout_truncated(&self) -> bool {
|
|
474
|
+
self.stdout.truncated
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/// Bytes captured on the guest's stderr channel, clipped to the cap.
|
|
478
|
+
fn stderr(&self) -> RString {
|
|
381
479
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
480
|
+
ruby.str_from_slice(&self.stderr.bytes)
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/// `true` iff the stderr channel reached its cap during this run.
|
|
484
|
+
fn stderr_truncated(&self) -> bool {
|
|
485
|
+
self.stderr.truncated
|
|
386
486
|
}
|
|
387
487
|
}
|