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