sqlite_graphrag/
stdin_helper.rs1use crate::errors::AppError;
11use std::io::Read;
12use std::sync::mpsc;
13use std::thread;
14use std::time::Duration;
15
16pub fn read_stdin_with_timeout(secs: u64) -> Result<String, AppError> {
22 let (tx, rx) = mpsc::channel::<std::io::Result<String>>();
23 thread::spawn(move || {
24 let mut buf = String::new();
25 let result = std::io::stdin().read_to_string(&mut buf).map(|_| buf);
26 let _ = tx.send(result);
27 });
28 match rx.recv_timeout(Duration::from_secs(secs)) {
29 Ok(Ok(buf)) => Ok(buf),
30 Ok(Err(e)) => Err(AppError::Io(e)),
31 Err(mpsc::RecvTimeoutError::Timeout) => Err(AppError::Internal(anyhow::anyhow!(
32 "stdin read timed out after {secs}s; pipe must close within timeout window"
33 ))),
34 Err(mpsc::RecvTimeoutError::Disconnected) => Err(AppError::Internal(anyhow::anyhow!(
35 "stdin reader thread disconnected unexpectedly"
36 ))),
37 }
38}
39
40#[cfg(test)]
41mod tests {
42 use super::*;
43 use std::time::Instant;
44
45 #[test]
48 fn read_stdin_with_timeout_returns_internal_error_on_timeout() {
49 let start = Instant::now();
51 let result = read_stdin_with_timeout(1);
52 let elapsed = start.elapsed();
53 match result {
55 Err(AppError::Internal(e)) => {
56 assert!(e.to_string().contains("timed out"), "unexpected error: {e}");
57 assert!(elapsed.as_secs_f64() >= 0.9 && elapsed.as_secs_f64() < 2.5);
58 }
59 Ok(_) | Err(AppError::Io(_)) => {
60 }
62 Err(other) => panic!("unexpected error variant: {other:?}"),
63 }
64 }
65}