@@ -1058,23 +1058,57 @@ mod tests {
10581058 }
10591059
10601060 /// PowerShell starts a ping that outlives it unless the whole tree is stopped,
1061- /// and writes the ping's pid to $env:BIAB_PIDFILE.
1061+ /// writes the ping's pid to $env:BIAB_PIDFILE, and says how far it got .
10621062 #[ cfg( windows) ]
1063- const TREE : & str = "$p = Start-Process -FilePath ping.exe -ArgumentList '-n','60','127.0.0.1' -PassThru -WindowStyle Hidden; \
1064- Set-Content -LiteralPath $env:BIAB_PIDFILE -Value $p.Id; $p.WaitForExit()";
1063+ const TREE : & str = "'up'; $p = Start-Process -FilePath ping.exe -ArgumentList '-n','60','127.0.0.1' -PassThru -WindowStyle Hidden; \
1064+ \" ping $($p.Id) \" ; Set-Content -LiteralPath $env:BIAB_PIDFILE -Value $p.Id; 'written' ; $p.WaitForExit()";
10651065
10661066 #[ cfg( windows) ]
1067- fn wait_for_pid ( file : & Path ) -> String {
1067+ fn wait_for_pid ( file : & Path , within : Duration ) -> Option < String > {
10681068 let t = Instant :: now ( ) ;
1069- while t. elapsed ( ) < Duration :: from_secs ( 60 ) {
1069+ while t. elapsed ( ) < within {
10701070 if let Ok ( pid) = std:: fs:: read_to_string ( file) {
10711071 if !pid. trim ( ) . is_empty ( ) {
1072- return pid. trim ( ) . to_string ( ) ;
1072+ return Some ( pid. trim ( ) . to_string ( ) ) ;
10731073 }
10741074 }
10751075 thread:: sleep ( TICK ) ;
10761076 }
1077- panic ! ( "the ping never started" ) ;
1077+ None
1078+ }
1079+
1080+ /// DIAG: what the machine shows of a PowerShell that did not write its pid.
1081+ #[ cfg( windows) ]
1082+ fn diag_dump ( why : & str ) -> String {
1083+ let me = std:: process:: id ( ) ;
1084+ let script = format ! (
1085+ r#"
1086+ $ErrorActionPreference = 'Continue'
1087+ "== {why}: tree under the test process {me}, $(Get-Date -f o)"
1088+ $all = Get-CimInstance Win32_Process
1089+ function t($id, $d) {{ $all | Where-Object ParentProcessId -eq $id | ForEach-Object {{ (' ' * $d) + "$($_.ProcessId) $($_.Name) created $($_.CreationDate.ToString('HH:mm:ss.fff')) :: $($_.CommandLine)"; t $_.ProcessId ($d + 1) }} }}
1090+ t {me} 0
1091+ "== every ping, powershell, conhost, WerFault"
1092+ $all | Where-Object {{ $_.Name -in 'ping.exe','powershell.exe','conhost.exe','WerFault.exe','pwsh.exe' }} | ForEach-Object {{ "$($_.ProcessId) parent $($_.ParentProcessId) $($_.Name) created $($_.CreationDate.ToString('HH:mm:ss.fff'))" }}
1093+ $kids = @($all | Where-Object {{ $_.ParentProcessId -eq {me} -and $_.Name -eq 'powershell.exe' }})
1094+ foreach ($k in $kids) {{
1095+ "== threads of powershell $($k.ProcessId)"
1096+ (Get-Process -Id $k.ProcessId -ErrorAction SilentlyContinue).Threads | ForEach-Object {{ " tid $($_.Id) state $($_.ThreadState) wait $($_.WaitReason) cpu $($_.TotalProcessorTime)" }}
1097+ "== modules of powershell $($k.ProcessId)"
1098+ ((Get-Process -Id $k.ProcessId -ErrorAction SilentlyContinue).Modules | ForEach-Object ModuleName) -join ' '
1099+ "== PowerShell event log for $($k.ProcessId)"
1100+ Get-WinEvent -LogName 'Microsoft-Windows-PowerShell/Operational' -MaxEvents 400 -ErrorAction SilentlyContinue | Where-Object ProcessId -eq $k.ProcessId | Sort-Object TimeCreated | ForEach-Object {{ " $($_.TimeCreated.ToString('HH:mm:ss.fff')) $($_.Id) $(($_.Message -split ""`n"")[0])" }}
1101+ $cdb = "${{env:ProgramFiles(x86)}}\Windows Kits\10\Debuggers\x64\cdb.exe"
1102+ if (Test-Path $cdb) {{
1103+ "== stacks of powershell $($k.ProcessId) (cdb)"
1104+ & $cdb -pv -p $k.ProcessId -c '.loadby sos clr; ~*e !clrstack; ~*kc 40; qd' 2>&1 | Out-String -Width 300
1105+ }} else {{ "== no cdb at $cdb" }}
1106+ }}
1107+ "== end of dump $(Get-Date -f o)"
1108+ "#
1109+ ) ;
1110+ let o = run ( & powershell ( ) , & [ "-NoProfile" , "-NonInteractive" , "-Command" , & script] , Duration :: from_secs ( 240 ) ) ;
1111+ format ! ( "{}\n {}\n (dump exit {})" , o. stdout, o. stderr, o. code)
10781112 }
10791113
10801114 #[ cfg( windows) ]
@@ -1109,41 +1143,156 @@ mod tests {
11091143 let pidfile_s = pidfile. to_string_lossy ( ) . into_owned ( ) ;
11101144 let watcher = thread:: spawn ( {
11111145 let pidfile = pidfile. clone ( ) ;
1112- move || wait_for_pid ( & pidfile)
1146+ move || wait_for_pid ( & pidfile, Duration :: from_secs ( 60 ) )
11131147 } ) ;
11141148 let t = Instant :: now ( ) ;
11151149 let o = run_with ( & powershell ( ) , & [ "-NoProfile" , "-Command" , TREE ] , & [ ( "BIAB_PIDFILE" , & pidfile_s) ] , None , Duration :: from_secs ( 20 ) ) ;
1116- assert_eq ! ( o. code, -1 , "{}" , o. stderr) ;
1117- assert ! ( t. elapsed( ) < Duration :: from_secs( 30 ) ) ;
1150+ let took = t. elapsed ( ) ;
11181151 let pid = watcher. join ( ) . unwrap ( ) ;
1152+ let said = format ! ( "PowerShell said {:?} {:?}, code {} after {took:?}" , o. stdout, o. stderr, o. code) ;
1153+ let pid = pid. unwrap_or_else ( || panic ! ( "no ping pid: {said}\n {}" , diag_dump( "timeout test" ) ) ) ;
1154+ assert_eq ! ( o. code, -1 , "{said}" ) ;
1155+ assert ! ( took < Duration :: from_secs( 30 ) , "{said}" ) ;
11191156 assert ! ( gone( & pid) , "ping {pid} survived" ) ;
11201157 }
11211158
1159+ /// The environment of the cancel test: what PowerShell itself needs to start, and the pid file.
1160+ #[ cfg( windows) ]
1161+ fn diag_small_env ( pidfile : & str ) -> Vec < ( String , String ) > {
1162+ [ "SystemRoot" , "windir" , "USERPROFILE" , "TEMP" , "TMP" , "LOCALAPPDATA" , "APPDATA" ]
1163+ . iter ( )
1164+ . filter_map ( |k| std:: env:: var ( k) . ok ( ) . map ( |v| ( k. to_string ( ) , v) ) )
1165+ . chain ( [ ( "PATH" . to_string ( ) , system32 ( "" ) . to_string_lossy ( ) . into_owned ( ) ) , ( "BIAB_PIDFILE" . to_string ( ) , pidfile. to_string ( ) ) ] )
1166+ . collect ( )
1167+ }
1168+
11221169 #[ cfg( windows) ]
11231170 #[ test]
11241171 fn windows_cancel_kills_the_whole_tree ( ) {
11251172 let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
11261173 let pidfile = dir. path ( ) . join ( "pid" ) ;
1127- let pidfile_s = pidfile. to_string_lossy ( ) . into_owned ( ) ;
1128- // What PowerShell itself needs to start, and the pid file.
1129- let kept: Vec < ( String , String ) > = [ "SystemRoot" , "windir" , "USERPROFILE" , "TEMP" , "TMP" , "LOCALAPPDATA" , "APPDATA" ]
1130- . iter ( )
1131- . filter_map ( |k| std:: env:: var ( k) . ok ( ) . map ( |v| ( k. to_string ( ) , v) ) )
1132- . chain ( [ ( "PATH" . to_string ( ) , system32 ( "" ) . to_string_lossy ( ) . into_owned ( ) ) , ( "BIAB_PIDFILE" . to_string ( ) , pidfile_s) ] )
1133- . collect ( ) ;
1174+ let kept = diag_small_env ( & pidfile. to_string_lossy ( ) ) ;
11341175 let env: Vec < ( & str , & str ) > = kept. iter ( ) . map ( |( k, v) | ( k. as_str ( ) , v. as_str ( ) ) ) . collect ( ) ;
11351176 let cancel = AtomicBool :: new ( false ) ;
11361177 let iso = Isolated { env : & env, cwd : dir. path ( ) , stdin : "" , limit : Duration :: from_secs ( 120 ) , cancel : & cancel } ;
1137- let code = thread:: scope ( |s| {
1138- s. spawn ( || {
1139- wait_for_pid ( & pidfile) ;
1178+ let t = Instant :: now ( ) ;
1179+ let mut said = Vec :: new ( ) ;
1180+ let ( code, pid) = thread:: scope ( |s| {
1181+ let watcher = s. spawn ( || {
1182+ let pid = wait_for_pid ( & pidfile, Duration :: from_secs ( 60 ) ) ;
1183+ let dump = if pid. is_none ( ) { diag_dump ( "cancel test" ) } else { String :: new ( ) } ;
11401184 cancel. store ( true , Ordering :: SeqCst ) ;
1185+ ( pid, dump)
11411186 } ) ;
1142- run_isolated ( & powershell ( ) , & [ "-NoProfile" , "-Command" , TREE ] , & iso, & mut |_| { } )
1187+ let code = run_isolated ( & powershell ( ) , & [ "-NoProfile" , "-Command" , TREE ] , & iso, & mut |l| said. push ( format ! ( "{:?} {l}" , t. elapsed( ) ) ) ) ;
1188+ ( code, watcher. join ( ) . unwrap ( ) )
11431189 } ) ;
1144- assert_eq ! ( code, -1 ) ;
1145- let pid = std:: fs:: read_to_string ( & pidfile) . unwrap ( ) ;
1146- assert ! ( gone( pid. trim( ) ) , "ping {} survived" , pid. trim( ) ) ;
1190+ let ( pid, dump) = pid;
1191+ let pid = pid. unwrap_or_else ( || panic ! ( "no ping pid after {:?}, code {code}, PowerShell said {said:?}\n {dump}" , t. elapsed( ) ) ) ;
1192+ eprintln ! ( "DIAG cancel ok: pid after {:?}, said {said:?}" , t. elapsed( ) ) ;
1193+ assert_eq ! ( code, -1 , "{said:?}" ) ;
1194+ assert ! ( gone( & pid) , "ping {pid} survived" ) ;
1195+ }
1196+
1197+ /// DIAG: the same PowerShell, launched in several ways at once, round after round.
1198+ #[ cfg( windows) ]
1199+ fn diag_launch ( env : Option < & [ ( String , String ) ] > , piped : bool , script : & str , pidfile : & Path , cwd : & Path ) -> String {
1200+ let mut c = Command :: new ( powershell ( ) ) ;
1201+ c. args ( [ "-NoProfile" , "-Command" , script] ) . current_dir ( cwd) ;
1202+ match env {
1203+ Some ( e) => {
1204+ c. env_clear ( ) . envs ( e. iter ( ) . map ( |( k, v) | ( k. as_str ( ) , v. as_str ( ) ) ) ) ;
1205+ }
1206+ None => {
1207+ c. env_remove ( "PSModulePath" ) . env ( "BIAB_PIDFILE" , pidfile) ;
1208+ }
1209+ }
1210+ c. stdin ( if piped { Stdio :: piped ( ) } else { Stdio :: null ( ) } ) ;
1211+ let t = Instant :: now ( ) ;
1212+ let ( mut child, rx) = start ( & mut c) . unwrap ( ) ;
1213+ if let Some ( mut sin) = child. stdin . take ( ) {
1214+ thread:: spawn ( move || {
1215+ let _ = sin. write_all ( b"" ) ;
1216+ } ) ;
1217+ }
1218+ let cancel = AtomicBool :: new ( false ) ;
1219+ let mut said = Vec :: new ( ) ;
1220+ let ( code, ( pid, dump) ) = thread:: scope ( |s| {
1221+ let watcher = s. spawn ( || {
1222+ let pid = wait_for_pid ( pidfile, Duration :: from_secs ( 60 ) ) ;
1223+ let at = t. elapsed ( ) ;
1224+ let dump = if pid. is_none ( ) { diag_dump ( "variant" ) } else { String :: new ( ) } ;
1225+ cancel. store ( true , Ordering :: SeqCst ) ;
1226+ ( pid. map ( |_| at) , dump)
1227+ } ) ;
1228+ let code = supervise ( & mut child, & rx, Duration :: from_secs ( 150 ) , Some ( & cancel) , & mut |_, b| {
1229+ said. push ( format ! ( "{:?} {}" , t. elapsed( ) , String :: from_utf8_lossy( & b) . trim_end( ) ) )
1230+ } ) ;
1231+ ( code, watcher. join ( ) . unwrap ( ) )
1232+ } ) ;
1233+ match pid {
1234+ Some ( at) => format ! ( "ok {:.1}s" , at. as_secs_f64( ) ) ,
1235+ None => format ! ( "HANG code {code:?} said {said:?}\n {dump}" ) ,
1236+ }
1237+ }
1238+
1239+ #[ cfg( windows) ]
1240+ #[ test]
1241+ #[ ignore]
1242+ fn diag_variants ( ) {
1243+ let rounds: usize = std:: env:: var ( "DIAG_ROUNDS" ) . ok ( ) . and_then ( |v| v. parse ( ) . ok ( ) ) . unwrap_or ( 10 ) ;
1244+ let nonew = TREE . replace ( "-WindowStyle Hidden" , "-NoNewWindow" ) ;
1245+ let variants: Vec < ( & str , bool , bool , bool , String ) > = vec ! [
1246+ // name, small env?, app env?, stdin piped?, script
1247+ ( "small-piped" , true , false , true , TREE . to_string( ) ) ,
1248+ ( "small-null" , true , false , false , TREE . to_string( ) ) ,
1249+ ( "appenv-piped" , true , true , true , TREE . to_string( ) ) ,
1250+ ( "full-piped" , false , false , true , TREE . to_string( ) ) ,
1251+ ( "full-null" , false , false , false , TREE . to_string( ) ) ,
1252+ ( "small-piped-nonew" , true , false , true , nonew) ,
1253+ ] ;
1254+ let mut tally: Vec < ( String , usize , usize , Vec < String > ) > = variants. iter ( ) . map ( |v| ( v. 0 . to_string ( ) , 0 , 0 , vec ! [ ] ) ) . collect ( ) ;
1255+ for round in 0 ..rounds {
1256+ let results: Vec < String > = thread:: scope ( |s| {
1257+ let hs: Vec < _ > = variants
1258+ . iter ( )
1259+ . map ( |( _, small, app, piped, script) | {
1260+ s. spawn ( move || {
1261+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
1262+ let pidfile = dir. path ( ) . join ( "pid" ) ;
1263+ let p = pidfile. to_string_lossy ( ) . into_owned ( ) ;
1264+ let mut env = diag_small_env ( & p) ;
1265+ if * app {
1266+ for k in [
1267+ "USERNAME" , "windir" , "SystemDrive" , "ComSpec" , "PATHEXT" , "ProgramFiles" , "ProgramFiles(x86)" , "ProgramW6432" ,
1268+ "ProgramData" , "CommonProgramFiles" , "NUMBER_OF_PROCESSORS" , "PROCESSOR_ARCHITECTURE" , "OS" ,
1269+ ] {
1270+ if let Ok ( v) = std:: env:: var ( k) {
1271+ env. push ( ( k. to_string ( ) , v) ) ;
1272+ }
1273+ }
1274+ }
1275+ diag_launch ( small. then_some ( env. as_slice ( ) ) , * piped, script, & pidfile, dir. path ( ) )
1276+ } )
1277+ } )
1278+ . collect ( ) ;
1279+ hs. into_iter ( ) . map ( |h| h. join ( ) . unwrap ( ) ) . collect ( )
1280+ } ) ;
1281+ for ( i, r) in results. into_iter ( ) . enumerate ( ) {
1282+ eprintln ! ( "round {round} {} {}" , variants[ i] . 0 , r. lines( ) . next( ) . unwrap_or( "" ) ) ;
1283+ if r. starts_with ( "ok" ) {
1284+ tally[ i] . 1 += 1 ;
1285+ } else {
1286+ tally[ i] . 2 += 1 ;
1287+ eprintln ! ( "{r}" ) ;
1288+ tally[ i] . 3 . push ( r) ;
1289+ }
1290+ }
1291+ }
1292+ for ( name, ok, hang, _) in & tally {
1293+ eprintln ! ( "DIAG TALLY {name}: ok {ok}, hang {hang}" ) ;
1294+ }
1295+ assert ! ( tally. iter( ) . all( |t| t. 2 == 0 ) , "some variant hung" ) ;
11471296 }
11481297
11491298 #[ cfg( windows) ]
0 commit comments