microsandbox-rb 0.17.1 → 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 +138 -8
- data/Cargo.lock +1 -1
- data/DESIGN.md +183 -36
- data/README.md +8 -7
- data/SECURITY.md +1 -1
- data/docs/errors.md +1 -0
- data/docs/execution.md +8 -1
- data/docs/lifecycle.md +33 -0
- data/docs/releasing.md +85 -34
- data/docs/runtime.md +88 -1
- data/docs/surface.md +16 -8
- data/ext/microsandbox/Cargo.toml +9 -5
- data/ext/microsandbox/src/agent.rs +26 -21
- data/ext/microsandbox/src/backend.rs +291 -34
- data/ext/microsandbox/src/error.rs +18 -0
- data/ext/microsandbox/src/exec.rs +39 -21
- data/ext/microsandbox/src/fs_stream.rs +30 -15
- data/ext/microsandbox/src/host_path.rs +4 -3
- data/ext/microsandbox/src/image.rs +2 -2
- data/ext/microsandbox/src/lib.rs +3 -2
- data/ext/microsandbox/src/runtime.rs +314 -47
- data/ext/microsandbox/src/sandbox.rs +225 -124
- data/ext/microsandbox/src/snapshot.rs +24 -15
- data/ext/microsandbox/src/ssh.rs +48 -39
- data/ext/microsandbox/src/stream.rs +19 -9
- data/ext/microsandbox/src/volume.rs +40 -32
- data/lib/microsandbox/errors.rb +8 -0
- data/lib/microsandbox/sandbox.rb +75 -4
- data/lib/microsandbox/version.rb +1 -1
- data/lib/microsandbox.rb +20 -1
- data/sig/microsandbox.rbs +25 -0
- metadata +1 -1
data/docs/releasing.md
CHANGED
|
@@ -7,54 +7,105 @@ or credential setup is needed.
|
|
|
7
7
|
|
|
8
8
|
## Each release
|
|
9
9
|
|
|
10
|
-
1. Bump the gem's **own** version
|
|
11
|
-
`[package] version` in `ext/microsandbox/Cargo.toml` (they must stay equal) —
|
|
12
|
-
on its independent semver track (see the
|
|
10
|
+
1. Bump the gem's **own** version on its independent semver track (see the
|
|
13
11
|
[Versioning](../README.md#versioning) section of the README); **don't** pick
|
|
14
|
-
the number to mirror the upstream tag.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
`
|
|
18
|
-
`
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
`
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
12
|
+
the number to mirror the upstream tag. The version is written in three
|
|
13
|
+
files that must stay equal (`spec/unit/version_spec.rb` asserts it), and
|
|
14
|
+
recorded in the lockfile:
|
|
15
|
+
- `lib/microsandbox/version.rb` — `Microsandbox::VERSION`;
|
|
16
|
+
- `ext/microsandbox/Cargo.toml` — the `[package] version`;
|
|
17
|
+
- `binaries/lib/microsandbox/binaries.rb` — `Microsandbox::Binaries::VERSION`
|
|
18
|
+
(the companion gem ships in lockstep);
|
|
19
|
+
- `Cargo.lock` — committed and packed into the gem, and it records the
|
|
20
|
+
extension crate's version, so a version bump must commit the regenerated
|
|
21
|
+
lock (`bundle exec rake compile` rewrites it).
|
|
22
|
+
|
|
23
|
+
If the release also adopts a new upstream runtime, move the runtime tag
|
|
24
|
+
everywhere it is named:
|
|
25
|
+
- `ext/microsandbox/Cargo.toml` — the `tag = "vX.Y.Z"` on **all three**
|
|
26
|
+
upstream git deps (`microsandbox`, `microsandbox-network`,
|
|
27
|
+
`microsandbox-runtime`);
|
|
28
|
+
- `lib/microsandbox/version.rb` — `Microsandbox::RUNTIME_VERSION`;
|
|
29
|
+
- `binaries/lib/microsandbox/binaries.rb` —
|
|
30
|
+
`Microsandbox::Binaries::RUNTIME_VERSION`;
|
|
31
|
+
- `binaries/checksums/<tag>.sha256` — the release's `checksums.sha256`,
|
|
32
|
+
committed (vendoring fails closed without it);
|
|
33
|
+
- `Cargo.lock` — regenerated again, since it pins the git deps to the new
|
|
34
|
+
tag's commit.
|
|
35
|
+
|
|
36
|
+
Then add a row to the Versioning table in the README and update
|
|
37
|
+
`CHANGELOG.md`.
|
|
38
|
+
2. Push a `vX.Y.Z` tag. Once the [release preflight](#release-preflight)
|
|
39
|
+
passes, CI pushes the **source gem** to RubyGems via
|
|
40
|
+
`rubygems/configure-rubygems-credentials` (OIDC, `id-token: write`) — no
|
|
25
41
|
`RUBYGEMS_API_KEY` secret required — then creates the GitHub Release.
|
|
26
42
|
|
|
43
|
+
## Release preflight
|
|
44
|
+
|
|
45
|
+
`release.yml`'s `preflight` job runs `.github/scripts/release_preflight.sh
|
|
46
|
+
<tag>` and the `publish` job needs it, so nothing reaches RubyGems unless:
|
|
47
|
+
|
|
48
|
+
- the tag is `vX.Y.Z` and the three version constants **in the tagged commit**
|
|
49
|
+
— `Microsandbox::VERSION`, `[package] version` in
|
|
50
|
+
`ext/microsandbox/Cargo.toml`, `Microsandbox::Binaries::VERSION` — all equal
|
|
51
|
+
`X.Y.Z`;
|
|
52
|
+
- the tagged commit is on `main`;
|
|
53
|
+
- the `CI` workflow's push run for that exact commit concluded `success`.
|
|
54
|
+
|
|
55
|
+
Pushing `main` and the tag together is fine: the preflight **waits** for that
|
|
56
|
+
CI run to appear and finish (polling every 30 s, up to 45 min —
|
|
57
|
+
`PREFLIGHT_CI_WAIT_SECONDS` / `PREFLIGHT_CI_POLL_SECONDS`), so a release
|
|
58
|
+
publishes only after CI, including the real-microVM integration lanes, is
|
|
59
|
+
green. A failed or timed-out preflight leaves the tag unpublished. How to
|
|
60
|
+
recover depends on whether the tag has to move:
|
|
61
|
+
|
|
62
|
+
- **The tagged commit is right** (CI flaked, or was still red): re-run the
|
|
63
|
+
`CI` run for that commit until it is green, then re-run the failed `Release`
|
|
64
|
+
run.
|
|
65
|
+
- **The tag must point at a different commit** (wrong version constants, not
|
|
66
|
+
on `main`): delete the tag, fix the commit, and push the tag again — that
|
|
67
|
+
starts a new `Release` run. Never re-run the old run after retagging: a
|
|
68
|
+
re-run keeps the commit it was started for, so it checks the old commit
|
|
69
|
+
again and fails the same way.
|
|
70
|
+
|
|
71
|
+
Dry-run it against an existing tag — nothing is published on a manual
|
|
72
|
+
dispatch — or run the script locally from a full clone:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
gh workflow run release.yml --ref main -f preflight_ref=vX.Y.Z
|
|
76
|
+
GH_TOKEN=$(gh auth token) .github/scripts/release_preflight.sh vX.Y.Z
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
It is a workflow-level gate; branch protection and a protected environment for
|
|
80
|
+
the trusted publisher are separate repository settings.
|
|
81
|
+
|
|
82
|
+
CI's `lint` job also runs `cargo deny check advisories` against `Cargo.lock`;
|
|
83
|
+
the exceptions (advisories with no fixed release at the pinned runtime) are in
|
|
84
|
+
`deny.toml` at the repository root — re-check them on every runtime adoption.
|
|
85
|
+
|
|
27
86
|
## Precompiled per-platform gems
|
|
28
87
|
|
|
29
|
-
Precompiled *extension* gems are built
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
prove otherwise — so promotion is manual after validating the artifact on each
|
|
35
|
-
platform. A precompiled gem ships the compiled extension (with the guest
|
|
36
|
-
`agentd` baked in by *target* arch); the host-side `msb` + `libkrunfw` runtime
|
|
37
|
-
is **not** in it — that comes from the companion `microsandbox-rb-binaries`
|
|
38
|
-
gem, or, when that isn't installed, is fetched into `~/.microsandbox` on first
|
|
39
|
-
use by `Microsandbox.ensure_runtime!` (libkrunfw is `dlopen`'d by `msb` at
|
|
40
|
-
runtime, never linked into the gem). The real cross-compile work is linking the
|
|
41
|
-
*target* native libraries — `libcap-ng` on Linux (handled via Debian multiarch
|
|
42
|
-
in the workflow) and the Hypervisor + Security frameworks on macOS (via
|
|
43
|
-
osxcross; the one platform left to confirm). Until promoted, users install the
|
|
44
|
-
source gem (which compiles via `rb_sys`).
|
|
88
|
+
Precompiled *extension* gems are not built or published. Users install the
|
|
89
|
+
source gem, which compiles the extension via `rb_sys` at install time (so a
|
|
90
|
+
stable Rust toolchain is required on the installing host). The companion
|
|
91
|
+
`microsandbox-rb-binaries` gems below are unaffected: they carry the host
|
|
92
|
+
runtime, not the extension.
|
|
45
93
|
|
|
46
94
|
## Runtime binaries gems
|
|
47
95
|
|
|
48
96
|
`microsandbox-rb-binaries` (source in `binaries/`) is a *separate* artifact from
|
|
49
|
-
the
|
|
97
|
+
the SDK gem: it carries no Ruby extension, only the
|
|
50
98
|
upstream `msb` + `libkrunfw` for one platform, verified against the release's
|
|
51
99
|
published `checksums.sha256` when vendored. CI's `binaries` job vendors and
|
|
52
100
|
builds all three (`arm64-darwin`, `x86_64-linux-gnu`, `aarch64-linux-gnu`) on
|
|
53
101
|
every run and smoke-tests the host one; `release.yml`'s `binaries-gems` job
|
|
54
102
|
does the same on a tag and a separate `publish-binaries` job pushes them after
|
|
55
|
-
the SDK gem is live
|
|
56
|
-
|
|
57
|
-
|
|
103
|
+
the SDK gem is live. The SDK `publish` job needs `binaries-gems`, so a
|
|
104
|
+
companion **build** failure blocks the SDK publish as well. A companion
|
|
105
|
+
**push** failure is its own red job once the SDK gem is live: the release is
|
|
106
|
+
then incomplete until `publish-binaries` is re-run. They use their own RubyGems
|
|
107
|
+
trusted-publisher entry (same repo + workflow, gem name
|
|
108
|
+
`microsandbox-rb-binaries`). To build them by hand:
|
|
58
109
|
|
|
59
110
|
```bash
|
|
60
111
|
rake -C binaries vendor[arm64-darwin] # download + sha256-verify into binaries/vendor
|
data/docs/runtime.md
CHANGED
|
@@ -85,7 +85,9 @@ Microsandbox.set_default_backend(:cloud, url: "https://api.example.com", api_key
|
|
|
85
85
|
Microsandbox.set_default_backend(:cloud, profile: "prod")
|
|
86
86
|
|
|
87
87
|
# Scoped override (restored afterward, even on error):
|
|
88
|
-
Microsandbox.with_backend(:local)
|
|
88
|
+
Microsandbox.with_backend(:local) do
|
|
89
|
+
Microsandbox::Sandbox.create("box", image: "alpine") { |sb| puts sb.shell("uname -a").stdout }
|
|
90
|
+
end
|
|
89
91
|
```
|
|
90
92
|
|
|
91
93
|
Resolution order when no backend is set programmatically: `MSB_BACKEND`
|
|
@@ -102,6 +104,91 @@ operations (create/start/stop/remove/get/list, one-shot exec, follow log
|
|
|
102
104
|
streaming); unsupported operations raise `Microsandbox::UnsupportedError`
|
|
103
105
|
(with `#operation` and `#hint`).
|
|
104
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
|
+
|
|
105
192
|
## One runtime line per home
|
|
106
193
|
|
|
107
194
|
The runtime home (`MSB_HOME`, default `~/.microsandbox`) belongs to one
|
data/docs/surface.md
CHANGED
|
@@ -23,8 +23,10 @@ counterpart, and [DESIGN.md](../DESIGN.md) explains how the layers fit.
|
|
|
23
23
|
`write_stream`.
|
|
24
24
|
- **Observability** — per-sandbox `metrics`, `Microsandbox.all_sandbox_metrics`,
|
|
25
25
|
streaming `metrics_stream`/`log_stream`, `logs`, `ping`/`touch`.
|
|
26
|
-
- **Live modification** — `modify` with `dry_run
|
|
27
|
-
resize, root-disk growth
|
|
26
|
+
- **Live modification** — `modify` with `dry_run:` and `policy:`: CPU/memory
|
|
27
|
+
resize (`cpus`/`max_cpus`/`memory`/`max_memory`), root-disk growth
|
|
28
|
+
(`root_disk_size`, MiB), `env`/`remove_env`, `labels`/`remove_labels`,
|
|
29
|
+
`workdir`, and secret rotation (`secrets`/`remove_secrets`).
|
|
28
30
|
- **OCI image cache** — `Image.get`/`list`/`inspect`/`remove`/`prune`, image
|
|
29
31
|
archives (`Image.load`/`Image.save`), streaming pull progress
|
|
30
32
|
(`Sandbox.create_with_progress` → `PullSession`).
|
|
@@ -35,14 +37,16 @@ counterpart, and [DESIGN.md](../DESIGN.md) explains how the layers fit.
|
|
|
35
37
|
- **Rootfs patches** — `Microsandbox::Patch` (text/file/append/copy_file/
|
|
36
38
|
copy_dir/symlink/mkdir/remove) via `create(patches:)`.
|
|
37
39
|
- **Root disk** — structured `root_disk:` (managed / tmpfs / disk / flat via
|
|
38
|
-
`Microsandbox::RootDisk`), `follow_root_symlinks
|
|
40
|
+
`Microsandbox::RootDisk`), `follow_root_symlinks:`; `oci_upper_size:` is the
|
|
41
|
+
deprecated alias for a managed `root_disk:` size.
|
|
39
42
|
- **Network configuration** — composable profiles, custom per-rule
|
|
40
|
-
`Microsandbox::NetworkPolicy`/`Rule`/`Destination`, DNS
|
|
41
|
-
|
|
43
|
+
`Microsandbox::NetworkPolicy`/`Rule`/`Destination`, custom DNS (`dns:`),
|
|
44
|
+
TLS-interception tuning (`tls:`), guest address pools (`ipv4_pool:`/
|
|
45
|
+
`ipv6_pool:`), `max_connections`, `trust_host_cas`, `strict` hostname mode,
|
|
42
46
|
outbound SOCKS4/SOCKS5 `proxy:` (`OutboundProxy`/`SecretSource`),
|
|
43
47
|
`rate_limiter:`, `vsock:`.
|
|
44
48
|
- **Secrets** — multi-host / wildcard allow-lists, injection toggles,
|
|
45
|
-
per-secret and sandbox-level
|
|
49
|
+
per-secret `on_violation:` and sandbox-level `on_secret_violation:` policy.
|
|
46
50
|
- **SSH** — `Sandbox#ssh` → `SshClient`/`SftpClient`/`SshServer`, with
|
|
47
51
|
`inactivity_timeout:`.
|
|
48
52
|
- **Raw agent client** — `Microsandbox::AgentClient` → `AgentStream`/
|
|
@@ -53,11 +57,15 @@ counterpart, and [DESIGN.md](../DESIGN.md) explains how the layers fit.
|
|
|
53
57
|
- **Registries** — `registry_auth`/`registry_insecure`/`registry_ca_certs` on
|
|
54
58
|
`create`.
|
|
55
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)).
|
|
56
63
|
|
|
57
64
|
Create options span resources (`cpus`/`memory`/`max_cpus`/`max_memory`),
|
|
58
|
-
`env`/`workdir`/`labels`/`user`/`hostname`/`shell`, `
|
|
65
|
+
`env`/`workdir`/`labels`/`user`/`hostname`/`shell`, named `scripts`, the image
|
|
66
|
+
`entrypoint`/`cmd` overrides, `init`/`ephemeral`,
|
|
59
67
|
disk-image `fstype`, `ports`/`ports_udp`, network policy + config,
|
|
60
|
-
`log_level`/`security`/`rlimits`/`pull_policy`/`secrets`/`patches`/`volumes`
|
|
68
|
+
`log_level`/`quiet_logs`/`security`/`rlimits`/`pull_policy`/`secrets`/`patches`/`volumes`
|
|
61
69
|
(bind/named/tmpfs/disk with mount policies, `uid`/`gid`, `quota_mib`),
|
|
62
70
|
`idle_timeout`/`max_duration`, `from_snapshot`, `detached`, and
|
|
63
71
|
`replace`/`replace_with_timeout`.
|
data/ext/microsandbox/Cargo.toml
CHANGED
|
@@ -6,8 +6,9 @@ name = "microsandbox_rb"
|
|
|
6
6
|
description = "Ruby SDK native extension for microsandbox — secure, fast microVM-based sandboxing."
|
|
7
7
|
# Must equal Microsandbox::VERSION (lib/microsandbox/version.rb) — Native.version
|
|
8
8
|
# returns this via env!("CARGO_PKG_VERSION") and version_spec.rb asserts equality.
|
|
9
|
-
# The core-crate
|
|
10
|
-
|
|
9
|
+
# The core-crate dependencies below stay on their own tag, which is
|
|
10
|
+
# Microsandbox::RUNTIME_VERSION and moves independently of this version.
|
|
11
|
+
version = "0.17.3"
|
|
11
12
|
authors = ["Super Rad Company <development@superrad.company>"]
|
|
12
13
|
repository = "https://github.com/superradcompany/microsandbox"
|
|
13
14
|
license = "Apache-2.0"
|
|
@@ -22,9 +23,12 @@ name = "microsandbox_rb"
|
|
|
22
23
|
crate-type = ["cdylib"]
|
|
23
24
|
|
|
24
25
|
[dependencies]
|
|
25
|
-
magnus
|
|
26
|
-
#
|
|
27
|
-
|
|
26
|
+
# "rb-sys" exposes `magnus::rb_sys::protect` (rb_protect), which the GVL bridge
|
|
27
|
+
# uses to check interrupts without letting Ruby longjmp over Rust frames.
|
|
28
|
+
magnus = { version = "0.8", features = ["rb-sys"] }
|
|
29
|
+
# Low-level Ruby C API — used directly for rb_thread_call_without_gvl2 (GVL
|
|
30
|
+
# release during blocking sandbox calls) and rb_thread_check_ints. Kept in
|
|
31
|
+
# lock-step with magnus's own rb-sys.
|
|
28
32
|
rb-sys = "0.9"
|
|
29
33
|
|
|
30
34
|
# The microsandbox core runtime, wrapped via FFI exactly like the official
|
|
@@ -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
|
|
|
@@ -41,8 +41,8 @@ impl AgentClient {
|
|
|
41
41
|
/// Connect to a running sandbox by name. `timeout` is optional seconds.
|
|
42
42
|
fn connect_sandbox(name: String, timeout: Option<f64>) -> Result<AgentClient, Error> {
|
|
43
43
|
let bridge = match dur(timeout)? {
|
|
44
|
-
Some(t) => block_on(AgentBridge::connect_sandbox_with_timeout(&name, t))
|
|
45
|
-
None => block_on(AgentBridge::connect_sandbox(&name))
|
|
44
|
+
Some(t) => block_on(AgentBridge::connect_sandbox_with_timeout(&name, t))?,
|
|
45
|
+
None => block_on(AgentBridge::connect_sandbox(&name))?,
|
|
46
46
|
}
|
|
47
47
|
.map_err(to_ruby_agent)?;
|
|
48
48
|
Ok(AgentClient::from_bridge(bridge))
|
|
@@ -51,8 +51,8 @@ impl AgentClient {
|
|
|
51
51
|
/// Connect to an agentd relay socket by path. `timeout` is optional seconds.
|
|
52
52
|
fn connect_path(path: String, timeout: Option<f64>) -> Result<AgentClient, Error> {
|
|
53
53
|
let bridge = match dur(timeout)? {
|
|
54
|
-
Some(t) => block_on(AgentBridge::connect_path_with_timeout(&path, t))
|
|
55
|
-
None => block_on(AgentBridge::connect_path(&path))
|
|
54
|
+
Some(t) => block_on(AgentBridge::connect_path_with_timeout(&path, t))?,
|
|
55
|
+
None => block_on(AgentBridge::connect_path(&path))?,
|
|
56
56
|
}
|
|
57
57
|
.map_err(to_ruby_agent)?;
|
|
58
58
|
Ok(AgentClient::from_bridge(bridge))
|
|
@@ -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,18 +74,18 @@ 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
|
-
block_on(async move { inner.request(flags, body).await })
|
|
79
|
+
block_on(async move { inner.request(flags, body).await })?.map_err(to_ruby_agent)?;
|
|
77
80
|
Ok(frame_to_hash(frame))
|
|
78
81
|
}
|
|
79
82
|
|
|
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(
|
|
84
|
-
let (id, handle) =
|
|
85
|
-
|
|
86
|
+
let inner = Arc::clone(self.inner.get()?);
|
|
87
|
+
let (id, handle) = block_on(async move { inner.stream_open(flags, body).await })?
|
|
88
|
+
.map_err(to_ruby_agent)?;
|
|
86
89
|
let hash = ruby().hash_new();
|
|
87
90
|
hash.aset("id", id)?;
|
|
88
91
|
hash.aset("handle", handle)?;
|
|
@@ -91,36 +94,36 @@ 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
|
-
block_on(async move { inner.stream_next(handle).await })
|
|
99
|
+
block_on(async move { inner.stream_next(handle).await })?.map_err(to_ruby_agent)?;
|
|
97
100
|
Ok(frame.map(frame_to_hash))
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
/// Close a stream handle. Idempotent.
|
|
101
104
|
fn stream_close(&self, handle: u64) -> Result<(), Error> {
|
|
102
|
-
let inner = Arc::clone(
|
|
103
|
-
block_on(async move { inner.stream_close(handle).await })
|
|
105
|
+
let inner = Arc::clone(self.inner.get()?);
|
|
106
|
+
block_on(async move { inner.stream_close(handle).await })?;
|
|
104
107
|
Ok(())
|
|
105
108
|
}
|
|
106
109
|
|
|
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(
|
|
111
|
-
block_on(async move { inner.send(id, flags, body).await })
|
|
113
|
+
let inner = Arc::clone(self.inner.get()?);
|
|
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(
|
|
123
|
-
block_on(async move { inner.close().await })
|
|
125
|
+
let inner = Arc::clone(self.inner.get()?);
|
|
126
|
+
block_on(async move { inner.close().await })?;
|
|
124
127
|
Ok(())
|
|
125
128
|
}
|
|
126
129
|
}
|
|
@@ -157,6 +160,8 @@ fn frame_to_hash(frame: BridgeFrame) -> RHash {
|
|
|
157
160
|
|
|
158
161
|
pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
159
162
|
let class = native.define_class("AgentClient", ruby.class_object())?;
|
|
163
|
+
// Resolve the wrapper class now, not on first use (see `preload_class`).
|
|
164
|
+
crate::runtime::preload_class::<AgentClient>(ruby);
|
|
160
165
|
|
|
161
166
|
class.define_singleton_method(
|
|
162
167
|
"connect_sandbox",
|