microsandbox-rb 0.17.2 → 0.17.3
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/CHANGELOG.md +45 -1
- data/Cargo.lock +1 -1
- data/DESIGN.md +98 -0
- data/README.md +3 -2
- data/docs/errors.md +1 -0
- data/docs/runtime.md +85 -0
- data/docs/surface.md +3 -0
- data/ext/microsandbox/Cargo.toml +1 -1
- data/ext/microsandbox/src/agent.rs +13 -10
- data/ext/microsandbox/src/backend.rs +290 -33
- data/ext/microsandbox/src/error.rs +18 -0
- data/ext/microsandbox/src/exec.rs +32 -18
- data/ext/microsandbox/src/fs_stream.rs +11 -8
- data/ext/microsandbox/src/host_path.rs +4 -3
- data/ext/microsandbox/src/lib.rs +1 -0
- data/ext/microsandbox/src/runtime.rs +127 -36
- data/ext/microsandbox/src/sandbox.rs +175 -91
- data/ext/microsandbox/src/snapshot.rs +7 -4
- data/ext/microsandbox/src/ssh.rs +26 -23
- data/ext/microsandbox/src/stream.rs +13 -7
- data/ext/microsandbox/src/volume.rs +22 -16
- data/lib/microsandbox/errors.rb +8 -0
- data/lib/microsandbox/version.rb +1 -1
- data/lib/microsandbox.rb +20 -1
- data/sig/microsandbox.rbs +1 -0
- metadata +1 -1
|
@@ -17,13 +17,13 @@
|
|
|
17
17
|
|
|
18
18
|
use std::collections::HashMap;
|
|
19
19
|
use std::sync::atomic::{AtomicU32, Ordering};
|
|
20
|
-
use std::sync::{Arc, Mutex, OnceLock};
|
|
20
|
+
use std::sync::{Arc, Mutex, MutexGuard, OnceLock, PoisonError};
|
|
21
21
|
|
|
22
22
|
use magnus::{function, prelude::*, Error, RModule, Ruby};
|
|
23
23
|
use microsandbox::{Backend, MicrosandboxError, Operation};
|
|
24
24
|
|
|
25
25
|
use crate::error;
|
|
26
|
-
use crate::runtime::block_on;
|
|
26
|
+
use crate::runtime::{block_on, current_pid};
|
|
27
27
|
|
|
28
28
|
/// Resolve the ambient default backend, requiring it to be local.
|
|
29
29
|
///
|
|
@@ -31,7 +31,8 @@ use crate::runtime::block_on;
|
|
|
31
31
|
/// borrowing `&LocalBackend` from it (`as_local()` borrows the `Arc`). Pure
|
|
32
32
|
/// Rust — safe to call inside `block_on` (no Ruby C API), and it returns a raw
|
|
33
33
|
/// `MicrosandboxError` so the Ruby-exception mapping happens *after* `block_on`
|
|
34
|
-
/// re-acquires the GVL.
|
|
34
|
+
/// re-acquires the GVL. It does not run [`ensure_this_process_backend`] (that
|
|
35
|
+
/// needs the GVL): the `block_on` it executes under already did. Cloud backends yield `Unsupported` for the given
|
|
35
36
|
/// operation (v0.6.8 `Operation`-keyed shape), mirroring pyo3's `resolve_local`.
|
|
36
37
|
pub fn local_backend(op: Operation) -> Result<Arc<dyn Backend>, MicrosandboxError> {
|
|
37
38
|
let backend = microsandbox::default_backend();
|
|
@@ -65,14 +66,16 @@ pub fn with_local_backend<T>(
|
|
|
65
66
|
|
|
66
67
|
/// Build an `Arc<dyn Backend>` from the SDK facade. Ported from pyo3
|
|
67
68
|
/// `build_backend` (`sdk/python/src/lib.rs`) / node `build_backend`. Synchronous
|
|
68
|
-
/// (no network I/O): cloud construction only builds the HTTP client.
|
|
69
|
-
///
|
|
69
|
+
/// (no network I/O): cloud construction only builds the HTTP client. Pure Rust
|
|
70
|
+
/// — it is also called with one of the mutexes below held, where calling into
|
|
71
|
+
/// Ruby (to build an exception) is not allowed; callers map the error
|
|
72
|
+
/// afterwards.
|
|
70
73
|
fn build_backend(
|
|
71
|
-
kind:
|
|
74
|
+
kind: &str,
|
|
72
75
|
url: Option<String>,
|
|
73
76
|
api_key: Option<String>,
|
|
74
77
|
profile: Option<String>,
|
|
75
|
-
) -> Result<Arc<dyn Backend>,
|
|
78
|
+
) -> Result<Arc<dyn Backend>, MicrosandboxError> {
|
|
76
79
|
match kind.trim().to_ascii_lowercase().as_str() {
|
|
77
80
|
"local" => Ok(Arc::new(microsandbox::LocalBackend::lazy())),
|
|
78
81
|
"cloud" => {
|
|
@@ -81,21 +84,166 @@ fn build_backend(
|
|
|
81
84
|
None => match (url, api_key) {
|
|
82
85
|
(Some(url), Some(api_key)) => microsandbox::CloudBackend::new(url, api_key),
|
|
83
86
|
_ => {
|
|
84
|
-
return Err(
|
|
87
|
+
return Err(MicrosandboxError::InvalidConfig(
|
|
85
88
|
"cloud backend requires url + api_key or profile".into(),
|
|
86
|
-
))
|
|
89
|
+
))
|
|
87
90
|
}
|
|
88
91
|
},
|
|
89
|
-
}
|
|
90
|
-
.map_err(error::to_ruby)?;
|
|
92
|
+
}?;
|
|
91
93
|
Ok(Arc::new(cloud))
|
|
92
94
|
}
|
|
93
|
-
other => Err(
|
|
95
|
+
other => Err(MicrosandboxError::InvalidConfig(format!(
|
|
94
96
|
"backend kind must be 'local' or 'cloud', got {other:?}"
|
|
95
|
-
)))
|
|
97
|
+
))),
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
//--------------------------------------------------------------------------------------------------
|
|
102
|
+
// Fork safety: the backend generation
|
|
103
|
+
//--------------------------------------------------------------------------------------------------
|
|
104
|
+
//
|
|
105
|
+
// `fork(2)` copies only the calling thread. A child of a process that used the
|
|
106
|
+
// SDK inherits the core's process-wide default backend — a `LocalBackend`
|
|
107
|
+
// whose SQLite connections are each served by a worker thread that does not
|
|
108
|
+
// exist in the child, or a `CloudBackend` whose pooled connections belong to
|
|
109
|
+
// the parent's dead tokio runtime. Using it hangs (Linux: the pool's 30 s
|
|
110
|
+
// acquire timeout) or kills the process (macOS: waking the missing worker
|
|
111
|
+
// thread traps in libdispatch), and so does DROPPING it, because the
|
|
112
|
+
// destructor wakes the same threads.
|
|
113
|
+
//
|
|
114
|
+
// So the child never touches what it inherited: the first native call after a
|
|
115
|
+
// fork builds a fresh backend the same way the inherited one was chosen and
|
|
116
|
+
// swaps it in, and every inherited `Arc` is `mem::forget`-ed — leaked once per
|
|
117
|
+
// fork generation — instead of dropped.
|
|
118
|
+
//
|
|
119
|
+
// Locking. The three mutexes below are only ever taken on a Ruby thread that
|
|
120
|
+
// holds the GVL, and nothing that runs while one is held calls into Ruby or
|
|
121
|
+
// releases the GVL (which is why the code under them is pure Rust and Ruby
|
|
122
|
+
// exceptions are built after they are released). Ruby-level `fork` needs the
|
|
123
|
+
// GVL too, so no fork can happen while one of them is held: a child never
|
|
124
|
+
// inherits them locked.
|
|
125
|
+
|
|
126
|
+
/// How the current default backend was chosen, so a forked child can choose
|
|
127
|
+
/// it again. Deliberately not `Debug`: `Explicit` carries the cloud API key
|
|
128
|
+
/// (which the backend `Arc` in this process holds anyway — recording it here
|
|
129
|
+
/// adds no new exposure, as long as it is never printed).
|
|
130
|
+
#[derive(Clone)]
|
|
131
|
+
enum Selection {
|
|
132
|
+
/// The core's own resolution ladder (`MSB_BACKEND`, `MSB_PROFILE`, the
|
|
133
|
+
/// config file, else local): what `default_backend()` does on first use.
|
|
134
|
+
Ladder,
|
|
135
|
+
/// A `set_default_backend` / `with_backend` selection.
|
|
136
|
+
Explicit {
|
|
137
|
+
kind: String,
|
|
138
|
+
url: Option<String>,
|
|
139
|
+
api_key: Option<String>,
|
|
140
|
+
profile: Option<String>,
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
impl Selection {
|
|
145
|
+
/// A fresh backend for this selection, owned by the calling process.
|
|
146
|
+
fn build(&self) -> Result<Arc<dyn Backend>, MicrosandboxError> {
|
|
147
|
+
match self {
|
|
148
|
+
Selection::Ladder => microsandbox::resolve_default_backend(),
|
|
149
|
+
Selection::Explicit {
|
|
150
|
+
kind,
|
|
151
|
+
url,
|
|
152
|
+
api_key,
|
|
153
|
+
profile,
|
|
154
|
+
} => build_backend(kind, url.clone(), api_key.clone(), profile.clone()),
|
|
155
|
+
}
|
|
96
156
|
}
|
|
97
157
|
}
|
|
98
158
|
|
|
159
|
+
static SELECTION: Mutex<Selection> = Mutex::new(Selection::Ladder);
|
|
160
|
+
|
|
161
|
+
/// The pid of the process whose default backend the core currently holds.
|
|
162
|
+
/// 0 = no native operation has touched the backend in this process lineage.
|
|
163
|
+
static BACKEND_PID: AtomicU32 = AtomicU32::new(0);
|
|
164
|
+
|
|
165
|
+
static REBUILD_LOCK: Mutex<()> = Mutex::new(());
|
|
166
|
+
|
|
167
|
+
/// Lock one of this module's mutexes. A poisoned one is recovered: the data
|
|
168
|
+
/// behind them stays consistent across a panic (plain assignments), and
|
|
169
|
+
/// refusing to go on would only turn one failure into a permanent one.
|
|
170
|
+
fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
|
|
171
|
+
mutex.lock().unwrap_or_else(PoisonError::into_inner)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/// Make sure the core's default backend belongs to THIS process, rebuilding it
|
|
175
|
+
/// after a fork. Every Ruby-facing entry that can reach the default backend
|
|
176
|
+
/// runs this first: [`crate::runtime::block_on`] for the async operations, and
|
|
177
|
+
/// each synchronous entry that reads the backend itself. (The three that are
|
|
178
|
+
/// about to replace the default anyway — `set_default_backend`,
|
|
179
|
+
/// `push_default_backend` and `pop_default_backend` — adopt with the
|
|
180
|
+
/// selection they install instead.)
|
|
181
|
+
///
|
|
182
|
+
/// Runs only with the GVL held — never inside a future (see the locking note
|
|
183
|
+
/// above).
|
|
184
|
+
pub fn ensure_this_process_backend() -> Result<(), Error> {
|
|
185
|
+
// The Ruby exception is built here, after the locks are released.
|
|
186
|
+
adopt_backend(current_pid(), None)
|
|
187
|
+
.map(|_| ())
|
|
188
|
+
.map_err(error::to_ruby)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/// Take the backend state over for process `cur` if it still belongs to a
|
|
192
|
+
/// parent process: install a backend built in this process and leak every
|
|
193
|
+
/// inherited one.
|
|
194
|
+
///
|
|
195
|
+
/// What gets installed is `replacement` when the caller itself determines the
|
|
196
|
+
/// backend (a selection, or the restore of a scope), and otherwise a rebuild
|
|
197
|
+
/// of the inherited selection. The difference
|
|
198
|
+
/// matters when the inherited selection no longer resolves in the child (say
|
|
199
|
+
/// the ladder now reads `MSB_BACKEND=cloud` without credentials): the rebuild
|
|
200
|
+
/// fails, and keeps failing on every call, but an explicit selection must
|
|
201
|
+
/// still get the child out of that state — so it never depends on the
|
|
202
|
+
/// inherited one.
|
|
203
|
+
///
|
|
204
|
+
/// Returns `Some(inherited selection)` when `replacement` was installed here
|
|
205
|
+
/// (the caller has nothing left to install), `None` when the caller proceeds
|
|
206
|
+
/// as in any process that already owns its backend. On an error nothing has
|
|
207
|
+
/// changed, `BACKEND_PID` included, so the next call tries again.
|
|
208
|
+
fn adopt_backend(
|
|
209
|
+
cur: u32,
|
|
210
|
+
replacement: Option<&Selection>,
|
|
211
|
+
) -> Result<Option<Selection>, MicrosandboxError> {
|
|
212
|
+
if BACKEND_PID.load(Ordering::Acquire) == cur {
|
|
213
|
+
return Ok(None);
|
|
214
|
+
}
|
|
215
|
+
let _rebuild = lock(&REBUILD_LOCK);
|
|
216
|
+
let seen = BACKEND_PID.load(Ordering::Acquire);
|
|
217
|
+
if seen == cur {
|
|
218
|
+
return Ok(None);
|
|
219
|
+
}
|
|
220
|
+
if seen == 0 {
|
|
221
|
+
// Nothing was inherited; the core resolves its default lazily, in this
|
|
222
|
+
// process, on first use.
|
|
223
|
+
BACKEND_PID.store(cur, Ordering::Release);
|
|
224
|
+
return Ok(None);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// A process that used the backend forked us. Build ours first, so that a
|
|
228
|
+
// failure leaves everything as it was.
|
|
229
|
+
let fresh = match replacement {
|
|
230
|
+
Some(selection) => selection.build()?,
|
|
231
|
+
None => lock(&SELECTION).build()?,
|
|
232
|
+
};
|
|
233
|
+
// The inherited default must never be dropped (see above).
|
|
234
|
+
std::mem::forget(microsandbox::swap_default_backend(fresh));
|
|
235
|
+
// Neither may the backends that open `with_backend` scopes saved for their
|
|
236
|
+
// restore. The entries themselves stay — pid and selection — so a scope
|
|
237
|
+
// the parent opened can still be closed here (see `pop_default_backend`).
|
|
238
|
+
for scope in lock(backend_scopes()).values_mut() {
|
|
239
|
+
std::mem::forget(scope.previous.take());
|
|
240
|
+
}
|
|
241
|
+
let inherited =
|
|
242
|
+
replacement.map(|selection| std::mem::replace(&mut *lock(&SELECTION), selection.clone()));
|
|
243
|
+
BACKEND_PID.store(cur, Ordering::Release);
|
|
244
|
+
Ok(inherited)
|
|
245
|
+
}
|
|
246
|
+
|
|
99
247
|
/// Install a process-wide default backend. Synchronous (an `RwLock` write) — no
|
|
100
248
|
/// `block_on`. Mirrors Python `set_default_backend` / Node `setDefaultBackend`.
|
|
101
249
|
fn set_default_backend(
|
|
@@ -104,7 +252,23 @@ fn set_default_backend(
|
|
|
104
252
|
api_key: Option<String>,
|
|
105
253
|
profile: Option<String>,
|
|
106
254
|
) -> Result<(), Error> {
|
|
107
|
-
|
|
255
|
+
let selection = Selection::Explicit {
|
|
256
|
+
kind,
|
|
257
|
+
url,
|
|
258
|
+
api_key,
|
|
259
|
+
profile,
|
|
260
|
+
};
|
|
261
|
+
// A forked child that starts by selecting a backend: the inherited default
|
|
262
|
+
// is forgotten and this selection installed in its place, without ever
|
|
263
|
+
// rebuilding the inherited one.
|
|
264
|
+
if adopt_backend(current_pid(), Some(&selection))
|
|
265
|
+
.map_err(error::to_ruby)?
|
|
266
|
+
.is_some()
|
|
267
|
+
{
|
|
268
|
+
return Ok(());
|
|
269
|
+
}
|
|
270
|
+
microsandbox::set_default_backend(selection.build().map_err(error::to_ruby)?);
|
|
271
|
+
*lock(&SELECTION) = selection;
|
|
108
272
|
Ok(())
|
|
109
273
|
}
|
|
110
274
|
|
|
@@ -114,9 +278,24 @@ fn set_default_backend(
|
|
|
114
278
|
// swaps the default and stores the previous backend under a fresh token; pop
|
|
115
279
|
// restores it. The Ruby wrapper drives this through an `ensure` block.
|
|
116
280
|
static NEXT_BACKEND_SCOPE: AtomicU32 = AtomicU32::new(1);
|
|
117
|
-
static BACKEND_SCOPES: OnceLock<Mutex<HashMap<u32,
|
|
281
|
+
static BACKEND_SCOPES: OnceLock<Mutex<HashMap<u32, Scope>>> = OnceLock::new();
|
|
282
|
+
|
|
283
|
+
/// What an open `with_backend` scope restores when it closes.
|
|
284
|
+
struct Scope {
|
|
285
|
+
/// The process that opened the scope.
|
|
286
|
+
pid: u32,
|
|
287
|
+
/// The backend that was the default before the push, kept to be put back
|
|
288
|
+
/// by a pop in the same process. `None` when there is no such backend of
|
|
289
|
+
/// this process's own: a forked child took it out to leak it, or the push
|
|
290
|
+
/// was the child's first use of the backend and replaced an inherited
|
|
291
|
+
/// default ([`adopt_backend`]). The pop then rebuilds from `selection`.
|
|
292
|
+
previous: Option<Arc<dyn Backend>>,
|
|
293
|
+
/// How that previous backend had been chosen: what a rebuild starts from,
|
|
294
|
+
/// and what `SELECTION` goes back to when the scope closes.
|
|
295
|
+
selection: Selection,
|
|
296
|
+
}
|
|
118
297
|
|
|
119
|
-
fn backend_scopes() -> &'static Mutex<HashMap<u32,
|
|
298
|
+
fn backend_scopes() -> &'static Mutex<HashMap<u32, Scope>> {
|
|
120
299
|
BACKEND_SCOPES.get_or_init(|| Mutex::new(HashMap::new()))
|
|
121
300
|
}
|
|
122
301
|
|
|
@@ -129,34 +308,111 @@ fn push_default_backend(
|
|
|
129
308
|
api_key: Option<String>,
|
|
130
309
|
profile: Option<String>,
|
|
131
310
|
) -> Result<u32, Error> {
|
|
132
|
-
let
|
|
311
|
+
let cur = current_pid();
|
|
312
|
+
let selection = Selection::Explicit {
|
|
313
|
+
kind,
|
|
314
|
+
url,
|
|
315
|
+
api_key,
|
|
316
|
+
profile,
|
|
317
|
+
};
|
|
318
|
+
let (previous, before) = match adopt_backend(cur, Some(&selection)).map_err(error::to_ruby)? {
|
|
319
|
+
// A forked child's first use of the backend: the scoped backend
|
|
320
|
+
// replaced the inherited default (now forgotten), so there is nothing
|
|
321
|
+
// of this process to put back. The pop rebuilds from how the inherited
|
|
322
|
+
// default had been chosen.
|
|
323
|
+
Some(inherited) => (None, inherited),
|
|
324
|
+
None => {
|
|
325
|
+
let previous =
|
|
326
|
+
microsandbox::swap_default_backend(selection.build().map_err(error::to_ruby)?);
|
|
327
|
+
let before = std::mem::replace(&mut *lock(&SELECTION), selection);
|
|
328
|
+
(Some(previous), before)
|
|
329
|
+
}
|
|
330
|
+
};
|
|
133
331
|
let token = NEXT_BACKEND_SCOPE.fetch_add(1, Ordering::Relaxed);
|
|
134
|
-
backend_scopes()
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
332
|
+
lock(backend_scopes()).insert(
|
|
333
|
+
token,
|
|
334
|
+
Scope {
|
|
335
|
+
pid: cur,
|
|
336
|
+
previous,
|
|
337
|
+
selection: before,
|
|
338
|
+
},
|
|
339
|
+
);
|
|
138
340
|
Ok(token)
|
|
139
341
|
}
|
|
140
342
|
|
|
141
|
-
///
|
|
343
|
+
/// Close the scope opened by [`push_default_backend`]: restore the backend it
|
|
344
|
+
/// saved.
|
|
345
|
+
///
|
|
346
|
+
/// A pop consumes its token whether or not it succeeds. It can fail only when
|
|
347
|
+
/// the backend to restore has to be rebuilt from how it had been chosen (the
|
|
348
|
+
/// saved one was the parent's) and that selection does not resolve in this
|
|
349
|
+
/// process. The scope is closed anyway and its temporary selection stays in
|
|
350
|
+
/// effect: the temporary backend remains installed, or — in a child that has
|
|
351
|
+
/// not yet built a backend of its own — is what the next call rebuilds. That
|
|
352
|
+
/// is a state the caller can describe and leave with `set_default_backend`;
|
|
353
|
+
/// a token nobody can retry (`with_backend` never hands it out) would not be.
|
|
142
354
|
fn pop_default_backend(token: u32) -> Result<(), Error> {
|
|
143
|
-
let
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
355
|
+
let cur = current_pid();
|
|
356
|
+
// (The registry lock is not held while an exception is built.)
|
|
357
|
+
let Some(scope) = lock(backend_scopes()).remove(&token) else {
|
|
358
|
+
return Err(error::base_error("unknown backend scope token"));
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
// A forked child whose first use of the backend is this pop (the parent
|
|
362
|
+
// forked inside a `with_backend` block and the child now leaves it) takes
|
|
363
|
+
// over with what the scope restores — directly, never by way of the
|
|
364
|
+
// scope's temporary backend, which the child may be unable to rebuild (a
|
|
365
|
+
// cloud profile that is gone there) and has no use for.
|
|
366
|
+
match adopt_backend(cur, Some(&scope.selection)) {
|
|
367
|
+
// Done: the restored selection is installed and recorded. The backend
|
|
368
|
+
// this scope had saved is the parent's; it left the registry above, so
|
|
369
|
+
// it is leaked here rather than by the adoption.
|
|
370
|
+
Ok(Some(_temporary)) => {
|
|
371
|
+
std::mem::forget(scope.previous);
|
|
372
|
+
return Ok(());
|
|
373
|
+
}
|
|
374
|
+
// This process already owns its backend: restore below.
|
|
375
|
+
Ok(None) => {}
|
|
376
|
+
// Nothing was changed (see `adopt_backend`), so everything in sight is
|
|
377
|
+
// still the parent's: leak what the scope saved, and report. The next
|
|
378
|
+
// call tries to take over with the temporary selection.
|
|
379
|
+
Err(e) => {
|
|
380
|
+
std::mem::forget(scope.previous);
|
|
381
|
+
return Err(error::to_ruby(e));
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
match scope.previous {
|
|
386
|
+
// Opened and closed in the same process: put the saved backend back.
|
|
387
|
+
Some(previous) if scope.pid == cur => microsandbox::set_default_backend(previous),
|
|
388
|
+
// No backend of this process's own was saved (see `Scope::previous`):
|
|
389
|
+
// restore one built here from how the saved one had been chosen. On
|
|
390
|
+
// failure the temporary backend stays installed (see above). The
|
|
391
|
+
// default being replaced was built in this process and could be
|
|
392
|
+
// dropped safely; it is forgotten all the same, so that no path of a
|
|
393
|
+
// forked child ever drops a backend it might have inherited. That
|
|
394
|
+
// leaks at most one backend per scope closed this way.
|
|
395
|
+
previous => {
|
|
396
|
+
// `None` unless the pid of the process that opened the scope was
|
|
397
|
+
// reused; an inherited `Arc` is leaked either way.
|
|
398
|
+
std::mem::forget(previous);
|
|
399
|
+
let fresh = scope.selection.build().map_err(error::to_ruby)?;
|
|
400
|
+
std::mem::forget(microsandbox::swap_default_backend(fresh));
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
*lock(&SELECTION) = scope.selection;
|
|
149
404
|
Ok(())
|
|
150
405
|
}
|
|
151
406
|
|
|
152
407
|
/// The active default backend kind: `"local"` or `"cloud"`. First call lazily
|
|
153
408
|
/// resolves the env/profile/config ladder. Synchronous (an `RwLock` read).
|
|
154
|
-
fn default_backend_kind() -> String {
|
|
155
|
-
|
|
409
|
+
fn default_backend_kind() -> Result<String, Error> {
|
|
410
|
+
ensure_this_process_backend()?;
|
|
411
|
+
Ok(match microsandbox::default_backend().kind() {
|
|
156
412
|
microsandbox::BackendKind::Local => "local",
|
|
157
413
|
microsandbox::BackendKind::Cloud => "cloud",
|
|
158
414
|
}
|
|
159
|
-
.to_string()
|
|
415
|
+
.to_string())
|
|
160
416
|
}
|
|
161
417
|
|
|
162
418
|
/// Secret-safe description of the active default backend (v0.6.9):
|
|
@@ -164,7 +420,8 @@ fn default_backend_kind() -> String {
|
|
|
164
420
|
/// (e.g. "MSB_BACKEND", "programmatic", "default"); the API key is never
|
|
165
421
|
/// included. Like `default_backend_kind`, the first call freezes ambient
|
|
166
422
|
/// env/profile resolution for the process.
|
|
167
|
-
fn default_backend_info() -> magnus::RHash {
|
|
423
|
+
fn default_backend_info() -> Result<magnus::RHash, Error> {
|
|
424
|
+
ensure_this_process_backend()?;
|
|
168
425
|
let info = microsandbox::default_backend_info();
|
|
169
426
|
let hash = crate::runtime::ruby().hash_new();
|
|
170
427
|
let _ = hash.aset(
|
|
@@ -177,7 +434,7 @@ fn default_backend_info() -> magnus::RHash {
|
|
|
177
434
|
let _ = hash.aset("api_url", info.api_url);
|
|
178
435
|
let _ = hash.aset("source", info.source.as_str());
|
|
179
436
|
let _ = hash.aset("profile", info.profile);
|
|
180
|
-
hash
|
|
437
|
+
Ok(hash)
|
|
181
438
|
}
|
|
182
439
|
|
|
183
440
|
pub fn define(_ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
@@ -202,3 +202,21 @@ pub fn base_error(message: impl Into<String>) -> Error {
|
|
|
202
202
|
Err(_) => Error::new(magnus::exception::runtime_error(), message),
|
|
203
203
|
}
|
|
204
204
|
}
|
|
205
|
+
|
|
206
|
+
/// `Microsandbox::ForkedHandleError`: a handle created in process `born` was
|
|
207
|
+
/// used in process `cur`, a fork descendant (see `runtime::ForkGuarded`).
|
|
208
|
+
/// `class` is the Ruby class users know the handle by.
|
|
209
|
+
#[allow(deprecated)]
|
|
210
|
+
pub fn forked_handle(class: &str, born: u32, cur: u32) -> Error {
|
|
211
|
+
let message = format!(
|
|
212
|
+
"{class} was created in process {born} and cannot be used in forked process {cur}; \
|
|
213
|
+
create a new handle after fork (the original keeps working in process {born})"
|
|
214
|
+
);
|
|
215
|
+
match Ruby::get() {
|
|
216
|
+
Ok(ruby) => match exception_class(&ruby, "ForkedHandleError") {
|
|
217
|
+
Some(class) => Error::new(class, message),
|
|
218
|
+
None => Error::new(ruby.exception_runtime_error(), message),
|
|
219
|
+
},
|
|
220
|
+
Err(_) => Error::new(magnus::exception::runtime_error(), message),
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -12,14 +12,20 @@ use magnus::{method, prelude::*, Error, RHash, RModule, RString, Ruby};
|
|
|
12
12
|
use tokio::sync::Mutex;
|
|
13
13
|
|
|
14
14
|
use crate::error;
|
|
15
|
-
use crate::runtime::{block_on, ruby};
|
|
15
|
+
use crate::runtime::{block_on, ruby, ForkGuarded};
|
|
16
16
|
|
|
17
17
|
#[magnus::wrap(class = "Microsandbox::Native::ExecHandle", free_immediately, size)]
|
|
18
18
|
pub struct ExecHandle {
|
|
19
|
-
inner:
|
|
19
|
+
inner: ForkGuarded<ExecParts>,
|
|
20
|
+
/// Plain data, readable in any process.
|
|
21
|
+
id: String,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// Everything of an exec session that is bound to the creating process.
|
|
25
|
+
struct ExecParts {
|
|
26
|
+
handle: Arc<Mutex<microsandbox::ExecHandle>>,
|
|
20
27
|
control: microsandbox::ExecControl,
|
|
21
28
|
stdin: std::sync::Mutex<Option<Arc<microsandbox::sandbox::exec::ExecSink>>>,
|
|
22
|
-
id: String,
|
|
23
29
|
}
|
|
24
30
|
|
|
25
31
|
impl ExecHandle {
|
|
@@ -29,9 +35,14 @@ impl ExecHandle {
|
|
|
29
35
|
let control = handle.control();
|
|
30
36
|
let stdin = handle.take_stdin().map(Arc::new);
|
|
31
37
|
Self {
|
|
32
|
-
inner:
|
|
33
|
-
|
|
34
|
-
|
|
38
|
+
inner: ForkGuarded::new(
|
|
39
|
+
"Microsandbox::ExecHandle",
|
|
40
|
+
ExecParts {
|
|
41
|
+
handle: Arc::new(Mutex::new(handle)),
|
|
42
|
+
control,
|
|
43
|
+
stdin: std::sync::Mutex::new(stdin),
|
|
44
|
+
},
|
|
45
|
+
),
|
|
35
46
|
id,
|
|
36
47
|
}
|
|
37
48
|
}
|
|
@@ -42,14 +53,14 @@ impl ExecHandle {
|
|
|
42
53
|
|
|
43
54
|
/// Next event as a Hash, or nil when the stream ends.
|
|
44
55
|
fn recv(&self) -> Result<Option<RHash>, Error> {
|
|
45
|
-
let inner = Arc::clone(&self.inner);
|
|
56
|
+
let inner = Arc::clone(&self.inner.get()?.handle);
|
|
46
57
|
let event = block_on(async move { inner.lock().await.recv().await })?;
|
|
47
58
|
Ok(event.map(exec_event_to_hash))
|
|
48
59
|
}
|
|
49
60
|
|
|
50
61
|
/// Block until exit; returns {exit_code, success}.
|
|
51
62
|
fn wait(&self) -> Result<RHash, Error> {
|
|
52
|
-
let inner = Arc::clone(&self.inner);
|
|
63
|
+
let inner = Arc::clone(&self.inner.get()?.handle);
|
|
53
64
|
let status =
|
|
54
65
|
block_on(async move { inner.lock().await.wait().await })?.map_err(error::to_ruby)?;
|
|
55
66
|
let hash = ruby().hash_new();
|
|
@@ -60,46 +71,49 @@ impl ExecHandle {
|
|
|
60
71
|
|
|
61
72
|
/// Drain the stream and return a collected exec-output Hash.
|
|
62
73
|
fn collect(&self) -> Result<RHash, Error> {
|
|
63
|
-
let inner = Arc::clone(&self.inner);
|
|
74
|
+
let inner = Arc::clone(&self.inner.get()?.handle);
|
|
64
75
|
let output =
|
|
65
76
|
block_on(async move { inner.lock().await.collect().await })?.map_err(error::to_ruby)?;
|
|
66
77
|
crate::sandbox::exec_output_to_hash(output)
|
|
67
78
|
}
|
|
68
79
|
|
|
69
80
|
fn signal(&self, sig: i32) -> Result<(), Error> {
|
|
70
|
-
block_on(self.control.signal(sig))?.map_err(error::to_ruby)
|
|
81
|
+
block_on(self.inner.get()?.control.signal(sig))?.map_err(error::to_ruby)
|
|
71
82
|
}
|
|
72
83
|
|
|
73
84
|
fn kill(&self) -> Result<(), Error> {
|
|
74
|
-
block_on(self.control.kill())?.map_err(error::to_ruby)
|
|
85
|
+
block_on(self.inner.get()?.control.kill())?.map_err(error::to_ruby)
|
|
75
86
|
}
|
|
76
87
|
|
|
77
88
|
fn resize(&self, rows: u16, cols: u16) -> Result<(), Error> {
|
|
78
|
-
block_on(self.control.resize(rows, cols))?.map_err(error::to_ruby)
|
|
89
|
+
block_on(self.inner.get()?.control.resize(rows, cols))?.map_err(error::to_ruby)
|
|
79
90
|
}
|
|
80
91
|
|
|
81
92
|
/// The stdin sink (only the first call returns it; nil afterwards or if
|
|
82
93
|
/// stdin was not piped).
|
|
83
|
-
fn take_stdin(&self) -> Option<ExecSink> {
|
|
84
|
-
let
|
|
85
|
-
guard.
|
|
94
|
+
fn take_stdin(&self) -> Result<Option<ExecSink>, Error> {
|
|
95
|
+
let parts = self.inner.get()?;
|
|
96
|
+
let mut guard = parts.stdin.lock().expect("exec stdin mutex poisoned");
|
|
97
|
+
Ok(guard.take().map(|sink| ExecSink {
|
|
98
|
+
inner: ForkGuarded::new("Microsandbox::ExecStdin", sink),
|
|
99
|
+
}))
|
|
86
100
|
}
|
|
87
101
|
}
|
|
88
102
|
|
|
89
103
|
#[magnus::wrap(class = "Microsandbox::Native::ExecSink", free_immediately, size)]
|
|
90
104
|
pub struct ExecSink {
|
|
91
|
-
inner: Arc<microsandbox::sandbox::exec::ExecSink
|
|
105
|
+
inner: ForkGuarded<Arc<microsandbox::sandbox::exec::ExecSink>>,
|
|
92
106
|
}
|
|
93
107
|
|
|
94
108
|
impl ExecSink {
|
|
95
109
|
fn write(&self, data: RString) -> Result<(), Error> {
|
|
96
110
|
// Copy out while holding the GVL (see sandbox::fs_write for the rationale).
|
|
97
111
|
let bytes = unsafe { data.as_slice() }.to_vec();
|
|
98
|
-
block_on(self.inner.write(&bytes))?.map_err(error::to_ruby)
|
|
112
|
+
block_on(self.inner.get()?.write(&bytes))?.map_err(error::to_ruby)
|
|
99
113
|
}
|
|
100
114
|
|
|
101
115
|
fn close(&self) -> Result<(), Error> {
|
|
102
|
-
block_on(self.inner.close())?.map_err(error::to_ruby)
|
|
116
|
+
block_on(self.inner.get()?.close())?.map_err(error::to_ruby)
|
|
103
117
|
}
|
|
104
118
|
}
|
|
105
119
|
|
|
@@ -16,23 +16,23 @@ use microsandbox::sandbox::{FsReadStream, FsWriteSink};
|
|
|
16
16
|
use tokio::sync::Mutex;
|
|
17
17
|
|
|
18
18
|
use crate::error;
|
|
19
|
-
use crate::runtime::{block_on, ruby};
|
|
19
|
+
use crate::runtime::{block_on, ruby, ForkGuarded};
|
|
20
20
|
|
|
21
21
|
#[magnus::wrap(class = "Microsandbox::Native::FsReadStream", free_immediately, size)]
|
|
22
22
|
pub struct FsReadStreamHandle {
|
|
23
|
-
inner: Arc<Mutex<FsReadStream
|
|
23
|
+
inner: ForkGuarded<Arc<Mutex<FsReadStream>>>,
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
impl FsReadStreamHandle {
|
|
27
27
|
pub fn new(stream: FsReadStream) -> Self {
|
|
28
28
|
Self {
|
|
29
|
-
inner: Arc::new(Mutex::new(stream)),
|
|
29
|
+
inner: ForkGuarded::new("Microsandbox::FsReadStream", Arc::new(Mutex::new(stream))),
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/// Next chunk of bytes (ASCII-8BIT), or nil at end of stream.
|
|
34
34
|
fn recv(&self) -> Result<Option<RString>, Error> {
|
|
35
|
-
let inner = Arc::clone(
|
|
35
|
+
let inner = Arc::clone(self.inner.get()?);
|
|
36
36
|
match block_on(async move { inner.lock().await.recv().await })?.map_err(error::to_ruby)? {
|
|
37
37
|
Some(bytes) => Ok(Some(ruby().str_from_slice(bytes.as_ref()))),
|
|
38
38
|
None => Ok(None),
|
|
@@ -42,13 +42,16 @@ impl FsReadStreamHandle {
|
|
|
42
42
|
|
|
43
43
|
#[magnus::wrap(class = "Microsandbox::Native::FsWriteSink", free_immediately, size)]
|
|
44
44
|
pub struct FsWriteSinkHandle {
|
|
45
|
-
inner: Arc<Mutex<Option<FsWriteSink
|
|
45
|
+
inner: ForkGuarded<Arc<Mutex<Option<FsWriteSink>>>>,
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
impl FsWriteSinkHandle {
|
|
49
49
|
pub fn new(sink: FsWriteSink) -> Self {
|
|
50
50
|
Self {
|
|
51
|
-
inner:
|
|
51
|
+
inner: ForkGuarded::new(
|
|
52
|
+
"Microsandbox::FsWriteSink",
|
|
53
|
+
Arc::new(Mutex::new(Some(sink))),
|
|
54
|
+
),
|
|
52
55
|
}
|
|
53
56
|
}
|
|
54
57
|
|
|
@@ -56,7 +59,7 @@ impl FsWriteSinkHandle {
|
|
|
56
59
|
fn write(&self, data: RString) -> Result<(), Error> {
|
|
57
60
|
// Copy out while the GVL is held (GC.compact could move the buffer).
|
|
58
61
|
let bytes = unsafe { data.as_slice() }.to_vec();
|
|
59
|
-
let inner = Arc::clone(
|
|
62
|
+
let inner = Arc::clone(self.inner.get()?);
|
|
60
63
|
let result = block_on(async move {
|
|
61
64
|
let guard = inner.lock().await;
|
|
62
65
|
match guard.as_ref() {
|
|
@@ -72,7 +75,7 @@ impl FsWriteSinkHandle {
|
|
|
72
75
|
|
|
73
76
|
/// Flush and close the sink. Idempotent.
|
|
74
77
|
fn close(&self) -> Result<(), Error> {
|
|
75
|
-
let inner = Arc::clone(
|
|
78
|
+
let inner = Arc::clone(self.inner.get()?);
|
|
76
79
|
// Take AND close in ONE `block_on`. `block_on` returns `Err` when an
|
|
77
80
|
// interrupt is delivered before it starts; as two calls, an interrupt
|
|
78
81
|
// between them would drop the sink the first one took without ever
|
|
@@ -47,10 +47,11 @@ pub struct HostPaths {
|
|
|
47
47
|
|
|
48
48
|
impl HostPaths {
|
|
49
49
|
/// Capture which backend the create call is about to dispatch to.
|
|
50
|
-
pub fn for_default_backend() -> Self {
|
|
51
|
-
|
|
50
|
+
pub fn for_default_backend() -> Result<Self, Error> {
|
|
51
|
+
crate::backend::ensure_this_process_backend()?;
|
|
52
|
+
Ok(Self {
|
|
52
53
|
local: matches!(microsandbox::default_backend().kind(), BackendKind::Local),
|
|
53
|
-
}
|
|
54
|
+
})
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
/// Anchor a relative host path to the current directory. Absolute paths
|
data/ext/microsandbox/src/lib.rs
CHANGED
|
@@ -108,6 +108,7 @@ fn set_runtime_libkrunfw_path(path: String) {
|
|
|
108
108
|
/// no async) — runs on the Ruby thread; resolves the path from the local
|
|
109
109
|
/// backend's config.
|
|
110
110
|
fn resolved_msb_path() -> Result<String, Error> {
|
|
111
|
+
backend::ensure_this_process_backend()?;
|
|
111
112
|
let backend = microsandbox::default_backend();
|
|
112
113
|
// Shim-only entry point with no SDK `Operation` — report the Ruby-facing
|
|
113
114
|
// name (`Microsandbox.runtime_path`), like Python's name-based local_only.
|