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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +94 -0
- data/Cargo.lock +7455 -0
- data/Cargo.toml +16 -0
- data/DESIGN.md +159 -0
- data/LICENSE +201 -0
- data/README.md +328 -0
- data/ext/microsandbox/Cargo.toml +45 -0
- data/ext/microsandbox/extconf.rb +14 -0
- data/ext/microsandbox/src/conv.rs +74 -0
- data/ext/microsandbox/src/error.rs +72 -0
- data/ext/microsandbox/src/exec.rs +158 -0
- data/ext/microsandbox/src/image.rs +114 -0
- data/ext/microsandbox/src/lib.rs +84 -0
- data/ext/microsandbox/src/runtime.rs +92 -0
- data/ext/microsandbox/src/sandbox.rs +812 -0
- data/ext/microsandbox/src/snapshot.rs +158 -0
- data/ext/microsandbox/src/stream.rs +86 -0
- data/ext/microsandbox/src/volume.rs +97 -0
- data/lib/microsandbox/errors.rb +68 -0
- data/lib/microsandbox/exec_handle.rb +154 -0
- data/lib/microsandbox/exec_output.rb +55 -0
- data/lib/microsandbox/fs.rb +172 -0
- data/lib/microsandbox/image.rb +111 -0
- data/lib/microsandbox/log_entry.rb +38 -0
- data/lib/microsandbox/metrics.rb +55 -0
- data/lib/microsandbox/sandbox.rb +461 -0
- data/lib/microsandbox/snapshot.rb +155 -0
- data/lib/microsandbox/streams.rb +54 -0
- data/lib/microsandbox/version.rb +7 -0
- data/lib/microsandbox/volume.rb +79 -0
- data/lib/microsandbox.rb +78 -0
- data/rust-toolchain.toml +5 -0
- data/sig/microsandbox.rbs +321 -0
- metadata +101 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
//! Snapshot management: `Microsandbox::Native::Snapshot`.
|
|
2
|
+
//!
|
|
3
|
+
//! Snapshots capture a stopped sandbox's upper layer into a portable artifact
|
|
4
|
+
//! that a later `Sandbox.create(from_snapshot:)` can boot from. Exposed as
|
|
5
|
+
//! singleton functions returning plain Hashes/Arrays (shaped into value objects
|
|
6
|
+
//! by the Ruby layer) — there is no long-lived handle to own.
|
|
7
|
+
|
|
8
|
+
use std::path::PathBuf;
|
|
9
|
+
|
|
10
|
+
use magnus::{function, prelude::*, Error, RArray, RHash, RModule, Ruby};
|
|
11
|
+
use microsandbox::snapshot::{
|
|
12
|
+
ExportOpts, Snapshot, SnapshotDestination, SnapshotFormat, SnapshotHandle,
|
|
13
|
+
SnapshotVerifyReport, UpperVerifyStatus,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
use crate::conv;
|
|
17
|
+
use crate::error;
|
|
18
|
+
use crate::runtime::{block_on, ruby};
|
|
19
|
+
|
|
20
|
+
fn format_str(format: SnapshotFormat) -> &'static str {
|
|
21
|
+
match format {
|
|
22
|
+
SnapshotFormat::Raw => "raw",
|
|
23
|
+
SnapshotFormat::Qcow2 => "qcow2",
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// Create a snapshot of a stopped sandbox. `opts`: name | path (destination),
|
|
28
|
+
/// labels, force, record_integrity. Returns {digest, path, size_bytes}.
|
|
29
|
+
fn create(source_sandbox: String, opts: RHash) -> Result<RHash, Error> {
|
|
30
|
+
let mut b = Snapshot::builder(source_sandbox);
|
|
31
|
+
if let Some(name) = conv::opt_string(opts, "name")? {
|
|
32
|
+
b = b.destination(SnapshotDestination::Name(name));
|
|
33
|
+
} else if let Some(path) = conv::opt_string(opts, "path")? {
|
|
34
|
+
b = b.destination(SnapshotDestination::Path(PathBuf::from(path)));
|
|
35
|
+
} else {
|
|
36
|
+
return Err(error::base_error(
|
|
37
|
+
"snapshot create needs a destination: pass name: or path:",
|
|
38
|
+
));
|
|
39
|
+
}
|
|
40
|
+
for (k, v) in conv::opt_string_map(opts, "labels")? {
|
|
41
|
+
b = b.label(k, v);
|
|
42
|
+
}
|
|
43
|
+
if conv::opt_bool(opts, "force")? {
|
|
44
|
+
b = b.force();
|
|
45
|
+
}
|
|
46
|
+
if conv::opt_bool(opts, "record_integrity")? {
|
|
47
|
+
b = b.record_integrity();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let snap = block_on(b.create()).map_err(error::to_ruby)?;
|
|
51
|
+
let hash = ruby().hash_new();
|
|
52
|
+
hash.aset("digest", snap.digest().to_string())?;
|
|
53
|
+
hash.aset("path", snap.path().to_string_lossy().into_owned())?;
|
|
54
|
+
hash.aset("size_bytes", snap.size_bytes())?;
|
|
55
|
+
Ok(hash)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// Metadata for one snapshot by name or digest.
|
|
59
|
+
fn get(name_or_digest: String) -> Result<RHash, Error> {
|
|
60
|
+
let handle = block_on(Snapshot::get(&name_or_digest)).map_err(error::to_ruby)?;
|
|
61
|
+
Ok(handle_to_hash(&handle))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// All snapshots as metadata hashes.
|
|
65
|
+
fn list() -> Result<RArray, Error> {
|
|
66
|
+
let handles = block_on(Snapshot::list()).map_err(error::to_ruby)?;
|
|
67
|
+
let arr = ruby().ary_new();
|
|
68
|
+
for h in &handles {
|
|
69
|
+
arr.push(handle_to_hash(h))?;
|
|
70
|
+
}
|
|
71
|
+
Ok(arr)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// Remove a snapshot artifact by name or path.
|
|
75
|
+
fn remove(name_or_path: String, force: bool) -> Result<(), Error> {
|
|
76
|
+
block_on(Snapshot::remove(&name_or_path, force)).map_err(error::to_ruby)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// Verify a snapshot's recorded upper-layer integrity. Returns
|
|
80
|
+
/// {digest, path, upper_status, upper_algorithm?, upper_digest?}.
|
|
81
|
+
fn verify(name_or_path: String) -> Result<RHash, Error> {
|
|
82
|
+
let snap = block_on(Snapshot::open(&name_or_path)).map_err(error::to_ruby)?;
|
|
83
|
+
let report = block_on(snap.verify()).map_err(error::to_ruby)?;
|
|
84
|
+
Ok(verify_report_to_hash(&report))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/// Bundle a snapshot into a `.tar.zst` (or plain `.tar`) archive. `opts`:
|
|
88
|
+
/// with_parents, with_image, plain_tar.
|
|
89
|
+
fn export(name_or_path: String, out_path: String, opts: RHash) -> Result<(), Error> {
|
|
90
|
+
let export_opts = ExportOpts {
|
|
91
|
+
with_parents: conv::opt_bool(opts, "with_parents")?,
|
|
92
|
+
with_image: conv::opt_bool(opts, "with_image")?,
|
|
93
|
+
plain_tar: conv::opt_bool(opts, "plain_tar")?,
|
|
94
|
+
};
|
|
95
|
+
block_on(Snapshot::export(
|
|
96
|
+
&name_or_path,
|
|
97
|
+
std::path::Path::new(&out_path),
|
|
98
|
+
export_opts,
|
|
99
|
+
))
|
|
100
|
+
.map_err(error::to_ruby)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// Unpack a snapshot archive into the snapshots dir. Returns the imported
|
|
104
|
+
/// snapshot's metadata hash. `dest` is an optional explicit directory.
|
|
105
|
+
fn import(archive_path: String, dest: Option<String>) -> Result<RHash, Error> {
|
|
106
|
+
let dest_path = dest.map(PathBuf::from);
|
|
107
|
+
let handle = block_on(Snapshot::import(
|
|
108
|
+
std::path::Path::new(&archive_path),
|
|
109
|
+
dest_path.as_deref(),
|
|
110
|
+
))
|
|
111
|
+
.map_err(error::to_ruby)?;
|
|
112
|
+
Ok(handle_to_hash(&handle))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
fn handle_to_hash(handle: &SnapshotHandle) -> RHash {
|
|
116
|
+
let hash = ruby().hash_new();
|
|
117
|
+
let _ = hash.aset("digest", handle.digest().to_string());
|
|
118
|
+
let _ = hash.aset("name", handle.name().map(str::to_string));
|
|
119
|
+
let _ = hash.aset("parent_digest", handle.parent_digest().map(str::to_string));
|
|
120
|
+
let _ = hash.aset("image_ref", handle.image_ref().to_string());
|
|
121
|
+
let _ = hash.aset("format", format_str(handle.format()));
|
|
122
|
+
let _ = hash.aset("size_bytes", handle.size_bytes());
|
|
123
|
+
let _ = hash.aset("path", handle.path().to_string_lossy().into_owned());
|
|
124
|
+
let _ = hash.aset(
|
|
125
|
+
"created_at_ms",
|
|
126
|
+
handle.created_at().and_utc().timestamp_millis(),
|
|
127
|
+
);
|
|
128
|
+
hash
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
fn verify_report_to_hash(report: &SnapshotVerifyReport) -> RHash {
|
|
132
|
+
let hash = ruby().hash_new();
|
|
133
|
+
let _ = hash.aset("digest", report.digest.clone());
|
|
134
|
+
let _ = hash.aset("path", report.path.to_string_lossy().into_owned());
|
|
135
|
+
match &report.upper {
|
|
136
|
+
UpperVerifyStatus::NotRecorded => {
|
|
137
|
+
let _ = hash.aset("upper_status", "not_recorded");
|
|
138
|
+
}
|
|
139
|
+
UpperVerifyStatus::Verified { algorithm, digest } => {
|
|
140
|
+
let _ = hash.aset("upper_status", "verified");
|
|
141
|
+
let _ = hash.aset("upper_algorithm", algorithm.clone());
|
|
142
|
+
let _ = hash.aset("upper_digest", digest.clone());
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
hash
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
149
|
+
let class = native.define_class("Snapshot", ruby.class_object())?;
|
|
150
|
+
class.define_singleton_method("create", function!(create, 2))?;
|
|
151
|
+
class.define_singleton_method("get", function!(get, 1))?;
|
|
152
|
+
class.define_singleton_method("list", function!(list, 0))?;
|
|
153
|
+
class.define_singleton_method("remove", function!(remove, 2))?;
|
|
154
|
+
class.define_singleton_method("verify", function!(verify, 1))?;
|
|
155
|
+
class.define_singleton_method("export", function!(export, 3))?;
|
|
156
|
+
class.define_singleton_method("import", function!(import, 2))?;
|
|
157
|
+
Ok(())
|
|
158
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
//! Streaming logs and metrics: `Microsandbox::Native::LogStream` and
|
|
2
|
+
//! `Microsandbox::Native::MetricsStream`.
|
|
3
|
+
//!
|
|
4
|
+
//! Mirrors the `ExecHandle` streaming pattern in `exec.rs`. The core
|
|
5
|
+
//! `log_stream`/`metrics_stream` return `impl Stream`; we box+pin each into an
|
|
6
|
+
//! `Arc<tokio::Mutex<…>>` and expose a synchronous `recv` that drives the next
|
|
7
|
+
//! item to completion with the GVL released, returning a Ruby Hash (or `nil` at
|
|
8
|
+
//! end of stream). The Ruby layer wraps each as an `Enumerable`.
|
|
9
|
+
|
|
10
|
+
use std::pin::Pin;
|
|
11
|
+
use std::sync::Arc;
|
|
12
|
+
|
|
13
|
+
use futures::Stream;
|
|
14
|
+
use magnus::{method, prelude::*, Error, RHash, RModule, Ruby};
|
|
15
|
+
use microsandbox::logs::LogEntry;
|
|
16
|
+
use microsandbox::sandbox::SandboxMetrics;
|
|
17
|
+
use microsandbox::MicrosandboxResult;
|
|
18
|
+
use tokio::sync::Mutex;
|
|
19
|
+
|
|
20
|
+
use crate::error;
|
|
21
|
+
use crate::runtime::block_on;
|
|
22
|
+
use crate::sandbox::{log_entry_to_hash, metrics_to_hash};
|
|
23
|
+
|
|
24
|
+
type BoxStream<T> = Pin<Box<dyn Stream<Item = MicrosandboxResult<T>> + Send>>;
|
|
25
|
+
|
|
26
|
+
#[magnus::wrap(class = "Microsandbox::Native::LogStream", free_immediately, size)]
|
|
27
|
+
pub struct LogStream {
|
|
28
|
+
inner: Arc<Mutex<BoxStream<LogEntry>>>,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
impl LogStream {
|
|
32
|
+
pub fn from_stream(
|
|
33
|
+
stream: impl Stream<Item = MicrosandboxResult<LogEntry>> + Send + 'static,
|
|
34
|
+
) -> Self {
|
|
35
|
+
Self {
|
|
36
|
+
inner: Arc::new(Mutex::new(Box::pin(stream))),
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// Next log entry as a Hash, or nil when the stream ends.
|
|
41
|
+
fn recv(&self) -> Result<Option<RHash>, Error> {
|
|
42
|
+
use futures::StreamExt;
|
|
43
|
+
let inner = Arc::clone(&self.inner);
|
|
44
|
+
match block_on(async move { inner.lock().await.next().await }) {
|
|
45
|
+
Some(Ok(entry)) => Ok(Some(log_entry_to_hash(&entry))),
|
|
46
|
+
Some(Err(e)) => Err(error::to_ruby(e)),
|
|
47
|
+
None => Ok(None),
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
#[magnus::wrap(class = "Microsandbox::Native::MetricsStream", free_immediately, size)]
|
|
53
|
+
pub struct MetricsStream {
|
|
54
|
+
inner: Arc<Mutex<BoxStream<SandboxMetrics>>>,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
impl MetricsStream {
|
|
58
|
+
pub fn from_stream(
|
|
59
|
+
stream: impl Stream<Item = MicrosandboxResult<SandboxMetrics>> + Send + 'static,
|
|
60
|
+
) -> Self {
|
|
61
|
+
Self {
|
|
62
|
+
inner: Arc::new(Mutex::new(Box::pin(stream))),
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// Next metrics snapshot as a Hash, or nil when the stream ends.
|
|
67
|
+
fn recv(&self) -> Result<Option<RHash>, Error> {
|
|
68
|
+
use futures::StreamExt;
|
|
69
|
+
let inner = Arc::clone(&self.inner);
|
|
70
|
+
match block_on(async move { inner.lock().await.next().await }) {
|
|
71
|
+
Some(Ok(metrics)) => Ok(Some(metrics_to_hash(&metrics))),
|
|
72
|
+
Some(Err(e)) => Err(error::to_ruby(e)),
|
|
73
|
+
None => Ok(None),
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
79
|
+
let logs = native.define_class("LogStream", ruby.class_object())?;
|
|
80
|
+
logs.define_method("recv", method!(LogStream::recv, 0))?;
|
|
81
|
+
|
|
82
|
+
let metrics = native.define_class("MetricsStream", ruby.class_object())?;
|
|
83
|
+
metrics.define_method("recv", method!(MetricsStream::recv, 0))?;
|
|
84
|
+
|
|
85
|
+
Ok(())
|
|
86
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
//! Named persistent volume management: `Microsandbox::Native::Volume`.
|
|
2
|
+
//!
|
|
3
|
+
//! Mirrors `sdk/python/src/volume.rs`. Static async CRUD over the volume store;
|
|
4
|
+
//! results are returned as plain Ruby Hashes/Arrays.
|
|
5
|
+
|
|
6
|
+
use magnus::{function, prelude::*, Error, RArray, RHash, RModule, Ruby};
|
|
7
|
+
use microsandbox::volume::VolumeHandle;
|
|
8
|
+
|
|
9
|
+
use crate::conv;
|
|
10
|
+
use crate::error;
|
|
11
|
+
use crate::runtime::{block_on, ruby};
|
|
12
|
+
|
|
13
|
+
fn handle_to_hash(h: &VolumeHandle) -> RHash {
|
|
14
|
+
let hash = ruby().hash_new();
|
|
15
|
+
let _ = hash.aset("name", h.name().to_string());
|
|
16
|
+
let _ = hash.aset("kind", h.kind().as_str().to_string());
|
|
17
|
+
let _ = hash.aset("quota_mib", h.quota_mib());
|
|
18
|
+
let _ = hash.aset("used_bytes", h.used_bytes());
|
|
19
|
+
let _ = hash.aset("capacity_bytes", h.capacity_bytes());
|
|
20
|
+
let _ = hash.aset("disk_format", h.disk_format().map(str::to_string));
|
|
21
|
+
let _ = hash.aset("disk_fstype", h.disk_fstype().map(str::to_string));
|
|
22
|
+
let _ = hash.aset(
|
|
23
|
+
"created_at_ms",
|
|
24
|
+
h.created_at().map(|dt| dt.timestamp_millis()),
|
|
25
|
+
);
|
|
26
|
+
let labels = ruby().hash_new();
|
|
27
|
+
for (k, v) in h.labels().iter() {
|
|
28
|
+
let _ = labels.aset(k.clone(), v.clone());
|
|
29
|
+
}
|
|
30
|
+
let _ = hash.aset("labels", labels);
|
|
31
|
+
hash
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// Create a named volume. `opts`: kind ("dir"|"disk"), size_mib, quota_mib, labels.
|
|
35
|
+
fn create(name: String, opts: RHash) -> Result<RHash, Error> {
|
|
36
|
+
let mut builder = microsandbox::Volume::builder(&name);
|
|
37
|
+
let kind = conv::opt_string(opts, "kind")?.unwrap_or_else(|| "dir".to_string());
|
|
38
|
+
let size_mib = conv::opt_u32(opts, "size_mib")?;
|
|
39
|
+
|
|
40
|
+
match kind.as_str() {
|
|
41
|
+
"dir" => {
|
|
42
|
+
builder = builder.directory();
|
|
43
|
+
if size_mib.is_some() {
|
|
44
|
+
return Err(error::base_error(
|
|
45
|
+
"size_mib is only supported with kind: \"disk\"",
|
|
46
|
+
));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
"disk" => {
|
|
50
|
+
builder = builder.disk();
|
|
51
|
+
let size = size_mib
|
|
52
|
+
.ok_or_else(|| error::base_error("size_mib is required with kind: \"disk\""))?;
|
|
53
|
+
builder = builder.size(size);
|
|
54
|
+
}
|
|
55
|
+
other => return Err(error::base_error(format!("unknown volume kind: {other:?}"))),
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if let Some(quota) = conv::opt_u32(opts, "quota_mib")? {
|
|
59
|
+
builder = builder.quota(quota);
|
|
60
|
+
}
|
|
61
|
+
for (k, v) in conv::opt_string_map(opts, "labels")? {
|
|
62
|
+
builder = builder.label(k, v);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let vol = block_on(builder.create()).map_err(error::to_ruby)?;
|
|
66
|
+
let hash = ruby().hash_new();
|
|
67
|
+
hash.aset("name", vol.name().to_string())?;
|
|
68
|
+
hash.aset("path", vol.path().display().to_string())?;
|
|
69
|
+
Ok(hash)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
fn get(name: String) -> Result<RHash, Error> {
|
|
73
|
+
let handle = block_on(microsandbox::Volume::get(&name)).map_err(error::to_ruby)?;
|
|
74
|
+
Ok(handle_to_hash(&handle))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
fn list() -> Result<RArray, Error> {
|
|
78
|
+
let handles = block_on(microsandbox::Volume::list()).map_err(error::to_ruby)?;
|
|
79
|
+
let arr = ruby().ary_new();
|
|
80
|
+
for h in handles.iter() {
|
|
81
|
+
arr.push(handle_to_hash(h))?;
|
|
82
|
+
}
|
|
83
|
+
Ok(arr)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
fn remove(name: String) -> Result<(), Error> {
|
|
87
|
+
block_on(microsandbox::Volume::remove(&name)).map_err(error::to_ruby)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
91
|
+
let class = native.define_class("Volume", ruby.class_object())?;
|
|
92
|
+
class.define_singleton_method("create", function!(create, 2))?;
|
|
93
|
+
class.define_singleton_method("get", function!(get, 1))?;
|
|
94
|
+
class.define_singleton_method("list", function!(list, 0))?;
|
|
95
|
+
class.define_singleton_method("remove", function!(remove, 1))?;
|
|
96
|
+
Ok(())
|
|
97
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Microsandbox
|
|
4
|
+
# Base class for every error raised by the SDK. Mirrors the Python SDK's
|
|
5
|
+
# flat hierarchy (`sdk/python/microsandbox/errors.py`): each class carries a
|
|
6
|
+
# stable `#code`. The native layer raises the matching subclass based on the
|
|
7
|
+
# core `MicrosandboxError` variant; unmapped variants surface as `Error`.
|
|
8
|
+
class Error < StandardError
|
|
9
|
+
CODE = "microsandbox-error"
|
|
10
|
+
|
|
11
|
+
# The stable, machine-readable error code for this class.
|
|
12
|
+
def self.code
|
|
13
|
+
const_get(:CODE)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# The stable, machine-readable error code for this instance.
|
|
17
|
+
def code
|
|
18
|
+
self.class.code
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Defines `Microsandbox::<name>` as a subclass of `parent` carrying `code`.
|
|
23
|
+
def self.define_error(name, code, parent = Error)
|
|
24
|
+
klass = Class.new(parent)
|
|
25
|
+
klass.const_set(:CODE, code)
|
|
26
|
+
const_set(name, klass)
|
|
27
|
+
end
|
|
28
|
+
private_class_method :define_error
|
|
29
|
+
|
|
30
|
+
# Configuration / validation errors --------------------------------------
|
|
31
|
+
define_error(:InvalidConfigError, "invalid-config")
|
|
32
|
+
|
|
33
|
+
# Lifecycle errors --------------------------------------------------------
|
|
34
|
+
define_error(:SandboxNotFoundError, "sandbox-not-found")
|
|
35
|
+
define_error(:SandboxNotRunningError, "sandbox-not-running")
|
|
36
|
+
define_error(:SandboxAlreadyExistsError, "sandbox-already-exists")
|
|
37
|
+
define_error(:SandboxStillRunningError, "sandbox-still-running")
|
|
38
|
+
|
|
39
|
+
# Execution errors --------------------------------------------------------
|
|
40
|
+
define_error(:ExecTimeoutError, "exec-timeout")
|
|
41
|
+
define_error(:ExecFailedError, "exec-failed")
|
|
42
|
+
|
|
43
|
+
# Filesystem errors -------------------------------------------------------
|
|
44
|
+
define_error(:FilesystemError, "filesystem-error")
|
|
45
|
+
define_error(:PathNotFoundError, "path-not-found")
|
|
46
|
+
|
|
47
|
+
# Volume / image errors ---------------------------------------------------
|
|
48
|
+
define_error(:VolumeNotFoundError, "volume-not-found")
|
|
49
|
+
define_error(:VolumeAlreadyExistsError, "volume-already-exists")
|
|
50
|
+
define_error(:ImageNotFoundError, "image-not-found")
|
|
51
|
+
define_error(:ImageInUseError, "image-in-use")
|
|
52
|
+
define_error(:ImagePullFailedError, "image-pull-failed")
|
|
53
|
+
|
|
54
|
+
# Networking / secrets errors ---------------------------------------------
|
|
55
|
+
define_error(:NetworkPolicyError, "network-policy-error")
|
|
56
|
+
define_error(:SecretViolationError, "secret-violation")
|
|
57
|
+
define_error(:TlsError, "tls-error")
|
|
58
|
+
|
|
59
|
+
# I/O ---------------------------------------------------------------------
|
|
60
|
+
define_error(:IoError, "io-error")
|
|
61
|
+
|
|
62
|
+
# Metrics errors ----------------------------------------------------------
|
|
63
|
+
define_error(:MetricsDisabledError, "metrics-disabled")
|
|
64
|
+
define_error(:MetricsUnavailableError, "metrics-unavailable")
|
|
65
|
+
|
|
66
|
+
# Runtime compatibility ---------------------------------------------------
|
|
67
|
+
define_error(:UnsupportedOperationError, "unsupported-operation")
|
|
68
|
+
end
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Microsandbox
|
|
4
|
+
# A single event from a streaming execution ({Sandbox#exec_stream}).
|
|
5
|
+
class ExecEvent
|
|
6
|
+
# @return [Symbol] :started, :stdout, :stderr, :exited, :failed, or :stdin_error
|
|
7
|
+
attr_reader :type
|
|
8
|
+
# @return [Integer, nil] pid (for :started)
|
|
9
|
+
attr_reader :pid
|
|
10
|
+
# @return [Integer, nil] exit code (:exited) or errno (:failed/:stdin_error)
|
|
11
|
+
attr_reader :code
|
|
12
|
+
# @return [String, nil] raw bytes (ASCII-8BIT) for :stdout/:stderr, or the
|
|
13
|
+
# message for :failed/:stdin_error
|
|
14
|
+
attr_reader :data
|
|
15
|
+
|
|
16
|
+
def initialize(event)
|
|
17
|
+
@type = event["type"].to_sym
|
|
18
|
+
@pid = event["pid"]
|
|
19
|
+
@code = event["code"]
|
|
20
|
+
@data = event["data"]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @return [String, nil] {#data} decoded as UTF-8 (lenient)
|
|
24
|
+
def text
|
|
25
|
+
@data && @data.dup.force_encoding(Encoding::UTF_8)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def started? = @type == :started
|
|
29
|
+
def stdout? = @type == :stdout
|
|
30
|
+
def stderr? = @type == :stderr
|
|
31
|
+
def exited? = @type == :exited
|
|
32
|
+
def failed? = @type == :failed
|
|
33
|
+
def stdin_error? = @type == :stdin_error
|
|
34
|
+
|
|
35
|
+
def inspect
|
|
36
|
+
"#<Microsandbox::ExecEvent type=#{@type}#{@pid ? " pid=#{@pid}" : ""}" \
|
|
37
|
+
"#{@code ? " code=#{@code}" : ""}#{@data ? " data=#{@data.bytesize}B" : ""}>"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# The terminal status of a streamed execution, from {ExecHandle#wait}.
|
|
42
|
+
class ExitStatus
|
|
43
|
+
# @return [Integer]
|
|
44
|
+
attr_reader :exit_code
|
|
45
|
+
|
|
46
|
+
def initialize(data)
|
|
47
|
+
@exit_code = data["exit_code"]
|
|
48
|
+
@success = data["success"]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def success? = @success
|
|
52
|
+
def failure? = !@success
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# A writer for a streamed process's stdin, from {ExecHandle#stdin}.
|
|
56
|
+
class ExecStdin
|
|
57
|
+
def initialize(native)
|
|
58
|
+
@native = native
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Write data to the process stdin.
|
|
62
|
+
# @return [self]
|
|
63
|
+
def write(data)
|
|
64
|
+
@native.write(data.to_s)
|
|
65
|
+
self
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Send EOF.
|
|
69
|
+
# @return [nil]
|
|
70
|
+
def close
|
|
71
|
+
@native.close
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# A live, streaming command execution, returned by {Sandbox#exec_stream} and
|
|
77
|
+
# {Sandbox#shell_stream}.
|
|
78
|
+
#
|
|
79
|
+
# Iterate it (it is {Enumerable}) to consume {ExecEvent}s as they arrive, or
|
|
80
|
+
# call {#collect} to drain it into an {ExecOutput}.
|
|
81
|
+
#
|
|
82
|
+
# @example
|
|
83
|
+
# handle = sb.exec_stream("python", ["-u", "script.py"])
|
|
84
|
+
# handle.each do |event|
|
|
85
|
+
# print event.text if event.stdout? || event.stderr?
|
|
86
|
+
# end
|
|
87
|
+
class ExecHandle
|
|
88
|
+
include Enumerable
|
|
89
|
+
|
|
90
|
+
def initialize(native)
|
|
91
|
+
@native = native
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# @return [String] the correlation id for this execution
|
|
95
|
+
def id
|
|
96
|
+
@native.id
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Yield each {ExecEvent} until the stream ends. Returns an Enumerator when
|
|
100
|
+
# called without a block.
|
|
101
|
+
# @yieldparam event [ExecEvent]
|
|
102
|
+
# @return [self, Enumerator]
|
|
103
|
+
def each
|
|
104
|
+
return enum_for(:each) unless block_given?
|
|
105
|
+
|
|
106
|
+
while (event = @native.recv)
|
|
107
|
+
yield ExecEvent.new(event)
|
|
108
|
+
end
|
|
109
|
+
self
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Block until the process exits.
|
|
113
|
+
# @return [ExitStatus]
|
|
114
|
+
def wait
|
|
115
|
+
ExitStatus.new(@native.wait)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Drain the stream and collect all output.
|
|
119
|
+
# @return [ExecOutput]
|
|
120
|
+
def collect
|
|
121
|
+
ExecOutput.new(@native.collect)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Send a signal (integer) to the running process.
|
|
125
|
+
# @return [nil]
|
|
126
|
+
def signal(sig)
|
|
127
|
+
@native.signal(Integer(sig))
|
|
128
|
+
nil
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Kill the running process (SIGKILL).
|
|
132
|
+
# @return [nil]
|
|
133
|
+
def kill
|
|
134
|
+
@native.kill
|
|
135
|
+
nil
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Resize the pseudo-terminal (only meaningful when started with tty: true).
|
|
139
|
+
# @return [nil]
|
|
140
|
+
def resize(rows, cols)
|
|
141
|
+
@native.resize(Integer(rows), Integer(cols))
|
|
142
|
+
nil
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# The stdin writer, or nil if stdin was not piped. Returned only once.
|
|
146
|
+
# @return [ExecStdin, nil]
|
|
147
|
+
def stdin
|
|
148
|
+
return @stdin if defined?(@stdin)
|
|
149
|
+
|
|
150
|
+
native_sink = @native.take_stdin
|
|
151
|
+
@stdin = native_sink && ExecStdin.new(native_sink)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Microsandbox
|
|
4
|
+
# The result of a completed `exec`/`shell` call.
|
|
5
|
+
#
|
|
6
|
+
# `stdout`/`stderr` return the captured bytes decoded as UTF-8 (lenient — the
|
|
7
|
+
# bytes are preserved even if they are not valid UTF-8); use `stdout_bytes`/
|
|
8
|
+
# `stderr_bytes` for the raw ASCII-8BIT bytes.
|
|
9
|
+
class ExecOutput
|
|
10
|
+
# @return [Integer] the process exit code
|
|
11
|
+
attr_reader :exit_code
|
|
12
|
+
# @return [String] raw stdout bytes (ASCII-8BIT)
|
|
13
|
+
attr_reader :stdout_bytes
|
|
14
|
+
# @return [String] raw stderr bytes (ASCII-8BIT)
|
|
15
|
+
attr_reader :stderr_bytes
|
|
16
|
+
|
|
17
|
+
# @param data [Hash] the native exec result hash
|
|
18
|
+
def initialize(data)
|
|
19
|
+
@exit_code = data["exit_code"]
|
|
20
|
+
@success = data["success"]
|
|
21
|
+
@stdout_bytes = data["stdout"]
|
|
22
|
+
@stderr_bytes = data["stderr"]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @return [Boolean] whether the process exited with status 0
|
|
26
|
+
def success?
|
|
27
|
+
@success
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @return [Boolean] whether the process exited non-zero
|
|
31
|
+
def failure?
|
|
32
|
+
!@success
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @return [String] stdout decoded as UTF-8
|
|
36
|
+
def stdout
|
|
37
|
+
@stdout ||= @stdout_bytes.dup.force_encoding(Encoding::UTF_8)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @return [String] stderr decoded as UTF-8
|
|
41
|
+
def stderr
|
|
42
|
+
@stderr ||= @stderr_bytes.dup.force_encoding(Encoding::UTF_8)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @return [String] stdout decoded as UTF-8 (alias for {#stdout})
|
|
46
|
+
def to_s
|
|
47
|
+
stdout
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def inspect
|
|
51
|
+
"#<Microsandbox::ExecOutput exit_code=#{@exit_code} success=#{@success} " \
|
|
52
|
+
"stdout=#{stdout.bytesize}B stderr=#{stderr.bytesize}B>"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|