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,812 @@
|
|
|
1
|
+
//! `Microsandbox::Native::Sandbox` — the single wrapped native class.
|
|
2
|
+
//!
|
|
3
|
+
//! Holds a core `microsandbox::Sandbox` (cheap to clone; Arc-based) and exposes
|
|
4
|
+
//! synchronous, primitive-typed methods. Filesystem operations are folded in as
|
|
5
|
+
//! `fs_*` methods rather than a separate wrapper class. Everything that isn't a
|
|
6
|
+
//! handle (exec output, metrics, log entries, fs entries/metadata) is returned
|
|
7
|
+
//! as a plain Ruby `Hash`/`Array`/`String` and shaped into value objects by the
|
|
8
|
+
//! Ruby layer.
|
|
9
|
+
|
|
10
|
+
use std::time::Duration;
|
|
11
|
+
|
|
12
|
+
use chrono::{DateTime, Utc};
|
|
13
|
+
use magnus::{function, method, prelude::*, Error, RArray, RHash, RModule, RString, Ruby};
|
|
14
|
+
use microsandbox::logs::{
|
|
15
|
+
LogCursor, LogEntry, LogOptions, LogSource, LogStreamOptions, LogStreamStart,
|
|
16
|
+
};
|
|
17
|
+
use microsandbox::sandbox::{
|
|
18
|
+
FsEntry, FsEntryKind, FsMetadata, PullPolicy, RlimitResource, SandboxFilter, SandboxHandle,
|
|
19
|
+
SandboxMetrics, SandboxStatus, SandboxStopResult, SecurityProfile,
|
|
20
|
+
};
|
|
21
|
+
use microsandbox::LogLevel;
|
|
22
|
+
use microsandbox_network::policy::NetworkPolicy;
|
|
23
|
+
|
|
24
|
+
use crate::conv;
|
|
25
|
+
use crate::error;
|
|
26
|
+
use crate::exec::ExecHandle;
|
|
27
|
+
use crate::runtime::{block_on, ruby};
|
|
28
|
+
use crate::stream::{LogStream, MetricsStream};
|
|
29
|
+
|
|
30
|
+
#[magnus::wrap(class = "Microsandbox::Native::Sandbox", free_immediately, size)]
|
|
31
|
+
pub struct Sandbox {
|
|
32
|
+
inner: microsandbox::Sandbox,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
impl Sandbox {
|
|
36
|
+
fn from_inner(inner: microsandbox::Sandbox) -> Self {
|
|
37
|
+
Self { inner }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
//----------------------------------------------------------------------
|
|
41
|
+
// Lifecycle (singleton methods)
|
|
42
|
+
//----------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
/// Create and boot a sandbox. `opts` is a string-keyed options Hash.
|
|
45
|
+
fn create(name: String, opts: RHash) -> Result<Sandbox, Error> {
|
|
46
|
+
let mut b = microsandbox::Sandbox::builder(name);
|
|
47
|
+
|
|
48
|
+
if let Some(v) = conv::opt_string(opts, "image")? {
|
|
49
|
+
b = b.image(v);
|
|
50
|
+
}
|
|
51
|
+
if let Some(v) = conv::opt_string(opts, "from_snapshot")? {
|
|
52
|
+
b = b.from_snapshot(v);
|
|
53
|
+
}
|
|
54
|
+
if let Some(v) = conv::opt_u8(opts, "cpus")? {
|
|
55
|
+
b = b.cpus(v);
|
|
56
|
+
}
|
|
57
|
+
if let Some(v) = conv::opt_u32(opts, "memory")? {
|
|
58
|
+
b = b.memory(v);
|
|
59
|
+
}
|
|
60
|
+
if let Some(v) = conv::opt_string(opts, "workdir")? {
|
|
61
|
+
b = b.workdir(v);
|
|
62
|
+
}
|
|
63
|
+
if let Some(v) = conv::opt_string(opts, "shell")? {
|
|
64
|
+
b = b.shell(v);
|
|
65
|
+
}
|
|
66
|
+
if let Some(v) = conv::opt_string(opts, "user")? {
|
|
67
|
+
b = b.user(v);
|
|
68
|
+
}
|
|
69
|
+
if let Some(v) = conv::opt_string(opts, "hostname")? {
|
|
70
|
+
b = b.hostname(v);
|
|
71
|
+
}
|
|
72
|
+
for (k, v) in conv::opt_string_map(opts, "env")? {
|
|
73
|
+
b = b.env(k, v);
|
|
74
|
+
}
|
|
75
|
+
for (k, v) in conv::opt_string_map(opts, "labels")? {
|
|
76
|
+
b = b.label(k, v);
|
|
77
|
+
}
|
|
78
|
+
for (k, v) in conv::opt_string_map(opts, "scripts")? {
|
|
79
|
+
b = b.script(k, v);
|
|
80
|
+
}
|
|
81
|
+
let entrypoint = conv::opt_string_vec(opts, "entrypoint")?;
|
|
82
|
+
if !entrypoint.is_empty() {
|
|
83
|
+
b = b.entrypoint(entrypoint);
|
|
84
|
+
}
|
|
85
|
+
for (host, guest) in conv::opt_port_map(opts, "ports")? {
|
|
86
|
+
b = b.port(host, guest);
|
|
87
|
+
}
|
|
88
|
+
// volumes: normalized by the Ruby layer to [guest, kind, source] triples.
|
|
89
|
+
for spec in conv::opt::<Vec<Vec<String>>>(opts, "volumes")?.unwrap_or_default() {
|
|
90
|
+
if spec.len() != 3 {
|
|
91
|
+
return Err(error::base_error("invalid volume mount spec"));
|
|
92
|
+
}
|
|
93
|
+
let (guest, kind, source) = (spec[0].clone(), spec[1].clone(), spec[2].clone());
|
|
94
|
+
match kind.as_str() {
|
|
95
|
+
"bind" | "named" => {}
|
|
96
|
+
other => {
|
|
97
|
+
return Err(error::base_error(format!(
|
|
98
|
+
"unknown volume mount kind {other:?} (expected \"bind\" or \"named\")"
|
|
99
|
+
)))
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
b = b.volume(guest, move |m| {
|
|
103
|
+
if kind == "named" {
|
|
104
|
+
m.named(source)
|
|
105
|
+
} else {
|
|
106
|
+
m.bind(source)
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
if let Some(net) = conv::opt_string(opts, "network")? {
|
|
111
|
+
match net.as_str() {
|
|
112
|
+
"none" | "disabled" | "disable" | "airgapped" => b = b.disable_network(),
|
|
113
|
+
// Default policy is public-only, so no builder call is needed.
|
|
114
|
+
"public" | "public_only" | "default" => {}
|
|
115
|
+
"all" | "allow_all" => b = b.network(|n| n.policy(NetworkPolicy::allow_all())),
|
|
116
|
+
"non_local" | "nonlocal" => b = b.network(|n| n.policy(NetworkPolicy::non_local())),
|
|
117
|
+
other => {
|
|
118
|
+
return Err(error::base_error(format!(
|
|
119
|
+
"unknown network mode {other:?} (expected one of \
|
|
120
|
+
public_only/none/allow_all/non_local)"
|
|
121
|
+
)))
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if let Some(level) = conv::opt_string(opts, "log_level")? {
|
|
126
|
+
b = b.log_level(log_level_from_str(&level)?);
|
|
127
|
+
}
|
|
128
|
+
if conv::opt_bool(opts, "quiet_logs")? {
|
|
129
|
+
b = b.quiet_logs();
|
|
130
|
+
}
|
|
131
|
+
if let Some(profile) = conv::opt_string(opts, "security")? {
|
|
132
|
+
b = b.security(security_profile_from_str(&profile)?);
|
|
133
|
+
}
|
|
134
|
+
if let Some(policy) = conv::opt_string(opts, "pull_policy")? {
|
|
135
|
+
b = b.pull_policy(pull_policy_from_str(&policy)?);
|
|
136
|
+
}
|
|
137
|
+
if let Some(mib) = conv::opt_u32(opts, "oci_upper_size")? {
|
|
138
|
+
b = b.oci_upper_size(mib);
|
|
139
|
+
}
|
|
140
|
+
if let Some(secs) = conv::opt::<u64>(opts, "max_duration")? {
|
|
141
|
+
b = b.max_duration(secs);
|
|
142
|
+
}
|
|
143
|
+
if let Some(secs) = conv::opt::<u64>(opts, "idle_timeout")? {
|
|
144
|
+
b = b.idle_timeout(secs);
|
|
145
|
+
}
|
|
146
|
+
for (host, guest) in conv::opt_port_map(opts, "ports_udp")? {
|
|
147
|
+
b = b.port_udp(host, guest);
|
|
148
|
+
}
|
|
149
|
+
for (resource, soft, hard) in parse_rlimits(opts)? {
|
|
150
|
+
b = b.rlimit_range(resource, soft, hard);
|
|
151
|
+
}
|
|
152
|
+
// secrets: normalized by the Ruby layer to [env_var, value, allowed_host]
|
|
153
|
+
// triples. Uses the placeholder-based `secret_env` shorthand, which also
|
|
154
|
+
// auto-enables TLS interception (required for value substitution).
|
|
155
|
+
for spec in conv::opt::<Vec<Vec<String>>>(opts, "secrets")?.unwrap_or_default() {
|
|
156
|
+
if spec.len() != 3 {
|
|
157
|
+
return Err(error::base_error(
|
|
158
|
+
"invalid secret spec (expected [env, value, host])",
|
|
159
|
+
));
|
|
160
|
+
}
|
|
161
|
+
b = b.secret_env(spec[0].clone(), spec[1].clone(), spec[2].clone());
|
|
162
|
+
}
|
|
163
|
+
if conv::opt_bool(opts, "detached")? {
|
|
164
|
+
b = b.detached(true);
|
|
165
|
+
}
|
|
166
|
+
if let Some(secs) = conv::opt_f64(opts, "replace_with_timeout")? {
|
|
167
|
+
b = b.replace_with_timeout(Duration::from_secs_f64(secs));
|
|
168
|
+
} else if conv::opt_bool(opts, "replace")? {
|
|
169
|
+
b = b.replace();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let inner = block_on(b.create()).map_err(error::to_ruby)?;
|
|
173
|
+
Ok(Sandbox::from_inner(inner))
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/// Restart a previously-defined sandbox by name.
|
|
177
|
+
fn start(name: String, opts: RHash) -> Result<Sandbox, Error> {
|
|
178
|
+
let detached = conv::opt_bool(opts, "detached")?;
|
|
179
|
+
let inner = if detached {
|
|
180
|
+
block_on(microsandbox::Sandbox::start_detached(&name)).map_err(error::to_ruby)?
|
|
181
|
+
} else {
|
|
182
|
+
block_on(microsandbox::Sandbox::start(&name)).map_err(error::to_ruby)?
|
|
183
|
+
};
|
|
184
|
+
Ok(Sandbox::from_inner(inner))
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/// Lightweight metadata for a sandbox by name (running or not).
|
|
188
|
+
fn get(name: String) -> Result<RHash, Error> {
|
|
189
|
+
let handle = block_on(microsandbox::Sandbox::get(&name)).map_err(error::to_ruby)?;
|
|
190
|
+
Ok(handle_to_hash(&handle))
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/// All sandboxes as metadata hashes.
|
|
194
|
+
fn list() -> Result<RArray, Error> {
|
|
195
|
+
let handles = block_on(microsandbox::Sandbox::list()).map_err(error::to_ruby)?;
|
|
196
|
+
rhash_array(handles.iter().map(handle_to_hash))
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/// Sandboxes filtered by required `key=value` labels (AND-matched). `opts`
|
|
200
|
+
/// carries a string→string `labels` map.
|
|
201
|
+
fn list_with(opts: RHash) -> Result<RArray, Error> {
|
|
202
|
+
let mut filter = SandboxFilter::new();
|
|
203
|
+
for (k, v) in conv::opt_string_map(opts, "labels")? {
|
|
204
|
+
filter = filter.label(k, v);
|
|
205
|
+
}
|
|
206
|
+
let handles = block_on(microsandbox::Sandbox::list_with(filter)).map_err(error::to_ruby)?;
|
|
207
|
+
rhash_array(handles.iter().map(handle_to_hash))
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/// Remove a (stopped) sandbox by name.
|
|
211
|
+
fn remove(name: String) -> Result<(), Error> {
|
|
212
|
+
block_on(microsandbox::Sandbox::remove(&name)).map_err(error::to_ruby)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
//----------------------------------------------------------------------
|
|
216
|
+
// Instance methods
|
|
217
|
+
//----------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
fn name(&self) -> String {
|
|
220
|
+
self.inner.name().to_string()
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/// Run a command (no shell). `args` is an Array of strings; `opts` is a
|
|
224
|
+
/// string-keyed Hash (cwd, user, env, timeout, tty, stdin).
|
|
225
|
+
fn exec(&self, cmd: String, args: Vec<String>, opts: RHash) -> Result<RHash, Error> {
|
|
226
|
+
let parsed = ExecOpts::parse(args, opts)?;
|
|
227
|
+
let output = block_on(self.inner.exec_with(cmd, move |b| parsed.apply(b)))
|
|
228
|
+
.map_err(error::to_ruby)?;
|
|
229
|
+
exec_output_to_hash(output)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/// Run a shell script (pipes/redirects allowed).
|
|
233
|
+
fn shell(&self, script: String, opts: RHash) -> Result<RHash, Error> {
|
|
234
|
+
let parsed = ExecOpts::parse(Vec::new(), opts)?;
|
|
235
|
+
let output = block_on(self.inner.shell_with(script, move |b| parsed.apply(b)))
|
|
236
|
+
.map_err(error::to_ruby)?;
|
|
237
|
+
exec_output_to_hash(output)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/// Streaming command execution. Returns an ExecHandle to pull events from.
|
|
241
|
+
fn exec_stream(
|
|
242
|
+
&self,
|
|
243
|
+
cmd: String,
|
|
244
|
+
args: Vec<String>,
|
|
245
|
+
opts: RHash,
|
|
246
|
+
) -> Result<ExecHandle, Error> {
|
|
247
|
+
let parsed = ExecOpts::parse(args, opts)?;
|
|
248
|
+
let handle = block_on(self.inner.exec_stream_with(cmd, move |b| parsed.apply(b)))
|
|
249
|
+
.map_err(error::to_ruby)?;
|
|
250
|
+
Ok(ExecHandle::from_core(handle))
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/// Streaming shell execution.
|
|
254
|
+
fn shell_stream(&self, script: String, opts: RHash) -> Result<ExecHandle, Error> {
|
|
255
|
+
let parsed = ExecOpts::parse(Vec::new(), opts)?;
|
|
256
|
+
let handle = block_on(
|
|
257
|
+
self.inner
|
|
258
|
+
.shell_stream_with(script, move |b| parsed.apply(b)),
|
|
259
|
+
)
|
|
260
|
+
.map_err(error::to_ruby)?;
|
|
261
|
+
Ok(ExecHandle::from_core(handle))
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/// Graceful stop (+ wait). `timeout` is optional seconds.
|
|
265
|
+
fn stop(&self, timeout: Option<f64>) -> Result<(), Error> {
|
|
266
|
+
match timeout {
|
|
267
|
+
Some(secs) => block_on(self.inner.stop_with_timeout(Duration::from_secs_f64(secs))),
|
|
268
|
+
None => block_on(self.inner.stop()),
|
|
269
|
+
}
|
|
270
|
+
.map_err(error::to_ruby)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/// Force kill (SIGKILL). `timeout` is optional seconds.
|
|
274
|
+
fn kill(&self, timeout: Option<f64>) -> Result<(), Error> {
|
|
275
|
+
match timeout {
|
|
276
|
+
Some(secs) => block_on(self.inner.kill_with_timeout(Duration::from_secs_f64(secs))),
|
|
277
|
+
None => block_on(self.inner.kill()),
|
|
278
|
+
}
|
|
279
|
+
.map_err(error::to_ruby)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/// Send the graceful-shutdown request and return without waiting.
|
|
283
|
+
fn request_stop(&self) -> Result<(), Error> {
|
|
284
|
+
block_on(self.inner.request_stop()).map_err(error::to_ruby)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/// Send the force-kill request and return without waiting.
|
|
288
|
+
fn request_kill(&self) -> Result<(), Error> {
|
|
289
|
+
block_on(self.inner.request_kill()).map_err(error::to_ruby)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/// Send the drain request and return without waiting.
|
|
293
|
+
fn request_drain(&self) -> Result<(), Error> {
|
|
294
|
+
block_on(self.inner.request_drain()).map_err(error::to_ruby)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/// Block until the sandbox is observed in a terminal state; returns a
|
|
298
|
+
/// stop-result Hash (name, status, exit_code, signal, observed_at_ms, source).
|
|
299
|
+
fn wait_until_stopped(&self) -> Result<RHash, Error> {
|
|
300
|
+
let result = block_on(self.inner.wait_until_stopped()).map_err(error::to_ruby)?;
|
|
301
|
+
Ok(stop_result_to_hash(&result))
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/// Whether this handle owns the sandbox process lifecycle (a synchronous,
|
|
305
|
+
/// local predicate — no runtime round-trip).
|
|
306
|
+
fn owns_lifecycle(&self) -> bool {
|
|
307
|
+
self.inner.owns_lifecycle()
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/// Disarm the SIGTERM safety net so the sandbox keeps running after this
|
|
311
|
+
/// handle is dropped. The core `detach` consumes the value; the core
|
|
312
|
+
/// `Sandbox` is `Clone` (Arc-backed, sharing the same process handle), so
|
|
313
|
+
/// detaching a clone disarms the shared handle just the same.
|
|
314
|
+
fn detach(&self) -> Result<(), Error> {
|
|
315
|
+
let inner = self.inner.clone();
|
|
316
|
+
block_on(inner.detach());
|
|
317
|
+
Ok(())
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/// Latest metrics snapshot as a Hash.
|
|
321
|
+
fn metrics(&self) -> Result<RHash, Error> {
|
|
322
|
+
let m = block_on(self.inner.metrics()).map_err(error::to_ruby)?;
|
|
323
|
+
Ok(metrics_to_hash(&m))
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/// Read captured logs as an Array of Hashes. `opts`: tail, since_ms,
|
|
327
|
+
/// until_ms, sources (Array of "stdout"/"stderr"/"output"/"system"/"all").
|
|
328
|
+
fn logs(&self, opts: RHash) -> Result<RArray, Error> {
|
|
329
|
+
let log_opts = parse_log_options(opts)?;
|
|
330
|
+
let entries = block_on(self.inner.logs(&log_opts)).map_err(error::to_ruby)?;
|
|
331
|
+
rhash_array(entries.iter().map(log_entry_to_hash))
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/// Stream metrics snapshots at `interval` seconds. Returns a MetricsStream
|
|
335
|
+
/// to pull snapshots from.
|
|
336
|
+
fn metrics_stream(&self, interval: f64) -> MetricsStream {
|
|
337
|
+
let dur = Duration::from_secs_f64(if interval <= 0.0 { 1.0 } else { interval });
|
|
338
|
+
// `metrics_stream` is synchronous but builds a `tokio::time::interval`,
|
|
339
|
+
// which panics ("no reactor running") unless constructed inside the
|
|
340
|
+
// runtime context — so build it under `block_on`. (`log_stream` is async
|
|
341
|
+
// and already runs inside `block_on`, so it needs no such wrapper.)
|
|
342
|
+
let stream = block_on(async { self.inner.metrics_stream(dur) });
|
|
343
|
+
MetricsStream::from_stream(stream)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/// Stream captured logs as they appear. `opts`: sources, since_ms,
|
|
347
|
+
/// from_cursor, until_ms, follow. Returns a LogStream.
|
|
348
|
+
fn log_stream(&self, opts: RHash) -> Result<LogStream, Error> {
|
|
349
|
+
let log_opts = parse_log_stream_options(opts)?;
|
|
350
|
+
let stream = block_on(self.inner.log_stream(&log_opts)).map_err(error::to_ruby)?;
|
|
351
|
+
Ok(LogStream::from_stream(stream))
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
//----------------------------------------------------------------------
|
|
355
|
+
// Filesystem (folded in; mirror SandboxFsOps)
|
|
356
|
+
//----------------------------------------------------------------------
|
|
357
|
+
|
|
358
|
+
fn fs_read(&self, path: String) -> Result<RString, Error> {
|
|
359
|
+
let fs = self.inner.fs();
|
|
360
|
+
let bytes = block_on(fs.read(&path)).map_err(error::to_ruby)?;
|
|
361
|
+
Ok(ruby().str_from_slice(bytes.as_ref()))
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
fn fs_read_text(&self, path: String) -> Result<String, Error> {
|
|
365
|
+
let fs = self.inner.fs();
|
|
366
|
+
block_on(fs.read_to_string(&path)).map_err(error::to_ruby)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
fn fs_write(&self, path: String, data: RString) -> Result<(), Error> {
|
|
370
|
+
// Copy out of the Ruby string while we still hold the GVL: the buffer
|
|
371
|
+
// could be moved/freed by GC.compact once block_on releases it.
|
|
372
|
+
let bytes = unsafe { data.as_slice() }.to_vec();
|
|
373
|
+
let fs = self.inner.fs();
|
|
374
|
+
block_on(fs.write(&path, &bytes)).map_err(error::to_ruby)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
fn fs_list(&self, path: String) -> Result<RArray, Error> {
|
|
378
|
+
let fs = self.inner.fs();
|
|
379
|
+
let entries = block_on(fs.list(&path)).map_err(error::to_ruby)?;
|
|
380
|
+
rhash_array(entries.iter().map(fs_entry_to_hash))
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
fn fs_mkdir(&self, path: String) -> Result<(), Error> {
|
|
384
|
+
let fs = self.inner.fs();
|
|
385
|
+
block_on(fs.mkdir(&path)).map_err(error::to_ruby)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
fn fs_remove(&self, path: String) -> Result<(), Error> {
|
|
389
|
+
let fs = self.inner.fs();
|
|
390
|
+
block_on(fs.remove(&path)).map_err(error::to_ruby)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
fn fs_remove_dir(&self, path: String) -> Result<(), Error> {
|
|
394
|
+
let fs = self.inner.fs();
|
|
395
|
+
block_on(fs.remove_dir(&path)).map_err(error::to_ruby)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
fn fs_copy(&self, src: String, dst: String) -> Result<(), Error> {
|
|
399
|
+
let fs = self.inner.fs();
|
|
400
|
+
block_on(fs.copy(&src, &dst)).map_err(error::to_ruby)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
fn fs_rename(&self, src: String, dst: String) -> Result<(), Error> {
|
|
404
|
+
let fs = self.inner.fs();
|
|
405
|
+
block_on(fs.rename(&src, &dst)).map_err(error::to_ruby)
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
fn fs_exists(&self, path: String) -> Result<bool, Error> {
|
|
409
|
+
let fs = self.inner.fs();
|
|
410
|
+
block_on(fs.exists(&path)).map_err(error::to_ruby)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
fn fs_stat(&self, path: String) -> Result<RHash, Error> {
|
|
414
|
+
let fs = self.inner.fs();
|
|
415
|
+
let meta = block_on(fs.stat(&path)).map_err(error::to_ruby)?;
|
|
416
|
+
Ok(fs_metadata_to_hash(&meta))
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
fn fs_copy_from_host(&self, host_path: String, guest_path: String) -> Result<(), Error> {
|
|
420
|
+
let fs = self.inner.fs();
|
|
421
|
+
block_on(fs.copy_from_host(&host_path, &guest_path)).map_err(error::to_ruby)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
fn fs_copy_to_host(&self, guest_path: String, host_path: String) -> Result<(), Error> {
|
|
425
|
+
let fs = self.inner.fs();
|
|
426
|
+
block_on(fs.copy_to_host(&guest_path, &host_path)).map_err(error::to_ruby)
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
//--------------------------------------------------------------------------------------------------
|
|
431
|
+
// Enum / rlimit parsing (string conventions mirror the Python/Go SDKs)
|
|
432
|
+
//--------------------------------------------------------------------------------------------------
|
|
433
|
+
|
|
434
|
+
fn log_level_from_str(s: &str) -> Result<LogLevel, Error> {
|
|
435
|
+
match s {
|
|
436
|
+
"error" => Ok(LogLevel::Error),
|
|
437
|
+
"warn" => Ok(LogLevel::Warn),
|
|
438
|
+
"info" => Ok(LogLevel::Info),
|
|
439
|
+
"debug" => Ok(LogLevel::Debug),
|
|
440
|
+
"trace" => Ok(LogLevel::Trace),
|
|
441
|
+
other => Err(error::base_error(format!(
|
|
442
|
+
"unknown log level {other:?} (expected error/warn/info/debug/trace)"
|
|
443
|
+
))),
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
fn pull_policy_from_str(s: &str) -> Result<PullPolicy, Error> {
|
|
448
|
+
match s {
|
|
449
|
+
"always" => Ok(PullPolicy::Always),
|
|
450
|
+
"if-missing" | "if_missing" => Ok(PullPolicy::IfMissing),
|
|
451
|
+
"never" => Ok(PullPolicy::Never),
|
|
452
|
+
other => Err(error::base_error(format!(
|
|
453
|
+
"unknown pull policy {other:?} (expected always/if-missing/never)"
|
|
454
|
+
))),
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
fn security_profile_from_str(s: &str) -> Result<SecurityProfile, Error> {
|
|
459
|
+
match s {
|
|
460
|
+
"default" => Ok(SecurityProfile::Default),
|
|
461
|
+
"restricted" => Ok(SecurityProfile::Restricted),
|
|
462
|
+
other => Err(error::base_error(format!(
|
|
463
|
+
"unknown security profile {other:?} (expected default/restricted)"
|
|
464
|
+
))),
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
fn rlimit_resource_from_str(s: &str) -> Result<RlimitResource, Error> {
|
|
469
|
+
use RlimitResource::*;
|
|
470
|
+
Ok(match s {
|
|
471
|
+
"cpu" => Cpu,
|
|
472
|
+
"fsize" => Fsize,
|
|
473
|
+
"data" => Data,
|
|
474
|
+
"stack" => Stack,
|
|
475
|
+
"core" => Core,
|
|
476
|
+
"rss" => Rss,
|
|
477
|
+
"nproc" => Nproc,
|
|
478
|
+
"nofile" => Nofile,
|
|
479
|
+
"memlock" => Memlock,
|
|
480
|
+
"as" => As,
|
|
481
|
+
"locks" => Locks,
|
|
482
|
+
"sigpending" => Sigpending,
|
|
483
|
+
"msgqueue" => Msgqueue,
|
|
484
|
+
"nice" => Nice,
|
|
485
|
+
"rtprio" => Rtprio,
|
|
486
|
+
"rttime" => Rttime,
|
|
487
|
+
other => {
|
|
488
|
+
return Err(error::base_error(format!(
|
|
489
|
+
"unknown rlimit resource {other:?}"
|
|
490
|
+
)))
|
|
491
|
+
}
|
|
492
|
+
})
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/// Parse the `rlimits` option — normalized by the Ruby layer to
|
|
496
|
+
/// `[[resource, soft, hard], …]` triples — into core (resource, soft, hard).
|
|
497
|
+
fn parse_rlimits(opts: RHash) -> Result<Vec<(RlimitResource, u64, u64)>, Error> {
|
|
498
|
+
let mut out = Vec::new();
|
|
499
|
+
for triple in conv::opt::<Vec<(String, u64, u64)>>(opts, "rlimits")?.unwrap_or_default() {
|
|
500
|
+
out.push((rlimit_resource_from_str(&triple.0)?, triple.1, triple.2));
|
|
501
|
+
}
|
|
502
|
+
Ok(out)
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
//--------------------------------------------------------------------------------------------------
|
|
506
|
+
// Exec option parsing
|
|
507
|
+
//--------------------------------------------------------------------------------------------------
|
|
508
|
+
|
|
509
|
+
struct ExecOpts {
|
|
510
|
+
args: Vec<String>,
|
|
511
|
+
cwd: Option<String>,
|
|
512
|
+
user: Option<String>,
|
|
513
|
+
env: Vec<(String, String)>,
|
|
514
|
+
timeout: Option<Duration>,
|
|
515
|
+
tty: bool,
|
|
516
|
+
stdin: Option<Vec<u8>>,
|
|
517
|
+
rlimits: Vec<(RlimitResource, u64, u64)>,
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
impl ExecOpts {
|
|
521
|
+
fn parse(args: Vec<String>, opts: RHash) -> Result<Self, Error> {
|
|
522
|
+
let stdin = conv::opt::<RString>(opts, "stdin")?.map(|s| unsafe { s.as_slice() }.to_vec());
|
|
523
|
+
Ok(Self {
|
|
524
|
+
args,
|
|
525
|
+
cwd: conv::opt_string(opts, "cwd")?,
|
|
526
|
+
user: conv::opt_string(opts, "user")?,
|
|
527
|
+
env: conv::opt_string_map(opts, "env")?,
|
|
528
|
+
timeout: conv::opt_f64(opts, "timeout")?.map(Duration::from_secs_f64),
|
|
529
|
+
tty: conv::opt_bool(opts, "tty")?,
|
|
530
|
+
stdin,
|
|
531
|
+
rlimits: parse_rlimits(opts)?,
|
|
532
|
+
})
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
fn apply(
|
|
536
|
+
self,
|
|
537
|
+
mut b: microsandbox::sandbox::exec::ExecOptionsBuilder,
|
|
538
|
+
) -> microsandbox::sandbox::exec::ExecOptionsBuilder {
|
|
539
|
+
if !self.args.is_empty() {
|
|
540
|
+
b = b.args(self.args);
|
|
541
|
+
}
|
|
542
|
+
if let Some(cwd) = self.cwd {
|
|
543
|
+
b = b.cwd(cwd);
|
|
544
|
+
}
|
|
545
|
+
if let Some(user) = self.user {
|
|
546
|
+
b = b.user(user);
|
|
547
|
+
}
|
|
548
|
+
for (k, v) in self.env {
|
|
549
|
+
b = b.env(k, v);
|
|
550
|
+
}
|
|
551
|
+
if let Some(timeout) = self.timeout {
|
|
552
|
+
b = b.timeout(timeout);
|
|
553
|
+
}
|
|
554
|
+
if self.tty {
|
|
555
|
+
b = b.tty(true);
|
|
556
|
+
}
|
|
557
|
+
if let Some(stdin) = self.stdin {
|
|
558
|
+
b = b.stdin_bytes(stdin);
|
|
559
|
+
}
|
|
560
|
+
for (resource, soft, hard) in self.rlimits {
|
|
561
|
+
b = b.rlimit_range(resource, soft, hard);
|
|
562
|
+
}
|
|
563
|
+
b
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
//--------------------------------------------------------------------------------------------------
|
|
568
|
+
// Value conversions
|
|
569
|
+
//--------------------------------------------------------------------------------------------------
|
|
570
|
+
|
|
571
|
+
/// Collect an iterator of `RHash` into a Ruby `Array`.
|
|
572
|
+
fn rhash_array<I: IntoIterator<Item = RHash>>(items: I) -> Result<RArray, Error> {
|
|
573
|
+
let arr = ruby().ary_new();
|
|
574
|
+
for item in items {
|
|
575
|
+
arr.push(item)?;
|
|
576
|
+
}
|
|
577
|
+
Ok(arr)
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
pub(crate) fn exec_output_to_hash(output: microsandbox::ExecOutput) -> Result<RHash, Error> {
|
|
581
|
+
let ruby = ruby();
|
|
582
|
+
let hash = ruby.hash_new();
|
|
583
|
+
let status = output.status();
|
|
584
|
+
hash.aset("exit_code", status.code)?;
|
|
585
|
+
hash.aset("success", status.success)?;
|
|
586
|
+
hash.aset(
|
|
587
|
+
"stdout",
|
|
588
|
+
ruby.str_from_slice(output.stdout_bytes().as_ref()),
|
|
589
|
+
)?;
|
|
590
|
+
hash.aset(
|
|
591
|
+
"stderr",
|
|
592
|
+
ruby.str_from_slice(output.stderr_bytes().as_ref()),
|
|
593
|
+
)?;
|
|
594
|
+
Ok(hash)
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
fn fs_entry_kind_str(kind: FsEntryKind) -> &'static str {
|
|
598
|
+
match kind {
|
|
599
|
+
FsEntryKind::File => "file",
|
|
600
|
+
FsEntryKind::Directory => "directory",
|
|
601
|
+
FsEntryKind::Symlink => "symlink",
|
|
602
|
+
FsEntryKind::Other => "other",
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
fn fs_entry_to_hash(entry: &FsEntry) -> RHash {
|
|
607
|
+
let hash = ruby().hash_new();
|
|
608
|
+
let _ = hash.aset("path", entry.path.clone());
|
|
609
|
+
let _ = hash.aset("type", fs_entry_kind_str(entry.kind));
|
|
610
|
+
let _ = hash.aset("size", entry.size);
|
|
611
|
+
let _ = hash.aset("mode", entry.mode);
|
|
612
|
+
let _ = hash.aset(
|
|
613
|
+
"modified_ms",
|
|
614
|
+
entry.modified.map(|dt| dt.timestamp_millis()),
|
|
615
|
+
);
|
|
616
|
+
hash
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
fn fs_metadata_to_hash(meta: &FsMetadata) -> RHash {
|
|
620
|
+
let hash = ruby().hash_new();
|
|
621
|
+
let _ = hash.aset("type", fs_entry_kind_str(meta.kind));
|
|
622
|
+
let _ = hash.aset("size", meta.size);
|
|
623
|
+
let _ = hash.aset("mode", meta.mode);
|
|
624
|
+
let _ = hash.aset("readonly", meta.readonly);
|
|
625
|
+
let _ = hash.aset("modified_ms", meta.modified.map(|dt| dt.timestamp_millis()));
|
|
626
|
+
let _ = hash.aset("created_ms", meta.created.map(|dt| dt.timestamp_millis()));
|
|
627
|
+
hash
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
pub(crate) fn metrics_to_hash(m: &SandboxMetrics) -> RHash {
|
|
631
|
+
let hash = ruby().hash_new();
|
|
632
|
+
let _ = hash.aset("cpu_percent", m.cpu_percent as f64);
|
|
633
|
+
let _ = hash.aset("vcpu_time_ns", m.vcpu_time_ns);
|
|
634
|
+
let _ = hash.aset("memory_bytes", m.memory_bytes);
|
|
635
|
+
let _ = hash.aset("memory_available_bytes", m.memory_available_bytes);
|
|
636
|
+
let _ = hash.aset("memory_host_resident_bytes", m.memory_host_resident_bytes);
|
|
637
|
+
let _ = hash.aset("memory_limit_bytes", m.memory_limit_bytes);
|
|
638
|
+
let _ = hash.aset("disk_read_bytes", m.disk_read_bytes);
|
|
639
|
+
let _ = hash.aset("disk_write_bytes", m.disk_write_bytes);
|
|
640
|
+
let _ = hash.aset("net_rx_bytes", m.net_rx_bytes);
|
|
641
|
+
let _ = hash.aset("net_tx_bytes", m.net_tx_bytes);
|
|
642
|
+
let _ = hash.aset("uptime_secs", m.uptime.as_secs_f64());
|
|
643
|
+
let _ = hash.aset("timestamp_ms", m.timestamp.timestamp_millis());
|
|
644
|
+
hash
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
fn sandbox_status_str(status: SandboxStatus) -> &'static str {
|
|
648
|
+
match status {
|
|
649
|
+
SandboxStatus::Running => "running",
|
|
650
|
+
SandboxStatus::Draining => "draining",
|
|
651
|
+
SandboxStatus::Paused => "paused",
|
|
652
|
+
SandboxStatus::Stopped => "stopped",
|
|
653
|
+
SandboxStatus::Crashed => "crashed",
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
fn stop_result_to_hash(result: &SandboxStopResult) -> RHash {
|
|
658
|
+
let hash = ruby().hash_new();
|
|
659
|
+
let _ = hash.aset("name", result.name.clone());
|
|
660
|
+
let _ = hash.aset("status", sandbox_status_str(result.status));
|
|
661
|
+
let _ = hash.aset("exit_code", result.exit_code);
|
|
662
|
+
let _ = hash.aset("signal", result.signal);
|
|
663
|
+
let _ = hash.aset("observed_at_ms", result.observed_at.timestamp_millis());
|
|
664
|
+
let _ = hash.aset("source", result.source.clone());
|
|
665
|
+
hash
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
fn handle_to_hash(handle: &SandboxHandle) -> RHash {
|
|
669
|
+
let hash = ruby().hash_new();
|
|
670
|
+
let _ = hash.aset("name", handle.name().to_string());
|
|
671
|
+
let _ = hash.aset("status", sandbox_status_str(handle.status()));
|
|
672
|
+
let _ = hash.aset(
|
|
673
|
+
"created_at_ms",
|
|
674
|
+
handle.created_at().map(|dt| dt.timestamp_millis()),
|
|
675
|
+
);
|
|
676
|
+
let _ = hash.aset(
|
|
677
|
+
"updated_at_ms",
|
|
678
|
+
handle.updated_at().map(|dt| dt.timestamp_millis()),
|
|
679
|
+
);
|
|
680
|
+
hash
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
//--------------------------------------------------------------------------------------------------
|
|
684
|
+
// Log option parsing
|
|
685
|
+
//--------------------------------------------------------------------------------------------------
|
|
686
|
+
|
|
687
|
+
fn ms_to_datetime(ms: f64) -> Option<DateTime<Utc>> {
|
|
688
|
+
let secs = (ms / 1000.0).trunc() as i64;
|
|
689
|
+
let nsecs = ((ms - secs as f64 * 1000.0) * 1_000_000.0).round() as u32;
|
|
690
|
+
DateTime::from_timestamp(secs, nsecs)
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
fn parse_log_sources(opts: RHash) -> Result<Vec<LogSource>, Error> {
|
|
694
|
+
let mut sources = Vec::new();
|
|
695
|
+
for s in conv::opt_string_vec(opts, "sources")? {
|
|
696
|
+
match s.as_str() {
|
|
697
|
+
"stdout" => sources.push(LogSource::Stdout),
|
|
698
|
+
"stderr" => sources.push(LogSource::Stderr),
|
|
699
|
+
"output" => sources.push(LogSource::Output),
|
|
700
|
+
"system" => sources.push(LogSource::System),
|
|
701
|
+
"all" => {
|
|
702
|
+
sources = vec![
|
|
703
|
+
LogSource::Stdout,
|
|
704
|
+
LogSource::Stderr,
|
|
705
|
+
LogSource::Output,
|
|
706
|
+
LogSource::System,
|
|
707
|
+
];
|
|
708
|
+
}
|
|
709
|
+
other => return Err(error::base_error(format!("unknown log source {other:?}"))),
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
Ok(sources)
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
fn parse_log_options(opts: RHash) -> Result<LogOptions, Error> {
|
|
716
|
+
Ok(LogOptions {
|
|
717
|
+
tail: conv::opt::<usize>(opts, "tail")?,
|
|
718
|
+
since: conv::opt_f64(opts, "since_ms")?.and_then(ms_to_datetime),
|
|
719
|
+
until: conv::opt_f64(opts, "until_ms")?.and_then(ms_to_datetime),
|
|
720
|
+
sources: parse_log_sources(opts)?,
|
|
721
|
+
})
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
fn parse_log_stream_options(opts: RHash) -> Result<LogStreamOptions, Error> {
|
|
725
|
+
// `from_cursor` takes precedence over `since_ms` (the two are mutually
|
|
726
|
+
// exclusive in the official SDKs); absent both, start at the beginning.
|
|
727
|
+
let start = if let Some(cursor) = conv::opt_string(opts, "from_cursor")? {
|
|
728
|
+
let parsed: LogCursor = cursor
|
|
729
|
+
.parse()
|
|
730
|
+
.map_err(|_| error::base_error(format!("invalid log cursor {cursor:?}")))?;
|
|
731
|
+
LogStreamStart::From(parsed)
|
|
732
|
+
} else if let Some(since) = conv::opt_f64(opts, "since_ms")?.and_then(ms_to_datetime) {
|
|
733
|
+
LogStreamStart::Since(since)
|
|
734
|
+
} else {
|
|
735
|
+
LogStreamStart::Beginning
|
|
736
|
+
};
|
|
737
|
+
Ok(LogStreamOptions {
|
|
738
|
+
sources: parse_log_sources(opts)?,
|
|
739
|
+
start,
|
|
740
|
+
until: conv::opt_f64(opts, "until_ms")?.and_then(ms_to_datetime),
|
|
741
|
+
follow: conv::opt_bool(opts, "follow")?,
|
|
742
|
+
})
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
pub(crate) fn log_entry_to_hash(entry: &LogEntry) -> RHash {
|
|
746
|
+
let source = match entry.source {
|
|
747
|
+
LogSource::Stdout => "stdout",
|
|
748
|
+
LogSource::Stderr => "stderr",
|
|
749
|
+
LogSource::Output => "output",
|
|
750
|
+
LogSource::System => "system",
|
|
751
|
+
};
|
|
752
|
+
let r = ruby();
|
|
753
|
+
let hash = r.hash_new();
|
|
754
|
+
let _ = hash.aset("timestamp_ms", entry.timestamp.timestamp_millis());
|
|
755
|
+
let _ = hash.aset("source", source);
|
|
756
|
+
let _ = hash.aset("session_id", entry.session_id);
|
|
757
|
+
let _ = hash.aset("cursor", entry.cursor.to_string());
|
|
758
|
+
let _ = hash.aset("data", r.str_from_slice(entry.data.as_ref()));
|
|
759
|
+
hash
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
//--------------------------------------------------------------------------------------------------
|
|
763
|
+
// Registration
|
|
764
|
+
//--------------------------------------------------------------------------------------------------
|
|
765
|
+
|
|
766
|
+
pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
767
|
+
let class = native.define_class("Sandbox", ruby.class_object())?;
|
|
768
|
+
|
|
769
|
+
class.define_singleton_method("create", function!(Sandbox::create, 2))?;
|
|
770
|
+
class.define_singleton_method("start", function!(Sandbox::start, 2))?;
|
|
771
|
+
class.define_singleton_method("get", function!(Sandbox::get, 1))?;
|
|
772
|
+
class.define_singleton_method("list", function!(Sandbox::list, 0))?;
|
|
773
|
+
class.define_singleton_method("list_with", function!(Sandbox::list_with, 1))?;
|
|
774
|
+
class.define_singleton_method("remove", function!(Sandbox::remove, 1))?;
|
|
775
|
+
|
|
776
|
+
class.define_method("name", method!(Sandbox::name, 0))?;
|
|
777
|
+
class.define_method("exec", method!(Sandbox::exec, 3))?;
|
|
778
|
+
class.define_method("shell", method!(Sandbox::shell, 2))?;
|
|
779
|
+
class.define_method("exec_stream", method!(Sandbox::exec_stream, 3))?;
|
|
780
|
+
class.define_method("shell_stream", method!(Sandbox::shell_stream, 2))?;
|
|
781
|
+
class.define_method("stop", method!(Sandbox::stop, 1))?;
|
|
782
|
+
class.define_method("kill", method!(Sandbox::kill, 1))?;
|
|
783
|
+
class.define_method("request_stop", method!(Sandbox::request_stop, 0))?;
|
|
784
|
+
class.define_method("request_kill", method!(Sandbox::request_kill, 0))?;
|
|
785
|
+
class.define_method("request_drain", method!(Sandbox::request_drain, 0))?;
|
|
786
|
+
class.define_method(
|
|
787
|
+
"wait_until_stopped",
|
|
788
|
+
method!(Sandbox::wait_until_stopped, 0),
|
|
789
|
+
)?;
|
|
790
|
+
class.define_method("owns_lifecycle", method!(Sandbox::owns_lifecycle, 0))?;
|
|
791
|
+
class.define_method("detach", method!(Sandbox::detach, 0))?;
|
|
792
|
+
class.define_method("metrics", method!(Sandbox::metrics, 0))?;
|
|
793
|
+
class.define_method("metrics_stream", method!(Sandbox::metrics_stream, 1))?;
|
|
794
|
+
class.define_method("logs", method!(Sandbox::logs, 1))?;
|
|
795
|
+
class.define_method("log_stream", method!(Sandbox::log_stream, 1))?;
|
|
796
|
+
|
|
797
|
+
class.define_method("fs_read", method!(Sandbox::fs_read, 1))?;
|
|
798
|
+
class.define_method("fs_read_text", method!(Sandbox::fs_read_text, 1))?;
|
|
799
|
+
class.define_method("fs_write", method!(Sandbox::fs_write, 2))?;
|
|
800
|
+
class.define_method("fs_list", method!(Sandbox::fs_list, 1))?;
|
|
801
|
+
class.define_method("fs_mkdir", method!(Sandbox::fs_mkdir, 1))?;
|
|
802
|
+
class.define_method("fs_remove", method!(Sandbox::fs_remove, 1))?;
|
|
803
|
+
class.define_method("fs_remove_dir", method!(Sandbox::fs_remove_dir, 1))?;
|
|
804
|
+
class.define_method("fs_copy", method!(Sandbox::fs_copy, 2))?;
|
|
805
|
+
class.define_method("fs_rename", method!(Sandbox::fs_rename, 2))?;
|
|
806
|
+
class.define_method("fs_exists", method!(Sandbox::fs_exists, 1))?;
|
|
807
|
+
class.define_method("fs_stat", method!(Sandbox::fs_stat, 1))?;
|
|
808
|
+
class.define_method("fs_copy_from_host", method!(Sandbox::fs_copy_from_host, 2))?;
|
|
809
|
+
class.define_method("fs_copy_to_host", method!(Sandbox::fs_copy_to_host, 2))?;
|
|
810
|
+
|
|
811
|
+
Ok(())
|
|
812
|
+
}
|