microsandbox-rb 0.14.0 → 0.16.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 +131 -0
- data/Cargo.lock +203 -82
- data/DESIGN.md +1 -1
- data/README.md +40 -3
- data/ext/microsandbox/Cargo.toml +5 -5
- data/ext/microsandbox/src/error.rs +5 -0
- data/ext/microsandbox/src/sandbox.rs +197 -18
- data/lib/microsandbox/errors.rb +7 -0
- data/lib/microsandbox/sandbox.rb +296 -2
- data/lib/microsandbox/ssh.rb +15 -2
- data/lib/microsandbox/version.rb +2 -2
- data/sig/microsandbox.rbs +19 -2
- metadata +1 -1
data/README.md
CHANGED
|
@@ -186,6 +186,39 @@ Microsandbox::Sandbox.start("box") # restart a stopped sandbox
|
|
|
186
186
|
Microsandbox::Sandbox.remove("box") # remove a stopped sandbox
|
|
187
187
|
```
|
|
188
188
|
|
|
189
|
+
**Convergent lifecycle** (runtime `v0.6.16`) — idempotent operations that take a
|
|
190
|
+
name to the state you want, whatever state it is in now:
|
|
191
|
+
|
|
192
|
+
```ruby
|
|
193
|
+
# Create it, or connect to (and start) the one that is already there. The
|
|
194
|
+
# keyword options are `create`'s and apply only when a create actually happens.
|
|
195
|
+
sb = Microsandbox::Sandbox.connect_or_create("box", image: "public.ecr.aws/docker/library/alpine:latest")
|
|
196
|
+
|
|
197
|
+
sb.id # opaque identity of the *persisted* sandbox — unlike
|
|
198
|
+
# the reusable name, it changes on remove+recreate
|
|
199
|
+
sb.wait_for_status(:running) # => SandboxHandle (no built-in timeout)
|
|
200
|
+
sb = sb.restart # stop + start; => a new live Sandbox
|
|
201
|
+
sb.destroy # stop + remove, in one step
|
|
202
|
+
|
|
203
|
+
h = Microsandbox::Sandbox.get("box")
|
|
204
|
+
h.connect_or_start # connect if running, start if not => Sandbox
|
|
205
|
+
h.restart(force: true, timeout: 5)
|
|
206
|
+
h.destroy
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Everything that acts on an *existing* receiver — `wait_for_status`, `restart`,
|
|
210
|
+
`destroy`, `connect_or_start` — compares the identity it was bound to against
|
|
211
|
+
the name's current owner first, raising `Microsandbox::SandboxReplacedError`
|
|
212
|
+
rather than touching a sandbox someone else recreated under the same name.
|
|
213
|
+
(`connect_or_create` is the entry point, so it has no prior identity to check:
|
|
214
|
+
it simply converges on whichever sandbox now owns the name.)
|
|
215
|
+
|
|
216
|
+
`connect_or_create`'s block form stops the sandbox only when the call owns its
|
|
217
|
+
lifecycle — i.e. when it created it attached. A sandbox it merely connected to,
|
|
218
|
+
or started with `detached: true`, is left running. And the stop it does issue is
|
|
219
|
+
scoped to that sandbox's `id`, so a name removed and recreated while the block
|
|
220
|
+
ran cannot be taken down by the teardown either.
|
|
221
|
+
|
|
189
222
|
> **v0.5.8 lifecycle change.** Upstream split the lifecycle into the live
|
|
190
223
|
> `Sandbox` and a controllable `SandboxHandle`, and the gem mirrors it. The live
|
|
191
224
|
> `Sandbox#stop`/`#kill` no longer take a `timeout:`; `#request_stop`/
|
|
@@ -430,7 +463,9 @@ Microsandbox::Volume.remove("cache")
|
|
|
430
463
|
```
|
|
431
464
|
|
|
432
465
|
`volumes:` accepts a host path String (bind mount) or `{ bind: "/host" }` /
|
|
433
|
-
`{ named: "volume-name" }` per guest path.
|
|
466
|
+
`{ named: "volume-name" }` per guest path. A bind or named mount may pin the
|
|
467
|
+
fallback guest owner for host files with `uid:`/`gid:` (both required together,
|
|
468
|
+
runtime `v0.6.15`). Boot from a snapshot with
|
|
434
469
|
`Sandbox.create(name, from_snapshot: "snap-name-or-path")`.
|
|
435
470
|
|
|
436
471
|
### Error handling
|
|
@@ -524,8 +559,8 @@ change diverged the two numbers — the gem version is **not** a reliable indica
|
|
|
524
559
|
of the embedded runtime version. To learn which runtime a build wraps, ask it:
|
|
525
560
|
|
|
526
561
|
```ruby
|
|
527
|
-
Microsandbox::VERSION # => "0.
|
|
528
|
-
Microsandbox.runtime_version # => "v0.6.
|
|
562
|
+
Microsandbox::VERSION # => "0.16.0" (the gem's own version)
|
|
563
|
+
Microsandbox.runtime_version # => "v0.6.16" (the embedded upstream runtime tag)
|
|
529
564
|
```
|
|
530
565
|
|
|
531
566
|
The companion [`microsandbox-rb-binaries`](#the-runtime-binaries) gem is
|
|
@@ -557,6 +592,8 @@ stale.
|
|
|
557
592
|
| `0.12.0` | `v0.6.8` | adopts upstream `v0.6.8` (**breaking**): `Sandbox.list`/`.list_with` return a cursor-paginated `SandboxPage` (`limit:`/`cursor:` keywords), `UnsupportedError` re-keyed by structured operations with `#operation`/`#hint`; runtime adds a shared log registry for followed streams and cloud exec/ssh reconnects |
|
|
558
593
|
| `0.13.0` | `v0.6.9` | adopts upstream `v0.6.9` (**breaking**): a bare `MSB_API_KEY` no longer selects the cloud backend (explicit `MSB_BACKEND=cloud` or a cloud profile required; invalid cloud config fails closed with `InvalidConfigError`); snapshot payload integrity becomes opt-in (`record_integrity:`, `verify` can report `:not_recorded`). Parity: default-workload execution (`exec_default`/`exec_default_stream`/`attach_default`, `cmd:`), flat root disks (`RootDisk.flat`), `modify(root_disk_size:)`, `rate_limiter:`, `vsock:`, `default_backend_info`, `Volume.get_default` |
|
|
559
594
|
| `0.14.0` | `v0.6.9` | two-gem split: SDK-only gem (no build-time runtime download) + companion `microsandbox-rb-binaries` platform gems |
|
|
595
|
+
| `0.15.0` | `v0.6.14` | adopts upstream `v0.6.10`–`v0.6.14` step by step: bind-mount correctness, guest bootstrap off the kernel command line, DNS pins for deferred domain allows, Linux glibc 2.28 baseline for the prebuilt runtime, legacy ext4 upper-disk resize, `msb_krun` 0.1.32. Parity: `ssh.open_client`/`prepare_server` accept `inactivity_timeout:` (seconds; `0` disables, `nil` inherits the 600s global default) |
|
|
596
|
+
| `0.16.0` | `v0.6.16` | adopts upstream `v0.6.15`+`v0.6.16` step by step: mount fallback ownership, readonly-mount write-probe fix, log retrieval rerouted through the SDK backends, config overlaid by field presence, network-slot recycling. Parity: per-mount `uid:`/`gid:`, and the convergent lifecycle — `Sandbox.connect_or_create`, `#id`, `#wait_for_status`, `#restart`, `#destroy`, `SandboxHandle#connect_or_start`, `SandboxReplacedError` |
|
|
560
597
|
|
|
561
598
|
**Going forward** — the gem version moves on its own semver track and no longer
|
|
562
599
|
mirrors the upstream tag:
|
data/ext/microsandbox/Cargo.toml
CHANGED
|
@@ -6,8 +6,8 @@ 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 dependency below stays pinned at its own tag (v0.6.
|
|
10
|
-
version = "0.
|
|
9
|
+
# The core-crate dependency below stays pinned at its own tag (v0.6.16).
|
|
10
|
+
version = "0.16.0"
|
|
11
11
|
authors = ["Super Rad Company <development@superrad.company>"]
|
|
12
12
|
repository = "https://github.com/superradcompany/microsandbox"
|
|
13
13
|
license = "Apache-2.0"
|
|
@@ -42,8 +42,8 @@ rb-sys = "0.9"
|
|
|
42
42
|
# first-use download into ~/.microsandbox (`setup::install`, which is not
|
|
43
43
|
# gated on "prebuilt"). "net"/"ssh"/"keyring" are the features the official
|
|
44
44
|
# SDKs ship with.
|
|
45
|
-
microsandbox = { git = "https://github.com/superradcompany/microsandbox", tag = "v0.6.
|
|
46
|
-
microsandbox-network = { git = "https://github.com/superradcompany/microsandbox", tag = "v0.6.
|
|
45
|
+
microsandbox = { git = "https://github.com/superradcompany/microsandbox", tag = "v0.6.16", default-features = false, features = ["keyring", "net", "ssh"] }
|
|
46
|
+
microsandbox-network = { git = "https://github.com/superradcompany/microsandbox", tag = "v0.6.16" }
|
|
47
47
|
# The *guest* agent (`agentd`, PID 1 inside every microVM) is embedded into the
|
|
48
48
|
# extension by microsandbox-filesystem's build.rs, which without its "prebuilt"
|
|
49
49
|
# feature demands a locally built `build/agentd` (workspace-only, `just
|
|
@@ -52,7 +52,7 @@ microsandbox-network = { git = "https://github.com/superradcompany/microsandbox"
|
|
|
52
52
|
# prebuilt agentd for the target arch is fetched (same behaviour as before)
|
|
53
53
|
# while the SDK-level msb download above stays off. Feature unification means
|
|
54
54
|
# this adds no new crates; keep it on the same tag as the deps above.
|
|
55
|
-
microsandbox-runtime = { git = "https://github.com/superradcompany/microsandbox", tag = "v0.6.
|
|
55
|
+
microsandbox-runtime = { git = "https://github.com/superradcompany/microsandbox", tag = "v0.6.16", default-features = false, features = ["prebuilt"] }
|
|
56
56
|
|
|
57
57
|
# Async core bridged to Ruby's synchronous API via a blocking tokio runtime.
|
|
58
58
|
tokio = { version = "1", features = ["rt-multi-thread", "sync", "time"] }
|
|
@@ -17,6 +17,11 @@ fn class_name(err: &MicrosandboxError) -> &'static str {
|
|
|
17
17
|
SandboxNotFound(_) => "SandboxNotFoundError",
|
|
18
18
|
SandboxAlreadyExists(_) => "SandboxAlreadyExistsError",
|
|
19
19
|
SandboxStillRunning(_) => "SandboxStillRunningError",
|
|
20
|
+
// v0.6.16 (#1462): the receiver's captured identity no longer owns the
|
|
21
|
+
// name — it was removed and recreated. Raised by the identity-checked
|
|
22
|
+
// convergent lifecycle APIs instead of acting on the replacement.
|
|
23
|
+
// Mirrors the Python `SandboxReplacedError`.
|
|
24
|
+
SandboxReplaced { .. } => "SandboxReplacedError",
|
|
20
25
|
// v0.6.6 (#1099): the sandbox exists but isn't running. Raised by the
|
|
21
26
|
// handle's exec/attach/ping/touch not-running guards and the fs
|
|
22
27
|
// agent-endpoint lookup. The `SandboxNotRunningError` class already
|
|
@@ -18,11 +18,11 @@ use microsandbox::logs::{
|
|
|
18
18
|
LogCursor, LogEntry, LogOptions, LogSource, LogStreamOptions, LogStreamStart,
|
|
19
19
|
};
|
|
20
20
|
use microsandbox::sandbox::{
|
|
21
|
-
AttachOptionsBuilder, DiskImageFormat, EnvVar, FlatClone, FsEntry, FsEntryKind,
|
|
22
|
-
HostPermissions, Patch, PullPolicy, PullProgress, PullProgressHandle,
|
|
23
|
-
RootDiskBuilder, SandboxBuilder, SandboxHandle, SandboxMetrics,
|
|
24
|
-
SandboxModificationPatch, SandboxStatus, SandboxStopResult,
|
|
25
|
-
SecretModificationPatch, SecretSource, SecurityProfile, StatVirtualization,
|
|
21
|
+
AttachOptionsBuilder, DestroyOptions, DiskImageFormat, EnvVar, FlatClone, FsEntry, FsEntryKind,
|
|
22
|
+
FsMetadata, HostPermissions, Patch, PullPolicy, PullProgress, PullProgressHandle,
|
|
23
|
+
RestartOptions, RlimitResource, RootDiskBuilder, SandboxBuilder, SandboxHandle, SandboxMetrics,
|
|
24
|
+
SandboxModificationBuilder, SandboxModificationPatch, SandboxStatus, SandboxStopResult,
|
|
25
|
+
SecretBuilder, SecretModificationPatch, SecretSource, SecurityProfile, StatVirtualization,
|
|
26
26
|
};
|
|
27
27
|
use microsandbox::LogLevel;
|
|
28
28
|
use microsandbox::MicrosandboxResult;
|
|
@@ -167,7 +167,8 @@ impl Sandbox {
|
|
|
167
167
|
// Hash — guest (req), kind ("bind"/"named"/"tmpfs"/"disk"), source
|
|
168
168
|
// (bind/named/disk), size_mib (tmpfs/disk), format + fstype (disk),
|
|
169
169
|
// readonly/noexec/nosuid/nodev (bool), stat_virtualization,
|
|
170
|
-
// host_permissions
|
|
170
|
+
// host_permissions, override_uid/override_gid (u32 pair).
|
|
171
|
+
// Enum-valued options are validated up front (the
|
|
171
172
|
// volume closure can't return an error); the core validates the rest
|
|
172
173
|
// (e.g. rejecting stat_virtualization on tmpfs/disk) at create().
|
|
173
174
|
for m in conv::opt_hash_vec(opts, "volumes")? {
|
|
@@ -195,6 +196,14 @@ impl Sandbox {
|
|
|
195
196
|
let host_perms = conv::opt_string(m, "host_permissions")?
|
|
196
197
|
.map(|s| host_permissions_from_str(&s))
|
|
197
198
|
.transpose()?;
|
|
199
|
+
// v0.6.15: the fallback guest owner presented for host files that
|
|
200
|
+
// carry no per-file stat override. The Ruby layer validates the pair
|
|
201
|
+
// (both-or-neither, u32 range, bind/named only, not with
|
|
202
|
+
// stat_virtualization=off); whether a *named* volume is directory-
|
|
203
|
+
// or disk-backed is known only to the core, which rejects the
|
|
204
|
+
// disk-backed case at build().
|
|
205
|
+
let override_uid = conv::opt_u32(m, "override_uid")?;
|
|
206
|
+
let override_gid = conv::opt_u32(m, "override_gid")?;
|
|
198
207
|
// v0.6.7: mount-root symlink protection is on by default; this is
|
|
199
208
|
// the per-mount opt-out. Valid for bind/named-directory mounts —
|
|
200
209
|
// the core rejects it elsewhere at build().
|
|
@@ -251,6 +260,9 @@ impl Sandbox {
|
|
|
251
260
|
if let Some(hp) = host_perms {
|
|
252
261
|
mb = mb.host_permissions(hp);
|
|
253
262
|
}
|
|
263
|
+
if let (Some(uid), Some(gid)) = (override_uid, override_gid) {
|
|
264
|
+
mb = mb.owner(uid, gid);
|
|
265
|
+
}
|
|
254
266
|
if let Some(follow) = follow_root_symlinks {
|
|
255
267
|
mb = mb.follow_root_symlinks(follow);
|
|
256
268
|
}
|
|
@@ -509,6 +521,16 @@ impl Sandbox {
|
|
|
509
521
|
Ok(PullSession::new(handle, join))
|
|
510
522
|
}
|
|
511
523
|
|
|
524
|
+
/// Converge on a live sandbox with this name: connect to (or start) the
|
|
525
|
+
/// persisted one when it already exists, otherwise create it from `opts`.
|
|
526
|
+
/// Concurrent callers converge on the winning identity. The core rejects
|
|
527
|
+
/// the combination with `replace`.
|
|
528
|
+
fn connect_or_create(name: String, opts: RHash) -> Result<Sandbox, Error> {
|
|
529
|
+
let b = Self::build_builder(name, opts)?;
|
|
530
|
+
let inner = block_on(b.connect_or_create()).map_err(error::to_ruby)?;
|
|
531
|
+
Ok(Sandbox::from_inner(inner))
|
|
532
|
+
}
|
|
533
|
+
|
|
512
534
|
/// Restart a previously-defined sandbox by name.
|
|
513
535
|
fn start(name: String, opts: RHash) -> Result<Sandbox, Error> {
|
|
514
536
|
let detached = conv::opt_bool(opts, "detached")?;
|
|
@@ -580,6 +602,13 @@ impl Sandbox {
|
|
|
580
602
|
self.inner.name().to_string()
|
|
581
603
|
}
|
|
582
604
|
|
|
605
|
+
/// The opaque, backend-assigned identity of this persisted sandbox
|
|
606
|
+
/// (v0.6.16). Stable for the sandbox's lifetime, and different once the
|
|
607
|
+
/// same name is removed and recreated.
|
|
608
|
+
fn id(&self) -> String {
|
|
609
|
+
self.inner.id().to_string()
|
|
610
|
+
}
|
|
611
|
+
|
|
583
612
|
/// Run a command (no shell). `args` is an Array of strings; `opts` is a
|
|
584
613
|
/// string-keyed Hash (cwd, user, env, timeout, tty, stdin).
|
|
585
614
|
fn exec(&self, cmd: String, args: Vec<String>, opts: RHash) -> Result<RHash, Error> {
|
|
@@ -644,17 +673,23 @@ impl Sandbox {
|
|
|
644
673
|
Ok(ExecHandle::from_core(handle))
|
|
645
674
|
}
|
|
646
675
|
|
|
647
|
-
/// Graceful stop
|
|
648
|
-
///
|
|
649
|
-
///
|
|
650
|
-
///
|
|
676
|
+
/// Graceful stop (SIGTERM→SIGKILL escalation with a 10s default), routed
|
|
677
|
+
/// straight through the live sandbox like every other lifecycle method here
|
|
678
|
+
/// and like both official bindings (`sdk/python/src/sandbox.rs`,
|
|
679
|
+
/// `sdk/ruby/ext/.../lib.rs`).
|
|
680
|
+
///
|
|
681
|
+
/// This deliberately does NOT re-fetch a `SandboxHandle` by name first. It
|
|
682
|
+
/// used to, back when the core's live `stop` was not identity-scoped; as of
|
|
683
|
+
/// v0.6.16 the core routes `stop` → `request_stop` → `stop_identified(name,
|
|
684
|
+
/// self.identity())`, so going through `self.inner` is what makes the stop
|
|
685
|
+
/// refuse a same-name replacement (`SandboxReplacedError`) instead of
|
|
686
|
+
/// terminating whatever sandbox happens to own the name right now. The old
|
|
687
|
+
/// by-name refetch threw that identity away.
|
|
688
|
+
///
|
|
689
|
+
/// Fine-grained control — a custom timeout or fire-and-return `request_*` —
|
|
690
|
+
/// lives on `SandboxHandle`, obtained via `Sandbox.get`.
|
|
651
691
|
fn stop(&self) -> Result<(), Error> {
|
|
652
|
-
|
|
653
|
-
block_on(async move {
|
|
654
|
-
let handle = microsandbox::sandbox::Sandbox::get(&name).await?;
|
|
655
|
-
handle.stop().await
|
|
656
|
-
})
|
|
657
|
-
.map_err(error::to_ruby)
|
|
692
|
+
block_on(self.inner.stop()).map_err(error::to_ruby)
|
|
658
693
|
}
|
|
659
694
|
|
|
660
695
|
/// Graceful stop, then wait for the process to exit. Returns an exit-status
|
|
@@ -680,6 +715,29 @@ impl Sandbox {
|
|
|
680
715
|
Ok(exit_status_to_hash(status))
|
|
681
716
|
}
|
|
682
717
|
|
|
718
|
+
/// Block until this exact sandbox reaches `status` (no built-in timeout),
|
|
719
|
+
/// returning a fresh handle. A same-name replacement raises
|
|
720
|
+
/// `SandboxReplacedError` instead of silently redirecting the wait.
|
|
721
|
+
fn wait_for_status(&self, status: String) -> Result<SbHandle, Error> {
|
|
722
|
+
let status = sandbox_status_from_str(&status)?;
|
|
723
|
+
let handle = block_on(self.inner.wait_for_status(status)).map_err(error::to_ruby)?;
|
|
724
|
+
Ok(SbHandle::from_inner(handle))
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/// Stop and start this exact sandbox, returning the new live sandbox.
|
|
728
|
+
/// `opts` carries `force`/`timeout`/`detached`.
|
|
729
|
+
fn restart(&self, opts: RHash) -> Result<Sandbox, Error> {
|
|
730
|
+
let options = restart_options(opts)?;
|
|
731
|
+
let inner = block_on(self.inner.restart_with(options)).map_err(error::to_ruby)?;
|
|
732
|
+
Ok(Sandbox::from_inner(inner))
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/// Stop and remove this exact sandbox. `opts` carries `force`/`timeout`.
|
|
736
|
+
fn destroy(&self, opts: RHash) -> Result<(), Error> {
|
|
737
|
+
let options = destroy_options(opts)?;
|
|
738
|
+
block_on(self.inner.destroy_with(options)).map_err(error::to_ruby)
|
|
739
|
+
}
|
|
740
|
+
|
|
683
741
|
/// Live status fetched from the backend (a round-trip per call).
|
|
684
742
|
fn status(&self) -> Result<String, Error> {
|
|
685
743
|
let status = block_on(self.inner.status()).map_err(error::to_ruby)?;
|
|
@@ -866,11 +924,14 @@ impl Sandbox {
|
|
|
866
924
|
//----------------------------------------------------------------------
|
|
867
925
|
|
|
868
926
|
/// Open a native in-process SSH client to this sandbox. `opts`: user, term,
|
|
869
|
-
/// sftp (bool, default true).
|
|
927
|
+
/// sftp (bool, default true), inactivity_timeout (seconds, f64; 0 disables).
|
|
870
928
|
fn ssh_open_client(&self, opts: RHash) -> Result<crate::ssh::SshClient, Error> {
|
|
871
929
|
let user = conv::opt_string(opts, "user")?;
|
|
872
930
|
let term = conv::opt_string(opts, "term")?;
|
|
873
931
|
let sftp = conv::opt::<bool>(opts, "sftp")?.unwrap_or(true);
|
|
932
|
+
let inactivity_timeout = conv::opt_f64(opts, "inactivity_timeout")?
|
|
933
|
+
.map(secs_to_duration)
|
|
934
|
+
.transpose()?;
|
|
874
935
|
let ssh = self.inner.ssh();
|
|
875
936
|
let client = block_on(ssh.open_client_with(move |mut b| {
|
|
876
937
|
if let Some(u) = user {
|
|
@@ -879,6 +940,9 @@ impl Sandbox {
|
|
|
879
940
|
if let Some(t) = term {
|
|
880
941
|
b = b.term(t);
|
|
881
942
|
}
|
|
943
|
+
if let Some(t) = inactivity_timeout {
|
|
944
|
+
b = b.inactivity_timeout(t);
|
|
945
|
+
}
|
|
882
946
|
b.sftp(sftp)
|
|
883
947
|
}))
|
|
884
948
|
.map_err(error::to_ruby)?;
|
|
@@ -886,12 +950,16 @@ impl Sandbox {
|
|
|
886
950
|
}
|
|
887
951
|
|
|
888
952
|
/// Prepare a reusable SSH server endpoint. `opts`: host_key_path,
|
|
889
|
-
/// authorized_keys_path, user, sftp (bool, default true)
|
|
953
|
+
/// authorized_keys_path, user, sftp (bool, default true),
|
|
954
|
+
/// inactivity_timeout (seconds, f64; 0 disables).
|
|
890
955
|
fn ssh_prepare_server(&self, opts: RHash) -> Result<crate::ssh::SshServer, Error> {
|
|
891
956
|
let host_key_path = conv::opt_string(opts, "host_key_path")?;
|
|
892
957
|
let authorized_keys_path = conv::opt_string(opts, "authorized_keys_path")?;
|
|
893
958
|
let user = conv::opt_string(opts, "user")?;
|
|
894
959
|
let sftp = conv::opt::<bool>(opts, "sftp")?.unwrap_or(true);
|
|
960
|
+
let inactivity_timeout = conv::opt_f64(opts, "inactivity_timeout")?
|
|
961
|
+
.map(secs_to_duration)
|
|
962
|
+
.transpose()?;
|
|
895
963
|
let ssh = self.inner.ssh();
|
|
896
964
|
let server = block_on(ssh.prepare_server_with(move |mut b| {
|
|
897
965
|
if let Some(p) = host_key_path {
|
|
@@ -903,6 +971,9 @@ impl Sandbox {
|
|
|
903
971
|
if let Some(u) = user {
|
|
904
972
|
b = b.user(u);
|
|
905
973
|
}
|
|
974
|
+
if let Some(t) = inactivity_timeout {
|
|
975
|
+
b = b.inactivity_timeout(t);
|
|
976
|
+
}
|
|
906
977
|
b.sftp(sftp)
|
|
907
978
|
}))
|
|
908
979
|
.map_err(error::to_ruby)?;
|
|
@@ -2083,6 +2154,55 @@ fn sandbox_status_str(status: SandboxStatus) -> &'static str {
|
|
|
2083
2154
|
}
|
|
2084
2155
|
}
|
|
2085
2156
|
|
|
2157
|
+
/// Parse a Ruby-facing status name back into a core `SandboxStatus`. The Ruby
|
|
2158
|
+
/// layer already validates the seven names (raising `ArgumentError`, the repo's
|
|
2159
|
+
/// idiom for a bad argument value), so reaching the fallback here means the
|
|
2160
|
+
/// native layer was called directly; keep it exhaustive rather than silent.
|
|
2161
|
+
fn sandbox_status_from_str(status: &str) -> Result<SandboxStatus, Error> {
|
|
2162
|
+
match status {
|
|
2163
|
+
"created" => Ok(SandboxStatus::Created),
|
|
2164
|
+
"starting" => Ok(SandboxStatus::Starting),
|
|
2165
|
+
"running" => Ok(SandboxStatus::Running),
|
|
2166
|
+
"draining" => Ok(SandboxStatus::Draining),
|
|
2167
|
+
"paused" => Ok(SandboxStatus::Paused),
|
|
2168
|
+
"stopped" => Ok(SandboxStatus::Stopped),
|
|
2169
|
+
"crashed" => Ok(SandboxStatus::Crashed),
|
|
2170
|
+
other => Err(error::base_error(format!(
|
|
2171
|
+
"unknown sandbox status {other:?} (expected created/starting/running/draining/\
|
|
2172
|
+
paused/stopped/crashed)"
|
|
2173
|
+
))),
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
/// Build the core's `RestartOptions` from a string-keyed Hash
|
|
2178
|
+
/// (`force`/`timeout`/`detached`). An absent `timeout` keeps the core's
|
|
2179
|
+
/// ten-second graceful-shutdown default.
|
|
2180
|
+
fn restart_options(opts: RHash) -> Result<RestartOptions, Error> {
|
|
2181
|
+
let mut options = RestartOptions {
|
|
2182
|
+
force: conv::opt_bool(opts, "force")?,
|
|
2183
|
+
detached: conv::opt_bool(opts, "detached")?,
|
|
2184
|
+
..Default::default()
|
|
2185
|
+
};
|
|
2186
|
+
if let Some(secs) = conv::opt_f64(opts, "timeout")? {
|
|
2187
|
+
options.timeout = secs_to_duration(secs)?;
|
|
2188
|
+
}
|
|
2189
|
+
Ok(options)
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
/// Build the core's `DestroyOptions` from a string-keyed Hash
|
|
2193
|
+
/// (`force`/`timeout`). An absent `timeout` keeps the core's ten-second
|
|
2194
|
+
/// graceful-shutdown default.
|
|
2195
|
+
fn destroy_options(opts: RHash) -> Result<DestroyOptions, Error> {
|
|
2196
|
+
let mut options = DestroyOptions {
|
|
2197
|
+
force: conv::opt_bool(opts, "force")?,
|
|
2198
|
+
..Default::default()
|
|
2199
|
+
};
|
|
2200
|
+
if let Some(secs) = conv::opt_f64(opts, "timeout")? {
|
|
2201
|
+
options.timeout = secs_to_duration(secs)?;
|
|
2202
|
+
}
|
|
2203
|
+
Ok(options)
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2086
2206
|
/// A `std::process::ExitStatus` as a Ruby Hash: `exit_code` (Integer or nil) and
|
|
2087
2207
|
/// `success` (Boolean). Returned by the live `Sandbox#wait` / `#stop_and_wait`.
|
|
2088
2208
|
fn exit_status_to_hash(status: std::process::ExitStatus) -> RHash {
|
|
@@ -2409,6 +2529,13 @@ impl SbHandle {
|
|
|
2409
2529
|
self.inner.name().to_string()
|
|
2410
2530
|
}
|
|
2411
2531
|
|
|
2532
|
+
/// The opaque, backend-assigned identity of the persisted sandbox this
|
|
2533
|
+
/// handle is bound to (v0.6.16). The lifecycle operations below refuse to
|
|
2534
|
+
/// act on a same-name replacement by comparing against it.
|
|
2535
|
+
fn id(&self) -> String {
|
|
2536
|
+
self.inner.id().to_string()
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2412
2539
|
/// Status snapshot captured when the handle was fetched (synchronous).
|
|
2413
2540
|
fn status(&self) -> String {
|
|
2414
2541
|
sandbox_status_str(self.inner.status_snapshot()).to_string()
|
|
@@ -2457,6 +2584,45 @@ impl SbHandle {
|
|
|
2457
2584
|
block_on(self.inner.request_drain()).map_err(error::to_ruby)
|
|
2458
2585
|
}
|
|
2459
2586
|
|
|
2587
|
+
/// Connect when this exact sandbox is running, wait while it is starting,
|
|
2588
|
+
/// or start it when it is created/stopped/crashed. `opts` carries
|
|
2589
|
+
/// `detached`, which applies only when a start is actually required.
|
|
2590
|
+
fn connect_or_start(&self, opts: RHash) -> Result<Sandbox, Error> {
|
|
2591
|
+
let detached = conv::opt_bool(opts, "detached")?;
|
|
2592
|
+
let inner = block_on(async {
|
|
2593
|
+
if detached {
|
|
2594
|
+
self.inner.connect_or_start_detached().await
|
|
2595
|
+
} else {
|
|
2596
|
+
self.inner.connect_or_start().await
|
|
2597
|
+
}
|
|
2598
|
+
})
|
|
2599
|
+
.map_err(error::to_ruby)?;
|
|
2600
|
+
Ok(Sandbox::from_inner(inner))
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
/// Block until this exact sandbox reaches `status` (no built-in timeout),
|
|
2604
|
+
/// returning a fresh handle. A same-name replacement raises
|
|
2605
|
+
/// `SandboxReplacedError` instead of silently redirecting the wait.
|
|
2606
|
+
fn wait_for_status(&self, status: String) -> Result<SbHandle, Error> {
|
|
2607
|
+
let status = sandbox_status_from_str(&status)?;
|
|
2608
|
+
let handle = block_on(self.inner.wait_for_status(status)).map_err(error::to_ruby)?;
|
|
2609
|
+
Ok(SbHandle::from_inner(handle))
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2612
|
+
/// Stop and start this exact sandbox, returning the new live sandbox.
|
|
2613
|
+
/// `opts` carries `force`/`timeout`/`detached`.
|
|
2614
|
+
fn restart(&self, opts: RHash) -> Result<Sandbox, Error> {
|
|
2615
|
+
let options = restart_options(opts)?;
|
|
2616
|
+
let inner = block_on(self.inner.restart_with(options)).map_err(error::to_ruby)?;
|
|
2617
|
+
Ok(Sandbox::from_inner(inner))
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2620
|
+
/// Stop and remove this exact sandbox. `opts` carries `force`/`timeout`.
|
|
2621
|
+
fn destroy(&self, opts: RHash) -> Result<(), Error> {
|
|
2622
|
+
let options = destroy_options(opts)?;
|
|
2623
|
+
block_on(self.inner.destroy_with(options)).map_err(error::to_ruby)
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2460
2626
|
/// Block until the sandbox reaches a terminal state; returns a stop-result
|
|
2461
2627
|
/// Hash (name, status, exit_code, signal, observed_at_ms, source).
|
|
2462
2628
|
fn wait_until_stopped(&self) -> Result<RHash, Error> {
|
|
@@ -2594,6 +2760,10 @@ pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
|
2594
2760
|
"create_with_progress",
|
|
2595
2761
|
function!(Sandbox::create_with_progress, 2),
|
|
2596
2762
|
)?;
|
|
2763
|
+
class.define_singleton_method(
|
|
2764
|
+
"connect_or_create",
|
|
2765
|
+
function!(Sandbox::connect_or_create, 2),
|
|
2766
|
+
)?;
|
|
2597
2767
|
class.define_singleton_method("start", function!(Sandbox::start, 2))?;
|
|
2598
2768
|
class.define_singleton_method("get", function!(Sandbox::get, 1))?;
|
|
2599
2769
|
class.define_singleton_method("list", function!(Sandbox::list, 0))?;
|
|
@@ -2601,6 +2771,7 @@ pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
|
2601
2771
|
class.define_singleton_method("remove", function!(Sandbox::remove, 1))?;
|
|
2602
2772
|
|
|
2603
2773
|
class.define_method("name", method!(Sandbox::name, 0))?;
|
|
2774
|
+
class.define_method("id", method!(Sandbox::id, 0))?;
|
|
2604
2775
|
class.define_method("exec", method!(Sandbox::exec, 3))?;
|
|
2605
2776
|
class.define_method("shell", method!(Sandbox::shell, 2))?;
|
|
2606
2777
|
class.define_method("exec_stream", method!(Sandbox::exec_stream, 3))?;
|
|
@@ -2615,6 +2786,9 @@ pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
|
2615
2786
|
class.define_method("kill", method!(Sandbox::kill, 0))?;
|
|
2616
2787
|
class.define_method("drain", method!(Sandbox::drain, 0))?;
|
|
2617
2788
|
class.define_method("wait", method!(Sandbox::wait, 0))?;
|
|
2789
|
+
class.define_method("wait_for_status", method!(Sandbox::wait_for_status, 1))?;
|
|
2790
|
+
class.define_method("restart", method!(Sandbox::restart, 1))?;
|
|
2791
|
+
class.define_method("destroy", method!(Sandbox::destroy, 1))?;
|
|
2618
2792
|
class.define_method("status", method!(Sandbox::status, 0))?;
|
|
2619
2793
|
class.define_method("owns_lifecycle", method!(Sandbox::owns_lifecycle, 0))?;
|
|
2620
2794
|
class.define_method("detach", method!(Sandbox::detach, 0))?;
|
|
@@ -2654,6 +2828,7 @@ pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
|
2654
2828
|
|
|
2655
2829
|
let handle = native.define_class("SandboxHandle", ruby.class_object())?;
|
|
2656
2830
|
handle.define_method("name", method!(SbHandle::name, 0))?;
|
|
2831
|
+
handle.define_method("id", method!(SbHandle::id, 0))?;
|
|
2657
2832
|
handle.define_method("status", method!(SbHandle::status, 0))?;
|
|
2658
2833
|
handle.define_method("created_at_ms", method!(SbHandle::created_at_ms, 0))?;
|
|
2659
2834
|
handle.define_method("updated_at_ms", method!(SbHandle::updated_at_ms, 0))?;
|
|
@@ -2661,6 +2836,10 @@ pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
|
2661
2836
|
handle.define_method("stop_with_timeout", method!(SbHandle::stop_with_timeout, 1))?;
|
|
2662
2837
|
handle.define_method("kill", method!(SbHandle::kill, 0))?;
|
|
2663
2838
|
handle.define_method("kill_with_timeout", method!(SbHandle::kill_with_timeout, 1))?;
|
|
2839
|
+
handle.define_method("connect_or_start", method!(SbHandle::connect_or_start, 1))?;
|
|
2840
|
+
handle.define_method("wait_for_status", method!(SbHandle::wait_for_status, 1))?;
|
|
2841
|
+
handle.define_method("restart", method!(SbHandle::restart, 1))?;
|
|
2842
|
+
handle.define_method("destroy", method!(SbHandle::destroy, 1))?;
|
|
2664
2843
|
handle.define_method("request_stop", method!(SbHandle::request_stop, 0))?;
|
|
2665
2844
|
handle.define_method("request_kill", method!(SbHandle::request_kill, 0))?;
|
|
2666
2845
|
handle.define_method("request_drain", method!(SbHandle::request_drain, 0))?;
|
data/lib/microsandbox/errors.rb
CHANGED
|
@@ -35,6 +35,13 @@ module Microsandbox
|
|
|
35
35
|
define_error(:SandboxNotRunningError, "sandbox-not-running")
|
|
36
36
|
define_error(:SandboxAlreadyExistsError, "sandbox-already-exists")
|
|
37
37
|
define_error(:SandboxStillRunningError, "sandbox-still-running")
|
|
38
|
+
# v0.6.16: a lifecycle operation was aimed at a sandbox identity that no
|
|
39
|
+
# longer owns the name — the name was removed and recreated behind the
|
|
40
|
+
# caller's back. Raised by the identity-checked convergent APIs
|
|
41
|
+
# (`wait_for_status`, `restart`, `destroy`, `connect_or_start`) rather than
|
|
42
|
+
# letting them act on the replacement. Mirrors the Python SDK's
|
|
43
|
+
# SandboxReplacedError.
|
|
44
|
+
define_error(:SandboxReplacedError, "sandbox-replaced")
|
|
38
45
|
|
|
39
46
|
# Execution errors --------------------------------------------------------
|
|
40
47
|
define_error(:ExecTimeoutError, "exec-timeout")
|