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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6d3b616f0ff13a9961eab2e0cdd3812f859754efd7489d5857db0e49588f2b73
|
|
4
|
+
data.tar.gz: a172d9b8b62bb19f8b4cc2accfadec90f56b83b0e99112109a42032b7ca9a6f4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9357333f88d8061e822da080bdc014438b7ca8bda1b94584a01e89fa1278c9d661b5a5eb2a7e5e36e5879076c2e9f6e82cb067dd6a4aa86023f16360953369ed
|
|
7
|
+
data.tar.gz: e7b24e0067df67c76a57f5b204f743d9420c4cd9d43f717bd3576e0324023493b68821d65e1c910168ad87a3e76261dcbb825b1476184b6c9318891cb4a99c1a
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,49 @@ wraps, and the README's Versioning section keeps the full gem→runtime map.
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.17.3] - 2026-10-13
|
|
12
|
+
|
|
13
|
+
Gem-only fix release; the embedded runtime stays at **`v0.6.18`**. A process
|
|
14
|
+
forked after the SDK was used — a Puma, Resque or Solid Queue worker — used to
|
|
15
|
+
hang for 30 s on Linux, or die with `SIGTRAP` on macOS, on its first SDK call.
|
|
16
|
+
The child's first SDK call now builds a fresh backend, and a handle created
|
|
17
|
+
before the fork raises the new `ForkedHandleError` there instead of being used.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- **Forking a process that has used the SDK no longer hangs or kills the
|
|
22
|
+
child.** A forked child inherited the parent's default backend, whose SQLite
|
|
23
|
+
worker threads (local) or pooled connections (cloud) do not exist in it: the
|
|
24
|
+
child's first catalog call waited out a 30 s pool timeout on Linux and then
|
|
25
|
+
raised a plain `Microsandbox::Error`, and on macOS the child died with
|
|
26
|
+
`SIGTRAP` — as it did when it merely selected a backend, because destroying
|
|
27
|
+
the inherited one traps too. The first SDK call in a child now builds a
|
|
28
|
+
fresh backend the same way the parent's was chosen (`set_default_backend`,
|
|
29
|
+
an enclosing `with_backend`, or the environment) and leaks the inherited one
|
|
30
|
+
instead of destroying it; a `with_backend` block entered before the fork can
|
|
31
|
+
be left in the child, and selecting a backend there works even when the
|
|
32
|
+
inherited choice no longer resolves. If the backend such a block should
|
|
33
|
+
restore cannot be chosen again in the child, the block's temporary selection
|
|
34
|
+
stays in effect and an exception raised by the block is no longer replaced
|
|
35
|
+
by the restore failure (which is warned about instead). The tokio runtime, already rebuilt after a fork, now
|
|
36
|
+
keeps its pointer and pid in one atomic cell, without the mutex a fork could
|
|
37
|
+
capture locked. See the new "Forking" section of
|
|
38
|
+
[docs/runtime.md](docs/runtime.md#forking-puma-resque-solid-queue) for what
|
|
39
|
+
is supported and what is not.
|
|
40
|
+
- A handle created before a fork (`Sandbox`, `SandboxHandle`, `ExecHandle`,
|
|
41
|
+
streams, SSH and agent clients, `VolumeFs`, ...) is refused in the child
|
|
42
|
+
with `ForkedHandleError` instead of being driven on the parent's dead
|
|
43
|
+
runtime, and a child that garbage-collects one — at the latest when it
|
|
44
|
+
exits — no longer runs its destructor. `Sandbox#owns_lifecycle?` is refused
|
|
45
|
+
the same way; `name`, `id` and the `SandboxHandle` snapshot fields stay
|
|
46
|
+
readable.
|
|
47
|
+
|
|
48
|
+
### Added
|
|
49
|
+
|
|
50
|
+
- `Microsandbox::ForkedHandleError` (`#code` `"forked-handle"`), raised when a
|
|
51
|
+
handle is used in a process forked from the one that created it. The message
|
|
52
|
+
names both pids.
|
|
53
|
+
|
|
11
54
|
## [0.17.2] - 2026-10-12
|
|
12
55
|
|
|
13
56
|
Gem-only fix and release-pipeline hardening release; the embedded runtime
|
|
@@ -1470,7 +1513,8 @@ microsandbox runtime, aligned with the official Python/Node/Go SDKs.
|
|
|
1470
1513
|
core crate has Apple-native deps). Until precompiled gems are published,
|
|
1471
1514
|
installing from source requires a Rust toolchain (stable >= 1.91).
|
|
1472
1515
|
|
|
1473
|
-
[Unreleased]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.17.
|
|
1516
|
+
[Unreleased]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.17.3...HEAD
|
|
1517
|
+
[0.17.3]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.17.2...v0.17.3
|
|
1474
1518
|
[0.17.2]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.17.1...v0.17.2
|
|
1475
1519
|
[0.17.1]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.17.0...v0.17.1
|
|
1476
1520
|
[0.17.0]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.16.0...v0.17.0
|
data/Cargo.lock
CHANGED
data/DESIGN.md
CHANGED
|
@@ -135,6 +135,104 @@ operation after the acquisition.
|
|
|
135
135
|
extension, driving it with a fake `msb` (`spec/support/fake_runtime/`) whose
|
|
136
136
|
log shows whether a sandbox was stopped, dropped, or left attached.
|
|
137
137
|
|
|
138
|
+
### Fork safety
|
|
139
|
+
|
|
140
|
+
`fork(2)` copies only the calling thread. A child of a process that has used
|
|
141
|
+
the SDK therefore inherits native state whose threads are gone: the tokio
|
|
142
|
+
runtime (workers, I/O driver), the core's process-wide default backend (each
|
|
143
|
+
SQLite connection of a `LocalBackend` is served by its own worker thread; a
|
|
144
|
+
`CloudBackend`'s pooled connections belong to the parent's runtime), and every
|
|
145
|
+
wrapped handle built on top of them. Using that state hangs or crashes — the
|
|
146
|
+
child's first catalog read used to wait out the pool's 30 s acquire timeout on
|
|
147
|
+
Linux and die with `SIGTRAP` on macOS — and so does *destroying* it, because
|
|
148
|
+
the destructors wake the same missing threads (on macOS, replacing an
|
|
149
|
+
inherited `LocalBackend` was enough to kill the child). One rule covers all of
|
|
150
|
+
it: **inherited native state is never used and never dropped in the child. It
|
|
151
|
+
is leaked, once per fork, and replaced by state built in the child.** Three
|
|
152
|
+
pieces implement it, all keyed on the pid (`runtime::current_pid`):
|
|
153
|
+
|
|
154
|
+
- **Runtime cell** (`runtime.rs`). The runtime and the pid it was built under
|
|
155
|
+
live in one heap cell behind one `AtomicPtr`. `runtime()` returns the cell's
|
|
156
|
+
runtime when its pid is ours; otherwise it builds a new cell and publishes
|
|
157
|
+
it with a compare-exchange (a loser of that race in the same process drops
|
|
158
|
+
its own, never-published runtime). There is no mutex, so nothing can be
|
|
159
|
+
inherited locked or poisoned, and no window in which pointer and pid
|
|
160
|
+
disagree. A cell built by a parent process is leaked: `Runtime`'s `Drop`
|
|
161
|
+
joins worker threads that do not exist.
|
|
162
|
+
- **Backend generation** (`backend.rs`). The binding records how the default
|
|
163
|
+
backend was chosen — the core's own ladder (environment, profile, config
|
|
164
|
+
file), or an explicit `set_default_backend`/`with_backend` selection — and
|
|
165
|
+
the pid it belongs to. `ensure_this_process_backend` runs, with the GVL
|
|
166
|
+
held, at the start of every `block_on` and of every synchronous entry that
|
|
167
|
+
reads the backend. In a process whose pid differs from the recorded one it
|
|
168
|
+
builds a fresh backend from the record, swaps it into the core and
|
|
169
|
+
`mem::forget`s the inherited one, along with the backends that open
|
|
170
|
+
`with_backend` scopes had saved for their restore. A scope the parent opened
|
|
171
|
+
can still be closed in the child: closing it installs a backend rebuilt from
|
|
172
|
+
how the saved one had been chosen. For the ladder this means the child
|
|
173
|
+
resolves it again, from its own environment, at its first native call; if
|
|
174
|
+
that fails, the call raises the configuration error, nothing is changed, and
|
|
175
|
+
the next call tries again. `set_default_backend` and a `with_backend` push
|
|
176
|
+
are the exception: they are about to replace the default anyway, so in a
|
|
177
|
+
child they take over with the backend they were asked for and never rebuild
|
|
178
|
+
the inherited selection — an explicit selection always gets a child out of
|
|
179
|
+
an inherited one that no longer resolves. Closing a scope works the same
|
|
180
|
+
way: a child whose first use of the backend is leaving a block its parent
|
|
181
|
+
entered takes over directly with what the scope restores, so it never needs
|
|
182
|
+
the scope's temporary backend (which it may be unable to rebuild). Closing a
|
|
183
|
+
scope always consumes it: when the backend it should restore cannot be
|
|
184
|
+
rebuilt in the child, the scope's temporary selection stays in effect and
|
|
185
|
+
the error is reported (by `with_backend` as a warning when the block itself
|
|
186
|
+
raised, so that the block's exception is the one that propagates).
|
|
187
|
+
- **`ForkGuarded<T>`** (`runtime.rs`). The payload of every `#[magnus::wrap]`
|
|
188
|
+
type sits in one, tagged with the creating pid. `get()` is the only way to
|
|
189
|
+
it for anything that acts on the value, and in another process it returns
|
|
190
|
+
`Microsandbox::ForkedHandleError` (there is no `Deref`, so an unchecked
|
|
191
|
+
access does not compile). Its `Drop` runs the payload's destructor only in
|
|
192
|
+
the creating process; a child that garbage-collects an inherited handle —
|
|
193
|
+
at the latest when it exits, since Ruby frees every wrapped object then —
|
|
194
|
+
leaks it. Accessors that only copy plain data out of the value (`name`,
|
|
195
|
+
`id`, the `SandboxHandle` snapshot fields, `ExecHandle#id`) skip the check
|
|
196
|
+
through `peek()`, so a child can still read the name it needs to open a
|
|
197
|
+
handle of its own.
|
|
198
|
+
|
|
199
|
+
What a child leaks is bounded: one runtime, the inherited default backend, the
|
|
200
|
+
saved backend of each open scope, and each inherited handle it collects; plus
|
|
201
|
+
one backend for each parent-opened scope it closes.
|
|
202
|
+
|
|
203
|
+
The mutexes involved (`backend.rs`) are only taken with the GVL held, and
|
|
204
|
+
nothing that runs under them calls into Ruby or releases the GVL — Ruby
|
|
205
|
+
exceptions are built after they are released. A Ruby-level `fork` needs the
|
|
206
|
+
GVL, so a child never inherits one of them locked.
|
|
207
|
+
|
|
208
|
+
Limits, by design:
|
|
209
|
+
|
|
210
|
+
- **Inherited file descriptors.** A child inherits the parent's open fds,
|
|
211
|
+
including the parent-watchdog pipe of every sandbox the parent created.
|
|
212
|
+
While such a child lives, the runtime sees no EOF on that pipe when the
|
|
213
|
+
parent exits, so the sandbox is not torn down until the child exits too.
|
|
214
|
+
- **Ownership.** A sandbox created before the fork belongs to the parent. The
|
|
215
|
+
child cannot act on it through the inherited handle; it can open its own by
|
|
216
|
+
name, with the usual ownership rules.
|
|
217
|
+
- **Threads mid-call.** A thread that is inside a native call when another
|
|
218
|
+
thread forks (the GVL is released there) may hold a lock in the core at
|
|
219
|
+
that instant — for example the read guard of the core's default-backend
|
|
220
|
+
`RwLock`, for the few instructions of an `Arc` clone — and the child's swap
|
|
221
|
+
then waits on that inherited lock forever. The binding cannot close this
|
|
222
|
+
window. Forking while another thread is inside a call is unsupported, as
|
|
223
|
+
with any threaded native library.
|
|
224
|
+
- **Other libraries.** The SDK no longer touches inherited state; code that
|
|
225
|
+
reaches it some other way can still hang or trap.
|
|
226
|
+
- **Pid reuse.** Process identity is the pid. A descendant that ends up with
|
|
227
|
+
the pid of an ancestor whose state it inherited without any SDK use in
|
|
228
|
+
between would be mistaken for that ancestor. This needs the pid counter to
|
|
229
|
+
wrap onto exactly that value.
|
|
230
|
+
|
|
231
|
+
`spec/unit/fork_safety_spec.rb` (catalog only, no runtime),
|
|
232
|
+
`spec/fake_runtime/fork_safety_spec.rb` (a live handle over the fake `msb`)
|
|
233
|
+
and `spec/integration/fork_safety_spec.rb` (a real microVM) pin the behaviour;
|
|
234
|
+
each forks from a probe process of its own.
|
|
235
|
+
|
|
138
236
|
## Error mapping
|
|
139
237
|
|
|
140
238
|
The core returns one big `MicrosandboxError` enum. `error.rs` maps each variant
|
data/README.md
CHANGED
|
@@ -167,7 +167,7 @@ See [docs/observability.md](docs/observability.md).
|
|
|
167
167
|
| Guest filesystem, streaming reads/writes, host copies | [docs/filesystem.md](docs/filesystem.md) |
|
|
168
168
|
| Metrics, logs, `ping`/`touch`, live `modify` plans | [docs/observability.md](docs/observability.md) |
|
|
169
169
|
| Image cache, private registries, snapshots | [docs/images.md](docs/images.md) |
|
|
170
|
-
| Runtime binaries gem, `msb` resolution order, backends, environment variables | [docs/runtime.md](docs/runtime.md) |
|
|
170
|
+
| Runtime binaries gem, `msb` resolution order, backends, forking, environment variables | [docs/runtime.md](docs/runtime.md) |
|
|
171
171
|
| Error classes and stable codes | [docs/errors.md](docs/errors.md) |
|
|
172
172
|
| Complete supported surface and what is not yet exposed | [docs/surface.md](docs/surface.md) |
|
|
173
173
|
| Release process, release preflight, binaries gems | [docs/releasing.md](docs/releasing.md) |
|
|
@@ -209,7 +209,7 @@ share the upstream tag, but the numbers have diverged since and the gem version
|
|
|
209
209
|
is **not** an indicator of the embedded runtime. Ask the build instead:
|
|
210
210
|
|
|
211
211
|
```ruby
|
|
212
|
-
Microsandbox::VERSION # => "0.17.
|
|
212
|
+
Microsandbox::VERSION # => "0.17.3" (the gem's own version)
|
|
213
213
|
Microsandbox.runtime_version # => "v0.6.18" (the embedded upstream runtime tag)
|
|
214
214
|
```
|
|
215
215
|
|
|
@@ -247,6 +247,7 @@ constants against this gem's. Every release records its runtime in
|
|
|
247
247
|
| `0.17.0` | `v0.6.18` | adopts upstream `v0.6.17`+`v0.6.18` step by step: outbound SOCKS4/SOCKS5 proxies (SOCKS5 UDP + credentials), migration-order fix for databases last opened by a `v0.6.15` `msb`, security hardening — plain-HTTP `Host`/`:authority` now checked against domain-rule policies even in non-strict mode, plus fail-closed strict hostname mode. Parity: `proxy:` (`Microsandbox::OutboundProxy` / `SecretSource`), `strict:` |
|
|
248
248
|
| `0.17.1` | `v0.6.18` | gem-only security and hardening release: relative host paths pinned at create time (GHSA-mcw9-9rf6-p4rm), `Volume.fs` path validation, `Cargo.lock` advisory updates, argument-guard and cleanup fixes, `SECURITY.md` |
|
|
249
249
|
| `0.17.2` | `v0.6.18` | gem-only fix and release-pipeline hardening release: an interrupted native call no longer loses its result (a sandbox the call owns is stopped on `Timeout`/`Thread#kill`), release preflight, SHA-pinned workflow actions and `cargo deny` advisory checks, RBS and docs contract specs |
|
|
250
|
+
| `0.17.3` | `v0.6.18` | gem-only fix release, fork safety: a forked child (Puma, Resque, Solid Queue) rebuilds its backend and runtime on its first SDK call instead of hanging or crashing, a handle created before the fork raises `ForkedHandleError` in the child, and nothing inherited from the parent is dropped there |
|
|
250
251
|
|
|
251
252
|
## Development
|
|
252
253
|
|
data/docs/errors.md
CHANGED
|
@@ -56,6 +56,7 @@ options) raises a plain `ArgumentError` before the native layer is reached.
|
|
|
56
56
|
| `UnsupportedOperationError` | `unsupported-operation` | The runtime version does not support the operation |
|
|
57
57
|
| `CloudHttpError` | `cloud-http` | Cloud backend HTTP failure |
|
|
58
58
|
| `UnsupportedError` | `unsupported` | The selected backend does not implement the operation; carries `#operation` (e.g. `"sandbox.kill"`) and `#hint` |
|
|
59
|
+
| `ForkedHandleError` | `forked-handle` | A handle was used in a process forked from the one that created it; open a new one in the child (see [runtime.md](runtime.md#forking-puma-resque-solid-queue)) |
|
|
59
60
|
|
|
60
61
|
The snapshot classes go beyond the Python SDK (which collapses them into its
|
|
61
62
|
base error) and match the Go SDK's per-variant coverage, so callers can rescue
|
data/docs/runtime.md
CHANGED
|
@@ -104,6 +104,91 @@ operations (create/start/stop/remove/get/list, one-shot exec, follow log
|
|
|
104
104
|
streaming); unsupported operations raise `Microsandbox::UnsupportedError`
|
|
105
105
|
(with `#operation` and `#hint`).
|
|
106
106
|
|
|
107
|
+
## Forking (Puma, Resque, Solid Queue)
|
|
108
|
+
|
|
109
|
+
Forking a process that has already used the SDK is supported: preload the
|
|
110
|
+
application, use the SDK in the master, fork workers, use it in the workers.
|
|
111
|
+
The first SDK call in a child sets up its own runtime threads and its own
|
|
112
|
+
backend (a new catalog connection pool, or a new HTTP client for the cloud
|
|
113
|
+
backend), chosen the same way as the parent's: an explicit
|
|
114
|
+
`set_default_backend`, an enclosing `with_backend` block, or — when neither
|
|
115
|
+
was used — the environment and config file as the child sees them at that
|
|
116
|
+
call. Everything addressed by name works in the child from then on:
|
|
117
|
+
`Sandbox.create`/`get`/`list`/`start`, `Volume`, `Image`, `Snapshot`.
|
|
118
|
+
|
|
119
|
+
If the parent's choice cannot be made again in the child (say the child's
|
|
120
|
+
environment now selects the cloud backend without credentials), SDK calls
|
|
121
|
+
there raise the configuration error until that is repaired. Selecting a
|
|
122
|
+
backend in the child — `set_default_backend`, or entering `with_backend` —
|
|
123
|
+
does not depend on the inherited choice and works regardless.
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
Microsandbox::Sandbox.list # the parent has used the SDK
|
|
127
|
+
|
|
128
|
+
pid = fork do
|
|
129
|
+
Microsandbox::Sandbox.create("job-#{Process.pid}", image: "alpine") do |sb|
|
|
130
|
+
puts sb.exec("echo", ["hello from the worker"]).stdout
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
Process.wait(pid)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
What does **not** cross a fork is an object created before it. A `Sandbox`,
|
|
137
|
+
`SandboxHandle`, `ExecHandle`, `ExecStdin`, `PullSession`, log/metrics/file
|
|
138
|
+
stream, `SshClient`/`SftpClient`/`SshServer`, `AgentClient` or `VolumeFs`
|
|
139
|
+
belongs to the process that created it. In a forked child, any call that acts
|
|
140
|
+
on one raises `Microsandbox::ForkedHandleError` (the message names both
|
|
141
|
+
pids); the original keeps working in the parent. Reading `name` or `id` — and
|
|
142
|
+
the snapshot fields of a `SandboxHandle` — still works, so a child can open
|
|
143
|
+
its own handle:
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
sandbox = Microsandbox::Sandbox.create("shared", image: "alpine")
|
|
147
|
+
|
|
148
|
+
pid = fork do
|
|
149
|
+
begin
|
|
150
|
+
sandbox.exec("true") # the parent's handle
|
|
151
|
+
rescue Microsandbox::ForkedHandleError => e
|
|
152
|
+
warn e.message
|
|
153
|
+
end
|
|
154
|
+
mine = Microsandbox::Sandbox.get(sandbox.name).connect_or_start
|
|
155
|
+
puts mine.exec("echo", ["hello"]).stdout
|
|
156
|
+
end
|
|
157
|
+
Process.wait(pid)
|
|
158
|
+
sandbox.stop
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Things to know:
|
|
162
|
+
|
|
163
|
+
- **Cleanup code inherited by the child.** The block forms
|
|
164
|
+
(`Sandbox.create(...) { }`, `connect_or_create`) swallow the
|
|
165
|
+
`ForkedHandleError` their teardown hits when a forked child leaves the
|
|
166
|
+
block, so the child does not stop the parent's sandbox. An `ensure
|
|
167
|
+
sandbox.stop` of your own raises it in the child; rescue it there, or run
|
|
168
|
+
the cleanup only in the process that created the sandbox.
|
|
169
|
+
- **Leaving a `with_backend` block in the child.** When the backend to
|
|
170
|
+
restore was the parent's — the child was forked inside the block, or the
|
|
171
|
+
block is the child's first use of the SDK — leaving the block installs a
|
|
172
|
+
backend chosen the way that one had been. The block's own temporary backend
|
|
173
|
+
is not needed for that: if only it can no longer be chosen in the child (a
|
|
174
|
+
cloud profile that is gone there), leaving the block still restores the
|
|
175
|
+
previous backend. If the choice to restore no longer resolves in the child,
|
|
176
|
+
the block is closed anyway and its temporary selection stays in effect.
|
|
177
|
+
`with_backend` then raises the configuration error — unless the block
|
|
178
|
+
itself raised, in which case the block's exception propagates and the
|
|
179
|
+
restore failure is reported as a warning.
|
|
180
|
+
- **A child keeps the parent's sandboxes alive.** A child inherits the
|
|
181
|
+
parent's open file descriptors, among them the pipe through which a sandbox
|
|
182
|
+
notices that its owner is gone. If the parent exits while such a child is
|
|
183
|
+
still running, the sandbox is not torn down until that child exits as well.
|
|
184
|
+
Fork before creating sandboxes, or create them in the children.
|
|
185
|
+
- **Do not fork while another thread is inside an SDK call.** That thread
|
|
186
|
+
does not exist in the child, and a lock it held at that instant stays
|
|
187
|
+
locked there.
|
|
188
|
+
- **Memory.** A child leaks, once, the native state it inherited (the
|
|
189
|
+
parent's runtime, backend and whichever inherited handles it collects)
|
|
190
|
+
instead of destroying it — destroying it is what used to crash.
|
|
191
|
+
|
|
107
192
|
## One runtime line per home
|
|
108
193
|
|
|
109
194
|
The runtime home (`MSB_HOME`, default `~/.microsandbox`) belongs to one
|
data/docs/surface.md
CHANGED
|
@@ -57,6 +57,9 @@ counterpart, and [DESIGN.md](../DESIGN.md) explains how the layers fit.
|
|
|
57
57
|
- **Registries** — `registry_auth`/`registry_insecure`/`registry_ca_certs` on
|
|
58
58
|
`create`.
|
|
59
59
|
- **Errors** — the typed hierarchy in [errors.md](errors.md).
|
|
60
|
+
- **Process model** — the SDK may be used before and after `fork`; objects
|
|
61
|
+
created before a fork are refused in the child with `ForkedHandleError`
|
|
62
|
+
(see [runtime.md](runtime.md#forking-puma-resque-solid-queue)).
|
|
60
63
|
|
|
61
64
|
Create options span resources (`cpus`/`memory`/`max_cpus`/`max_memory`),
|
|
62
65
|
`env`/`workdir`/`labels`/`user`/`hostname`/`shell`, named `scripts`, the image
|
data/ext/microsandbox/Cargo.toml
CHANGED
|
@@ -8,7 +8,7 @@ description = "Ruby SDK native extension for microsandbox — secure, fast micro
|
|
|
8
8
|
# returns this via env!("CARGO_PKG_VERSION") and version_spec.rb asserts equality.
|
|
9
9
|
# The core-crate dependencies below stay on their own tag, which is
|
|
10
10
|
# Microsandbox::RUNTIME_VERSION and moves independently of this version.
|
|
11
|
-
version = "0.17.
|
|
11
|
+
version = "0.17.3"
|
|
12
12
|
authors = ["Super Rad Company <development@superrad.company>"]
|
|
13
13
|
repository = "https://github.com/superradcompany/microsandbox"
|
|
14
14
|
license = "Apache-2.0"
|
|
@@ -14,7 +14,7 @@ use microsandbox::agent::AgentClient as CoreAgentClient;
|
|
|
14
14
|
use microsandbox::{AgentBridge, BridgeFrame, MicrosandboxError};
|
|
15
15
|
|
|
16
16
|
use crate::error;
|
|
17
|
-
use crate::runtime::{block_on, ruby};
|
|
17
|
+
use crate::runtime::{block_on, ruby, ForkGuarded};
|
|
18
18
|
|
|
19
19
|
/// Map an agent-client error onto the Ruby exception hierarchy (via the core
|
|
20
20
|
/// `MicrosandboxError::AgentClient` wrapper, exactly like the Python binding).
|
|
@@ -24,13 +24,13 @@ fn to_ruby_agent(err: microsandbox::AgentClientError) -> Error {
|
|
|
24
24
|
|
|
25
25
|
#[magnus::wrap(class = "Microsandbox::Native::AgentClient", free_immediately, size)]
|
|
26
26
|
pub struct AgentClient {
|
|
27
|
-
inner: Arc<AgentBridge
|
|
27
|
+
inner: ForkGuarded<Arc<AgentBridge>>,
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
impl AgentClient {
|
|
31
31
|
fn from_bridge(bridge: AgentBridge) -> Self {
|
|
32
32
|
Self {
|
|
33
|
-
inner: Arc::new(bridge),
|
|
33
|
+
inner: ForkGuarded::new("Microsandbox::AgentClient", Arc::new(bridge)),
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -60,6 +60,9 @@ impl AgentClient {
|
|
|
60
60
|
|
|
61
61
|
/// Resolve a sandbox's agent relay socket path without connecting.
|
|
62
62
|
fn socket_path(name: String) -> Result<String, Error> {
|
|
63
|
+
// Synchronous, and the core derives the path from the default
|
|
64
|
+
// backend's config.
|
|
65
|
+
crate::backend::ensure_this_process_backend()?;
|
|
63
66
|
let path = CoreAgentClient::socket_path(&name).map_err(error::to_ruby)?;
|
|
64
67
|
Ok(path.to_string_lossy().into_owned())
|
|
65
68
|
}
|
|
@@ -71,7 +74,7 @@ impl AgentClient {
|
|
|
71
74
|
/// Send one frame and await a single response frame ({id, flags, body}).
|
|
72
75
|
fn request(&self, flags: u8, body: RString) -> Result<RHash, Error> {
|
|
73
76
|
let body = unsafe { body.as_slice() }.to_vec();
|
|
74
|
-
let inner = Arc::clone(
|
|
77
|
+
let inner = Arc::clone(self.inner.get()?);
|
|
75
78
|
let frame =
|
|
76
79
|
block_on(async move { inner.request(flags, body).await })?.map_err(to_ruby_agent)?;
|
|
77
80
|
Ok(frame_to_hash(frame))
|
|
@@ -80,7 +83,7 @@ impl AgentClient {
|
|
|
80
83
|
/// Open a streaming session; returns {id, handle}.
|
|
81
84
|
fn stream_open(&self, flags: u8, body: RString) -> Result<RHash, Error> {
|
|
82
85
|
let body = unsafe { body.as_slice() }.to_vec();
|
|
83
|
-
let inner = Arc::clone(
|
|
86
|
+
let inner = Arc::clone(self.inner.get()?);
|
|
84
87
|
let (id, handle) = block_on(async move { inner.stream_open(flags, body).await })?
|
|
85
88
|
.map_err(to_ruby_agent)?;
|
|
86
89
|
let hash = ruby().hash_new();
|
|
@@ -91,7 +94,7 @@ impl AgentClient {
|
|
|
91
94
|
|
|
92
95
|
/// Pull the next frame from a stream; nil at end-of-stream.
|
|
93
96
|
fn stream_next(&self, handle: u64) -> Result<Option<RHash>, Error> {
|
|
94
|
-
let inner = Arc::clone(
|
|
97
|
+
let inner = Arc::clone(self.inner.get()?);
|
|
95
98
|
let frame =
|
|
96
99
|
block_on(async move { inner.stream_next(handle).await })?.map_err(to_ruby_agent)?;
|
|
97
100
|
Ok(frame.map(frame_to_hash))
|
|
@@ -99,7 +102,7 @@ impl AgentClient {
|
|
|
99
102
|
|
|
100
103
|
/// Close a stream handle. Idempotent.
|
|
101
104
|
fn stream_close(&self, handle: u64) -> Result<(), Error> {
|
|
102
|
-
let inner = Arc::clone(
|
|
105
|
+
let inner = Arc::clone(self.inner.get()?);
|
|
103
106
|
block_on(async move { inner.stream_close(handle).await })?;
|
|
104
107
|
Ok(())
|
|
105
108
|
}
|
|
@@ -107,19 +110,19 @@ impl AgentClient {
|
|
|
107
110
|
/// Send a follow-up frame on an existing correlation id.
|
|
108
111
|
fn send(&self, id: u32, flags: u8, body: RString) -> Result<(), Error> {
|
|
109
112
|
let body = unsafe { body.as_slice() }.to_vec();
|
|
110
|
-
let inner = Arc::clone(
|
|
113
|
+
let inner = Arc::clone(self.inner.get()?);
|
|
111
114
|
block_on(async move { inner.send(id, flags, body).await })?.map_err(to_ruby_agent)
|
|
112
115
|
}
|
|
113
116
|
|
|
114
117
|
/// Cached handshake `core.ready` frame body bytes (CBOR).
|
|
115
118
|
fn ready_bytes(&self) -> Result<RString, Error> {
|
|
116
|
-
let bytes = self.inner.ready_bytes().map_err(to_ruby_agent)?;
|
|
119
|
+
let bytes = self.inner.get()?.ready_bytes().map_err(to_ruby_agent)?;
|
|
117
120
|
Ok(ruby().str_from_slice(&bytes))
|
|
118
121
|
}
|
|
119
122
|
|
|
120
123
|
/// Close the connection. Idempotent.
|
|
121
124
|
fn close(&self) -> Result<(), Error> {
|
|
122
|
-
let inner = Arc::clone(
|
|
125
|
+
let inner = Arc::clone(self.inner.get()?);
|
|
123
126
|
block_on(async move { inner.close().await })?;
|
|
124
127
|
Ok(())
|
|
125
128
|
}
|