1use std::path::{Path, PathBuf};
4use std::sync::Arc;
5
6use a3s_box_core::error::{BoxError, Result};
7use tokio::io::AsyncWriteExt;
8use tokio::net::UnixStream;
9use tokio::sync::Mutex;
10
11const EXEC_CONTROL_CANCEL: &[u8] = b"cancel";
12const EXEC_CONTROL_STDIN_CLOSE: &[u8] = b"stdin-close";
13const EXEC_CONTROL_FLUSH: &[u8] = b"flush";
15const EXEC_CONTROL_ARCHIVE_ROOTFS: &[u8] = b"archive-rootfs-v1";
17const EXEC_CONTROL_ARCHIVE_ROOTFS_PAUSE: &[u8] = b"archive-rootfs-v1:pause";
18const EXEC_ARCHIVE_ROOTFS_DONE: &[u8] = b"archive-rootfs-v1-done";
20const EXEC_FLUSH_ACK: &[u8] = b"flush-ack";
24const EXEC_SIGNAL_MAIN_ACK: &[u8] = b"signal-main-ack";
28const EXEC_SPAWN_MAIN_ACK: &[u8] = b"spawn-main-ack";
32const EXEC_SPAWN_MAIN_NACK: &[u8] = b"spawn-main-nack:";
35
36const EXEC_HOST_SLACK_SECS: u64 = 10;
40const SIGNAL_MAIN_ACK_TIMEOUT_SECS: u64 = 10;
44
45type ExecFrameReader = a3s_transport::FrameReader<tokio::io::ReadHalf<tokio::net::UnixStream>>;
46type ExecFrameWriter = a3s_transport::FrameWriter<tokio::io::WriteHalf<tokio::net::UnixStream>>;
47
48#[derive(Debug)]
53pub struct ExecClient {
54 socket_path: PathBuf,
55}
56
57impl ExecClient {
58 pub async fn connect(socket_path: &Path) -> Result<Self> {
62 let _stream = UnixStream::connect(socket_path).await.map_err(|e| {
63 BoxError::ExecError(format!(
64 "Failed to connect to exec server at {}: {}",
65 socket_path.display(),
66 e,
67 ))
68 })?;
69
70 Ok(Self {
71 socket_path: socket_path.to_path_buf(),
72 })
73 }
74
75 pub fn socket_path(&self) -> &Path {
77 &self.socket_path
78 }
79
80 pub async fn exec_command(
84 &self,
85 request: &a3s_box_core::exec::ExecRequest,
86 ) -> Result<a3s_box_core::exec::ExecOutput> {
87 let payload = serde_json::to_vec(request)
88 .map_err(|e| BoxError::ExecError(format!("Failed to serialize exec request: {}", e)))?;
89
90 let mut stream = UnixStream::connect(&self.socket_path).await.map_err(|e| {
91 BoxError::ExecError(format!(
92 "Exec connection failed to {}: {}",
93 self.socket_path.display(),
94 e,
95 ))
96 })?;
97
98 let request_frame = a3s_transport::Frame::data(payload);
100 let encoded = request_frame.encode().map_err(|e| {
101 BoxError::ExecError(format!("Failed to encode exec request frame: {}", e))
102 })?;
103 stream
104 .write_all(&encoded)
105 .await
106 .map_err(|e| BoxError::ExecError(format!("Exec request write failed: {}", e)))?;
107
108 let (r, _w) = tokio::io::split(stream);
115 let mut reader = a3s_transport::FrameReader::new(r);
116 let host_deadline = std::time::Duration::from_nanos(request.timeout_ns)
117 .saturating_add(std::time::Duration::from_secs(EXEC_HOST_SLACK_SECS));
118 let frame = tokio::time::timeout(host_deadline, reader.read_frame())
119 .await
120 .map_err(|_| {
121 BoxError::ExecError(format!(
122 "Exec response timed out after {host_deadline:?} (guest may be wedged)"
123 ))
124 })?
125 .map_err(|e| BoxError::ExecError(format!("Exec response read failed: {}", e)))?
126 .ok_or_else(|| {
127 BoxError::ExecError("Exec server closed without response".to_string())
128 })?;
129
130 match frame.frame_type {
131 a3s_transport::FrameType::Data => {
132 let output: a3s_box_core::exec::ExecOutput = serde_json::from_slice(&frame.payload)
133 .map_err(|e| {
134 BoxError::ExecError(format!("Failed to parse exec response: {}", e))
135 })?;
136 Ok(output)
137 }
138 a3s_transport::FrameType::Error => {
139 let msg = String::from_utf8_lossy(&frame.payload);
140 Err(BoxError::ExecError(format!("Exec server error: {}", msg)))
141 }
142 _ => Err(BoxError::ExecError(format!(
143 "Unexpected frame type: {:?}",
144 frame.frame_type
145 ))),
146 }
147 }
148
149 pub async fn exec_stream(
157 &self,
158 request: &a3s_box_core::exec::ExecRequest,
159 ) -> Result<StreamingExec> {
160 let mut req = request.clone();
161 req.streaming = true;
162
163 let payload = serde_json::to_vec(&req)
164 .map_err(|e| BoxError::ExecError(format!("Failed to serialize exec request: {}", e)))?;
165
166 let stream = UnixStream::connect(&self.socket_path).await.map_err(|e| {
167 BoxError::ExecError(format!(
168 "Exec connection failed to {}: {}",
169 self.socket_path.display(),
170 e,
171 ))
172 })?;
173
174 let (r, w) = tokio::io::split(stream);
175 let mut writer = a3s_transport::FrameWriter::new(w);
176 writer
177 .write_data(&payload)
178 .await
179 .map_err(|e| BoxError::ExecError(format!("Exec request write failed: {}", e)))?;
180
181 let reader = a3s_transport::FrameReader::new(r);
182 let started = std::time::Instant::now();
183
184 Ok(StreamingExec {
185 reader,
186 writer: Arc::new(Mutex::new(writer)),
187 started,
188 stdout_bytes: 0,
189 stderr_bytes: 0,
190 done: false,
191 })
192 }
193
194 pub async fn archive_rootfs<W>(&self, output: &mut W, pause: bool) -> Result<u64>
200 where
201 W: tokio::io::AsyncWrite + Unpin,
202 {
203 let mut stream = UnixStream::connect(&self.socket_path)
204 .await
205 .map_err(|error| {
206 BoxError::ExecError(format!(
207 "Rootfs archive connection failed to {}: {error}",
208 self.socket_path.display()
209 ))
210 })?;
211
212 let control = if pause {
213 EXEC_CONTROL_ARCHIVE_ROOTFS_PAUSE
214 } else {
215 EXEC_CONTROL_ARCHIVE_ROOTFS
216 };
217 let request = a3s_transport::Frame::control(control.to_vec());
218 stream
219 .write_all(&request.encode().map_err(|error| {
220 BoxError::ExecError(format!("Rootfs archive request encode failed: {error}"))
221 })?)
222 .await
223 .map_err(|error| {
224 BoxError::ExecError(format!("Rootfs archive request write failed: {error}"))
225 })?;
226
227 let (reader, _writer) = tokio::io::split(stream);
228 let mut reader = a3s_transport::FrameReader::new(reader);
229 let mut written = 0u64;
230 loop {
231 let frame = reader
232 .read_frame()
233 .await
234 .map_err(|error| {
235 BoxError::ExecError(format!("Rootfs archive read failed: {error}"))
236 })?
237 .ok_or_else(|| {
238 BoxError::ExecError(
239 "Rootfs archive stream closed before completion".to_string(),
240 )
241 })?;
242
243 match frame.frame_type {
244 a3s_transport::FrameType::Data => {
245 output.write_all(&frame.payload).await.map_err(|error| {
246 BoxError::ExecError(format!("Rootfs archive output write failed: {error}"))
247 })?;
248 written = written.saturating_add(frame.payload.len() as u64);
249 }
250 a3s_transport::FrameType::Control if frame.payload == EXEC_ARCHIVE_ROOTFS_DONE => {
251 output.flush().await.map_err(|error| {
252 BoxError::ExecError(format!("Rootfs archive output flush failed: {error}"))
253 })?;
254 return Ok(written);
255 }
256 a3s_transport::FrameType::Error => {
257 return Err(BoxError::ExecError(format!(
258 "Guest rootfs archive failed: {}",
259 String::from_utf8_lossy(&frame.payload)
260 )));
261 }
262 other => {
263 return Err(BoxError::ExecError(format!(
264 "Unexpected rootfs archive frame: {other:?}"
265 )));
266 }
267 }
268 }
269 }
270
271 pub async fn file_transfer(
275 &self,
276 request: &a3s_box_core::exec::FileRequest,
277 ) -> Result<a3s_box_core::exec::FileResponse> {
278 let payload = serde_json::to_vec(request)
279 .map_err(|e| BoxError::ExecError(format!("Failed to serialize file request: {}", e)))?;
280
281 let mut stream = UnixStream::connect(&self.socket_path).await.map_err(|e| {
282 BoxError::ExecError(format!(
283 "Exec connection failed to {}: {}",
284 self.socket_path.display(),
285 e,
286 ))
287 })?;
288
289 let request_frame = a3s_transport::Frame::data(payload);
290 let encoded = request_frame.encode().map_err(|e| {
291 BoxError::ExecError(format!("Failed to encode file request frame: {}", e))
292 })?;
293 stream
294 .write_all(&encoded)
295 .await
296 .map_err(|e| BoxError::ExecError(format!("File request write failed: {}", e)))?;
297
298 let (r, _w) = tokio::io::split(stream);
299 let mut reader = a3s_transport::FrameReader::new(r);
300 let frame = reader
301 .read_frame()
302 .await
303 .map_err(|e| BoxError::ExecError(format!("File response read failed: {}", e)))?
304 .ok_or_else(|| {
305 BoxError::ExecError("Exec server closed without response".to_string())
306 })?;
307
308 match frame.frame_type {
309 a3s_transport::FrameType::Data => {
310 let response: a3s_box_core::exec::FileResponse =
311 serde_json::from_slice(&frame.payload).map_err(|e| {
312 BoxError::ExecError(format!("Failed to parse file response: {}", e))
313 })?;
314 Ok(response)
315 }
316 a3s_transport::FrameType::Error => {
317 let msg = String::from_utf8_lossy(&frame.payload);
318 Err(BoxError::ExecError(format!("File transfer error: {}", msg)))
319 }
320 _ => Err(BoxError::ExecError(format!(
321 "Unexpected frame type: {:?}",
322 frame.frame_type
323 ))),
324 }
325 }
326
327 pub async fn heartbeat(&self) -> Result<bool> {
331 let mut stream = match UnixStream::connect(&self.socket_path).await {
332 Ok(s) => s,
333 Err(_) => return Ok(false),
334 };
335
336 let frame = a3s_transport::Frame::heartbeat();
337 let encoded = match frame.encode() {
338 Ok(e) => e,
339 Err(_) => return Ok(false),
340 };
341
342 if stream.write_all(&encoded).await.is_err() {
343 return Ok(false);
344 }
345
346 let (r, _w) = tokio::io::split(stream);
347 let mut reader = a3s_transport::FrameReader::new(r);
348 match reader.read_frame().await {
349 Ok(Some(f)) if f.frame_type == a3s_transport::FrameType::Heartbeat => Ok(true),
350 _ => Ok(false),
351 }
352 }
353
354 pub async fn signal_main(&self, signal: i32) -> Result<bool> {
360 let mut stream = match UnixStream::connect(&self.socket_path).await {
361 Ok(s) => s,
362 Err(_) => return Ok(false),
363 };
364
365 let payload = format!("signal-main:{}", signal).into_bytes();
366 let frame = a3s_transport::Frame::control(payload);
367 let encoded = frame
368 .encode()
369 .map_err(|e| BoxError::ExecError(format!("signal-main frame encode failed: {}", e)))?;
370
371 if stream.write_all(&encoded).await.is_err() {
372 return Ok(false);
373 }
374
375 let (r, _w) = tokio::io::split(stream);
381 let mut reader = a3s_transport::FrameReader::new(r);
382 let read = tokio::time::timeout(
383 std::time::Duration::from_secs(SIGNAL_MAIN_ACK_TIMEOUT_SECS),
384 reader.read_frame(),
385 )
386 .await;
387 match read {
388 Ok(Ok(Some(f)))
389 if f.frame_type == a3s_transport::FrameType::Control
390 && f.payload == EXEC_SIGNAL_MAIN_ACK =>
391 {
392 Ok(true)
393 }
394 _ => Ok(false),
395 }
396 }
397
398 pub async fn spawn_main(&self, spec_json: Option<&[u8]>) -> Result<bool> {
403 let mut stream = match UnixStream::connect(&self.socket_path).await {
404 Ok(s) => s,
405 Err(_) => return Ok(false),
406 };
407
408 let mut payload = b"spawn-main:".to_vec();
409 if let Some(json) = spec_json {
410 payload.extend_from_slice(json);
411 }
412 let frame = a3s_transport::Frame::control(payload);
413 let encoded = frame
414 .encode()
415 .map_err(|e| BoxError::ExecError(format!("spawn-main frame encode failed: {}", e)))?;
416
417 if stream.write_all(&encoded).await.is_err() {
418 return Ok(false);
419 }
420
421 let (r, _w) = tokio::io::split(stream);
422 let mut reader = a3s_transport::FrameReader::new(r);
423 match reader.read_frame().await {
424 Ok(Some(f))
425 if f.frame_type == a3s_transport::FrameType::Control
426 && f.payload == EXEC_SPAWN_MAIN_ACK =>
427 {
428 Ok(true)
429 }
430 Ok(Some(f))
431 if f.frame_type == a3s_transport::FrameType::Control
432 && f.payload.starts_with(EXEC_SPAWN_MAIN_NACK) =>
433 {
434 let reason = String::from_utf8_lossy(&f.payload[EXEC_SPAWN_MAIN_NACK.len()..]);
435 Err(BoxError::ExecError(format!(
436 "spawn-main rejected by guest: {reason}"
437 )))
438 }
439 _ => Ok(false),
440 }
441 }
442}
443
444pub struct StreamingExec {
449 reader: ExecFrameReader,
450 writer: Arc<Mutex<ExecFrameWriter>>,
451 started: std::time::Instant,
452 stdout_bytes: u64,
453 stderr_bytes: u64,
454 done: bool,
455}
456
457#[derive(Clone, Debug)]
459pub struct StreamingExecInput {
460 writer: Arc<Mutex<ExecFrameWriter>>,
461}
462
463impl StreamingExecInput {
464 pub async fn write_stdin(&self, data: &[u8]) -> Result<()> {
466 self.writer
467 .lock()
468 .await
469 .write_data(data)
470 .await
471 .map_err(|e| BoxError::ExecError(format!("Streaming exec stdin write failed: {}", e)))
472 }
473
474 pub async fn close_stdin(&self) -> Result<()> {
476 self.writer
477 .lock()
478 .await
479 .write_control(EXEC_CONTROL_STDIN_CLOSE)
480 .await
481 .map_err(|e| {
482 BoxError::ExecError(format!("Streaming exec stdin close write failed: {}", e))
483 })
484 }
485
486 pub async fn cancel(&self) -> Result<()> {
488 self.writer
489 .lock()
490 .await
491 .write_control(EXEC_CONTROL_CANCEL)
492 .await
493 .map_err(|e| BoxError::ExecError(format!("Streaming exec cancel write failed: {}", e)))
494 }
495
496 pub async fn flush(&self) -> Result<()> {
500 self.writer
501 .lock()
502 .await
503 .write_control(EXEC_CONTROL_FLUSH)
504 .await
505 .map_err(|e| BoxError::ExecError(format!("Streaming exec flush write failed: {}", e)))
506 }
507}
508
509impl StreamingExec {
510 pub fn input(&self) -> StreamingExecInput {
512 StreamingExecInput {
513 writer: self.writer.clone(),
514 }
515 }
516
517 pub async fn write_stdin(&self, data: &[u8]) -> Result<()> {
519 self.input().write_stdin(data).await
520 }
521
522 pub async fn close_stdin(&self) -> Result<()> {
524 self.input().close_stdin().await
525 }
526
527 pub async fn flush(&self) -> Result<()> {
530 self.input().flush().await
531 }
532
533 pub async fn next_event(&mut self) -> Result<Option<a3s_box_core::exec::ExecEvent>> {
537 use a3s_box_core::exec::{ExecChunk, ExecEvent, ExecExit};
538
539 if self.done {
540 return Ok(None);
541 }
542
543 let frame = match self.reader.read_frame().await {
544 Ok(Some(f)) => f,
545 Ok(None) => {
546 self.done = true;
547 return Ok(None);
548 }
549 Err(e) => {
550 self.done = true;
551 return Err(BoxError::ExecError(format!(
552 "Streaming exec read failed: {}",
553 e
554 )));
555 }
556 };
557
558 match frame.frame_type {
559 a3s_transport::FrameType::Data => {
560 let chunk: ExecChunk = serde_json::from_slice(&frame.payload).map_err(|e| {
562 BoxError::ExecError(format!("Failed to parse exec chunk: {}", e))
563 })?;
564 match chunk.stream {
565 a3s_box_core::exec::StreamType::Stdout => {
566 self.stdout_bytes += chunk.data.len() as u64;
567 }
568 a3s_box_core::exec::StreamType::Stderr => {
569 self.stderr_bytes += chunk.data.len() as u64;
570 }
571 }
572 Ok(Some(ExecEvent::Chunk(chunk)))
573 }
574 a3s_transport::FrameType::Control => {
575 if frame.payload == EXEC_FLUSH_ACK {
577 return Ok(Some(ExecEvent::FlushAck));
579 }
580 let exit: ExecExit = serde_json::from_slice(&frame.payload).map_err(|e| {
581 BoxError::ExecError(format!("Failed to parse exec exit: {}", e))
582 })?;
583 self.done = true;
584 Ok(Some(ExecEvent::Exit(exit)))
585 }
586 a3s_transport::FrameType::Error => {
587 let msg = String::from_utf8_lossy(&frame.payload);
588 self.done = true;
589 Err(BoxError::ExecError(format!(
590 "Streaming exec error: {}",
591 msg
592 )))
593 }
594 _ => Err(BoxError::ExecError(format!(
595 "Unexpected frame type in stream: {:?}",
596 frame.frame_type
597 ))),
598 }
599 }
600
601 pub async fn cancel(&mut self) -> Result<()> {
606 self.input().cancel().await
607 }
608
609 pub async fn collect(
613 mut self,
614 ) -> Result<(
615 a3s_box_core::exec::ExecOutput,
616 a3s_box_core::exec::ExecMetrics,
617 )> {
618 use a3s_box_core::exec::{ExecEvent, ExecMetrics, ExecOutput};
619
620 let mut stdout = Vec::new();
621 let mut stderr = Vec::new();
622 let mut exit_code = -1;
623
624 while let Some(event) = self.next_event().await? {
625 match event {
626 ExecEvent::Chunk(chunk) => match chunk.stream {
627 a3s_box_core::exec::StreamType::Stdout => stdout.extend_from_slice(&chunk.data),
628 a3s_box_core::exec::StreamType::Stderr => stderr.extend_from_slice(&chunk.data),
629 },
630 ExecEvent::FlushAck => {}
631 ExecEvent::Exit(exit) => {
632 exit_code = exit.exit_code;
633 }
634 }
635 }
636
637 let metrics = ExecMetrics {
638 duration_ms: self.started.elapsed().as_millis() as u64,
639 peak_memory_bytes: None,
640 stdout_bytes: self.stdout_bytes,
641 stderr_bytes: self.stderr_bytes,
642 };
643
644 let output = ExecOutput {
645 stdout,
646 stderr,
647 exit_code,
648 };
649
650 Ok((output, metrics))
651 }
652
653 pub fn is_done(&self) -> bool {
655 self.done
656 }
657
658 pub fn metrics(&self) -> a3s_box_core::exec::ExecMetrics {
660 a3s_box_core::exec::ExecMetrics {
661 duration_ms: self.started.elapsed().as_millis() as u64,
662 peak_memory_bytes: None,
663 stdout_bytes: self.stdout_bytes,
664 stderr_bytes: self.stderr_bytes,
665 }
666 }
667}
668
669impl std::fmt::Debug for StreamingExec {
670 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
671 f.debug_struct("StreamingExec")
672 .field("done", &self.done)
673 .field("stdout_bytes", &self.stdout_bytes)
674 .field("stderr_bytes", &self.stderr_bytes)
675 .finish()
676 }
677}
678
679#[cfg(test)]
680mod tests {
681 use super::*;
682 use tokio::io::AsyncReadExt;
683 use tokio::net::UnixListener;
684
685 fn bind_test_listener(path: &Path) -> Option<UnixListener> {
686 match UnixListener::bind(path) {
687 Ok(listener) => Some(listener),
688 Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
689 eprintln!(
690 "skipping Unix socket test; sandbox denied bind at {}: {}",
691 path.display(),
692 e
693 );
694 None
695 }
696 Err(e) => panic!("failed to bind test socket {}: {}", path.display(), e),
697 }
698 }
699
700 #[tokio::test]
701 async fn test_exec_connect_nonexistent_socket() {
702 let result = ExecClient::connect(Path::new("/tmp/nonexistent-a3s-exec-test.sock")).await;
703 assert!(result.is_err());
704 let err = result.unwrap_err();
705 assert!(matches!(err, BoxError::ExecError(_)));
706 }
707
708 #[tokio::test]
709 async fn test_exec_connect_and_socket_path() {
710 let tmp = tempfile::TempDir::new().unwrap();
711 let sock_path = tmp.path().join("exec.sock");
712 let Some(_listener) = bind_test_listener(&sock_path) else {
713 return;
714 };
715
716 let client = ExecClient::connect(&sock_path).await.unwrap();
717 assert_eq!(client.socket_path(), sock_path);
718 }
719
720 #[tokio::test]
721 async fn test_exec_heartbeat_with_echo_server() {
722 let tmp = tempfile::TempDir::new().unwrap();
723 let sock_path = tmp.path().join("hb_echo.sock");
724 let Some(listener) = bind_test_listener(&sock_path) else {
725 return;
726 };
727
728 tokio::spawn(async move {
729 let (stream, _) = listener.accept().await.unwrap();
731 drop(stream);
732 let (mut stream, _) = listener.accept().await.unwrap();
734 let mut header = [0u8; 5];
736 stream.read_exact(&mut header).await.unwrap();
737 let len = u32::from_be_bytes([header[1], header[2], header[3], header[4]]) as usize;
738 let mut payload = vec![0u8; len];
739 if len > 0 {
740 stream.read_exact(&mut payload).await.unwrap();
741 }
742 let response = a3s_transport::Frame::heartbeat();
744 let encoded = response.encode().unwrap();
745 stream.write_all(&encoded).await.unwrap();
746 });
747
748 tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
749
750 let client = ExecClient::connect(&sock_path).await.unwrap();
751 let result = client.heartbeat().await.unwrap();
752 assert!(result);
753 }
754
755 #[tokio::test]
756 async fn test_exec_heartbeat_no_response() {
757 let tmp = tempfile::TempDir::new().unwrap();
758 let sock_path = tmp.path().join("hb_close.sock");
759 let Some(listener) = bind_test_listener(&sock_path) else {
760 return;
761 };
762
763 tokio::spawn(async move {
764 let (stream, _) = listener.accept().await.unwrap();
766 drop(stream);
767 let (mut stream, _) = listener.accept().await.unwrap();
769 let mut buf = vec![0u8; 1024];
770 let _ = stream.read(&mut buf).await;
771 drop(stream);
772 });
773
774 tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
775
776 let client = ExecClient::connect(&sock_path).await.unwrap();
777 let result = client.heartbeat().await.unwrap();
778 assert!(!result);
779 }
780
781 #[tokio::test]
782 async fn test_exec_heartbeat_nonexistent_socket() {
783 let client = ExecClient {
785 socket_path: PathBuf::from("/tmp/nonexistent-hb-test.sock"),
786 };
787 let result = client.heartbeat().await.unwrap();
788 assert!(!result);
789 }
790
791 #[tokio::test]
792 async fn test_archive_rootfs_streams_data_until_done_marker() {
793 let tmp = tempfile::TempDir::new().unwrap();
794 let sock_path = tmp.path().join("archive.sock");
795 let Some(listener) = bind_test_listener(&sock_path) else {
796 return;
797 };
798
799 tokio::spawn(async move {
800 let (stream, _) = listener.accept().await.unwrap();
802 drop(stream);
803 let (mut stream, _) = listener.accept().await.unwrap();
804 let mut header = [0u8; 5];
805 stream.read_exact(&mut header).await.unwrap();
806 assert_eq!(header[0], a3s_transport::FrameType::Control as u8);
807 let length = u32::from_be_bytes(header[1..5].try_into().unwrap()) as usize;
808 let mut payload = vec![0u8; length];
809 stream.read_exact(&mut payload).await.unwrap();
810 assert_eq!(payload, EXEC_CONTROL_ARCHIVE_ROOTFS);
811
812 for payload in [b"first".as_slice(), b"-second".as_slice()] {
813 let frame = a3s_transport::Frame::data(payload.to_vec());
814 stream.write_all(&frame.encode().unwrap()).await.unwrap();
815 }
816 let done = a3s_transport::Frame::control(EXEC_ARCHIVE_ROOTFS_DONE.to_vec());
817 stream.write_all(&done.encode().unwrap()).await.unwrap();
818 });
819
820 let client = ExecClient::connect(&sock_path).await.unwrap();
821 let output_path = tmp.path().join("rootfs.tar");
822 let mut output = tokio::fs::File::create(&output_path).await.unwrap();
823 let written = client.archive_rootfs(&mut output, false).await.unwrap();
824 drop(output);
825
826 assert_eq!(written, 12);
827 assert_eq!(std::fs::read(output_path).unwrap(), b"first-second");
828 }
829
830 #[tokio::test]
831 async fn test_exec_signal_main_round_trip() {
832 let tmp = tempfile::TempDir::new().unwrap();
833 let sock_path = tmp.path().join("signal_main.sock");
834 let Some(listener) = bind_test_listener(&sock_path) else {
835 return;
836 };
837
838 tokio::spawn(async move {
839 let (stream, _) = listener.accept().await.unwrap();
841 drop(stream);
842 let (stream, _) = listener.accept().await.unwrap();
844 let (r, w) = tokio::io::split(stream);
845 let mut reader = a3s_transport::FrameReader::new(r);
846 let mut writer = a3s_transport::FrameWriter::new(w);
847
848 let frame = reader.read_frame().await.unwrap().unwrap();
849 assert_eq!(frame.frame_type, a3s_transport::FrameType::Control);
850 assert_eq!(frame.payload, b"signal-main:2");
851
852 writer.write_control(EXEC_SIGNAL_MAIN_ACK).await.unwrap();
853 });
854
855 tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
856
857 let client = ExecClient::connect(&sock_path).await.unwrap();
858 let acked = client.signal_main(2).await.unwrap();
860 assert!(acked);
861 }
862
863 #[tokio::test]
864 async fn test_exec_signal_main_nonexistent_socket() {
865 let client = ExecClient {
868 socket_path: PathBuf::from("/tmp/nonexistent-signal-main-test.sock"),
869 };
870 let acked = client.signal_main(15).await.unwrap();
871 assert!(!acked);
872 }
873
874 #[tokio::test]
875 async fn test_exec_client_exec_command() {
876 let tmp = tempfile::TempDir::new().unwrap();
877 let sock_path = tmp.path().join("exec_cmd.sock");
878 let Some(listener) = bind_test_listener(&sock_path) else {
879 return;
880 };
881
882 tokio::spawn(async move {
883 let (stream, _) = listener.accept().await.unwrap();
885 drop(stream);
886 let (stream, _) = listener.accept().await.unwrap();
888 let (r, w) = tokio::io::split(stream);
889 let mut reader = a3s_transport::FrameReader::new(r);
890 let mut writer = a3s_transport::FrameWriter::new(w);
891
892 let _frame = reader.read_frame().await.unwrap().unwrap();
894
895 let output = a3s_box_core::exec::ExecOutput {
897 stdout: b"hello\n".to_vec(),
898 stderr: vec![],
899 exit_code: 0,
900 };
901 let payload = serde_json::to_vec(&output).unwrap();
902 writer.write_data(&payload).await.unwrap();
903 });
904
905 tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
906
907 let client = ExecClient::connect(&sock_path).await.unwrap();
908 let req = a3s_box_core::exec::ExecRequest {
909 cmd: vec!["echo".to_string(), "hello".to_string()],
910 env: vec![],
911 working_dir: None,
912 rootfs: None,
913 user: None,
914 stdin: None,
915 stdin_streaming: false,
916 timeout_ns: 0,
917 streaming: false,
918 };
919 let output = client.exec_command(&req).await.unwrap();
920 assert_eq!(output.exit_code, 0);
921 assert_eq!(&output.stdout[..], b"hello\n");
922 assert!(output.stderr.is_empty());
923 }
924
925 #[tokio::test]
926 async fn test_exec_client_exec_stream_collect() {
927 let tmp = tempfile::TempDir::new().unwrap();
928 let sock_path = tmp.path().join("exec_stream.sock");
929 let Some(listener) = bind_test_listener(&sock_path) else {
930 return;
931 };
932
933 tokio::spawn(async move {
934 let (stream, _) = listener.accept().await.unwrap();
935 drop(stream);
936
937 let (stream, _) = listener.accept().await.unwrap();
938 let (r, w) = tokio::io::split(stream);
939 let mut reader = a3s_transport::FrameReader::new(r);
940 let mut writer = a3s_transport::FrameWriter::new(w);
941
942 let frame = reader.read_frame().await.unwrap().unwrap();
943 let request: a3s_box_core::exec::ExecRequest =
944 serde_json::from_slice(&frame.payload).unwrap();
945 assert!(request.streaming);
946
947 let stdout = a3s_box_core::exec::ExecChunk {
948 stream: a3s_box_core::exec::StreamType::Stdout,
949 data: b"hello ".to_vec(),
950 };
951 writer
952 .write_data(&serde_json::to_vec(&stdout).unwrap())
953 .await
954 .unwrap();
955
956 let stderr = a3s_box_core::exec::ExecChunk {
957 stream: a3s_box_core::exec::StreamType::Stderr,
958 data: b"warn".to_vec(),
959 };
960 writer
961 .write_data(&serde_json::to_vec(&stderr).unwrap())
962 .await
963 .unwrap();
964
965 let exit = a3s_box_core::exec::ExecExit {
966 exit_code: 17,
967 oom_killed: false,
968 };
969 writer
970 .write_control(&serde_json::to_vec(&exit).unwrap())
971 .await
972 .unwrap();
973 });
974
975 tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
976
977 let client = ExecClient::connect(&sock_path).await.unwrap();
978 let req = a3s_box_core::exec::ExecRequest {
979 cmd: vec!["echo".to_string(), "hello".to_string()],
980 env: vec![],
981 working_dir: None,
982 rootfs: None,
983 user: None,
984 stdin: None,
985 stdin_streaming: false,
986 timeout_ns: 0,
987 streaming: false,
988 };
989
990 let stream = client.exec_stream(&req).await.unwrap();
991 let (output, metrics) = stream.collect().await.unwrap();
992 assert_eq!(output.stdout, b"hello ");
993 assert_eq!(output.stderr, b"warn");
994 assert_eq!(output.exit_code, 17);
995 assert_eq!(metrics.stdout_bytes, 6);
996 assert_eq!(metrics.stderr_bytes, 4);
997 }
998
999 #[tokio::test]
1000 async fn test_exec_client_exec_stream_cancel_writes_control_frame() {
1001 let tmp = tempfile::TempDir::new().unwrap();
1002 let sock_path = tmp.path().join("exec_stream_cancel.sock");
1003 let Some(listener) = bind_test_listener(&sock_path) else {
1004 return;
1005 };
1006
1007 tokio::spawn(async move {
1008 let (stream, _) = listener.accept().await.unwrap();
1009 drop(stream);
1010
1011 let (stream, _) = listener.accept().await.unwrap();
1012 let (r, w) = tokio::io::split(stream);
1013 let mut reader = a3s_transport::FrameReader::new(r);
1014 let mut writer = a3s_transport::FrameWriter::new(w);
1015
1016 let frame = reader.read_frame().await.unwrap().unwrap();
1017 let request: a3s_box_core::exec::ExecRequest =
1018 serde_json::from_slice(&frame.payload).unwrap();
1019 assert!(request.streaming);
1020
1021 let cancel = reader.read_frame().await.unwrap().unwrap();
1022 assert_eq!(cancel.frame_type, a3s_transport::FrameType::Control);
1023 assert_eq!(cancel.payload, b"cancel");
1024
1025 let exit = a3s_box_core::exec::ExecExit {
1026 exit_code: 137,
1027 oom_killed: false,
1028 };
1029 writer
1030 .write_control(&serde_json::to_vec(&exit).unwrap())
1031 .await
1032 .unwrap();
1033 });
1034
1035 tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
1036
1037 let client = ExecClient::connect(&sock_path).await.unwrap();
1038 let req = a3s_box_core::exec::ExecRequest {
1039 cmd: vec!["sleep".to_string(), "60".to_string()],
1040 env: vec![],
1041 working_dir: None,
1042 rootfs: None,
1043 user: None,
1044 stdin: None,
1045 stdin_streaming: false,
1046 timeout_ns: 0,
1047 streaming: false,
1048 };
1049
1050 let mut stream = client.exec_stream(&req).await.unwrap();
1051 stream.cancel().await.unwrap();
1052 let event = stream.next_event().await.unwrap().unwrap();
1053 match event {
1054 a3s_box_core::exec::ExecEvent::Exit(exit) => assert_eq!(exit.exit_code, 137),
1055 other => panic!("unexpected event: {other:?}"),
1056 }
1057 }
1058
1059 #[tokio::test]
1060 async fn test_exec_client_exec_stream_input_writes_stdin_and_close() {
1061 let tmp = tempfile::TempDir::new().unwrap();
1062 let sock_path = tmp.path().join("exec_stream_stdin.sock");
1063 let Some(listener) = bind_test_listener(&sock_path) else {
1064 return;
1065 };
1066
1067 tokio::spawn(async move {
1068 let (stream, _) = listener.accept().await.unwrap();
1069 drop(stream);
1070
1071 let (stream, _) = listener.accept().await.unwrap();
1072 let (r, w) = tokio::io::split(stream);
1073 let mut reader = a3s_transport::FrameReader::new(r);
1074 let mut writer = a3s_transport::FrameWriter::new(w);
1075
1076 let frame = reader.read_frame().await.unwrap().unwrap();
1077 let request: a3s_box_core::exec::ExecRequest =
1078 serde_json::from_slice(&frame.payload).unwrap();
1079 assert!(request.streaming);
1080
1081 let stdin = reader.read_frame().await.unwrap().unwrap();
1082 assert_eq!(stdin.frame_type, a3s_transport::FrameType::Data);
1083 assert_eq!(stdin.payload, b"hello stdin\n");
1084
1085 let close = reader.read_frame().await.unwrap().unwrap();
1086 assert_eq!(close.frame_type, a3s_transport::FrameType::Control);
1087 assert_eq!(close.payload, EXEC_CONTROL_STDIN_CLOSE);
1088
1089 let exit = a3s_box_core::exec::ExecExit {
1090 exit_code: 0,
1091 oom_killed: false,
1092 };
1093 writer
1094 .write_control(&serde_json::to_vec(&exit).unwrap())
1095 .await
1096 .unwrap();
1097 });
1098
1099 tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
1100
1101 let client = ExecClient::connect(&sock_path).await.unwrap();
1102 let req = a3s_box_core::exec::ExecRequest {
1103 cmd: vec!["cat".to_string()],
1104 env: vec![],
1105 working_dir: None,
1106 rootfs: None,
1107 user: None,
1108 stdin: None,
1109 stdin_streaming: true,
1110 timeout_ns: 0,
1111 streaming: false,
1112 };
1113
1114 let mut stream = client.exec_stream(&req).await.unwrap();
1115 let input = stream.input();
1116 input.write_stdin(b"hello stdin\n").await.unwrap();
1117 input.close_stdin().await.unwrap();
1118 let event = stream.next_event().await.unwrap().unwrap();
1119 match event {
1120 a3s_box_core::exec::ExecEvent::Exit(exit) => assert_eq!(exit.exit_code, 0),
1121 other => panic!("unexpected event: {other:?}"),
1122 }
1123 }
1124
1125 #[tokio::test]
1126 async fn test_exec_client_flush_sends_control_and_parses_ack_then_exit() {
1127 let tmp = tempfile::TempDir::new().unwrap();
1128 let sock_path = tmp.path().join("exec_stream_flush.sock");
1129 let Some(listener) = bind_test_listener(&sock_path) else {
1130 return;
1131 };
1132
1133 tokio::spawn(async move {
1134 let (stream, _) = listener.accept().await.unwrap();
1135 drop(stream);
1136
1137 let (stream, _) = listener.accept().await.unwrap();
1138 let (r, w) = tokio::io::split(stream);
1139 let mut reader = a3s_transport::FrameReader::new(r);
1140 let mut writer = a3s_transport::FrameWriter::new(w);
1141
1142 let _req = reader.read_frame().await.unwrap().unwrap();
1144 let flush = reader.read_frame().await.unwrap().unwrap();
1145 assert_eq!(flush.frame_type, a3s_transport::FrameType::Control);
1146 assert_eq!(flush.payload, EXEC_CONTROL_FLUSH);
1147
1148 let chunk = a3s_box_core::exec::ExecChunk {
1150 stream: a3s_box_core::exec::StreamType::Stdout,
1151 data: b"pre-rotation\n".to_vec(),
1152 };
1153 writer
1154 .write_data(&serde_json::to_vec(&chunk).unwrap())
1155 .await
1156 .unwrap();
1157 writer.write_control(EXEC_FLUSH_ACK).await.unwrap();
1158 let exit = a3s_box_core::exec::ExecExit {
1159 exit_code: 0,
1160 oom_killed: false,
1161 };
1162 writer
1163 .write_control(&serde_json::to_vec(&exit).unwrap())
1164 .await
1165 .unwrap();
1166 });
1167
1168 tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
1169
1170 let client = ExecClient::connect(&sock_path).await.unwrap();
1171 let req = a3s_box_core::exec::ExecRequest {
1172 cmd: vec!["sh".to_string()],
1173 env: vec![],
1174 working_dir: None,
1175 rootfs: None,
1176 user: None,
1177 stdin: None,
1178 stdin_streaming: false,
1179 timeout_ns: 0,
1180 streaming: false,
1181 };
1182
1183 let mut stream = client.exec_stream(&req).await.unwrap();
1184 stream.flush().await.unwrap();
1185
1186 use a3s_box_core::exec::ExecEvent;
1187 match stream.next_event().await.unwrap().unwrap() {
1188 ExecEvent::Chunk(c) => assert_eq!(c.data, b"pre-rotation\n"),
1189 other => panic!("expected chunk, got {other:?}"),
1190 }
1191 match stream.next_event().await.unwrap().unwrap() {
1194 ExecEvent::FlushAck => {}
1195 other => panic!("expected flush-ack, got {other:?}"),
1196 }
1197 match stream.next_event().await.unwrap().unwrap() {
1198 ExecEvent::Exit(exit) => assert_eq!(exit.exit_code, 0),
1199 other => panic!("expected exit, got {other:?}"),
1200 }
1201 }
1202
1203 #[tokio::test]
1204 async fn test_exec_client_malformed_response() {
1205 let tmp = tempfile::TempDir::new().unwrap();
1206 let sock_path = tmp.path().join("exec_bad.sock");
1207 let Some(listener) = bind_test_listener(&sock_path) else {
1208 return;
1209 };
1210
1211 tokio::spawn(async move {
1212 let (stream, _) = listener.accept().await.unwrap();
1213 drop(stream);
1214 let (mut stream, _) = listener.accept().await.unwrap();
1215 let mut buf = vec![0u8; 4096];
1216 let _ = stream.read(&mut buf).await;
1217 stream.write_all(b"garbage").await.unwrap();
1219 drop(stream);
1220 });
1221
1222 tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
1223
1224 let client = ExecClient::connect(&sock_path).await.unwrap();
1225 let req = a3s_box_core::exec::ExecRequest {
1226 cmd: vec!["test".to_string()],
1227 env: vec![],
1228 working_dir: None,
1229 rootfs: None,
1230 user: None,
1231 stdin: None,
1232 stdin_streaming: false,
1233 timeout_ns: 0,
1234 streaming: false,
1235 };
1236 let result = client.exec_command(&req).await;
1237 assert!(result.is_err());
1238 }
1239}