microsandbox-rb 0.5.9 → 0.5.10
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 +77 -0
- data/Cargo.lock +90 -45
- data/DESIGN.md +7 -3
- data/README.md +91 -26
- data/ext/microsandbox/Cargo.toml +4 -4
- data/ext/microsandbox/extconf.rb +6 -2
- data/ext/microsandbox/src/backend.rs +170 -0
- data/ext/microsandbox/src/error.rs +6 -0
- data/ext/microsandbox/src/image.rs +7 -7
- data/ext/microsandbox/src/lib.rs +27 -4
- data/ext/microsandbox/src/sandbox.rs +172 -58
- data/ext/microsandbox/src/volume.rs +6 -1
- data/lib/microsandbox/errors.rb +6 -0
- data/lib/microsandbox/exec_handle.rb +14 -11
- data/lib/microsandbox/fs.rb +7 -7
- data/lib/microsandbox/image.rb +1 -1
- data/lib/microsandbox/network.rb +19 -19
- data/lib/microsandbox/patch.rb +8 -8
- data/lib/microsandbox/sandbox.rb +199 -75
- data/lib/microsandbox/snapshot.rb +2 -2
- data/lib/microsandbox/ssh.rb +2 -2
- data/lib/microsandbox/version.rb +2 -2
- data/lib/microsandbox/volume.rb +3 -3
- data/lib/microsandbox.rb +61 -0
- data/sig/microsandbox.rbs +31 -13
- metadata +2 -1
data/ext/microsandbox/extconf.rb
CHANGED
|
@@ -13,12 +13,16 @@ MSRV = Gem::Version.new("1.91")
|
|
|
13
13
|
rustc_version = begin
|
|
14
14
|
out = `rustc --version 2>/dev/null`
|
|
15
15
|
out[/\d+\.\d+(\.\d+)?/] && Gem::Version.new(out[/\d+\.\d+(\.\d+)?/])
|
|
16
|
-
rescue
|
|
16
|
+
rescue
|
|
17
17
|
nil
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
if rustc_version && rustc_version < MSRV
|
|
21
|
-
which_rustc =
|
|
21
|
+
which_rustc = begin
|
|
22
|
+
`which rustc 2>/dev/null`.strip
|
|
23
|
+
rescue
|
|
24
|
+
""
|
|
25
|
+
end
|
|
22
26
|
abort(<<~MSG)
|
|
23
27
|
|
|
24
28
|
[microsandbox-rb] Rust #{rustc_version} is too old — the embedded core requires rustc >= #{MSRV}.
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
//! Backend routing: the ambient process-wide backend and its selection surface.
|
|
2
|
+
//!
|
|
3
|
+
//! Mirrors the official Python (`sdk/python/src/lib.rs`) and Node
|
|
4
|
+
//! (`sdk/node-ts/native/runtime_config.rs`) bindings. As of upstream v0.5.8
|
|
5
|
+
//! (PR #754) every operation routes through a [`microsandbox::Backend`]: the
|
|
6
|
+
//! ambient default is a lazily-initialised [`microsandbox::LocalBackend`], and
|
|
7
|
+
//! callers may install a different default process-wide or for a scoped block.
|
|
8
|
+
//!
|
|
9
|
+
//! Local-only operations (image cache, aggregate metrics, `msb` path) need a
|
|
10
|
+
//! concrete `&LocalBackend`; [`local_backend`] resolves the ambient default and
|
|
11
|
+
//! downcasts it, surfacing a clean [`MicrosandboxError::Unsupported`] under a
|
|
12
|
+
//! cloud backend (exactly as the pyo3 `resolve_local` helper does). The backend
|
|
13
|
+
//! selection setters (`set_default_backend` / `with_backend` push-pop /
|
|
14
|
+
//! `default_backend_kind`) deliberately expose only a `kind`/`url`/`api_key`/
|
|
15
|
+
//! `profile` facade — the raw `LocalBackend`/`CloudBackend` builders stay hidden,
|
|
16
|
+
//! matching Python and Node.
|
|
17
|
+
|
|
18
|
+
use std::collections::HashMap;
|
|
19
|
+
use std::sync::atomic::{AtomicU32, Ordering};
|
|
20
|
+
use std::sync::{Arc, Mutex, OnceLock};
|
|
21
|
+
|
|
22
|
+
use magnus::{function, prelude::*, Error, RModule, Ruby};
|
|
23
|
+
use microsandbox::{Backend, MicrosandboxError};
|
|
24
|
+
|
|
25
|
+
use crate::error;
|
|
26
|
+
use crate::runtime::block_on;
|
|
27
|
+
|
|
28
|
+
/// Resolve the ambient default backend, requiring it to be local.
|
|
29
|
+
///
|
|
30
|
+
/// Returns the `Arc<dyn Backend>` so the caller can keep it alive while
|
|
31
|
+
/// borrowing `&LocalBackend` from it (`as_local()` borrows the `Arc`). Pure
|
|
32
|
+
/// Rust — safe to call inside `block_on` (no Ruby C API), and it returns a raw
|
|
33
|
+
/// `MicrosandboxError` so the Ruby-exception mapping happens *after* `block_on`
|
|
34
|
+
/// re-acquires the GVL. Cloud backends yield `Unsupported`, mirroring pyo3's
|
|
35
|
+
/// `resolve_local`.
|
|
36
|
+
pub fn local_backend() -> Result<Arc<dyn Backend>, MicrosandboxError> {
|
|
37
|
+
let backend = microsandbox::default_backend();
|
|
38
|
+
if backend.as_local().is_some() {
|
|
39
|
+
Ok(backend)
|
|
40
|
+
} else {
|
|
41
|
+
Err(MicrosandboxError::Unsupported {
|
|
42
|
+
feature: "this operation requires a local backend".into(),
|
|
43
|
+
available_when: "with the local backend (the default)".into(),
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// Resolve the ambient backend (requiring it to be local), then run `op` with a
|
|
49
|
+
/// borrowed `&LocalBackend` inside the blocking runtime, mapping any core error
|
|
50
|
+
/// to a Ruby exception. Local-only operations (image cache, aggregate metrics)
|
|
51
|
+
/// share this instead of repeating the resolve/downcast dance; the single
|
|
52
|
+
/// `as_local()` unwrap is provably infallible — [`local_backend`] just checked
|
|
53
|
+
/// it and returns the same `Arc` kept alive for the borrow — so it lives here
|
|
54
|
+
/// once rather than at every call site.
|
|
55
|
+
pub fn with_local_backend<T>(
|
|
56
|
+
op: impl AsyncFnOnce(µsandbox::LocalBackend) -> Result<T, MicrosandboxError>,
|
|
57
|
+
) -> Result<T, Error> {
|
|
58
|
+
block_on(async move {
|
|
59
|
+
let backend = local_backend()?;
|
|
60
|
+
let local = backend
|
|
61
|
+
.as_local()
|
|
62
|
+
.expect("local_backend() guarantees a local backend");
|
|
63
|
+
op(local).await
|
|
64
|
+
})
|
|
65
|
+
.map_err(error::to_ruby)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// Build an `Arc<dyn Backend>` from the SDK facade. Ported from pyo3
|
|
69
|
+
/// `build_backend` (`sdk/python/src/lib.rs`) / node `build_backend`. Synchronous
|
|
70
|
+
/// (no network I/O): cloud construction only builds the HTTP client. Runs on the
|
|
71
|
+
/// Ruby thread with the GVL held, so it may map errors to Ruby directly.
|
|
72
|
+
fn build_backend(
|
|
73
|
+
kind: String,
|
|
74
|
+
url: Option<String>,
|
|
75
|
+
api_key: Option<String>,
|
|
76
|
+
profile: Option<String>,
|
|
77
|
+
) -> Result<Arc<dyn Backend>, Error> {
|
|
78
|
+
match kind.trim().to_ascii_lowercase().as_str() {
|
|
79
|
+
"local" => Ok(Arc::new(microsandbox::LocalBackend::lazy())),
|
|
80
|
+
"cloud" => {
|
|
81
|
+
let cloud = match profile {
|
|
82
|
+
Some(profile) => microsandbox::CloudBackend::from_profile(&profile),
|
|
83
|
+
None => match (url, api_key) {
|
|
84
|
+
(Some(url), Some(api_key)) => microsandbox::CloudBackend::new(url, api_key),
|
|
85
|
+
_ => {
|
|
86
|
+
return Err(error::to_ruby(MicrosandboxError::InvalidConfig(
|
|
87
|
+
"cloud backend requires url + api_key or profile".into(),
|
|
88
|
+
)))
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
.map_err(error::to_ruby)?;
|
|
93
|
+
Ok(Arc::new(cloud))
|
|
94
|
+
}
|
|
95
|
+
other => Err(error::to_ruby(MicrosandboxError::InvalidConfig(format!(
|
|
96
|
+
"backend kind must be 'local' or 'cloud', got {other:?}"
|
|
97
|
+
)))),
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// Install a process-wide default backend. Synchronous (an `RwLock` write) — no
|
|
102
|
+
/// `block_on`. Mirrors Python `set_default_backend` / Node `setDefaultBackend`.
|
|
103
|
+
fn set_default_backend(
|
|
104
|
+
kind: String,
|
|
105
|
+
url: Option<String>,
|
|
106
|
+
api_key: Option<String>,
|
|
107
|
+
profile: Option<String>,
|
|
108
|
+
) -> Result<(), Error> {
|
|
109
|
+
microsandbox::set_default_backend(build_backend(kind, url, api_key, profile)?);
|
|
110
|
+
Ok(())
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Scoped-override registry. `with_backend` in Ruby is a swap-and-restore around
|
|
114
|
+
// a synchronous block, so it cannot use the core's async task-local
|
|
115
|
+
// `with_backend`. We mirror Node's process-wide push/pop token registry: push
|
|
116
|
+
// swaps the default and stores the previous backend under a fresh token; pop
|
|
117
|
+
// restores it. The Ruby wrapper drives this through an `ensure` block.
|
|
118
|
+
static NEXT_BACKEND_SCOPE: AtomicU32 = AtomicU32::new(1);
|
|
119
|
+
static BACKEND_SCOPES: OnceLock<Mutex<HashMap<u32, Arc<dyn Backend>>>> = OnceLock::new();
|
|
120
|
+
|
|
121
|
+
fn backend_scopes() -> &'static Mutex<HashMap<u32, Arc<dyn Backend>>> {
|
|
122
|
+
BACKEND_SCOPES.get_or_init(|| Mutex::new(HashMap::new()))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// Swap in a new default backend and return a token for restoring the previous
|
|
126
|
+
/// one via [`pop_default_backend`]. Process-wide while active (not task-local):
|
|
127
|
+
/// concurrent Ruby threads observe the swapped default.
|
|
128
|
+
fn push_default_backend(
|
|
129
|
+
kind: String,
|
|
130
|
+
url: Option<String>,
|
|
131
|
+
api_key: Option<String>,
|
|
132
|
+
profile: Option<String>,
|
|
133
|
+
) -> Result<u32, Error> {
|
|
134
|
+
let previous = microsandbox::swap_default_backend(build_backend(kind, url, api_key, profile)?);
|
|
135
|
+
let token = NEXT_BACKEND_SCOPE.fetch_add(1, Ordering::Relaxed);
|
|
136
|
+
backend_scopes()
|
|
137
|
+
.lock()
|
|
138
|
+
.map_err(|_| error::base_error("backend scope registry poisoned"))?
|
|
139
|
+
.insert(token, previous);
|
|
140
|
+
Ok(token)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/// Restore the backend saved by [`push_default_backend`].
|
|
144
|
+
fn pop_default_backend(token: u32) -> Result<(), Error> {
|
|
145
|
+
let previous = backend_scopes()
|
|
146
|
+
.lock()
|
|
147
|
+
.map_err(|_| error::base_error("backend scope registry poisoned"))?
|
|
148
|
+
.remove(&token)
|
|
149
|
+
.ok_or_else(|| error::base_error("unknown backend scope token"))?;
|
|
150
|
+
microsandbox::set_default_backend(previous);
|
|
151
|
+
Ok(())
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/// The active default backend kind: `"local"` or `"cloud"`. First call lazily
|
|
155
|
+
/// resolves the env/profile/config ladder. Synchronous (an `RwLock` read).
|
|
156
|
+
fn default_backend_kind() -> String {
|
|
157
|
+
match microsandbox::default_backend().kind() {
|
|
158
|
+
microsandbox::BackendKind::Local => "local",
|
|
159
|
+
microsandbox::BackendKind::Cloud => "cloud",
|
|
160
|
+
}
|
|
161
|
+
.to_string()
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
pub fn define(_ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
165
|
+
native.define_singleton_method("set_default_backend", function!(set_default_backend, 4))?;
|
|
166
|
+
native.define_singleton_method("push_default_backend", function!(push_default_backend, 4))?;
|
|
167
|
+
native.define_singleton_method("pop_default_backend", function!(pop_default_backend, 1))?;
|
|
168
|
+
native.define_singleton_method("default_backend_kind", function!(default_backend_kind, 0))?;
|
|
169
|
+
Ok(())
|
|
170
|
+
}
|
|
@@ -28,6 +28,12 @@ fn class_name(err: &MicrosandboxError) -> &'static str {
|
|
|
28
28
|
MetricsDisabled(_) => "MetricsDisabledError",
|
|
29
29
|
MetricsUnavailable(_) => "MetricsUnavailableError",
|
|
30
30
|
AgentClient(AgentClientError::UnsupportedOperation { .. }) => "UnsupportedOperationError",
|
|
31
|
+
// Backend routing (v0.5.8 / PR #754). `Unsupported` is reachable on the
|
|
32
|
+
// local backend too (e.g. `Volume::path` on a cloud volume, snapshot
|
|
33
|
+
// ops), so it must map even for local-only use. Distinct from the agent
|
|
34
|
+
// client's `UnsupportedOperation` above.
|
|
35
|
+
CloudHttp { .. } => "CloudHttpError",
|
|
36
|
+
Unsupported { .. } => "UnsupportedError",
|
|
31
37
|
_ => "Error",
|
|
32
38
|
}
|
|
33
39
|
}
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
use magnus::{function, prelude::*, Error, RArray, RHash, RModule, Ruby};
|
|
9
9
|
use microsandbox::image::{Image, ImageDetail, ImageHandle, ImagePruneReport};
|
|
10
10
|
|
|
11
|
-
use crate::
|
|
12
|
-
use crate::runtime::
|
|
11
|
+
use crate::backend::with_local_backend;
|
|
12
|
+
use crate::runtime::ruby;
|
|
13
13
|
|
|
14
14
|
fn handle_to_hash(h: &ImageHandle) -> RHash {
|
|
15
15
|
let hash = ruby().hash_new();
|
|
@@ -76,12 +76,12 @@ fn report_to_hash(report: ImagePruneReport) -> RHash {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
fn get(reference: String) -> Result<RHash, Error> {
|
|
79
|
-
let handle =
|
|
79
|
+
let handle = with_local_backend(async |local| Image::get(local, &reference).await)?;
|
|
80
80
|
Ok(handle_to_hash(&handle))
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
fn list() -> Result<RArray, Error> {
|
|
84
|
-
let handles =
|
|
84
|
+
let handles = with_local_backend(async |local| Image::list(local).await)?;
|
|
85
85
|
let arr = ruby().ary_new();
|
|
86
86
|
for h in handles.iter() {
|
|
87
87
|
arr.push(handle_to_hash(h))?;
|
|
@@ -90,16 +90,16 @@ fn list() -> Result<RArray, Error> {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
fn inspect(reference: String) -> Result<RHash, Error> {
|
|
93
|
-
let detail =
|
|
93
|
+
let detail = with_local_backend(async |local| Image::inspect(local, &reference).await)?;
|
|
94
94
|
Ok(detail_to_hash(detail))
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
fn remove(reference: String, force: bool) -> Result<(), Error> {
|
|
98
|
-
|
|
98
|
+
with_local_backend(async |local| Image::remove(local, &reference, force).await)
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
fn prune() -> Result<RHash, Error> {
|
|
102
|
-
let report =
|
|
102
|
+
let report = with_local_backend(async |local| Image::prune(local).await)?;
|
|
103
103
|
Ok(report_to_hash(report))
|
|
104
104
|
}
|
|
105
105
|
|
data/ext/microsandbox/src/lib.rs
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
//! surface is the pure-Ruby layer in `lib/microsandbox/`.
|
|
10
10
|
|
|
11
11
|
mod agent;
|
|
12
|
+
mod backend;
|
|
12
13
|
mod conv;
|
|
13
14
|
mod error;
|
|
14
15
|
mod exec;
|
|
@@ -31,8 +32,9 @@ fn version() -> String {
|
|
|
31
32
|
/// Ruby Hash. Mirrors the official `all_sandbox_metrics` / `allSandboxMetrics`
|
|
32
33
|
/// helpers (Python/Node/Go).
|
|
33
34
|
fn all_sandbox_metrics() -> Result<RHash, Error> {
|
|
34
|
-
let map =
|
|
35
|
-
|
|
35
|
+
let map = backend::with_local_backend(async |local| {
|
|
36
|
+
microsandbox::sandbox::all_sandbox_metrics(local).await
|
|
37
|
+
})?;
|
|
36
38
|
let hash = runtime::ruby().hash_new();
|
|
37
39
|
for (name, metrics) in &map {
|
|
38
40
|
hash.aset(name.as_str(), sandbox::metrics_to_hash(metrics))?;
|
|
@@ -55,9 +57,25 @@ fn set_runtime_msb_path(path: String) {
|
|
|
55
57
|
microsandbox::config::set_sdk_msb_path(path);
|
|
56
58
|
}
|
|
57
59
|
|
|
58
|
-
///
|
|
60
|
+
/// Override the resolved `libkrunfw` path (SDK tier of the resolver). Set-once
|
|
61
|
+
/// per process; the `MSB_LIBKRUNFW_PATH` env var still takes precedence. Mirrors
|
|
62
|
+
/// `set_runtime_msb_path` for the libkrunfw shared library.
|
|
63
|
+
fn set_runtime_libkrunfw_path(path: String) {
|
|
64
|
+
microsandbox::config::set_sdk_libkrunfw_path(path);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// The currently-resolved `msb` runtime path. Synchronous (filesystem probes,
|
|
68
|
+
/// no async) — runs on the Ruby thread; resolves the path from the local
|
|
69
|
+
/// backend's config.
|
|
59
70
|
fn resolved_msb_path() -> Result<String, Error> {
|
|
60
|
-
let
|
|
71
|
+
let backend = microsandbox::default_backend();
|
|
72
|
+
let local = backend.as_local().ok_or_else(|| {
|
|
73
|
+
error::to_ruby(microsandbox::MicrosandboxError::Unsupported {
|
|
74
|
+
feature: "resolved_msb_path requires a local backend".into(),
|
|
75
|
+
available_when: "with the local backend".into(),
|
|
76
|
+
})
|
|
77
|
+
})?;
|
|
78
|
+
let path = microsandbox::config::resolve_msb_path(local.config()).map_err(error::to_ruby)?;
|
|
61
79
|
Ok(path.to_string_lossy().into_owned())
|
|
62
80
|
}
|
|
63
81
|
|
|
@@ -72,9 +90,14 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
72
90
|
native.define_singleton_method("install", function!(install, 0))?;
|
|
73
91
|
native.define_singleton_method("installed?", function!(is_installed, 0))?;
|
|
74
92
|
native.define_singleton_method("set_runtime_msb_path", function!(set_runtime_msb_path, 1))?;
|
|
93
|
+
native.define_singleton_method(
|
|
94
|
+
"set_runtime_libkrunfw_path",
|
|
95
|
+
function!(set_runtime_libkrunfw_path, 1),
|
|
96
|
+
)?;
|
|
75
97
|
native.define_singleton_method("resolved_msb_path", function!(resolved_msb_path, 0))?;
|
|
76
98
|
native.define_singleton_method("all_sandbox_metrics", function!(all_sandbox_metrics, 0))?;
|
|
77
99
|
|
|
100
|
+
backend::define(ruby, &native)?;
|
|
78
101
|
sandbox::define(ruby, &native)?;
|
|
79
102
|
exec::define(ruby, &native)?;
|
|
80
103
|
stream::define(ruby, &native)?;
|
|
@@ -209,27 +209,36 @@ impl Sandbox {
|
|
|
209
209
|
Ok(Sandbox::from_inner(inner))
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
-
///
|
|
213
|
-
|
|
212
|
+
/// A controllable handle for a sandbox by name (running or not). Carries
|
|
213
|
+
/// metadata accessors and the full lifecycle surface (see `SbHandle`).
|
|
214
|
+
fn get(name: String) -> Result<SbHandle, Error> {
|
|
214
215
|
let handle = block_on(microsandbox::Sandbox::get(&name)).map_err(error::to_ruby)?;
|
|
215
|
-
Ok(
|
|
216
|
+
Ok(SbHandle::from_inner(handle))
|
|
216
217
|
}
|
|
217
218
|
|
|
218
|
-
/// All sandboxes as
|
|
219
|
+
/// All sandboxes as controllable handles.
|
|
219
220
|
fn list() -> Result<RArray, Error> {
|
|
220
221
|
let handles = block_on(microsandbox::Sandbox::list()).map_err(error::to_ruby)?;
|
|
221
|
-
|
|
222
|
+
let arr = ruby().ary_new();
|
|
223
|
+
for h in handles {
|
|
224
|
+
arr.push(SbHandle::from_inner(h))?;
|
|
225
|
+
}
|
|
226
|
+
Ok(arr)
|
|
222
227
|
}
|
|
223
228
|
|
|
224
|
-
/// Sandboxes filtered by required `key=value` labels (AND-matched)
|
|
225
|
-
/// carries a string→string `labels` map.
|
|
229
|
+
/// Sandboxes filtered by required `key=value` labels (AND-matched), as
|
|
230
|
+
/// controllable handles. `opts` carries a string→string `labels` map.
|
|
226
231
|
fn list_with(opts: RHash) -> Result<RArray, Error> {
|
|
227
232
|
let mut filter = SandboxFilter::new();
|
|
228
233
|
for (k, v) in conv::opt_string_map(opts, "labels")? {
|
|
229
234
|
filter = filter.label(k, v);
|
|
230
235
|
}
|
|
231
236
|
let handles = block_on(microsandbox::Sandbox::list_with(filter)).map_err(error::to_ruby)?;
|
|
232
|
-
|
|
237
|
+
let arr = ruby().ary_new();
|
|
238
|
+
for h in handles {
|
|
239
|
+
arr.push(SbHandle::from_inner(h))?;
|
|
240
|
+
}
|
|
241
|
+
Ok(arr)
|
|
233
242
|
}
|
|
234
243
|
|
|
235
244
|
/// Remove a (stopped) sandbox by name.
|
|
@@ -286,44 +295,46 @@ impl Sandbox {
|
|
|
286
295
|
Ok(ExecHandle::from_core(handle))
|
|
287
296
|
}
|
|
288
297
|
|
|
289
|
-
/// Graceful stop
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
298
|
+
/// Graceful stop. Mirrors the official SDKs: the live handle routes through
|
|
299
|
+
/// a freshly fetched `SandboxHandle::stop` (SIGTERM→SIGKILL escalation with
|
|
300
|
+
/// a 10s default). Fine-grained control — a custom timeout or fire-and-
|
|
301
|
+
/// return `request_*` — lives on `SandboxHandle`, obtained via `Sandbox.get`.
|
|
302
|
+
fn stop(&self) -> Result<(), Error> {
|
|
303
|
+
let name = self.inner.name().to_string();
|
|
304
|
+
block_on(async move {
|
|
305
|
+
let handle = microsandbox::sandbox::Sandbox::get(&name).await?;
|
|
306
|
+
handle.stop().await
|
|
307
|
+
})
|
|
295
308
|
.map_err(error::to_ruby)
|
|
296
309
|
}
|
|
297
310
|
|
|
298
|
-
///
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
}
|
|
304
|
-
.map_err(error::to_ruby)
|
|
311
|
+
/// Graceful stop, then wait for the process to exit. Returns an exit-status
|
|
312
|
+
/// Hash (`exit_code`, `success`). Local backend only.
|
|
313
|
+
fn stop_and_wait(&self) -> Result<RHash, Error> {
|
|
314
|
+
let status = block_on(self.inner.stop_and_wait()).map_err(error::to_ruby)?;
|
|
315
|
+
Ok(exit_status_to_hash(status))
|
|
305
316
|
}
|
|
306
317
|
|
|
307
|
-
///
|
|
308
|
-
fn
|
|
309
|
-
block_on(self.inner.
|
|
318
|
+
/// Force kill (SIGKILL).
|
|
319
|
+
fn kill(&self) -> Result<(), Error> {
|
|
320
|
+
block_on(self.inner.kill()).map_err(error::to_ruby)
|
|
310
321
|
}
|
|
311
322
|
|
|
312
|
-
///
|
|
313
|
-
fn
|
|
314
|
-
block_on(self.inner.
|
|
323
|
+
/// Trigger a graceful drain (SIGUSR1 on local).
|
|
324
|
+
fn drain(&self) -> Result<(), Error> {
|
|
325
|
+
block_on(self.inner.drain()).map_err(error::to_ruby)
|
|
315
326
|
}
|
|
316
327
|
|
|
317
|
-
///
|
|
318
|
-
fn
|
|
319
|
-
block_on(self.inner.
|
|
328
|
+
/// Wait for the process to exit. Returns an exit-status Hash. Local only.
|
|
329
|
+
fn wait(&self) -> Result<RHash, Error> {
|
|
330
|
+
let status = block_on(self.inner.wait()).map_err(error::to_ruby)?;
|
|
331
|
+
Ok(exit_status_to_hash(status))
|
|
320
332
|
}
|
|
321
333
|
|
|
322
|
-
///
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
Ok(stop_result_to_hash(&result))
|
|
334
|
+
/// Live status fetched from the backend (a round-trip per call).
|
|
335
|
+
fn status(&self) -> Result<String, Error> {
|
|
336
|
+
let status = block_on(self.inner.status()).map_err(error::to_ruby)?;
|
|
337
|
+
Ok(sandbox_status_str(status).to_string())
|
|
327
338
|
}
|
|
328
339
|
|
|
329
340
|
/// Whether this handle owns the sandbox process lifecycle (a synchronous,
|
|
@@ -1064,6 +1075,7 @@ struct ExecOpts {
|
|
|
1064
1075
|
timeout: Option<Duration>,
|
|
1065
1076
|
tty: bool,
|
|
1066
1077
|
stdin: Option<Vec<u8>>,
|
|
1078
|
+
stdin_pipe: bool,
|
|
1067
1079
|
rlimits: Vec<(RlimitResource, u64, u64)>,
|
|
1068
1080
|
}
|
|
1069
1081
|
|
|
@@ -1078,6 +1090,7 @@ impl ExecOpts {
|
|
|
1078
1090
|
timeout: conv::opt_f64(opts, "timeout")?.map(Duration::from_secs_f64),
|
|
1079
1091
|
tty: conv::opt_bool(opts, "tty")?,
|
|
1080
1092
|
stdin,
|
|
1093
|
+
stdin_pipe: conv::opt_bool(opts, "stdin_pipe")?,
|
|
1081
1094
|
rlimits: parse_rlimits(opts)?,
|
|
1082
1095
|
})
|
|
1083
1096
|
}
|
|
@@ -1104,7 +1117,13 @@ impl ExecOpts {
|
|
|
1104
1117
|
if self.tty {
|
|
1105
1118
|
b = b.tty(true);
|
|
1106
1119
|
}
|
|
1107
|
-
|
|
1120
|
+
// Pipe mode opens a writable stdin sink (lifted out by ExecHandle via
|
|
1121
|
+
// `take_stdin`); bytes mode feeds a fixed buffer and closes. The core's
|
|
1122
|
+
// `StdinMode` is a single enum, so the two are mutually exclusive — pipe
|
|
1123
|
+
// wins if a caller somehow sets both.
|
|
1124
|
+
if self.stdin_pipe {
|
|
1125
|
+
b = b.stdin_pipe();
|
|
1126
|
+
} else if let Some(stdin) = self.stdin {
|
|
1108
1127
|
b = b.stdin_bytes(stdin);
|
|
1109
1128
|
}
|
|
1110
1129
|
for (resource, soft, hard) in self.rlimits {
|
|
@@ -1195,7 +1214,13 @@ pub(crate) fn metrics_to_hash(m: &SandboxMetrics) -> RHash {
|
|
|
1195
1214
|
}
|
|
1196
1215
|
|
|
1197
1216
|
fn sandbox_status_str(status: SandboxStatus) -> &'static str {
|
|
1217
|
+
// Lowercased `Debug` names, matching the official SDKs' `format!("{:?}")`.
|
|
1218
|
+
// `Created`/`Starting` are new in v0.5.8 (cloud-only today). The match is
|
|
1219
|
+
// intentionally exhaustive — no wildcard — so a future upstream variant
|
|
1220
|
+
// surfaces as a compile error rather than a silent fallback.
|
|
1198
1221
|
match status {
|
|
1222
|
+
SandboxStatus::Created => "created",
|
|
1223
|
+
SandboxStatus::Starting => "starting",
|
|
1199
1224
|
SandboxStatus::Running => "running",
|
|
1200
1225
|
SandboxStatus::Draining => "draining",
|
|
1201
1226
|
SandboxStatus::Paused => "paused",
|
|
@@ -1204,6 +1229,15 @@ fn sandbox_status_str(status: SandboxStatus) -> &'static str {
|
|
|
1204
1229
|
}
|
|
1205
1230
|
}
|
|
1206
1231
|
|
|
1232
|
+
/// A `std::process::ExitStatus` as a Ruby Hash: `exit_code` (Integer or nil) and
|
|
1233
|
+
/// `success` (Boolean). Returned by the live `Sandbox#wait` / `#stop_and_wait`.
|
|
1234
|
+
fn exit_status_to_hash(status: std::process::ExitStatus) -> RHash {
|
|
1235
|
+
let hash = ruby().hash_new();
|
|
1236
|
+
let _ = hash.aset("exit_code", status.code());
|
|
1237
|
+
let _ = hash.aset("success", status.success());
|
|
1238
|
+
hash
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1207
1241
|
fn stop_result_to_hash(result: &SandboxStopResult) -> RHash {
|
|
1208
1242
|
let hash = ruby().hash_new();
|
|
1209
1243
|
let _ = hash.aset("name", result.name.clone());
|
|
@@ -1215,19 +1249,85 @@ fn stop_result_to_hash(result: &SandboxStopResult) -> RHash {
|
|
|
1215
1249
|
hash
|
|
1216
1250
|
}
|
|
1217
1251
|
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1252
|
+
//--------------------------------------------------------------------------------------------------
|
|
1253
|
+
// SandboxHandle — the controllable lightweight handle
|
|
1254
|
+
//--------------------------------------------------------------------------------------------------
|
|
1255
|
+
|
|
1256
|
+
/// Wraps a core `microsandbox::sandbox::SandboxHandle` (returned by
|
|
1257
|
+
/// `Sandbox.get`/`list`/`list_with`). Carries metadata accessors plus the rich
|
|
1258
|
+
/// lifecycle surface that moved off the live `Sandbox` in v0.5.8 — mirroring the
|
|
1259
|
+
/// official Python (`PySandboxHandle`) and Node (`SandboxHandle`) SDKs. Status is
|
|
1260
|
+
/// a synchronous snapshot read off the handle (no round-trip).
|
|
1261
|
+
#[magnus::wrap(class = "Microsandbox::Native::SandboxHandle", free_immediately, size)]
|
|
1262
|
+
pub struct SbHandle {
|
|
1263
|
+
inner: SandboxHandle,
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
impl SbHandle {
|
|
1267
|
+
fn from_inner(inner: SandboxHandle) -> Self {
|
|
1268
|
+
Self { inner }
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
fn name(&self) -> String {
|
|
1272
|
+
self.inner.name().to_string()
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
/// Status snapshot captured when the handle was fetched (synchronous).
|
|
1276
|
+
fn status(&self) -> String {
|
|
1277
|
+
sandbox_status_str(self.inner.status_snapshot()).to_string()
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
fn created_at_ms(&self) -> Option<i64> {
|
|
1281
|
+
self.inner.created_at().map(|dt| dt.timestamp_millis())
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
fn updated_at_ms(&self) -> Option<i64> {
|
|
1285
|
+
self.inner.updated_at().map(|dt| dt.timestamp_millis())
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
/// Graceful stop (SIGTERM→SIGKILL escalation, 10s default) and wait.
|
|
1289
|
+
fn stop(&self) -> Result<(), Error> {
|
|
1290
|
+
block_on(self.inner.stop()).map_err(error::to_ruby)
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
/// Graceful stop with a custom escalation timeout (seconds).
|
|
1294
|
+
fn stop_with_timeout(&self, secs: f64) -> Result<(), Error> {
|
|
1295
|
+
block_on(self.inner.stop_with_timeout(Duration::from_secs_f64(secs)))
|
|
1296
|
+
.map_err(error::to_ruby)
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
/// Force kill (SIGKILL) and wait.
|
|
1300
|
+
fn kill(&self) -> Result<(), Error> {
|
|
1301
|
+
block_on(self.inner.kill()).map_err(error::to_ruby)
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
/// Force kill, waiting up to `secs` for the process to disappear.
|
|
1305
|
+
fn kill_with_timeout(&self, secs: f64) -> Result<(), Error> {
|
|
1306
|
+
block_on(self.inner.kill_with_timeout(Duration::from_secs_f64(secs)))
|
|
1307
|
+
.map_err(error::to_ruby)
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
/// Send the graceful-shutdown request and return without waiting.
|
|
1311
|
+
fn request_stop(&self) -> Result<(), Error> {
|
|
1312
|
+
block_on(self.inner.request_stop()).map_err(error::to_ruby)
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
/// Send the force-kill request and return without waiting.
|
|
1316
|
+
fn request_kill(&self) -> Result<(), Error> {
|
|
1317
|
+
block_on(self.inner.request_kill()).map_err(error::to_ruby)
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
/// Send the drain request (SIGUSR1) and return without waiting.
|
|
1321
|
+
fn request_drain(&self) -> Result<(), Error> {
|
|
1322
|
+
block_on(self.inner.request_drain()).map_err(error::to_ruby)
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
/// Block until the sandbox reaches a terminal state; returns a stop-result
|
|
1326
|
+
/// Hash (name, status, exit_code, signal, observed_at_ms, source).
|
|
1327
|
+
fn wait_until_stopped(&self) -> Result<RHash, Error> {
|
|
1328
|
+
let result = block_on(self.inner.wait_until_stopped()).map_err(error::to_ruby)?;
|
|
1329
|
+
Ok(stop_result_to_hash(&result))
|
|
1330
|
+
}
|
|
1231
1331
|
}
|
|
1232
1332
|
|
|
1233
1333
|
//--------------------------------------------------------------------------------------------------
|
|
@@ -1328,15 +1428,12 @@ pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
|
1328
1428
|
class.define_method("shell", method!(Sandbox::shell, 2))?;
|
|
1329
1429
|
class.define_method("exec_stream", method!(Sandbox::exec_stream, 3))?;
|
|
1330
1430
|
class.define_method("shell_stream", method!(Sandbox::shell_stream, 2))?;
|
|
1331
|
-
class.define_method("stop", method!(Sandbox::stop,
|
|
1332
|
-
class.define_method("
|
|
1333
|
-
class.define_method("
|
|
1334
|
-
class.define_method("
|
|
1335
|
-
class.define_method("
|
|
1336
|
-
class.define_method(
|
|
1337
|
-
"wait_until_stopped",
|
|
1338
|
-
method!(Sandbox::wait_until_stopped, 0),
|
|
1339
|
-
)?;
|
|
1431
|
+
class.define_method("stop", method!(Sandbox::stop, 0))?;
|
|
1432
|
+
class.define_method("stop_and_wait", method!(Sandbox::stop_and_wait, 0))?;
|
|
1433
|
+
class.define_method("kill", method!(Sandbox::kill, 0))?;
|
|
1434
|
+
class.define_method("drain", method!(Sandbox::drain, 0))?;
|
|
1435
|
+
class.define_method("wait", method!(Sandbox::wait, 0))?;
|
|
1436
|
+
class.define_method("status", method!(Sandbox::status, 0))?;
|
|
1340
1437
|
class.define_method("owns_lifecycle", method!(Sandbox::owns_lifecycle, 0))?;
|
|
1341
1438
|
class.define_method("detach", method!(Sandbox::detach, 0))?;
|
|
1342
1439
|
class.define_method("metrics", method!(Sandbox::metrics, 0))?;
|
|
@@ -1367,5 +1464,22 @@ pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
|
1367
1464
|
class.define_method("attach", method!(Sandbox::attach, 3))?;
|
|
1368
1465
|
class.define_method("attach_shell", method!(Sandbox::attach_shell, 0))?;
|
|
1369
1466
|
|
|
1467
|
+
let handle = native.define_class("SandboxHandle", ruby.class_object())?;
|
|
1468
|
+
handle.define_method("name", method!(SbHandle::name, 0))?;
|
|
1469
|
+
handle.define_method("status", method!(SbHandle::status, 0))?;
|
|
1470
|
+
handle.define_method("created_at_ms", method!(SbHandle::created_at_ms, 0))?;
|
|
1471
|
+
handle.define_method("updated_at_ms", method!(SbHandle::updated_at_ms, 0))?;
|
|
1472
|
+
handle.define_method("stop", method!(SbHandle::stop, 0))?;
|
|
1473
|
+
handle.define_method("stop_with_timeout", method!(SbHandle::stop_with_timeout, 1))?;
|
|
1474
|
+
handle.define_method("kill", method!(SbHandle::kill, 0))?;
|
|
1475
|
+
handle.define_method("kill_with_timeout", method!(SbHandle::kill_with_timeout, 1))?;
|
|
1476
|
+
handle.define_method("request_stop", method!(SbHandle::request_stop, 0))?;
|
|
1477
|
+
handle.define_method("request_kill", method!(SbHandle::request_kill, 0))?;
|
|
1478
|
+
handle.define_method("request_drain", method!(SbHandle::request_drain, 0))?;
|
|
1479
|
+
handle.define_method(
|
|
1480
|
+
"wait_until_stopped",
|
|
1481
|
+
method!(SbHandle::wait_until_stopped, 0),
|
|
1482
|
+
)?;
|
|
1483
|
+
|
|
1370
1484
|
Ok(())
|
|
1371
1485
|
}
|
|
@@ -65,7 +65,12 @@ fn create(name: String, opts: RHash) -> Result<RHash, Error> {
|
|
|
65
65
|
let vol = block_on(builder.create()).map_err(error::to_ruby)?;
|
|
66
66
|
let hash = ruby().hash_new();
|
|
67
67
|
hash.aset("name", vol.name().to_string())?;
|
|
68
|
-
|
|
68
|
+
// `path()` is fallible as of v0.5.8 (returns `Unsupported` for cloud
|
|
69
|
+
// volumes); the volume just created here is local, so this resolves.
|
|
70
|
+
hash.aset(
|
|
71
|
+
"path",
|
|
72
|
+
vol.path().map_err(error::to_ruby)?.display().to_string(),
|
|
73
|
+
)?;
|
|
69
74
|
Ok(hash)
|
|
70
75
|
}
|
|
71
76
|
|
data/lib/microsandbox/errors.rb
CHANGED
|
@@ -65,4 +65,10 @@ module Microsandbox
|
|
|
65
65
|
|
|
66
66
|
# Runtime compatibility ---------------------------------------------------
|
|
67
67
|
define_error(:UnsupportedOperationError, "unsupported-operation")
|
|
68
|
+
|
|
69
|
+
# Cloud / backend routing errors (v0.5.8) ---------------------------------
|
|
70
|
+
# `UnsupportedError` (a backend feature gap, e.g. an op not yet wired on the
|
|
71
|
+
# cloud backend) is distinct from `UnsupportedOperationError` above.
|
|
72
|
+
define_error(:CloudHttpError, "cloud-http")
|
|
73
|
+
define_error(:UnsupportedError, "unsupported")
|
|
68
74
|
end
|