microsandbox-rb 0.17.2 → 0.17.4
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 +112 -1
- data/Cargo.lock +34 -32
- data/DESIGN.md +199 -8
- data/README.md +4 -2
- data/docs/errors.md +1 -0
- data/docs/runtime.md +115 -0
- data/docs/surface.md +6 -0
- data/ext/microsandbox/Cargo.toml +9 -2
- data/ext/microsandbox/src/agent.rs +13 -10
- data/ext/microsandbox/src/backend.rs +295 -34
- data/ext/microsandbox/src/error.rs +41 -23
- data/ext/microsandbox/src/exec.rs +32 -18
- data/ext/microsandbox/src/fs_stream.rs +11 -8
- data/ext/microsandbox/src/host_path.rs +4 -3
- data/ext/microsandbox/src/lib.rs +51 -2
- data/ext/microsandbox/src/runtime.rs +276 -39
- data/ext/microsandbox/src/sandbox.rs +175 -91
- data/ext/microsandbox/src/snapshot.rs +7 -4
- data/ext/microsandbox/src/ssh.rs +26 -23
- data/ext/microsandbox/src/stream.rs +13 -7
- data/ext/microsandbox/src/volume.rs +22 -16
- data/lib/microsandbox/errors.rb +8 -0
- data/lib/microsandbox/sandbox.rb +39 -20
- data/lib/microsandbox/version.rb +1 -1
- data/lib/microsandbox.rb +20 -1
- data/sig/microsandbox.rbs +1 -0
- metadata +1 -1
|
@@ -41,7 +41,7 @@ use crate::conv;
|
|
|
41
41
|
use crate::error;
|
|
42
42
|
use crate::exec::ExecHandle;
|
|
43
43
|
use crate::host_path::{self, HostPaths};
|
|
44
|
-
use crate::runtime::{block_on, ruby};
|
|
44
|
+
use crate::runtime::{block_on, ruby, ForkGuarded};
|
|
45
45
|
use crate::stream::{LogStream, MetricsStream};
|
|
46
46
|
|
|
47
47
|
/// Convert a seconds `f64` into a `Duration`, surfacing a clean Ruby error
|
|
@@ -61,12 +61,14 @@ fn secs_to_duration(secs: f64) -> Result<Duration, Error> {
|
|
|
61
61
|
|
|
62
62
|
#[magnus::wrap(class = "Microsandbox::Native::Sandbox", free_immediately, size)]
|
|
63
63
|
pub struct Sandbox {
|
|
64
|
-
inner: microsandbox::Sandbox
|
|
64
|
+
inner: ForkGuarded<microsandbox::Sandbox>,
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
impl Sandbox {
|
|
68
68
|
fn from_inner(inner: microsandbox::Sandbox) -> Self {
|
|
69
|
-
Self {
|
|
69
|
+
Self {
|
|
70
|
+
inner: ForkGuarded::new("Microsandbox::Sandbox", inner),
|
|
71
|
+
}
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
//----------------------------------------------------------------------
|
|
@@ -76,10 +78,13 @@ impl Sandbox {
|
|
|
76
78
|
/// Build a configured `SandboxBuilder` from a string-keyed options Hash.
|
|
77
79
|
/// Shared by `create` (blocking) and `create_with_progress` (streaming pull).
|
|
78
80
|
fn build_builder(name: String, opts: RHash) -> Result<SandboxBuilder, Error> {
|
|
81
|
+
// `Sandbox::builder` reads the default backend synchronously (it
|
|
82
|
+
// overlays the local backend's global config), before any `block_on`.
|
|
83
|
+
crate::backend::ensure_this_process_backend()?;
|
|
79
84
|
let mut b = microsandbox::Sandbox::builder(name);
|
|
80
85
|
// Relative host paths are anchored to the current directory here, once,
|
|
81
86
|
// before the core persists them (see host_path.rs).
|
|
82
|
-
let host_paths = HostPaths::for_default_backend()
|
|
87
|
+
let host_paths = HostPaths::for_default_backend()?;
|
|
83
88
|
|
|
84
89
|
if let Some(v) = conv::opt_string(opts, "image")? {
|
|
85
90
|
if let Some(fstype) = conv::opt_string(opts, "fstype")? {
|
|
@@ -640,30 +645,34 @@ impl Sandbox {
|
|
|
640
645
|
// Instance methods
|
|
641
646
|
//----------------------------------------------------------------------
|
|
642
647
|
|
|
648
|
+
/// Plain data, so (like `id`) it stays readable in a forked child, which
|
|
649
|
+
/// needs the name to open a handle of its own (`ForkGuarded::peek`).
|
|
643
650
|
fn name(&self) -> String {
|
|
644
|
-
self.inner.name().to_string()
|
|
651
|
+
self.inner.peek().name().to_string()
|
|
645
652
|
}
|
|
646
653
|
|
|
647
654
|
/// The opaque, backend-assigned identity of this persisted sandbox
|
|
648
655
|
/// (v0.6.16). Stable for the sandbox's lifetime, and different once the
|
|
649
656
|
/// same name is removed and recreated.
|
|
650
657
|
fn id(&self) -> String {
|
|
651
|
-
self.inner.id().to_string()
|
|
658
|
+
self.inner.peek().id().to_string()
|
|
652
659
|
}
|
|
653
660
|
|
|
654
661
|
/// Run a command (no shell). `args` is an Array of strings; `opts` is a
|
|
655
662
|
/// string-keyed Hash (cwd, user, env, timeout, tty, stdin).
|
|
656
663
|
fn exec(&self, cmd: String, args: Vec<String>, opts: RHash) -> Result<RHash, Error> {
|
|
664
|
+
let inner = self.inner.get()?;
|
|
657
665
|
let parsed = ExecOpts::parse(args, opts)?;
|
|
658
|
-
let output =
|
|
659
|
-
.map_err(error::to_ruby)?;
|
|
666
|
+
let output =
|
|
667
|
+
block_on(inner.exec_with(cmd, move |b| parsed.apply(b)))?.map_err(error::to_ruby)?;
|
|
660
668
|
exec_output_to_hash(output)
|
|
661
669
|
}
|
|
662
670
|
|
|
663
671
|
/// Run a shell script (pipes/redirects allowed).
|
|
664
672
|
fn shell(&self, script: String, opts: RHash) -> Result<RHash, Error> {
|
|
673
|
+
let inner = self.inner.get()?;
|
|
665
674
|
let parsed = ExecOpts::parse(Vec::new(), opts)?;
|
|
666
|
-
let output = block_on(
|
|
675
|
+
let output = block_on(inner.shell_with(script, move |b| parsed.apply(b)))?
|
|
667
676
|
.map_err(error::to_ruby)?;
|
|
668
677
|
exec_output_to_hash(output)
|
|
669
678
|
}
|
|
@@ -675,8 +684,9 @@ impl Sandbox {
|
|
|
675
684
|
args: Vec<String>,
|
|
676
685
|
opts: RHash,
|
|
677
686
|
) -> Result<ExecHandle, Error> {
|
|
687
|
+
let inner = self.inner.get()?;
|
|
678
688
|
let parsed = ExecOpts::parse(args, opts)?;
|
|
679
|
-
let handle = block_on(
|
|
689
|
+
let handle = block_on(inner.exec_stream_with(cmd, move |b| parsed.apply(b)))?
|
|
680
690
|
.map_err(error::to_ruby)?;
|
|
681
691
|
Ok(ExecHandle::from_core(handle))
|
|
682
692
|
}
|
|
@@ -687,31 +697,28 @@ impl Sandbox {
|
|
|
687
697
|
/// the command: cwd, user, env, timeout, tty, stdin, rlimits. Raises
|
|
688
698
|
/// NoDefaultCommandError when the image resolves no executable command.
|
|
689
699
|
fn exec_default(&self, opts: RHash) -> Result<RHash, Error> {
|
|
700
|
+
let inner = self.inner.get()?;
|
|
690
701
|
let parsed = ExecOpts::parse(Vec::new(), opts)?;
|
|
691
|
-
let output =
|
|
692
|
-
.map_err(error::to_ruby)?;
|
|
702
|
+
let output =
|
|
703
|
+
block_on(inner.exec_default_with(move |b| parsed.apply(b)))?.map_err(error::to_ruby)?;
|
|
693
704
|
exec_output_to_hash(output)
|
|
694
705
|
}
|
|
695
706
|
|
|
696
707
|
/// Streaming default-workload execution. Returns an ExecHandle.
|
|
697
708
|
fn exec_default_stream(&self, opts: RHash) -> Result<ExecHandle, Error> {
|
|
709
|
+
let inner = self.inner.get()?;
|
|
698
710
|
let parsed = ExecOpts::parse(Vec::new(), opts)?;
|
|
699
|
-
let handle = block_on(
|
|
700
|
-
|
|
701
|
-
.exec_default_stream_with(move |b| parsed.apply(b)),
|
|
702
|
-
)?
|
|
703
|
-
.map_err(error::to_ruby)?;
|
|
711
|
+
let handle = block_on(inner.exec_default_stream_with(move |b| parsed.apply(b)))?
|
|
712
|
+
.map_err(error::to_ruby)?;
|
|
704
713
|
Ok(ExecHandle::from_core(handle))
|
|
705
714
|
}
|
|
706
715
|
|
|
707
716
|
/// Streaming shell execution.
|
|
708
717
|
fn shell_stream(&self, script: String, opts: RHash) -> Result<ExecHandle, Error> {
|
|
718
|
+
let inner = self.inner.get()?;
|
|
709
719
|
let parsed = ExecOpts::parse(Vec::new(), opts)?;
|
|
710
|
-
let handle = block_on(
|
|
711
|
-
|
|
712
|
-
.shell_stream_with(script, move |b| parsed.apply(b)),
|
|
713
|
-
)?
|
|
714
|
-
.map_err(error::to_ruby)?;
|
|
720
|
+
let handle = block_on(inner.shell_stream_with(script, move |b| parsed.apply(b)))?
|
|
721
|
+
.map_err(error::to_ruby)?;
|
|
715
722
|
Ok(ExecHandle::from_core(handle))
|
|
716
723
|
}
|
|
717
724
|
|
|
@@ -731,29 +738,34 @@ impl Sandbox {
|
|
|
731
738
|
/// Fine-grained control — a custom timeout or fire-and-return `request_*` —
|
|
732
739
|
/// lives on `SandboxHandle`, obtained via `Sandbox.get`.
|
|
733
740
|
fn stop(&self) -> Result<(), Error> {
|
|
734
|
-
|
|
741
|
+
let inner = self.inner.get()?;
|
|
742
|
+
block_on(inner.stop())?.map_err(error::to_ruby)
|
|
735
743
|
}
|
|
736
744
|
|
|
737
745
|
/// Graceful stop, then wait for the process to exit. Returns an exit-status
|
|
738
746
|
/// Hash (`exit_code`, `success`). Local backend only.
|
|
739
747
|
fn stop_and_wait(&self) -> Result<RHash, Error> {
|
|
740
|
-
let
|
|
748
|
+
let inner = self.inner.get()?;
|
|
749
|
+
let status = block_on(inner.stop_and_wait())?.map_err(error::to_ruby)?;
|
|
741
750
|
Ok(exit_status_to_hash(status))
|
|
742
751
|
}
|
|
743
752
|
|
|
744
753
|
/// Force kill (SIGKILL).
|
|
745
754
|
fn kill(&self) -> Result<(), Error> {
|
|
746
|
-
|
|
755
|
+
let inner = self.inner.get()?;
|
|
756
|
+
block_on(inner.kill())?.map_err(error::to_ruby)
|
|
747
757
|
}
|
|
748
758
|
|
|
749
759
|
/// Trigger a graceful drain (SIGUSR1 on local).
|
|
750
760
|
fn drain(&self) -> Result<(), Error> {
|
|
751
|
-
|
|
761
|
+
let inner = self.inner.get()?;
|
|
762
|
+
block_on(inner.drain())?.map_err(error::to_ruby)
|
|
752
763
|
}
|
|
753
764
|
|
|
754
765
|
/// Wait for the process to exit. Returns an exit-status Hash. Local only.
|
|
755
766
|
fn wait(&self) -> Result<RHash, Error> {
|
|
756
|
-
let
|
|
767
|
+
let inner = self.inner.get()?;
|
|
768
|
+
let status = block_on(inner.wait())?.map_err(error::to_ruby)?;
|
|
757
769
|
Ok(exit_status_to_hash(status))
|
|
758
770
|
}
|
|
759
771
|
|
|
@@ -761,35 +773,42 @@ impl Sandbox {
|
|
|
761
773
|
/// returning a fresh handle. A same-name replacement raises
|
|
762
774
|
/// `SandboxReplacedError` instead of silently redirecting the wait.
|
|
763
775
|
fn wait_for_status(&self, status: String) -> Result<SbHandle, Error> {
|
|
776
|
+
let inner = self.inner.get()?;
|
|
764
777
|
let status = sandbox_status_from_str(&status)?;
|
|
765
|
-
let handle = block_on(
|
|
778
|
+
let handle = block_on(inner.wait_for_status(status))?.map_err(error::to_ruby)?;
|
|
766
779
|
Ok(SbHandle::from_inner(handle))
|
|
767
780
|
}
|
|
768
781
|
|
|
769
782
|
/// Stop and start this exact sandbox, returning the new live sandbox.
|
|
770
783
|
/// `opts` carries `force`/`timeout`/`detached`.
|
|
771
784
|
fn restart(&self, opts: RHash) -> Result<Sandbox, Error> {
|
|
785
|
+
let inner = self.inner.get()?;
|
|
772
786
|
let options = restart_options(opts)?;
|
|
773
|
-
let inner = block_on(
|
|
787
|
+
let inner = block_on(inner.restart_with(options))?.map_err(error::to_ruby)?;
|
|
774
788
|
Ok(Sandbox::from_inner(inner))
|
|
775
789
|
}
|
|
776
790
|
|
|
777
791
|
/// Stop and remove this exact sandbox. `opts` carries `force`/`timeout`.
|
|
778
792
|
fn destroy(&self, opts: RHash) -> Result<(), Error> {
|
|
793
|
+
let inner = self.inner.get()?;
|
|
779
794
|
let options = destroy_options(opts)?;
|
|
780
|
-
block_on(
|
|
795
|
+
block_on(inner.destroy_with(options))?.map_err(error::to_ruby)
|
|
781
796
|
}
|
|
782
797
|
|
|
783
798
|
/// Live status fetched from the backend (a round-trip per call).
|
|
784
799
|
fn status(&self) -> Result<String, Error> {
|
|
785
|
-
let
|
|
800
|
+
let inner = self.inner.get()?;
|
|
801
|
+
let status = block_on(inner.status())?.map_err(error::to_ruby)?;
|
|
786
802
|
Ok(sandbox_status_str(status).to_string())
|
|
787
803
|
}
|
|
788
804
|
|
|
789
805
|
/// Whether this handle owns the sandbox process lifecycle (a synchronous,
|
|
790
|
-
/// local predicate — no runtime round-trip).
|
|
791
|
-
|
|
792
|
-
|
|
806
|
+
/// local predicate — no runtime round-trip). Process-checked even though
|
|
807
|
+
/// it only reads memory: in a forked child the inherited answer would be
|
|
808
|
+
/// wrong (the parent owns the sandbox), so it raises like the lifecycle
|
|
809
|
+
/// calls it guards.
|
|
810
|
+
fn owns_lifecycle(&self) -> Result<bool, Error> {
|
|
811
|
+
Ok(self.inner.get()?.owns_lifecycle())
|
|
793
812
|
}
|
|
794
813
|
|
|
795
814
|
/// Disarm the SIGTERM safety net so the sandbox keeps running after this
|
|
@@ -797,14 +816,15 @@ impl Sandbox {
|
|
|
797
816
|
/// `Sandbox` is `Clone` (Arc-backed, sharing the same process handle), so
|
|
798
817
|
/// detaching a clone disarms the shared handle just the same.
|
|
799
818
|
fn detach(&self) -> Result<(), Error> {
|
|
800
|
-
let inner = self.inner.clone();
|
|
819
|
+
let inner = self.inner.get()?.clone();
|
|
801
820
|
block_on(inner.detach())?;
|
|
802
821
|
Ok(())
|
|
803
822
|
}
|
|
804
823
|
|
|
805
824
|
/// Latest metrics snapshot as a Hash.
|
|
806
825
|
fn metrics(&self) -> Result<RHash, Error> {
|
|
807
|
-
let
|
|
826
|
+
let inner = self.inner.get()?;
|
|
827
|
+
let m = block_on(inner.metrics())?.map_err(error::to_ruby)?;
|
|
808
828
|
Ok(metrics_to_hash(&m))
|
|
809
829
|
}
|
|
810
830
|
|
|
@@ -816,14 +836,16 @@ impl Sandbox {
|
|
|
816
836
|
/// `{ name, latency_secs }` (round-trip latency as f64 seconds; the Ruby
|
|
817
837
|
/// layer exposes both seconds and milliseconds).
|
|
818
838
|
fn ping(&self) -> Result<RHash, Error> {
|
|
819
|
-
let
|
|
839
|
+
let inner = self.inner.get()?;
|
|
840
|
+
let result = block_on(inner.ping())?.map_err(error::to_ruby)?;
|
|
820
841
|
Ok(ping_result_to_hash(&result))
|
|
821
842
|
}
|
|
822
843
|
|
|
823
844
|
/// Explicitly refresh the idle-activity timer. Returns
|
|
824
845
|
/// `{ name, activity_seq }`.
|
|
825
846
|
fn touch(&self) -> Result<RHash, Error> {
|
|
826
|
-
let
|
|
847
|
+
let inner = self.inner.get()?;
|
|
848
|
+
let result = block_on(inner.touch())?.map_err(error::to_ruby)?;
|
|
827
849
|
Ok(touch_result_to_hash(&result))
|
|
828
850
|
}
|
|
829
851
|
|
|
@@ -833,20 +855,23 @@ impl Sandbox {
|
|
|
833
855
|
/// into a `ModificationPlan`. Mirrors the Node native binding (native returns
|
|
834
856
|
/// JSON, the wrapper shapes it).
|
|
835
857
|
fn modify(&self, opts: RHash) -> Result<String, Error> {
|
|
836
|
-
|
|
858
|
+
let inner = self.inner.get()?;
|
|
859
|
+
run_modify(inner.modify(), opts)
|
|
837
860
|
}
|
|
838
861
|
|
|
839
862
|
/// Read captured logs as an Array of Hashes. `opts`: tail, since_ms,
|
|
840
863
|
/// until_ms, sources (Array of "stdout"/"stderr"/"output"/"system"/"all").
|
|
841
864
|
fn logs(&self, opts: RHash) -> Result<RArray, Error> {
|
|
865
|
+
let inner = self.inner.get()?;
|
|
842
866
|
let log_opts = parse_log_options(opts)?;
|
|
843
|
-
let entries = block_on(
|
|
867
|
+
let entries = block_on(inner.logs(&log_opts))?.map_err(error::to_ruby)?;
|
|
844
868
|
rhash_array(entries.iter().map(log_entry_to_hash))
|
|
845
869
|
}
|
|
846
870
|
|
|
847
871
|
/// Stream metrics snapshots at `interval` seconds. Returns a MetricsStream
|
|
848
872
|
/// to pull snapshots from.
|
|
849
873
|
fn metrics_stream(&self, interval: f64) -> Result<MetricsStream, Error> {
|
|
874
|
+
let inner = self.inner.get()?;
|
|
850
875
|
// `interval <= 0.0` (0 or negative) keeps the prior 1s-default behavior;
|
|
851
876
|
// NaN (for which `<= 0.0` is false) and finite-but-out-of-range values
|
|
852
877
|
// fall through to `secs_to_duration`, which errors cleanly rather than
|
|
@@ -860,15 +885,16 @@ impl Sandbox {
|
|
|
860
885
|
// which panics ("no reactor running") unless constructed inside the
|
|
861
886
|
// runtime context — so build it under `block_on`. (`log_stream` is async
|
|
862
887
|
// and already runs inside `block_on`, so it needs no such wrapper.)
|
|
863
|
-
let stream = block_on(async {
|
|
888
|
+
let stream = block_on(async { inner.metrics_stream(dur) })?;
|
|
864
889
|
Ok(MetricsStream::from_stream(stream))
|
|
865
890
|
}
|
|
866
891
|
|
|
867
892
|
/// Stream captured logs as they appear. `opts`: sources, since_ms,
|
|
868
893
|
/// from_cursor, until_ms, follow. Returns a LogStream.
|
|
869
894
|
fn log_stream(&self, opts: RHash) -> Result<LogStream, Error> {
|
|
895
|
+
let inner = self.inner.get()?;
|
|
870
896
|
let log_opts = parse_log_stream_options(opts)?;
|
|
871
|
-
let stream = block_on(
|
|
897
|
+
let stream = block_on(inner.log_stream(&log_opts))?.map_err(error::to_ruby)?;
|
|
872
898
|
Ok(LogStream::from_stream(stream))
|
|
873
899
|
}
|
|
874
900
|
|
|
@@ -877,86 +903,101 @@ impl Sandbox {
|
|
|
877
903
|
//----------------------------------------------------------------------
|
|
878
904
|
|
|
879
905
|
fn fs_read(&self, path: String) -> Result<RString, Error> {
|
|
880
|
-
let
|
|
906
|
+
let inner = self.inner.get()?;
|
|
907
|
+
let fs = inner.fs();
|
|
881
908
|
let bytes = block_on(fs.read(&path))?.map_err(error::to_ruby)?;
|
|
882
909
|
Ok(ruby().str_from_slice(bytes.as_ref()))
|
|
883
910
|
}
|
|
884
911
|
|
|
885
912
|
fn fs_read_text(&self, path: String) -> Result<String, Error> {
|
|
886
|
-
let
|
|
913
|
+
let inner = self.inner.get()?;
|
|
914
|
+
let fs = inner.fs();
|
|
887
915
|
block_on(fs.read_to_string(&path))?.map_err(error::to_ruby)
|
|
888
916
|
}
|
|
889
917
|
|
|
890
918
|
fn fs_write(&self, path: String, data: RString) -> Result<(), Error> {
|
|
919
|
+
let inner = self.inner.get()?;
|
|
891
920
|
// Copy out of the Ruby string while we still hold the GVL: the buffer
|
|
892
921
|
// could be moved/freed by GC.compact once block_on releases it.
|
|
893
922
|
let bytes = unsafe { data.as_slice() }.to_vec();
|
|
894
|
-
let fs =
|
|
923
|
+
let fs = inner.fs();
|
|
895
924
|
block_on(fs.write(&path, &bytes))?.map_err(error::to_ruby)
|
|
896
925
|
}
|
|
897
926
|
|
|
898
927
|
fn fs_list(&self, path: String) -> Result<RArray, Error> {
|
|
899
|
-
let
|
|
928
|
+
let inner = self.inner.get()?;
|
|
929
|
+
let fs = inner.fs();
|
|
900
930
|
let entries = block_on(fs.list(&path))?.map_err(error::to_ruby)?;
|
|
901
931
|
rhash_array(entries.iter().map(fs_entry_to_hash))
|
|
902
932
|
}
|
|
903
933
|
|
|
904
934
|
fn fs_mkdir(&self, path: String) -> Result<(), Error> {
|
|
905
|
-
let
|
|
935
|
+
let inner = self.inner.get()?;
|
|
936
|
+
let fs = inner.fs();
|
|
906
937
|
block_on(fs.mkdir(&path))?.map_err(error::to_ruby)
|
|
907
938
|
}
|
|
908
939
|
|
|
909
940
|
fn fs_remove(&self, path: String) -> Result<(), Error> {
|
|
910
|
-
let
|
|
941
|
+
let inner = self.inner.get()?;
|
|
942
|
+
let fs = inner.fs();
|
|
911
943
|
block_on(fs.remove(&path))?.map_err(error::to_ruby)
|
|
912
944
|
}
|
|
913
945
|
|
|
914
946
|
fn fs_remove_dir(&self, path: String) -> Result<(), Error> {
|
|
915
|
-
let
|
|
947
|
+
let inner = self.inner.get()?;
|
|
948
|
+
let fs = inner.fs();
|
|
916
949
|
block_on(fs.remove_dir(&path))?.map_err(error::to_ruby)
|
|
917
950
|
}
|
|
918
951
|
|
|
919
952
|
fn fs_copy(&self, src: String, dst: String) -> Result<(), Error> {
|
|
920
|
-
let
|
|
953
|
+
let inner = self.inner.get()?;
|
|
954
|
+
let fs = inner.fs();
|
|
921
955
|
block_on(fs.copy(&src, &dst))?.map_err(error::to_ruby)
|
|
922
956
|
}
|
|
923
957
|
|
|
924
958
|
fn fs_rename(&self, src: String, dst: String) -> Result<(), Error> {
|
|
925
|
-
let
|
|
959
|
+
let inner = self.inner.get()?;
|
|
960
|
+
let fs = inner.fs();
|
|
926
961
|
block_on(fs.rename(&src, &dst))?.map_err(error::to_ruby)
|
|
927
962
|
}
|
|
928
963
|
|
|
929
964
|
fn fs_exists(&self, path: String) -> Result<bool, Error> {
|
|
930
|
-
let
|
|
965
|
+
let inner = self.inner.get()?;
|
|
966
|
+
let fs = inner.fs();
|
|
931
967
|
block_on(fs.exists(&path))?.map_err(error::to_ruby)
|
|
932
968
|
}
|
|
933
969
|
|
|
934
970
|
fn fs_stat(&self, path: String) -> Result<RHash, Error> {
|
|
935
|
-
let
|
|
971
|
+
let inner = self.inner.get()?;
|
|
972
|
+
let fs = inner.fs();
|
|
936
973
|
let meta = block_on(fs.stat(&path))?.map_err(error::to_ruby)?;
|
|
937
974
|
Ok(fs_metadata_to_hash(&meta))
|
|
938
975
|
}
|
|
939
976
|
|
|
940
977
|
fn fs_copy_from_host(&self, host_path: String, guest_path: String) -> Result<(), Error> {
|
|
941
|
-
let
|
|
978
|
+
let inner = self.inner.get()?;
|
|
979
|
+
let fs = inner.fs();
|
|
942
980
|
block_on(fs.copy_from_host(&host_path, &guest_path))?.map_err(error::to_ruby)
|
|
943
981
|
}
|
|
944
982
|
|
|
945
983
|
fn fs_copy_to_host(&self, guest_path: String, host_path: String) -> Result<(), Error> {
|
|
946
|
-
let
|
|
984
|
+
let inner = self.inner.get()?;
|
|
985
|
+
let fs = inner.fs();
|
|
947
986
|
block_on(fs.copy_to_host(&guest_path, &host_path))?.map_err(error::to_ruby)
|
|
948
987
|
}
|
|
949
988
|
|
|
950
989
|
/// Open a streaming reader over a guest file (for files too large to buffer).
|
|
951
990
|
fn fs_read_stream(&self, path: String) -> Result<crate::fs_stream::FsReadStreamHandle, Error> {
|
|
952
|
-
let
|
|
991
|
+
let inner = self.inner.get()?;
|
|
992
|
+
let fs = inner.fs();
|
|
953
993
|
let stream = block_on(fs.read_stream(&path))?.map_err(error::to_ruby)?;
|
|
954
994
|
Ok(crate::fs_stream::FsReadStreamHandle::new(stream))
|
|
955
995
|
}
|
|
956
996
|
|
|
957
997
|
/// Open a streaming writer to a guest file.
|
|
958
998
|
fn fs_write_stream(&self, path: String) -> Result<crate::fs_stream::FsWriteSinkHandle, Error> {
|
|
959
|
-
let
|
|
999
|
+
let inner = self.inner.get()?;
|
|
1000
|
+
let fs = inner.fs();
|
|
960
1001
|
let sink = block_on(fs.write_stream(&path))?.map_err(error::to_ruby)?;
|
|
961
1002
|
Ok(crate::fs_stream::FsWriteSinkHandle::new(sink))
|
|
962
1003
|
}
|
|
@@ -968,13 +1009,14 @@ impl Sandbox {
|
|
|
968
1009
|
/// Open a native in-process SSH client to this sandbox. `opts`: user, term,
|
|
969
1010
|
/// sftp (bool, default true), inactivity_timeout (seconds, f64; 0 disables).
|
|
970
1011
|
fn ssh_open_client(&self, opts: RHash) -> Result<crate::ssh::SshClient, Error> {
|
|
1012
|
+
let inner = self.inner.get()?;
|
|
971
1013
|
let user = conv::opt_string(opts, "user")?;
|
|
972
1014
|
let term = conv::opt_string(opts, "term")?;
|
|
973
1015
|
let sftp = conv::opt::<bool>(opts, "sftp")?.unwrap_or(true);
|
|
974
1016
|
let inactivity_timeout = conv::opt_f64(opts, "inactivity_timeout")?
|
|
975
1017
|
.map(secs_to_duration)
|
|
976
1018
|
.transpose()?;
|
|
977
|
-
let ssh =
|
|
1019
|
+
let ssh = inner.ssh();
|
|
978
1020
|
let client = block_on(ssh.open_client_with(move |mut b| {
|
|
979
1021
|
if let Some(u) = user {
|
|
980
1022
|
b = b.user(u);
|
|
@@ -995,6 +1037,7 @@ impl Sandbox {
|
|
|
995
1037
|
/// authorized_keys_path, user, sftp (bool, default true),
|
|
996
1038
|
/// inactivity_timeout (seconds, f64; 0 disables).
|
|
997
1039
|
fn ssh_prepare_server(&self, opts: RHash) -> Result<crate::ssh::SshServer, Error> {
|
|
1040
|
+
let inner = self.inner.get()?;
|
|
998
1041
|
let host_key_path = conv::opt_string(opts, "host_key_path")?;
|
|
999
1042
|
let authorized_keys_path = conv::opt_string(opts, "authorized_keys_path")?;
|
|
1000
1043
|
let user = conv::opt_string(opts, "user")?;
|
|
@@ -1002,7 +1045,7 @@ impl Sandbox {
|
|
|
1002
1045
|
let inactivity_timeout = conv::opt_f64(opts, "inactivity_timeout")?
|
|
1003
1046
|
.map(secs_to_duration)
|
|
1004
1047
|
.transpose()?;
|
|
1005
|
-
let ssh =
|
|
1048
|
+
let ssh = inner.ssh();
|
|
1006
1049
|
let server = block_on(ssh.prepare_server_with(move |mut b| {
|
|
1007
1050
|
if let Some(p) = host_key_path {
|
|
1008
1051
|
b = b.host_key_path(p);
|
|
@@ -1031,21 +1074,24 @@ impl Sandbox {
|
|
|
1031
1074
|
/// blocks until the command exits or the detach sequence is typed. `opts`:
|
|
1032
1075
|
/// cwd, user, env, detach_keys, rlimits.
|
|
1033
1076
|
fn attach(&self, cmd: String, args: Vec<String>, opts: RHash) -> Result<i32, Error> {
|
|
1077
|
+
let inner = self.inner.get()?;
|
|
1034
1078
|
let parsed = AttachOpts::parse(args, opts)?;
|
|
1035
|
-
block_on(
|
|
1079
|
+
block_on(inner.attach_with(cmd, move |b| parsed.apply(b)))?.map_err(error::to_ruby)
|
|
1036
1080
|
}
|
|
1037
1081
|
|
|
1038
1082
|
/// Attach an interactive terminal running the sandbox's default shell.
|
|
1039
1083
|
fn attach_shell(&self) -> Result<i32, Error> {
|
|
1040
|
-
|
|
1084
|
+
let inner = self.inner.get()?;
|
|
1085
|
+
block_on(inner.attach_shell())?.map_err(error::to_ruby)
|
|
1041
1086
|
}
|
|
1042
1087
|
|
|
1043
1088
|
/// Attach an interactive terminal to the image's resolved OCI ENTRYPOINT
|
|
1044
1089
|
/// and CMD (the default workload, v0.6.9); returns its exit code. `opts`:
|
|
1045
1090
|
/// cwd, user, env, detach_keys, rlimits.
|
|
1046
1091
|
fn attach_default(&self, opts: RHash) -> Result<i32, Error> {
|
|
1092
|
+
let inner = self.inner.get()?;
|
|
1047
1093
|
let parsed = AttachOpts::parse(Vec::new(), opts)?;
|
|
1048
|
-
block_on(
|
|
1094
|
+
block_on(inner.attach_default_with(move |b| parsed.apply(b)))?.map_err(error::to_ruby)
|
|
1049
1095
|
}
|
|
1050
1096
|
}
|
|
1051
1097
|
|
|
@@ -2583,6 +2629,12 @@ fn pull_progress_to_hash(p: &PullProgress) -> RHash {
|
|
|
2583
2629
|
/// handle is consumed once, so both sit behind a `tokio::Mutex`.
|
|
2584
2630
|
#[magnus::wrap(class = "Microsandbox::Native::PullSession", free_immediately, size)]
|
|
2585
2631
|
pub struct PullSession {
|
|
2632
|
+
inner: ForkGuarded<PullParts>,
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
/// The two halves of a pull session; both belong to the creating process's
|
|
2636
|
+
/// runtime (a channel fed by, and the join handle of, a task spawned on it).
|
|
2637
|
+
struct PullParts {
|
|
2586
2638
|
progress: Arc<Mutex<PullProgressHandle>>,
|
|
2587
2639
|
join: Arc<Mutex<Option<JoinHandle<MicrosandboxResult<microsandbox::Sandbox>>>>>,
|
|
2588
2640
|
}
|
|
@@ -2593,14 +2645,19 @@ impl PullSession {
|
|
|
2593
2645
|
join: JoinHandle<MicrosandboxResult<microsandbox::Sandbox>>,
|
|
2594
2646
|
) -> Self {
|
|
2595
2647
|
Self {
|
|
2596
|
-
|
|
2597
|
-
|
|
2648
|
+
inner: ForkGuarded::new(
|
|
2649
|
+
"Microsandbox::PullSession",
|
|
2650
|
+
PullParts {
|
|
2651
|
+
progress: Arc::new(Mutex::new(progress)),
|
|
2652
|
+
join: Arc::new(Mutex::new(Some(join))),
|
|
2653
|
+
},
|
|
2654
|
+
),
|
|
2598
2655
|
}
|
|
2599
2656
|
}
|
|
2600
2657
|
|
|
2601
2658
|
/// Next progress event as a Hash, or nil when the pull is finished.
|
|
2602
2659
|
fn recv(&self) -> Result<Option<RHash>, Error> {
|
|
2603
|
-
let progress = Arc::clone(&self.progress);
|
|
2660
|
+
let progress = Arc::clone(&self.inner.get()?.progress);
|
|
2604
2661
|
let event = block_on(async move { progress.lock().await.recv().await })?;
|
|
2605
2662
|
Ok(event.map(|p| pull_progress_to_hash(&p)))
|
|
2606
2663
|
}
|
|
@@ -2608,7 +2665,7 @@ impl PullSession {
|
|
|
2608
2665
|
/// Await the booted sandbox. Call after draining `recv` (it joins the
|
|
2609
2666
|
/// create task). Consumes the join handle, so it is callable only once.
|
|
2610
2667
|
fn result(&self) -> Result<Sandbox, Error> {
|
|
2611
|
-
let join = Arc::clone(&self.join);
|
|
2668
|
+
let join = Arc::clone(&self.inner.get()?.join);
|
|
2612
2669
|
// Take the join handle AND await it in ONE `block_on`. `block_on`
|
|
2613
2670
|
// returns `Err` when an interrupt is delivered before it starts; as two
|
|
2614
2671
|
// calls, an interrupt between them would drop the taken `JoinHandle`
|
|
@@ -2640,83 +2697,102 @@ impl PullSession {
|
|
|
2640
2697
|
/// a synchronous snapshot read off the handle (no round-trip).
|
|
2641
2698
|
#[magnus::wrap(class = "Microsandbox::Native::SandboxHandle", free_immediately, size)]
|
|
2642
2699
|
pub struct SbHandle {
|
|
2643
|
-
inner: SandboxHandle
|
|
2700
|
+
inner: ForkGuarded<SandboxHandle>,
|
|
2644
2701
|
}
|
|
2645
2702
|
|
|
2646
2703
|
impl SbHandle {
|
|
2647
2704
|
fn from_inner(inner: SandboxHandle) -> Self {
|
|
2648
|
-
Self {
|
|
2705
|
+
Self {
|
|
2706
|
+
inner: ForkGuarded::new("Microsandbox::SandboxHandle", inner),
|
|
2707
|
+
}
|
|
2649
2708
|
}
|
|
2650
2709
|
|
|
2710
|
+
/// Plain data — like every other snapshot accessor here (`id`, `status`,
|
|
2711
|
+
/// the timestamps, `config_json`) it stays readable in a forked child
|
|
2712
|
+
/// (`ForkGuarded::peek`); the lifecycle operations do not.
|
|
2651
2713
|
fn name(&self) -> String {
|
|
2652
|
-
self.inner.name().to_string()
|
|
2714
|
+
self.inner.peek().name().to_string()
|
|
2653
2715
|
}
|
|
2654
2716
|
|
|
2655
2717
|
/// The opaque, backend-assigned identity of the persisted sandbox this
|
|
2656
2718
|
/// handle is bound to (v0.6.16). The lifecycle operations below refuse to
|
|
2657
2719
|
/// act on a same-name replacement by comparing against it.
|
|
2658
2720
|
fn id(&self) -> String {
|
|
2659
|
-
self.inner.id().to_string()
|
|
2721
|
+
self.inner.peek().id().to_string()
|
|
2660
2722
|
}
|
|
2661
2723
|
|
|
2662
2724
|
/// Status snapshot captured when the handle was fetched (synchronous).
|
|
2663
2725
|
fn status(&self) -> String {
|
|
2664
|
-
sandbox_status_str(self.inner.status_snapshot()).to_string()
|
|
2726
|
+
sandbox_status_str(self.inner.peek().status_snapshot()).to_string()
|
|
2665
2727
|
}
|
|
2666
2728
|
|
|
2667
2729
|
fn created_at_ms(&self) -> Option<i64> {
|
|
2668
|
-
self.inner
|
|
2730
|
+
self.inner
|
|
2731
|
+
.peek()
|
|
2732
|
+
.created_at()
|
|
2733
|
+
.map(|dt| dt.timestamp_millis())
|
|
2669
2734
|
}
|
|
2670
2735
|
|
|
2671
2736
|
fn updated_at_ms(&self) -> Option<i64> {
|
|
2672
|
-
self.inner
|
|
2737
|
+
self.inner
|
|
2738
|
+
.peek()
|
|
2739
|
+
.updated_at()
|
|
2740
|
+
.map(|dt| dt.timestamp_millis())
|
|
2673
2741
|
}
|
|
2674
2742
|
|
|
2675
2743
|
/// Graceful stop (SIGTERM→SIGKILL escalation, 10s default) and wait.
|
|
2676
2744
|
fn stop(&self) -> Result<(), Error> {
|
|
2677
|
-
|
|
2745
|
+
let inner = self.inner.get()?;
|
|
2746
|
+
block_on(inner.stop())?.map_err(error::to_ruby)
|
|
2678
2747
|
}
|
|
2679
2748
|
|
|
2680
2749
|
/// Graceful stop with a custom escalation timeout (seconds).
|
|
2681
2750
|
fn stop_with_timeout(&self, secs: f64) -> Result<(), Error> {
|
|
2682
|
-
|
|
2751
|
+
let inner = self.inner.get()?;
|
|
2752
|
+
block_on(inner.stop_with_timeout(secs_to_duration(secs)?))?.map_err(error::to_ruby)
|
|
2683
2753
|
}
|
|
2684
2754
|
|
|
2685
2755
|
/// Force kill (SIGKILL) and wait.
|
|
2686
2756
|
fn kill(&self) -> Result<(), Error> {
|
|
2687
|
-
|
|
2757
|
+
let inner = self.inner.get()?;
|
|
2758
|
+
block_on(inner.kill())?.map_err(error::to_ruby)
|
|
2688
2759
|
}
|
|
2689
2760
|
|
|
2690
2761
|
/// Force kill, waiting up to `secs` for the process to disappear.
|
|
2691
2762
|
fn kill_with_timeout(&self, secs: f64) -> Result<(), Error> {
|
|
2692
|
-
|
|
2763
|
+
let inner = self.inner.get()?;
|
|
2764
|
+
block_on(inner.kill_with_timeout(secs_to_duration(secs)?))?.map_err(error::to_ruby)
|
|
2693
2765
|
}
|
|
2694
2766
|
|
|
2695
2767
|
/// Send the graceful-shutdown request and return without waiting.
|
|
2696
2768
|
fn request_stop(&self) -> Result<(), Error> {
|
|
2697
|
-
|
|
2769
|
+
let inner = self.inner.get()?;
|
|
2770
|
+
block_on(inner.request_stop())?.map_err(error::to_ruby)
|
|
2698
2771
|
}
|
|
2699
2772
|
|
|
2700
2773
|
/// Send the force-kill request and return without waiting.
|
|
2701
2774
|
fn request_kill(&self) -> Result<(), Error> {
|
|
2702
|
-
|
|
2775
|
+
let inner = self.inner.get()?;
|
|
2776
|
+
block_on(inner.request_kill())?.map_err(error::to_ruby)
|
|
2703
2777
|
}
|
|
2704
2778
|
|
|
2705
2779
|
/// Send the drain request (SIGUSR1) and return without waiting.
|
|
2706
2780
|
fn request_drain(&self) -> Result<(), Error> {
|
|
2707
|
-
|
|
2781
|
+
let inner = self.inner.get()?;
|
|
2782
|
+
block_on(inner.request_drain())?.map_err(error::to_ruby)
|
|
2708
2783
|
}
|
|
2709
2784
|
|
|
2710
2785
|
/// Connect when this exact sandbox is running, wait while it is starting,
|
|
2711
2786
|
/// or start it when it is created/stopped/crashed. `opts` carries
|
|
2712
2787
|
/// `detached`, which applies only when a start is actually required.
|
|
2713
2788
|
fn connect_or_start(&self, opts: RHash) -> Result<Sandbox, Error> {
|
|
2789
|
+
let inner = self.inner.get()?;
|
|
2714
2790
|
let detached = conv::opt_bool(opts, "detached")?;
|
|
2715
2791
|
let inner = block_on(async {
|
|
2716
2792
|
if detached {
|
|
2717
|
-
|
|
2793
|
+
inner.connect_or_start_detached().await
|
|
2718
2794
|
} else {
|
|
2719
|
-
|
|
2795
|
+
inner.connect_or_start().await
|
|
2720
2796
|
}
|
|
2721
2797
|
})?
|
|
2722
2798
|
.map_err(error::to_ruby)?;
|
|
@@ -2727,29 +2803,33 @@ impl SbHandle {
|
|
|
2727
2803
|
/// returning a fresh handle. A same-name replacement raises
|
|
2728
2804
|
/// `SandboxReplacedError` instead of silently redirecting the wait.
|
|
2729
2805
|
fn wait_for_status(&self, status: String) -> Result<SbHandle, Error> {
|
|
2806
|
+
let inner = self.inner.get()?;
|
|
2730
2807
|
let status = sandbox_status_from_str(&status)?;
|
|
2731
|
-
let handle = block_on(
|
|
2808
|
+
let handle = block_on(inner.wait_for_status(status))?.map_err(error::to_ruby)?;
|
|
2732
2809
|
Ok(SbHandle::from_inner(handle))
|
|
2733
2810
|
}
|
|
2734
2811
|
|
|
2735
2812
|
/// Stop and start this exact sandbox, returning the new live sandbox.
|
|
2736
2813
|
/// `opts` carries `force`/`timeout`/`detached`.
|
|
2737
2814
|
fn restart(&self, opts: RHash) -> Result<Sandbox, Error> {
|
|
2815
|
+
let inner = self.inner.get()?;
|
|
2738
2816
|
let options = restart_options(opts)?;
|
|
2739
|
-
let inner = block_on(
|
|
2817
|
+
let inner = block_on(inner.restart_with(options))?.map_err(error::to_ruby)?;
|
|
2740
2818
|
Ok(Sandbox::from_inner(inner))
|
|
2741
2819
|
}
|
|
2742
2820
|
|
|
2743
2821
|
/// Stop and remove this exact sandbox. `opts` carries `force`/`timeout`.
|
|
2744
2822
|
fn destroy(&self, opts: RHash) -> Result<(), Error> {
|
|
2823
|
+
let inner = self.inner.get()?;
|
|
2745
2824
|
let options = destroy_options(opts)?;
|
|
2746
|
-
block_on(
|
|
2825
|
+
block_on(inner.destroy_with(options))?.map_err(error::to_ruby)
|
|
2747
2826
|
}
|
|
2748
2827
|
|
|
2749
2828
|
/// Block until the sandbox reaches a terminal state; returns a stop-result
|
|
2750
2829
|
/// Hash (name, status, exit_code, signal, observed_at_ms, source).
|
|
2751
2830
|
fn wait_until_stopped(&self) -> Result<RHash, Error> {
|
|
2752
|
-
let
|
|
2831
|
+
let inner = self.inner.get()?;
|
|
2832
|
+
let result = block_on(inner.wait_until_stopped())?.map_err(error::to_ruby)?;
|
|
2753
2833
|
Ok(stop_result_to_hash(&result))
|
|
2754
2834
|
}
|
|
2755
2835
|
|
|
@@ -2757,7 +2837,7 @@ impl SbHandle {
|
|
|
2757
2837
|
/// handle already carries it, no runtime round-trip). The Ruby layer parses
|
|
2758
2838
|
/// it into a Hash for `#config`. Mirrors the Python/Node `config_json`.
|
|
2759
2839
|
fn config_json(&self) -> String {
|
|
2760
|
-
self.inner.config_json().to_string()
|
|
2840
|
+
self.inner.peek().config_json().to_string()
|
|
2761
2841
|
}
|
|
2762
2842
|
|
|
2763
2843
|
/// Snapshot this (stopped) sandbox under a bare name (resolved under the
|
|
@@ -2766,7 +2846,8 @@ impl SbHandle {
|
|
|
2766
2846
|
/// (`snapshot_to` was removed upstream in v0.6.7 — use `Snapshot.create`
|
|
2767
2847
|
/// with `dest_dir:` for explicit placement.)
|
|
2768
2848
|
fn snapshot(&self, name: String) -> Result<RHash, Error> {
|
|
2769
|
-
let
|
|
2849
|
+
let inner = self.inner.get()?;
|
|
2850
|
+
let snap = block_on(inner.snapshot(&name))?.map_err(error::to_ruby)?;
|
|
2770
2851
|
Ok(crate::snapshot::snapshot_to_hash(&snap))
|
|
2771
2852
|
}
|
|
2772
2853
|
|
|
@@ -2774,21 +2855,24 @@ impl SbHandle {
|
|
|
2774
2855
|
/// sandbox raises `SandboxNotRunningError` (the handle does not start it
|
|
2775
2856
|
/// implicitly). Returns `{ name, latency_secs }`.
|
|
2776
2857
|
fn ping(&self) -> Result<RHash, Error> {
|
|
2777
|
-
let
|
|
2858
|
+
let inner = self.inner.get()?;
|
|
2859
|
+
let result = block_on(inner.ping())?.map_err(error::to_ruby)?;
|
|
2778
2860
|
Ok(ping_result_to_hash(&result))
|
|
2779
2861
|
}
|
|
2780
2862
|
|
|
2781
2863
|
/// Explicitly refresh the idle-activity timer. A stopped sandbox raises
|
|
2782
2864
|
/// `SandboxNotRunningError`. Returns `{ name, activity_seq }`.
|
|
2783
2865
|
fn touch(&self) -> Result<RHash, Error> {
|
|
2784
|
-
let
|
|
2866
|
+
let inner = self.inner.get()?;
|
|
2867
|
+
let result = block_on(inner.touch())?.map_err(error::to_ruby)?;
|
|
2785
2868
|
Ok(touch_result_to_hash(&result))
|
|
2786
2869
|
}
|
|
2787
2870
|
|
|
2788
2871
|
/// Plan or apply a live sandbox modification (see `Sandbox::modify`). Returns
|
|
2789
2872
|
/// the `SandboxModificationPlan` as a JSON string.
|
|
2790
2873
|
fn modify(&self, opts: RHash) -> Result<String, Error> {
|
|
2791
|
-
|
|
2874
|
+
let inner = self.inner.get()?;
|
|
2875
|
+
run_modify(inner.modify(), opts)
|
|
2792
2876
|
}
|
|
2793
2877
|
}
|
|
2794
2878
|
|