Skip to content

Commit da53fc1

Browse files
committed
Bound login-shell env reader wait on timeout
1 parent 60ca3d5 commit da53fc1

1 file changed

Lines changed: 41 additions & 6 deletions

File tree

desktop/src-tauri/src/lib.rs

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use std::net::{Ipv4Addr, SocketAddrV4, TcpStream};
88
use std::path::PathBuf;
99
use std::process::Stdio;
1010
use std::sync::atomic::{AtomicBool, Ordering};
11-
use std::sync::{Arc, Mutex};
11+
use std::sync::{mpsc, Arc, Mutex};
1212
use std::thread;
1313
use std::time::{Duration, Instant};
1414

@@ -22,6 +22,7 @@ const PREFERRED_PORT: u16 = 8080;
2222
const READY_TIMEOUT: Duration = Duration::from_secs(30);
2323
const READY_POLL_INTERVAL: Duration = Duration::from_millis(125);
2424
const LOGIN_SHELL_ENV_TIMEOUT: Duration = Duration::from_secs(3);
25+
const LOGIN_SHELL_READER_TIMEOUT: Duration = Duration::from_millis(300);
2526

2627
type DynError = Box<dyn Error>;
2728
type CommandRx = Receiver<CommandEvent>;
@@ -186,9 +187,10 @@ fn run_login_shell_env(shell: &str, timeout: Duration) -> Option<Vec<u8>> {
186187
.spawn()
187188
.ok()?;
188189
let mut stdout = child.stdout.take()?;
189-
let reader = thread::spawn(move || {
190+
let (tx, rx) = mpsc::sync_channel(1);
191+
thread::spawn(move || {
190192
let mut out = Vec::new();
191-
stdout.read_to_end(&mut out).ok().map(|_| out)
193+
let _ = tx.send(stdout.read_to_end(&mut out).ok().map(|_| out));
192194
});
193195

194196
let deadline = Instant::now() + timeout;
@@ -201,19 +203,19 @@ fn run_login_shell_env(shell: &str, timeout: Duration) -> Option<Vec<u8>> {
201203
if Instant::now() >= deadline {
202204
let _ = child.kill();
203205
let _ = child.wait();
204-
let _ = reader.join();
206+
let _ = rx.recv_timeout(LOGIN_SHELL_READER_TIMEOUT);
205207
return None;
206208
}
207209
thread::sleep(Duration::from_millis(25));
208210
}
209211
}
210212
};
211213
if !status.success() {
212-
let _ = reader.join();
214+
let _ = rx.recv_timeout(LOGIN_SHELL_READER_TIMEOUT);
213215
return None;
214216
}
215217

216-
reader.join().ok().flatten()
218+
rx.recv_timeout(LOGIN_SHELL_READER_TIMEOUT).ok().flatten()
217219
}
218220

219221
fn parse_nul_env(content: &[u8]) -> Vec<(OsString, OsString)> {
@@ -624,4 +626,37 @@ mod tests {
624626
output.len()
625627
);
626628
}
629+
630+
#[cfg(unix)]
631+
#[test]
632+
fn run_login_shell_env_timeout_returns_when_stdout_fd_stays_open() {
633+
let stamp = SystemTime::now()
634+
.duration_since(UNIX_EPOCH)
635+
.expect("valid clock")
636+
.as_nanos();
637+
let script_path = std::env::temp_dir().join(format!(
638+
"agentsview-login-shell-timeout-{stamp}-{}.sh",
639+
std::process::id()
640+
));
641+
fs::write(&script_path, "#!/bin/sh\n(sleep 2) &\nsleep 10\n").expect("write shell script");
642+
let mut perms = fs::metadata(&script_path)
643+
.expect("read shell script metadata")
644+
.permissions();
645+
perms.set_mode(0o700);
646+
fs::set_permissions(&script_path, perms).expect("set executable permissions");
647+
648+
let started = Instant::now();
649+
let output = run_login_shell_env(
650+
script_path.to_str().expect("script path utf-8"),
651+
Duration::from_millis(120),
652+
);
653+
let elapsed = started.elapsed();
654+
let _ = fs::remove_file(&script_path);
655+
656+
assert!(output.is_none(), "timeout path should return None");
657+
assert!(
658+
elapsed < Duration::from_secs(1),
659+
"timeout path took too long: {elapsed:?}"
660+
);
661+
}
627662
}

0 commit comments

Comments
 (0)