microsandbox-rb 0.17.1 → 0.17.3

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.
@@ -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))?;
@@ -140,24 +140,27 @@ fn list_dir(dir: String) -> Result<RArray, Error> {
140
140
  fn reindex(dir: Option<String>) -> Result<u64, Error> {
141
141
  let dir: PathBuf = match dir {
142
142
  Some(d) => PathBuf::from(d),
143
- None => microsandbox::default_backend()
144
- .as_local()
145
- .map(|l| l.snapshots_dir())
146
- .unwrap_or_else(|| PathBuf::from(".")),
143
+ None => {
144
+ crate::backend::ensure_this_process_backend()?;
145
+ microsandbox::default_backend()
146
+ .as_local()
147
+ .map(|l| l.snapshots_dir())
148
+ .unwrap_or_else(|| PathBuf::from("."))
149
+ }
147
150
  };
148
- let n = block_on(Snapshot::reindex(&dir)).map_err(error::to_ruby)?;
151
+ let n = block_on(Snapshot::reindex(&dir))?.map_err(error::to_ruby)?;
149
152
  Ok(n as u64)
150
153
  }
151
154
 
152
155
  /// Metadata for one snapshot by name or digest.
153
156
  fn get(name_or_digest: String) -> Result<RHash, Error> {
154
- let handle = block_on(Snapshot::get(&name_or_digest)).map_err(error::to_ruby)?;
157
+ let handle = block_on(Snapshot::get(&name_or_digest))?.map_err(error::to_ruby)?;
155
158
  Ok(handle_to_hash(&handle))
156
159
  }
157
160
 
158
161
  /// All snapshots as metadata hashes.
159
162
  fn list() -> Result<RArray, Error> {
160
- let handles = block_on(Snapshot::list()).map_err(error::to_ruby)?;
163
+ let handles = block_on(Snapshot::list())?.map_err(error::to_ruby)?;
161
164
  let arr = ruby().ary_new();
162
165
  for h in &handles {
163
166
  arr.push(handle_to_hash(h))?;
@@ -167,7 +170,7 @@ fn list() -> Result<RArray, Error> {
167
170
 
168
171
  /// Remove a snapshot artifact by name or path.
169
172
  fn remove(name_or_path: String, force: bool) -> Result<(), Error> {
170
- block_on(Snapshot::remove(&name_or_path, force)).map_err(error::to_ruby)
173
+ block_on(Snapshot::remove(&name_or_path, force))?.map_err(error::to_ruby)
171
174
  }
172
175
 
173
176
  /// Verify a snapshot's recorded upper-layer integrity. Returns
@@ -176,8 +179,14 @@ fn remove(name_or_path: String, force: bool) -> Result<(), Error> {
176
179
  /// "verified" (mismatches raise SnapshotIntegrityError), snapshots without
177
180
  /// recorded integrity report "not_recorded" with no algorithm/digest keys.
178
181
  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)?;
182
+ // Open AND verify in ONE `block_on`, so the opened snapshot never sits in
183
+ // this frame between two calls (`block_on` returns `Err` when an interrupt
184
+ // is delivered before it starts; one call leaves no such in-between state).
185
+ let report = block_on(async move {
186
+ let snap = Snapshot::open(&name_or_path).await?;
187
+ snap.verify().await
188
+ })?
189
+ .map_err(error::to_ruby)?;
181
190
  Ok(verify_report_to_hash(&report))
182
191
  }
183
192
 
@@ -193,7 +202,7 @@ fn save(name_or_path: String, out_path: String, opts: RHash) -> Result<(), Error
193
202
  &name_or_path,
194
203
  std::path::Path::new(&out_path),
195
204
  save_opts,
196
- ))
205
+ ))?
197
206
  .map_err(error::to_ruby)
198
207
  }
199
208
 
@@ -204,7 +213,7 @@ fn load(archive_path: String, dest: Option<String>) -> Result<RHash, Error> {
204
213
  let handle = block_on(Snapshot::load(
205
214
  std::path::Path::new(&archive_path),
206
215
  dest_path.as_deref(),
207
- ))
216
+ ))?
208
217
  .map_err(error::to_ruby)?;
209
218
  Ok(handle_to_hash(&handle))
210
219
  }
@@ -21,7 +21,7 @@ use tokio::io::AsyncWriteExt;
21
21
  use tokio::sync::Mutex;
22
22
 
23
23
  use crate::error;
24
- use crate::runtime::{block_on, ruby};
24
+ use crate::runtime::{block_on, ruby, ForkGuarded};
25
25
 
26
26
  /// A core error for a session whose value was already taken (closed). Built
27
27
  /// inside `block_on`, so it must be a plain Rust error, not a Ruby one.
@@ -35,31 +35,31 @@ fn consumed() -> MicrosandboxError {
35
35
 
36
36
  #[magnus::wrap(class = "Microsandbox::Native::SshClient", free_immediately, size)]
37
37
  pub struct SshClient {
38
- inner: Arc<Mutex<Option<CoreSshClient>>>,
38
+ inner: ForkGuarded<Arc<Mutex<Option<CoreSshClient>>>>,
39
39
  }
40
40
 
41
41
  impl SshClient {
42
42
  pub fn from_core(inner: CoreSshClient) -> Self {
43
43
  Self {
44
- inner: Arc::new(Mutex::new(Some(inner))),
44
+ inner: ForkGuarded::new("Microsandbox::SshClient", Arc::new(Mutex::new(Some(inner)))),
45
45
  }
46
46
  }
47
47
 
48
48
  /// Run a command over SSH and collect {status, stdout, stderr}.
49
49
  fn exec(&self, command: String, tty: bool) -> Result<RHash, Error> {
50
- let inner = Arc::clone(&self.inner);
50
+ let inner = Arc::clone(self.inner.get()?);
51
51
  let result: MicrosandboxResult<SshOutput> = block_on(async move {
52
52
  let guard = inner.lock().await;
53
53
  let client = guard.as_ref().ok_or_else(consumed)?;
54
54
  client.exec_with(command, |b| b.tty(tty)).await
55
- });
55
+ })?;
56
56
  Ok(ssh_output_to_hash(result.map_err(error::to_ruby)?))
57
57
  }
58
58
 
59
59
  /// Attach the local terminal to an interactive SSH shell; returns the exit
60
60
  /// status. Host-TTY coupled (raw mode); not meaningful without a real tty.
61
61
  fn attach(&self, term: Option<String>, detach_keys: Option<String>) -> Result<i32, Error> {
62
- let inner = Arc::clone(&self.inner);
62
+ let inner = Arc::clone(self.inner.get()?);
63
63
  let result: MicrosandboxResult<i32> = block_on(async move {
64
64
  let guard = inner.lock().await;
65
65
  let client = guard.as_ref().ok_or_else(consumed)?;
@@ -74,24 +74,24 @@ impl SshClient {
74
74
  b
75
75
  })
76
76
  .await
77
- });
77
+ })?;
78
78
  result.map_err(error::to_ruby)
79
79
  }
80
80
 
81
81
  /// Open an SFTP session over this SSH connection.
82
82
  fn sftp(&self) -> Result<SftpClient, Error> {
83
- let inner = Arc::clone(&self.inner);
83
+ let inner = Arc::clone(self.inner.get()?);
84
84
  let result: MicrosandboxResult<CoreSftp> = block_on(async move {
85
85
  let guard = inner.lock().await;
86
86
  let client = guard.as_ref().ok_or_else(consumed)?;
87
87
  client.sftp().await
88
- });
88
+ })?;
89
89
  Ok(SftpClient::from_core(result.map_err(error::to_ruby)?))
90
90
  }
91
91
 
92
92
  /// Close the SSH client session. Idempotent.
93
93
  fn close(&self) -> Result<(), Error> {
94
- let inner = Arc::clone(&self.inner);
94
+ let inner = Arc::clone(self.inner.get()?);
95
95
  let result: MicrosandboxResult<()> = block_on(async move {
96
96
  let client = {
97
97
  let mut guard = inner.lock().await;
@@ -101,7 +101,7 @@ impl SshClient {
101
101
  Some(c) => c.close().await,
102
102
  None => Ok(()),
103
103
  }
104
- });
104
+ })?;
105
105
  result.map_err(error::to_ruby)
106
106
  }
107
107
  }
@@ -122,7 +122,7 @@ fn ssh_output_to_hash(output: SshOutput) -> RHash {
122
122
 
123
123
  #[magnus::wrap(class = "Microsandbox::Native::SftpClient", free_immediately, size)]
124
124
  pub struct SftpClient {
125
- inner: Arc<Mutex<Option<CoreSftp>>>,
125
+ inner: ForkGuarded<Arc<Mutex<Option<CoreSftp>>>>,
126
126
  }
127
127
 
128
128
  /// Format an SFTP-layer error as a plain string inside `block_on`; the Ruby
@@ -134,23 +134,26 @@ fn sftp_str(e: impl std::fmt::Display) -> String {
134
134
  impl SftpClient {
135
135
  pub fn from_core(inner: CoreSftp) -> Self {
136
136
  Self {
137
- inner: Arc::new(Mutex::new(Some(inner))),
137
+ inner: ForkGuarded::new(
138
+ "Microsandbox::SftpClient",
139
+ Arc::new(Mutex::new(Some(inner))),
140
+ ),
138
141
  }
139
142
  }
140
143
 
141
144
  fn read(&self, path: String) -> Result<RString, Error> {
142
- let inner = Arc::clone(&self.inner);
145
+ let inner = Arc::clone(self.inner.get()?);
143
146
  let result: Result<Vec<u8>, String> = block_on(async move {
144
147
  let guard = inner.lock().await;
145
148
  let sftp = guard.as_ref().ok_or_else(|| sftp_str("session closed"))?;
146
149
  sftp.read(path).await.map_err(sftp_str)
147
- });
150
+ })?;
148
151
  Ok(ruby().str_from_slice(&result.map_err(error::base_error)?))
149
152
  }
150
153
 
151
154
  fn write(&self, path: String, data: RString) -> Result<(), Error> {
152
155
  let bytes = unsafe { data.as_slice() }.to_vec();
153
- let inner = Arc::clone(&self.inner);
156
+ let inner = Arc::clone(self.inner.get()?);
154
157
  let result: Result<(), String> = block_on(async move {
155
158
  let guard = inner.lock().await;
156
159
  let sftp = guard.as_ref().ok_or_else(|| sftp_str("session closed"))?;
@@ -158,82 +161,82 @@ impl SftpClient {
158
161
  file.write_all(&bytes).await.map_err(sftp_str)?;
159
162
  file.shutdown().await.map_err(sftp_str)?;
160
163
  Ok(())
161
- });
164
+ })?;
162
165
  result.map_err(error::base_error)
163
166
  }
164
167
 
165
168
  fn mkdir(&self, path: String) -> Result<(), Error> {
166
- let inner = Arc::clone(&self.inner);
169
+ let inner = Arc::clone(self.inner.get()?);
167
170
  let result: Result<(), String> = block_on(async move {
168
171
  let guard = inner.lock().await;
169
172
  let sftp = guard.as_ref().ok_or_else(|| sftp_str("session closed"))?;
170
173
  sftp.create_dir(path).await.map_err(sftp_str)
171
- });
174
+ })?;
172
175
  result.map_err(error::base_error)
173
176
  }
174
177
 
175
178
  fn remove_file(&self, path: String) -> Result<(), Error> {
176
- let inner = Arc::clone(&self.inner);
179
+ let inner = Arc::clone(self.inner.get()?);
177
180
  let result: Result<(), String> = block_on(async move {
178
181
  let guard = inner.lock().await;
179
182
  let sftp = guard.as_ref().ok_or_else(|| sftp_str("session closed"))?;
180
183
  sftp.remove_file(path).await.map_err(sftp_str)
181
- });
184
+ })?;
182
185
  result.map_err(error::base_error)
183
186
  }
184
187
 
185
188
  fn remove_dir(&self, path: String) -> Result<(), Error> {
186
- let inner = Arc::clone(&self.inner);
189
+ let inner = Arc::clone(self.inner.get()?);
187
190
  let result: Result<(), String> = block_on(async move {
188
191
  let guard = inner.lock().await;
189
192
  let sftp = guard.as_ref().ok_or_else(|| sftp_str("session closed"))?;
190
193
  sftp.remove_dir(path).await.map_err(sftp_str)
191
- });
194
+ })?;
192
195
  result.map_err(error::base_error)
193
196
  }
194
197
 
195
198
  fn rename(&self, old_path: String, new_path: String) -> Result<(), Error> {
196
- let inner = Arc::clone(&self.inner);
199
+ let inner = Arc::clone(self.inner.get()?);
197
200
  let result: Result<(), String> = block_on(async move {
198
201
  let guard = inner.lock().await;
199
202
  let sftp = guard.as_ref().ok_or_else(|| sftp_str("session closed"))?;
200
203
  sftp.rename(old_path, new_path).await.map_err(sftp_str)
201
- });
204
+ })?;
202
205
  result.map_err(error::base_error)
203
206
  }
204
207
 
205
208
  fn symlink(&self, target: String, link_path: String) -> Result<(), Error> {
206
- let inner = Arc::clone(&self.inner);
209
+ let inner = Arc::clone(self.inner.get()?);
207
210
  let result: Result<(), String> = block_on(async move {
208
211
  let guard = inner.lock().await;
209
212
  let sftp = guard.as_ref().ok_or_else(|| sftp_str("session closed"))?;
210
213
  sftp.symlink(target, link_path).await.map_err(sftp_str)
211
- });
214
+ })?;
212
215
  result.map_err(error::base_error)
213
216
  }
214
217
 
215
218
  fn real_path(&self, path: String) -> Result<String, Error> {
216
- let inner = Arc::clone(&self.inner);
219
+ let inner = Arc::clone(self.inner.get()?);
217
220
  let result: Result<String, String> = block_on(async move {
218
221
  let guard = inner.lock().await;
219
222
  let sftp = guard.as_ref().ok_or_else(|| sftp_str("session closed"))?;
220
223
  sftp.canonicalize(path).await.map_err(sftp_str)
221
- });
224
+ })?;
222
225
  result.map_err(error::base_error)
223
226
  }
224
227
 
225
228
  fn read_link(&self, path: String) -> Result<String, Error> {
226
- let inner = Arc::clone(&self.inner);
229
+ let inner = Arc::clone(self.inner.get()?);
227
230
  let result: Result<String, String> = block_on(async move {
228
231
  let guard = inner.lock().await;
229
232
  let sftp = guard.as_ref().ok_or_else(|| sftp_str("session closed"))?;
230
233
  sftp.read_link(path).await.map_err(sftp_str)
231
- });
234
+ })?;
232
235
  result.map_err(error::base_error)
233
236
  }
234
237
 
235
238
  fn close(&self) -> Result<(), Error> {
236
- let inner = Arc::clone(&self.inner);
239
+ let inner = Arc::clone(self.inner.get()?);
237
240
  let result: Result<(), String> = block_on(async move {
238
241
  let sftp = {
239
242
  let mut guard = inner.lock().await;
@@ -243,7 +246,7 @@ impl SftpClient {
243
246
  Some(s) => s.close().await.map_err(sftp_str),
244
247
  None => Ok(()),
245
248
  }
246
- });
249
+ })?;
247
250
  result.map_err(error::base_error)
248
251
  }
249
252
  }
@@ -254,19 +257,19 @@ impl SftpClient {
254
257
 
255
258
  #[magnus::wrap(class = "Microsandbox::Native::SshServer", free_immediately, size)]
256
259
  pub struct SshServer {
257
- inner: Arc<Mutex<Option<CoreSshServer>>>,
260
+ inner: ForkGuarded<Arc<Mutex<Option<CoreSshServer>>>>,
258
261
  }
259
262
 
260
263
  impl SshServer {
261
264
  pub fn from_core(inner: CoreSshServer) -> Self {
262
265
  Self {
263
- inner: Arc::new(Mutex::new(Some(inner))),
266
+ inner: ForkGuarded::new("Microsandbox::SshServer", Arc::new(Mutex::new(Some(inner)))),
264
267
  }
265
268
  }
266
269
 
267
270
  /// Serve one SSH connection over this process's stdin/stdout.
268
271
  fn serve_connection(&self) -> Result<(), Error> {
269
- let inner = Arc::clone(&self.inner);
272
+ let inner = Arc::clone(self.inner.get()?);
270
273
  let result: MicrosandboxResult<()> = block_on(async move {
271
274
  let server = {
272
275
  let guard = inner.lock().await;
@@ -276,28 +279,32 @@ impl SshServer {
276
279
  Some(s) => s.serve_connection(SshStdioStream::new()).await,
277
280
  None => Err(consumed()),
278
281
  }
279
- });
282
+ })?;
280
283
  result.map_err(error::to_ruby)
281
284
  }
282
285
 
283
286
  /// Release the prepared server endpoint. Idempotent.
284
287
  fn close(&self) -> Result<(), Error> {
285
- let inner = Arc::clone(&self.inner);
288
+ let inner = Arc::clone(self.inner.get()?);
286
289
  block_on(async move {
287
290
  inner.lock().await.take();
288
- });
291
+ })?;
289
292
  Ok(())
290
293
  }
291
294
  }
292
295
 
293
296
  pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
294
297
  let client = native.define_class("SshClient", ruby.class_object())?;
298
+ // Resolve the wrapper class now, not on first use (see `preload_class`).
299
+ crate::runtime::preload_class::<SshClient>(ruby);
295
300
  client.define_method("exec", method!(SshClient::exec, 2))?;
296
301
  client.define_method("attach", method!(SshClient::attach, 2))?;
297
302
  client.define_method("sftp", method!(SshClient::sftp, 0))?;
298
303
  client.define_method("close", method!(SshClient::close, 0))?;
299
304
 
300
305
  let sftp = native.define_class("SftpClient", ruby.class_object())?;
306
+ // Resolve the wrapper class now, not on first use (see `preload_class`).
307
+ crate::runtime::preload_class::<SftpClient>(ruby);
301
308
  sftp.define_method("read", method!(SftpClient::read, 1))?;
302
309
  sftp.define_method("write", method!(SftpClient::write, 2))?;
303
310
  sftp.define_method("mkdir", method!(SftpClient::mkdir, 1))?;
@@ -310,6 +317,8 @@ pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
310
317
  sftp.define_method("close", method!(SftpClient::close, 0))?;
311
318
 
312
319
  let server = native.define_class("SshServer", ruby.class_object())?;
320
+ // Resolve the wrapper class now, not on first use (see `preload_class`).
321
+ crate::runtime::preload_class::<SshServer>(ruby);
313
322
  server.define_method("serve_connection", method!(SshServer::serve_connection, 0))?;
314
323
  server.define_method("close", method!(SshServer::close, 0))?;
315
324
 
@@ -18,14 +18,14 @@ use microsandbox::MicrosandboxResult;
18
18
  use tokio::sync::Mutex;
19
19
 
20
20
  use crate::error;
21
- use crate::runtime::block_on;
21
+ use crate::runtime::{block_on, ForkGuarded};
22
22
  use crate::sandbox::{log_entry_to_hash, metrics_to_hash};
23
23
 
24
24
  type BoxStream<T> = Pin<Box<dyn Stream<Item = MicrosandboxResult<T>> + Send>>;
25
25
 
26
26
  #[magnus::wrap(class = "Microsandbox::Native::LogStream", free_immediately, size)]
27
27
  pub struct LogStream {
28
- inner: Arc<Mutex<BoxStream<LogEntry>>>,
28
+ inner: ForkGuarded<Arc<Mutex<BoxStream<LogEntry>>>>,
29
29
  }
30
30
 
31
31
  impl LogStream {
@@ -33,15 +33,18 @@ impl LogStream {
33
33
  stream: impl Stream<Item = MicrosandboxResult<LogEntry>> + Send + 'static,
34
34
  ) -> Self {
35
35
  Self {
36
- inner: Arc::new(Mutex::new(Box::pin(stream))),
36
+ inner: ForkGuarded::new(
37
+ "Microsandbox::LogStream",
38
+ Arc::new(Mutex::new(Box::pin(stream))),
39
+ ),
37
40
  }
38
41
  }
39
42
 
40
43
  /// Next log entry as a Hash, or nil when the stream ends.
41
44
  fn recv(&self) -> Result<Option<RHash>, Error> {
42
45
  use futures::StreamExt;
43
- let inner = Arc::clone(&self.inner);
44
- match block_on(async move { inner.lock().await.next().await }) {
46
+ let inner = Arc::clone(self.inner.get()?);
47
+ match block_on(async move { inner.lock().await.next().await })? {
45
48
  Some(Ok(entry)) => Ok(Some(log_entry_to_hash(&entry))),
46
49
  Some(Err(e)) => Err(error::to_ruby(e)),
47
50
  None => Ok(None),
@@ -51,7 +54,7 @@ impl LogStream {
51
54
 
52
55
  #[magnus::wrap(class = "Microsandbox::Native::MetricsStream", free_immediately, size)]
53
56
  pub struct MetricsStream {
54
- inner: Arc<Mutex<BoxStream<SandboxMetrics>>>,
57
+ inner: ForkGuarded<Arc<Mutex<BoxStream<SandboxMetrics>>>>,
55
58
  }
56
59
 
57
60
  impl MetricsStream {
@@ -59,15 +62,18 @@ impl MetricsStream {
59
62
  stream: impl Stream<Item = MicrosandboxResult<SandboxMetrics>> + Send + 'static,
60
63
  ) -> Self {
61
64
  Self {
62
- inner: Arc::new(Mutex::new(Box::pin(stream))),
65
+ inner: ForkGuarded::new(
66
+ "Microsandbox::MetricsStream",
67
+ Arc::new(Mutex::new(Box::pin(stream))),
68
+ ),
63
69
  }
64
70
  }
65
71
 
66
72
  /// Next metrics snapshot as a Hash, or nil when the stream ends.
67
73
  fn recv(&self) -> Result<Option<RHash>, Error> {
68
74
  use futures::StreamExt;
69
- let inner = Arc::clone(&self.inner);
70
- match block_on(async move { inner.lock().await.next().await }) {
75
+ let inner = Arc::clone(self.inner.get()?);
76
+ match block_on(async move { inner.lock().await.next().await })? {
71
77
  Some(Ok(metrics)) => Ok(Some(metrics_to_hash(&metrics))),
72
78
  Some(Err(e)) => Err(error::to_ruby(e)),
73
79
  None => Ok(None),
@@ -77,9 +83,13 @@ impl MetricsStream {
77
83
 
78
84
  pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
79
85
  let logs = native.define_class("LogStream", ruby.class_object())?;
86
+ // Resolve the wrapper class now, not on first use (see `preload_class`).
87
+ crate::runtime::preload_class::<LogStream>(ruby);
80
88
  logs.define_method("recv", method!(LogStream::recv, 0))?;
81
89
 
82
90
  let metrics = native.define_class("MetricsStream", ruby.class_object())?;
91
+ // Resolve the wrapper class now, not on first use (see `preload_class`).
92
+ crate::runtime::preload_class::<MetricsStream>(ruby);
83
93
  metrics.define_method("recv", method!(MetricsStream::recv, 0))?;
84
94
 
85
95
  Ok(())
@@ -12,7 +12,7 @@ use microsandbox::{Backend, MicrosandboxError};
12
12
 
13
13
  use crate::conv;
14
14
  use crate::error;
15
- use crate::runtime::{block_on, ruby};
15
+ use crate::runtime::{block_on, ruby, ForkGuarded};
16
16
  use crate::sandbox::{fs_entry_to_hash, fs_metadata_to_hash};
17
17
 
18
18
  fn handle_to_hash(h: &VolumeHandle) -> RHash {
@@ -68,7 +68,7 @@ fn create(name: String, opts: RHash) -> Result<RHash, Error> {
68
68
  builder = builder.label(k, v);
69
69
  }
70
70
 
71
- let vol = block_on(builder.create()).map_err(error::to_ruby)?;
71
+ let vol = block_on(builder.create())?.map_err(error::to_ruby)?;
72
72
  let hash = ruby().hash_new();
73
73
  hash.aset("name", vol.name().to_string())?;
74
74
  // `path()` is fallible as of v0.5.8 (returns `Unsupported` for cloud
@@ -81,19 +81,19 @@ fn create(name: String, opts: RHash) -> Result<RHash, Error> {
81
81
  }
82
82
 
83
83
  fn get(name: String) -> Result<RHash, Error> {
84
- let handle = block_on(microsandbox::Volume::get(&name)).map_err(error::to_ruby)?;
84
+ let handle = block_on(microsandbox::Volume::get(&name))?.map_err(error::to_ruby)?;
85
85
  Ok(handle_to_hash(&handle))
86
86
  }
87
87
 
88
88
  /// The backend's default volume (v0.6.9). Cloud-backend only — the local
89
89
  /// backend rejects it (Unsupported) to avoid accidental host access.
90
90
  fn get_default() -> Result<RHash, Error> {
91
- let handle = block_on(microsandbox::Volume::get_default()).map_err(error::to_ruby)?;
91
+ let handle = block_on(microsandbox::Volume::get_default())?.map_err(error::to_ruby)?;
92
92
  Ok(handle_to_hash(&handle))
93
93
  }
94
94
 
95
95
  fn list() -> Result<RArray, Error> {
96
- let handles = block_on(microsandbox::Volume::list()).map_err(error::to_ruby)?;
96
+ let handles = block_on(microsandbox::Volume::list())?.map_err(error::to_ruby)?;
97
97
  let arr = ruby().ary_new();
98
98
  for h in handles.iter() {
99
99
  arr.push(handle_to_hash(h))?;
@@ -102,7 +102,7 @@ fn list() -> Result<RArray, Error> {
102
102
  }
103
103
 
104
104
  fn remove(name: String) -> Result<(), Error> {
105
- block_on(microsandbox::Volume::remove(&name)).map_err(error::to_ruby)
105
+ block_on(microsandbox::Volume::remove(&name))?.map_err(error::to_ruby)
106
106
  }
107
107
 
108
108
  /// Volume paths are relative to the volume root and never need a `..` segment,
@@ -128,7 +128,9 @@ fn contained(path: &str) -> Result<&str, Error> {
128
128
  /// each operation rebuilds it from the stored backend + name.
129
129
  #[magnus::wrap(class = "Microsandbox::Native::VolumeFs", free_immediately, size)]
130
130
  pub struct VolumeFs {
131
- backend: Arc<dyn Backend>,
131
+ /// The backend this view is bound to — a clone of the creating process's
132
+ /// default backend, so it must not cross a fork.
133
+ backend: ForkGuarded<Arc<dyn Backend>>,
132
134
  name: String,
133
135
  }
134
136
 
@@ -138,41 +140,45 @@ impl VolumeFs {
138
140
  /// trait, which yields precise per-operation `Unsupported` errors on
139
141
  /// backends that can't serve it — no local-only downcast needed here.
140
142
  fn for_volume(name: String) -> Result<VolumeFs, Error> {
143
+ crate::backend::ensure_this_process_backend()?;
141
144
  Ok(VolumeFs {
142
- backend: microsandbox::default_backend(),
145
+ backend: ForkGuarded::new("Microsandbox::VolumeFs", microsandbox::default_backend()),
143
146
  name,
144
147
  })
145
148
  }
146
149
 
147
- fn fs(&self) -> microsandbox::volume::VolumeFs<'_> {
148
- microsandbox::volume::VolumeFs::with_backend(self.backend.clone(), &self.name)
150
+ fn fs(&self) -> Result<microsandbox::volume::VolumeFs<'_>, Error> {
151
+ let backend = self.backend.get()?.clone();
152
+ Ok(microsandbox::volume::VolumeFs::with_backend(
153
+ backend, &self.name,
154
+ ))
149
155
  }
150
156
 
151
157
  fn read(&self, path: String) -> Result<RString, Error> {
158
+ let fs = self.fs()?;
152
159
  let path = contained(&path)?;
153
- let fs = self.fs();
154
- let bytes = block_on(fs.read(path)).map_err(error::to_ruby)?;
160
+ let bytes = block_on(fs.read(path))?.map_err(error::to_ruby)?;
155
161
  Ok(ruby().str_from_slice(bytes.as_ref()))
156
162
  }
157
163
 
158
164
  fn read_text(&self, path: String) -> Result<String, Error> {
165
+ let fs = self.fs()?;
159
166
  let path = contained(&path)?;
160
- let fs = self.fs();
161
- block_on(fs.read_to_string(path)).map_err(error::to_ruby)
167
+ block_on(fs.read_to_string(path))?.map_err(error::to_ruby)
162
168
  }
163
169
 
164
170
  fn write(&self, path: String, data: RString) -> Result<(), Error> {
165
171
  // Copy the bytes out while the GVL is held (GC.compact could move them).
166
172
  let bytes = unsafe { data.as_slice() }.to_vec();
173
+ let fs = self.fs()?;
167
174
  let path = contained(&path)?;
168
- let fs = self.fs();
169
- block_on(fs.write(path, &bytes)).map_err(error::to_ruby)
175
+ block_on(fs.write(path, &bytes))?.map_err(error::to_ruby)
170
176
  }
171
177
 
172
178
  fn list(&self, path: String) -> Result<RArray, Error> {
179
+ let fs = self.fs()?;
173
180
  let path = contained(&path)?;
174
- let fs = self.fs();
175
- let entries = block_on(fs.list(path)).map_err(error::to_ruby)?;
181
+ let entries = block_on(fs.list(path))?.map_err(error::to_ruby)?;
176
182
  let arr = ruby().ary_new();
177
183
  for entry in &entries {
178
184
  arr.push(fs_entry_to_hash(entry))?;
@@ -181,47 +187,47 @@ impl VolumeFs {
181
187
  }
182
188
 
183
189
  fn mkdir(&self, path: String) -> Result<(), Error> {
190
+ let fs = self.fs()?;
184
191
  let path = contained(&path)?;
185
- let fs = self.fs();
186
- block_on(fs.mkdir(path)).map_err(error::to_ruby)
192
+ block_on(fs.mkdir(path))?.map_err(error::to_ruby)
187
193
  }
188
194
 
189
195
  fn remove_file(&self, path: String) -> Result<(), Error> {
196
+ let fs = self.fs()?;
190
197
  let path = contained(&path)?;
191
- let fs = self.fs();
192
- block_on(fs.remove(path)).map_err(error::to_ruby)
198
+ block_on(fs.remove(path))?.map_err(error::to_ruby)
193
199
  }
194
200
 
195
201
  fn remove_dir(&self, path: String) -> Result<(), Error> {
202
+ let fs = self.fs()?;
196
203
  let path = contained(&path)?;
197
- let fs = self.fs();
198
- block_on(fs.remove_dir(path)).map_err(error::to_ruby)
204
+ block_on(fs.remove_dir(path))?.map_err(error::to_ruby)
199
205
  }
200
206
 
201
207
  fn exists(&self, path: String) -> Result<bool, Error> {
208
+ let fs = self.fs()?;
202
209
  let path = contained(&path)?;
203
- let fs = self.fs();
204
- block_on(fs.exists(path)).map_err(error::to_ruby)
210
+ block_on(fs.exists(path))?.map_err(error::to_ruby)
205
211
  }
206
212
 
207
213
  fn copy(&self, from: String, to: String) -> Result<(), Error> {
214
+ let fs = self.fs()?;
208
215
  let from = contained(&from)?;
209
216
  let to = contained(&to)?;
210
- let fs = self.fs();
211
- block_on(fs.copy(from, to)).map_err(error::to_ruby)
217
+ block_on(fs.copy(from, to))?.map_err(error::to_ruby)
212
218
  }
213
219
 
214
220
  fn rename(&self, from: String, to: String) -> Result<(), Error> {
221
+ let fs = self.fs()?;
215
222
  let from = contained(&from)?;
216
223
  let to = contained(&to)?;
217
- let fs = self.fs();
218
- block_on(fs.rename(from, to)).map_err(error::to_ruby)
224
+ block_on(fs.rename(from, to))?.map_err(error::to_ruby)
219
225
  }
220
226
 
221
227
  fn stat(&self, path: String) -> Result<RHash, Error> {
228
+ let fs = self.fs()?;
222
229
  let path = contained(&path)?;
223
- let fs = self.fs();
224
- let meta = block_on(fs.stat(path)).map_err(error::to_ruby)?;
230
+ let meta = block_on(fs.stat(path))?.map_err(error::to_ruby)?;
225
231
  Ok(fs_metadata_to_hash(&meta))
226
232
  }
227
233
  }
@@ -241,6 +247,8 @@ pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
241
247
  class.define_singleton_method("fs", function!(fs, 1))?;
242
248
 
243
249
  let vfs = native.define_class("VolumeFs", ruby.class_object())?;
250
+ // Resolve the wrapper class now, not on first use (see `preload_class`).
251
+ crate::runtime::preload_class::<VolumeFs>(ruby);
244
252
  vfs.define_method("read", method!(VolumeFs::read, 1))?;
245
253
  vfs.define_method("read_text", method!(VolumeFs::read_text, 1))?;
246
254
  vfs.define_method("write", method!(VolumeFs::write, 2))?;