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_FLUSH_ACK: &[u8] = b"flush-ack";
19const EXEC_SIGNAL_MAIN_ACK: &[u8] = b"signal-main-ack";
23const EXEC_SPAWN_MAIN_ACK: &[u8] = b"spawn-main-ack";
27
28const EXEC_HOST_SLACK_SECS: u64 = 10;
32const SIGNAL_MAIN_ACK_TIMEOUT_SECS: u64 = 10;
36
37type ExecFrameReader = a3s_transport::FrameReader<tokio::io::ReadHalf<tokio::net::UnixStream>>;
38type ExecFrameWriter = a3s_transport::FrameWriter<tokio::io::WriteHalf<tokio::net::UnixStream>>;
39
40#[derive(Debug)]
45pub struct ExecClient {
46 socket_path: PathBuf,
47}
48
49impl ExecClient {
50 pub async fn connect(socket_path: &Path) -> Result<Self> {
54 let _stream = UnixStream::connect(socket_path).await.map_err(|e| {
55 BoxError::ExecError(format!(
56 "Failed to connect to exec server at {}: {}",
57 socket_path.display(),
58 e,
59 ))
60 })?;
61
62 Ok(Self {
63 socket_path: socket_path.to_path_buf(),
64 })
65 }
66
67 pub fn socket_path(&self) -> &Path {
69 &self.socket_path
70 }
71
72 pub async fn exec_command(
76 &self,
77 request: &a3s_box_core::exec::ExecRequest,
78 ) -> Result<a3s_box_core::exec::ExecOutput> {
79 let payload = serde_json::to_vec(request)
80 .map_err(|e| BoxError::ExecError(format!("Failed to serialize exec request: {}", e)))?;
81
82 let mut stream = UnixStream::connect(&self.socket_path).await.map_err(|e| {
83 BoxError::ExecError(format!(
84 "Exec connection failed to {}: {}",
85 self.socket_path.display(),
86 e,
87 ))
88 })?;
89
90 let request_frame = a3s_transport::Frame::data(payload);
92 let encoded = request_frame.encode().map_err(|e| {
93 BoxError::ExecError(format!("Failed to encode exec request frame: {}", e))
94 })?;
95 stream
96 .write_all(&encoded)
97 .await
98 .map_err(|e| BoxError::ExecError(format!("Exec request write failed: {}", e)))?;
99
100 let (r, _w) = tokio::io::split(stream);
107 let mut reader = a3s_transport::FrameReader::new(r);
108 let host_deadline = std::time::Duration::from_nanos(request.timeout_ns)
109 .saturating_add(std::time::Duration::from_secs(EXEC_HOST_SLACK_SECS));
110 let frame = tokio::time::timeout(host_deadline, reader.read_frame())
111 .await
112 .map_err(|_| {
113 BoxError::ExecError(format!(
114 "Exec response timed out after {host_deadline:?} (guest may be wedged)"
115 ))
116 })?
117 .map_err(|e| BoxError::ExecError(format!("Exec response read failed: {}", e)))?
118 .ok_or_else(|| {
119 BoxError::ExecError("Exec server closed without response".to_string())
120 })?;
121
122 match frame.frame_type {
123 a3s_transport::FrameType::Data => {
124 let output: a3s_box_core::exec::ExecOutput = serde_json::from_slice(&frame.payload)
125 .map_err(|e| {
126 BoxError::ExecError(format!("Failed to parse exec response: {}", e))
127 })?;
128 Ok(output)
129 }
130 a3s_transport::FrameType::Error => {
131 let msg = String::from_utf8_lossy(&frame.payload);
132 Err(BoxError::ExecError(format!("Exec server error: {}", msg)))
133 }
134 _ => Err(BoxError::ExecError(format!(
135 "Unexpected frame type: {:?}",
136 frame.frame_type
137 ))),
138 }
139 }
140
141 pub async fn exec_stream(
149 &self,
150 request: &a3s_box_core::exec::ExecRequest,
151 ) -> Result<StreamingExec> {
152 let mut req = request.clone();
153 req.streaming = true;
154
155 let payload = serde_json::to_vec(&req)
156 .map_err(|e| BoxError::ExecError(format!("Failed to serialize exec request: {}", e)))?;
157
158 let stream = UnixStream::connect(&self.socket_path).await.map_err(|e| {
159 BoxError::ExecError(format!(
160 "Exec connection failed to {}: {}",
161 self.socket_path.display(),
162 e,
163 ))
164 })?;
165
166 let (r, w) = tokio::io::split(stream);
167 let mut writer = a3s_transport::FrameWriter::new(w);
168 writer
169 .write_data(&payload)
170 .await
171 .map_err(|e| BoxError::ExecError(format!("Exec request write failed: {}", e)))?;
172
173 let reader = a3s_transport::FrameReader::new(r);
174 let started = std::time::Instant::now();
175
176 Ok(StreamingExec {
177 reader,
178 writer: Arc::new(Mutex::new(writer)),
179 started,
180 stdout_bytes: 0,
181 stderr_bytes: 0,
182 done: false,
183 })
184 }
185
186 pub async fn file_transfer(
190 &self,
191 request: &a3s_box_core::exec::FileRequest,
192 ) -> Result<a3s_box_core::exec::FileResponse> {
193 let payload = serde_json::to_vec(request)
194 .map_err(|e| BoxError::ExecError(format!("Failed to serialize file request: {}", e)))?;
195
196 let mut stream = UnixStream::connect(&self.socket_path).await.map_err(|e| {
197 BoxError::ExecError(format!(
198 "Exec connection failed to {}: {}",
199 self.socket_path.display(),
200 e,
201 ))
202 })?;
203
204 let request_frame = a3s_transport::Frame::data(payload);
205 let encoded = request_frame.encode().map_err(|e| {
206 BoxError::ExecError(format!("Failed to encode file request frame: {}", e))
207 })?;
208 stream
209 .write_all(&encoded)
210 .await
211 .map_err(|e| BoxError::ExecError(format!("File request write failed: {}", e)))?;
212
213 let (r, _w) = tokio::io::split(stream);
214 let mut reader = a3s_transport::FrameReader::new(r);
215 let frame = reader
216 .read_frame()
217 .await
218 .map_err(|e| BoxError::ExecError(format!("File response read failed: {}", e)))?
219 .ok_or_else(|| {
220 BoxError::ExecError("Exec server closed without response".to_string())
221 })?;
222
223 match frame.frame_type {
224 a3s_transport::FrameType::Data => {
225 let response: a3s_box_core::exec::FileResponse =
226 serde_json::from_slice(&frame.payload).map_err(|e| {
227 BoxError::ExecError(format!("Failed to parse file response: {}", e))
228 })?;
229 Ok(response)
230 }
231 a3s_transport::FrameType::Error => {
232 let msg = String::from_utf8_lossy(&frame.payload);
233 Err(BoxError::ExecError(format!("File transfer error: {}", msg)))
234 }
235 _ => Err(BoxError::ExecError(format!(
236 "Unexpected frame type: {:?}",
237 frame.frame_type
238 ))),
239 }
240 }
241
242 pub async fn heartbeat(&self) -> Result<bool> {
246 let mut stream = match UnixStream::connect(&self.socket_path).await {
247 Ok(s) => s,
248 Err(_) => return Ok(false),
249 };
250
251 let frame = a3s_transport::Frame::heartbeat();
252 let encoded = match frame.encode() {
253 Ok(e) => e,
254 Err(_) => return Ok(false),
255 };
256
257 if stream.write_all(&encoded).await.is_err() {
258 return Ok(false);
259 }
260
261 let (r, _w) = tokio::io::split(stream);
262 let mut reader = a3s_transport::FrameReader::new(r);
263 match reader.read_frame().await {
264 Ok(Some(f)) if f.frame_type == a3s_transport::FrameType::Heartbeat => Ok(true),
265 _ => Ok(false),
266 }
267 }
268
269 pub async fn signal_main(&self, signal: i32) -> Result<bool> {
275 let mut stream = match UnixStream::connect(&self.socket_path).await {
276 Ok(s) => s,
277 Err(_) => return Ok(false),
278 };
279
280 let payload = format!("signal-main:{}", signal).into_bytes();
281 let frame = a3s_transport::Frame::control(payload);
282 let encoded = frame
283 .encode()
284 .map_err(|e| BoxError::ExecError(format!("signal-main frame encode failed: {}", e)))?;
285
286 if stream.write_all(&encoded).await.is_err() {
287 return Ok(false);
288 }
289
290 let (r, _w) = tokio::io::split(stream);
296 let mut reader = a3s_transport::FrameReader::new(r);
297 let read = tokio::time::timeout(
298 std::time::Duration::from_secs(SIGNAL_MAIN_ACK_TIMEOUT_SECS),
299 reader.read_frame(),
300 )
301 .await;
302 match read {
303 Ok(Ok(Some(f)))
304 if f.frame_type == a3s_transport::FrameType::Control
305 && f.payload == EXEC_SIGNAL_MAIN_ACK =>
306 {
307 Ok(true)
308 }
309 _ => Ok(false),
310 }
311 }
312
313 pub async fn spawn_main(&self, spec_json: Option<&[u8]>) -> Result<bool> {
318 let mut stream = match UnixStream::connect(&self.socket_path).await {
319 Ok(s) => s,
320 Err(_) => return Ok(false),
321 };
322
323 let mut payload = b"spawn-main:".to_vec();
324 if let Some(json) = spec_json {
325 payload.extend_from_slice(json);
326 }
327 let frame = a3s_transport::Frame::control(payload);
328 let encoded = frame
329 .encode()
330 .map_err(|e| BoxError::ExecError(format!("spawn-main frame encode failed: {}", e)))?;
331
332 if stream.write_all(&encoded).await.is_err() {
333 return Ok(false);
334 }
335
336 let (r, _w) = tokio::io::split(stream);
337 let mut reader = a3s_transport::FrameReader::new(r);
338 match reader.read_frame().await {
339 Ok(Some(f))
340 if f.frame_type == a3s_transport::FrameType::Control
341 && f.payload == EXEC_SPAWN_MAIN_ACK =>
342 {
343 Ok(true)
344 }
345 _ => Ok(false),
346 }
347 }
348}
349
350pub struct StreamingExec {
355 reader: ExecFrameReader,
356 writer: Arc<Mutex<ExecFrameWriter>>,
357 started: std::time::Instant,
358 stdout_bytes: u64,
359 stderr_bytes: u64,
360 done: bool,
361}
362
363#[derive(Clone, Debug)]
365pub struct StreamingExecInput {
366 writer: Arc<Mutex<ExecFrameWriter>>,
367}
368
369impl StreamingExecInput {
370 pub async fn write_stdin(&self, data: &[u8]) -> Result<()> {
372 self.writer
373 .lock()
374 .await
375 .write_data(data)
376 .await
377 .map_err(|e| BoxError::ExecError(format!("Streaming exec stdin write failed: {}", e)))
378 }
379
380 pub async fn close_stdin(&self) -> Result<()> {
382 self.writer
383 .lock()
384 .await
385 .write_control(EXEC_CONTROL_STDIN_CLOSE)
386 .await
387 .map_err(|e| {
388 BoxError::ExecError(format!("Streaming exec stdin close write failed: {}", e))
389 })
390 }
391
392 pub async fn cancel(&self) -> Result<()> {
394 self.writer
395 .lock()
396 .await
397 .write_control(EXEC_CONTROL_CANCEL)
398 .await
399 .map_err(|e| BoxError::ExecError(format!("Streaming exec cancel write failed: {}", e)))
400 }
401
402 pub async fn flush(&self) -> Result<()> {
406 self.writer
407 .lock()
408 .await
409 .write_control(EXEC_CONTROL_FLUSH)
410 .await
411 .map_err(|e| BoxError::ExecError(format!("Streaming exec flush write failed: {}", e)))
412 }
413}
414
415impl StreamingExec {
416 pub fn input(&self) -> StreamingExecInput {
418 StreamingExecInput {
419 writer: self.writer.clone(),
420 }
421 }
422
423 pub async fn write_stdin(&self, data: &[u8]) -> Result<()> {
425 self.input().write_stdin(data).await
426 }
427
428 pub async fn close_stdin(&self) -> Result<()> {
430 self.input().close_stdin().await
431 }
432
433 pub async fn flush(&self) -> Result<()> {
436 self.input().flush().await
437 }
438
439 pub async fn next_event(&mut self) -> Result<Option<a3s_box_core::exec::ExecEvent>> {
443 use a3s_box_core::exec::{ExecChunk, ExecEvent, ExecExit};
444
445 if self.done {
446 return Ok(None);
447 }
448
449 let frame = match self.reader.read_frame().await {
450 Ok(Some(f)) => f,
451 Ok(None) => {
452 self.done = true;
453 return Ok(None);
454 }
455 Err(e) => {
456 self.done = true;
457 return Err(BoxError::ExecError(format!(
458 "Streaming exec read failed: {}",
459 e
460 )));
461 }
462 };
463
464 match frame.frame_type {
465 a3s_transport::FrameType::Data => {
466 let chunk: ExecChunk = serde_json::from_slice(&frame.payload).map_err(|e| {
468 BoxError::ExecError(format!("Failed to parse exec chunk: {}", e))
469 })?;
470 match chunk.stream {
471 a3s_box_core::exec::StreamType::Stdout => {
472 self.stdout_bytes += chunk.data.len() as u64;
473 }
474 a3s_box_core::exec::StreamType::Stderr => {
475 self.stderr_bytes += chunk.data.len() as u64;
476 }
477 }
478 Ok(Some(ExecEvent::Chunk(chunk)))
479 }
480 a3s_transport::FrameType::Control => {
481 if frame.payload == EXEC_FLUSH_ACK {
483 return Ok(Some(ExecEvent::FlushAck));
485 }
486 let exit: ExecExit = serde_json::from_slice(&frame.payload).map_err(|e| {
487 BoxError::ExecError(format!("Failed to parse exec exit: {}", e))
488 })?;
489 self.done = true;
490 Ok(Some(ExecEvent::Exit(exit)))
491 }
492 a3s_transport::FrameType::Error => {
493 let msg = String::from_utf8_lossy(&frame.payload);
494 self.done = true;
495 Err(BoxError::ExecError(format!(
496 "Streaming exec error: {}",
497 msg
498 )))
499 }
500 _ => Err(BoxError::ExecError(format!(
501 "Unexpected frame type in stream: {:?}",
502 frame.frame_type
503 ))),
504 }
505 }
506
507 pub async fn cancel(&mut self) -> Result<()> {
512 self.input().cancel().await
513 }
514
515 pub async fn collect(
519 mut self,
520 ) -> Result<(
521 a3s_box_core::exec::ExecOutput,
522 a3s_box_core::exec::ExecMetrics,
523 )> {
524 use a3s_box_core::exec::{ExecEvent, ExecMetrics, ExecOutput};
525
526 let mut stdout = Vec::new();
527 let mut stderr = Vec::new();
528 let mut exit_code = -1;
529
530 while let Some(event) = self.next_event().await? {
531 match event {
532 ExecEvent::Chunk(chunk) => match chunk.stream {
533 a3s_box_core::exec::StreamType::Stdout => stdout.extend_from_slice(&chunk.data),
534 a3s_box_core::exec::StreamType::Stderr => stderr.extend_from_slice(&chunk.data),
535 },
536 ExecEvent::FlushAck => {}
537 ExecEvent::Exit(exit) => {
538 exit_code = exit.exit_code;
539 }
540 }
541 }
542
543 let metrics = ExecMetrics {
544 duration_ms: self.started.elapsed().as_millis() as u64,
545 peak_memory_bytes: None,
546 stdout_bytes: self.stdout_bytes,
547 stderr_bytes: self.stderr_bytes,
548 };
549
550 let output = ExecOutput {
551 stdout,
552 stderr,
553 exit_code,
554 };
555
556 Ok((output, metrics))
557 }
558
559 pub fn is_done(&self) -> bool {
561 self.done
562 }
563
564 pub fn metrics(&self) -> a3s_box_core::exec::ExecMetrics {
566 a3s_box_core::exec::ExecMetrics {
567 duration_ms: self.started.elapsed().as_millis() as u64,
568 peak_memory_bytes: None,
569 stdout_bytes: self.stdout_bytes,
570 stderr_bytes: self.stderr_bytes,
571 }
572 }
573}
574
575impl std::fmt::Debug for StreamingExec {
576 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
577 f.debug_struct("StreamingExec")
578 .field("done", &self.done)
579 .field("stdout_bytes", &self.stdout_bytes)
580 .field("stderr_bytes", &self.stderr_bytes)
581 .finish()
582 }
583}
584
585#[cfg(test)]
586mod tests {
587 use super::*;
588 use tokio::io::AsyncReadExt;
589 use tokio::net::UnixListener;
590
591 fn bind_test_listener(path: &Path) -> Option<UnixListener> {
592 match UnixListener::bind(path) {
593 Ok(listener) => Some(listener),
594 Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
595 eprintln!(
596 "skipping Unix socket test; sandbox denied bind at {}: {}",
597 path.display(),
598 e
599 );
600 None
601 }
602 Err(e) => panic!("failed to bind test socket {}: {}", path.display(), e),
603 }
604 }
605
606 #[tokio::test]
607 async fn test_exec_connect_nonexistent_socket() {
608 let result = ExecClient::connect(Path::new("/tmp/nonexistent-a3s-exec-test.sock")).await;
609 assert!(result.is_err());
610 let err = result.unwrap_err();
611 assert!(matches!(err, BoxError::ExecError(_)));
612 }
613
614 #[tokio::test]
615 async fn test_exec_connect_and_socket_path() {
616 let tmp = tempfile::TempDir::new().unwrap();
617 let sock_path = tmp.path().join("exec.sock");
618 let Some(_listener) = bind_test_listener(&sock_path) else {
619 return;
620 };
621
622 let client = ExecClient::connect(&sock_path).await.unwrap();
623 assert_eq!(client.socket_path(), sock_path);
624 }
625
626 #[tokio::test]
627 async fn test_exec_heartbeat_with_echo_server() {
628 let tmp = tempfile::TempDir::new().unwrap();
629 let sock_path = tmp.path().join("hb_echo.sock");
630 let Some(listener) = bind_test_listener(&sock_path) else {
631 return;
632 };
633
634 tokio::spawn(async move {
635 let (stream, _) = listener.accept().await.unwrap();
637 drop(stream);
638 let (mut stream, _) = listener.accept().await.unwrap();
640 let mut header = [0u8; 5];
642 stream.read_exact(&mut header).await.unwrap();
643 let len = u32::from_be_bytes([header[1], header[2], header[3], header[4]]) as usize;
644 let mut payload = vec![0u8; len];
645 if len > 0 {
646 stream.read_exact(&mut payload).await.unwrap();
647 }
648 let response = a3s_transport::Frame::heartbeat();
650 let encoded = response.encode().unwrap();
651 stream.write_all(&encoded).await.unwrap();
652 });
653
654 tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
655
656 let client = ExecClient::connect(&sock_path).await.unwrap();
657 let result = client.heartbeat().await.unwrap();
658 assert!(result);
659 }
660
661 #[tokio::test]
662 async fn test_exec_heartbeat_no_response() {
663 let tmp = tempfile::TempDir::new().unwrap();
664 let sock_path = tmp.path().join("hb_close.sock");
665 let Some(listener) = bind_test_listener(&sock_path) else {
666 return;
667 };
668
669 tokio::spawn(async move {
670 let (stream, _) = listener.accept().await.unwrap();
672 drop(stream);
673 let (mut stream, _) = listener.accept().await.unwrap();
675 let mut buf = vec![0u8; 1024];
676 let _ = stream.read(&mut buf).await;
677 drop(stream);
678 });
679
680 tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
681
682 let client = ExecClient::connect(&sock_path).await.unwrap();
683 let result = client.heartbeat().await.unwrap();
684 assert!(!result);
685 }
686
687 #[tokio::test]
688 async fn test_exec_heartbeat_nonexistent_socket() {
689 let client = ExecClient {
691 socket_path: PathBuf::from("/tmp/nonexistent-hb-test.sock"),
692 };
693 let result = client.heartbeat().await.unwrap();
694 assert!(!result);
695 }
696
697 #[tokio::test]
698 async fn test_exec_signal_main_round_trip() {
699 let tmp = tempfile::TempDir::new().unwrap();
700 let sock_path = tmp.path().join("signal_main.sock");
701 let Some(listener) = bind_test_listener(&sock_path) else {
702 return;
703 };
704
705 tokio::spawn(async move {
706 let (stream, _) = listener.accept().await.unwrap();
708 drop(stream);
709 let (stream, _) = listener.accept().await.unwrap();
711 let (r, w) = tokio::io::split(stream);
712 let mut reader = a3s_transport::FrameReader::new(r);
713 let mut writer = a3s_transport::FrameWriter::new(w);
714
715 let frame = reader.read_frame().await.unwrap().unwrap();
716 assert_eq!(frame.frame_type, a3s_transport::FrameType::Control);
717 assert_eq!(frame.payload, b"signal-main:2");
718
719 writer.write_control(EXEC_SIGNAL_MAIN_ACK).await.unwrap();
720 });
721
722 tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
723
724 let client = ExecClient::connect(&sock_path).await.unwrap();
725 let acked = client.signal_main(2).await.unwrap();
727 assert!(acked);
728 }
729
730 #[tokio::test]
731 async fn test_exec_signal_main_nonexistent_socket() {
732 let client = ExecClient {
735 socket_path: PathBuf::from("/tmp/nonexistent-signal-main-test.sock"),
736 };
737 let acked = client.signal_main(15).await.unwrap();
738 assert!(!acked);
739 }
740
741 #[tokio::test]
742 async fn test_exec_client_exec_command() {
743 let tmp = tempfile::TempDir::new().unwrap();
744 let sock_path = tmp.path().join("exec_cmd.sock");
745 let Some(listener) = bind_test_listener(&sock_path) else {
746 return;
747 };
748
749 tokio::spawn(async move {
750 let (stream, _) = listener.accept().await.unwrap();
752 drop(stream);
753 let (stream, _) = listener.accept().await.unwrap();
755 let (r, w) = tokio::io::split(stream);
756 let mut reader = a3s_transport::FrameReader::new(r);
757 let mut writer = a3s_transport::FrameWriter::new(w);
758
759 let _frame = reader.read_frame().await.unwrap().unwrap();
761
762 let output = a3s_box_core::exec::ExecOutput {
764 stdout: b"hello\n".to_vec(),
765 stderr: vec![],
766 exit_code: 0,
767 };
768 let payload = serde_json::to_vec(&output).unwrap();
769 writer.write_data(&payload).await.unwrap();
770 });
771
772 tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
773
774 let client = ExecClient::connect(&sock_path).await.unwrap();
775 let req = a3s_box_core::exec::ExecRequest {
776 cmd: vec!["echo".to_string(), "hello".to_string()],
777 env: vec![],
778 working_dir: None,
779 rootfs: None,
780 user: None,
781 stdin: None,
782 stdin_streaming: false,
783 timeout_ns: 0,
784 streaming: false,
785 };
786 let output = client.exec_command(&req).await.unwrap();
787 assert_eq!(output.exit_code, 0);
788 assert_eq!(&output.stdout[..], b"hello\n");
789 assert!(output.stderr.is_empty());
790 }
791
792 #[tokio::test]
793 async fn test_exec_client_exec_stream_collect() {
794 let tmp = tempfile::TempDir::new().unwrap();
795 let sock_path = tmp.path().join("exec_stream.sock");
796 let Some(listener) = bind_test_listener(&sock_path) else {
797 return;
798 };
799
800 tokio::spawn(async move {
801 let (stream, _) = listener.accept().await.unwrap();
802 drop(stream);
803
804 let (stream, _) = listener.accept().await.unwrap();
805 let (r, w) = tokio::io::split(stream);
806 let mut reader = a3s_transport::FrameReader::new(r);
807 let mut writer = a3s_transport::FrameWriter::new(w);
808
809 let frame = reader.read_frame().await.unwrap().unwrap();
810 let request: a3s_box_core::exec::ExecRequest =
811 serde_json::from_slice(&frame.payload).unwrap();
812 assert!(request.streaming);
813
814 let stdout = a3s_box_core::exec::ExecChunk {
815 stream: a3s_box_core::exec::StreamType::Stdout,
816 data: b"hello ".to_vec(),
817 };
818 writer
819 .write_data(&serde_json::to_vec(&stdout).unwrap())
820 .await
821 .unwrap();
822
823 let stderr = a3s_box_core::exec::ExecChunk {
824 stream: a3s_box_core::exec::StreamType::Stderr,
825 data: b"warn".to_vec(),
826 };
827 writer
828 .write_data(&serde_json::to_vec(&stderr).unwrap())
829 .await
830 .unwrap();
831
832 let exit = a3s_box_core::exec::ExecExit {
833 exit_code: 17,
834 oom_killed: false,
835 };
836 writer
837 .write_control(&serde_json::to_vec(&exit).unwrap())
838 .await
839 .unwrap();
840 });
841
842 tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
843
844 let client = ExecClient::connect(&sock_path).await.unwrap();
845 let req = a3s_box_core::exec::ExecRequest {
846 cmd: vec!["echo".to_string(), "hello".to_string()],
847 env: vec![],
848 working_dir: None,
849 rootfs: None,
850 user: None,
851 stdin: None,
852 stdin_streaming: false,
853 timeout_ns: 0,
854 streaming: false,
855 };
856
857 let stream = client.exec_stream(&req).await.unwrap();
858 let (output, metrics) = stream.collect().await.unwrap();
859 assert_eq!(output.stdout, b"hello ");
860 assert_eq!(output.stderr, b"warn");
861 assert_eq!(output.exit_code, 17);
862 assert_eq!(metrics.stdout_bytes, 6);
863 assert_eq!(metrics.stderr_bytes, 4);
864 }
865
866 #[tokio::test]
867 async fn test_exec_client_exec_stream_cancel_writes_control_frame() {
868 let tmp = tempfile::TempDir::new().unwrap();
869 let sock_path = tmp.path().join("exec_stream_cancel.sock");
870 let Some(listener) = bind_test_listener(&sock_path) else {
871 return;
872 };
873
874 tokio::spawn(async move {
875 let (stream, _) = listener.accept().await.unwrap();
876 drop(stream);
877
878 let (stream, _) = listener.accept().await.unwrap();
879 let (r, w) = tokio::io::split(stream);
880 let mut reader = a3s_transport::FrameReader::new(r);
881 let mut writer = a3s_transport::FrameWriter::new(w);
882
883 let frame = reader.read_frame().await.unwrap().unwrap();
884 let request: a3s_box_core::exec::ExecRequest =
885 serde_json::from_slice(&frame.payload).unwrap();
886 assert!(request.streaming);
887
888 let cancel = reader.read_frame().await.unwrap().unwrap();
889 assert_eq!(cancel.frame_type, a3s_transport::FrameType::Control);
890 assert_eq!(cancel.payload, b"cancel");
891
892 let exit = a3s_box_core::exec::ExecExit {
893 exit_code: 137,
894 oom_killed: false,
895 };
896 writer
897 .write_control(&serde_json::to_vec(&exit).unwrap())
898 .await
899 .unwrap();
900 });
901
902 tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
903
904 let client = ExecClient::connect(&sock_path).await.unwrap();
905 let req = a3s_box_core::exec::ExecRequest {
906 cmd: vec!["sleep".to_string(), "60".to_string()],
907 env: vec![],
908 working_dir: None,
909 rootfs: None,
910 user: None,
911 stdin: None,
912 stdin_streaming: false,
913 timeout_ns: 0,
914 streaming: false,
915 };
916
917 let mut stream = client.exec_stream(&req).await.unwrap();
918 stream.cancel().await.unwrap();
919 let event = stream.next_event().await.unwrap().unwrap();
920 match event {
921 a3s_box_core::exec::ExecEvent::Exit(exit) => assert_eq!(exit.exit_code, 137),
922 other => panic!("unexpected event: {other:?}"),
923 }
924 }
925
926 #[tokio::test]
927 async fn test_exec_client_exec_stream_input_writes_stdin_and_close() {
928 let tmp = tempfile::TempDir::new().unwrap();
929 let sock_path = tmp.path().join("exec_stream_stdin.sock");
930 let Some(listener) = bind_test_listener(&sock_path) else {
931 return;
932 };
933
934 tokio::spawn(async move {
935 let (stream, _) = listener.accept().await.unwrap();
936 drop(stream);
937
938 let (stream, _) = listener.accept().await.unwrap();
939 let (r, w) = tokio::io::split(stream);
940 let mut reader = a3s_transport::FrameReader::new(r);
941 let mut writer = a3s_transport::FrameWriter::new(w);
942
943 let frame = reader.read_frame().await.unwrap().unwrap();
944 let request: a3s_box_core::exec::ExecRequest =
945 serde_json::from_slice(&frame.payload).unwrap();
946 assert!(request.streaming);
947
948 let stdin = reader.read_frame().await.unwrap().unwrap();
949 assert_eq!(stdin.frame_type, a3s_transport::FrameType::Data);
950 assert_eq!(stdin.payload, b"hello stdin\n");
951
952 let close = reader.read_frame().await.unwrap().unwrap();
953 assert_eq!(close.frame_type, a3s_transport::FrameType::Control);
954 assert_eq!(close.payload, EXEC_CONTROL_STDIN_CLOSE);
955
956 let exit = a3s_box_core::exec::ExecExit {
957 exit_code: 0,
958 oom_killed: false,
959 };
960 writer
961 .write_control(&serde_json::to_vec(&exit).unwrap())
962 .await
963 .unwrap();
964 });
965
966 tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
967
968 let client = ExecClient::connect(&sock_path).await.unwrap();
969 let req = a3s_box_core::exec::ExecRequest {
970 cmd: vec!["cat".to_string()],
971 env: vec![],
972 working_dir: None,
973 rootfs: None,
974 user: None,
975 stdin: None,
976 stdin_streaming: true,
977 timeout_ns: 0,
978 streaming: false,
979 };
980
981 let mut stream = client.exec_stream(&req).await.unwrap();
982 let input = stream.input();
983 input.write_stdin(b"hello stdin\n").await.unwrap();
984 input.close_stdin().await.unwrap();
985 let event = stream.next_event().await.unwrap().unwrap();
986 match event {
987 a3s_box_core::exec::ExecEvent::Exit(exit) => assert_eq!(exit.exit_code, 0),
988 other => panic!("unexpected event: {other:?}"),
989 }
990 }
991
992 #[tokio::test]
993 async fn test_exec_client_flush_sends_control_and_parses_ack_then_exit() {
994 let tmp = tempfile::TempDir::new().unwrap();
995 let sock_path = tmp.path().join("exec_stream_flush.sock");
996 let Some(listener) = bind_test_listener(&sock_path) else {
997 return;
998 };
999
1000 tokio::spawn(async move {
1001 let (stream, _) = listener.accept().await.unwrap();
1002 drop(stream);
1003
1004 let (stream, _) = listener.accept().await.unwrap();
1005 let (r, w) = tokio::io::split(stream);
1006 let mut reader = a3s_transport::FrameReader::new(r);
1007 let mut writer = a3s_transport::FrameWriter::new(w);
1008
1009 let _req = reader.read_frame().await.unwrap().unwrap();
1011 let flush = reader.read_frame().await.unwrap().unwrap();
1012 assert_eq!(flush.frame_type, a3s_transport::FrameType::Control);
1013 assert_eq!(flush.payload, EXEC_CONTROL_FLUSH);
1014
1015 let chunk = a3s_box_core::exec::ExecChunk {
1017 stream: a3s_box_core::exec::StreamType::Stdout,
1018 data: b"pre-rotation\n".to_vec(),
1019 };
1020 writer
1021 .write_data(&serde_json::to_vec(&chunk).unwrap())
1022 .await
1023 .unwrap();
1024 writer.write_control(EXEC_FLUSH_ACK).await.unwrap();
1025 let exit = a3s_box_core::exec::ExecExit {
1026 exit_code: 0,
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!["sh".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.flush().await.unwrap();
1052
1053 use a3s_box_core::exec::ExecEvent;
1054 match stream.next_event().await.unwrap().unwrap() {
1055 ExecEvent::Chunk(c) => assert_eq!(c.data, b"pre-rotation\n"),
1056 other => panic!("expected chunk, got {other:?}"),
1057 }
1058 match stream.next_event().await.unwrap().unwrap() {
1061 ExecEvent::FlushAck => {}
1062 other => panic!("expected flush-ack, got {other:?}"),
1063 }
1064 match stream.next_event().await.unwrap().unwrap() {
1065 ExecEvent::Exit(exit) => assert_eq!(exit.exit_code, 0),
1066 other => panic!("expected exit, got {other:?}"),
1067 }
1068 }
1069
1070 #[tokio::test]
1071 async fn test_exec_client_malformed_response() {
1072 let tmp = tempfile::TempDir::new().unwrap();
1073 let sock_path = tmp.path().join("exec_bad.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 let (mut stream, _) = listener.accept().await.unwrap();
1082 let mut buf = vec![0u8; 4096];
1083 let _ = stream.read(&mut buf).await;
1084 stream.write_all(b"garbage").await.unwrap();
1086 drop(stream);
1087 });
1088
1089 tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
1090
1091 let client = ExecClient::connect(&sock_path).await.unwrap();
1092 let req = a3s_box_core::exec::ExecRequest {
1093 cmd: vec!["test".to_string()],
1094 env: vec![],
1095 working_dir: None,
1096 rootfs: None,
1097 user: None,
1098 stdin: None,
1099 stdin_streaming: false,
1100 timeout_ns: 0,
1101 streaming: false,
1102 };
1103 let result = client.exec_command(&req).await;
1104 assert!(result.is_err());
1105 }
1106}