microsandbox-rb 0.5.7

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.
@@ -0,0 +1,158 @@
1
+ //! Streaming command execution: `Microsandbox::Native::ExecHandle` and
2
+ //! `Microsandbox::Native::ExecSink`.
3
+ //!
4
+ //! Mirrors `sdk/python/src/exec.rs`. The core `ExecHandle` is `&mut`-driven and
5
+ //! lives behind an `Arc<tokio::Mutex<…>>`; each call locks it inside `block_on`.
6
+ //! Signal/kill go through a cloned `ExecControl` so they never contend with an
7
+ //! in-flight `recv`. Stdin (when piped) is exposed as a separate `ExecSink`.
8
+
9
+ use std::sync::Arc;
10
+
11
+ use magnus::{method, prelude::*, Error, RHash, RModule, RString, Ruby};
12
+ use tokio::sync::Mutex;
13
+
14
+ use crate::error;
15
+ use crate::runtime::{block_on, ruby};
16
+
17
+ #[magnus::wrap(class = "Microsandbox::Native::ExecHandle", free_immediately, size)]
18
+ pub struct ExecHandle {
19
+ inner: Arc<Mutex<microsandbox::ExecHandle>>,
20
+ control: microsandbox::ExecControl,
21
+ stdin: std::sync::Mutex<Option<Arc<microsandbox::sandbox::exec::ExecSink>>>,
22
+ id: String,
23
+ }
24
+
25
+ impl ExecHandle {
26
+ /// Wrap a freshly-opened core handle, lifting out its id and stdin sink.
27
+ pub fn from_core(mut handle: microsandbox::ExecHandle) -> Self {
28
+ let id = handle.id();
29
+ let control = handle.control();
30
+ let stdin = handle.take_stdin().map(Arc::new);
31
+ Self {
32
+ inner: Arc::new(Mutex::new(handle)),
33
+ control,
34
+ stdin: std::sync::Mutex::new(stdin),
35
+ id,
36
+ }
37
+ }
38
+
39
+ fn id(&self) -> String {
40
+ self.id.clone()
41
+ }
42
+
43
+ /// Next event as a Hash, or nil when the stream ends.
44
+ fn recv(&self) -> Result<Option<RHash>, Error> {
45
+ let inner = Arc::clone(&self.inner);
46
+ let event = block_on(async move { inner.lock().await.recv().await });
47
+ Ok(event.map(exec_event_to_hash))
48
+ }
49
+
50
+ /// Block until exit; returns {exit_code, success}.
51
+ fn wait(&self) -> Result<RHash, Error> {
52
+ let inner = Arc::clone(&self.inner);
53
+ let status =
54
+ block_on(async move { inner.lock().await.wait().await }).map_err(error::to_ruby)?;
55
+ let hash = ruby().hash_new();
56
+ hash.aset("exit_code", status.code)?;
57
+ hash.aset("success", status.success)?;
58
+ Ok(hash)
59
+ }
60
+
61
+ /// Drain the stream and return a collected exec-output Hash.
62
+ fn collect(&self) -> Result<RHash, Error> {
63
+ let inner = Arc::clone(&self.inner);
64
+ let output =
65
+ block_on(async move { inner.lock().await.collect().await }).map_err(error::to_ruby)?;
66
+ crate::sandbox::exec_output_to_hash(output)
67
+ }
68
+
69
+ fn signal(&self, sig: i32) -> Result<(), Error> {
70
+ block_on(self.control.signal(sig)).map_err(error::to_ruby)
71
+ }
72
+
73
+ fn kill(&self) -> Result<(), Error> {
74
+ block_on(self.control.kill()).map_err(error::to_ruby)
75
+ }
76
+
77
+ fn resize(&self, rows: u16, cols: u16) -> Result<(), Error> {
78
+ block_on(self.control.resize(rows, cols)).map_err(error::to_ruby)
79
+ }
80
+
81
+ /// The stdin sink (only the first call returns it; nil afterwards or if
82
+ /// stdin was not piped).
83
+ fn take_stdin(&self) -> Option<ExecSink> {
84
+ let mut guard = self.stdin.lock().expect("exec stdin mutex poisoned");
85
+ guard.take().map(|sink| ExecSink { inner: sink })
86
+ }
87
+ }
88
+
89
+ #[magnus::wrap(class = "Microsandbox::Native::ExecSink", free_immediately, size)]
90
+ pub struct ExecSink {
91
+ inner: Arc<microsandbox::sandbox::exec::ExecSink>,
92
+ }
93
+
94
+ impl ExecSink {
95
+ fn write(&self, data: RString) -> Result<(), Error> {
96
+ // Copy out while holding the GVL (see sandbox::fs_write for the rationale).
97
+ let bytes = unsafe { data.as_slice() }.to_vec();
98
+ block_on(self.inner.write(&bytes)).map_err(error::to_ruby)
99
+ }
100
+
101
+ fn close(&self) -> Result<(), Error> {
102
+ block_on(self.inner.close()).map_err(error::to_ruby)
103
+ }
104
+ }
105
+
106
+ /// Convert a core `ExecEvent` into a Ruby Hash. `data` is binary (ASCII-8BIT).
107
+ fn exec_event_to_hash(event: microsandbox::ExecEvent) -> RHash {
108
+ use microsandbox::ExecEvent::*;
109
+ let r = ruby();
110
+ let hash = r.hash_new();
111
+ match event {
112
+ Started { pid } => {
113
+ let _ = hash.aset("type", "started");
114
+ let _ = hash.aset("pid", pid);
115
+ }
116
+ Stdout(data) => {
117
+ let _ = hash.aset("type", "stdout");
118
+ let _ = hash.aset("data", r.str_from_slice(data.as_ref()));
119
+ }
120
+ Stderr(data) => {
121
+ let _ = hash.aset("type", "stderr");
122
+ let _ = hash.aset("data", r.str_from_slice(data.as_ref()));
123
+ }
124
+ Exited { code } => {
125
+ let _ = hash.aset("type", "exited");
126
+ let _ = hash.aset("code", code);
127
+ }
128
+ Failed(payload) => {
129
+ let _ = hash.aset("type", "failed");
130
+ let _ = hash.aset("data", r.str_from_slice(payload.message.as_bytes()));
131
+ let _ = hash.aset("code", payload.errno);
132
+ }
133
+ StdinError(payload) => {
134
+ let _ = hash.aset("type", "stdin_error");
135
+ let _ = hash.aset("data", r.str_from_slice(payload.message.as_bytes()));
136
+ let _ = hash.aset("code", payload.errno);
137
+ }
138
+ }
139
+ hash
140
+ }
141
+
142
+ pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
143
+ let handle = native.define_class("ExecHandle", ruby.class_object())?;
144
+ handle.define_method("id", method!(ExecHandle::id, 0))?;
145
+ handle.define_method("recv", method!(ExecHandle::recv, 0))?;
146
+ handle.define_method("wait", method!(ExecHandle::wait, 0))?;
147
+ handle.define_method("collect", method!(ExecHandle::collect, 0))?;
148
+ handle.define_method("signal", method!(ExecHandle::signal, 1))?;
149
+ handle.define_method("kill", method!(ExecHandle::kill, 0))?;
150
+ handle.define_method("resize", method!(ExecHandle::resize, 2))?;
151
+ handle.define_method("take_stdin", method!(ExecHandle::take_stdin, 0))?;
152
+
153
+ let sink = native.define_class("ExecSink", ruby.class_object())?;
154
+ sink.define_method("write", method!(ExecSink::write, 1))?;
155
+ sink.define_method("close", method!(ExecSink::close, 0))?;
156
+
157
+ Ok(())
158
+ }
@@ -0,0 +1,114 @@
1
+ //! OCI image-cache management: `Microsandbox::Native::Image`.
2
+ //!
3
+ //! Mirrors `sdk/python/src/image.rs`. Static async operations over the cached
4
+ //! image store; results are returned as plain Ruby Hashes/Arrays. Note the core
5
+ //! `ImageHandle` exposes accessor *methods* while `ImageDetail`/`ImageConfigDetail`/
6
+ //! `ImageLayerDetail`/`ImagePruneReport` expose public *fields*.
7
+
8
+ use magnus::{function, prelude::*, Error, RArray, RHash, RModule, Ruby};
9
+ use microsandbox::image::{Image, ImageDetail, ImageHandle, ImagePruneReport};
10
+
11
+ use crate::error;
12
+ use crate::runtime::{block_on, ruby};
13
+
14
+ fn handle_to_hash(h: &ImageHandle) -> RHash {
15
+ let hash = ruby().hash_new();
16
+ let _ = hash.aset("reference", h.reference().to_string());
17
+ let _ = hash.aset("size_bytes", h.size_bytes());
18
+ let _ = hash.aset("manifest_digest", h.manifest_digest().map(str::to_string));
19
+ let _ = hash.aset("architecture", h.architecture().map(str::to_string));
20
+ let _ = hash.aset("os", h.os().map(str::to_string));
21
+ let _ = hash.aset("layer_count", h.layer_count());
22
+ let _ = hash.aset(
23
+ "created_at_ms",
24
+ h.created_at().map(|dt| dt.timestamp_millis()),
25
+ );
26
+ let _ = hash.aset(
27
+ "last_used_at_ms",
28
+ h.last_used_at().map(|dt| dt.timestamp_millis()),
29
+ );
30
+ hash
31
+ }
32
+
33
+ fn detail_to_hash(detail: ImageDetail) -> RHash {
34
+ let r = ruby();
35
+ let hash = r.hash_new();
36
+ let _ = hash.aset("handle", handle_to_hash(&detail.handle));
37
+
38
+ if let Some(config) = detail.config {
39
+ let c = r.hash_new();
40
+ let _ = c.aset("digest", config.digest);
41
+ let _ = c.aset("env", config.env);
42
+ let _ = c.aset("cmd", config.cmd);
43
+ let _ = c.aset("entrypoint", config.entrypoint);
44
+ let _ = c.aset("working_dir", config.working_dir);
45
+ let _ = c.aset("user", config.user);
46
+ let _ = c.aset("stop_signal", config.stop_signal);
47
+ let _ = hash.aset("config", c);
48
+ } else {
49
+ let _ = hash.aset("config", r.qnil());
50
+ }
51
+
52
+ let layers = r.ary_new();
53
+ for layer in detail.layers {
54
+ let l = r.hash_new();
55
+ let _ = l.aset("diff_id", layer.diff_id);
56
+ let _ = l.aset("blob_digest", layer.blob_digest);
57
+ let _ = l.aset("media_type", layer.media_type);
58
+ let _ = l.aset("compressed_size_bytes", layer.compressed_size_bytes);
59
+ let _ = l.aset("erofs_size_bytes", layer.erofs_size_bytes);
60
+ let _ = l.aset("position", layer.position);
61
+ let _ = layers.push(l);
62
+ }
63
+ let _ = hash.aset("layers", layers);
64
+ hash
65
+ }
66
+
67
+ fn report_to_hash(report: ImagePruneReport) -> RHash {
68
+ let hash = ruby().hash_new();
69
+ let _ = hash.aset("image_refs_removed", report.image_refs_removed);
70
+ let _ = hash.aset("manifests_removed", report.manifests_removed);
71
+ let _ = hash.aset("layers_removed", report.layers_removed);
72
+ let _ = hash.aset("fsmeta_removed", report.fsmeta_removed);
73
+ let _ = hash.aset("vmdk_removed", report.vmdk_removed);
74
+ let _ = hash.aset("bytes_reclaimed", report.bytes_reclaimed);
75
+ hash
76
+ }
77
+
78
+ fn get(reference: String) -> Result<RHash, Error> {
79
+ let handle = block_on(Image::get(&reference)).map_err(error::to_ruby)?;
80
+ Ok(handle_to_hash(&handle))
81
+ }
82
+
83
+ fn list() -> Result<RArray, Error> {
84
+ let handles = block_on(Image::list()).map_err(error::to_ruby)?;
85
+ let arr = ruby().ary_new();
86
+ for h in handles.iter() {
87
+ arr.push(handle_to_hash(h))?;
88
+ }
89
+ Ok(arr)
90
+ }
91
+
92
+ fn inspect(reference: String) -> Result<RHash, Error> {
93
+ let detail = block_on(Image::inspect(&reference)).map_err(error::to_ruby)?;
94
+ Ok(detail_to_hash(detail))
95
+ }
96
+
97
+ fn remove(reference: String, force: bool) -> Result<(), Error> {
98
+ block_on(Image::remove(&reference, force)).map_err(error::to_ruby)
99
+ }
100
+
101
+ fn prune() -> Result<RHash, Error> {
102
+ let report = block_on(Image::prune()).map_err(error::to_ruby)?;
103
+ Ok(report_to_hash(report))
104
+ }
105
+
106
+ pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
107
+ let class = native.define_class("Image", ruby.class_object())?;
108
+ class.define_singleton_method("get", function!(get, 1))?;
109
+ class.define_singleton_method("list", function!(list, 0))?;
110
+ class.define_singleton_method("inspect", function!(inspect, 1))?;
111
+ class.define_singleton_method("remove", function!(remove, 2))?;
112
+ class.define_singleton_method("prune", function!(prune, 0))?;
113
+ Ok(())
114
+ }
@@ -0,0 +1,84 @@
1
+ //! Ruby SDK native extension for microsandbox.
2
+ //!
3
+ //! The Ruby analogue of the official Python (pyo3) and Node (napi) bindings: it
4
+ //! exposes the embedded `microsandbox` runtime to Ruby via magnus. The core is
5
+ //! async (tokio); the Ruby API is synchronous, so every binding blocks on a
6
+ //! shared multi-threaded tokio runtime with the GVL released (see `runtime`).
7
+ //!
8
+ //! Everything here lives under `Microsandbox::Native`; the ergonomic, idiomatic
9
+ //! surface is the pure-Ruby layer in `lib/microsandbox/`.
10
+
11
+ mod conv;
12
+ mod error;
13
+ mod exec;
14
+ mod image;
15
+ mod runtime;
16
+ mod sandbox;
17
+ mod snapshot;
18
+ mod stream;
19
+ mod volume;
20
+
21
+ use magnus::{function, prelude::*, Error, RHash, Ruby};
22
+
23
+ /// Gem/runtime version string.
24
+ fn version() -> String {
25
+ env!("CARGO_PKG_VERSION").to_string()
26
+ }
27
+
28
+ /// Latest metrics for every running sandbox, as a `{ name => metrics_hash }`
29
+ /// Ruby Hash. Mirrors the official `all_sandbox_metrics` / `allSandboxMetrics`
30
+ /// helpers (Python/Node/Go).
31
+ fn all_sandbox_metrics() -> Result<RHash, Error> {
32
+ let map =
33
+ runtime::block_on(microsandbox::sandbox::all_sandbox_metrics()).map_err(error::to_ruby)?;
34
+ let hash = runtime::ruby().hash_new();
35
+ for (name, metrics) in &map {
36
+ hash.aset(name.as_str(), sandbox::metrics_to_hash(metrics))?;
37
+ }
38
+ Ok(hash)
39
+ }
40
+
41
+ /// Download and install the `msb` runtime + `libkrunfw` into `~/.microsandbox`.
42
+ fn install() -> Result<(), Error> {
43
+ runtime::block_on(microsandbox::setup::install()).map_err(error::to_ruby)
44
+ }
45
+
46
+ /// Whether the `msb` runtime + `libkrunfw` are installed and resolvable.
47
+ fn is_installed() -> bool {
48
+ microsandbox::setup::is_installed()
49
+ }
50
+
51
+ /// Override the resolved `msb` runtime path (SDK tier of the resolver).
52
+ fn set_runtime_msb_path(path: String) {
53
+ microsandbox::config::set_sdk_msb_path(path);
54
+ }
55
+
56
+ /// The currently-resolved `msb` runtime path.
57
+ fn resolved_msb_path() -> Result<String, Error> {
58
+ let path = microsandbox::config::resolve_msb_path().map_err(error::to_ruby)?;
59
+ Ok(path.to_string_lossy().into_owned())
60
+ }
61
+
62
+ /// magnus entry point. RubyGems loads this via `require "microsandbox/microsandbox_rb"`,
63
+ /// which calls `Init_microsandbox_rb` (matching the cdylib `[lib] name`).
64
+ #[magnus::init]
65
+ fn init(ruby: &Ruby) -> Result<(), Error> {
66
+ let module = ruby.define_module("Microsandbox")?;
67
+ let native = module.define_module("Native")?;
68
+
69
+ native.define_singleton_method("version", function!(version, 0))?;
70
+ native.define_singleton_method("install", function!(install, 0))?;
71
+ native.define_singleton_method("installed?", function!(is_installed, 0))?;
72
+ native.define_singleton_method("set_runtime_msb_path", function!(set_runtime_msb_path, 1))?;
73
+ native.define_singleton_method("resolved_msb_path", function!(resolved_msb_path, 0))?;
74
+ native.define_singleton_method("all_sandbox_metrics", function!(all_sandbox_metrics, 0))?;
75
+
76
+ sandbox::define(ruby, &native)?;
77
+ exec::define(ruby, &native)?;
78
+ stream::define(ruby, &native)?;
79
+ snapshot::define(ruby, &native)?;
80
+ image::define(ruby, &native)?;
81
+ volume::define(ruby, &native)?;
82
+
83
+ Ok(())
84
+ }
@@ -0,0 +1,92 @@
1
+ //! Shared tokio runtime and the GVL-release bridge.
2
+ //!
3
+ //! The microsandbox core is async; the Ruby API is synchronous. Every native
4
+ //! method runs its future to completion on a single, process-wide multi-threaded
5
+ //! tokio runtime. Crucially, the blocking call happens inside [`nogvl`], which
6
+ //! releases Ruby's Global VM Lock for the duration so other Ruby threads keep
7
+ //! running while a (potentially long) sandbox operation is in flight.
8
+
9
+ use std::ffi::c_void;
10
+ use std::future::Future;
11
+ use std::panic::{catch_unwind, AssertUnwindSafe};
12
+ use std::sync::OnceLock;
13
+
14
+ use magnus::Ruby;
15
+ use tokio::runtime::Runtime;
16
+
17
+ /// The current Ruby handle. Safe to call from any bound method or value
18
+ /// conversion: we always hold the GVL there (conversions run after `block_on`
19
+ /// returns and re-acquires it).
20
+ pub fn ruby() -> Ruby {
21
+ Ruby::get().expect("microsandbox: not on a Ruby thread")
22
+ }
23
+
24
+ static RUNTIME: OnceLock<Runtime> = OnceLock::new();
25
+
26
+ /// The process-wide multi-threaded tokio runtime, built on first use.
27
+ pub fn runtime() -> &'static Runtime {
28
+ RUNTIME.get_or_init(|| {
29
+ tokio::runtime::Builder::new_multi_thread()
30
+ .enable_all()
31
+ .thread_name("microsandbox-rb")
32
+ .build()
33
+ .expect("microsandbox: failed to build tokio runtime")
34
+ })
35
+ }
36
+
37
+ /// Run `f` with the Ruby GVL released.
38
+ ///
39
+ /// `f` runs on the *same* OS thread (the C call blocks until it returns), so no
40
+ /// `Send`/`'static` bound is required. `f` MUST NOT touch the Ruby C API while
41
+ /// the GVL is released. Panics are caught and re-raised after the GVL is
42
+ /// re-acquired, because unwinding across the C frame would be undefined
43
+ /// behaviour.
44
+ pub fn nogvl<F, R>(f: F) -> R
45
+ where
46
+ F: FnOnce() -> R,
47
+ {
48
+ struct State<F, R> {
49
+ f: Option<F>,
50
+ out: Option<std::thread::Result<R>>,
51
+ }
52
+
53
+ unsafe extern "C" fn call<F, R>(arg: *mut c_void) -> *mut c_void
54
+ where
55
+ F: FnOnce() -> R,
56
+ {
57
+ // SAFETY: `arg` points to the `State` on the caller's stack, which
58
+ // outlives this call (rb_thread_call_without_gvl blocks until return).
59
+ let state = unsafe { &mut *(arg as *mut State<F, R>) };
60
+ let f = state.f.take().expect("nogvl callback invoked twice");
61
+ state.out = Some(catch_unwind(AssertUnwindSafe(f)));
62
+ std::ptr::null_mut()
63
+ }
64
+
65
+ let mut state: State<F, R> = State {
66
+ f: Some(f),
67
+ out: None,
68
+ };
69
+
70
+ unsafe {
71
+ rb_sys::rb_thread_call_without_gvl(
72
+ Some(call::<F, R>),
73
+ &mut state as *mut _ as *mut c_void,
74
+ None,
75
+ std::ptr::null_mut(),
76
+ );
77
+ }
78
+
79
+ match state.out.take() {
80
+ Some(Ok(value)) => value,
81
+ Some(Err(panic)) => std::panic::resume_unwind(panic),
82
+ None => unreachable!("nogvl callback did not run"),
83
+ }
84
+ }
85
+
86
+ /// Drive a future to completion on the shared runtime with the GVL released.
87
+ pub fn block_on<F>(fut: F) -> F::Output
88
+ where
89
+ F: Future,
90
+ {
91
+ nogvl(|| runtime().block_on(fut))
92
+ }