microsandbox-rb 0.6.0 → 0.7.0
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 +96 -1
- data/Cargo.lock +1 -1
- data/DESIGN.md +16 -6
- data/README.md +27 -14
- data/ext/microsandbox/Cargo.toml +1 -1
- data/ext/microsandbox/src/agent.rs +18 -7
- data/ext/microsandbox/src/conv.rs +37 -1
- data/ext/microsandbox/src/fs_stream.rs +92 -0
- data/ext/microsandbox/src/image.rs +6 -0
- data/ext/microsandbox/src/lib.rs +40 -0
- data/ext/microsandbox/src/sandbox.rs +673 -64
- data/ext/microsandbox/src/snapshot.rs +72 -6
- data/ext/microsandbox/src/volume.rs +113 -1
- data/lib/microsandbox/agent.rb +3 -1
- data/lib/microsandbox/exec_handle.rb +7 -3
- data/lib/microsandbox/exec_output.rb +7 -7
- data/lib/microsandbox/fs.rb +84 -2
- data/lib/microsandbox/image.rb +2 -1
- data/lib/microsandbox/log_entry.rb +4 -2
- data/lib/microsandbox/metrics.rb +9 -0
- data/lib/microsandbox/sandbox.rb +461 -70
- data/lib/microsandbox/snapshot.rb +63 -6
- data/lib/microsandbox/ssh.rb +14 -9
- data/lib/microsandbox/version.rb +1 -1
- data/lib/microsandbox/volume.rb +100 -1
- data/lib/microsandbox.rb +35 -1
- data/sig/microsandbox.rbs +70 -6
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 158469a5ebe84ee4863c2a141cbeb486407bfc5ee05866be9005f9aee3bd249d
|
|
4
|
+
data.tar.gz: ef08b6519ac87f66c20f704e4c55590f56b3d6c05fa01ab6f9187db46f8b43af
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d63ed9d45bdf68fb2f8352927412b23debd45e5e011f30a835fd2e75bae206e2ce19d1875c6099ce72c808c7d5e05e62a459f81794589e090ab837b9541d5da7
|
|
7
|
+
data.tar.gz: '09a726c22f8b12ecb0d575f0c4e20ce446aa5761cf5ed8cc15155a6b567e7ee867c4f95545a9595f786b88ec2f6dbd75b1bfe721fb629025ce8bfc19aa63fda0'
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,96 @@ wraps, and the README's Versioning section keeps the full gem→runtime map.
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.7.0] - 2026-06-23
|
|
12
|
+
|
|
13
|
+
A large parity release closing the binding gaps an audit against the upstream
|
|
14
|
+
Python/Node SDKs (at the wrapped `v0.5.8` runtime) surfaced. The runtime tag is
|
|
15
|
+
unchanged. Two genuine bug fixes; the rest is newly-exposed surface plus a few
|
|
16
|
+
behavior corrections (see **Changed**).
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- **Lossy UTF-8 decoding.** `LogEntry#text`, `ExecOutput#stdout`/`#stderr`,
|
|
21
|
+
`ExecEvent#text`, `SshOutput#stdout`/`#stderr`, and `SftpClient#read_text` now
|
|
22
|
+
scrub invalid byte sequences (replacing them with U+FFFD) so they always
|
|
23
|
+
return a *valid* UTF-8 String — matching the Python/Node SDKs. Previously they
|
|
24
|
+
re-tagged raw bytes as UTF-8 without transcoding, so captured output
|
|
25
|
+
containing invalid UTF-8 produced strings that raised downstream (regex,
|
|
26
|
+
concatenation, `JSON.generate`). Raw bytes remain available via `#data` /
|
|
27
|
+
`#stdout_bytes` / `#stderr_bytes`.
|
|
28
|
+
- **`runtime_path=` spec** no longer pollutes the process-wide set-once
|
|
29
|
+
`msb`-path `OnceLock` (it now stubs the native setter), removing an
|
|
30
|
+
order-dependent failure in combined unit+integration runs.
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- **Streaming image-pull progress** — `Sandbox.create_with_progress` returns a
|
|
35
|
+
`PullSession` (an `Enumerable` of progress-event Hashes) with `#sandbox` for
|
|
36
|
+
the booted sandbox.
|
|
37
|
+
- **Host-side volume filesystem** — `Volume.fs(name)` / `VolumeInfo#fs` return a
|
|
38
|
+
`VolumeFs` (read/read_text/write/list/mkdir/remove_file/remove_dir/exists?/
|
|
39
|
+
copy/rename/stat) that reads and writes a named volume without a running
|
|
40
|
+
sandbox.
|
|
41
|
+
- **Streaming guest filesystem** — `FS#read_stream` / `FS#write_stream`
|
|
42
|
+
(`FsReadStream`/`FsWriteSink`) for files too large to buffer in memory.
|
|
43
|
+
- **Full secrets surface** — `secrets:` entries accept `hosts:` / `host_patterns:`
|
|
44
|
+
(wildcards) allow-lists, `placeholder:`, `require_tls:`, injection toggles
|
|
45
|
+
(`inject_headers:`/`inject_basic_auth:`/`inject_query:`/`inject_body:`), and
|
|
46
|
+
per-secret `on_violation:`; plus a sandbox-level `on_secret_violation:`. The
|
|
47
|
+
block-variant actions accept both the underscore form (`block_and_log`) and the
|
|
48
|
+
upstream kebab-case wire spelling (`block-and-log`) used by the CLI / Go SDK /
|
|
49
|
+
config files; the bare `"passthrough"` string (passthrough-all-hosts, as in the
|
|
50
|
+
Python/Node SDKs) is also accepted, so a policy copied from another SDK ports
|
|
51
|
+
over unchanged.
|
|
52
|
+
- **Network configuration** — `Sandbox.create` now accepts `dns:` (nameservers/
|
|
53
|
+
rebind_protection/query_timeout_ms), `tls:` (interception tuning incl. bypass
|
|
54
|
+
patterns, intercepted ports, block_quic, and CA cert/key paths), `ipv4_pool:`/
|
|
55
|
+
`ipv6_pool:`, `max_connections:`, and `trust_host_cas:`.
|
|
56
|
+
- **Create options** — `init:`/`init_with` (hand guest PID 1 to an init system),
|
|
57
|
+
`ephemeral:` (auto-remove state on terminal), and disk-image `fstype:`.
|
|
58
|
+
`fstype:` is rejected up front unless `image:` is a disk-image path (a local
|
|
59
|
+
path ending in `.raw`/`.qcow2`/`.vmdk`); pairing it with an OCI reference no
|
|
60
|
+
longer routes the ref through the disk-image builder and fails at boot.
|
|
61
|
+
- **Full mount options** — `volumes:` now supports `{ tmpfs: }`, `{ disk:,
|
|
62
|
+
format:, fstype: }`, and per-mount `stat_virtualization:`/`host_permissions:`
|
|
63
|
+
alongside the existing bind/named + ro/noexec/nosuid/nodev flags. The pre-0.7.0
|
|
64
|
+
`options: %w[ro noexec]` array form is still honored (translated onto the
|
|
65
|
+
boolean flags); an unrecognized token now raises rather than being silently
|
|
66
|
+
dropped, so a requested read-only/noexec mount can't quietly become writable.
|
|
67
|
+
- **Snapshots** — `Snapshot.open`/`list_dir`/`reindex`, `SnapshotInfo#open`/
|
|
68
|
+
`#remove`, and `SandboxHandle#snapshot`/`#snapshot_to`. `SnapshotInfo` now
|
|
69
|
+
carries the full manifest (`image_manifest_digest`, `fstype`,
|
|
70
|
+
`source_sandbox`, `labels`) on the artifact-opening paths.
|
|
71
|
+
- **`SandboxHandle#config` / `#config_json`** — read the stored sandbox config.
|
|
72
|
+
- **Metrics** — `upper_used_bytes`, `upper_free_bytes`,
|
|
73
|
+
`upper_host_allocated_bytes` (OCI writable-upper-layer accounting).
|
|
74
|
+
- **`ImageDetail#config["labels"]`** — OCI config labels.
|
|
75
|
+
- **`Microsandbox.setup`** — customizable runtime install (`base_dir:`,
|
|
76
|
+
`version:`, `force:`, `skip_verify:`); `force:` repairs a corrupt install.
|
|
77
|
+
|
|
78
|
+
### Changed
|
|
79
|
+
|
|
80
|
+
- **`exec`/`shell` stdin** is now a closed set: `nil`/`:null` = no stdin,
|
|
81
|
+
`:pipe` = streaming pipe (streaming variants only), a String = bytes. An
|
|
82
|
+
unrecognized Symbol now raises `ArgumentError` instead of being fed as its
|
|
83
|
+
characters (so a mistaken `stdin: :null` no longer sends the literal `"null"`).
|
|
84
|
+
- **Write methods reject non-Strings.** `FS#write`, `SftpClient#write`,
|
|
85
|
+
`ExecStdin#write`, `VolumeFs#write`, and `FsWriteSink#write` now raise
|
|
86
|
+
`TypeError` for non-String data instead of silently writing its `to_s` form.
|
|
87
|
+
- **Agent connect timeout.** `AgentClient.connect_sandbox`/`connect_path`
|
|
88
|
+
`timeout:` now treats `0` as an immediate deadline and raises on a
|
|
89
|
+
negative/non-finite value, instead of silently falling back to the default.
|
|
90
|
+
- **Secrets shorthand** still accepts `{ env:, value:, host: }`; the validation
|
|
91
|
+
message changed and a host allow-list is now required.
|
|
92
|
+
|
|
93
|
+
### Docs
|
|
94
|
+
|
|
95
|
+
- README/DESIGN implemented-surface corrected to match the binding (and to list
|
|
96
|
+
the few secondary knobs still not exposed); assorted YARD fixes
|
|
97
|
+
(`runtime_path=` set-once note, `VolumeInfo#kind` `:dir`, `create`'s
|
|
98
|
+
`volumes:`/`from_snapshot:` params, `log_stream` `'all'` source); CHANGELOG
|
|
99
|
+
compare links added for 0.5.9–0.5.12.
|
|
100
|
+
|
|
11
101
|
## [0.6.0] - 2026-06-23
|
|
12
102
|
|
|
13
103
|
This release puts the gem on its **own semantic version**, decoupled from the
|
|
@@ -291,7 +381,12 @@ microsandbox runtime, aligned with the official Python/Node/Go SDKs.
|
|
|
291
381
|
core crate has Apple-native deps). Until precompiled gems are published,
|
|
292
382
|
installing from source requires a Rust toolchain (stable >= 1.91).
|
|
293
383
|
|
|
294
|
-
[Unreleased]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.
|
|
384
|
+
[Unreleased]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.7.0...HEAD
|
|
385
|
+
[0.7.0]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.6.0...v0.7.0
|
|
295
386
|
[0.6.0]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.5.12...v0.6.0
|
|
387
|
+
[0.5.12]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.5.11...v0.5.12
|
|
388
|
+
[0.5.11]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.5.10...v0.5.11
|
|
389
|
+
[0.5.10]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.5.9...v0.5.10
|
|
390
|
+
[0.5.9]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.5.8...v0.5.9
|
|
296
391
|
[0.5.8]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.5.7...v0.5.8
|
|
297
392
|
[0.5.7]: https://github.com/superradcompany/microsandbox/releases/tag/v0.5.7
|
data/Cargo.lock
CHANGED
data/DESIGN.md
CHANGED
|
@@ -183,9 +183,19 @@ The binding is verified at four levels:
|
|
|
183
183
|
microVM, confirming the gem manifest and source-install path are complete.
|
|
184
184
|
|
|
185
185
|
**Roadmap:** the v1 roadmap (custom per-rule network policies, file patches,
|
|
186
|
-
interactive `attach`/`attach_shell`, SSH, and the raw agent client) is
|
|
187
|
-
implemented
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
186
|
+
interactive `attach`/`attach_shell`, SSH, and the raw agent client) is
|
|
187
|
+
implemented, and so is the bulk of the v0.5.8 configuration surface that a
|
|
188
|
+
later parity pass added: streaming image-pull progress, host-side `VolumeFs`,
|
|
189
|
+
streaming guest fs (`read_stream`/`write_stream`), the full secrets surface,
|
|
190
|
+
network configuration (DNS, TLS-interception tuning, IPv4/IPv6 pools,
|
|
191
|
+
`max_connections`, `trust_host_cas`), `init`/`ephemeral`/disk-image `fstype`
|
|
192
|
+
create options, full mount options (tmpfs/disk + stat-virtualization/
|
|
193
|
+
host-permissions), and snapshot inspection (`open`/`list_dir`/`reindex`).
|
|
194
|
+
|
|
195
|
+
A few **secondary** upstream knobs remain unexposed (a genuine binding gap, not
|
|
196
|
+
upstream-gated — they exist at the pinned `v0.5.8`): per-published-port host
|
|
197
|
+
**bind address** (ports always bind loopback), network **interface overrides**,
|
|
198
|
+
and inline **named-volume create-mode** (pre-create with `Volume.create`, then
|
|
199
|
+
mount with `{ named: }`). These slot in module-by-module exactly as the existing
|
|
200
|
+
bindings do. Beyond those, surfacing genuinely newer core features is gated on
|
|
201
|
+
advancing the pinned core-crate tag.
|
data/README.md
CHANGED
|
@@ -347,7 +347,7 @@ variable → an SDK-set override → the config file → `~/.microsandbox/bin/ms
|
|
|
347
347
|
Microsandbox.installed? # => true/false
|
|
348
348
|
Microsandbox.install # download + install the runtime (idempotent)
|
|
349
349
|
Microsandbox.runtime_path # => "/Users/you/.microsandbox/bin/msb"
|
|
350
|
-
Microsandbox.runtime_path = "/opt/microsandbox/bin/msb" # override
|
|
350
|
+
Microsandbox.runtime_path = "/opt/microsandbox/bin/msb" # override (set-once)
|
|
351
351
|
Microsandbox.libkrunfw_path = "/opt/microsandbox/lib/libkrunfw.dylib" # override (set-once)
|
|
352
352
|
```
|
|
353
353
|
|
|
@@ -470,25 +470,38 @@ or credential setup is needed.
|
|
|
470
470
|
> Until promoted, users install the source gem (which compiles via `rb_sys`).
|
|
471
471
|
|
|
472
472
|
See [DESIGN.md](DESIGN.md) for the architecture and the implemented-surface
|
|
473
|
-
section. The binding
|
|
473
|
+
section. The binding covers the official-SDK surface: sandbox
|
|
474
474
|
lifecycle (the live `Sandbox` `stop`/`stop_and_wait`/`kill`/`drain`/`wait`/
|
|
475
475
|
`status`/`detach`/`owns_lifecycle?`, plus the `SandboxHandle` controls
|
|
476
476
|
`stop_with_timeout`/`request_stop`/`request_kill`/`request_drain`/
|
|
477
|
-
`wait_until_stopped` from
|
|
477
|
+
`wait_until_stopped`/`config`/`config_json`/`snapshot`/`snapshot_to` from
|
|
478
|
+
`Sandbox.get`, and label-filtered `list_with`),
|
|
478
479
|
backend routing (`set_default_backend`/`with_backend`/`default_backend_kind`),
|
|
479
480
|
`exec`/`shell` (collected and streaming), interactive `attach`/
|
|
480
|
-
`attach_shell`, the full guest filesystem
|
|
481
|
-
`Microsandbox.all_sandbox_metrics`, and
|
|
482
|
-
logs, OCI image-cache management,
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
`
|
|
487
|
-
(`Microsandbox::
|
|
488
|
-
`
|
|
489
|
-
|
|
481
|
+
`attach_shell`, the full guest filesystem (incl. streaming `read_stream`/
|
|
482
|
+
`write_stream`), metrics (per-sandbox, `Microsandbox.all_sandbox_metrics`, and
|
|
483
|
+
streaming `metrics_stream`/`log_stream`), logs, OCI image-cache management,
|
|
484
|
+
named volumes (incl. host-side `Volume.fs`/`VolumeInfo#fs` read/write),
|
|
485
|
+
snapshots (create/open/list/list_dir/reindex/verify/export/import +
|
|
486
|
+
boot-from-snapshot), streaming image-pull progress
|
|
487
|
+
(`Sandbox.create_with_progress` → `PullSession`), **rootfs patches**
|
|
488
|
+
(`Microsandbox::Patch`), **network configuration** (presets, custom per-rule
|
|
489
|
+
`Microsandbox::NetworkPolicy`/`Rule`/`Destination`, plus DNS, TLS interception,
|
|
490
|
+
IPv4/IPv6 pools, `max_connections`, `trust_host_cas`), **secrets** (multi-host /
|
|
491
|
+
wildcard allow-lists, injection toggles, per-secret + sandbox-level violation
|
|
492
|
+
policy), **SSH** (`Sandbox#ssh` → `SshClient`/`SftpClient`/`SshServer`), and the
|
|
493
|
+
**raw agent client** (`Microsandbox::AgentClient`). Create options span
|
|
494
|
+
resources, `init`/`ephemeral`, disk-image `fstype`, network policy + config,
|
|
495
|
+
`log_level`/`security`/`rlimits`/`pull_policy`/`secrets`/`patches`/`volumes`
|
|
496
|
+
(bind/named/tmpfs/disk with mount policies) and more; `exec`/`shell` take
|
|
497
|
+
per-call `rlimits`, and `create` accepts
|
|
490
498
|
`registry_auth`/`registry_insecure`/`registry_ca_certs` for private and
|
|
491
|
-
authenticated registries.
|
|
499
|
+
authenticated registries, plus customizable provisioning via `Microsandbox.setup`.
|
|
500
|
+
|
|
501
|
+
A few secondary upstream knobs are not yet exposed: per-published-port host bind
|
|
502
|
+
address (ports always bind loopback), network interface overrides, and inline
|
|
503
|
+
named-volume create-mode (pre-create the volume with `Volume.create`, then mount
|
|
504
|
+
it with `{ named: "…" }`).
|
|
492
505
|
|
|
493
506
|
## Contributing
|
|
494
507
|
|
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.8).
|
|
10
|
-
version = "0.
|
|
10
|
+
version = "0.7.0"
|
|
11
11
|
authors = ["Super Rad Company <development@superrad.company>"]
|
|
12
12
|
repository = "https://github.com/superradcompany/microsandbox"
|
|
13
13
|
license = "Apache-2.0"
|
|
@@ -40,7 +40,7 @@ impl AgentClient {
|
|
|
40
40
|
|
|
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
|
-
let bridge = match dur(timeout) {
|
|
43
|
+
let bridge = match dur(timeout)? {
|
|
44
44
|
Some(t) => block_on(AgentBridge::connect_sandbox_with_timeout(&name, t)),
|
|
45
45
|
None => block_on(AgentBridge::connect_sandbox(&name)),
|
|
46
46
|
}
|
|
@@ -50,7 +50,7 @@ impl AgentClient {
|
|
|
50
50
|
|
|
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
|
-
let bridge = match dur(timeout) {
|
|
53
|
+
let bridge = match dur(timeout)? {
|
|
54
54
|
Some(t) => block_on(AgentBridge::connect_path_with_timeout(&path, t)),
|
|
55
55
|
None => block_on(AgentBridge::connect_path(&path)),
|
|
56
56
|
}
|
|
@@ -125,12 +125,23 @@ impl AgentClient {
|
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
/// Convert seconds into
|
|
129
|
-
///
|
|
130
|
-
|
|
128
|
+
/// Convert an optional `timeout` (seconds) into an optional `Duration`,
|
|
129
|
+
/// mirroring the Python SDK's `timeout_duration`:
|
|
130
|
+
/// - absent (`nil`) → `None` → the core's default handshake timeout
|
|
131
|
+
/// - `0` → an explicit zero deadline (fail fast), *not* "use the default"
|
|
132
|
+
/// - negative or non-finite (NaN/Inf) → a caller error (rather than being
|
|
133
|
+
/// silently swallowed into the default)
|
|
134
|
+
/// - finite but out of `Duration` range (e.g. `Float::MAX`) → a caller error
|
|
135
|
+
/// via `try_from_secs_f64`, rather than the panic `from_secs_f64` would raise
|
|
136
|
+
fn dur(timeout: Option<f64>) -> Result<Option<Duration>, Error> {
|
|
131
137
|
match timeout {
|
|
132
|
-
|
|
133
|
-
|
|
138
|
+
None => Ok(None),
|
|
139
|
+
Some(t) if t.is_finite() && t >= 0.0 => Duration::try_from_secs_f64(t)
|
|
140
|
+
.map(Some)
|
|
141
|
+
.map_err(|e| error::base_error(format!("timeout {t} seconds is out of range: {e}"))),
|
|
142
|
+
Some(t) => Err(error::base_error(format!(
|
|
143
|
+
"timeout must be a non-negative, finite number of seconds (got {t})"
|
|
144
|
+
))),
|
|
134
145
|
}
|
|
135
146
|
}
|
|
136
147
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
use std::collections::HashMap;
|
|
7
7
|
|
|
8
|
-
use magnus::{value::ReprValue, Error, RArray, RHash, TryConvert, Value};
|
|
8
|
+
use magnus::{value::ReprValue, Error, IntoValue, RArray, RHash, TryConvert, Value};
|
|
9
9
|
|
|
10
10
|
/// Fetch a non-nil value for `key`, if present.
|
|
11
11
|
fn get(hash: RHash, key: &str) -> Option<Value> {
|
|
@@ -90,3 +90,39 @@ pub fn opt_port_map(hash: RHash, key: &str) -> Result<Vec<(u16, u16)>, Error> {
|
|
|
90
90
|
None => Ok(Vec::new()),
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
|
+
|
|
94
|
+
/// Recursively convert a `serde_json::Value` into a Ruby value. Used for
|
|
95
|
+
/// pass-through JSON whose shape is not fixed — e.g. an image config's OCI
|
|
96
|
+
/// `labels` object, which the Ruby layer hands back verbatim (mirroring the
|
|
97
|
+
/// Python SDK's `dict | None`).
|
|
98
|
+
pub fn json_to_ruby(value: &serde_json::Value) -> Value {
|
|
99
|
+
let ruby = crate::runtime::ruby();
|
|
100
|
+
match value {
|
|
101
|
+
serde_json::Value::Null => ruby.qnil().as_value(),
|
|
102
|
+
serde_json::Value::Bool(b) => b.into_value_with(&ruby),
|
|
103
|
+
serde_json::Value::Number(n) => {
|
|
104
|
+
if let Some(i) = n.as_i64() {
|
|
105
|
+
i.into_value_with(&ruby)
|
|
106
|
+
} else if let Some(u) = n.as_u64() {
|
|
107
|
+
u.into_value_with(&ruby)
|
|
108
|
+
} else {
|
|
109
|
+
n.as_f64().unwrap_or(0.0).into_value_with(&ruby)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
serde_json::Value::String(s) => s.as_str().into_value_with(&ruby),
|
|
113
|
+
serde_json::Value::Array(items) => {
|
|
114
|
+
let arr = ruby.ary_new();
|
|
115
|
+
for item in items {
|
|
116
|
+
let _ = arr.push(json_to_ruby(item));
|
|
117
|
+
}
|
|
118
|
+
arr.into_value_with(&ruby)
|
|
119
|
+
}
|
|
120
|
+
serde_json::Value::Object(map) => {
|
|
121
|
+
let hash = ruby.hash_new();
|
|
122
|
+
for (k, v) in map {
|
|
123
|
+
let _ = hash.aset(k.as_str(), json_to_ruby(v));
|
|
124
|
+
}
|
|
125
|
+
hash.into_value_with(&ruby)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
//! Streaming guest-filesystem I/O: `Microsandbox::Native::FsReadStream` and
|
|
2
|
+
//! `Microsandbox::Native::FsWriteSink`.
|
|
3
|
+
//!
|
|
4
|
+
//! Wraps the core `SandboxFs::read_stream`/`write_stream` handles so large files
|
|
5
|
+
//! can be moved without buffering the whole thing in memory. The core
|
|
6
|
+
//! `FsReadStream::recv` needs `&mut self` and `FsWriteSink::close` consumes
|
|
7
|
+
//! `self`, so each is held behind a `tokio::Mutex` (the sink as an `Option`, so
|
|
8
|
+
//! `close` can take it and be idempotent). Each call drives the future to
|
|
9
|
+
//! completion with the GVL released; the Ruby layer wraps these as an
|
|
10
|
+
//! `Enumerable` reader and a writer with a block form.
|
|
11
|
+
|
|
12
|
+
use std::sync::Arc;
|
|
13
|
+
|
|
14
|
+
use magnus::{method, prelude::*, Error, RModule, RString, Ruby};
|
|
15
|
+
use microsandbox::sandbox::{FsReadStream, FsWriteSink};
|
|
16
|
+
use tokio::sync::Mutex;
|
|
17
|
+
|
|
18
|
+
use crate::error;
|
|
19
|
+
use crate::runtime::{block_on, ruby};
|
|
20
|
+
|
|
21
|
+
#[magnus::wrap(class = "Microsandbox::Native::FsReadStream", free_immediately, size)]
|
|
22
|
+
pub struct FsReadStreamHandle {
|
|
23
|
+
inner: Arc<Mutex<FsReadStream>>,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
impl FsReadStreamHandle {
|
|
27
|
+
pub fn new(stream: FsReadStream) -> Self {
|
|
28
|
+
Self {
|
|
29
|
+
inner: Arc::new(Mutex::new(stream)),
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// Next chunk of bytes (ASCII-8BIT), or nil at end of stream.
|
|
34
|
+
fn recv(&self) -> Result<Option<RString>, Error> {
|
|
35
|
+
let inner = Arc::clone(&self.inner);
|
|
36
|
+
match block_on(async move { inner.lock().await.recv().await }).map_err(error::to_ruby)? {
|
|
37
|
+
Some(bytes) => Ok(Some(ruby().str_from_slice(bytes.as_ref()))),
|
|
38
|
+
None => Ok(None),
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#[magnus::wrap(class = "Microsandbox::Native::FsWriteSink", free_immediately, size)]
|
|
44
|
+
pub struct FsWriteSinkHandle {
|
|
45
|
+
inner: Arc<Mutex<Option<FsWriteSink>>>,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
impl FsWriteSinkHandle {
|
|
49
|
+
pub fn new(sink: FsWriteSink) -> Self {
|
|
50
|
+
Self {
|
|
51
|
+
inner: Arc::new(Mutex::new(Some(sink))),
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// Write a chunk of bytes. Errors if the sink is already closed.
|
|
56
|
+
fn write(&self, data: RString) -> Result<(), Error> {
|
|
57
|
+
// Copy out while the GVL is held (GC.compact could move the buffer).
|
|
58
|
+
let bytes = unsafe { data.as_slice() }.to_vec();
|
|
59
|
+
let inner = Arc::clone(&self.inner);
|
|
60
|
+
let result = block_on(async move {
|
|
61
|
+
let guard = inner.lock().await;
|
|
62
|
+
match guard.as_ref() {
|
|
63
|
+
Some(sink) => Some(sink.write(&bytes).await),
|
|
64
|
+
None => None,
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
match result {
|
|
68
|
+
Some(r) => r.map_err(error::to_ruby),
|
|
69
|
+
None => Err(error::base_error("write to a closed FsWriteSink")),
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// Flush and close the sink. Idempotent.
|
|
74
|
+
fn close(&self) -> Result<(), Error> {
|
|
75
|
+
let inner = Arc::clone(&self.inner);
|
|
76
|
+
let taken = block_on(async move { inner.lock().await.take() });
|
|
77
|
+
match taken {
|
|
78
|
+
Some(sink) => block_on(sink.close()).map_err(error::to_ruby),
|
|
79
|
+
None => Ok(()),
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
85
|
+
let rs = native.define_class("FsReadStream", ruby.class_object())?;
|
|
86
|
+
rs.define_method("recv", method!(FsReadStreamHandle::recv, 0))?;
|
|
87
|
+
|
|
88
|
+
let ws = native.define_class("FsWriteSink", ruby.class_object())?;
|
|
89
|
+
ws.define_method("write", method!(FsWriteSinkHandle::write, 1))?;
|
|
90
|
+
ws.define_method("close", method!(FsWriteSinkHandle::close, 0))?;
|
|
91
|
+
Ok(())
|
|
92
|
+
}
|
|
@@ -9,6 +9,7 @@ use magnus::{function, prelude::*, Error, RArray, RHash, RModule, Ruby};
|
|
|
9
9
|
use microsandbox::image::{Image, ImageDetail, ImageHandle, ImagePruneReport};
|
|
10
10
|
|
|
11
11
|
use crate::backend::with_local_backend;
|
|
12
|
+
use crate::conv;
|
|
12
13
|
use crate::runtime::ruby;
|
|
13
14
|
|
|
14
15
|
fn handle_to_hash(h: &ImageHandle) -> RHash {
|
|
@@ -43,6 +44,11 @@ fn detail_to_hash(detail: ImageDetail) -> RHash {
|
|
|
43
44
|
let _ = c.aset("entrypoint", config.entrypoint);
|
|
44
45
|
let _ = c.aset("working_dir", config.working_dir);
|
|
45
46
|
let _ = c.aset("user", config.user);
|
|
47
|
+
// OCI config labels: a free-form JSON object (or nil). Converted to a
|
|
48
|
+
// Ruby Hash so `ImageDetail#config["labels"]` matches the Python/Node
|
|
49
|
+
// `config.labels` dict. (The pinned v0.5.8 runtime persists this as
|
|
50
|
+
// null today; the key exists for forward-compatibility/parity.)
|
|
51
|
+
let _ = c.aset("labels", config.labels.as_ref().map(conv::json_to_ruby));
|
|
46
52
|
let _ = c.aset("stop_signal", config.stop_signal);
|
|
47
53
|
let _ = hash.aset("config", c);
|
|
48
54
|
} else {
|
data/ext/microsandbox/src/lib.rs
CHANGED
|
@@ -13,6 +13,7 @@ mod backend;
|
|
|
13
13
|
mod conv;
|
|
14
14
|
mod error;
|
|
15
15
|
mod exec;
|
|
16
|
+
mod fs_stream;
|
|
16
17
|
mod image;
|
|
17
18
|
mod runtime;
|
|
18
19
|
mod sandbox;
|
|
@@ -47,6 +48,43 @@ fn install() -> Result<(), Error> {
|
|
|
47
48
|
runtime::block_on(microsandbox::setup::install()).map_err(error::to_ruby)
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
/// Customizable install via the core `Setup` builder. `opts`: base_dir (install
|
|
52
|
+
/// root), version (pin the runtime version), force (re-download even if present
|
|
53
|
+
/// — repairs a corrupt install), skip_verify. Mirrors the Node `Setup` builder.
|
|
54
|
+
fn setup(opts: RHash) -> Result<(), Error> {
|
|
55
|
+
use microsandbox::setup::Setup;
|
|
56
|
+
let base_dir = conv::opt_string(opts, "base_dir")?;
|
|
57
|
+
let version = conv::opt_string(opts, "version")?;
|
|
58
|
+
let skip_verify = conv::opt_bool(opts, "skip_verify")?;
|
|
59
|
+
let force = conv::opt_bool(opts, "force")?;
|
|
60
|
+
// `Setup` uses a typed-builder whose `strip_option` setters change the type
|
|
61
|
+
// on each call, so optional fields can't be set conditionally on one binding
|
|
62
|
+
// — branch on presence instead (matching the Node binding).
|
|
63
|
+
let setup = match (base_dir, version) {
|
|
64
|
+
(Some(d), Some(v)) => Setup::builder()
|
|
65
|
+
.base_dir(d)
|
|
66
|
+
.version(v)
|
|
67
|
+
.skip_verify(skip_verify)
|
|
68
|
+
.force(force)
|
|
69
|
+
.build(),
|
|
70
|
+
(Some(d), None) => Setup::builder()
|
|
71
|
+
.base_dir(d)
|
|
72
|
+
.skip_verify(skip_verify)
|
|
73
|
+
.force(force)
|
|
74
|
+
.build(),
|
|
75
|
+
(None, Some(v)) => Setup::builder()
|
|
76
|
+
.version(v)
|
|
77
|
+
.skip_verify(skip_verify)
|
|
78
|
+
.force(force)
|
|
79
|
+
.build(),
|
|
80
|
+
(None, None) => Setup::builder()
|
|
81
|
+
.skip_verify(skip_verify)
|
|
82
|
+
.force(force)
|
|
83
|
+
.build(),
|
|
84
|
+
};
|
|
85
|
+
runtime::block_on(setup.install()).map_err(error::to_ruby)
|
|
86
|
+
}
|
|
87
|
+
|
|
50
88
|
/// Whether the `msb` runtime + `libkrunfw` are installed and resolvable.
|
|
51
89
|
fn is_installed() -> bool {
|
|
52
90
|
microsandbox::setup::is_installed()
|
|
@@ -88,6 +126,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
88
126
|
|
|
89
127
|
native.define_singleton_method("version", function!(version, 0))?;
|
|
90
128
|
native.define_singleton_method("install", function!(install, 0))?;
|
|
129
|
+
native.define_singleton_method("setup", function!(setup, 1))?;
|
|
91
130
|
native.define_singleton_method("installed?", function!(is_installed, 0))?;
|
|
92
131
|
native.define_singleton_method("set_runtime_msb_path", function!(set_runtime_msb_path, 1))?;
|
|
93
132
|
native.define_singleton_method(
|
|
@@ -101,6 +140,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
101
140
|
sandbox::define(ruby, &native)?;
|
|
102
141
|
exec::define(ruby, &native)?;
|
|
103
142
|
stream::define(ruby, &native)?;
|
|
143
|
+
fs_stream::define(ruby, &native)?;
|
|
104
144
|
snapshot::define(ruby, &native)?;
|
|
105
145
|
image::define(ruby, &native)?;
|
|
106
146
|
volume::define(ruby, &native)?;
|