microsandbox-rb 0.17.1 → 0.17.2

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.
@@ -546,7 +546,7 @@ impl Sandbox {
546
546
  /// Create and boot a sandbox. `opts` is a string-keyed options Hash.
547
547
  fn create(name: String, opts: RHash) -> Result<Sandbox, Error> {
548
548
  let b = Self::build_builder(name, opts)?;
549
- let inner = block_on(b.create()).map_err(error::to_ruby)?;
549
+ let inner = block_on(b.create())?.map_err(error::to_ruby)?;
550
550
  Ok(Sandbox::from_inner(inner))
551
551
  }
552
552
 
@@ -559,7 +559,7 @@ impl Sandbox {
559
559
  // `create_with_pull_progress` spawns a tokio task, so it must run inside
560
560
  // the runtime context even though the call itself is synchronous.
561
561
  let (handle, join) =
562
- block_on(async move { b.create_with_pull_progress() }).map_err(error::to_ruby)?;
562
+ block_on(async move { b.create_with_pull_progress() })?.map_err(error::to_ruby)?;
563
563
  Ok(PullSession::new(handle, join))
564
564
  }
565
565
 
@@ -569,7 +569,7 @@ impl Sandbox {
569
569
  /// the combination with `replace`.
570
570
  fn connect_or_create(name: String, opts: RHash) -> Result<Sandbox, Error> {
571
571
  let b = Self::build_builder(name, opts)?;
572
- let inner = block_on(b.connect_or_create()).map_err(error::to_ruby)?;
572
+ let inner = block_on(b.connect_or_create())?.map_err(error::to_ruby)?;
573
573
  Ok(Sandbox::from_inner(inner))
574
574
  }
575
575
 
@@ -577,9 +577,9 @@ impl Sandbox {
577
577
  fn start(name: String, opts: RHash) -> Result<Sandbox, Error> {
578
578
  let detached = conv::opt_bool(opts, "detached")?;
579
579
  let inner = if detached {
580
- block_on(microsandbox::Sandbox::start_detached(&name)).map_err(error::to_ruby)?
580
+ block_on(microsandbox::Sandbox::start_detached(&name))?.map_err(error::to_ruby)?
581
581
  } else {
582
- block_on(microsandbox::Sandbox::start(&name)).map_err(error::to_ruby)?
582
+ block_on(microsandbox::Sandbox::start(&name))?.map_err(error::to_ruby)?
583
583
  };
584
584
  Ok(Sandbox::from_inner(inner))
585
585
  }
@@ -587,7 +587,7 @@ impl Sandbox {
587
587
  /// A controllable handle for a sandbox by name (running or not). Carries
588
588
  /// metadata accessors and the full lifecycle surface (see `SbHandle`).
589
589
  fn get(name: String) -> Result<SbHandle, Error> {
590
- let handle = block_on(microsandbox::Sandbox::get(&name)).map_err(error::to_ruby)?;
590
+ let handle = block_on(microsandbox::Sandbox::get(&name))?.map_err(error::to_ruby)?;
591
591
  Ok(SbHandle::from_inner(handle))
592
592
  }
593
593
 
@@ -607,7 +607,7 @@ impl Sandbox {
607
607
  /// First page of sandboxes (default page size), as a page Hash of
608
608
  /// controllable handles.
609
609
  fn list() -> Result<RHash, Error> {
610
- let page = block_on(microsandbox::Sandbox::list()).map_err(error::to_ruby)?;
610
+ let page = block_on(microsandbox::Sandbox::list())?.map_err(error::to_ruby)?;
611
611
  Self::page_to_hash(page)
612
612
  }
613
613
 
@@ -626,14 +626,14 @@ impl Sandbox {
626
626
  b = b.cursor(cursor);
627
627
  }
628
628
  b.labels(labels)
629
- }))
629
+ }))?
630
630
  .map_err(error::to_ruby)?;
631
631
  Self::page_to_hash(page)
632
632
  }
633
633
 
634
634
  /// Remove a (stopped) sandbox by name.
635
635
  fn remove(name: String) -> Result<(), Error> {
636
- block_on(microsandbox::Sandbox::remove(&name)).map_err(error::to_ruby)
636
+ block_on(microsandbox::Sandbox::remove(&name))?.map_err(error::to_ruby)
637
637
  }
638
638
 
639
639
  //----------------------------------------------------------------------
@@ -655,7 +655,7 @@ impl Sandbox {
655
655
  /// string-keyed Hash (cwd, user, env, timeout, tty, stdin).
656
656
  fn exec(&self, cmd: String, args: Vec<String>, opts: RHash) -> Result<RHash, Error> {
657
657
  let parsed = ExecOpts::parse(args, opts)?;
658
- let output = block_on(self.inner.exec_with(cmd, move |b| parsed.apply(b)))
658
+ let output = block_on(self.inner.exec_with(cmd, move |b| parsed.apply(b)))?
659
659
  .map_err(error::to_ruby)?;
660
660
  exec_output_to_hash(output)
661
661
  }
@@ -663,7 +663,7 @@ impl Sandbox {
663
663
  /// Run a shell script (pipes/redirects allowed).
664
664
  fn shell(&self, script: String, opts: RHash) -> Result<RHash, Error> {
665
665
  let parsed = ExecOpts::parse(Vec::new(), opts)?;
666
- let output = block_on(self.inner.shell_with(script, move |b| parsed.apply(b)))
666
+ let output = block_on(self.inner.shell_with(script, move |b| parsed.apply(b)))?
667
667
  .map_err(error::to_ruby)?;
668
668
  exec_output_to_hash(output)
669
669
  }
@@ -676,7 +676,7 @@ impl Sandbox {
676
676
  opts: RHash,
677
677
  ) -> Result<ExecHandle, Error> {
678
678
  let parsed = ExecOpts::parse(args, opts)?;
679
- let handle = block_on(self.inner.exec_stream_with(cmd, move |b| parsed.apply(b)))
679
+ let handle = block_on(self.inner.exec_stream_with(cmd, move |b| parsed.apply(b)))?
680
680
  .map_err(error::to_ruby)?;
681
681
  Ok(ExecHandle::from_core(handle))
682
682
  }
@@ -688,7 +688,7 @@ impl Sandbox {
688
688
  /// NoDefaultCommandError when the image resolves no executable command.
689
689
  fn exec_default(&self, opts: RHash) -> Result<RHash, Error> {
690
690
  let parsed = ExecOpts::parse(Vec::new(), opts)?;
691
- let output = block_on(self.inner.exec_default_with(move |b| parsed.apply(b)))
691
+ let output = block_on(self.inner.exec_default_with(move |b| parsed.apply(b)))?
692
692
  .map_err(error::to_ruby)?;
693
693
  exec_output_to_hash(output)
694
694
  }
@@ -699,7 +699,7 @@ impl Sandbox {
699
699
  let handle = block_on(
700
700
  self.inner
701
701
  .exec_default_stream_with(move |b| parsed.apply(b)),
702
- )
702
+ )?
703
703
  .map_err(error::to_ruby)?;
704
704
  Ok(ExecHandle::from_core(handle))
705
705
  }
@@ -710,7 +710,7 @@ impl Sandbox {
710
710
  let handle = block_on(
711
711
  self.inner
712
712
  .shell_stream_with(script, move |b| parsed.apply(b)),
713
- )
713
+ )?
714
714
  .map_err(error::to_ruby)?;
715
715
  Ok(ExecHandle::from_core(handle))
716
716
  }
@@ -731,29 +731,29 @@ impl Sandbox {
731
731
  /// Fine-grained control — a custom timeout or fire-and-return `request_*` —
732
732
  /// lives on `SandboxHandle`, obtained via `Sandbox.get`.
733
733
  fn stop(&self) -> Result<(), Error> {
734
- block_on(self.inner.stop()).map_err(error::to_ruby)
734
+ block_on(self.inner.stop())?.map_err(error::to_ruby)
735
735
  }
736
736
 
737
737
  /// Graceful stop, then wait for the process to exit. Returns an exit-status
738
738
  /// Hash (`exit_code`, `success`). Local backend only.
739
739
  fn stop_and_wait(&self) -> Result<RHash, Error> {
740
- let status = block_on(self.inner.stop_and_wait()).map_err(error::to_ruby)?;
740
+ let status = block_on(self.inner.stop_and_wait())?.map_err(error::to_ruby)?;
741
741
  Ok(exit_status_to_hash(status))
742
742
  }
743
743
 
744
744
  /// Force kill (SIGKILL).
745
745
  fn kill(&self) -> Result<(), Error> {
746
- block_on(self.inner.kill()).map_err(error::to_ruby)
746
+ block_on(self.inner.kill())?.map_err(error::to_ruby)
747
747
  }
748
748
 
749
749
  /// Trigger a graceful drain (SIGUSR1 on local).
750
750
  fn drain(&self) -> Result<(), Error> {
751
- block_on(self.inner.drain()).map_err(error::to_ruby)
751
+ block_on(self.inner.drain())?.map_err(error::to_ruby)
752
752
  }
753
753
 
754
754
  /// Wait for the process to exit. Returns an exit-status Hash. Local only.
755
755
  fn wait(&self) -> Result<RHash, Error> {
756
- let status = block_on(self.inner.wait()).map_err(error::to_ruby)?;
756
+ let status = block_on(self.inner.wait())?.map_err(error::to_ruby)?;
757
757
  Ok(exit_status_to_hash(status))
758
758
  }
759
759
 
@@ -762,7 +762,7 @@ impl Sandbox {
762
762
  /// `SandboxReplacedError` instead of silently redirecting the wait.
763
763
  fn wait_for_status(&self, status: String) -> Result<SbHandle, Error> {
764
764
  let status = sandbox_status_from_str(&status)?;
765
- let handle = block_on(self.inner.wait_for_status(status)).map_err(error::to_ruby)?;
765
+ let handle = block_on(self.inner.wait_for_status(status))?.map_err(error::to_ruby)?;
766
766
  Ok(SbHandle::from_inner(handle))
767
767
  }
768
768
 
@@ -770,19 +770,19 @@ impl Sandbox {
770
770
  /// `opts` carries `force`/`timeout`/`detached`.
771
771
  fn restart(&self, opts: RHash) -> Result<Sandbox, Error> {
772
772
  let options = restart_options(opts)?;
773
- let inner = block_on(self.inner.restart_with(options)).map_err(error::to_ruby)?;
773
+ let inner = block_on(self.inner.restart_with(options))?.map_err(error::to_ruby)?;
774
774
  Ok(Sandbox::from_inner(inner))
775
775
  }
776
776
 
777
777
  /// Stop and remove this exact sandbox. `opts` carries `force`/`timeout`.
778
778
  fn destroy(&self, opts: RHash) -> Result<(), Error> {
779
779
  let options = destroy_options(opts)?;
780
- block_on(self.inner.destroy_with(options)).map_err(error::to_ruby)
780
+ block_on(self.inner.destroy_with(options))?.map_err(error::to_ruby)
781
781
  }
782
782
 
783
783
  /// Live status fetched from the backend (a round-trip per call).
784
784
  fn status(&self) -> Result<String, Error> {
785
- let status = block_on(self.inner.status()).map_err(error::to_ruby)?;
785
+ let status = block_on(self.inner.status())?.map_err(error::to_ruby)?;
786
786
  Ok(sandbox_status_str(status).to_string())
787
787
  }
788
788
 
@@ -798,13 +798,13 @@ impl Sandbox {
798
798
  /// detaching a clone disarms the shared handle just the same.
799
799
  fn detach(&self) -> Result<(), Error> {
800
800
  let inner = self.inner.clone();
801
- block_on(inner.detach());
801
+ block_on(inner.detach())?;
802
802
  Ok(())
803
803
  }
804
804
 
805
805
  /// Latest metrics snapshot as a Hash.
806
806
  fn metrics(&self) -> Result<RHash, Error> {
807
- let m = block_on(self.inner.metrics()).map_err(error::to_ruby)?;
807
+ let m = block_on(self.inner.metrics())?.map_err(error::to_ruby)?;
808
808
  Ok(metrics_to_hash(&m))
809
809
  }
810
810
 
@@ -816,14 +816,14 @@ impl Sandbox {
816
816
  /// `{ name, latency_secs }` (round-trip latency as f64 seconds; the Ruby
817
817
  /// layer exposes both seconds and milliseconds).
818
818
  fn ping(&self) -> Result<RHash, Error> {
819
- let result = block_on(self.inner.ping()).map_err(error::to_ruby)?;
819
+ let result = block_on(self.inner.ping())?.map_err(error::to_ruby)?;
820
820
  Ok(ping_result_to_hash(&result))
821
821
  }
822
822
 
823
823
  /// Explicitly refresh the idle-activity timer. Returns
824
824
  /// `{ name, activity_seq }`.
825
825
  fn touch(&self) -> Result<RHash, Error> {
826
- let result = block_on(self.inner.touch()).map_err(error::to_ruby)?;
826
+ let result = block_on(self.inner.touch())?.map_err(error::to_ruby)?;
827
827
  Ok(touch_result_to_hash(&result))
828
828
  }
829
829
 
@@ -840,7 +840,7 @@ impl Sandbox {
840
840
  /// until_ms, sources (Array of "stdout"/"stderr"/"output"/"system"/"all").
841
841
  fn logs(&self, opts: RHash) -> Result<RArray, Error> {
842
842
  let log_opts = parse_log_options(opts)?;
843
- let entries = block_on(self.inner.logs(&log_opts)).map_err(error::to_ruby)?;
843
+ let entries = block_on(self.inner.logs(&log_opts))?.map_err(error::to_ruby)?;
844
844
  rhash_array(entries.iter().map(log_entry_to_hash))
845
845
  }
846
846
 
@@ -860,7 +860,7 @@ impl Sandbox {
860
860
  // which panics ("no reactor running") unless constructed inside the
861
861
  // runtime context — so build it under `block_on`. (`log_stream` is async
862
862
  // and already runs inside `block_on`, so it needs no such wrapper.)
863
- let stream = block_on(async { self.inner.metrics_stream(dur) });
863
+ let stream = block_on(async { self.inner.metrics_stream(dur) })?;
864
864
  Ok(MetricsStream::from_stream(stream))
865
865
  }
866
866
 
@@ -868,7 +868,7 @@ impl Sandbox {
868
868
  /// from_cursor, until_ms, follow. Returns a LogStream.
869
869
  fn log_stream(&self, opts: RHash) -> Result<LogStream, Error> {
870
870
  let log_opts = parse_log_stream_options(opts)?;
871
- let stream = block_on(self.inner.log_stream(&log_opts)).map_err(error::to_ruby)?;
871
+ let stream = block_on(self.inner.log_stream(&log_opts))?.map_err(error::to_ruby)?;
872
872
  Ok(LogStream::from_stream(stream))
873
873
  }
874
874
 
@@ -878,13 +878,13 @@ impl Sandbox {
878
878
 
879
879
  fn fs_read(&self, path: String) -> Result<RString, Error> {
880
880
  let fs = self.inner.fs();
881
- let bytes = block_on(fs.read(&path)).map_err(error::to_ruby)?;
881
+ let bytes = block_on(fs.read(&path))?.map_err(error::to_ruby)?;
882
882
  Ok(ruby().str_from_slice(bytes.as_ref()))
883
883
  }
884
884
 
885
885
  fn fs_read_text(&self, path: String) -> Result<String, Error> {
886
886
  let fs = self.inner.fs();
887
- block_on(fs.read_to_string(&path)).map_err(error::to_ruby)
887
+ block_on(fs.read_to_string(&path))?.map_err(error::to_ruby)
888
888
  }
889
889
 
890
890
  fn fs_write(&self, path: String, data: RString) -> Result<(), Error> {
@@ -892,72 +892,72 @@ impl Sandbox {
892
892
  // could be moved/freed by GC.compact once block_on releases it.
893
893
  let bytes = unsafe { data.as_slice() }.to_vec();
894
894
  let fs = self.inner.fs();
895
- block_on(fs.write(&path, &bytes)).map_err(error::to_ruby)
895
+ block_on(fs.write(&path, &bytes))?.map_err(error::to_ruby)
896
896
  }
897
897
 
898
898
  fn fs_list(&self, path: String) -> Result<RArray, Error> {
899
899
  let fs = self.inner.fs();
900
- let entries = block_on(fs.list(&path)).map_err(error::to_ruby)?;
900
+ let entries = block_on(fs.list(&path))?.map_err(error::to_ruby)?;
901
901
  rhash_array(entries.iter().map(fs_entry_to_hash))
902
902
  }
903
903
 
904
904
  fn fs_mkdir(&self, path: String) -> Result<(), Error> {
905
905
  let fs = self.inner.fs();
906
- block_on(fs.mkdir(&path)).map_err(error::to_ruby)
906
+ block_on(fs.mkdir(&path))?.map_err(error::to_ruby)
907
907
  }
908
908
 
909
909
  fn fs_remove(&self, path: String) -> Result<(), Error> {
910
910
  let fs = self.inner.fs();
911
- block_on(fs.remove(&path)).map_err(error::to_ruby)
911
+ block_on(fs.remove(&path))?.map_err(error::to_ruby)
912
912
  }
913
913
 
914
914
  fn fs_remove_dir(&self, path: String) -> Result<(), Error> {
915
915
  let fs = self.inner.fs();
916
- block_on(fs.remove_dir(&path)).map_err(error::to_ruby)
916
+ block_on(fs.remove_dir(&path))?.map_err(error::to_ruby)
917
917
  }
918
918
 
919
919
  fn fs_copy(&self, src: String, dst: String) -> Result<(), Error> {
920
920
  let fs = self.inner.fs();
921
- block_on(fs.copy(&src, &dst)).map_err(error::to_ruby)
921
+ block_on(fs.copy(&src, &dst))?.map_err(error::to_ruby)
922
922
  }
923
923
 
924
924
  fn fs_rename(&self, src: String, dst: String) -> Result<(), Error> {
925
925
  let fs = self.inner.fs();
926
- block_on(fs.rename(&src, &dst)).map_err(error::to_ruby)
926
+ block_on(fs.rename(&src, &dst))?.map_err(error::to_ruby)
927
927
  }
928
928
 
929
929
  fn fs_exists(&self, path: String) -> Result<bool, Error> {
930
930
  let fs = self.inner.fs();
931
- block_on(fs.exists(&path)).map_err(error::to_ruby)
931
+ block_on(fs.exists(&path))?.map_err(error::to_ruby)
932
932
  }
933
933
 
934
934
  fn fs_stat(&self, path: String) -> Result<RHash, Error> {
935
935
  let fs = self.inner.fs();
936
- let meta = block_on(fs.stat(&path)).map_err(error::to_ruby)?;
936
+ let meta = block_on(fs.stat(&path))?.map_err(error::to_ruby)?;
937
937
  Ok(fs_metadata_to_hash(&meta))
938
938
  }
939
939
 
940
940
  fn fs_copy_from_host(&self, host_path: String, guest_path: String) -> Result<(), Error> {
941
941
  let fs = self.inner.fs();
942
- block_on(fs.copy_from_host(&host_path, &guest_path)).map_err(error::to_ruby)
942
+ block_on(fs.copy_from_host(&host_path, &guest_path))?.map_err(error::to_ruby)
943
943
  }
944
944
 
945
945
  fn fs_copy_to_host(&self, guest_path: String, host_path: String) -> Result<(), Error> {
946
946
  let fs = self.inner.fs();
947
- block_on(fs.copy_to_host(&guest_path, &host_path)).map_err(error::to_ruby)
947
+ block_on(fs.copy_to_host(&guest_path, &host_path))?.map_err(error::to_ruby)
948
948
  }
949
949
 
950
950
  /// Open a streaming reader over a guest file (for files too large to buffer).
951
951
  fn fs_read_stream(&self, path: String) -> Result<crate::fs_stream::FsReadStreamHandle, Error> {
952
952
  let fs = self.inner.fs();
953
- let stream = block_on(fs.read_stream(&path)).map_err(error::to_ruby)?;
953
+ let stream = block_on(fs.read_stream(&path))?.map_err(error::to_ruby)?;
954
954
  Ok(crate::fs_stream::FsReadStreamHandle::new(stream))
955
955
  }
956
956
 
957
957
  /// Open a streaming writer to a guest file.
958
958
  fn fs_write_stream(&self, path: String) -> Result<crate::fs_stream::FsWriteSinkHandle, Error> {
959
959
  let fs = self.inner.fs();
960
- let sink = block_on(fs.write_stream(&path)).map_err(error::to_ruby)?;
960
+ let sink = block_on(fs.write_stream(&path))?.map_err(error::to_ruby)?;
961
961
  Ok(crate::fs_stream::FsWriteSinkHandle::new(sink))
962
962
  }
963
963
 
@@ -986,7 +986,7 @@ impl Sandbox {
986
986
  b = b.inactivity_timeout(t);
987
987
  }
988
988
  b.sftp(sftp)
989
- }))
989
+ }))?
990
990
  .map_err(error::to_ruby)?;
991
991
  Ok(crate::ssh::SshClient::from_core(client))
992
992
  }
@@ -1017,7 +1017,7 @@ impl Sandbox {
1017
1017
  b = b.inactivity_timeout(t);
1018
1018
  }
1019
1019
  b.sftp(sftp)
1020
- }))
1020
+ }))?
1021
1021
  .map_err(error::to_ruby)?;
1022
1022
  Ok(crate::ssh::SshServer::from_core(server))
1023
1023
  }
@@ -1032,12 +1032,12 @@ impl Sandbox {
1032
1032
  /// cwd, user, env, detach_keys, rlimits.
1033
1033
  fn attach(&self, cmd: String, args: Vec<String>, opts: RHash) -> Result<i32, Error> {
1034
1034
  let parsed = AttachOpts::parse(args, opts)?;
1035
- block_on(self.inner.attach_with(cmd, move |b| parsed.apply(b))).map_err(error::to_ruby)
1035
+ block_on(self.inner.attach_with(cmd, move |b| parsed.apply(b)))?.map_err(error::to_ruby)
1036
1036
  }
1037
1037
 
1038
1038
  /// Attach an interactive terminal running the sandbox's default shell.
1039
1039
  fn attach_shell(&self) -> Result<i32, Error> {
1040
- block_on(self.inner.attach_shell()).map_err(error::to_ruby)
1040
+ block_on(self.inner.attach_shell())?.map_err(error::to_ruby)
1041
1041
  }
1042
1042
 
1043
1043
  /// Attach an interactive terminal to the image's resolved OCI ENTRYPOINT
@@ -1045,7 +1045,7 @@ impl Sandbox {
1045
1045
  /// cwd, user, env, detach_keys, rlimits.
1046
1046
  fn attach_default(&self, opts: RHash) -> Result<i32, Error> {
1047
1047
  let parsed = AttachOpts::parse(Vec::new(), opts)?;
1048
- block_on(self.inner.attach_default_with(move |b| parsed.apply(b))).map_err(error::to_ruby)
1048
+ block_on(self.inner.attach_default_with(move |b| parsed.apply(b)))?.map_err(error::to_ruby)
1049
1049
  }
1050
1050
  }
1051
1051
 
@@ -2373,7 +2373,7 @@ fn run_modify(builder: SandboxModificationBuilder, opts: RHash) -> Result<String
2373
2373
  } else {
2374
2374
  builder.apply().await
2375
2375
  }
2376
- })
2376
+ })?
2377
2377
  .map_err(error::to_ruby)?;
2378
2378
  serde_json::to_string(&plan)
2379
2379
  .map_err(|e| error::base_error(format!("failed to serialize modification plan: {e}")))
@@ -2601,7 +2601,7 @@ impl PullSession {
2601
2601
  /// Next progress event as a Hash, or nil when the pull is finished.
2602
2602
  fn recv(&self) -> Result<Option<RHash>, Error> {
2603
2603
  let progress = Arc::clone(&self.progress);
2604
- let event = block_on(async move { progress.lock().await.recv().await });
2604
+ let event = block_on(async move { progress.lock().await.recv().await })?;
2605
2605
  Ok(event.map(|p| pull_progress_to_hash(&p)))
2606
2606
  }
2607
2607
 
@@ -2609,9 +2609,20 @@ impl PullSession {
2609
2609
  /// create task). Consumes the join handle, so it is callable only once.
2610
2610
  fn result(&self) -> Result<Sandbox, Error> {
2611
2611
  let join = Arc::clone(&self.join);
2612
- let taken = block_on(async move { join.lock().await.take() });
2613
- let handle = taken.ok_or_else(|| error::base_error("pull session result already taken"))?;
2614
- let inner = block_on(handle)
2612
+ // Take the join handle AND await it in ONE `block_on`. `block_on`
2613
+ // returns `Err` when an interrupt is delivered before it starts; as two
2614
+ // calls, an interrupt between them would drop the taken `JoinHandle`
2615
+ // (which detaches the create task rather than cancelling it) and with
2616
+ // it the only way to reach the sandbox that task boots.
2617
+ let joined = block_on(async move {
2618
+ let taken = join.lock().await.take();
2619
+ match taken {
2620
+ Some(handle) => Some(handle.await),
2621
+ None => None,
2622
+ }
2623
+ })?;
2624
+ let inner = joined
2625
+ .ok_or_else(|| error::base_error("pull session result already taken"))?
2615
2626
  .map_err(|e| error::base_error(format!("sandbox creation task failed: {e}")))?
2616
2627
  .map_err(error::to_ruby)?;
2617
2628
  Ok(Sandbox::from_inner(inner))
@@ -2663,37 +2674,37 @@ impl SbHandle {
2663
2674
 
2664
2675
  /// Graceful stop (SIGTERM→SIGKILL escalation, 10s default) and wait.
2665
2676
  fn stop(&self) -> Result<(), Error> {
2666
- block_on(self.inner.stop()).map_err(error::to_ruby)
2677
+ block_on(self.inner.stop())?.map_err(error::to_ruby)
2667
2678
  }
2668
2679
 
2669
2680
  /// Graceful stop with a custom escalation timeout (seconds).
2670
2681
  fn stop_with_timeout(&self, secs: f64) -> Result<(), Error> {
2671
- block_on(self.inner.stop_with_timeout(secs_to_duration(secs)?)).map_err(error::to_ruby)
2682
+ block_on(self.inner.stop_with_timeout(secs_to_duration(secs)?))?.map_err(error::to_ruby)
2672
2683
  }
2673
2684
 
2674
2685
  /// Force kill (SIGKILL) and wait.
2675
2686
  fn kill(&self) -> Result<(), Error> {
2676
- block_on(self.inner.kill()).map_err(error::to_ruby)
2687
+ block_on(self.inner.kill())?.map_err(error::to_ruby)
2677
2688
  }
2678
2689
 
2679
2690
  /// Force kill, waiting up to `secs` for the process to disappear.
2680
2691
  fn kill_with_timeout(&self, secs: f64) -> Result<(), Error> {
2681
- block_on(self.inner.kill_with_timeout(secs_to_duration(secs)?)).map_err(error::to_ruby)
2692
+ block_on(self.inner.kill_with_timeout(secs_to_duration(secs)?))?.map_err(error::to_ruby)
2682
2693
  }
2683
2694
 
2684
2695
  /// Send the graceful-shutdown request and return without waiting.
2685
2696
  fn request_stop(&self) -> Result<(), Error> {
2686
- block_on(self.inner.request_stop()).map_err(error::to_ruby)
2697
+ block_on(self.inner.request_stop())?.map_err(error::to_ruby)
2687
2698
  }
2688
2699
 
2689
2700
  /// Send the force-kill request and return without waiting.
2690
2701
  fn request_kill(&self) -> Result<(), Error> {
2691
- block_on(self.inner.request_kill()).map_err(error::to_ruby)
2702
+ block_on(self.inner.request_kill())?.map_err(error::to_ruby)
2692
2703
  }
2693
2704
 
2694
2705
  /// Send the drain request (SIGUSR1) and return without waiting.
2695
2706
  fn request_drain(&self) -> Result<(), Error> {
2696
- block_on(self.inner.request_drain()).map_err(error::to_ruby)
2707
+ block_on(self.inner.request_drain())?.map_err(error::to_ruby)
2697
2708
  }
2698
2709
 
2699
2710
  /// Connect when this exact sandbox is running, wait while it is starting,
@@ -2707,7 +2718,7 @@ impl SbHandle {
2707
2718
  } else {
2708
2719
  self.inner.connect_or_start().await
2709
2720
  }
2710
- })
2721
+ })?
2711
2722
  .map_err(error::to_ruby)?;
2712
2723
  Ok(Sandbox::from_inner(inner))
2713
2724
  }
@@ -2717,7 +2728,7 @@ impl SbHandle {
2717
2728
  /// `SandboxReplacedError` instead of silently redirecting the wait.
2718
2729
  fn wait_for_status(&self, status: String) -> Result<SbHandle, Error> {
2719
2730
  let status = sandbox_status_from_str(&status)?;
2720
- let handle = block_on(self.inner.wait_for_status(status)).map_err(error::to_ruby)?;
2731
+ let handle = block_on(self.inner.wait_for_status(status))?.map_err(error::to_ruby)?;
2721
2732
  Ok(SbHandle::from_inner(handle))
2722
2733
  }
2723
2734
 
@@ -2725,20 +2736,20 @@ impl SbHandle {
2725
2736
  /// `opts` carries `force`/`timeout`/`detached`.
2726
2737
  fn restart(&self, opts: RHash) -> Result<Sandbox, Error> {
2727
2738
  let options = restart_options(opts)?;
2728
- let inner = block_on(self.inner.restart_with(options)).map_err(error::to_ruby)?;
2739
+ let inner = block_on(self.inner.restart_with(options))?.map_err(error::to_ruby)?;
2729
2740
  Ok(Sandbox::from_inner(inner))
2730
2741
  }
2731
2742
 
2732
2743
  /// Stop and remove this exact sandbox. `opts` carries `force`/`timeout`.
2733
2744
  fn destroy(&self, opts: RHash) -> Result<(), Error> {
2734
2745
  let options = destroy_options(opts)?;
2735
- block_on(self.inner.destroy_with(options)).map_err(error::to_ruby)
2746
+ block_on(self.inner.destroy_with(options))?.map_err(error::to_ruby)
2736
2747
  }
2737
2748
 
2738
2749
  /// Block until the sandbox reaches a terminal state; returns a stop-result
2739
2750
  /// Hash (name, status, exit_code, signal, observed_at_ms, source).
2740
2751
  fn wait_until_stopped(&self) -> Result<RHash, Error> {
2741
- let result = block_on(self.inner.wait_until_stopped()).map_err(error::to_ruby)?;
2752
+ let result = block_on(self.inner.wait_until_stopped())?.map_err(error::to_ruby)?;
2742
2753
  Ok(stop_result_to_hash(&result))
2743
2754
  }
2744
2755
 
@@ -2755,7 +2766,7 @@ impl SbHandle {
2755
2766
  /// (`snapshot_to` was removed upstream in v0.6.7 — use `Snapshot.create`
2756
2767
  /// with `dest_dir:` for explicit placement.)
2757
2768
  fn snapshot(&self, name: String) -> Result<RHash, Error> {
2758
- let snap = block_on(self.inner.snapshot(&name)).map_err(error::to_ruby)?;
2769
+ let snap = block_on(self.inner.snapshot(&name))?.map_err(error::to_ruby)?;
2759
2770
  Ok(crate::snapshot::snapshot_to_hash(&snap))
2760
2771
  }
2761
2772
 
@@ -2763,14 +2774,14 @@ impl SbHandle {
2763
2774
  /// sandbox raises `SandboxNotRunningError` (the handle does not start it
2764
2775
  /// implicitly). Returns `{ name, latency_secs }`.
2765
2776
  fn ping(&self) -> Result<RHash, Error> {
2766
- let result = block_on(self.inner.ping()).map_err(error::to_ruby)?;
2777
+ let result = block_on(self.inner.ping())?.map_err(error::to_ruby)?;
2767
2778
  Ok(ping_result_to_hash(&result))
2768
2779
  }
2769
2780
 
2770
2781
  /// Explicitly refresh the idle-activity timer. A stopped sandbox raises
2771
2782
  /// `SandboxNotRunningError`. Returns `{ name, activity_seq }`.
2772
2783
  fn touch(&self) -> Result<RHash, Error> {
2773
- let result = block_on(self.inner.touch()).map_err(error::to_ruby)?;
2784
+ let result = block_on(self.inner.touch())?.map_err(error::to_ruby)?;
2774
2785
  Ok(touch_result_to_hash(&result))
2775
2786
  }
2776
2787
 
@@ -2866,6 +2877,8 @@ pub(crate) fn log_entry_to_hash(entry: &LogEntry) -> RHash {
2866
2877
 
2867
2878
  pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
2868
2879
  let class = native.define_class("Sandbox", ruby.class_object())?;
2880
+ // Resolve the wrapper class now, not on first use (see `preload_class`).
2881
+ crate::runtime::preload_class::<Sandbox>(ruby);
2869
2882
 
2870
2883
  class.define_singleton_method("create", function!(Sandbox::create, 2))?;
2871
2884
  class.define_singleton_method(
@@ -2939,6 +2952,8 @@ pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
2939
2952
  class.define_method("attach_shell", method!(Sandbox::attach_shell, 0))?;
2940
2953
 
2941
2954
  let handle = native.define_class("SandboxHandle", ruby.class_object())?;
2955
+ // Resolve the wrapper class now, not on first use (see `preload_class`).
2956
+ crate::runtime::preload_class::<SbHandle>(ruby);
2942
2957
  handle.define_method("name", method!(SbHandle::name, 0))?;
2943
2958
  handle.define_method("id", method!(SbHandle::id, 0))?;
2944
2959
  handle.define_method("status", method!(SbHandle::status, 0))?;
@@ -2966,6 +2981,8 @@ pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
2966
2981
  handle.define_method("modify", method!(SbHandle::modify, 1))?;
2967
2982
 
2968
2983
  let session = native.define_class("PullSession", ruby.class_object())?;
2984
+ // Resolve the wrapper class now, not on first use (see `preload_class`).
2985
+ crate::runtime::preload_class::<PullSession>(ruby);
2969
2986
  session.define_method("recv", method!(PullSession::recv, 0))?;
2970
2987
  session.define_method("result", method!(PullSession::result, 0))?;
2971
2988
 
@@ -110,7 +110,7 @@ fn create(name: String, opts: RHash) -> Result<RHash, Error> {
110
110
  b = b.resumable();
111
111
  }
112
112
 
113
- let snap = block_on(b.create()).map_err(error::to_ruby)?;
113
+ let snap = block_on(b.create())?.map_err(error::to_ruby)?;
114
114
  Ok(snapshot_to_hash(&snap))
115
115
  }
116
116
 
@@ -119,14 +119,14 @@ fn create(name: String, opts: RHash) -> Result<RHash, Error> {
119
119
  /// Hash — the only way to inspect an artifact addressed by path (`get`/`list`
120
120
  /// read the local index, which path-addressed artifacts are absent from).
121
121
  fn open(path_or_name: String) -> Result<RHash, Error> {
122
- let snap = block_on(Snapshot::open(&path_or_name)).map_err(error::to_ruby)?;
122
+ let snap = block_on(Snapshot::open(&path_or_name))?.map_err(error::to_ruby)?;
123
123
  Ok(snapshot_to_hash(&snap))
124
124
  }
125
125
 
126
126
  /// Walk `dir` and parse each subdirectory's `snapshot.json` without touching
127
127
  /// the local index — for enumerating external/un-imported snapshot collections.
128
128
  fn list_dir(dir: String) -> Result<RArray, Error> {
129
- let snaps = block_on(Snapshot::list_dir(Path::new(&dir))).map_err(error::to_ruby)?;
129
+ let snaps = block_on(Snapshot::list_dir(Path::new(&dir)))?.map_err(error::to_ruby)?;
130
130
  let arr = ruby().ary_new();
131
131
  for snap in &snaps {
132
132
  arr.push(snapshot_to_hash(snap))?;
@@ -145,19 +145,19 @@ fn reindex(dir: Option<String>) -> Result<u64, Error> {
145
145
  .map(|l| l.snapshots_dir())
146
146
  .unwrap_or_else(|| PathBuf::from(".")),
147
147
  };
148
- let n = block_on(Snapshot::reindex(&dir)).map_err(error::to_ruby)?;
148
+ let n = block_on(Snapshot::reindex(&dir))?.map_err(error::to_ruby)?;
149
149
  Ok(n as u64)
150
150
  }
151
151
 
152
152
  /// Metadata for one snapshot by name or digest.
153
153
  fn get(name_or_digest: String) -> Result<RHash, Error> {
154
- let handle = block_on(Snapshot::get(&name_or_digest)).map_err(error::to_ruby)?;
154
+ let handle = block_on(Snapshot::get(&name_or_digest))?.map_err(error::to_ruby)?;
155
155
  Ok(handle_to_hash(&handle))
156
156
  }
157
157
 
158
158
  /// All snapshots as metadata hashes.
159
159
  fn list() -> Result<RArray, Error> {
160
- let handles = block_on(Snapshot::list()).map_err(error::to_ruby)?;
160
+ let handles = block_on(Snapshot::list())?.map_err(error::to_ruby)?;
161
161
  let arr = ruby().ary_new();
162
162
  for h in &handles {
163
163
  arr.push(handle_to_hash(h))?;
@@ -167,7 +167,7 @@ fn list() -> Result<RArray, Error> {
167
167
 
168
168
  /// Remove a snapshot artifact by name or path.
169
169
  fn remove(name_or_path: String, force: bool) -> Result<(), Error> {
170
- block_on(Snapshot::remove(&name_or_path, force)).map_err(error::to_ruby)
170
+ block_on(Snapshot::remove(&name_or_path, force))?.map_err(error::to_ruby)
171
171
  }
172
172
 
173
173
  /// Verify a snapshot's recorded upper-layer integrity. Returns
@@ -176,8 +176,14 @@ fn remove(name_or_path: String, force: bool) -> Result<(), Error> {
176
176
  /// "verified" (mismatches raise SnapshotIntegrityError), snapshots without
177
177
  /// recorded integrity report "not_recorded" with no algorithm/digest keys.
178
178
  fn verify(name_or_path: String) -> Result<RHash, Error> {
179
- let snap = block_on(Snapshot::open(&name_or_path)).map_err(error::to_ruby)?;
180
- let report = block_on(snap.verify()).map_err(error::to_ruby)?;
179
+ // Open AND verify in ONE `block_on`, so the opened snapshot never sits in
180
+ // this frame between two calls (`block_on` returns `Err` when an interrupt
181
+ // is delivered before it starts; one call leaves no such in-between state).
182
+ let report = block_on(async move {
183
+ let snap = Snapshot::open(&name_or_path).await?;
184
+ snap.verify().await
185
+ })?
186
+ .map_err(error::to_ruby)?;
181
187
  Ok(verify_report_to_hash(&report))
182
188
  }
183
189
 
@@ -193,7 +199,7 @@ fn save(name_or_path: String, out_path: String, opts: RHash) -> Result<(), Error
193
199
  &name_or_path,
194
200
  std::path::Path::new(&out_path),
195
201
  save_opts,
196
- ))
202
+ ))?
197
203
  .map_err(error::to_ruby)
198
204
  }
199
205
 
@@ -204,7 +210,7 @@ fn load(archive_path: String, dest: Option<String>) -> Result<RHash, Error> {
204
210
  let handle = block_on(Snapshot::load(
205
211
  std::path::Path::new(&archive_path),
206
212
  dest_path.as_deref(),
207
- ))
213
+ ))?
208
214
  .map_err(error::to_ruby)?;
209
215
  Ok(handle_to_hash(&handle))
210
216
  }