hardware_enclave/internal/core/
timeout.rs1#![allow(dead_code, unused_imports, unused_qualifications, unreachable_patterns)]
2use std::io::{self, BufRead, BufReader, Read};
8use std::process::{Child, ExitStatus, Output, Stdio};
9use std::sync::mpsc;
10use std::thread;
11use std::time::{Duration, Instant};
12
13#[derive(Debug)]
15pub enum TimeoutResult<T> {
16 Completed(T),
18 TimedOut,
20}
21
22impl<T> TimeoutResult<T> {
23 pub fn into_option(self) -> Option<T> {
24 match self {
25 TimeoutResult::Completed(v) => Some(v),
26 TimeoutResult::TimedOut => None,
27 }
28 }
29
30 pub fn is_timed_out(&self) -> bool {
31 matches!(self, TimeoutResult::TimedOut)
32 }
33}
34
35const POLL_INTERVAL: Duration = Duration::from_millis(50);
37
38pub fn wait_with_timeout(
41 child: &mut Child,
42 timeout: Duration,
43) -> io::Result<TimeoutResult<ExitStatus>> {
44 let start = Instant::now();
45 loop {
46 match child.try_wait()? {
47 Some(status) => return Ok(TimeoutResult::Completed(status)),
48 None => {
49 if start.elapsed() >= timeout {
50 return Ok(TimeoutResult::TimedOut);
51 }
52 thread::sleep(POLL_INTERVAL);
53 }
54 }
55 }
56}
57
58pub fn wait_output_with_timeout(
64 mut child: Child,
65 timeout: Duration,
66) -> io::Result<TimeoutResult<Output>> {
67 let stdout_thread = child.stdout.take().map(|mut s| {
70 thread::Builder::new()
71 .name("enclaveapp-child-stdout".into())
72 .spawn(move || -> io::Result<Vec<u8>> {
73 let mut buf = Vec::new();
74 s.read_to_end(&mut buf)?;
75 Ok(buf)
76 })
77 });
78 let stderr_thread = child.stderr.take().map(|mut s| {
79 thread::Builder::new()
80 .name("enclaveapp-child-stderr".into())
81 .spawn(move || -> io::Result<Vec<u8>> {
82 let mut buf = Vec::new();
83 s.read_to_end(&mut buf)?;
84 Ok(buf)
85 })
86 });
87
88 match wait_with_timeout(&mut child, timeout)? {
89 TimeoutResult::Completed(status) => {
90 let stdout = match stdout_thread {
91 Some(Ok(t)) => t.join().unwrap_or_else(|_| Ok(Vec::new()))?,
92 _ => Vec::new(),
93 };
94 let stderr = match stderr_thread {
95 Some(Ok(t)) => t.join().unwrap_or_else(|_| Ok(Vec::new()))?,
96 _ => Vec::new(),
97 };
98 Ok(TimeoutResult::Completed(Output {
99 status,
100 stdout,
101 stderr,
102 }))
103 }
104 TimeoutResult::TimedOut => {
105 drop(child.kill());
106 drop(child.wait());
107 Ok(TimeoutResult::TimedOut)
108 }
109 }
110}
111
112pub fn run_with_timeout(
115 mut cmd: std::process::Command,
116 timeout: Duration,
117) -> io::Result<TimeoutResult<Output>> {
118 cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
119 let child = cmd.spawn()?;
120 wait_output_with_timeout(child, timeout)
121}
122
123pub fn run_status_with_timeout(
126 mut cmd: std::process::Command,
127 timeout: Duration,
128) -> io::Result<TimeoutResult<ExitStatus>> {
129 let mut child = cmd.spawn()?;
130 match wait_with_timeout(&mut child, timeout)? {
131 TimeoutResult::Completed(status) => Ok(TimeoutResult::Completed(status)),
132 TimeoutResult::TimedOut => {
133 drop(child.kill());
134 drop(child.wait());
135 Ok(TimeoutResult::TimedOut)
136 }
137 }
138}
139
140#[derive(Debug)]
147pub struct LineReaderWithTimeout {
148 rx: mpsc::Receiver<io::Result<String>>,
149 _thread: thread::JoinHandle<()>,
150}
151
152impl LineReaderWithTimeout {
153 pub fn new<R: Read + Send + 'static>(reader: R) -> Self {
160 Self::spawn(reader, None)
161 }
162
163 pub fn with_max_line_bytes<R: Read + Send + 'static>(reader: R, max_line_bytes: usize) -> Self {
169 Self::spawn(reader, Some(max_line_bytes))
170 }
171
172 fn spawn<R: Read + Send + 'static>(reader: R, max_line_bytes: Option<usize>) -> Self {
173 let (tx, rx) = mpsc::channel();
174 let thread = thread::Builder::new()
175 .name("enclaveapp-line-reader".into())
176 .spawn(move || {
177 let mut buf_reader = BufReader::new(reader);
178 loop {
179 let result = match max_line_bytes {
180 Some(max) => read_line_bounded(&mut buf_reader, max),
181 None => {
182 let mut line = String::new();
183 match buf_reader.read_line(&mut line) {
184 Ok(0) => Ok(None),
185 Ok(_) => Ok(Some(line)),
186 Err(e) => Err(e),
187 }
188 }
189 };
190 match result {
191 Ok(None) => break, Ok(Some(line)) => {
193 if tx.send(Ok(line)).is_err() {
194 break;
195 }
196 }
197 Err(e) => {
198 drop(tx.send(Err(e)));
199 break;
200 }
201 }
202 }
203 })
204 .expect("spawn line reader thread");
205 Self {
206 rx,
207 _thread: thread,
208 }
209 }
210
211 pub fn recv_line(&self, timeout: Duration) -> TimeoutResult<io::Result<String>> {
215 match self.rx.recv_timeout(timeout) {
216 Ok(result) => TimeoutResult::Completed(result),
217 Err(mpsc::RecvTimeoutError::Timeout) => TimeoutResult::TimedOut,
218 Err(mpsc::RecvTimeoutError::Disconnected) => {
219 TimeoutResult::Completed(Ok(String::new()))
220 }
221 }
222 }
223}
224
225pub fn read_line_bounded<R: BufRead>(
236 reader: &mut R,
237 max_bytes: usize,
238) -> io::Result<Option<String>> {
239 let mut buf: Vec<u8> = Vec::new();
240 loop {
241 let available = match reader.fill_buf() {
242 Ok(b) => b,
243 Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
244 Err(e) => return Err(e),
245 };
246 if available.is_empty() {
247 return if buf.is_empty() {
249 Ok(None)
250 } else {
251 String::from_utf8(buf)
252 .map(Some)
253 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
254 };
255 }
256 let remaining = max_bytes.saturating_sub(buf.len());
259 let usable = &available[..available.len().min(remaining + 1)];
260 if let Some(pos) = usable.iter().position(|&b| b == b'\n') {
261 buf.extend_from_slice(&usable[..=pos]);
262 reader.consume(pos + 1);
263 return String::from_utf8(buf)
264 .map(Some)
265 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e));
266 }
267 if remaining == 0 {
274 return Err(io::Error::new(
275 io::ErrorKind::InvalidData,
276 format!("line exceeds {max_bytes}-byte cap before newline"),
277 ));
278 }
279 let take = remaining.min(available.len());
280 buf.extend_from_slice(&available[..take]);
281 reader.consume(take);
282 }
283}
284
285#[cfg(test)]
286#[allow(clippy::unwrap_used, clippy::panic)]
287mod pure_tests {
288 use super::*;
289 use std::io::{self, Cursor};
290
291 #[test]
292 fn timeout_result_completed_into_option_is_some() {
293 let r: TimeoutResult<i32> = TimeoutResult::Completed(42);
294 assert_eq!(r.into_option(), Some(42));
295 }
296
297 #[test]
298 fn timeout_result_timed_out_into_option_is_none() {
299 let r: TimeoutResult<i32> = TimeoutResult::TimedOut;
300 assert_eq!(r.into_option(), None);
301 }
302
303 #[test]
304 fn timeout_result_completed_is_not_timed_out() {
305 let r: TimeoutResult<i32> = TimeoutResult::Completed(1);
306 assert!(!r.is_timed_out());
307 }
308
309 #[test]
310 fn timeout_result_timed_out_is_timed_out() {
311 let r: TimeoutResult<i32> = TimeoutResult::TimedOut;
312 assert!(r.is_timed_out());
313 }
314
315 #[test]
316 fn read_line_bounded_empty_reader_returns_none() {
317 let mut cursor = Cursor::new(b"");
318 let result = read_line_bounded(&mut cursor, 1024).unwrap();
319 assert!(result.is_none());
320 }
321
322 #[test]
323 fn read_line_bounded_single_line_with_newline() {
324 let mut cursor = Cursor::new(b"hello\n");
325 let result = read_line_bounded(&mut cursor, 1024).unwrap();
326 assert_eq!(result.as_deref(), Some("hello\n"));
327 }
328
329 #[test]
330 fn read_line_bounded_eof_without_newline() {
331 let mut cursor = Cursor::new(b"hello");
332 let result = read_line_bounded(&mut cursor, 1024).unwrap();
333 assert_eq!(result.as_deref(), Some("hello"));
334 }
335
336 #[test]
337 fn read_line_bounded_multiple_lines_reads_sequentially() {
338 let mut cursor = Cursor::new(b"first\nsecond\n");
339 let line1 = read_line_bounded(&mut cursor, 1024).unwrap();
340 let line2 = read_line_bounded(&mut cursor, 1024).unwrap();
341 let line3 = read_line_bounded(&mut cursor, 1024).unwrap();
342 assert_eq!(line1.as_deref(), Some("first\n"));
343 assert_eq!(line2.as_deref(), Some("second\n"));
344 assert!(line3.is_none());
345 }
346
347 #[test]
348 fn read_line_bounded_line_exceeds_cap_returns_invalid_data() {
349 let mut cursor = Cursor::new(b"hello\n");
351 let err = read_line_bounded(&mut cursor, 3).unwrap_err();
352 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
353 }
354
355 #[test]
356 fn read_line_bounded_line_at_exact_cap_succeeds() {
357 let mut cursor = Cursor::new(b"hello\n");
359 let result = read_line_bounded(&mut cursor, 5).unwrap();
360 assert_eq!(result.as_deref(), Some("hello\n"));
361 }
362
363 #[test]
364 fn read_line_bounded_max_bytes_zero_newline_first_succeeds() {
365 let mut cursor = Cursor::new(b"\nhello");
367 let result = read_line_bounded(&mut cursor, 0).unwrap();
368 assert_eq!(result.as_deref(), Some("\n"));
369 }
370
371 #[test]
372 fn read_line_bounded_max_bytes_zero_non_newline_first_is_error() {
373 let mut cursor = Cursor::new(b"a\nhello");
374 let err = read_line_bounded(&mut cursor, 0).unwrap_err();
375 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
376 }
377
378 #[test]
379 fn read_line_bounded_utf8_content() {
380 let input = "héllo\n";
381 let mut cursor = Cursor::new(input.as_bytes());
382 let result = read_line_bounded(&mut cursor, 128).unwrap();
383 assert_eq!(result.as_deref(), Some("héllo\n"));
384 }
385
386 #[test]
387 fn read_line_bounded_exactly_max_bytes_at_eof_no_newline() {
388 let mut cursor = Cursor::new(b"abc");
389 let result = read_line_bounded(&mut cursor, 3).unwrap();
390 assert_eq!(result.as_deref(), Some("abc"));
391 }
392
393 #[test]
394 fn read_line_bounded_large_cap_long_line() {
395 let line: Vec<u8> = std::iter::repeat(b'x').take(100).chain([b'\n']).collect();
396 let mut cursor = Cursor::new(line);
397 let result = read_line_bounded(&mut cursor, 200).unwrap();
398 let s = result.unwrap();
399 assert_eq!(s.len(), 101);
400 assert!(s.starts_with('x'));
401 assert!(s.ends_with('\n'));
402 }
403
404 #[test]
405 fn read_line_bounded_empty_line_newline_only() {
406 let mut cursor = Cursor::new(b"\n");
407 let result = read_line_bounded(&mut cursor, 1024).unwrap();
408 assert_eq!(result.as_deref(), Some("\n"));
409 }
410
411 #[test]
412 fn read_line_bounded_after_eof_returns_none() {
413 let mut cursor = Cursor::new(b"hi\n");
414 let _unused = read_line_bounded(&mut cursor, 1024).unwrap();
415 let eof = read_line_bounded(&mut cursor, 1024).unwrap();
416 assert!(eof.is_none());
417 }
418
419 #[test]
420 fn read_line_bounded_only_newlines() {
421 let mut cursor = Cursor::new(b"\n\n\n");
422 let r1 = read_line_bounded(&mut cursor, 10).unwrap();
423 let r2 = read_line_bounded(&mut cursor, 10).unwrap();
424 let r3 = read_line_bounded(&mut cursor, 10).unwrap();
425 let r4 = read_line_bounded(&mut cursor, 10).unwrap();
426 assert_eq!(r1.as_deref(), Some("\n"));
427 assert_eq!(r2.as_deref(), Some("\n"));
428 assert_eq!(r3.as_deref(), Some("\n"));
429 assert!(r4.is_none());
430 }
431
432 #[test]
433 fn read_line_bounded_single_char_at_eof() {
434 let mut cursor = Cursor::new(b"x");
435 let result = read_line_bounded(&mut cursor, 1).unwrap();
436 assert_eq!(result.as_deref(), Some("x"));
437 }
438
439 #[test]
440 fn read_line_bounded_error_message_contains_cap() {
441 let mut cursor = Cursor::new(b"toolongline\n");
442 let err = read_line_bounded(&mut cursor, 5).unwrap_err();
443 assert!(err.to_string().contains("5"));
444 }
445}
446
447#[cfg(all(test, unix))]
448#[allow(clippy::unwrap_used, clippy::panic, let_underscore_drop)]
449mod tests {
450 use super::*;
451 use std::process::Command;
452
453 #[cfg(unix)]
454 #[test]
455 fn run_with_timeout_completes_fast_command() {
456 let result = run_with_timeout(
457 {
458 let mut c = Command::new("/bin/sh");
459 c.args(["-c", "echo hello"]);
460 c
461 },
462 Duration::from_secs(5),
463 )
464 .unwrap();
465 match result {
466 TimeoutResult::Completed(output) => {
467 assert!(output.status.success());
468 assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "hello");
469 }
470 TimeoutResult::TimedOut => panic!("fast command should not time out"),
471 }
472 }
473
474 #[cfg(unix)]
475 #[test]
476 fn run_with_timeout_kills_slow_command() {
477 let start = Instant::now();
478 let result = run_with_timeout(
479 {
480 let mut c = Command::new("/bin/sh");
481 c.args(["-c", "sleep 10"]);
482 c
483 },
484 Duration::from_millis(200),
485 )
486 .unwrap();
487 assert!(result.is_timed_out());
488 assert!(start.elapsed() < Duration::from_secs(2));
490 }
491
492 #[cfg(unix)]
493 #[test]
494 fn line_reader_delivers_line_within_timeout() {
495 use std::io::Write;
496 let mut cmd = Command::new("/bin/sh");
497 cmd.args(["-c", "cat"])
498 .stdin(Stdio::piped())
499 .stdout(Stdio::piped());
500 let mut child = cmd.spawn().unwrap();
501 let r = child.stdout.take().unwrap();
502 let mut w = child.stdin.take().unwrap();
503 let reader = LineReaderWithTimeout::new(r);
504 writeln!(w, "hello world").unwrap();
505 w.flush().unwrap();
506 match reader.recv_line(Duration::from_secs(2)) {
507 TimeoutResult::Completed(Ok(line)) => assert_eq!(line.trim(), "hello world"),
508 other => panic!("unexpected result: {:?}", other),
509 }
510 drop(w);
512 drop(child.wait());
513 }
514
515 #[cfg(unix)]
516 #[test]
517 fn bounded_line_reader_aborts_when_line_exceeds_cap() {
518 use std::io::Write;
528 let mut cmd = Command::new("/bin/sh");
529 cmd.args(["-c", "seq 1 200 | xargs printf 'x%.0s' && printf '\\n'"])
530 .stdin(Stdio::null())
531 .stdout(Stdio::piped());
532 let mut child = cmd.spawn().unwrap();
533 let stdout = child.stdout.take().unwrap();
534 let reader = LineReaderWithTimeout::with_max_line_bytes(stdout, 100);
535 match reader.recv_line(Duration::from_secs(2)) {
536 TimeoutResult::Completed(Err(e)) => {
537 assert_eq!(e.kind(), io::ErrorKind::InvalidData);
538 }
539 other => panic!("expected InvalidData, got: {:?}", other),
540 }
541 drop(child.kill());
542 drop(child.wait());
543 let _ = Write::flush(&mut io::stdout());
544 }
545
546 #[cfg(unix)]
547 #[test]
548 fn bounded_line_reader_accepts_line_within_cap() {
549 use std::io::Write;
550 let mut cmd = Command::new("/bin/sh");
551 cmd.args(["-c", "printf 'short line\\n'"])
552 .stdin(Stdio::null())
553 .stdout(Stdio::piped());
554 let mut child = cmd.spawn().unwrap();
555 let stdout = child.stdout.take().unwrap();
556 let reader = LineReaderWithTimeout::with_max_line_bytes(stdout, 1024);
557 match reader.recv_line(Duration::from_secs(2)) {
558 TimeoutResult::Completed(Ok(line)) => assert_eq!(line, "short line\n"),
559 other => panic!("expected short line, got: {:?}", other),
560 }
561 drop(child.wait());
562 let _ = Write::flush(&mut io::stdout());
563 }
564
565 #[cfg(unix)]
566 #[test]
567 fn line_reader_times_out_when_no_data() {
568 let mut cmd = Command::new("/bin/sh");
569 cmd.args(["-c", "sleep 10"]).stdout(Stdio::piped());
570 let mut child = cmd.spawn().unwrap();
571 let stdout = child.stdout.take().unwrap();
572 let reader = LineReaderWithTimeout::new(stdout);
573 let start = Instant::now();
574 let result = reader.recv_line(Duration::from_millis(200));
575 assert!(result.is_timed_out());
576 assert!(start.elapsed() < Duration::from_secs(1));
577 drop(child.kill());
578 drop(child.wait());
579 }
580
581 #[cfg(unix)]
582 #[test]
583 fn run_status_with_timeout_completes_fast_command() {
584 let mut cmd = Command::new("/bin/sh");
585 cmd.args(["-c", "exit 0"]);
586 let result = run_status_with_timeout(cmd, Duration::from_secs(5)).unwrap();
587 match result {
588 TimeoutResult::Completed(status) => assert!(status.success()),
589 TimeoutResult::TimedOut => panic!("fast command should not time out"),
590 }
591 }
592
593 #[cfg(unix)]
594 #[test]
595 fn run_status_with_timeout_kills_slow_command() {
596 let start = Instant::now();
597 let mut cmd = Command::new("/bin/sh");
598 cmd.args(["-c", "sleep 10"]);
599 let result = run_status_with_timeout(cmd, Duration::from_millis(200)).unwrap();
600 assert!(result.is_timed_out());
601 assert!(start.elapsed() < Duration::from_secs(2));
602 }
603
604 #[test]
605 fn line_reader_eof_disconnects_and_returns_empty_string() {
606 let reader = LineReaderWithTimeout::new(io::Cursor::new(b""));
610 thread::sleep(Duration::from_millis(50));
612 let result = reader.recv_line(Duration::from_millis(200));
613 assert!(
614 matches!(result, TimeoutResult::Completed(Ok(ref s)) if s.is_empty()),
615 "expected Completed(Ok(\"\")) after sender disconnect, got: {result:?}"
616 );
617 }
618}