microsandbox-rb 0.5.8 → 0.5.10
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 +125 -0
- data/Cargo.lock +90 -45
- data/DESIGN.md +32 -19
- data/README.md +108 -36
- data/ext/microsandbox/Cargo.toml +4 -4
- data/ext/microsandbox/extconf.rb +6 -2
- data/ext/microsandbox/src/agent.rs +166 -0
- data/ext/microsandbox/src/backend.rs +170 -0
- data/ext/microsandbox/src/conv.rs +19 -1
- data/ext/microsandbox/src/error.rs +6 -0
- data/ext/microsandbox/src/image.rs +7 -7
- data/ext/microsandbox/src/lib.rs +31 -4
- data/ext/microsandbox/src/sandbox.rs +666 -61
- data/ext/microsandbox/src/ssh.rs +317 -0
- data/ext/microsandbox/src/volume.rs +6 -1
- data/lib/microsandbox/agent.rb +181 -0
- data/lib/microsandbox/errors.rb +6 -0
- data/lib/microsandbox/exec_handle.rb +14 -11
- data/lib/microsandbox/fs.rb +7 -7
- data/lib/microsandbox/image.rb +1 -1
- data/lib/microsandbox/network.rb +300 -0
- data/lib/microsandbox/patch.rb +98 -0
- data/lib/microsandbox/sandbox.rb +285 -77
- data/lib/microsandbox/snapshot.rb +2 -2
- data/lib/microsandbox/ssh.rb +247 -0
- data/lib/microsandbox/version.rb +2 -2
- data/lib/microsandbox/volume.rb +3 -3
- data/lib/microsandbox.rb +65 -0
- data/sig/microsandbox.rbs +164 -14
- metadata +8 -1
|
@@ -15,12 +15,15 @@ use microsandbox::logs::{
|
|
|
15
15
|
LogCursor, LogEntry, LogOptions, LogSource, LogStreamOptions, LogStreamStart,
|
|
16
16
|
};
|
|
17
17
|
use microsandbox::sandbox::{
|
|
18
|
-
FsEntry, FsEntryKind, FsMetadata, PullPolicy, RlimitResource,
|
|
19
|
-
SandboxMetrics, SandboxStatus, SandboxStopResult,
|
|
18
|
+
AttachOptionsBuilder, FsEntry, FsEntryKind, FsMetadata, Patch, PullPolicy, RlimitResource,
|
|
19
|
+
SandboxFilter, SandboxHandle, SandboxMetrics, SandboxStatus, SandboxStopResult,
|
|
20
|
+
SecurityProfile,
|
|
20
21
|
};
|
|
21
22
|
use microsandbox::LogLevel;
|
|
22
23
|
use microsandbox::RegistryAuth;
|
|
23
|
-
use microsandbox_network::policy::
|
|
24
|
+
use microsandbox_network::policy::{
|
|
25
|
+
Action, Destination, DestinationGroup, Direction, NetworkPolicy, PortRange, Protocol, Rule,
|
|
26
|
+
};
|
|
24
27
|
|
|
25
28
|
use crate::conv;
|
|
26
29
|
use crate::error;
|
|
@@ -108,6 +111,12 @@ impl Sandbox {
|
|
|
108
111
|
}
|
|
109
112
|
});
|
|
110
113
|
}
|
|
114
|
+
// patches: rootfs modifications applied before boot. The Ruby layer
|
|
115
|
+
// normalizes each `Microsandbox::Patch.*` into a string-keyed Hash with
|
|
116
|
+
// a `kind` discriminator; mirrors the Python binding's `apply_patch`.
|
|
117
|
+
for patch in parse_patches(opts)? {
|
|
118
|
+
b = b.add_patch(patch);
|
|
119
|
+
}
|
|
111
120
|
if let Some(net) = conv::opt_string(opts, "network")? {
|
|
112
121
|
match net.as_str() {
|
|
113
122
|
"none" | "disabled" | "disable" | "airgapped" => b = b.disable_network(),
|
|
@@ -123,6 +132,13 @@ impl Sandbox {
|
|
|
123
132
|
}
|
|
124
133
|
}
|
|
125
134
|
}
|
|
135
|
+
// Custom network policy: an ordered allow/deny rule list with per-direction
|
|
136
|
+
// defaults and bulk domain denials. The Ruby layer routes bare presets to
|
|
137
|
+
// the `network` key above and full policies here; mirrors the Python
|
|
138
|
+
// binding's `apply_network`.
|
|
139
|
+
if let Some(policy) = parse_network_policy(opts)? {
|
|
140
|
+
b = b.network(move |n| n.policy(policy));
|
|
141
|
+
}
|
|
126
142
|
if let Some(level) = conv::opt_string(opts, "log_level")? {
|
|
127
143
|
b = b.log_level(log_level_from_str(&level)?);
|
|
128
144
|
}
|
|
@@ -193,27 +209,36 @@ impl Sandbox {
|
|
|
193
209
|
Ok(Sandbox::from_inner(inner))
|
|
194
210
|
}
|
|
195
211
|
|
|
196
|
-
///
|
|
197
|
-
|
|
212
|
+
/// A controllable handle for a sandbox by name (running or not). Carries
|
|
213
|
+
/// metadata accessors and the full lifecycle surface (see `SbHandle`).
|
|
214
|
+
fn get(name: String) -> Result<SbHandle, Error> {
|
|
198
215
|
let handle = block_on(microsandbox::Sandbox::get(&name)).map_err(error::to_ruby)?;
|
|
199
|
-
Ok(
|
|
216
|
+
Ok(SbHandle::from_inner(handle))
|
|
200
217
|
}
|
|
201
218
|
|
|
202
|
-
/// All sandboxes as
|
|
219
|
+
/// All sandboxes as controllable handles.
|
|
203
220
|
fn list() -> Result<RArray, Error> {
|
|
204
221
|
let handles = block_on(microsandbox::Sandbox::list()).map_err(error::to_ruby)?;
|
|
205
|
-
|
|
222
|
+
let arr = ruby().ary_new();
|
|
223
|
+
for h in handles {
|
|
224
|
+
arr.push(SbHandle::from_inner(h))?;
|
|
225
|
+
}
|
|
226
|
+
Ok(arr)
|
|
206
227
|
}
|
|
207
228
|
|
|
208
|
-
/// Sandboxes filtered by required `key=value` labels (AND-matched)
|
|
209
|
-
/// carries a string→string `labels` map.
|
|
229
|
+
/// Sandboxes filtered by required `key=value` labels (AND-matched), as
|
|
230
|
+
/// controllable handles. `opts` carries a string→string `labels` map.
|
|
210
231
|
fn list_with(opts: RHash) -> Result<RArray, Error> {
|
|
211
232
|
let mut filter = SandboxFilter::new();
|
|
212
233
|
for (k, v) in conv::opt_string_map(opts, "labels")? {
|
|
213
234
|
filter = filter.label(k, v);
|
|
214
235
|
}
|
|
215
236
|
let handles = block_on(microsandbox::Sandbox::list_with(filter)).map_err(error::to_ruby)?;
|
|
216
|
-
|
|
237
|
+
let arr = ruby().ary_new();
|
|
238
|
+
for h in handles {
|
|
239
|
+
arr.push(SbHandle::from_inner(h))?;
|
|
240
|
+
}
|
|
241
|
+
Ok(arr)
|
|
217
242
|
}
|
|
218
243
|
|
|
219
244
|
/// Remove a (stopped) sandbox by name.
|
|
@@ -270,44 +295,46 @@ impl Sandbox {
|
|
|
270
295
|
Ok(ExecHandle::from_core(handle))
|
|
271
296
|
}
|
|
272
297
|
|
|
273
|
-
/// Graceful stop
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
298
|
+
/// Graceful stop. Mirrors the official SDKs: the live handle routes through
|
|
299
|
+
/// a freshly fetched `SandboxHandle::stop` (SIGTERM→SIGKILL escalation with
|
|
300
|
+
/// a 10s default). Fine-grained control — a custom timeout or fire-and-
|
|
301
|
+
/// return `request_*` — lives on `SandboxHandle`, obtained via `Sandbox.get`.
|
|
302
|
+
fn stop(&self) -> Result<(), Error> {
|
|
303
|
+
let name = self.inner.name().to_string();
|
|
304
|
+
block_on(async move {
|
|
305
|
+
let handle = microsandbox::sandbox::Sandbox::get(&name).await?;
|
|
306
|
+
handle.stop().await
|
|
307
|
+
})
|
|
279
308
|
.map_err(error::to_ruby)
|
|
280
309
|
}
|
|
281
310
|
|
|
282
|
-
///
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
}
|
|
288
|
-
.map_err(error::to_ruby)
|
|
311
|
+
/// Graceful stop, then wait for the process to exit. Returns an exit-status
|
|
312
|
+
/// Hash (`exit_code`, `success`). Local backend only.
|
|
313
|
+
fn stop_and_wait(&self) -> Result<RHash, Error> {
|
|
314
|
+
let status = block_on(self.inner.stop_and_wait()).map_err(error::to_ruby)?;
|
|
315
|
+
Ok(exit_status_to_hash(status))
|
|
289
316
|
}
|
|
290
317
|
|
|
291
|
-
///
|
|
292
|
-
fn
|
|
293
|
-
block_on(self.inner.
|
|
318
|
+
/// Force kill (SIGKILL).
|
|
319
|
+
fn kill(&self) -> Result<(), Error> {
|
|
320
|
+
block_on(self.inner.kill()).map_err(error::to_ruby)
|
|
294
321
|
}
|
|
295
322
|
|
|
296
|
-
///
|
|
297
|
-
fn
|
|
298
|
-
block_on(self.inner.
|
|
323
|
+
/// Trigger a graceful drain (SIGUSR1 on local).
|
|
324
|
+
fn drain(&self) -> Result<(), Error> {
|
|
325
|
+
block_on(self.inner.drain()).map_err(error::to_ruby)
|
|
299
326
|
}
|
|
300
327
|
|
|
301
|
-
///
|
|
302
|
-
fn
|
|
303
|
-
block_on(self.inner.
|
|
328
|
+
/// Wait for the process to exit. Returns an exit-status Hash. Local only.
|
|
329
|
+
fn wait(&self) -> Result<RHash, Error> {
|
|
330
|
+
let status = block_on(self.inner.wait()).map_err(error::to_ruby)?;
|
|
331
|
+
Ok(exit_status_to_hash(status))
|
|
304
332
|
}
|
|
305
333
|
|
|
306
|
-
///
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
Ok(stop_result_to_hash(&result))
|
|
334
|
+
/// Live status fetched from the backend (a round-trip per call).
|
|
335
|
+
fn status(&self) -> Result<String, Error> {
|
|
336
|
+
let status = block_on(self.inner.status()).map_err(error::to_ruby)?;
|
|
337
|
+
Ok(sandbox_status_str(status).to_string())
|
|
311
338
|
}
|
|
312
339
|
|
|
313
340
|
/// Whether this handle owns the sandbox process lifecycle (a synchronous,
|
|
@@ -434,6 +461,120 @@ impl Sandbox {
|
|
|
434
461
|
let fs = self.inner.fs();
|
|
435
462
|
block_on(fs.copy_to_host(&guest_path, &host_path)).map_err(error::to_ruby)
|
|
436
463
|
}
|
|
464
|
+
|
|
465
|
+
//----------------------------------------------------------------------
|
|
466
|
+
// SSH (mirror SandboxSshOps)
|
|
467
|
+
//----------------------------------------------------------------------
|
|
468
|
+
|
|
469
|
+
/// Open a native in-process SSH client to this sandbox. `opts`: user, term,
|
|
470
|
+
/// sftp (bool, default true).
|
|
471
|
+
fn ssh_open_client(&self, opts: RHash) -> Result<crate::ssh::SshClient, Error> {
|
|
472
|
+
let user = conv::opt_string(opts, "user")?;
|
|
473
|
+
let term = conv::opt_string(opts, "term")?;
|
|
474
|
+
let sftp = conv::opt::<bool>(opts, "sftp")?.unwrap_or(true);
|
|
475
|
+
let ssh = self.inner.ssh();
|
|
476
|
+
let client = block_on(ssh.open_client_with(move |mut b| {
|
|
477
|
+
if let Some(u) = user {
|
|
478
|
+
b = b.user(u);
|
|
479
|
+
}
|
|
480
|
+
if let Some(t) = term {
|
|
481
|
+
b = b.term(t);
|
|
482
|
+
}
|
|
483
|
+
b.sftp(sftp)
|
|
484
|
+
}))
|
|
485
|
+
.map_err(error::to_ruby)?;
|
|
486
|
+
Ok(crate::ssh::SshClient::from_core(client))
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/// Prepare a reusable SSH server endpoint. `opts`: host_key_path,
|
|
490
|
+
/// authorized_keys_path, user, sftp (bool, default true).
|
|
491
|
+
fn ssh_prepare_server(&self, opts: RHash) -> Result<crate::ssh::SshServer, Error> {
|
|
492
|
+
let host_key_path = conv::opt_string(opts, "host_key_path")?;
|
|
493
|
+
let authorized_keys_path = conv::opt_string(opts, "authorized_keys_path")?;
|
|
494
|
+
let user = conv::opt_string(opts, "user")?;
|
|
495
|
+
let sftp = conv::opt::<bool>(opts, "sftp")?.unwrap_or(true);
|
|
496
|
+
let ssh = self.inner.ssh();
|
|
497
|
+
let server = block_on(ssh.prepare_server_with(move |mut b| {
|
|
498
|
+
if let Some(p) = host_key_path {
|
|
499
|
+
b = b.host_key_path(p);
|
|
500
|
+
}
|
|
501
|
+
if let Some(p) = authorized_keys_path {
|
|
502
|
+
b = b.authorized_keys_path(p);
|
|
503
|
+
}
|
|
504
|
+
if let Some(u) = user {
|
|
505
|
+
b = b.user(u);
|
|
506
|
+
}
|
|
507
|
+
b.sftp(sftp)
|
|
508
|
+
}))
|
|
509
|
+
.map_err(error::to_ruby)?;
|
|
510
|
+
Ok(crate::ssh::SshServer::from_core(server))
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
//----------------------------------------------------------------------
|
|
514
|
+
// Interactive attach (host-TTY coupled)
|
|
515
|
+
//----------------------------------------------------------------------
|
|
516
|
+
|
|
517
|
+
/// Attach an interactive terminal to a command in the sandbox; returns its
|
|
518
|
+
/// exit code. Puts the host terminal in raw mode (requires a real tty) and
|
|
519
|
+
/// blocks until the command exits or the detach sequence is typed. `opts`:
|
|
520
|
+
/// cwd, user, env, detach_keys, rlimits.
|
|
521
|
+
fn attach(&self, cmd: String, args: Vec<String>, opts: RHash) -> Result<i32, Error> {
|
|
522
|
+
let parsed = AttachOpts::parse(args, opts)?;
|
|
523
|
+
block_on(self.inner.attach_with(cmd, move |b| parsed.apply(b))).map_err(error::to_ruby)
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/// Attach an interactive terminal running the sandbox's default shell.
|
|
527
|
+
fn attach_shell(&self) -> Result<i32, Error> {
|
|
528
|
+
block_on(self.inner.attach_shell()).map_err(error::to_ruby)
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
//--------------------------------------------------------------------------------------------------
|
|
533
|
+
// Attach option parsing
|
|
534
|
+
//--------------------------------------------------------------------------------------------------
|
|
535
|
+
|
|
536
|
+
struct AttachOpts {
|
|
537
|
+
args: Vec<String>,
|
|
538
|
+
cwd: Option<String>,
|
|
539
|
+
user: Option<String>,
|
|
540
|
+
env: Vec<(String, String)>,
|
|
541
|
+
detach_keys: Option<String>,
|
|
542
|
+
rlimits: Vec<(RlimitResource, u64, u64)>,
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
impl AttachOpts {
|
|
546
|
+
fn parse(args: Vec<String>, opts: RHash) -> Result<Self, Error> {
|
|
547
|
+
Ok(Self {
|
|
548
|
+
args,
|
|
549
|
+
cwd: conv::opt_string(opts, "cwd")?,
|
|
550
|
+
user: conv::opt_string(opts, "user")?,
|
|
551
|
+
env: conv::opt_string_map(opts, "env")?,
|
|
552
|
+
detach_keys: conv::opt_string(opts, "detach_keys")?,
|
|
553
|
+
rlimits: parse_rlimits(opts)?,
|
|
554
|
+
})
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
fn apply(self, mut b: AttachOptionsBuilder) -> AttachOptionsBuilder {
|
|
558
|
+
if !self.args.is_empty() {
|
|
559
|
+
b = b.args(self.args);
|
|
560
|
+
}
|
|
561
|
+
if let Some(cwd) = self.cwd {
|
|
562
|
+
b = b.cwd(cwd);
|
|
563
|
+
}
|
|
564
|
+
if let Some(user) = self.user {
|
|
565
|
+
b = b.user(user);
|
|
566
|
+
}
|
|
567
|
+
for (k, v) in self.env {
|
|
568
|
+
b = b.env(k, v);
|
|
569
|
+
}
|
|
570
|
+
if let Some(keys) = self.detach_keys {
|
|
571
|
+
b = b.detach_keys(keys);
|
|
572
|
+
}
|
|
573
|
+
for (resource, soft, hard) in self.rlimits {
|
|
574
|
+
b = b.rlimit_range(resource, soft, hard);
|
|
575
|
+
}
|
|
576
|
+
b
|
|
577
|
+
}
|
|
437
578
|
}
|
|
438
579
|
|
|
439
580
|
//--------------------------------------------------------------------------------------------------
|
|
@@ -511,6 +652,358 @@ fn parse_rlimits(opts: RHash) -> Result<Vec<(RlimitResource, u64, u64)>, Error>
|
|
|
511
652
|
Ok(out)
|
|
512
653
|
}
|
|
513
654
|
|
|
655
|
+
//--------------------------------------------------------------------------------------------------
|
|
656
|
+
// Patch parsing (mirrors the Python binding's `apply_patch`)
|
|
657
|
+
//--------------------------------------------------------------------------------------------------
|
|
658
|
+
|
|
659
|
+
/// Read a required string field from a per-patch Hash.
|
|
660
|
+
fn patch_str(h: RHash, key: &str) -> Result<String, Error> {
|
|
661
|
+
conv::opt_string(h, key)?
|
|
662
|
+
.ok_or_else(|| error::base_error(format!("patch is missing required key :{key}")))
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/// Read a required field as raw bytes (for the binary `file` patch content).
|
|
666
|
+
fn patch_bytes(h: RHash, key: &str) -> Result<Vec<u8>, Error> {
|
|
667
|
+
let s = conv::opt::<RString>(h, key)?
|
|
668
|
+
.ok_or_else(|| error::base_error(format!("patch is missing required key :{key}")))?;
|
|
669
|
+
// Copy out while the GVL is held; the buffer is consumed synchronously here.
|
|
670
|
+
Ok(unsafe { s.as_slice() }.to_vec())
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/// Parse the `patches` option into core `Patch` operations. The Ruby layer
|
|
674
|
+
/// normalizes each `Microsandbox::Patch.*` into a string-keyed Hash carrying a
|
|
675
|
+
/// `kind` discriminator plus the variant-specific fields.
|
|
676
|
+
fn parse_patches(opts: RHash) -> Result<Vec<Patch>, Error> {
|
|
677
|
+
let mut out = Vec::new();
|
|
678
|
+
for h in conv::opt_hash_vec(opts, "patches")? {
|
|
679
|
+
let kind = patch_str(h, "kind")?;
|
|
680
|
+
let mode = conv::opt_u32(h, "mode")?;
|
|
681
|
+
let replace = conv::opt_bool(h, "replace")?;
|
|
682
|
+
let patch = match kind.as_str() {
|
|
683
|
+
"text" => Patch::Text {
|
|
684
|
+
path: patch_str(h, "path")?,
|
|
685
|
+
content: patch_str(h, "content")?,
|
|
686
|
+
mode,
|
|
687
|
+
replace,
|
|
688
|
+
},
|
|
689
|
+
"file" => Patch::File {
|
|
690
|
+
path: patch_str(h, "path")?,
|
|
691
|
+
content: patch_bytes(h, "content")?,
|
|
692
|
+
mode,
|
|
693
|
+
replace,
|
|
694
|
+
},
|
|
695
|
+
"append" => Patch::Append {
|
|
696
|
+
path: patch_str(h, "path")?,
|
|
697
|
+
content: patch_str(h, "content")?,
|
|
698
|
+
},
|
|
699
|
+
"copy_file" => Patch::CopyFile {
|
|
700
|
+
src: patch_str(h, "src")?.into(),
|
|
701
|
+
dst: patch_str(h, "dst")?,
|
|
702
|
+
mode,
|
|
703
|
+
replace,
|
|
704
|
+
},
|
|
705
|
+
"copy_dir" => Patch::CopyDir {
|
|
706
|
+
src: patch_str(h, "src")?.into(),
|
|
707
|
+
dst: patch_str(h, "dst")?,
|
|
708
|
+
replace,
|
|
709
|
+
},
|
|
710
|
+
"symlink" => Patch::Symlink {
|
|
711
|
+
target: patch_str(h, "target")?,
|
|
712
|
+
link: patch_str(h, "link")?,
|
|
713
|
+
replace,
|
|
714
|
+
},
|
|
715
|
+
"mkdir" => Patch::Mkdir {
|
|
716
|
+
path: patch_str(h, "path")?,
|
|
717
|
+
mode,
|
|
718
|
+
},
|
|
719
|
+
"remove" => Patch::Remove {
|
|
720
|
+
path: patch_str(h, "path")?,
|
|
721
|
+
},
|
|
722
|
+
other => {
|
|
723
|
+
return Err(error::base_error(format!(
|
|
724
|
+
"unknown patch kind {other:?} (expected one of \
|
|
725
|
+
text/file/append/copy_file/copy_dir/symlink/mkdir/remove)"
|
|
726
|
+
)))
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
out.push(patch);
|
|
730
|
+
}
|
|
731
|
+
Ok(out)
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
//--------------------------------------------------------------------------------------------------
|
|
735
|
+
// Network policy parsing (mirrors the Python binding's `apply_network`)
|
|
736
|
+
//--------------------------------------------------------------------------------------------------
|
|
737
|
+
|
|
738
|
+
/// Parse the `network_policy` option (a Hash normalized by the Ruby layer) into
|
|
739
|
+
/// a core `NetworkPolicy`. Returns `None` when the option is absent (bare
|
|
740
|
+
/// presets travel via the separate `network` key handled in `create`).
|
|
741
|
+
///
|
|
742
|
+
/// Composition (mirrors the Go SDK's `NetworkConfig`): bulk domain-deny rules
|
|
743
|
+
/// come first (so they outrank later allow rules), then a preset's rules (if a
|
|
744
|
+
/// preset base is given), then the caller's explicit `rules`. Per-direction
|
|
745
|
+
/// defaults come from the explicit `default_egress`/`default_ingress` when set,
|
|
746
|
+
/// else the preset's defaults, else the asymmetric default (deny egress / allow
|
|
747
|
+
/// ingress).
|
|
748
|
+
fn parse_network_policy(opts: RHash) -> Result<Option<NetworkPolicy>, Error> {
|
|
749
|
+
let Some(np) = conv::opt::<RHash>(opts, "network_policy")? else {
|
|
750
|
+
return Ok(None);
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
// Bulk domain denials → prepended deny-egress rules.
|
|
754
|
+
let mut rules: Vec<Rule> = Vec::new();
|
|
755
|
+
for d in conv::opt_string_vec(np, "deny_domains")? {
|
|
756
|
+
let domain = d
|
|
757
|
+
.parse()
|
|
758
|
+
.map_err(|e| error::base_error(format!("deny_domains {d:?}: {e}")))?;
|
|
759
|
+
rules.push(Rule::deny_egress(Destination::Domain(domain)));
|
|
760
|
+
}
|
|
761
|
+
for s in conv::opt_string_vec(np, "deny_domain_suffixes")? {
|
|
762
|
+
let suffix = s
|
|
763
|
+
.parse()
|
|
764
|
+
.map_err(|e| error::base_error(format!("deny_domain_suffixes {s:?}: {e}")))?;
|
|
765
|
+
rules.push(Rule::deny_egress(Destination::DomainSuffix(suffix)));
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Optional preset base (its rules and defaults seed the policy).
|
|
769
|
+
let (preset_egress, preset_ingress) = match conv::opt_string(np, "preset")? {
|
|
770
|
+
Some(p) => {
|
|
771
|
+
let mut base = network_preset(&p)?;
|
|
772
|
+
rules.append(&mut base.rules);
|
|
773
|
+
(Some(base.default_egress), Some(base.default_ingress))
|
|
774
|
+
}
|
|
775
|
+
None => (None, None),
|
|
776
|
+
};
|
|
777
|
+
|
|
778
|
+
// Caller's explicit rules come after preset rules.
|
|
779
|
+
for rd in conv::opt_hash_vec(np, "rules")? {
|
|
780
|
+
rules.push(parse_rule(rd)?);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
let default_egress = match conv::opt_string(np, "default_egress")? {
|
|
784
|
+
Some(s) => action_from_str(&s)?,
|
|
785
|
+
None => preset_egress.unwrap_or(Action::Deny),
|
|
786
|
+
};
|
|
787
|
+
let default_ingress = match conv::opt_string(np, "default_ingress")? {
|
|
788
|
+
Some(s) => action_from_str(&s)?,
|
|
789
|
+
None => preset_ingress.unwrap_or(Action::Allow),
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
Ok(Some(NetworkPolicy {
|
|
793
|
+
default_egress,
|
|
794
|
+
default_ingress,
|
|
795
|
+
rules,
|
|
796
|
+
}))
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
fn network_preset(p: &str) -> Result<NetworkPolicy, Error> {
|
|
800
|
+
Ok(match p {
|
|
801
|
+
"none" | "disabled" | "disable" | "airgapped" => NetworkPolicy::none(),
|
|
802
|
+
"public" | "public_only" | "public-only" | "default" => NetworkPolicy::public_only(),
|
|
803
|
+
"all" | "allow_all" | "allow-all" => NetworkPolicy::allow_all(),
|
|
804
|
+
"non_local" | "non-local" | "nonlocal" => NetworkPolicy::non_local(),
|
|
805
|
+
other => {
|
|
806
|
+
return Err(error::base_error(format!(
|
|
807
|
+
"unknown network preset {other:?} (expected one of \
|
|
808
|
+
public_only/none/allow_all/non_local)"
|
|
809
|
+
)))
|
|
810
|
+
}
|
|
811
|
+
})
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
fn action_from_str(s: &str) -> Result<Action, Error> {
|
|
815
|
+
match s {
|
|
816
|
+
"allow" => Ok(Action::Allow),
|
|
817
|
+
"deny" => Ok(Action::Deny),
|
|
818
|
+
other => Err(error::base_error(format!(
|
|
819
|
+
"unknown network action {other:?} (expected allow/deny)"
|
|
820
|
+
))),
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
fn direction_from_str(s: &str) -> Result<Direction, Error> {
|
|
825
|
+
match s {
|
|
826
|
+
"egress" => Ok(Direction::Egress),
|
|
827
|
+
"ingress" => Ok(Direction::Ingress),
|
|
828
|
+
"any" => Ok(Direction::Any),
|
|
829
|
+
other => Err(error::base_error(format!(
|
|
830
|
+
"unknown rule direction {other:?} (expected egress/ingress/any)"
|
|
831
|
+
))),
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
fn protocol_from_str(s: &str) -> Result<Protocol, Error> {
|
|
836
|
+
match s {
|
|
837
|
+
"tcp" => Ok(Protocol::Tcp),
|
|
838
|
+
"udp" => Ok(Protocol::Udp),
|
|
839
|
+
"icmpv4" => Ok(Protocol::Icmpv4),
|
|
840
|
+
"icmpv6" => Ok(Protocol::Icmpv6),
|
|
841
|
+
other => Err(error::base_error(format!(
|
|
842
|
+
"unknown protocol {other:?} (expected tcp/udp/icmpv4/icmpv6)"
|
|
843
|
+
))),
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/// Parse a single rule Hash into a core `Rule`.
|
|
848
|
+
fn parse_rule(rd: RHash) -> Result<Rule, Error> {
|
|
849
|
+
let action = action_from_str(&patch_str(rd, "action")?)?;
|
|
850
|
+
let direction = match conv::opt_string(rd, "direction")? {
|
|
851
|
+
Some(s) => direction_from_str(&s)?,
|
|
852
|
+
None => Direction::Egress,
|
|
853
|
+
};
|
|
854
|
+
let kind = conv::opt_string(rd, "destination_kind")?;
|
|
855
|
+
let raw = conv::opt_string(rd, "destination")?;
|
|
856
|
+
let destination = parse_destination(kind.as_deref(), raw.as_deref())?;
|
|
857
|
+
|
|
858
|
+
let mut protocols = Vec::new();
|
|
859
|
+
for p in conv::opt_string_vec(rd, "protocols")? {
|
|
860
|
+
let proto = protocol_from_str(&p)?;
|
|
861
|
+
if !protocols.contains(&proto) {
|
|
862
|
+
protocols.push(proto);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
let mut ports = Vec::new();
|
|
867
|
+
for p in conv::opt_string_vec(rd, "ports")? {
|
|
868
|
+
let range = parse_port_range(&p)?;
|
|
869
|
+
if !ports.contains(&range) {
|
|
870
|
+
ports.push(range);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
Ok(Rule {
|
|
875
|
+
direction,
|
|
876
|
+
destination,
|
|
877
|
+
protocols,
|
|
878
|
+
ports,
|
|
879
|
+
action,
|
|
880
|
+
})
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/// Parse a port string into a `PortRange`. Accepts a single port (`"443"`) or
|
|
884
|
+
/// an inclusive range (`"8000-9000"`).
|
|
885
|
+
fn parse_port_range(raw: &str) -> Result<PortRange, Error> {
|
|
886
|
+
let invalid = || error::base_error(format!("invalid port {raw:?} (expected N or N-M)"));
|
|
887
|
+
if let Some((lo, hi)) = raw.split_once('-') {
|
|
888
|
+
let lo: u16 = lo.trim().parse().map_err(|_| invalid())?;
|
|
889
|
+
let hi: u16 = hi.trim().parse().map_err(|_| invalid())?;
|
|
890
|
+
if lo > hi {
|
|
891
|
+
return Err(error::base_error(format!(
|
|
892
|
+
"invalid port range {raw:?}: low {lo} exceeds high {hi}"
|
|
893
|
+
)));
|
|
894
|
+
}
|
|
895
|
+
Ok(PortRange::range(lo, hi))
|
|
896
|
+
} else {
|
|
897
|
+
let p: u16 = raw.trim().parse().map_err(|_| invalid())?;
|
|
898
|
+
Ok(PortRange::single(p))
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/// Resolve a destination from an explicit `kind` + raw value, or — when `kind`
|
|
903
|
+
/// is absent — classify the raw shorthand string. Mirrors the Python binding's
|
|
904
|
+
/// `parse_network_destination` / `parse_shorthand_destination`.
|
|
905
|
+
fn parse_destination(kind: Option<&str>, raw: Option<&str>) -> Result<Destination, Error> {
|
|
906
|
+
let required = |raw: Option<&str>, kind: &str| -> Result<String, Error> {
|
|
907
|
+
raw.map(str::to_string).ok_or_else(|| {
|
|
908
|
+
error::base_error(format!(
|
|
909
|
+
"destination is required for destination kind {kind:?}"
|
|
910
|
+
))
|
|
911
|
+
})
|
|
912
|
+
};
|
|
913
|
+
match kind {
|
|
914
|
+
Some("any") => Ok(Destination::Any),
|
|
915
|
+
Some("ip") => parse_ip_destination(&required(raw, "ip")?),
|
|
916
|
+
Some("cidr") => parse_cidr_destination(&required(raw, "cidr")?),
|
|
917
|
+
Some("domain") => parse_domain_destination(&required(raw, "domain")?),
|
|
918
|
+
Some("domain_suffix") | Some("domain-suffix") => {
|
|
919
|
+
parse_domain_suffix_destination(&required(raw, "domain_suffix")?)
|
|
920
|
+
}
|
|
921
|
+
Some("group") => parse_group_destination(&required(raw, "group")?),
|
|
922
|
+
Some(other) => Err(error::base_error(format!(
|
|
923
|
+
"unknown destination kind {other:?}"
|
|
924
|
+
))),
|
|
925
|
+
None => parse_shorthand_destination(raw),
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
fn parse_shorthand_destination(raw: Option<&str>) -> Result<Destination, Error> {
|
|
930
|
+
let Some(raw) = raw else {
|
|
931
|
+
return Ok(Destination::Any);
|
|
932
|
+
};
|
|
933
|
+
if raw == "*" {
|
|
934
|
+
return Ok(Destination::Any);
|
|
935
|
+
}
|
|
936
|
+
if let Some(rest) = raw.strip_prefix("domain=") {
|
|
937
|
+
return parse_domain_destination(rest);
|
|
938
|
+
}
|
|
939
|
+
if let Some(rest) = raw.strip_prefix("suffix=") {
|
|
940
|
+
return parse_domain_suffix_destination(rest);
|
|
941
|
+
}
|
|
942
|
+
if let Some(dest) = maybe_group_destination(raw) {
|
|
943
|
+
return Ok(dest);
|
|
944
|
+
}
|
|
945
|
+
if raw.starts_with('.') {
|
|
946
|
+
return parse_domain_suffix_destination(raw);
|
|
947
|
+
}
|
|
948
|
+
if raw.contains('/') {
|
|
949
|
+
return parse_cidr_destination(raw);
|
|
950
|
+
}
|
|
951
|
+
if raw.parse::<std::net::IpAddr>().is_ok() {
|
|
952
|
+
return parse_ip_destination(raw);
|
|
953
|
+
}
|
|
954
|
+
parse_domain_destination(raw)
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
fn parse_ip_destination(raw: &str) -> Result<Destination, Error> {
|
|
958
|
+
let ip: std::net::IpAddr = raw
|
|
959
|
+
.parse()
|
|
960
|
+
.map_err(|e| error::base_error(format!("invalid IP address {raw:?}: {e}")))?;
|
|
961
|
+
let prefix = if ip.is_ipv4() { 32 } else { 128 };
|
|
962
|
+
let cidr = ipnetwork::IpNetwork::new(ip, prefix)
|
|
963
|
+
.map_err(|e| error::base_error(format!("invalid IP address {raw:?}: {e}")))?;
|
|
964
|
+
Ok(Destination::Cidr(cidr))
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
fn parse_cidr_destination(raw: &str) -> Result<Destination, Error> {
|
|
968
|
+
let cidr: ipnetwork::IpNetwork = raw
|
|
969
|
+
.parse()
|
|
970
|
+
.map_err(|e| error::base_error(format!("invalid CIDR {raw:?}: {e}")))?;
|
|
971
|
+
Ok(Destination::Cidr(cidr))
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
fn parse_domain_destination(raw: &str) -> Result<Destination, Error> {
|
|
975
|
+
let name = raw
|
|
976
|
+
.parse()
|
|
977
|
+
.map_err(|e| error::base_error(format!("invalid domain {raw:?}: {e}")))?;
|
|
978
|
+
Ok(Destination::Domain(name))
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
fn parse_domain_suffix_destination(raw: &str) -> Result<Destination, Error> {
|
|
982
|
+
let name = raw
|
|
983
|
+
.parse()
|
|
984
|
+
.map_err(|e| error::base_error(format!("invalid domain suffix {raw:?}: {e}")))?;
|
|
985
|
+
Ok(Destination::DomainSuffix(name))
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
fn parse_group_destination(raw: &str) -> Result<Destination, Error> {
|
|
989
|
+
maybe_group_destination(raw)
|
|
990
|
+
.ok_or_else(|| error::base_error(format!("unknown destination group {raw:?}")))
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
fn maybe_group_destination(raw: &str) -> Option<Destination> {
|
|
994
|
+
let group = match raw {
|
|
995
|
+
"public" => DestinationGroup::Public,
|
|
996
|
+
"loopback" => DestinationGroup::Loopback,
|
|
997
|
+
"private" => DestinationGroup::Private,
|
|
998
|
+
"link-local" | "link_local" => DestinationGroup::LinkLocal,
|
|
999
|
+
"metadata" => DestinationGroup::Metadata,
|
|
1000
|
+
"multicast" => DestinationGroup::Multicast,
|
|
1001
|
+
"host" => DestinationGroup::Host,
|
|
1002
|
+
_ => return None,
|
|
1003
|
+
};
|
|
1004
|
+
Some(Destination::Group(group))
|
|
1005
|
+
}
|
|
1006
|
+
|
|
514
1007
|
//--------------------------------------------------------------------------------------------------
|
|
515
1008
|
// Registry option parsing
|
|
516
1009
|
//--------------------------------------------------------------------------------------------------
|
|
@@ -582,6 +1075,7 @@ struct ExecOpts {
|
|
|
582
1075
|
timeout: Option<Duration>,
|
|
583
1076
|
tty: bool,
|
|
584
1077
|
stdin: Option<Vec<u8>>,
|
|
1078
|
+
stdin_pipe: bool,
|
|
585
1079
|
rlimits: Vec<(RlimitResource, u64, u64)>,
|
|
586
1080
|
}
|
|
587
1081
|
|
|
@@ -596,6 +1090,7 @@ impl ExecOpts {
|
|
|
596
1090
|
timeout: conv::opt_f64(opts, "timeout")?.map(Duration::from_secs_f64),
|
|
597
1091
|
tty: conv::opt_bool(opts, "tty")?,
|
|
598
1092
|
stdin,
|
|
1093
|
+
stdin_pipe: conv::opt_bool(opts, "stdin_pipe")?,
|
|
599
1094
|
rlimits: parse_rlimits(opts)?,
|
|
600
1095
|
})
|
|
601
1096
|
}
|
|
@@ -622,7 +1117,13 @@ impl ExecOpts {
|
|
|
622
1117
|
if self.tty {
|
|
623
1118
|
b = b.tty(true);
|
|
624
1119
|
}
|
|
625
|
-
|
|
1120
|
+
// Pipe mode opens a writable stdin sink (lifted out by ExecHandle via
|
|
1121
|
+
// `take_stdin`); bytes mode feeds a fixed buffer and closes. The core's
|
|
1122
|
+
// `StdinMode` is a single enum, so the two are mutually exclusive — pipe
|
|
1123
|
+
// wins if a caller somehow sets both.
|
|
1124
|
+
if self.stdin_pipe {
|
|
1125
|
+
b = b.stdin_pipe();
|
|
1126
|
+
} else if let Some(stdin) = self.stdin {
|
|
626
1127
|
b = b.stdin_bytes(stdin);
|
|
627
1128
|
}
|
|
628
1129
|
for (resource, soft, hard) in self.rlimits {
|
|
@@ -713,7 +1214,13 @@ pub(crate) fn metrics_to_hash(m: &SandboxMetrics) -> RHash {
|
|
|
713
1214
|
}
|
|
714
1215
|
|
|
715
1216
|
fn sandbox_status_str(status: SandboxStatus) -> &'static str {
|
|
1217
|
+
// Lowercased `Debug` names, matching the official SDKs' `format!("{:?}")`.
|
|
1218
|
+
// `Created`/`Starting` are new in v0.5.8 (cloud-only today). The match is
|
|
1219
|
+
// intentionally exhaustive — no wildcard — so a future upstream variant
|
|
1220
|
+
// surfaces as a compile error rather than a silent fallback.
|
|
716
1221
|
match status {
|
|
1222
|
+
SandboxStatus::Created => "created",
|
|
1223
|
+
SandboxStatus::Starting => "starting",
|
|
717
1224
|
SandboxStatus::Running => "running",
|
|
718
1225
|
SandboxStatus::Draining => "draining",
|
|
719
1226
|
SandboxStatus::Paused => "paused",
|
|
@@ -722,6 +1229,15 @@ fn sandbox_status_str(status: SandboxStatus) -> &'static str {
|
|
|
722
1229
|
}
|
|
723
1230
|
}
|
|
724
1231
|
|
|
1232
|
+
/// A `std::process::ExitStatus` as a Ruby Hash: `exit_code` (Integer or nil) and
|
|
1233
|
+
/// `success` (Boolean). Returned by the live `Sandbox#wait` / `#stop_and_wait`.
|
|
1234
|
+
fn exit_status_to_hash(status: std::process::ExitStatus) -> RHash {
|
|
1235
|
+
let hash = ruby().hash_new();
|
|
1236
|
+
let _ = hash.aset("exit_code", status.code());
|
|
1237
|
+
let _ = hash.aset("success", status.success());
|
|
1238
|
+
hash
|
|
1239
|
+
}
|
|
1240
|
+
|
|
725
1241
|
fn stop_result_to_hash(result: &SandboxStopResult) -> RHash {
|
|
726
1242
|
let hash = ruby().hash_new();
|
|
727
1243
|
let _ = hash.aset("name", result.name.clone());
|
|
@@ -733,19 +1249,85 @@ fn stop_result_to_hash(result: &SandboxStopResult) -> RHash {
|
|
|
733
1249
|
hash
|
|
734
1250
|
}
|
|
735
1251
|
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
1252
|
+
//--------------------------------------------------------------------------------------------------
|
|
1253
|
+
// SandboxHandle — the controllable lightweight handle
|
|
1254
|
+
//--------------------------------------------------------------------------------------------------
|
|
1255
|
+
|
|
1256
|
+
/// Wraps a core `microsandbox::sandbox::SandboxHandle` (returned by
|
|
1257
|
+
/// `Sandbox.get`/`list`/`list_with`). Carries metadata accessors plus the rich
|
|
1258
|
+
/// lifecycle surface that moved off the live `Sandbox` in v0.5.8 — mirroring the
|
|
1259
|
+
/// official Python (`PySandboxHandle`) and Node (`SandboxHandle`) SDKs. Status is
|
|
1260
|
+
/// a synchronous snapshot read off the handle (no round-trip).
|
|
1261
|
+
#[magnus::wrap(class = "Microsandbox::Native::SandboxHandle", free_immediately, size)]
|
|
1262
|
+
pub struct SbHandle {
|
|
1263
|
+
inner: SandboxHandle,
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
impl SbHandle {
|
|
1267
|
+
fn from_inner(inner: SandboxHandle) -> Self {
|
|
1268
|
+
Self { inner }
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
fn name(&self) -> String {
|
|
1272
|
+
self.inner.name().to_string()
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
/// Status snapshot captured when the handle was fetched (synchronous).
|
|
1276
|
+
fn status(&self) -> String {
|
|
1277
|
+
sandbox_status_str(self.inner.status_snapshot()).to_string()
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
fn created_at_ms(&self) -> Option<i64> {
|
|
1281
|
+
self.inner.created_at().map(|dt| dt.timestamp_millis())
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
fn updated_at_ms(&self) -> Option<i64> {
|
|
1285
|
+
self.inner.updated_at().map(|dt| dt.timestamp_millis())
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
/// Graceful stop (SIGTERM→SIGKILL escalation, 10s default) and wait.
|
|
1289
|
+
fn stop(&self) -> Result<(), Error> {
|
|
1290
|
+
block_on(self.inner.stop()).map_err(error::to_ruby)
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
/// Graceful stop with a custom escalation timeout (seconds).
|
|
1294
|
+
fn stop_with_timeout(&self, secs: f64) -> Result<(), Error> {
|
|
1295
|
+
block_on(self.inner.stop_with_timeout(Duration::from_secs_f64(secs)))
|
|
1296
|
+
.map_err(error::to_ruby)
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
/// Force kill (SIGKILL) and wait.
|
|
1300
|
+
fn kill(&self) -> Result<(), Error> {
|
|
1301
|
+
block_on(self.inner.kill()).map_err(error::to_ruby)
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
/// Force kill, waiting up to `secs` for the process to disappear.
|
|
1305
|
+
fn kill_with_timeout(&self, secs: f64) -> Result<(), Error> {
|
|
1306
|
+
block_on(self.inner.kill_with_timeout(Duration::from_secs_f64(secs)))
|
|
1307
|
+
.map_err(error::to_ruby)
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
/// Send the graceful-shutdown request and return without waiting.
|
|
1311
|
+
fn request_stop(&self) -> Result<(), Error> {
|
|
1312
|
+
block_on(self.inner.request_stop()).map_err(error::to_ruby)
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
/// Send the force-kill request and return without waiting.
|
|
1316
|
+
fn request_kill(&self) -> Result<(), Error> {
|
|
1317
|
+
block_on(self.inner.request_kill()).map_err(error::to_ruby)
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
/// Send the drain request (SIGUSR1) and return without waiting.
|
|
1321
|
+
fn request_drain(&self) -> Result<(), Error> {
|
|
1322
|
+
block_on(self.inner.request_drain()).map_err(error::to_ruby)
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
/// Block until the sandbox reaches a terminal state; returns a stop-result
|
|
1326
|
+
/// Hash (name, status, exit_code, signal, observed_at_ms, source).
|
|
1327
|
+
fn wait_until_stopped(&self) -> Result<RHash, Error> {
|
|
1328
|
+
let result = block_on(self.inner.wait_until_stopped()).map_err(error::to_ruby)?;
|
|
1329
|
+
Ok(stop_result_to_hash(&result))
|
|
1330
|
+
}
|
|
749
1331
|
}
|
|
750
1332
|
|
|
751
1333
|
//--------------------------------------------------------------------------------------------------
|
|
@@ -846,15 +1428,12 @@ pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
|
846
1428
|
class.define_method("shell", method!(Sandbox::shell, 2))?;
|
|
847
1429
|
class.define_method("exec_stream", method!(Sandbox::exec_stream, 3))?;
|
|
848
1430
|
class.define_method("shell_stream", method!(Sandbox::shell_stream, 2))?;
|
|
849
|
-
class.define_method("stop", method!(Sandbox::stop,
|
|
850
|
-
class.define_method("
|
|
851
|
-
class.define_method("
|
|
852
|
-
class.define_method("
|
|
853
|
-
class.define_method("
|
|
854
|
-
class.define_method(
|
|
855
|
-
"wait_until_stopped",
|
|
856
|
-
method!(Sandbox::wait_until_stopped, 0),
|
|
857
|
-
)?;
|
|
1431
|
+
class.define_method("stop", method!(Sandbox::stop, 0))?;
|
|
1432
|
+
class.define_method("stop_and_wait", method!(Sandbox::stop_and_wait, 0))?;
|
|
1433
|
+
class.define_method("kill", method!(Sandbox::kill, 0))?;
|
|
1434
|
+
class.define_method("drain", method!(Sandbox::drain, 0))?;
|
|
1435
|
+
class.define_method("wait", method!(Sandbox::wait, 0))?;
|
|
1436
|
+
class.define_method("status", method!(Sandbox::status, 0))?;
|
|
858
1437
|
class.define_method("owns_lifecycle", method!(Sandbox::owns_lifecycle, 0))?;
|
|
859
1438
|
class.define_method("detach", method!(Sandbox::detach, 0))?;
|
|
860
1439
|
class.define_method("metrics", method!(Sandbox::metrics, 0))?;
|
|
@@ -876,5 +1455,31 @@ pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
|
876
1455
|
class.define_method("fs_copy_from_host", method!(Sandbox::fs_copy_from_host, 2))?;
|
|
877
1456
|
class.define_method("fs_copy_to_host", method!(Sandbox::fs_copy_to_host, 2))?;
|
|
878
1457
|
|
|
1458
|
+
class.define_method("ssh_open_client", method!(Sandbox::ssh_open_client, 1))?;
|
|
1459
|
+
class.define_method(
|
|
1460
|
+
"ssh_prepare_server",
|
|
1461
|
+
method!(Sandbox::ssh_prepare_server, 1),
|
|
1462
|
+
)?;
|
|
1463
|
+
|
|
1464
|
+
class.define_method("attach", method!(Sandbox::attach, 3))?;
|
|
1465
|
+
class.define_method("attach_shell", method!(Sandbox::attach_shell, 0))?;
|
|
1466
|
+
|
|
1467
|
+
let handle = native.define_class("SandboxHandle", ruby.class_object())?;
|
|
1468
|
+
handle.define_method("name", method!(SbHandle::name, 0))?;
|
|
1469
|
+
handle.define_method("status", method!(SbHandle::status, 0))?;
|
|
1470
|
+
handle.define_method("created_at_ms", method!(SbHandle::created_at_ms, 0))?;
|
|
1471
|
+
handle.define_method("updated_at_ms", method!(SbHandle::updated_at_ms, 0))?;
|
|
1472
|
+
handle.define_method("stop", method!(SbHandle::stop, 0))?;
|
|
1473
|
+
handle.define_method("stop_with_timeout", method!(SbHandle::stop_with_timeout, 1))?;
|
|
1474
|
+
handle.define_method("kill", method!(SbHandle::kill, 0))?;
|
|
1475
|
+
handle.define_method("kill_with_timeout", method!(SbHandle::kill_with_timeout, 1))?;
|
|
1476
|
+
handle.define_method("request_stop", method!(SbHandle::request_stop, 0))?;
|
|
1477
|
+
handle.define_method("request_kill", method!(SbHandle::request_kill, 0))?;
|
|
1478
|
+
handle.define_method("request_drain", method!(SbHandle::request_drain, 0))?;
|
|
1479
|
+
handle.define_method(
|
|
1480
|
+
"wait_until_stopped",
|
|
1481
|
+
method!(SbHandle::wait_until_stopped, 0),
|
|
1482
|
+
)?;
|
|
1483
|
+
|
|
879
1484
|
Ok(())
|
|
880
1485
|
}
|