microsandbox-rb 0.8.1 → 0.8.2
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 +126 -1
- data/Cargo.lock +1 -1
- data/DESIGN.md +22 -4
- data/README.md +9 -1
- data/ext/microsandbox/Cargo.toml +1 -1
- data/ext/microsandbox/extconf.rb +33 -14
- data/ext/microsandbox/src/error.rs +16 -0
- data/ext/microsandbox/src/sandbox.rs +32 -9
- data/lib/microsandbox/agent.rb +4 -0
- data/lib/microsandbox/errors.rb +15 -0
- data/lib/microsandbox/exec_handle.rb +7 -0
- data/lib/microsandbox/fs.rb +5 -0
- data/lib/microsandbox/sandbox.rb +25 -3
- data/lib/microsandbox/ssh.rb +15 -4
- data/lib/microsandbox/streams.rb +13 -0
- data/lib/microsandbox/version.rb +1 -1
- data/lib/microsandbox.rb +9 -5
- data/sig/microsandbox.rbs +14 -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: 9fa6d9703f579a81424b9a32d4ab7c066eb5a48c1f8f7407ae2f6ca98770fd85
|
|
4
|
+
data.tar.gz: 6f83fea6ecf71906786bbb2416d6ee3ad52a588077024a5da0eed71eb49430ef
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e18a042c506b1d501f072816c6bdd20f429c52a18410112cb82fb9fc97981d2ce38f53dc02b4024627c64a7dc6b643060c0d67c0dd073500b91a83a3d7c6319b
|
|
7
|
+
data.tar.gz: b7b473d7e9d5fc5321abd6f9e2348ed9f0e75d1a924e64fa2e19580eb78e40230cddbdc310132ffd9248c984e6684293868956abb365a72ca3dcf4d3b828f384
|
data/CHANGELOG.md
CHANGED
|
@@ -6,7 +6,132 @@ All notable changes to this gem are documented here. The format is based on
|
|
|
6
6
|
microsandbox runtime it embeds; each release notes the upstream runtime tag it
|
|
7
7
|
wraps, and the README's Versioning section keeps the full gem→runtime map.
|
|
8
8
|
|
|
9
|
-
## [
|
|
9
|
+
## [0.8.2] - 2026-06-29
|
|
10
|
+
|
|
11
|
+
Gem-only release on the `v0.5.10` runtime (unchanged). Bundles the post-`0.8.1`
|
|
12
|
+
audit follow-ups: a secret-leak fix, typed snapshot errors, panic-free duration
|
|
13
|
+
parsing, the precompiled fat-gem loader + `extconf` preflight corrections, and a
|
|
14
|
+
sweep of threading/streaming/SSH documentation.
|
|
15
|
+
|
|
16
|
+
### Documentation
|
|
17
|
+
|
|
18
|
+
- **Calling-thread non-preemption is now documented** (issue #24). The GVL is
|
|
19
|
+
released during native calls so *other* threads keep running, but the
|
|
20
|
+
*calling* thread blocks uninterruptibly until the call returns —
|
|
21
|
+
`Timeout::timeout`/`Thread#kill`/Ctrl-C can't interrupt it. README and
|
|
22
|
+
DESIGN.md now state this and steer callers to the genuinely-bounding
|
|
23
|
+
`exec(timeout:)` / `shell(timeout:)` knobs, and clarify that
|
|
24
|
+
`AgentClient.connect_sandbox`/`connect_path`'s `timeout:` bounds only the
|
|
25
|
+
connect handshake while `AgentClient#request`/`#stream` and the streaming
|
|
26
|
+
paths have no timeout knob and can block indefinitely — rather than reaching
|
|
27
|
+
for `Timeout::timeout`.
|
|
28
|
+
- **`exec` `timeout: 0` semantics clarified** (issue #29). The `@param timeout`
|
|
29
|
+
doc now notes the asymmetry: omit or `nil` means *no* timeout, while `0` is an
|
|
30
|
+
immediate (zero) deadline that kills the command before any output and raises
|
|
31
|
+
`ExecTimeoutError` — so use `nil`/omit, never `0`, for "no limit". Also noted
|
|
32
|
+
that `exec_stream`/`shell_stream` accept `timeout:` but do **not** apply it
|
|
33
|
+
(the streaming path discards it).
|
|
34
|
+
- **Streaming classes documented as single-pass / single-consumer** (issues #34,
|
|
35
|
+
#31). `ExecHandle`, `LogStream`, `MetricsStream`, `FsReadStream`,
|
|
36
|
+
`PullSession`, and `AgentStream` are `Enumerable` but drain a one-shot native
|
|
37
|
+
channel: forward-only, not rewindable, and meant for one consumer on one
|
|
38
|
+
thread. A second `each` (or a combinator after a partial drain) silently
|
|
39
|
+
yields nothing. Noted on each class and in a README streaming caveat.
|
|
40
|
+
- **SSH `close` disconnect behavior documented** (issue #33). `SshClient#close` /
|
|
41
|
+
`SftpClient#close` send the graceful protocol disconnect; relying on GC skips
|
|
42
|
+
it (only the in-process server task is aborted). The block-less
|
|
43
|
+
`open_client`/`sftp` docs now tell callers to `close` (or use the block form)
|
|
44
|
+
for a clean disconnect.
|
|
45
|
+
|
|
46
|
+
### Internal
|
|
47
|
+
|
|
48
|
+
- **`DESIGN.md` refreshed** (issue #35). The stale runtime-pin references
|
|
49
|
+
(`v0.5.7`/`v0.5.8`) now point at `v0.5.10` via `RUNTIME_VERSION`, and the
|
|
50
|
+
hard-coded unit-example count is replaced with rot-proof phrasing.
|
|
51
|
+
- **RBS gains a note on SDK-constructed types** (issue #36). `sig/microsandbox.rbs`
|
|
52
|
+
now carries a top-of-file note explaining that native-backed value/handle/stream
|
|
53
|
+
types are constructed by the SDK from an internal native handle or data hash, not
|
|
54
|
+
by user code. Their `initialize` signatures are kept: RBS derives `new` from
|
|
55
|
+
`initialize`, so omitting them would not hide the constructor — it would
|
|
56
|
+
synthesize a misleading zero-arg `() -> instance` that Ruby actually rejects.
|
|
57
|
+
|
|
58
|
+
### Fixed
|
|
59
|
+
|
|
60
|
+
- **Precompiled fat-gem loader now finds the staged binary** (issue #25). The
|
|
61
|
+
native-extension require used `RbConfig::CONFIG["ruby_version"]` — the API
|
|
62
|
+
string `"3.4.0"` — but rake-compiler stages a multi-version fat gem's binaries
|
|
63
|
+
under the **major.minor** subdir (`3.4`), so the versioned require always
|
|
64
|
+
missed and fell to the flat-path rescue, which is **absent** in a precompiled
|
|
65
|
+
gem (only the versioned binary is packed). Every fat-gem install would have
|
|
66
|
+
failed at `require "microsandbox"` the moment precompiled gems are promoted.
|
|
67
|
+
The loader now derives the subdir from `RUBY_VERSION[/\d+\.\d+/]` (`"3.4"`),
|
|
68
|
+
matching the staged path; the flat-path rescue still covers source builds.
|
|
69
|
+
- **`extconf` MSRV preflight probes the compiler the build actually runs**
|
|
70
|
+
(issue #39). The preflight ran a bare `rustc --version` and hard-aborted when
|
|
71
|
+
`< 1.91`, but the build is driven by `cargo`, which resolves its compiler from
|
|
72
|
+
`$RUSTC` if set, otherwise the bare `rustc` on PATH — it never uses the `rustc`
|
|
73
|
+
beside the `cargo` binary, and the rustup `cargo` shim neither sets `$RUSTC` nor
|
|
74
|
+
reorders PATH. The preflight now mirrors that exact resolution (`$RUSTC`, else
|
|
75
|
+
PATH `rustc`), so it neither false-passes when a stale non-rustup `rustc`
|
|
76
|
+
shadows a rustup `cargo` (the build would compile with that stale `rustc` and
|
|
77
|
+
fail deep in smoltcp) nor false-aborts when `$RUSTC` points at a newer compiler.
|
|
78
|
+
|
|
79
|
+
### Internal
|
|
80
|
+
|
|
81
|
+
- **CI now installs the packed gem from source** (issue #37). A new `package`
|
|
82
|
+
job runs `rake build`, `gem install`s the packed gem (exercising the gemspec
|
|
83
|
+
`spec.files` glob and the full from-gem `extconf` + `cargo` compile against the
|
|
84
|
+
packed `Cargo.toml`/`Cargo.lock`/`rust-toolchain.toml`), and requires it from
|
|
85
|
+
outside the repo. Previously every job compiled the working tree in place, so
|
|
86
|
+
a packaging regression could reach RubyGems undetected.
|
|
87
|
+
- **`version_spec` now guards the `Cargo.lock` version** (issue #38). The spec
|
|
88
|
+
already asserted `Native.version == VERSION` and the runtime-tag pin, but
|
|
89
|
+
nothing checked the `microsandbox_rb` version in the committed `Cargo.lock`,
|
|
90
|
+
which the gemspec packs. A release that bumped `version.rb` + `Cargo.toml` but
|
|
91
|
+
forgot to refresh the lock would ship a stale lock (and a `--locked` build
|
|
92
|
+
would reject it) — a recurring release mistake this now catches.
|
|
93
|
+
### Security
|
|
94
|
+
|
|
95
|
+
- **Secret values no longer leak into `ArgumentError` messages** (issue #23).
|
|
96
|
+
`Sandbox.create(secrets:)` validation interpolated the whole secret spec via
|
|
97
|
+
`spec.inspect` into two error messages — and because the `:value`-present
|
|
98
|
+
guard runs first, the "needs `:host`/`:hosts`/`:host_patterns`" error *always*
|
|
99
|
+
embedded the cleartext secret value (and the env/value error did whenever a
|
|
100
|
+
value was supplied). Such messages routinely reach logs and error trackers.
|
|
101
|
+
Both messages now report the spec's keys only (`spec.keys.inspect`), mirroring
|
|
102
|
+
the existing `registry_auth` handling, with a unit spec asserting the value
|
|
103
|
+
is never present in the raised message.
|
|
104
|
+
|
|
105
|
+
### Fixed
|
|
106
|
+
|
|
107
|
+
- **Native duration parsing is panic-free regardless of the Ruby layer**
|
|
108
|
+
(issue #30). The native binding called `Duration::from_secs_f64` directly at
|
|
109
|
+
five sites (`exec`/`shell` timeout, `stop_with_timeout`, `kill_with_timeout`,
|
|
110
|
+
`metrics_stream` interval, `replace_with_timeout`); that panics on NaN/Inf/
|
|
111
|
+
negative *and on finite-but-out-of-range* values (e.g. `Float::MAX`), which
|
|
112
|
+
surfaced as an ugly panic-turned-exception. The Ruby `coerce_duration` guard
|
|
113
|
+
set no upper bound, so a large finite value still reached and panicked the
|
|
114
|
+
native layer. All five sites now route through a `secs_to_duration` helper
|
|
115
|
+
(`try_from_secs_f64` + a clean `Microsandbox::Error`), matching the existing
|
|
116
|
+
agent-client pattern — defense in depth so the native layer is panic-free on
|
|
117
|
+
its own.
|
|
118
|
+
### Added
|
|
119
|
+
|
|
120
|
+
- **Typed snapshot error classes** (issue #28). The five core snapshot error
|
|
121
|
+
variants — reachable through the gem's fully-wired `Snapshot` API — previously
|
|
122
|
+
collapsed to the base `Microsandbox::Error`, forcing callers to string-match
|
|
123
|
+
the message. They now raise typed subclasses:
|
|
124
|
+
`SnapshotNotFoundError` (`snapshot-not-found`),
|
|
125
|
+
`SnapshotAlreadyExistsError` (`snapshot-already-exists`),
|
|
126
|
+
`SnapshotSandboxRunningError` (`snapshot-sandbox-running`),
|
|
127
|
+
`SnapshotImageMissingError` (`snapshot-image-missing`), and
|
|
128
|
+
`SnapshotIntegrityError` (`snapshot-integrity`). This goes **beyond** the
|
|
129
|
+
Python SDK mirror (which defines no snapshot classes), matching the Go SDK's
|
|
130
|
+
per-variant coverage — a deliberate divergence. Additionally, the previously
|
|
131
|
+
orphaned `NetworkPolicyError` now also carries the core's `NetworkBuilder`
|
|
132
|
+
build/validation error (a `network(|n| ...)` failure), which previously fell
|
|
133
|
+
through to the base `Error`. All additive — existing `rescue Microsandbox::Error`
|
|
134
|
+
handlers still catch them.
|
|
10
135
|
|
|
11
136
|
## [0.8.1] - 2026-06-25
|
|
12
137
|
|
data/Cargo.lock
CHANGED
data/DESIGN.md
CHANGED
|
@@ -58,6 +58,23 @@ only (never touches the Ruby C API) and uses `catch_unwind` so a Rust panic is
|
|
|
58
58
|
captured and re-raised *after* the GVL is re-acquired rather than unwinding
|
|
59
59
|
across the C frame (which would be UB).
|
|
60
60
|
|
|
61
|
+
**The *calling* thread is not preemptible during the call.** `nogvl` passes a
|
|
62
|
+
null unblock-function to `rb_thread_call_without_gvl`, and Ruby only checks
|
|
63
|
+
pending interrupts *before* and *after* the GVL-released region — so while a
|
|
64
|
+
native call blocks, the thread that issued it cannot be interrupted:
|
|
65
|
+
`Timeout::timeout` (which relies on `Thread#raise`), `Thread#kill`, and `SIGINT`
|
|
66
|
+
(Ctrl-C) are all deferred until the call returns on its own. The GVL release
|
|
67
|
+
keeps *other* threads live; it does **not** make the calling thread
|
|
68
|
+
cancelable. This is harmless for the bounded calls (`exec`/`shell` with a
|
|
69
|
+
`timeout:`, `AgentClient` with a `timeout:`) because the deadline fires inside
|
|
70
|
+
the future, but the unbounded/streaming paths — `exec`/`shell` with
|
|
71
|
+
`timeout: nil`, `ExecHandle#recv`/`#wait`, `log_stream`/`metrics_stream` `recv`
|
|
72
|
+
(especially `follow: true`), `Sandbox#wait`/`SandboxHandle#wait_until_stopped`,
|
|
73
|
+
and `AgentClient#request` with no timeout — can block their caller indefinitely
|
|
74
|
+
if the guest wedges or a relay drops. **Bound such calls with the explicit
|
|
75
|
+
`timeout:` knobs rather than wrapping them in `Timeout::timeout`**, which will
|
|
76
|
+
not fire while the native call is in flight.
|
|
77
|
+
|
|
61
78
|
## Error mapping
|
|
62
79
|
|
|
63
80
|
The core returns one big `MicrosandboxError` enum. `error.rs` maps each variant
|
|
@@ -88,7 +105,8 @@ out (air-gapped hosts that provision out of band). libkrunfw is `dlopen`'d by
|
|
|
88
105
|
## Core-crate dependency (self-contained)
|
|
89
106
|
|
|
90
107
|
`ext/microsandbox/Cargo.toml` depends on the core crate via a **pinned git tag**
|
|
91
|
-
(`microsandbox` / `microsandbox-network
|
|
108
|
+
(`microsandbox` / `microsandbox-network`, pinned to the same tag as
|
|
109
|
+
`Microsandbox::RUNTIME_VERSION` — currently `v0.5.10`), so the gem builds anywhere
|
|
92
110
|
— CI, `rake-compiler-dock` release containers, and end-user source installs —
|
|
93
111
|
without an adjacent checkout. For fast local development against a sibling
|
|
94
112
|
microsandbox checkout, copy `.cargo/config.toml.example` to `.cargo/config.toml`
|
|
@@ -166,8 +184,8 @@ Create options now cover `image`, `cpus`, `memory`, `oci_upper_size`, `env`,
|
|
|
166
184
|
|
|
167
185
|
The binding is verified at four levels:
|
|
168
186
|
|
|
169
|
-
1. **Unit** (
|
|
170
|
-
objects, with the native layer stubbed.
|
|
187
|
+
1. **Unit** (several hundred examples) — the Ruby layer's option normalization
|
|
188
|
+
and value objects, with the native layer stubbed.
|
|
171
189
|
2. **Real-microVM integration** (`spec/integration`, opt-in via
|
|
172
190
|
`MICROSANDBOX_INTEGRATION=1`) — boots actual sandboxes and round-trips
|
|
173
191
|
`exec`/`shell`/`fs`/`metrics`/`logs`/streaming/snapshots. Run locally on
|
|
@@ -193,7 +211,7 @@ create options, full mount options (tmpfs/disk + stat-virtualization/
|
|
|
193
211
|
host-permissions), and snapshot inspection (`open`/`list_dir`/`reindex`).
|
|
194
212
|
|
|
195
213
|
A few **secondary** upstream knobs remain unexposed (a genuine binding gap, not
|
|
196
|
-
upstream-gated — they exist at the pinned `v0.5.
|
|
214
|
+
upstream-gated — they exist at the pinned `v0.5.10` runtime): per-published-port host
|
|
197
215
|
**bind address** (ports always bind loopback), network **interface overrides**,
|
|
198
216
|
and inline **named-volume create-mode** (pre-create with `Volume.create`, then
|
|
199
217
|
mount with `{ named: }`). These slot in module-by-module exactly as the existing
|
data/README.md
CHANGED
|
@@ -43,7 +43,7 @@ them. Our deepest thanks to the maintainers and community. 🙏
|
|
|
43
43
|
- **SSH & SFTP** — native in-process SSH client/server and file transfer (`Sandbox#ssh`)
|
|
44
44
|
- **Raw agent client** — byte-level access to the guest `agentd` protocol (`Microsandbox::AgentClient`)
|
|
45
45
|
- **Idiomatic Ruby** — keyword arguments, block-scoped lifecycle, a typed error hierarchy
|
|
46
|
-
- **Thread-friendly** — the GVL is released during sandbox calls, so
|
|
46
|
+
- **Thread-friendly** — the GVL is released during sandbox calls, so _other_ Ruby threads keep running. The _calling_ thread blocks uninterruptibly until the call returns (`Timeout::timeout`/`Thread#kill`/Ctrl-C can't interrupt a blocked native call), so bound long-running work with a real deadline where one exists: `exec(timeout:)` / `shell(timeout:)` kill the guest command after N seconds, and `AgentClient.connect_sandbox`/`connect_path` take a `timeout:` that bounds only the connect handshake. The streaming paths and `AgentClient#request`/`#stream` have no timeout knob and can block indefinitely if the guest wedges — see DESIGN.md
|
|
47
47
|
|
|
48
48
|
## Requirements
|
|
49
49
|
|
|
@@ -244,6 +244,13 @@ Microsandbox::Sandbox.create("stream", image: "public.ecr.aws/docker/library/pyt
|
|
|
244
244
|
end
|
|
245
245
|
```
|
|
246
246
|
|
|
247
|
+
> **Streams are single-pass.** `ExecHandle`, `LogStream`, `MetricsStream`,
|
|
248
|
+
> `FsReadStream`, `PullSession`, and `AgentStream` are `Enumerable`, but `each`
|
|
249
|
+
> drains a one-shot native channel — they are forward-only, not rewindable, and
|
|
250
|
+
> meant for a single consumer. Iterate (or `collect`/`read`) exactly once: a
|
|
251
|
+
> second `each`, or a combinator after a partial drain (`count` then `each`,
|
|
252
|
+
> `to_a` twice), silently yields nothing. Don't share one handle across threads.
|
|
253
|
+
|
|
247
254
|
### Images
|
|
248
255
|
|
|
249
256
|
Manage the local OCI image cache (images are pulled automatically on `create`):
|
|
@@ -399,6 +406,7 @@ Microsandbox.runtime_version # => "v0.5.10" (the embedded upstream runtime ta
|
|
|
399
406
|
| `0.7.0` | `v0.5.8` | SDK parity release (large binding-gap closure) |
|
|
400
407
|
| `0.8.0` | `v0.5.10` | adopts upstream `v0.5.10` (idle-only heartbeat, config-fd hardening, **4 GiB default bind-mount quota**); supersedes the reverted `v0.5.9` attempt |
|
|
401
408
|
| `0.8.1` | `v0.5.10` | gem-only: re-provision a stale local runtime; per-bind-mount `quota_mib:` override |
|
|
409
|
+
| `0.8.2` | `v0.5.10` | gem-only: redact secrets from errors, typed snapshot errors, panic-free durations, fat-gem loader + `extconf` preflight fixes, threading/streaming docs |
|
|
402
410
|
|
|
403
411
|
**Going forward** — the gem version moves on its own semver track and no longer
|
|
404
412
|
mirrors the upstream tag:
|
data/ext/microsandbox/Cargo.toml
CHANGED
|
@@ -7,7 +7,7 @@ description = "Ruby SDK native extension for microsandbox — secure, fast micro
|
|
|
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
9
|
# The core-crate dependency below stays pinned at its own tag (v0.5.10).
|
|
10
|
-
version = "0.8.
|
|
10
|
+
version = "0.8.2"
|
|
11
11
|
authors = ["Super Rad Company <development@superrad.company>"]
|
|
12
12
|
repository = "https://github.com/superradcompany/microsandbox"
|
|
13
13
|
license = "Apache-2.0"
|
data/ext/microsandbox/extconf.rb
CHANGED
|
@@ -2,31 +2,46 @@
|
|
|
2
2
|
|
|
3
3
|
require "mkmf"
|
|
4
4
|
require "rb_sys/mkmf"
|
|
5
|
+
require "shellwords"
|
|
5
6
|
|
|
6
7
|
# Preflight: the embedded microsandbox core is edition 2024 and pulls smoltcp,
|
|
7
|
-
# which sets a Minimum Supported Rust Version of 1.91.
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
8
|
+
# which sets a Minimum Supported Rust Version of 1.91. An older rustc (commonly
|
|
9
|
+
# Homebrew's, which shadows a newer rustup on PATH and ignores this gem's
|
|
10
|
+
# rust-toolchain.toml) fails deep in the build with a cryptic "rustc X is not
|
|
11
|
+
# supported by smoltcp" error. Detect it up front and explain the fix instead.
|
|
12
|
+
#
|
|
13
|
+
# Probe the *same* rustc the build will invoke. create_rust_makefile drives the
|
|
14
|
+
# build through `cargo`, and cargo resolves its compiler exactly as: the `RUSTC`
|
|
15
|
+
# env var if set, otherwise the bare `rustc` found on PATH. It does NOT use the
|
|
16
|
+
# `rustc` sitting beside the `cargo` binary, and the rustup `cargo` shim neither
|
|
17
|
+
# sets `RUSTC` nor prepends its toolchain bin to PATH — toolchain selection
|
|
18
|
+
# survives only because the PATH `rustc` is normally itself a rustup shim that
|
|
19
|
+
# honors rust-toolchain.toml. So a non-rustup rustc earlier on PATH (which reads
|
|
20
|
+
# neither RUSTUP_TOOLCHAIN nor the toolchain file) is what the build actually
|
|
21
|
+
# runs. Mirroring cargo's RUSTC-then-PATH resolution is the only probe that
|
|
22
|
+
# neither false-passes (the trap of checking the cargo sibling, which stays a
|
|
23
|
+
# valid rustup shim while the build silently uses the stale PATH rustc) nor
|
|
24
|
+
# false-aborts (when `RUSTC` points at a newer compiler than the PATH `rustc`).
|
|
12
25
|
MSRV = Gem::Version.new("1.91")
|
|
26
|
+
|
|
27
|
+
def build_rustc
|
|
28
|
+
rustc = ENV["RUSTC"].to_s.strip
|
|
29
|
+
rustc.empty? ? "rustc" : rustc
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
rustc = build_rustc
|
|
13
33
|
rustc_version = begin
|
|
14
|
-
out =
|
|
34
|
+
out = `#{rustc.shellescape} --version 2>/dev/null`
|
|
15
35
|
out[/\d+\.\d+(\.\d+)?/] && Gem::Version.new(out[/\d+\.\d+(\.\d+)?/])
|
|
16
36
|
rescue
|
|
17
37
|
nil
|
|
18
38
|
end
|
|
19
39
|
|
|
20
40
|
if rustc_version && rustc_version < MSRV
|
|
21
|
-
which_rustc = begin
|
|
22
|
-
`which rustc 2>/dev/null`.strip
|
|
23
|
-
rescue
|
|
24
|
-
""
|
|
25
|
-
end
|
|
26
41
|
abort(<<~MSG)
|
|
27
42
|
|
|
28
43
|
[microsandbox-rb] Rust #{rustc_version} is too old — the embedded core requires rustc >= #{MSRV}.
|
|
29
|
-
Found: #{
|
|
44
|
+
Found: #{rustc} (#{rustc_version})
|
|
30
45
|
|
|
31
46
|
This usually means an older rustc (e.g. Homebrew's) is ahead of a newer
|
|
32
47
|
rustup toolchain on your PATH. Fixes:
|
|
@@ -37,8 +52,12 @@ if rustc_version && rustc_version < MSRV
|
|
|
37
52
|
rustup install stable && rustup default stable
|
|
38
53
|
• Or upgrade your system Rust to >= #{MSRV}.
|
|
39
54
|
|
|
40
|
-
|
|
41
|
-
|
|
55
|
+
• Or point `RUSTC` at a recent compiler (cargo honors it over PATH):
|
|
56
|
+
RUSTC="$HOME/.cargo/bin/rustc" gem install microsandbox-rb
|
|
57
|
+
|
|
58
|
+
(This gem ships a rust-toolchain.toml pinning `stable`, but only a rustup
|
|
59
|
+
`rustc` shim reads it — a non-rustup `rustc` ahead on PATH ignores it, and
|
|
60
|
+
cargo invokes that PATH `rustc` unless `RUSTC` says otherwise.)
|
|
42
61
|
MSG
|
|
43
62
|
end
|
|
44
63
|
|
|
@@ -34,6 +34,22 @@ fn class_name(err: &MicrosandboxError) -> &'static str {
|
|
|
34
34
|
// client's `UnsupportedOperation` above.
|
|
35
35
|
CloudHttp { .. } => "CloudHttpError",
|
|
36
36
|
Unsupported { .. } => "UnsupportedError",
|
|
37
|
+
// Snapshot operations, all reachable through the gem's fully-wired
|
|
38
|
+
// `Snapshot` API. Upstream raises these un-wrapped, so without a mapping
|
|
39
|
+
// they collapse to the base `Error` and callers must string-match the
|
|
40
|
+
// message. This goes BEYOND the Python mirror (which has no Snapshot
|
|
41
|
+
// classes and matches the Go SDK's per-variant coverage instead) — a
|
|
42
|
+
// deliberate divergence noted in `lib/microsandbox/errors.rb`.
|
|
43
|
+
SnapshotNotFound(_) => "SnapshotNotFoundError",
|
|
44
|
+
SnapshotAlreadyExists(_) => "SnapshotAlreadyExistsError",
|
|
45
|
+
SnapshotSandboxRunning(_) => "SnapshotSandboxRunningError",
|
|
46
|
+
SnapshotImageMissing(_) => "SnapshotImageMissingError",
|
|
47
|
+
SnapshotIntegrity(_) => "SnapshotIntegrityError",
|
|
48
|
+
// Give the already-defined-but-orphaned `NetworkPolicyError` a mapping:
|
|
49
|
+
// a builder parse/validation error from `network(|n| ...)`. The gem
|
|
50
|
+
// unconditionally enables the core's `net` feature (default-features),
|
|
51
|
+
// so this variant is always present.
|
|
52
|
+
NetworkBuilder(_) => "NetworkPolicyError",
|
|
37
53
|
_ => "Error",
|
|
38
54
|
}
|
|
39
55
|
}
|
|
@@ -40,6 +40,21 @@ use crate::exec::ExecHandle;
|
|
|
40
40
|
use crate::runtime::{block_on, ruby};
|
|
41
41
|
use crate::stream::{LogStream, MetricsStream};
|
|
42
42
|
|
|
43
|
+
/// Convert a seconds `f64` into a `Duration`, surfacing a clean Ruby error
|
|
44
|
+
/// instead of the panic `Duration::from_secs_f64` raises on NaN/Inf/negative —
|
|
45
|
+
/// *and on finite-but-out-of-range* values (e.g. `Float::MAX`). The Ruby
|
|
46
|
+
/// `coerce_duration` already guards the public paths, but it sets no upper
|
|
47
|
+
/// bound, so a large finite value would still reach (and panic) the native
|
|
48
|
+
/// layer; this keeps the native layer panic-free regardless of the Ruby layer,
|
|
49
|
+
/// mirroring `agent::dur`.
|
|
50
|
+
fn secs_to_duration(secs: f64) -> Result<Duration, Error> {
|
|
51
|
+
Duration::try_from_secs_f64(secs).map_err(|e| {
|
|
52
|
+
error::base_error(format!(
|
|
53
|
+
"duration must be a non-negative, finite number of seconds in range (got {secs}: {e})"
|
|
54
|
+
))
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
43
58
|
#[magnus::wrap(class = "Microsandbox::Native::Sandbox", free_immediately, size)]
|
|
44
59
|
pub struct Sandbox {
|
|
45
60
|
inner: microsandbox::Sandbox,
|
|
@@ -380,7 +395,7 @@ impl Sandbox {
|
|
|
380
395
|
b = b.detached(true);
|
|
381
396
|
}
|
|
382
397
|
if let Some(secs) = conv::opt_f64(opts, "replace_with_timeout")? {
|
|
383
|
-
b = b.replace_with_timeout(
|
|
398
|
+
b = b.replace_with_timeout(secs_to_duration(secs)?);
|
|
384
399
|
} else if conv::opt_bool(opts, "replace")? {
|
|
385
400
|
b = b.replace();
|
|
386
401
|
}
|
|
@@ -579,14 +594,22 @@ impl Sandbox {
|
|
|
579
594
|
|
|
580
595
|
/// Stream metrics snapshots at `interval` seconds. Returns a MetricsStream
|
|
581
596
|
/// to pull snapshots from.
|
|
582
|
-
fn metrics_stream(&self, interval: f64) -> MetricsStream {
|
|
583
|
-
|
|
597
|
+
fn metrics_stream(&self, interval: f64) -> Result<MetricsStream, Error> {
|
|
598
|
+
// `interval <= 0.0` (0 or negative) keeps the prior 1s-default behavior;
|
|
599
|
+
// NaN (for which `<= 0.0` is false) and finite-but-out-of-range values
|
|
600
|
+
// fall through to `secs_to_duration`, which errors cleanly rather than
|
|
601
|
+
// letting `from_secs_f64` panic across the FFI boundary.
|
|
602
|
+
let dur = if interval <= 0.0 {
|
|
603
|
+
Duration::from_secs(1)
|
|
604
|
+
} else {
|
|
605
|
+
secs_to_duration(interval)?
|
|
606
|
+
};
|
|
584
607
|
// `metrics_stream` is synchronous but builds a `tokio::time::interval`,
|
|
585
608
|
// which panics ("no reactor running") unless constructed inside the
|
|
586
609
|
// runtime context — so build it under `block_on`. (`log_stream` is async
|
|
587
610
|
// and already runs inside `block_on`, so it needs no such wrapper.)
|
|
588
611
|
let stream = block_on(async { self.inner.metrics_stream(dur) });
|
|
589
|
-
MetricsStream::from_stream(stream)
|
|
612
|
+
Ok(MetricsStream::from_stream(stream))
|
|
590
613
|
}
|
|
591
614
|
|
|
592
615
|
/// Stream captured logs as they appear. `opts`: sources, since_ms,
|
|
@@ -1544,7 +1567,9 @@ impl ExecOpts {
|
|
|
1544
1567
|
cwd: conv::opt_string(opts, "cwd")?,
|
|
1545
1568
|
user: conv::opt_string(opts, "user")?,
|
|
1546
1569
|
env: conv::opt_string_map(opts, "env")?,
|
|
1547
|
-
timeout: conv::opt_f64(opts, "timeout")
|
|
1570
|
+
timeout: conv::opt_f64(opts, "timeout")?
|
|
1571
|
+
.map(secs_to_duration)
|
|
1572
|
+
.transpose()?,
|
|
1548
1573
|
tty: conv::opt_bool(opts, "tty")?,
|
|
1549
1574
|
stdin,
|
|
1550
1575
|
stdin_pipe: conv::opt_bool(opts, "stdin_pipe")?,
|
|
@@ -1907,8 +1932,7 @@ impl SbHandle {
|
|
|
1907
1932
|
|
|
1908
1933
|
/// Graceful stop with a custom escalation timeout (seconds).
|
|
1909
1934
|
fn stop_with_timeout(&self, secs: f64) -> Result<(), Error> {
|
|
1910
|
-
block_on(self.inner.stop_with_timeout(
|
|
1911
|
-
.map_err(error::to_ruby)
|
|
1935
|
+
block_on(self.inner.stop_with_timeout(secs_to_duration(secs)?)).map_err(error::to_ruby)
|
|
1912
1936
|
}
|
|
1913
1937
|
|
|
1914
1938
|
/// Force kill (SIGKILL) and wait.
|
|
@@ -1918,8 +1942,7 @@ impl SbHandle {
|
|
|
1918
1942
|
|
|
1919
1943
|
/// Force kill, waiting up to `secs` for the process to disappear.
|
|
1920
1944
|
fn kill_with_timeout(&self, secs: f64) -> Result<(), Error> {
|
|
1921
|
-
block_on(self.inner.kill_with_timeout(
|
|
1922
|
-
.map_err(error::to_ruby)
|
|
1945
|
+
block_on(self.inner.kill_with_timeout(secs_to_duration(secs)?)).map_err(error::to_ruby)
|
|
1923
1946
|
}
|
|
1924
1947
|
|
|
1925
1948
|
/// Send the graceful-shutdown request and return without waiting.
|
data/lib/microsandbox/agent.rb
CHANGED
|
@@ -35,6 +35,10 @@ module Microsandbox
|
|
|
35
35
|
# consume {AgentFrame}s until the stream ends (the terminal frame is delivered,
|
|
36
36
|
# then iteration stops). Mirrors the official SDKs' `AgentStream`.
|
|
37
37
|
#
|
|
38
|
+
# @note **Single-pass, forward-only, single-consumer.** `each` drains a
|
|
39
|
+
# one-shot native channel — not rewindable, iterate once from a single
|
|
40
|
+
# thread; a second pass or a post-drain combinator yields nothing.
|
|
41
|
+
#
|
|
38
42
|
# @example
|
|
39
43
|
# stream = client.stream(0, request_body)
|
|
40
44
|
# stream.each { |frame| handle(frame) }
|
data/lib/microsandbox/errors.rb
CHANGED
|
@@ -51,7 +51,22 @@ module Microsandbox
|
|
|
51
51
|
define_error(:ImageInUseError, "image-in-use")
|
|
52
52
|
define_error(:ImagePullFailedError, "image-pull-failed")
|
|
53
53
|
|
|
54
|
+
# Snapshot errors ---------------------------------------------------------
|
|
55
|
+
# These go BEYOND the Python SDK mirror (which has no Snapshot classes and
|
|
56
|
+
# collapses all five to its base error) — a deliberate divergence matching the
|
|
57
|
+
# Go SDK's per-variant coverage, so callers can rescue a missing/duplicate/
|
|
58
|
+
# running-source/missing-image/corrupt snapshot specifically. The native layer
|
|
59
|
+
# (ext/microsandbox/src/error.rs) maps the five core Snapshot* variants here.
|
|
60
|
+
define_error(:SnapshotNotFoundError, "snapshot-not-found")
|
|
61
|
+
define_error(:SnapshotAlreadyExistsError, "snapshot-already-exists")
|
|
62
|
+
define_error(:SnapshotSandboxRunningError, "snapshot-sandbox-running")
|
|
63
|
+
define_error(:SnapshotImageMissingError, "snapshot-image-missing")
|
|
64
|
+
define_error(:SnapshotIntegrityError, "snapshot-integrity")
|
|
65
|
+
|
|
54
66
|
# Networking / secrets errors ---------------------------------------------
|
|
67
|
+
# NetworkPolicyError now also carries the core's `NetworkBuilder` build/parse
|
|
68
|
+
# error (a `network(|n| ...)` validation failure), which was previously
|
|
69
|
+
# unmapped and fell through to the base Error.
|
|
55
70
|
define_error(:NetworkPolicyError, "network-policy-error")
|
|
56
71
|
define_error(:SecretViolationError, "secret-violation")
|
|
57
72
|
define_error(:TlsError, "tls-error")
|
|
@@ -86,6 +86,13 @@ module Microsandbox
|
|
|
86
86
|
# Iterate it (it is {Enumerable}) to consume {ExecEvent}s as they arrive, or
|
|
87
87
|
# call {#collect} to drain it into an {ExecOutput}.
|
|
88
88
|
#
|
|
89
|
+
# @note **Single-pass, forward-only, single-consumer.** `each`/`collect` drain
|
|
90
|
+
# a one-shot native event channel, so the handle is *not* rewindable: a
|
|
91
|
+
# second `each`, or `collect` after a partial `each` (and vice versa),
|
|
92
|
+
# yields only what is left. Consume it once, from one thread. (An
|
|
93
|
+
# out-of-band {#kill}/{#signal} via the control channel is the exception —
|
|
94
|
+
# it can unblock a parked `each` from another thread.)
|
|
95
|
+
#
|
|
89
96
|
# @example
|
|
90
97
|
# handle = sb.exec_stream("python", ["-u", "script.py"])
|
|
91
98
|
# handle.each do |event|
|
data/lib/microsandbox/fs.rb
CHANGED
|
@@ -199,6 +199,11 @@ module Microsandbox
|
|
|
199
199
|
# A streaming reader over a guest file, from {FS#read_stream}. Iterate it (it
|
|
200
200
|
# is {Enumerable}) to consume byte chunks (ASCII-8BIT) as they arrive, or call
|
|
201
201
|
# {#read} to drain it into one String.
|
|
202
|
+
#
|
|
203
|
+
# @note **Single-pass, forward-only, single-consumer.** `each`/`read` drain a
|
|
204
|
+
# one-shot native channel — not rewindable, and not safe to share across
|
|
205
|
+
# threads. Consume it once: a second `each`, or `read` after a partial
|
|
206
|
+
# `each`, yields only the remaining bytes.
|
|
202
207
|
class FsReadStream
|
|
203
208
|
include Enumerable
|
|
204
209
|
|
data/lib/microsandbox/sandbox.rb
CHANGED
|
@@ -526,7 +526,10 @@ module Microsandbox
|
|
|
526
526
|
env = spec[:env] || spec["env"]
|
|
527
527
|
value = spec[:value] || spec["value"]
|
|
528
528
|
unless env && value
|
|
529
|
-
|
|
529
|
+
# Report only the keys given, never the values — a secret spec carries
|
|
530
|
+
# the cleartext :value, and exception messages routinely land in logs
|
|
531
|
+
# and error trackers. Mirrors apply_registry_opts above.
|
|
532
|
+
raise ArgumentError, "secret spec needs :env and :value (got keys: #{spec.keys.inspect})"
|
|
530
533
|
end
|
|
531
534
|
out = {"env" => env.to_s, "value" => value.to_s}
|
|
532
535
|
hosts = Array(spec[:hosts] || spec["hosts"]).map(&:to_s)
|
|
@@ -534,8 +537,10 @@ module Microsandbox
|
|
|
534
537
|
hosts << single.to_s if single
|
|
535
538
|
patterns = Array(spec[:host_patterns] || spec["host_patterns"]).map(&:to_s)
|
|
536
539
|
if hosts.empty? && patterns.empty?
|
|
540
|
+
# Keys only — this branch runs after :value is confirmed present, so
|
|
541
|
+
# spec.inspect would always embed the cleartext secret value.
|
|
537
542
|
raise ArgumentError,
|
|
538
|
-
"secret spec needs :host, :hosts, or :host_patterns (got #{spec.inspect})"
|
|
543
|
+
"secret spec needs :host, :hosts, or :host_patterns (got keys: #{spec.keys.inspect})"
|
|
539
544
|
end
|
|
540
545
|
out["hosts"] = hosts unless hosts.empty?
|
|
541
546
|
out["host_patterns"] = patterns unless patterns.empty?
|
|
@@ -820,7 +825,11 @@ module Microsandbox
|
|
|
820
825
|
# @param cwd [String, nil] working directory
|
|
821
826
|
# @param user [String, nil] user to run as
|
|
822
827
|
# @param env [Hash, nil] extra environment variables
|
|
823
|
-
# @param timeout [Numeric, nil] kill after N seconds
|
|
828
|
+
# @param timeout [Numeric, nil] kill the command after N seconds and raise
|
|
829
|
+
# {ExecTimeoutError}. Omit or pass +nil+ for *no* timeout. Note the
|
|
830
|
+
# asymmetry: +0+ is **not** "disable" — it is an immediate (zero) deadline,
|
|
831
|
+
# so the command is killed before producing any output and {ExecTimeoutError}
|
|
832
|
+
# is raised. Use +nil+/omit, never +0+, to mean "no limit".
|
|
824
833
|
# @param tty [Boolean] allocate a pseudo-terminal
|
|
825
834
|
# @param stdin [String, Symbol, nil] bytes to feed to stdin, or +:pipe+ to
|
|
826
835
|
# open a streaming stdin pipe (write/close it via {ExecHandle#stdin}; only
|
|
@@ -843,6 +852,11 @@ module Microsandbox
|
|
|
843
852
|
# Pass +stdin: :pipe+ to feed the process interactively: {ExecHandle#stdin}
|
|
844
853
|
# then returns a writable sink; close it to send EOF (a process like +cat+
|
|
845
854
|
# that reads until EOF will otherwise block forever).
|
|
855
|
+
#
|
|
856
|
+
# @note +timeout:+ is accepted for signature symmetry with {#exec} but is
|
|
857
|
+
# **not applied** on the streaming path (the runtime discards it). Enforce a
|
|
858
|
+
# deadline yourself around the iteration, or use blocking {#exec} with
|
|
859
|
+
# +timeout:+ for the kill-after-N-seconds behavior.
|
|
846
860
|
# @return [ExecHandle]
|
|
847
861
|
# @see ExecHandle
|
|
848
862
|
def exec_stream(command, args = [], cwd: nil, user: nil, env: nil, timeout: nil, tty: false, stdin: nil, rlimits: nil)
|
|
@@ -851,6 +865,8 @@ module Microsandbox
|
|
|
851
865
|
end
|
|
852
866
|
|
|
853
867
|
# Run a shell script and stream its output as it arrives.
|
|
868
|
+
# @note Like {#exec_stream}, +timeout:+ is accepted but **not applied** on
|
|
869
|
+
# the streaming path.
|
|
854
870
|
# @return [ExecHandle]
|
|
855
871
|
def shell_stream(script, cwd: nil, user: nil, env: nil, timeout: nil, tty: false, stdin: nil, rlimits: nil)
|
|
856
872
|
ExecHandle.new(@native.shell_stream(script.to_s,
|
|
@@ -1083,6 +1099,12 @@ module Microsandbox
|
|
|
1083
1099
|
# "kind" key (e.g. "resolving", "resolved", "layer_download_progress",
|
|
1084
1100
|
# "layer_materialize_progress", "complete") plus kind-specific fields.
|
|
1085
1101
|
#
|
|
1102
|
+
# @note **Single-pass, forward-only, single-consumer.** `each` drains a
|
|
1103
|
+
# one-shot native progress channel — not rewindable, iterate once from a
|
|
1104
|
+
# single thread. {#sandbox} works whether or not you iterated (it awaits the
|
|
1105
|
+
# create either way), so you can skip `each` entirely; but don't expect a
|
|
1106
|
+
# second `each` to replay the events.
|
|
1107
|
+
#
|
|
1086
1108
|
# @example
|
|
1087
1109
|
# session = Microsandbox::Sandbox.create_with_progress("box", image: "python")
|
|
1088
1110
|
# session.each { |ev| puts "#{ev["kind"]} #{ev["downloaded_bytes"]}" }
|
data/lib/microsandbox/ssh.rb
CHANGED
|
@@ -121,7 +121,10 @@ module Microsandbox
|
|
|
121
121
|
@native.read_link(path.to_s)
|
|
122
122
|
end
|
|
123
123
|
|
|
124
|
-
# Close the SFTP session. Idempotent.
|
|
124
|
+
# Close the SFTP session. Idempotent. Sends the graceful SFTP close;
|
|
125
|
+
# relying on GC to reclaim an unclosed session drops it abruptly (no
|
|
126
|
+
# graceful close), so call this — or use the block form of
|
|
127
|
+
# {SshClient#sftp} — for a clean shutdown.
|
|
125
128
|
# @return [nil]
|
|
126
129
|
def close
|
|
127
130
|
@native.close
|
|
@@ -161,7 +164,9 @@ module Microsandbox
|
|
|
161
164
|
end
|
|
162
165
|
|
|
163
166
|
# Open an SFTP session over this connection. With a block, the session is
|
|
164
|
-
# yielded and closed when the block returns.
|
|
167
|
+
# yielded and closed when the block returns. Without a block you own the
|
|
168
|
+
# returned {SftpClient} and must call {SftpClient#close} (letting it GC skips
|
|
169
|
+
# the graceful disconnect).
|
|
165
170
|
# @yieldparam sftp [SftpClient]
|
|
166
171
|
# @return [SftpClient, Object]
|
|
167
172
|
def sftp
|
|
@@ -175,7 +180,11 @@ module Microsandbox
|
|
|
175
180
|
end
|
|
176
181
|
end
|
|
177
182
|
|
|
178
|
-
# Close the SSH client session. Idempotent.
|
|
183
|
+
# Close the SSH client session. Idempotent. Sends the graceful protocol
|
|
184
|
+
# disconnect (russh `Disconnect::ByApplication`); if the client is instead
|
|
185
|
+
# left to GC, only the in-process server task is aborted and that disconnect
|
|
186
|
+
# is skipped. Prefer the block form of {SshOps#open_client}, or call this in
|
|
187
|
+
# an `ensure`, so the session closes cleanly.
|
|
179
188
|
# @return [nil]
|
|
180
189
|
def close
|
|
181
190
|
@native.close
|
|
@@ -216,7 +225,9 @@ module Microsandbox
|
|
|
216
225
|
end
|
|
217
226
|
|
|
218
227
|
# Open a native in-process SSH client to the sandbox. With a block, the
|
|
219
|
-
# client is yielded and closed when the block returns.
|
|
228
|
+
# client is yielded and closed when the block returns. Without a block you
|
|
229
|
+
# own the returned {SshClient} and must call {SshClient#close} for a clean
|
|
230
|
+
# protocol disconnect (letting it GC only aborts the in-process server task).
|
|
220
231
|
# @param user [String] guest user to authenticate as (default "root")
|
|
221
232
|
# @param term [String, nil] TERM value for the session
|
|
222
233
|
# @param sftp [Boolean] enable the SFTP subsystem (default true)
|
data/lib/microsandbox/streams.rb
CHANGED
|
@@ -6,6 +6,14 @@ module Microsandbox
|
|
|
6
6
|
# iteration blocks for new entries until the sandbox stops; otherwise it ends
|
|
7
7
|
# once the historical log is drained.
|
|
8
8
|
#
|
|
9
|
+
# @note **Single-pass, forward-only, single-consumer.** `each` drains a
|
|
10
|
+
# one-shot native channel, so it is *not* rewindable: a second `each` — or
|
|
11
|
+
# any `Enumerable` combinator after a partial drain (`to_a` twice, `count`
|
|
12
|
+
# then `each`, `first(n)` then `each`) — silently yields nothing. Iterate
|
|
13
|
+
# exactly once, from one thread. With `follow: true`, `each` blocks the
|
|
14
|
+
# calling thread uninterruptibly until the sandbox stops (see DESIGN.md on
|
|
15
|
+
# GVL release).
|
|
16
|
+
#
|
|
9
17
|
# @example
|
|
10
18
|
# sb.log_stream(follow: true).each { |entry| print entry.text }
|
|
11
19
|
class LogStream
|
|
@@ -31,6 +39,11 @@ module Microsandbox
|
|
|
31
39
|
# Enumerable: iteration yields one snapshot per interval tick until the
|
|
32
40
|
# sandbox stops.
|
|
33
41
|
#
|
|
42
|
+
# @note **Single-pass, forward-only, single-consumer.** Like {LogStream},
|
|
43
|
+
# `each` drains a one-shot native channel — not rewindable, iterate once
|
|
44
|
+
# from a single thread; a second pass or a post-drain combinator yields
|
|
45
|
+
# nothing.
|
|
46
|
+
#
|
|
34
47
|
# @example
|
|
35
48
|
# sb.metrics_stream(interval: 0.5).each { |m| puts m.cpu_percent }
|
|
36
49
|
class MetricsStream
|
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.8.
|
|
11
|
+
VERSION = "0.8.2"
|
|
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
|
data/lib/microsandbox.rb
CHANGED
|
@@ -2,12 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "microsandbox/version"
|
|
4
4
|
|
|
5
|
-
# Load the compiled native extension. Precompiled platform gems
|
|
6
|
-
#
|
|
7
|
-
#
|
|
5
|
+
# Load the compiled native extension. Precompiled platform gems stage the
|
|
6
|
+
# binary under a major.minor subdirectory (e.g.
|
|
7
|
+
# lib/microsandbox/3.4/microsandbox_rb.bundle) — that is the directory
|
|
8
|
+
# rake-compiler builds (it matches the `ruby_version` against /(\d+\.\d+)/), NOT
|
|
9
|
+
# the API string "3.4.0" that RbConfig::CONFIG["ruby_version"] returns. Use
|
|
10
|
+
# RUBY_VERSION's major.minor so the require hits the staged path; fall back to
|
|
11
|
+
# the flat path source builds produce.
|
|
8
12
|
begin
|
|
9
|
-
|
|
10
|
-
require_relative "microsandbox/#{
|
|
13
|
+
abi_version = RUBY_VERSION[/\d+\.\d+/]
|
|
14
|
+
require_relative "microsandbox/#{abi_version}/microsandbox_rb"
|
|
11
15
|
rescue LoadError
|
|
12
16
|
require_relative "microsandbox/microsandbox_rb"
|
|
13
17
|
end
|
data/sig/microsandbox.rbs
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
# Type signatures for the public microsandbox Ruby API.
|
|
2
|
+
#
|
|
3
|
+
# Note: value, stream, and handle types (e.g. ExecOutput, Metrics, SandboxHandle,
|
|
4
|
+
# the Ssh* clients, AgentClient/AgentStream, PullSession, Snapshot*/Image*/Volume*)
|
|
5
|
+
# are constructed by the SDK from an internal native handle or data hash, not by
|
|
6
|
+
# user code — you receive them from SDK calls. Their `initialize` is documented
|
|
7
|
+
# below only to keep `new`'s synthesized signature accurate (RBS derives `new`
|
|
8
|
+
# from `initialize`; omitting it does NOT hide the constructor — it falls back to
|
|
9
|
+
# a misleading zero-arg `() -> instance` that Ruby actually rejects). Treat these
|
|
10
|
+
# constructors as internal.
|
|
2
11
|
|
|
3
12
|
module Microsandbox
|
|
4
13
|
VERSION: String
|
|
@@ -38,6 +47,11 @@ module Microsandbox
|
|
|
38
47
|
class ImageNotFoundError < Error end
|
|
39
48
|
class ImageInUseError < Error end
|
|
40
49
|
class ImagePullFailedError < Error end
|
|
50
|
+
class SnapshotNotFoundError < Error end
|
|
51
|
+
class SnapshotAlreadyExistsError < Error end
|
|
52
|
+
class SnapshotSandboxRunningError < Error end
|
|
53
|
+
class SnapshotImageMissingError < Error end
|
|
54
|
+
class SnapshotIntegrityError < Error end
|
|
41
55
|
class NetworkPolicyError < Error end
|
|
42
56
|
class SecretViolationError < Error end
|
|
43
57
|
class TlsError < Error end
|