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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +138 -8
- data/Cargo.lock +1 -1
- data/DESIGN.md +183 -36
- data/README.md +8 -7
- data/SECURITY.md +1 -1
- data/docs/errors.md +1 -0
- data/docs/execution.md +8 -1
- data/docs/lifecycle.md +33 -0
- data/docs/releasing.md +85 -34
- data/docs/runtime.md +88 -1
- data/docs/surface.md +16 -8
- data/ext/microsandbox/Cargo.toml +9 -5
- data/ext/microsandbox/src/agent.rs +26 -21
- data/ext/microsandbox/src/backend.rs +291 -34
- data/ext/microsandbox/src/error.rs +18 -0
- data/ext/microsandbox/src/exec.rs +39 -21
- data/ext/microsandbox/src/fs_stream.rs +30 -15
- data/ext/microsandbox/src/host_path.rs +4 -3
- data/ext/microsandbox/src/image.rs +2 -2
- data/ext/microsandbox/src/lib.rs +3 -2
- data/ext/microsandbox/src/runtime.rs +314 -47
- data/ext/microsandbox/src/sandbox.rs +225 -124
- data/ext/microsandbox/src/snapshot.rs +24 -15
- data/ext/microsandbox/src/ssh.rs +48 -39
- data/ext/microsandbox/src/stream.rs +19 -9
- data/ext/microsandbox/src/volume.rs +40 -32
- data/lib/microsandbox/errors.rb +8 -0
- data/lib/microsandbox/sandbox.rb +75 -4
- data/lib/microsandbox/version.rb +1 -1
- data/lib/microsandbox.rb +20 -1
- data/sig/microsandbox.rbs +25 -0
- metadata +1 -1
|
@@ -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())
|
|
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))
|
|
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)))
|
|
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 =>
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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))
|
|
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))
|
|
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())
|
|
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))
|
|
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
|
-
|
|
180
|
-
|
|
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
|
}
|
data/ext/microsandbox/src/ssh.rs
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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(
|
|
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:
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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:
|
|
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(
|
|
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:
|
|
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(
|
|
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())
|
|
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))
|
|
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())
|
|
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())
|
|
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))
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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))?;
|