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.
@@ -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 StandardError
16
+ rescue
17
17
  nil
18
18
  end
19
19
 
20
20
  if rustc_version && rustc_version < MSRV
21
- which_rustc = (`which rustc 2>/dev/null`.strip rescue "")
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(&microsandbox::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::error;
12
- use crate::runtime::{block_on, ruby};
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 = block_on(Image::get(&reference)).map_err(error::to_ruby)?;
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 = block_on(Image::list()).map_err(error::to_ruby)?;
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 = block_on(Image::inspect(&reference)).map_err(error::to_ruby)?;
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
- block_on(Image::remove(&reference, force)).map_err(error::to_ruby)
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 = block_on(Image::prune()).map_err(error::to_ruby)?;
102
+ let report = with_local_backend(async |local| Image::prune(local).await)?;
103
103
  Ok(report_to_hash(report))
104
104
  }
105
105
 
@@ -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
- runtime::block_on(microsandbox::sandbox::all_sandbox_metrics()).map_err(error::to_ruby)?;
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
- /// The currently-resolved `msb` runtime path.
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 path = microsandbox::config::resolve_msb_path().map_err(error::to_ruby)?;
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)?;