Skip to content

Commit 54a882c

Browse files
DIAG (temporary): loop windows_cancel_kills_the_whole_tree on fresh VMs
1 parent d13f156 commit 54a882c

2 files changed

Lines changed: 258 additions & 24 deletions

File tree

‎.github/workflows/diag-flaky.yml‎

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# Disposable: reproduces the windows_cancel_kills_the_whole_tree flake on fresh VMs.
2+
# Removed before the PR is merged.
3+
name: diag-flaky
4+
5+
on:
6+
push:
7+
branches: [fix/windows-cancel-test]
8+
paths: ['.github/workflows/diag-flaky.yml']
9+
10+
permissions:
11+
contents: read
12+
13+
jobs:
14+
loop:
15+
name: loop (${{ matrix.shard }})
16+
strategy:
17+
fail-fast: false
18+
matrix:
19+
include:
20+
- { shard: full-1, mode: full }
21+
- { shard: full-2, mode: full }
22+
- { shard: full-3, mode: full }
23+
- { shard: full-4, mode: full }
24+
- { shard: alone-1, mode: alone }
25+
- { shard: alone-2, mode: alone }
26+
- { shard: variants-1, mode: variants }
27+
- { shard: variants-2, mode: variants }
28+
- { shard: variants-3, mode: variants }
29+
- { shard: variants-4, mode: variants }
30+
runs-on: windows-latest
31+
timeout-minutes: 75
32+
defaults:
33+
run:
34+
working-directory: app/src-tauri
35+
env:
36+
RUN: 1
37+
steps:
38+
- uses: actions/checkout@v4
39+
with:
40+
persist-credentials: false
41+
- name: Build the test binary
42+
run: cargo test --locked --lib --no-run
43+
- name: Loop
44+
shell: pwsh
45+
env:
46+
MODE: ${{ matrix.mode }}
47+
run: |
48+
$ErrorActionPreference = 'Continue'
49+
$exe = (Get-ChildItem target\debug\deps\biab_lib-*.exe | Sort-Object LastWriteTime | Select-Object -Last 1).FullName
50+
"exe $exe"
51+
$cdb = "${env:ProgramFiles(x86)}\Windows Kits\10\Debuggers\x64\cdb.exe"
52+
"cdb present: $(Test-Path $cdb)"
53+
Get-MpComputerStatus -ErrorAction SilentlyContinue | Select-Object AMServiceEnabled, RealTimeProtectionEnabled, AntivirusEnabled | Format-List | Out-String
54+
"PSModulePath (runner): $env:PSModulePath"
55+
New-Item -ItemType Directory -Force logs | Out-Null
56+
$script:fails = 0
57+
function go($name, [string[]]$a) {
58+
$t = Get-Date
59+
& $exe @a *> "logs\$name.log"
60+
$code = $LASTEXITCODE
61+
"$name exit $code in $([int]((Get-Date) - $t).TotalSeconds)s"
62+
Select-String -Path "logs\$name.log" -Pattern 'DIAG cancel ok' | ForEach-Object { ' ' + $_.Line }
63+
if ($code -ne 0) {
64+
$script:fails++
65+
"::group::$name output"
66+
Get-Content "logs\$name.log"
67+
"::endgroup::"
68+
}
69+
}
70+
# As in CI: the whole suite, once, on a VM that never ran PowerShell for these tests.
71+
go cold @('--nocapture')
72+
switch ($env:MODE) {
73+
'full' { foreach ($i in 1..15) { go "full-$i" @('--nocapture') } }
74+
'alone' { foreach ($i in 1..50) { go "alone-$i" @('sys::tests::windows_cancel_kills_the_whole_tree', '--exact', '--nocapture') } }
75+
'variants' { $env:DIAG_ROUNDS = '25'; go variants @('diag_variants', '--ignored', '--nocapture'); Select-String -Path logs\variants.log -Pattern 'DIAG TALLY' | ForEach-Object Line }
76+
}
77+
"failures: $script:fails"
78+
if ($script:fails) { exit 1 }
79+
- name: Logs
80+
if: always()
81+
uses: actions/upload-artifact@v4
82+
with:
83+
name: logs-${{ matrix.shard }}
84+
path: app/src-tauri/logs
85+
retention-days: 7

‎app/src-tauri/src/sys.rs‎

Lines changed: 173 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)