@@ -8,7 +8,7 @@ use std::net::{Ipv4Addr, SocketAddrV4, TcpStream};
88use std:: path:: PathBuf ;
99use std:: process:: Stdio ;
1010use std:: sync:: atomic:: { AtomicBool , Ordering } ;
11- use std:: sync:: { Arc , Mutex } ;
11+ use std:: sync:: { mpsc , Arc , Mutex } ;
1212use std:: thread;
1313use std:: time:: { Duration , Instant } ;
1414
@@ -22,6 +22,7 @@ const PREFERRED_PORT: u16 = 8080;
2222const READY_TIMEOUT : Duration = Duration :: from_secs ( 30 ) ;
2323const READY_POLL_INTERVAL : Duration = Duration :: from_millis ( 125 ) ;
2424const LOGIN_SHELL_ENV_TIMEOUT : Duration = Duration :: from_secs ( 3 ) ;
25+ const LOGIN_SHELL_READER_TIMEOUT : Duration = Duration :: from_millis ( 300 ) ;
2526
2627type DynError = Box < dyn Error > ;
2728type 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
219221fn 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) &\n sleep 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