microsandbox-rb 0.17.3 → 0.17.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +68 -1
- data/Cargo.lock +34 -32
- data/DESIGN.md +134 -41
- data/README.md +2 -1
- data/docs/runtime.md +33 -3
- data/docs/surface.md +4 -1
- data/ext/microsandbox/Cargo.toml +9 -2
- data/ext/microsandbox/src/backend.rs +28 -24
- data/ext/microsandbox/src/error.rs +37 -37
- data/ext/microsandbox/src/lib.rs +50 -2
- data/ext/microsandbox/src/runtime.rs +173 -27
- data/lib/microsandbox/sandbox.rb +39 -20
- data/lib/microsandbox/version.rb +1 -1
- metadata +1 -1
|
@@ -43,28 +43,123 @@ use std::ffi::c_void;
|
|
|
43
43
|
use std::future::Future;
|
|
44
44
|
use std::mem::ManuallyDrop;
|
|
45
45
|
use std::panic::{catch_unwind, AssertUnwindSafe};
|
|
46
|
-
use std::sync::atomic::{AtomicPtr, Ordering};
|
|
46
|
+
use std::sync::atomic::{AtomicPtr, AtomicU32, Ordering};
|
|
47
47
|
|
|
48
48
|
use magnus::{Error, Ruby, TypedData};
|
|
49
49
|
use tokio::runtime::Runtime;
|
|
50
50
|
|
|
51
51
|
use crate::error;
|
|
52
52
|
|
|
53
|
-
/// The current Ruby handle. Safe to call from any bound method
|
|
54
|
-
/// conversion: we always hold the GVL there
|
|
55
|
-
/// returns and re-acquires it).
|
|
53
|
+
/// The current Ruby handle. Safe to call from any bound method, value
|
|
54
|
+
/// conversion or error construction (`error::*`): we always hold the GVL there
|
|
55
|
+
/// (both run after `block_on` returns and re-acquires it).
|
|
56
|
+
///
|
|
57
|
+
/// Panics when the calling thread does not hold the GVL (a `nogvl` callback, a
|
|
58
|
+
/// tokio worker): `Ruby::get` checks that in release builds too, so a misplaced
|
|
59
|
+
/// call fails loudly instead of touching Ruby without the lock.
|
|
56
60
|
pub fn ruby() -> Ruby {
|
|
57
|
-
Ruby::get().expect("microsandbox: not on a Ruby thread")
|
|
61
|
+
Ruby::get().expect("microsandbox: not on a Ruby thread holding the GVL")
|
|
58
62
|
}
|
|
59
63
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
//--------------------------------------------------------------------------------------------------
|
|
65
|
+
// Process identity
|
|
66
|
+
//--------------------------------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
/// Which process this is, for fork safety: the pid plus a fork generation.
|
|
69
|
+
///
|
|
70
|
+
/// Everything fork-aware in the extension — the runtime cell below, the
|
|
71
|
+
/// backend record in `backend.rs`, [`ForkGuarded`] — tags what it builds with
|
|
72
|
+
/// the identity of the process that built it and compares that against
|
|
73
|
+
/// [`ProcessIdentity::current`] before use. Both halves are compared:
|
|
74
|
+
///
|
|
75
|
+
/// * The pid alone is not enough. A descendant can end up with the pid of an
|
|
76
|
+
/// ancestor whose state it inherited, if the pid counter wraps onto exactly
|
|
77
|
+
/// that value and no SDK call happened in between; it would then pass for
|
|
78
|
+
/// that ancestor.
|
|
79
|
+
/// * The generation alone is not enough either. It counts the `fork`s that
|
|
80
|
+
/// ran this library's atfork child handler, and a process can be created
|
|
81
|
+
/// without running it — `vfork`, a raw `clone` — in which case only the pid
|
|
82
|
+
/// tells the child apart.
|
|
83
|
+
///
|
|
84
|
+
/// Every `fork(2)` that runs atfork handlers (which `Kernel#fork` /
|
|
85
|
+
/// `Process.fork` always do: Ruby calls libc's `fork`) gives the child a
|
|
86
|
+
/// generation one higher than its parent's, so parent and child differ even
|
|
87
|
+
/// when their pids are equal.
|
|
88
|
+
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
89
|
+
pub struct ProcessIdentity {
|
|
90
|
+
pub pid: u32,
|
|
91
|
+
generation: u32,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/// How many `fork`s, counted from the process that loaded the extension, led
|
|
95
|
+
/// to this process. Incremented only by [`on_fork_child`], in a child, and
|
|
96
|
+
/// inherited (as plain memory) by every child. 32 bits: it would take four
|
|
97
|
+
/// billion nested forks in one lineage to wrap, and a wrap matters only if
|
|
98
|
+
/// the pid matches as well.
|
|
99
|
+
static FORK_GENERATION: AtomicU32 = AtomicU32::new(0);
|
|
100
|
+
|
|
101
|
+
/// The `pthread_atfork` child handler. It runs in the new child before `fork`
|
|
102
|
+
/// returns there, while the child is single-threaded, and must be
|
|
103
|
+
/// async-signal-safe: one lock-free atomic add, nothing else (no allocation,
|
|
104
|
+
/// no lock, no Ruby). `Relaxed` is enough: the only thread that exists to read
|
|
105
|
+
/// the counter is this one, and threads started later see it through the
|
|
106
|
+
/// spawn's own synchronization. The parent never changes its counter.
|
|
107
|
+
extern "C" fn on_fork_child() {
|
|
108
|
+
FORK_GENERATION.fetch_add(1, Ordering::Relaxed);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// Register [`on_fork_child`]. Called once, from the extension's init.
|
|
112
|
+
///
|
|
113
|
+
/// Ruby never unloads (`dlclose`s) an extension, so the handler's code stays
|
|
114
|
+
/// mapped for the life of the process, as `pthread_atfork` requires — handlers
|
|
115
|
+
/// cannot be unregistered. If registration fails (`ENOMEM`), the generation
|
|
116
|
+
/// stays 0 and identity degrades to the pid alone, which is how the extension
|
|
117
|
+
/// behaved before the generation existed; that is not worth refusing to load
|
|
118
|
+
/// over. Platforms without `pthread_atfork` (the gem ships for macOS and
|
|
119
|
+
/// Linux only) get the same pid-only identity.
|
|
120
|
+
pub fn register_fork_handler() {
|
|
121
|
+
static ONCE: std::sync::Once = std::sync::Once::new();
|
|
122
|
+
ONCE.call_once(|| {
|
|
123
|
+
#[cfg(unix)]
|
|
124
|
+
// SAFETY: `on_fork_child` is an `extern "C" fn()` that stays valid for
|
|
125
|
+
// the life of the process (see above) and is async-signal-safe.
|
|
126
|
+
unsafe {
|
|
127
|
+
let _ = libc::pthread_atfork(None, None, Some(on_fork_child));
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
impl ProcessIdentity {
|
|
133
|
+
/// This process's identity. The single `std::process::id()` call site.
|
|
134
|
+
pub fn current() -> Self {
|
|
135
|
+
Self {
|
|
136
|
+
pid: std::process::id(),
|
|
137
|
+
generation: FORK_GENERATION.load(Ordering::Relaxed),
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/// An identity from its two halves — ONLY for the fork-identity
|
|
142
|
+
/// diagnostics in `lib.rs`, which let the specs compare identities that no
|
|
143
|
+
/// real fork can produce on demand (an equal pid with another generation).
|
|
144
|
+
pub fn from_parts(pid: u32, generation: u32) -> Self {
|
|
145
|
+
Self { pid, generation }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/// The fork generation, for `Native.fork_generation` (a diagnostic the
|
|
149
|
+
/// fork specs read; not part of the Ruby API).
|
|
150
|
+
pub fn generation(self) -> u32 {
|
|
151
|
+
self.generation
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/// Both halves in one word, for a lock-free atomic record. `0` is never a
|
|
155
|
+
/// real identity (no user process has pid 0), so it can mean "none".
|
|
156
|
+
pub fn to_bits(self) -> u64 {
|
|
157
|
+
(u64::from(self.pid) << 32) | u64::from(self.generation)
|
|
158
|
+
}
|
|
65
159
|
}
|
|
66
160
|
|
|
67
|
-
// The process-wide tokio runtime, tagged with the
|
|
161
|
+
// The process-wide tokio runtime, tagged with the identity of the process it
|
|
162
|
+
// was built under (see `ProcessIdentity`). A
|
|
68
163
|
// multi-threaded runtime owns worker + I/O-driver threads, and `fork(2)` copies
|
|
69
164
|
// ONLY the calling thread — so a child that inherits this runtime has a runtime
|
|
70
165
|
// whose threads are gone: `block_on` can still drive the current thread, but the
|
|
@@ -72,12 +167,12 @@ pub fn current_pid() -> u32 {
|
|
|
72
167
|
// connections stall/drop mid-use. This is hit in practice by forking job servers
|
|
73
168
|
// (Solid Queue, Resque) and clustered web servers (Puma workers).
|
|
74
169
|
//
|
|
75
|
-
// The runtime and its
|
|
76
|
-
// reader sees either a whole cell or none: there is no window in
|
|
77
|
-
// pointer is new and the
|
|
78
|
-
// could capture locked or that a panic could poison.
|
|
170
|
+
// The runtime and its process identity live in ONE heap cell behind ONE atomic
|
|
171
|
+
// pointer, so a reader sees either a whole cell or none: there is no window in
|
|
172
|
+
// which the pointer is new and the identity old (or the reverse), and no mutex
|
|
173
|
+
// that a fork could capture locked or that a panic could poison.
|
|
79
174
|
struct RuntimeCell {
|
|
80
|
-
|
|
175
|
+
owner: ProcessIdentity,
|
|
81
176
|
rt: Runtime,
|
|
82
177
|
}
|
|
83
178
|
|
|
@@ -94,13 +189,13 @@ fn build_runtime() -> Runtime {
|
|
|
94
189
|
/// The multi-threaded tokio runtime for THIS process, built on first use and
|
|
95
190
|
/// rebuilt after a `fork(2)` (see the note above).
|
|
96
191
|
pub fn runtime() -> &'static Runtime {
|
|
97
|
-
let cur =
|
|
192
|
+
let cur = ProcessIdentity::current();
|
|
98
193
|
let mut observed = RUNTIME.load(Ordering::Acquire);
|
|
99
194
|
// SAFETY (every dereference below): a non-null pointer in `RUNTIME` came
|
|
100
195
|
// from `Box::into_raw` and is never freed once published, so it is valid
|
|
101
196
|
// for `'static`.
|
|
102
197
|
if let Some(cell) = unsafe { observed.as_ref() } {
|
|
103
|
-
if cell.
|
|
198
|
+
if cell.owner == cur {
|
|
104
199
|
// Built in this process: its threads are alive here.
|
|
105
200
|
return &cell.rt;
|
|
106
201
|
}
|
|
@@ -109,7 +204,7 @@ pub fn runtime() -> &'static Runtime {
|
|
|
109
204
|
// Slow path: no runtime yet, or the one we see was built by a parent
|
|
110
205
|
// process. Build ours and publish it with a compare-exchange.
|
|
111
206
|
let fresh = Box::into_raw(Box::new(RuntimeCell {
|
|
112
|
-
|
|
207
|
+
owner: cur,
|
|
113
208
|
rt: build_runtime(),
|
|
114
209
|
}));
|
|
115
210
|
loop {
|
|
@@ -117,12 +212,12 @@ pub fn runtime() -> &'static Runtime {
|
|
|
117
212
|
// Published. The cell we replaced (if any) belonged to a parent
|
|
118
213
|
// process and is LEAKED on purpose, never dropped: its worker
|
|
119
214
|
// threads did not survive the fork, and `Runtime`'s `Drop` joins
|
|
120
|
-
// them. One runtime is leaked per
|
|
215
|
+
// them. One runtime is leaked per forked process that uses the
|
|
121
216
|
// SDK.
|
|
122
217
|
Ok(_) => return unsafe { &(*fresh).rt },
|
|
123
218
|
Err(winner) => {
|
|
124
219
|
if let Some(cell) = unsafe { winner.as_ref() } {
|
|
125
|
-
if cell.
|
|
220
|
+
if cell.owner == cur {
|
|
126
221
|
// Another thread of THIS process won the race. Our
|
|
127
222
|
// cell was never published and its threads are alive
|
|
128
223
|
// here, so dropping it is safe (and joins them).
|
|
@@ -153,7 +248,7 @@ pub fn runtime() -> &'static Runtime {
|
|
|
153
248
|
/// exits: Ruby frees every `T_DATA` object in `rb_objspace_call_finalizer` —
|
|
154
249
|
/// leaks it instead.
|
|
155
250
|
pub struct ForkGuarded<T> {
|
|
156
|
-
|
|
251
|
+
owner: ProcessIdentity,
|
|
157
252
|
/// The Ruby class users know the handle by, for the error message.
|
|
158
253
|
class: &'static str,
|
|
159
254
|
inner: ManuallyDrop<T>,
|
|
@@ -162,7 +257,18 @@ pub struct ForkGuarded<T> {
|
|
|
162
257
|
impl<T> ForkGuarded<T> {
|
|
163
258
|
pub fn new(class: &'static str, inner: T) -> Self {
|
|
164
259
|
Self {
|
|
165
|
-
|
|
260
|
+
owner: ProcessIdentity::current(),
|
|
261
|
+
class,
|
|
262
|
+
inner: ManuallyDrop::new(inner),
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/// A guard owned by `owner` instead of this process — ONLY for the
|
|
267
|
+
/// fork-identity diagnostic in `lib.rs`, which runs the real ownership
|
|
268
|
+
/// decision ([`get_in`](Self::get_in)) on identities given by a spec.
|
|
269
|
+
pub fn owned_by(owner: ProcessIdentity, class: &'static str, inner: T) -> Self {
|
|
270
|
+
Self {
|
|
271
|
+
owner,
|
|
166
272
|
class,
|
|
167
273
|
inner: ManuallyDrop::new(inner),
|
|
168
274
|
}
|
|
@@ -171,11 +277,15 @@ impl<T> ForkGuarded<T> {
|
|
|
171
277
|
/// The payload, or `ForkedHandleError` when this is not the process that
|
|
172
278
|
/// created it.
|
|
173
279
|
pub fn get(&self) -> Result<&T, Error> {
|
|
174
|
-
|
|
175
|
-
|
|
280
|
+
self.get_in(ProcessIdentity::current())
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/// [`get`](Self::get) as decided in the process identified by `cur`.
|
|
284
|
+
pub fn get_in(&self, cur: ProcessIdentity) -> Result<&T, Error> {
|
|
285
|
+
if self.owner == cur {
|
|
176
286
|
Ok(&self.inner)
|
|
177
287
|
} else {
|
|
178
|
-
Err(error::forked_handle(self.class, self.pid, cur))
|
|
288
|
+
Err(error::forked_handle(self.class, self.owner.pid, cur.pid))
|
|
179
289
|
}
|
|
180
290
|
}
|
|
181
291
|
|
|
@@ -191,7 +301,7 @@ impl<T> ForkGuarded<T> {
|
|
|
191
301
|
|
|
192
302
|
impl<T> Drop for ForkGuarded<T> {
|
|
193
303
|
fn drop(&mut self) {
|
|
194
|
-
if self.
|
|
304
|
+
if self.owner == ProcessIdentity::current() {
|
|
195
305
|
// SAFETY: dropped exactly once, here; `inner` is not used again.
|
|
196
306
|
unsafe { ManuallyDrop::drop(&mut self.inner) };
|
|
197
307
|
}
|
|
@@ -209,6 +319,13 @@ impl<T> Drop for ForkGuarded<T> {
|
|
|
209
319
|
/// Every module's `define` calls this for each type it wraps, right after
|
|
210
320
|
/// defining the class, so wrapping a value afterwards is `TypedData::class`
|
|
211
321
|
/// reading a resolved static plus `rb_data_typed_object_wrap`.
|
|
322
|
+
///
|
|
323
|
+
/// Doing it at load also keeps that first resolution away from concurrent
|
|
324
|
+
/// callers, which matters since magnus 0.9: the static is a magnus `Lazy`, an
|
|
325
|
+
/// `OnceLock` whose initializer (the `const_get`) runs while holding the lock.
|
|
326
|
+
/// That `funcall` can switch Ruby threads, and a second thread reaching the
|
|
327
|
+
/// same lock would block on it while holding the GVL the first one needs to
|
|
328
|
+
/// finish.
|
|
212
329
|
pub fn preload_class<T: TypedData>(ruby: &Ruby) {
|
|
213
330
|
let _ = T::class(ruby);
|
|
214
331
|
}
|
|
@@ -390,6 +507,9 @@ where
|
|
|
390
507
|
/// `Err` means the future was never polled (see [`nogvl`], and the fork check
|
|
391
508
|
/// below); propagate it with `?` before looking at the future's own output:
|
|
392
509
|
/// `block_on(fut)?.map_err(error::to_ruby)?`.
|
|
510
|
+
///
|
|
511
|
+
/// Every native call goes through this, with one deliberate exception: the
|
|
512
|
+
/// runtime installer, see [`block_on_runtime_install`].
|
|
393
513
|
pub fn block_on<F>(fut: F) -> Result<F::Output, Error>
|
|
394
514
|
where
|
|
395
515
|
F: Future,
|
|
@@ -401,3 +521,29 @@ where
|
|
|
401
521
|
crate::backend::ensure_this_process_backend()?;
|
|
402
522
|
nogvl(|| runtime().block_on(fut))
|
|
403
523
|
}
|
|
524
|
+
|
|
525
|
+
/// [`block_on`] without its backend check, for the runtime installer ONLY:
|
|
526
|
+
/// `Native.install` and `Native.setup` (`microsandbox::setup`).
|
|
527
|
+
///
|
|
528
|
+
/// The check at the top of [`block_on`] is deliberate and stays there: it is
|
|
529
|
+
/// what makes every other native call safe after a fork. This entry is not a
|
|
530
|
+
/// cheaper alternative to it. It exists because the installer never touches
|
|
531
|
+
/// the default backend or the catalog — `Setup::install` creates the
|
|
532
|
+
/// `bin`/`lib` directories, runs the installed `msb --version`, copies a CI
|
|
533
|
+
/// build or downloads and unpacks the release bundle, and checks that the two
|
|
534
|
+
/// files exist, all under the base directory it resolves from `MSB_HOME` —
|
|
535
|
+
/// so in a forked child the check would only rebuild (and leak) a backend
|
|
536
|
+
/// nothing uses, or fail the install when the inherited backend selection no
|
|
537
|
+
/// longer resolves there. Skipping it leaves the backend record as it was:
|
|
538
|
+
/// the child's next call that does reach the backend is the one that takes
|
|
539
|
+
/// it over.
|
|
540
|
+
///
|
|
541
|
+
/// Before adding a caller, trace its core path and confirm it never reaches
|
|
542
|
+
/// `microsandbox::default_backend()` (directly or through a `Sandbox`,
|
|
543
|
+
/// `Volume`, `Image`, `Snapshot` or agent function that resolves it).
|
|
544
|
+
pub fn block_on_runtime_install<F>(fut: F) -> Result<F::Output, Error>
|
|
545
|
+
where
|
|
546
|
+
F: Future,
|
|
547
|
+
{
|
|
548
|
+
nogvl(|| runtime().block_on(fut))
|
|
549
|
+
}
|
data/lib/microsandbox/sandbox.rb
CHANGED
|
@@ -497,7 +497,7 @@ module Microsandbox
|
|
|
497
497
|
opts = build_create_opts(**kwargs)
|
|
498
498
|
Microsandbox.ensure_runtime!
|
|
499
499
|
# Everything Ruby-level happens BEFORE `acquire`: its interrupt mask must
|
|
500
|
-
# cover only the native call
|
|
500
|
+
# cover only the native call, never caller-supplied code.
|
|
501
501
|
sandbox_name = name.to_s
|
|
502
502
|
sandbox = acquire { Native::Sandbox.create(sandbox_name, opts) }
|
|
503
503
|
return sandbox unless block_given?
|
|
@@ -603,17 +603,34 @@ module Microsandbox
|
|
|
603
603
|
# sandbox would be garbage and only GC would stop it, "eventually"; for
|
|
604
604
|
# `Timeout.timeout { Sandbox.create(...) }` that is not good enough. So:
|
|
605
605
|
#
|
|
606
|
-
# - the
|
|
607
|
-
# which holds a queued exception (or kill) back until the
|
|
608
|
-
# assigned. The mask covers only
|
|
609
|
-
#
|
|
610
|
-
# ({Microsandbox.ensure_runtime!})
|
|
611
|
-
#
|
|
612
|
-
#
|
|
613
|
-
#
|
|
614
|
-
#
|
|
615
|
-
#
|
|
616
|
-
#
|
|
606
|
+
# - the native call runs under `Thread.handle_interrupt(Object => :never)`,
|
|
607
|
+
# which holds a queued exception (or kill) back until the bare
|
|
608
|
+
# {Native::Sandbox} is assigned to a local. The mask covers only that
|
|
609
|
+
# uninterruptible call, so it changes no observable timing; runtime
|
|
610
|
+
# provisioning ({Microsandbox.ensure_runtime!}), the wrapper's
|
|
611
|
+
# construction and the block forms' `yield` stay outside it, with
|
|
612
|
+
# normal interrupt handling. For the same reason the block passed here
|
|
613
|
+
# must contain NOTHING but the native call: argument conversion
|
|
614
|
+
# (`name.to_s`, option Hashes) is the caller's own code and has to run
|
|
615
|
+
# before `acquire`, where it stays interruptible;
|
|
616
|
+
# - the wrapper is built after the mask (`new` may run a subclass's
|
|
617
|
+
# `initialize`, which must not hold a `Timeout`/`Thread#kill` back), so
|
|
618
|
+
# a held-back interrupt is delivered as the mask ends, before `new`.
|
|
619
|
+
# Until `new` has returned, the cleanup therefore works on the bare
|
|
620
|
+
# native handle, never on the wrapper: an interrupt at that point, one
|
|
621
|
+
# that lands inside a subclass's `initialize`, and an exception that
|
|
622
|
+
# `initialize` raises itself all leave the caller without a reference,
|
|
623
|
+
# so all three stop the sandbox (when owned, see below);
|
|
624
|
+
# - the `ensure` plus the `handed_off` flag — not `rescue Exception;
|
|
625
|
+
# raise` — is what also covers `Thread#kill`, which no `rescue` can
|
|
626
|
+
# catch.
|
|
627
|
+
#
|
|
628
|
+
# The protected window is the native handle's assignment through the
|
|
629
|
+
# wrapper's construction, and the cleanup in it is best-effort: a
|
|
630
|
+
# *further* interrupt that lands during the cleanup, before the native
|
|
631
|
+
# `stop` has started, or one delivered at this method's return boundary
|
|
632
|
+
# (after `handed_off` is set, before the caller has assigned the result),
|
|
633
|
+
# leaves the teardown to GC.
|
|
617
634
|
#
|
|
618
635
|
# A SIGINT (Ctrl-C on the main thread) is the exception: it is not queued
|
|
619
636
|
# like those two and cannot be masked. One that lands during the native
|
|
@@ -625,30 +642,32 @@ module Microsandbox
|
|
|
625
642
|
# Ownership: {connect_or_create} can hand back a sandbox that was already
|
|
626
643
|
# running for someone else, and {start} restarts a persisted one. The
|
|
627
644
|
# cleanup therefore applies EXACTLY the predicate the block form of
|
|
628
|
-
# {connect_or_create} uses —
|
|
645
|
+
# {connect_or_create} uses — the lifecycle ownership behind
|
|
646
|
+
# {Sandbox#owns_lifecycle?}, read from the native handle — in all three
|
|
629
647
|
# callers, {create} included: a sandbox this call does not own is never
|
|
630
648
|
# stopped here. Its reference is just let go (GC closes the handle, the
|
|
631
649
|
# sandbox keeps running).
|
|
632
650
|
# @yieldreturn [Native::Sandbox]
|
|
633
651
|
# @return [Sandbox]
|
|
634
652
|
def acquire
|
|
635
|
-
|
|
636
|
-
|
|
653
|
+
native = nil
|
|
654
|
+
handed_off = false
|
|
637
655
|
begin
|
|
638
656
|
Thread.handle_interrupt(Object => :never) do
|
|
639
|
-
|
|
657
|
+
native = yield
|
|
640
658
|
end
|
|
641
|
-
|
|
659
|
+
sandbox = new(native)
|
|
660
|
+
handed_off = true
|
|
661
|
+
sandbox
|
|
642
662
|
ensure
|
|
643
|
-
if
|
|
663
|
+
if native && !handed_off
|
|
644
664
|
begin
|
|
645
|
-
|
|
665
|
+
native.stop if native.owns_lifecycle
|
|
646
666
|
rescue Microsandbox::Error
|
|
647
667
|
# best-effort cleanup; the interrupt is what the caller must see
|
|
648
668
|
end
|
|
649
669
|
end
|
|
650
670
|
end
|
|
651
|
-
sandbox
|
|
652
671
|
end
|
|
653
672
|
|
|
654
673
|
# @api private
|
data/lib/microsandbox/version.rb
CHANGED
|
@@ -8,7 +8,7 @@ module Microsandbox
|
|
|
8
8
|
# Versioning section of the README for the full gem-to-runtime map. Must equal
|
|
9
9
|
# the native ext's Cargo crate version (`Native.version`), enforced by
|
|
10
10
|
# spec/unit/version_spec.rb.
|
|
11
|
-
VERSION = "0.17.
|
|
11
|
+
VERSION = "0.17.4"
|
|
12
12
|
|
|
13
13
|
# The upstream microsandbox runtime release this gem build embeds — the `tag`
|
|
14
14
|
# pinned on the `microsandbox`/`microsandbox-network` git deps in
|