microsandbox-rb 0.5.9 → 0.5.11
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 +91 -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 +208 -61
- 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 +230 -84
- 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)?;
|