1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
//! Sandbox file I/O streaming handlers and path verbs (CORE-62).
use std::time::Duration;
use arcbox_connect::sandbox_v1;
use buffa::Message;
use tokio::io::{AsyncRead, AsyncWrite};
use super::{SandboxService, convert};
use crate::error::SandboxError;
use crate::rpc::{ErrorResponse, MessageType, read_message, write_message};
/// How often an idle watch stream emits a keepalive frame. Below the
/// daemon's own 15 s client-facing keepalive so the host↔guest hop never
/// looks dead first.
const WATCH_KEEPALIVE_INTERVAL: Duration = Duration::from_secs(10);
impl SandboxService {
/// Stream a file out of a sandbox as `SandboxFileData` frames.
///
/// The final frame carries `done == true`. Errors (missing sandbox,
/// missing file, wrong state) are reported as a single `Error` frame.
pub async fn handle_read_file<S>(
&self,
stream: &mut S,
trace_id: &str,
payload: &[u8],
) -> anyhow::Result<()>
where
S: AsyncWrite + Unpin,
{
let req = match sandbox_v1::ReadFileRequest::decode_from_slice(payload) {
Ok(r) => r,
Err(e) => {
let err = ErrorResponse::new(400, format!("decode error: {e}"));
write_message(stream, MessageType::Error, trace_id, &err.encode()).await?;
return Ok(());
}
};
let data = match self.manager.read_sandbox_file(&req.id, &req.path).await {
Ok(d) => d,
Err(e) => {
let e = SandboxError::from(e);
let err = ErrorResponse::new(e.status_code(), e.to_string());
write_message(stream, MessageType::Error, trace_id, &err.encode()).await?;
return Ok(());
}
};
const CHUNK_SIZE: usize = 1024 * 1024;
for chunk in data.chunks(CHUNK_SIZE) {
let msg = sandbox_v1::FileChunk {
data: chunk.to_vec(),
..Default::default()
};
write_message(
stream,
MessageType::SandboxFileData,
trace_id,
&msg.encode_to_vec(),
)
.await?;
}
let done = sandbox_v1::FileChunk {
done: true,
..Default::default()
};
write_message(
stream,
MessageType::SandboxFileData,
trace_id,
&done.encode_to_vec(),
)
.await?;
Ok(())
}
/// Receive a `SandboxFileChunk` stream and store it inside the sandbox.
///
/// The open payload was already parsed by the dispatcher frame; chunk
/// frames follow on the same connection until `done == true`, then a
/// `SandboxFileWriteResponse` (or `Error`) frame answers.
pub async fn handle_write_file<S>(
&self,
stream: &mut S,
trace_id: &str,
payload: &[u8],
) -> anyhow::Result<()>
where
S: AsyncRead + AsyncWrite + Unpin,
{
let open = match sandbox_v1::WriteFileOpen::decode_from_slice(payload) {
Ok(o) => o,
Err(e) => {
let err = ErrorResponse::new(400, format!("decode error: {e}"));
write_message(stream, MessageType::Error, trace_id, &err.encode()).await?;
return Ok(());
}
};
let max = arcbox_vm::file_io::proto::MAX_FILE_SIZE;
let mut data = Vec::new();
loop {
match read_message(stream).await {
Ok((MessageType::SandboxFileChunk, _, frame)) => {
let chunk = match sandbox_v1::FileChunk::decode_from_slice(&frame) {
Ok(c) => c,
Err(e) => {
let err = ErrorResponse::new(400, format!("decode error: {e}"));
write_message(stream, MessageType::Error, trace_id, &err.encode())
.await?;
return Ok(());
}
};
if data.len() + chunk.data.len() > max {
let err = ErrorResponse::new(
400,
format!("file exceeds the {max}-byte write limit"),
);
write_message(stream, MessageType::Error, trace_id, &err.encode()).await?;
return Ok(());
}
data.extend_from_slice(&chunk.data);
if chunk.done {
break;
}
}
Ok((other, _, _)) => {
let err = ErrorResponse::new(
400,
format!("unexpected frame during write: {other:?}"),
);
write_message(stream, MessageType::Error, trace_id, &err.encode()).await?;
return Ok(());
}
Err(e) => {
tracing::warn!(error = %e, "write stream ended before done chunk");
return Ok(());
}
}
}
let mode = if open.mode == 0 { 0o644 } else { open.mode };
match self
.manager
.write_sandbox_file(&open.id, &open.path, mode, &data)
.await
{
Ok(()) => {
write_message(stream, MessageType::SandboxFileWriteResponse, trace_id, &[]).await?;
}
Err(e) => {
let e = SandboxError::from(e);
let err = ErrorResponse::new(e.status_code(), e.to_string());
write_message(stream, MessageType::Error, trace_id, &err.encode()).await?;
}
}
Ok(())
}
/// Stat one path inside a sandbox (symlinks reported, not followed).
pub async fn stat_file(&self, payload: &[u8]) -> Result<sandbox_v1::FileStat, SandboxError> {
let req = sandbox_v1::StatFileRequest::decode_from_slice(payload)
.map_err(|e| SandboxError::Decode(e.to_string()))?;
let dto = self
.manager
.stat_sandbox_path(&req.id, &req.path)
.await
.map_err(SandboxError::from)?;
Ok(convert::file_stat_to_proto(&dto))
}
/// List a directory inside a sandbox, non-recursively.
pub async fn list_dir(
&self,
payload: &[u8],
) -> Result<sandbox_v1::ListDirResponse, SandboxError> {
let req = sandbox_v1::ListDirRequest::decode_from_slice(payload)
.map_err(|e| SandboxError::Decode(e.to_string()))?;
let entries = self
.manager
.list_sandbox_dir(&req.id, &req.path)
.await
.map_err(SandboxError::from)?;
Ok(sandbox_v1::ListDirResponse {
entries: entries.iter().map(convert::file_stat_to_proto).collect(),
..Default::default()
})
}
/// Create a directory (with `mkdir -p` semantics) inside a sandbox.
pub async fn make_dir(&self, payload: &[u8]) -> Result<(), SandboxError> {
let req = sandbox_v1::MakeDirRequest::decode_from_slice(payload)
.map_err(|e| SandboxError::Decode(e.to_string()))?;
let mode = if req.mode == 0 { 0o755 } else { req.mode };
self.manager
.make_sandbox_dir(&req.id, &req.path, mode)
.await
.map_err(SandboxError::from)
}
/// Remove a file, symlink, or directory inside a sandbox.
pub async fn remove_entry(&self, payload: &[u8]) -> Result<(), SandboxError> {
let req = sandbox_v1::RemoveEntryRequest::decode_from_slice(payload)
.map_err(|e| SandboxError::Decode(e.to_string()))?;
self.manager
.remove_sandbox_path(&req.id, &req.path, req.recursive)
.await
.map_err(SandboxError::from)
}
/// Rename / move an entry within a sandbox.
pub async fn move_entry(&self, payload: &[u8]) -> Result<(), SandboxError> {
let req = sandbox_v1::MoveEntryRequest::decode_from_slice(payload)
.map_err(|e| SandboxError::Decode(e.to_string()))?;
self.manager
.move_sandbox_path(&req.id, &req.from_path, &req.to_path)
.await
.map_err(SandboxError::from)
}
/// Stream `SandboxFileWatchEvent` frames for a directory watch.
///
/// An immediate keepalive frame confirms the watch is established (it
/// also lets the daemon's paused-sandbox first-frame peek return fast);
/// further keepalives are interleaved while idle. The stream ends with
/// `SandboxFileWatchEnd` when the sandbox stops (the vm-agent side of
/// the vsock channel closes), or an `Error` frame on setup and stream
/// failures. Host cancellation surfaces as a failed write here, which
/// drops the watch and closes the vm-agent connection.
pub async fn handle_watch_dir<S>(
&self,
stream: &mut S,
trace_id: &str,
payload: &[u8],
) -> anyhow::Result<()>
where
S: AsyncWrite + Unpin,
{
let req = match sandbox_v1::WatchDirRequest::decode_from_slice(payload) {
Ok(r) => r,
Err(e) => {
let err = ErrorResponse::new(400, format!("decode error: {e}"));
write_message(stream, MessageType::Error, trace_id, &err.encode()).await?;
return Ok(());
}
};
let mut watch = match self
.manager
.watch_sandbox_dir(&req.id, &req.path, req.recursive)
.await
{
Ok(watch) => watch,
Err(e) => {
let e = SandboxError::from(e);
let err = ErrorResponse::new(e.status_code(), e.to_string());
write_message(stream, MessageType::Error, trace_id, &err.encode()).await?;
return Ok(());
}
};
// Relay events through a channel: `next_event` is not cancellation
// safe (a frame read spans several awaits), so it must not race a
// keepalive tick inside select!. Dropping the receiver stops the
// relay, which drops the watch and closes the vm-agent connection.
let (event_tx, mut event_rx) = tokio::sync::mpsc::channel(64);
tokio::spawn(async move {
loop {
tokio::select! {
event = watch.next_event() => match event {
Ok(Some(event)) => {
if event_tx.send(Ok(Some(event))).await.is_err() {
return;
}
}
terminal => {
let _ = event_tx.send(terminal).await;
return;
}
},
// The handler dropped the receiver (host cancelled): stop
// relaying even while no event ever arrives, so the watch
// and its vm-agent connection are released immediately
// rather than on the next filesystem event. Cutting
// `next_event` mid-frame is fine — the connection is being
// torn down either way.
() = event_tx.closed() => return,
}
}
});
write_watch_keepalive(stream, trace_id).await?;
let mut keepalive = tokio::time::interval_at(
tokio::time::Instant::now() + WATCH_KEEPALIVE_INTERVAL,
WATCH_KEEPALIVE_INTERVAL,
);
loop {
tokio::select! {
event = event_rx.recv() => match event {
Some(Ok(Some(event))) => {
let frame = sandbox_v1::WatchDirResponse {
payload: convert::fs_event_to_proto(event).into(),
..Default::default()
};
write_message(
stream,
MessageType::SandboxFileWatchEvent,
trace_id,
&frame.encode_to_vec(),
)
.await?;
}
// Clean EOF from the vm-agent: the sandbox stopped.
Some(Ok(None)) | None => {
write_message(stream, MessageType::SandboxFileWatchEnd, trace_id, &[])
.await?;
return Ok(());
}
Some(Err(e)) => {
let e = SandboxError::from(e);
let err = ErrorResponse::new(e.status_code(), e.to_string());
write_message(stream, MessageType::Error, trace_id, &err.encode()).await?;
return Ok(());
}
},
_ = keepalive.tick() => write_watch_keepalive(stream, trace_id).await?,
}
}
}
}
/// One `SandboxFileWatchEvent` frame carrying a keepalive payload.
async fn write_watch_keepalive<S>(stream: &mut S, trace_id: &str) -> anyhow::Result<()>
where
S: AsyncWrite + Unpin,
{
let frame = sandbox_v1::WatchDirResponse {
payload: sandbox_v1::KeepAlive::default().into(),
..Default::default()
};
write_message(
stream,
MessageType::SandboxFileWatchEvent,
trace_id,
&frame.encode_to_vec(),
)
.await
}