microsandbox-rb 0.6.0 → 0.7.0
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 +96 -1
- data/Cargo.lock +1 -1
- data/DESIGN.md +16 -6
- data/README.md +27 -14
- data/ext/microsandbox/Cargo.toml +1 -1
- data/ext/microsandbox/src/agent.rs +18 -7
- data/ext/microsandbox/src/conv.rs +37 -1
- data/ext/microsandbox/src/fs_stream.rs +92 -0
- data/ext/microsandbox/src/image.rs +6 -0
- data/ext/microsandbox/src/lib.rs +40 -0
- data/ext/microsandbox/src/sandbox.rs +673 -64
- data/ext/microsandbox/src/snapshot.rs +72 -6
- data/ext/microsandbox/src/volume.rs +113 -1
- data/lib/microsandbox/agent.rb +3 -1
- data/lib/microsandbox/exec_handle.rb +7 -3
- data/lib/microsandbox/exec_output.rb +7 -7
- data/lib/microsandbox/fs.rb +84 -2
- data/lib/microsandbox/image.rb +2 -1
- data/lib/microsandbox/log_entry.rb +4 -2
- data/lib/microsandbox/metrics.rb +9 -0
- data/lib/microsandbox/sandbox.rb +461 -70
- data/lib/microsandbox/snapshot.rb +63 -6
- data/lib/microsandbox/ssh.rb +14 -9
- data/lib/microsandbox/version.rb +1 -1
- data/lib/microsandbox/volume.rb +100 -1
- data/lib/microsandbox.rb +35 -1
- data/sig/microsandbox.rbs +70 -6
- metadata +2 -1
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
//! singleton functions returning plain Hashes/Arrays (shaped into value objects
|
|
6
6
|
//! by the Ruby layer) — there is no long-lived handle to own.
|
|
7
7
|
|
|
8
|
-
use std::path::PathBuf;
|
|
8
|
+
use std::path::{Path, PathBuf};
|
|
9
9
|
|
|
10
10
|
use magnus::{function, prelude::*, Error, RArray, RHash, RModule, Ruby};
|
|
11
11
|
use microsandbox::snapshot::{
|
|
@@ -24,6 +24,38 @@ fn format_str(format: SnapshotFormat) -> &'static str {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/// Parse a manifest's RFC 3339 `created_at` into epoch-ms (nil if unparseable).
|
|
28
|
+
fn created_at_ms(rfc3339: &str) -> Option<i64> {
|
|
29
|
+
chrono::DateTime::parse_from_rfc3339(rfc3339)
|
|
30
|
+
.ok()
|
|
31
|
+
.map(|dt| dt.timestamp_millis())
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// Convert a fully-opened `Snapshot` into the `SnapshotInfo` Hash. Unlike a
|
|
35
|
+
/// `SnapshotHandle` (a lightweight index row), an opened snapshot carries the
|
|
36
|
+
/// full manifest, so this is the richest shape — `create`/`open`/`list_dir`
|
|
37
|
+
/// and the `SandboxHandle#snapshot`/`#snapshot_to` shortcuts all funnel here.
|
|
38
|
+
pub(crate) fn snapshot_to_hash(snap: &Snapshot) -> RHash {
|
|
39
|
+
let m = snap.manifest();
|
|
40
|
+
let hash = ruby().hash_new();
|
|
41
|
+
let _ = hash.aset("digest", snap.digest().to_string());
|
|
42
|
+
let _ = hash.aset("path", snap.path().to_string_lossy().into_owned());
|
|
43
|
+
let _ = hash.aset("size_bytes", snap.size_bytes());
|
|
44
|
+
let _ = hash.aset("image_ref", m.image.reference.clone());
|
|
45
|
+
let _ = hash.aset("image_manifest_digest", m.image.manifest_digest.clone());
|
|
46
|
+
let _ = hash.aset("format", format_str(m.format));
|
|
47
|
+
let _ = hash.aset("fstype", m.fstype.clone());
|
|
48
|
+
let _ = hash.aset("parent_digest", m.parent.clone());
|
|
49
|
+
let _ = hash.aset("created_at_ms", created_at_ms(&m.created_at));
|
|
50
|
+
let _ = hash.aset("source_sandbox", m.source_sandbox.clone());
|
|
51
|
+
let labels = ruby().hash_new();
|
|
52
|
+
for (k, v) in &m.labels {
|
|
53
|
+
let _ = labels.aset(k.as_str(), v.as_str());
|
|
54
|
+
}
|
|
55
|
+
let _ = hash.aset("labels", labels);
|
|
56
|
+
hash
|
|
57
|
+
}
|
|
58
|
+
|
|
27
59
|
/// Create a snapshot of a stopped sandbox. `opts`: name | path (destination),
|
|
28
60
|
/// labels, force, record_integrity. Returns {digest, path, size_bytes}.
|
|
29
61
|
fn create(source_sandbox: String, opts: RHash) -> Result<RHash, Error> {
|
|
@@ -48,11 +80,42 @@ fn create(source_sandbox: String, opts: RHash) -> Result<RHash, Error> {
|
|
|
48
80
|
}
|
|
49
81
|
|
|
50
82
|
let snap = block_on(b.create()).map_err(error::to_ruby)?;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
83
|
+
Ok(snapshot_to_hash(&snap))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// Open an existing snapshot artifact by bare name or path. Cheap metadata
|
|
87
|
+
/// validation only (does not read the upper file). Returns a full SnapshotInfo
|
|
88
|
+
/// Hash — the only way to inspect an artifact addressed by path (`get`/`list`
|
|
89
|
+
/// read the local index, which path-addressed artifacts are absent from).
|
|
90
|
+
fn open(path_or_name: String) -> Result<RHash, Error> {
|
|
91
|
+
let snap = block_on(Snapshot::open(&path_or_name)).map_err(error::to_ruby)?;
|
|
92
|
+
Ok(snapshot_to_hash(&snap))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// Walk `dir` and parse each subdirectory's `manifest.json` without touching the
|
|
96
|
+
/// local index — for enumerating external/un-imported snapshot collections.
|
|
97
|
+
fn list_dir(dir: String) -> Result<RArray, Error> {
|
|
98
|
+
let snaps = block_on(Snapshot::list_dir(Path::new(&dir))).map_err(error::to_ruby)?;
|
|
99
|
+
let arr = ruby().ary_new();
|
|
100
|
+
for snap in &snaps {
|
|
101
|
+
arr.push(snapshot_to_hash(snap))?;
|
|
102
|
+
}
|
|
103
|
+
Ok(arr)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/// Rebuild the local snapshot index from `dir` (defaults to the configured
|
|
107
|
+
/// snapshots directory). Returns the number of indexed snapshots — the repair
|
|
108
|
+
/// for index drift or out-of-band imports that `get`/`list` can't see.
|
|
109
|
+
fn reindex(dir: Option<String>) -> Result<u64, Error> {
|
|
110
|
+
let dir: PathBuf = match dir {
|
|
111
|
+
Some(d) => PathBuf::from(d),
|
|
112
|
+
None => microsandbox::default_backend()
|
|
113
|
+
.as_local()
|
|
114
|
+
.map(|l| l.snapshots_dir())
|
|
115
|
+
.unwrap_or_else(|| PathBuf::from(".")),
|
|
116
|
+
};
|
|
117
|
+
let n = block_on(Snapshot::reindex(&dir)).map_err(error::to_ruby)?;
|
|
118
|
+
Ok(n as u64)
|
|
56
119
|
}
|
|
57
120
|
|
|
58
121
|
/// Metadata for one snapshot by name or digest.
|
|
@@ -148,8 +211,11 @@ fn verify_report_to_hash(report: &SnapshotVerifyReport) -> RHash {
|
|
|
148
211
|
pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
149
212
|
let class = native.define_class("Snapshot", ruby.class_object())?;
|
|
150
213
|
class.define_singleton_method("create", function!(create, 2))?;
|
|
214
|
+
class.define_singleton_method("open", function!(open, 1))?;
|
|
151
215
|
class.define_singleton_method("get", function!(get, 1))?;
|
|
152
216
|
class.define_singleton_method("list", function!(list, 0))?;
|
|
217
|
+
class.define_singleton_method("list_dir", function!(list_dir, 1))?;
|
|
218
|
+
class.define_singleton_method("reindex", function!(reindex, 1))?;
|
|
153
219
|
class.define_singleton_method("remove", function!(remove, 2))?;
|
|
154
220
|
class.define_singleton_method("verify", function!(verify, 1))?;
|
|
155
221
|
class.define_singleton_method("export", function!(export, 3))?;
|
|
@@ -3,12 +3,17 @@
|
|
|
3
3
|
//! Mirrors `sdk/python/src/volume.rs`. Static async CRUD over the volume store;
|
|
4
4
|
//! results are returned as plain Ruby Hashes/Arrays.
|
|
5
5
|
|
|
6
|
-
use
|
|
6
|
+
use std::sync::Arc;
|
|
7
|
+
|
|
8
|
+
use magnus::{function, method, prelude::*, Error, RArray, RHash, RModule, RString, Ruby};
|
|
7
9
|
use microsandbox::volume::VolumeHandle;
|
|
10
|
+
use microsandbox::Backend;
|
|
8
11
|
|
|
12
|
+
use crate::backend::local_backend;
|
|
9
13
|
use crate::conv;
|
|
10
14
|
use crate::error;
|
|
11
15
|
use crate::runtime::{block_on, ruby};
|
|
16
|
+
use crate::sandbox::{fs_entry_to_hash, fs_metadata_to_hash};
|
|
12
17
|
|
|
13
18
|
fn handle_to_hash(h: &VolumeHandle) -> RHash {
|
|
14
19
|
let hash = ruby().hash_new();
|
|
@@ -92,11 +97,118 @@ fn remove(name: String) -> Result<(), Error> {
|
|
|
92
97
|
block_on(microsandbox::Volume::remove(&name)).map_err(error::to_ruby)
|
|
93
98
|
}
|
|
94
99
|
|
|
100
|
+
/// A host-side filesystem view over a named volume — read/write its contents
|
|
101
|
+
/// without a running sandbox. Mirrors the Python `VolumeFs` / Node `VolumeFs`.
|
|
102
|
+
/// The core `VolumeFs<'a>` borrows the volume name, so (like the Python binding)
|
|
103
|
+
/// each operation rebuilds it from the stored backend + name.
|
|
104
|
+
#[magnus::wrap(class = "Microsandbox::Native::VolumeFs", free_immediately, size)]
|
|
105
|
+
pub struct VolumeFs {
|
|
106
|
+
backend: Arc<dyn Backend>,
|
|
107
|
+
name: String,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
impl VolumeFs {
|
|
111
|
+
/// Resolve the (local) backend once and bind it to `name`.
|
|
112
|
+
fn for_volume(name: String) -> Result<VolumeFs, Error> {
|
|
113
|
+
Ok(VolumeFs {
|
|
114
|
+
backend: local_backend().map_err(error::to_ruby)?,
|
|
115
|
+
name,
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
fn fs(&self) -> microsandbox::volume::VolumeFs<'_> {
|
|
120
|
+
microsandbox::volume::VolumeFs::with_backend(self.backend.clone(), &self.name)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
fn read(&self, path: String) -> Result<RString, Error> {
|
|
124
|
+
let fs = self.fs();
|
|
125
|
+
let bytes = block_on(fs.read(&path)).map_err(error::to_ruby)?;
|
|
126
|
+
Ok(ruby().str_from_slice(bytes.as_ref()))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
fn read_text(&self, path: String) -> Result<String, Error> {
|
|
130
|
+
let fs = self.fs();
|
|
131
|
+
block_on(fs.read_to_string(&path)).map_err(error::to_ruby)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
fn write(&self, path: String, data: RString) -> Result<(), Error> {
|
|
135
|
+
// Copy the bytes out while the GVL is held (GC.compact could move them).
|
|
136
|
+
let bytes = unsafe { data.as_slice() }.to_vec();
|
|
137
|
+
let fs = self.fs();
|
|
138
|
+
block_on(fs.write(&path, &bytes)).map_err(error::to_ruby)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
fn list(&self, path: String) -> Result<RArray, Error> {
|
|
142
|
+
let fs = self.fs();
|
|
143
|
+
let entries = block_on(fs.list(&path)).map_err(error::to_ruby)?;
|
|
144
|
+
let arr = ruby().ary_new();
|
|
145
|
+
for entry in &entries {
|
|
146
|
+
arr.push(fs_entry_to_hash(entry))?;
|
|
147
|
+
}
|
|
148
|
+
Ok(arr)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
fn mkdir(&self, path: String) -> Result<(), Error> {
|
|
152
|
+
let fs = self.fs();
|
|
153
|
+
block_on(fs.mkdir(&path)).map_err(error::to_ruby)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
fn remove_file(&self, path: String) -> Result<(), Error> {
|
|
157
|
+
let fs = self.fs();
|
|
158
|
+
block_on(fs.remove(&path)).map_err(error::to_ruby)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
fn remove_dir(&self, path: String) -> Result<(), Error> {
|
|
162
|
+
let fs = self.fs();
|
|
163
|
+
block_on(fs.remove_dir(&path)).map_err(error::to_ruby)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
fn exists(&self, path: String) -> Result<bool, Error> {
|
|
167
|
+
let fs = self.fs();
|
|
168
|
+
block_on(fs.exists(&path)).map_err(error::to_ruby)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
fn copy(&self, from: String, to: String) -> Result<(), Error> {
|
|
172
|
+
let fs = self.fs();
|
|
173
|
+
block_on(fs.copy(&from, &to)).map_err(error::to_ruby)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
fn rename(&self, from: String, to: String) -> Result<(), Error> {
|
|
177
|
+
let fs = self.fs();
|
|
178
|
+
block_on(fs.rename(&from, &to)).map_err(error::to_ruby)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
fn stat(&self, path: String) -> Result<RHash, Error> {
|
|
182
|
+
let fs = self.fs();
|
|
183
|
+
let meta = block_on(fs.stat(&path)).map_err(error::to_ruby)?;
|
|
184
|
+
Ok(fs_metadata_to_hash(&meta))
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/// Open a host-side filesystem view over a named volume.
|
|
189
|
+
fn fs(name: String) -> Result<VolumeFs, Error> {
|
|
190
|
+
VolumeFs::for_volume(name)
|
|
191
|
+
}
|
|
192
|
+
|
|
95
193
|
pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
96
194
|
let class = native.define_class("Volume", ruby.class_object())?;
|
|
97
195
|
class.define_singleton_method("create", function!(create, 2))?;
|
|
98
196
|
class.define_singleton_method("get", function!(get, 1))?;
|
|
99
197
|
class.define_singleton_method("list", function!(list, 0))?;
|
|
100
198
|
class.define_singleton_method("remove", function!(remove, 1))?;
|
|
199
|
+
class.define_singleton_method("fs", function!(fs, 1))?;
|
|
200
|
+
|
|
201
|
+
let vfs = native.define_class("VolumeFs", ruby.class_object())?;
|
|
202
|
+
vfs.define_method("read", method!(VolumeFs::read, 1))?;
|
|
203
|
+
vfs.define_method("read_text", method!(VolumeFs::read_text, 1))?;
|
|
204
|
+
vfs.define_method("write", method!(VolumeFs::write, 2))?;
|
|
205
|
+
vfs.define_method("list", method!(VolumeFs::list, 1))?;
|
|
206
|
+
vfs.define_method("mkdir", method!(VolumeFs::mkdir, 1))?;
|
|
207
|
+
vfs.define_method("remove_file", method!(VolumeFs::remove_file, 1))?;
|
|
208
|
+
vfs.define_method("remove_dir", method!(VolumeFs::remove_dir, 1))?;
|
|
209
|
+
vfs.define_method("exists", method!(VolumeFs::exists, 1))?;
|
|
210
|
+
vfs.define_method("copy", method!(VolumeFs::copy, 2))?;
|
|
211
|
+
vfs.define_method("rename", method!(VolumeFs::rename, 2))?;
|
|
212
|
+
vfs.define_method("stat", method!(VolumeFs::stat, 1))?;
|
|
101
213
|
Ok(())
|
|
102
214
|
}
|
data/lib/microsandbox/agent.rb
CHANGED
|
@@ -100,7 +100,9 @@ module Microsandbox
|
|
|
100
100
|
# Connect to a running sandbox by name (max 128 UTF-8 bytes). With a block,
|
|
101
101
|
# the client is yielded and closed when the block returns.
|
|
102
102
|
# @param name [String]
|
|
103
|
-
# @param timeout [Numeric, nil] handshake timeout in seconds (
|
|
103
|
+
# @param timeout [Numeric, nil] handshake timeout in seconds. nil (the
|
|
104
|
+
# default) uses the core default (~10s); 0 fails fast (an immediate
|
|
105
|
+
# deadline); a negative or non-finite value raises {Error}.
|
|
104
106
|
# @yieldparam client [AgentClient]
|
|
105
107
|
# @return [AgentClient, Object]
|
|
106
108
|
def connect_sandbox(name, timeout: nil, &block)
|
|
@@ -20,9 +20,10 @@ module Microsandbox
|
|
|
20
20
|
@data = event["data"]
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
# @return [String, nil] {#data} decoded as UTF-8 (
|
|
23
|
+
# @return [String, nil] {#data} decoded as UTF-8 (lossy — invalid byte
|
|
24
|
+
# sequences are replaced with U+FFFD, so the result is always valid UTF-8)
|
|
24
25
|
def text
|
|
25
|
-
@data&.dup&.force_encoding(Encoding::UTF_8)
|
|
26
|
+
@data&.dup&.force_encoding(Encoding::UTF_8)&.scrub
|
|
26
27
|
end
|
|
27
28
|
|
|
28
29
|
def started? = @type == :started
|
|
@@ -62,9 +63,12 @@ module Microsandbox
|
|
|
62
63
|
end
|
|
63
64
|
|
|
64
65
|
# Write data to the process stdin.
|
|
66
|
+
# @param data [String] raw bytes to write (binary-safe)
|
|
67
|
+
# @raise [TypeError] if +data+ is not a String
|
|
65
68
|
# @return [self]
|
|
66
69
|
def write(data)
|
|
67
|
-
|
|
70
|
+
bytes = Microsandbox.coerce_write_bytes(data)
|
|
71
|
+
@native.write(bytes)
|
|
68
72
|
self
|
|
69
73
|
end
|
|
70
74
|
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
module Microsandbox
|
|
4
4
|
# The result of a completed `exec`/`shell` call.
|
|
5
5
|
#
|
|
6
|
-
# `stdout`/`stderr` return the captured bytes decoded as UTF-8 (
|
|
7
|
-
#
|
|
8
|
-
# `stderr_bytes` for the raw ASCII-8BIT bytes.
|
|
6
|
+
# `stdout`/`stderr` return the captured bytes decoded as UTF-8 (lossy —
|
|
7
|
+
# invalid byte sequences are replaced with U+FFFD, so the result is always
|
|
8
|
+
# valid UTF-8); use `stdout_bytes`/`stderr_bytes` for the raw ASCII-8BIT bytes.
|
|
9
9
|
class ExecOutput
|
|
10
10
|
# @return [Integer] the process exit code
|
|
11
11
|
attr_reader :exit_code
|
|
@@ -32,14 +32,14 @@ module Microsandbox
|
|
|
32
32
|
!@success
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
# @return [String] stdout decoded as UTF-8
|
|
35
|
+
# @return [String] stdout decoded as UTF-8 (lossy)
|
|
36
36
|
def stdout
|
|
37
|
-
@stdout ||= @stdout_bytes.dup.force_encoding(Encoding::UTF_8)
|
|
37
|
+
@stdout ||= @stdout_bytes.dup.force_encoding(Encoding::UTF_8).scrub
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
# @return [String] stderr decoded as UTF-8
|
|
40
|
+
# @return [String] stderr decoded as UTF-8 (lossy)
|
|
41
41
|
def stderr
|
|
42
|
-
@stderr ||= @stderr_bytes.dup.force_encoding(Encoding::UTF_8)
|
|
42
|
+
@stderr ||= @stderr_bytes.dup.force_encoding(Encoding::UTF_8).scrub
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
# @return [String] stdout decoded as UTF-8 (alias for {#stdout})
|
data/lib/microsandbox/fs.rb
CHANGED
|
@@ -96,10 +96,14 @@ module Microsandbox
|
|
|
96
96
|
@native.fs_read_text(path.to_s)
|
|
97
97
|
end
|
|
98
98
|
|
|
99
|
-
# Write data
|
|
99
|
+
# Write data to a file, creating or truncating it.
|
|
100
|
+
# @param data [String] raw bytes to write (binary-safe; ASCII-8BIT is fine)
|
|
101
|
+
# @raise [TypeError] if +data+ is not a String (rather than silently writing
|
|
102
|
+
# its +to_s+ form, e.g. the inspect string of a StringIO or "42")
|
|
100
103
|
# @return [nil]
|
|
101
104
|
def write(path, data)
|
|
102
|
-
|
|
105
|
+
bytes = Microsandbox.coerce_write_bytes(data)
|
|
106
|
+
@native.fs_write(path.to_s, bytes)
|
|
103
107
|
nil
|
|
104
108
|
end
|
|
105
109
|
|
|
@@ -168,5 +172,83 @@ module Microsandbox
|
|
|
168
172
|
@native.fs_copy_to_host(guest_path.to_s, host_path.to_s)
|
|
169
173
|
nil
|
|
170
174
|
end
|
|
175
|
+
|
|
176
|
+
# Open a streaming reader over a guest file — for files too large to read
|
|
177
|
+
# into memory at once (unlike {#read}, which buffers the whole file).
|
|
178
|
+
# @return [FsReadStream] an {Enumerable} of byte chunks (ASCII-8BIT)
|
|
179
|
+
def read_stream(path)
|
|
180
|
+
FsReadStream.new(@native.fs_read_stream(path.to_s))
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Open a streaming writer to a guest file. With a block, the sink is yielded
|
|
184
|
+
# and closed (flushed) when the block returns.
|
|
185
|
+
# @yieldparam sink [FsWriteSink]
|
|
186
|
+
# @return [FsWriteSink, Object]
|
|
187
|
+
def write_stream(path)
|
|
188
|
+
sink = FsWriteSink.new(@native.fs_write_stream(path.to_s))
|
|
189
|
+
return sink unless block_given?
|
|
190
|
+
|
|
191
|
+
begin
|
|
192
|
+
yield sink
|
|
193
|
+
ensure
|
|
194
|
+
sink.close
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# A streaming reader over a guest file, from {FS#read_stream}. Iterate it (it
|
|
200
|
+
# is {Enumerable}) to consume byte chunks (ASCII-8BIT) as they arrive, or call
|
|
201
|
+
# {#read} to drain it into one String.
|
|
202
|
+
class FsReadStream
|
|
203
|
+
include Enumerable
|
|
204
|
+
|
|
205
|
+
def initialize(native)
|
|
206
|
+
@native = native
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Yield each chunk of bytes until the stream ends. Returns an Enumerator when
|
|
210
|
+
# called without a block.
|
|
211
|
+
# @yieldparam chunk [String] raw bytes (ASCII-8BIT)
|
|
212
|
+
# @return [self, Enumerator]
|
|
213
|
+
def each
|
|
214
|
+
return enum_for(:each) unless block_given?
|
|
215
|
+
|
|
216
|
+
while (chunk = @native.recv)
|
|
217
|
+
yield chunk
|
|
218
|
+
end
|
|
219
|
+
self
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Drain the stream into a single byte String.
|
|
223
|
+
# @return [String] raw bytes (ASCII-8BIT)
|
|
224
|
+
def read
|
|
225
|
+
buffer = +"".b
|
|
226
|
+
each { |chunk| buffer << chunk }
|
|
227
|
+
buffer
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# A streaming writer to a guest file, from {FS#write_stream}.
|
|
232
|
+
class FsWriteSink
|
|
233
|
+
def initialize(native)
|
|
234
|
+
@native = native
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Write a chunk of bytes.
|
|
238
|
+
# @param data [String] raw bytes (binary-safe)
|
|
239
|
+
# @raise [TypeError] if +data+ is not a String
|
|
240
|
+
# @return [self]
|
|
241
|
+
def write(data)
|
|
242
|
+
bytes = Microsandbox.coerce_write_bytes(data)
|
|
243
|
+
@native.write(bytes)
|
|
244
|
+
self
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Flush and close the sink. Idempotent.
|
|
248
|
+
# @return [nil]
|
|
249
|
+
def close
|
|
250
|
+
@native.close
|
|
251
|
+
nil
|
|
252
|
+
end
|
|
171
253
|
end
|
|
172
254
|
end
|
data/lib/microsandbox/image.rb
CHANGED
|
@@ -35,7 +35,8 @@ module Microsandbox
|
|
|
35
35
|
class ImageDetail
|
|
36
36
|
# @return [ImageInfo]
|
|
37
37
|
attr_reader :handle
|
|
38
|
-
# @return [Hash, nil] OCI config (digest, env, cmd, entrypoint, working_dir,
|
|
38
|
+
# @return [Hash, nil] OCI config (digest, env, cmd, entrypoint, working_dir,
|
|
39
|
+
# user, labels, stop_signal). `labels` is a Hash (or nil) of OCI config labels.
|
|
39
40
|
attr_reader :config
|
|
40
41
|
# @return [Array<Hash>] layer descriptors
|
|
41
42
|
attr_reader :layers
|
|
@@ -25,9 +25,11 @@ module Microsandbox
|
|
|
25
25
|
Time.at(@timestamp_ms / 1000.0)
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
# @return [String] the captured bytes decoded as UTF-8 (
|
|
28
|
+
# @return [String] the captured bytes decoded as UTF-8 (lossy — invalid byte
|
|
29
|
+
# sequences are replaced with U+FFFD, so the result is always valid UTF-8;
|
|
30
|
+
# use {#data} for the raw ASCII-8BIT bytes)
|
|
29
31
|
def text
|
|
30
|
-
@data.dup.force_encoding(Encoding::UTF_8)
|
|
32
|
+
@data.dup.force_encoding(Encoding::UTF_8).scrub
|
|
31
33
|
end
|
|
32
34
|
|
|
33
35
|
def inspect
|
data/lib/microsandbox/metrics.rb
CHANGED
|
@@ -24,6 +24,12 @@ module Microsandbox
|
|
|
24
24
|
attr_reader :net_rx_bytes
|
|
25
25
|
# @return [Integer] cumulative bytes transmitted over the network
|
|
26
26
|
attr_reader :net_tx_bytes
|
|
27
|
+
# @return [Integer, nil] bytes used in the writable OCI upper layer
|
|
28
|
+
attr_reader :upper_used_bytes
|
|
29
|
+
# @return [Integer, nil] bytes free in the writable OCI upper layer
|
|
30
|
+
attr_reader :upper_free_bytes
|
|
31
|
+
# @return [Integer, nil] host bytes allocated to back the upper layer
|
|
32
|
+
attr_reader :upper_host_allocated_bytes
|
|
27
33
|
# @return [Float] sandbox uptime in seconds
|
|
28
34
|
attr_reader :uptime_secs
|
|
29
35
|
|
|
@@ -38,6 +44,9 @@ module Microsandbox
|
|
|
38
44
|
@disk_write_bytes = data["disk_write_bytes"]
|
|
39
45
|
@net_rx_bytes = data["net_rx_bytes"]
|
|
40
46
|
@net_tx_bytes = data["net_tx_bytes"]
|
|
47
|
+
@upper_used_bytes = data["upper_used_bytes"]
|
|
48
|
+
@upper_free_bytes = data["upper_free_bytes"]
|
|
49
|
+
@upper_host_allocated_bytes = data["upper_host_allocated_bytes"]
|
|
41
50
|
@uptime_secs = data["uptime_secs"]
|
|
42
51
|
@timestamp_ms = data["timestamp_ms"]
|
|
43
52
|
end
|