1use serde::{Deserialize, Serialize};
7
8pub const EXEC_VSOCK_PORT: u32 = a3s_transport::ports::EXEC_SERVER;
10
11pub const PORT_FWD_VSOCK_PORT: u32 = 4093;
13
14pub const DEFAULT_EXEC_TIMEOUT_NS: u64 = 5_000_000_000;
16
17pub const MAX_OUTPUT_BYTES: usize = 16 * 1024 * 1024;
19
20pub const FRAME_EXEC_CHUNK: u8 = 0x01;
22
23pub const FRAME_EXEC_EXIT: u8 = 0x02;
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct ExecRequest {
29 pub cmd: Vec<String>,
31 pub timeout_ns: u64,
33 #[serde(default)]
35 pub env: Vec<String>,
36 #[serde(default)]
38 pub working_dir: Option<String>,
39 #[serde(default)]
41 pub rootfs: Option<String>,
42 #[serde(default)]
44 pub stdin: Option<Vec<u8>>,
45 #[serde(default)]
47 pub stdin_streaming: bool,
48 #[serde(default)]
50 pub user: Option<String>,
51 #[serde(default)]
53 pub streaming: bool,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct ExecOutput {
59 pub stdout: Vec<u8>,
61 pub stderr: Vec<u8>,
63 pub exit_code: i32,
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
69pub enum StreamType {
70 Stdout,
72 Stderr,
74}
75
76impl std::fmt::Display for StreamType {
77 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 match self {
79 StreamType::Stdout => write!(f, "stdout"),
80 StreamType::Stderr => write!(f, "stderr"),
81 }
82 }
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct ExecChunk {
88 pub stream: StreamType,
90 pub data: Vec<u8>,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct ExecExit {
97 pub exit_code: i32,
99 #[serde(default)]
103 pub oom_killed: bool,
104}
105
106#[derive(Debug, Clone)]
109pub enum ExecEvent {
110 Chunk(ExecChunk),
112 FlushAck,
117 Exit(ExecExit),
119}
120
121#[derive(Debug, Clone, Default, Serialize, Deserialize)]
123pub struct ExecMetrics {
124 pub duration_ms: u64,
126 #[serde(default)]
128 pub peak_memory_bytes: Option<u64>,
129 pub stdout_bytes: u64,
131 pub stderr_bytes: u64,
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct FileRequest {
138 pub op: FileOp,
140 pub guest_path: String,
142 #[serde(default)]
144 pub data: Option<String>,
145}
146
147#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
149pub enum FileOp {
150 Upload,
152 Download,
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct FileResponse {
159 pub success: bool,
161 #[serde(default)]
163 pub data: Option<String>,
164 #[serde(default)]
166 pub size: u64,
167 #[serde(default)]
169 pub error: Option<String>,
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 #[test]
177 fn test_exec_request_serialization_roundtrip() {
178 let req = ExecRequest {
179 cmd: vec!["ls".to_string(), "-la".to_string()],
180 timeout_ns: 3_000_000_000,
181 env: vec!["FOO=bar".to_string()],
182 working_dir: Some("/tmp".to_string()),
183 rootfs: Some("/run/a3s/cri/rootfs/sb/c/rootfs".to_string()),
184 stdin: None,
185 stdin_streaming: false,
186 user: None,
187 streaming: false,
188 };
189 let json = serde_json::to_string(&req).unwrap();
190 let parsed: ExecRequest = serde_json::from_str(&json).unwrap();
191 assert_eq!(parsed.cmd, vec!["ls", "-la"]);
192 assert_eq!(parsed.timeout_ns, 3_000_000_000);
193 assert_eq!(parsed.env, vec!["FOO=bar"]);
194 assert_eq!(parsed.working_dir, Some("/tmp".to_string()));
195 assert_eq!(
196 parsed.rootfs,
197 Some("/run/a3s/cri/rootfs/sb/c/rootfs".to_string())
198 );
199 assert!(parsed.stdin.is_none());
200 assert!(!parsed.stdin_streaming);
201 assert!(parsed.user.is_none());
202 assert!(!parsed.streaming);
203 }
204
205 #[test]
206 fn test_exec_request_streaming_flag() {
207 let req = ExecRequest {
208 cmd: vec!["tail".to_string(), "-f".to_string()],
209 timeout_ns: 0,
210 env: vec![],
211 working_dir: None,
212 rootfs: None,
213 stdin: None,
214 stdin_streaming: false,
215 user: None,
216 streaming: true,
217 };
218 let json = serde_json::to_string(&req).unwrap();
219 let parsed: ExecRequest = serde_json::from_str(&json).unwrap();
220 assert!(parsed.streaming);
221 assert!(!parsed.stdin_streaming);
222 }
223
224 #[test]
225 fn test_exec_request_stdin_streaming_flag() {
226 let req = ExecRequest {
227 cmd: vec!["cat".to_string()],
228 timeout_ns: 0,
229 env: vec![],
230 working_dir: None,
231 rootfs: None,
232 stdin: None,
233 stdin_streaming: true,
234 user: None,
235 streaming: true,
236 };
237 let json = serde_json::to_string(&req).unwrap();
238 let parsed: ExecRequest = serde_json::from_str(&json).unwrap();
239 assert!(parsed.stdin_streaming);
240 }
241
242 #[test]
243 fn test_exec_output_serialization_roundtrip() {
244 let output = ExecOutput {
245 stdout: b"hello\n".to_vec(),
246 stderr: b"warning\n".to_vec(),
247 exit_code: 0,
248 };
249 let json = serde_json::to_string(&output).unwrap();
250 let parsed: ExecOutput = serde_json::from_str(&json).unwrap();
251 assert_eq!(parsed.stdout, b"hello\n");
252 assert_eq!(parsed.stderr, b"warning\n");
253 assert_eq!(parsed.exit_code, 0);
254 }
255
256 #[test]
257 fn test_exec_output_non_zero_exit() {
258 let output = ExecOutput {
259 stdout: vec![],
260 stderr: b"not found\n".to_vec(),
261 exit_code: 127,
262 };
263 let json = serde_json::to_string(&output).unwrap();
264 let parsed: ExecOutput = serde_json::from_str(&json).unwrap();
265 assert_eq!(parsed.exit_code, 127);
266 assert!(parsed.stdout.is_empty());
267 }
268
269 #[test]
270 fn test_default_timeout_constant() {
271 assert_eq!(DEFAULT_EXEC_TIMEOUT_NS, 5_000_000_000);
272 }
273
274 #[test]
275 fn test_max_output_bytes_constant() {
276 assert_eq!(MAX_OUTPUT_BYTES, 16 * 1024 * 1024);
277 }
278
279 #[test]
280 fn test_exec_request_empty_cmd() {
281 let req = ExecRequest {
282 cmd: vec![],
283 timeout_ns: 0,
284 env: vec![],
285 working_dir: None,
286 rootfs: None,
287 stdin: None,
288 stdin_streaming: false,
289 user: None,
290 streaming: false,
291 };
292 let json = serde_json::to_string(&req).unwrap();
293 let parsed: ExecRequest = serde_json::from_str(&json).unwrap();
294 assert!(parsed.cmd.is_empty());
295 assert_eq!(parsed.timeout_ns, 0);
296 assert!(parsed.env.is_empty());
297 assert!(parsed.working_dir.is_none());
298 assert!(parsed.rootfs.is_none());
299 assert!(!parsed.stdin_streaming);
300 assert!(parsed.user.is_none());
301 }
302
303 #[test]
304 fn test_exec_request_backward_compatible_deserialization() {
305 let json = r#"{"cmd":["ls"],"timeout_ns":0}"#;
307 let parsed: ExecRequest = serde_json::from_str(json).unwrap();
308 assert_eq!(parsed.cmd, vec!["ls"]);
309 assert!(parsed.env.is_empty());
310 assert!(parsed.working_dir.is_none());
311 assert!(parsed.rootfs.is_none());
312 assert!(parsed.stdin.is_none());
313 assert!(!parsed.stdin_streaming);
314 assert!(parsed.user.is_none());
315 assert!(!parsed.streaming);
316 }
317
318 #[test]
319 fn test_exec_request_with_stdin() {
320 let req = ExecRequest {
321 cmd: vec!["sh".to_string()],
322 timeout_ns: 0,
323 env: vec![],
324 working_dir: None,
325 rootfs: None,
326 stdin: Some(b"echo hello\n".to_vec()),
327 stdin_streaming: false,
328 user: None,
329 streaming: false,
330 };
331 let json = serde_json::to_string(&req).unwrap();
332 let parsed: ExecRequest = serde_json::from_str(&json).unwrap();
333 assert_eq!(parsed.stdin, Some(b"echo hello\n".to_vec()));
334 assert!(!parsed.stdin_streaming);
335 }
336
337 #[test]
338 fn test_exec_request_with_user() {
339 let req = ExecRequest {
340 cmd: vec!["whoami".to_string()],
341 timeout_ns: 0,
342 env: vec![],
343 working_dir: None,
344 rootfs: None,
345 stdin: None,
346 stdin_streaming: false,
347 user: Some("root".to_string()),
348 streaming: false,
349 };
350 let json = serde_json::to_string(&req).unwrap();
351 let parsed: ExecRequest = serde_json::from_str(&json).unwrap();
352 assert_eq!(parsed.user, Some("root".to_string()));
353 }
354
355 #[test]
356 fn test_exec_request_with_user_uid_gid() {
357 let req = ExecRequest {
358 cmd: vec!["id".to_string()],
359 timeout_ns: 0,
360 env: vec![],
361 working_dir: None,
362 rootfs: None,
363 stdin: None,
364 stdin_streaming: false,
365 user: Some("1000:1000".to_string()),
366 streaming: false,
367 };
368 let json = serde_json::to_string(&req).unwrap();
369 let parsed: ExecRequest = serde_json::from_str(&json).unwrap();
370 assert_eq!(parsed.user, Some("1000:1000".to_string()));
371 }
372
373 #[test]
374 fn test_exec_output_empty() {
375 let output = ExecOutput {
376 stdout: vec![],
377 stderr: vec![],
378 exit_code: 0,
379 };
380 assert!(output.stdout.is_empty());
381 assert!(output.stderr.is_empty());
382 assert_eq!(output.exit_code, 0);
383 }
384
385 #[test]
388 fn test_stream_type_display() {
389 assert_eq!(StreamType::Stdout.to_string(), "stdout");
390 assert_eq!(StreamType::Stderr.to_string(), "stderr");
391 }
392
393 #[test]
394 fn test_exec_chunk_serde_roundtrip() {
395 let chunk = ExecChunk {
396 stream: StreamType::Stdout,
397 data: b"hello world\n".to_vec(),
398 };
399 let json = serde_json::to_string(&chunk).unwrap();
400 let parsed: ExecChunk = serde_json::from_str(&json).unwrap();
401 assert_eq!(parsed.stream, StreamType::Stdout);
402 assert_eq!(parsed.data, b"hello world\n");
403 }
404
405 #[test]
406 fn test_exec_chunk_stderr() {
407 let chunk = ExecChunk {
408 stream: StreamType::Stderr,
409 data: b"error: not found\n".to_vec(),
410 };
411 let json = serde_json::to_string(&chunk).unwrap();
412 let parsed: ExecChunk = serde_json::from_str(&json).unwrap();
413 assert_eq!(parsed.stream, StreamType::Stderr);
414 }
415
416 #[test]
417 fn test_exec_exit_serde_roundtrip() {
418 let exit = ExecExit {
419 exit_code: 42,
420 oom_killed: false,
421 };
422 let json = serde_json::to_string(&exit).unwrap();
423 let parsed: ExecExit = serde_json::from_str(&json).unwrap();
424 assert_eq!(parsed.exit_code, 42);
425 }
426
427 #[test]
428 fn test_exec_metrics_default() {
429 let m = ExecMetrics::default();
430 assert_eq!(m.duration_ms, 0);
431 assert!(m.peak_memory_bytes.is_none());
432 assert_eq!(m.stdout_bytes, 0);
433 assert_eq!(m.stderr_bytes, 0);
434 }
435
436 #[test]
437 fn test_exec_metrics_serde_roundtrip() {
438 let m = ExecMetrics {
439 duration_ms: 1234,
440 peak_memory_bytes: Some(65536),
441 stdout_bytes: 100,
442 stderr_bytes: 50,
443 };
444 let json = serde_json::to_string(&m).unwrap();
445 let parsed: ExecMetrics = serde_json::from_str(&json).unwrap();
446 assert_eq!(parsed.duration_ms, 1234);
447 assert_eq!(parsed.peak_memory_bytes, Some(65536));
448 assert_eq!(parsed.stdout_bytes, 100);
449 assert_eq!(parsed.stderr_bytes, 50);
450 }
451
452 #[test]
455 fn test_file_request_upload() {
456 let req = FileRequest {
457 op: FileOp::Upload,
458 guest_path: "/tmp/test.txt".to_string(),
459 data: Some("aGVsbG8=".to_string()),
460 };
461 let json = serde_json::to_string(&req).unwrap();
462 let parsed: FileRequest = serde_json::from_str(&json).unwrap();
463 assert_eq!(parsed.op, FileOp::Upload);
464 assert_eq!(parsed.guest_path, "/tmp/test.txt");
465 assert_eq!(parsed.data.as_deref(), Some("aGVsbG8="));
466 }
467
468 #[test]
469 fn test_file_request_download() {
470 let req = FileRequest {
471 op: FileOp::Download,
472 guest_path: "/etc/hostname".to_string(),
473 data: None,
474 };
475 let json = serde_json::to_string(&req).unwrap();
476 let parsed: FileRequest = serde_json::from_str(&json).unwrap();
477 assert_eq!(parsed.op, FileOp::Download);
478 assert!(parsed.data.is_none());
479 }
480
481 #[test]
482 fn test_file_response_success() {
483 let resp = FileResponse {
484 success: true,
485 data: Some("Y29udGVudA==".to_string()),
486 size: 7,
487 error: None,
488 };
489 let json = serde_json::to_string(&resp).unwrap();
490 let parsed: FileResponse = serde_json::from_str(&json).unwrap();
491 assert!(parsed.success);
492 assert_eq!(parsed.size, 7);
493 assert!(parsed.error.is_none());
494 }
495
496 #[test]
497 fn test_file_response_error() {
498 let resp = FileResponse {
499 success: false,
500 data: None,
501 size: 0,
502 error: Some("file not found".to_string()),
503 };
504 let json = serde_json::to_string(&resp).unwrap();
505 let parsed: FileResponse = serde_json::from_str(&json).unwrap();
506 assert!(!parsed.success);
507 assert_eq!(parsed.error.as_deref(), Some("file not found"));
508 }
509
510 #[test]
511 fn test_frame_exec_constants() {
512 assert_eq!(FRAME_EXEC_CHUNK, 0x01);
513 assert_eq!(FRAME_EXEC_EXIT, 0x02);
514 }
515}