the terminal that never closes: every launcher waited for ANY btl4.exe on the machine
Operator report: "X-closing the game leaves the terminal open and leaves orphaned processes." Investigated on the rig against 4.11.600. The terminal half is real and is THIS: :btwait polled `tasklist /FI "IMAGENAME eq btl4.exe"`, which is machine-wide, so a bat that launched nothing at all keeps spinning while an unrelated instance lives -- proved with btwait_probe.ps1. A second client, the operator's own pod, or an orphan from a crash therefore hangs every join window, which reads as "the game never exited" and invites people to start killing processes. All four launchers carried the identical block. Fix: snapshot the btl4 PIDs alive BEFORE the launch; wait only on PIDs absent from that snapshot. The `if /I "%%P"=="btl4.exe"` guard is deliberately kept -- tokens=2 alone parses tasklist's "INFO: No tasks are running" line as a PID and spins forever with nothing running, which would be worse than the bug. Verified with the text lifted verbatim from the shipped play_solo.bat: nothing running -> signs off (the regression guard); someone else's instance -> signs off; our own generation -> keeps waiting; decoy gone -> signs off. The patched bat still launches (pid + launch_report.txt). NOT verified: the full handoff E2E, because the bat blocks on the FE menu waiting for a human. The orphan half did NOT reproduce on 600: closing the MAIN window exits cleanly in ~1s during solo model-load, in the relay join wait, and after a real console launch, with the relay logging the seat freed. The orphans the playtesters saw match 584 and earlier, where every close relaunched. Full write-up, including the aux windows that hide instead of closing and the WM_QUIT that BTLoadPump swallows, in phases/phase-12-orphan-processes.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
afbbb7c59c
commit
7afbda900e
@@ -0,0 +1,33 @@
|
||||
# Does join.bat's :btwait loop wait for ANY btl4.exe, or only its own?
|
||||
# Run the EXACT fragment from join.bat while an unrelated btl4 is alive.
|
||||
$content = 'C:\git\bt411\content'
|
||||
$scratch = 'C:\Users\epilectrik\AppData\Local\Temp\claude\C--git-bt411\89ab96e9-6cf0-4555-939d-05bf3708745a\scratchpad'
|
||||
|
||||
# an UNRELATED instance -- stands in for the operator's own pod, a second
|
||||
# client, or a genuine orphan. Nothing to do with the bat we are testing.
|
||||
$env:BT_LOG='btwait_other.log'; $env:BT_START_INSIDE='1'
|
||||
foreach ($v in 'BT_RELAY','BT_FE_LOOP','BT_SELF') { Remove-Item -LiteralPath "Env:\$v" -ErrorAction SilentlyContinue }
|
||||
$other = Start-Process 'C:\git\bt411\build\Release\btl4.exe' -ArgumentList '-egg','ARENA1.EGG' `
|
||||
-WorkingDirectory $content -PassThru
|
||||
"unrelated btl4 running as pid $($other.Id)"
|
||||
Start-Sleep -Seconds 6
|
||||
|
||||
# the exact loop body from join.bat lines 55-63, minus the goto
|
||||
$frag = @'
|
||||
@echo off
|
||||
set "BT_STILL_RUNNING="
|
||||
for /f "tokens=1" %%P in ('%SystemRoot%\System32\tasklist.exe /FI "IMAGENAME eq btl4.exe" /NH 2^>nul') do (
|
||||
if /I "%%P"=="btl4.exe" set "BT_STILL_RUNNING=1"
|
||||
)
|
||||
if defined BT_STILL_RUNNING (echo BTWAIT: would KEEP SPINNING -- terminal stays open) else (echo BTWAIT: would exit and print "The game has exited")
|
||||
'@
|
||||
$fragPath = Join-Path $scratch 'btwait_frag.bat'
|
||||
Set-Content $fragPath $frag -Encoding Ascii
|
||||
|
||||
'--- the bat that launched NOTHING now runs the wait loop ---'
|
||||
& cmd.exe /c $fragPath
|
||||
|
||||
Stop-Process -Id $other.Id -Force -ErrorAction SilentlyContinue
|
||||
Start-Sleep -Seconds 2
|
||||
'--- same loop with no btl4 anywhere ---'
|
||||
& cmd.exe /c $fragPath
|
||||
@@ -0,0 +1,106 @@
|
||||
# Identify every top-level window the pod owns (real titles + class names),
|
||||
# then X-close ONLY the main game window -- the actual user action.
|
||||
param([int]$CloseAfterLaunch = 5, [string]$Tag = 'mainwin', [switch]$SkipLaunch)
|
||||
|
||||
Add-Type @'
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Runtime.InteropServices;
|
||||
public class W4 {
|
||||
[DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern bool EnumWindows(EnumProc cb, IntPtr p);
|
||||
[DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
|
||||
[DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern int GetWindowTextW(IntPtr h, StringBuilder s, int n);
|
||||
[DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern int GetClassNameW(IntPtr h, StringBuilder s, int n);
|
||||
[DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr h);
|
||||
[DllImport("user32.dll")] public static extern bool IsWindow(IntPtr h);
|
||||
[DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern bool PostMessageW(IntPtr h, uint m, IntPtr w, IntPtr l);
|
||||
[DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out RECT r);
|
||||
[StructLayout(LayoutKind.Sequential)] public struct RECT { public int L, T, R, B; }
|
||||
public delegate bool EnumProc(IntPtr h, IntPtr p);
|
||||
public class Win { public IntPtr H; public string Title; public string Cls; public bool Vis; public int W, Ht; }
|
||||
public static System.Collections.Generic.List<Win> List = new System.Collections.Generic.List<Win>();
|
||||
public static uint Target;
|
||||
public static bool Cb(IntPtr h, IntPtr p) {
|
||||
uint pid; GetWindowThreadProcessId(h, out pid);
|
||||
if (pid == Target) {
|
||||
StringBuilder t = new StringBuilder(256); GetWindowTextW(h, t, 256);
|
||||
StringBuilder c = new StringBuilder(256); GetClassNameW(h, c, 256);
|
||||
RECT r; GetWindowRect(h, out r);
|
||||
List.Add(new Win { H=h, Title=t.ToString(), Cls=c.ToString(), Vis=IsWindowVisible(h),
|
||||
W=r.R-r.L, Ht=r.B-r.T });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
public static void Scan(uint pid) { List.Clear(); Target = pid; EnumWindows(Cb, IntPtr.Zero); }
|
||||
}
|
||||
'@
|
||||
|
||||
$content = 'C:\git\bt411\content'
|
||||
$mine = @()
|
||||
function Pods { @(Get-CimInstance Win32_Process -Filter "Name='btl4.exe'" | Select-Object -ExpandProperty ProcessId) }
|
||||
function ShowWins($pid_, $label) {
|
||||
[W4]::Scan([uint32]$pid_)
|
||||
" $label"
|
||||
foreach ($w in [W4]::List) {
|
||||
" hwnd={0,-10} vis={1,-5} {2,4}x{3,-4} class='{4}' title='{5}'" -f $w.H.ToInt64(), $w.Vis, $w.W, $w.Ht, $w.Cls, $w.Title
|
||||
}
|
||||
}
|
||||
|
||||
$relayLog = Join-Path $content "orphan_relay_$Tag.log"
|
||||
$relay = Start-Process 'python' -ArgumentList 'C:\git\bt411\tools\btconsole.py','--relay','1600','OPERATOR.EGG' `
|
||||
-WorkingDirectory $content -PassThru -RedirectStandardOutput $relayLog `
|
||||
-RedirectStandardError (Join-Path $content "orphan_relay_$Tag.err") -WindowStyle Hidden
|
||||
$mine += $relay.Id
|
||||
Start-Sleep -Seconds 4
|
||||
|
||||
$log = "orphan_$Tag.log"
|
||||
Remove-Item (Join-Path $content $log) -Force -ErrorAction SilentlyContinue
|
||||
$env:BT_LOG=$log; $env:BT_LOG_APPEND='1'; $env:BT_START_INSIDE='1'; $env:BT_DEV_GAUGES='1'
|
||||
$env:BT_PLATFORM='glass'; $env:BT_RELAY='127.0.0.1:1600'; $env:BT_FE_LOOP='1'; $env:BT_CALLSIGN='ORPHANTEST'
|
||||
$pod = Start-Process 'C:\git\bt411\build\Release\btl4.exe' `
|
||||
-ArgumentList '-egg','OPERATOR.EGG','-net','1601','-platform','glass' `
|
||||
-WorkingDirectory $content -PassThru
|
||||
$mine += $pod.Id
|
||||
"pod pid $($pod.Id)"
|
||||
Start-Sleep -Seconds 20
|
||||
ShowWins $pod.Id "WINDOWS IN THE JOIN WAIT:"
|
||||
|
||||
if (-not $SkipLaunch) {
|
||||
$secret = (Get-Content (Join-Path $content 'operator_secret.txt') -Raw).Trim()
|
||||
$cli = New-Object System.Net.Sockets.TcpClient('127.0.0.1', 1607)
|
||||
$wr = New-Object System.IO.StreamWriter($cli.GetStream()); $wr.AutoFlush = $true
|
||||
$wr.WriteLine("AUTH $secret"); Start-Sleep -Milliseconds 700; $wr.WriteLine("launch")
|
||||
"LAUNCH sent; closing in ${CloseAfterLaunch}s"
|
||||
Start-Sleep -Seconds $CloseAfterLaunch
|
||||
ShowWins $pod.Id "WINDOWS MID-LOAD:"
|
||||
}
|
||||
|
||||
# The MAIN game window: the big visible one (the D3D target), not a panel.
|
||||
[W4]::Scan([uint32]$pod.Id)
|
||||
$main = [W4]::List | Where-Object { $_.Vis } | Sort-Object { $_.W * $_.Ht } -Descending | Select-Object -First 1
|
||||
"--- X-closing ONLY the main window: hwnd=$($main.H.ToInt64()) '$($main.Title)' $($main.W)x$($main.Ht) ---"
|
||||
[void][W4]::PostMessageW($main.H, 0x0010, [IntPtr]::Zero, [IntPtr]::Zero)
|
||||
|
||||
$exited = $false; $secs = 0
|
||||
for ($i = 1; $i -le 45; $i++) {
|
||||
Start-Sleep -Milliseconds 1000
|
||||
if (-not $exited -and (Get-Process -Id $pod.Id -ErrorAction SilentlyContinue) -eq $null) { $exited=$true; $secs=$i }
|
||||
}
|
||||
""
|
||||
"RESULT exited : $exited$(if($exited){" after ${secs}s"})"
|
||||
if (-not $exited) {
|
||||
ShowWins $pod.Id "*** ORPHAN SURVIVES -- its windows now: ***"
|
||||
" cpu=$([math]::Round((Get-Process -Id $pod.Id).CPU,1))s"
|
||||
Get-NetTCPConnection -OwningProcess $pod.Id -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.State -eq 'Established' } |
|
||||
ForEach-Object { " STILL CONNECTED: :$($_.LocalPort) -> $($_.RemoteAddress):$($_.RemotePort)" }
|
||||
" --- relay's view (is the ghost still seated?) ---"
|
||||
Get-Content $relayLog -Tail 4 | ForEach-Object { " $_" }
|
||||
}
|
||||
"--- pod log tail ---"
|
||||
Get-Content (Join-Path $content $log) -Tail 5 | ForEach-Object { " $_" }
|
||||
|
||||
"--- cleanup (only my pids: $($mine -join ',')) ---"
|
||||
foreach ($m in $mine) { Stop-Process -Id $m -Force -ErrorAction SilentlyContinue }
|
||||
Start-Sleep -Milliseconds 800
|
||||
"btl4 remaining: $((Pods) -join ',')"
|
||||
@@ -0,0 +1,48 @@
|
||||
# Test the PATCHED wait logic, using the exact snapshot + loop text lifted
|
||||
# out of the shipped play_solo.bat. Three cases, one of them the regression
|
||||
# guard that matters most (nothing running must never spin).
|
||||
$scratch = 'C:\Users\epilectrik\AppData\Local\Temp\claude\C--git-bt411\89ab96e9-6cf0-4555-939d-05bf3708745a\scratchpad'
|
||||
$bat = Get-Content 'C:\git\bt411\players\play_solo.bat' -Raw
|
||||
|
||||
# lift the two patched blocks verbatim
|
||||
$snap = [regex]::Match($bat, '(?ms)^setlocal EnableDelayedExpansion\r?\nset "BT_PRE=,".*?^\)\r?\n').Value
|
||||
$loop = [regex]::Match($bat, '(?ms)^:btwait\r?\nset "BT_STILL_RUNNING=".*?^\)\r?\n').Value
|
||||
if (-not $snap -or -not $loop) { throw "could not lift the blocks from play_solo.bat" }
|
||||
"lifted snapshot block : $($snap.Split("`n").Count) lines"
|
||||
"lifted wait block : $($loop.Split("`n").Count) lines"
|
||||
|
||||
function Probe($label, $snapshotFirst) {
|
||||
# $snapshotFirst = does the decoy exist when we take the snapshot?
|
||||
$body = "@echo off`r`n"
|
||||
if ($snapshotFirst) { $body += $snap } else {
|
||||
# snapshot with nothing running, decoy starts after -> looks like OURS
|
||||
$body += "setlocal EnableDelayedExpansion`r`nset `"BT_PRE=,`"`r`n"
|
||||
}
|
||||
$body += $loop
|
||||
$body += 'if defined BT_STILL_RUNNING (echo VERDICT: SPIN) else (echo VERDICT: SIGN-OFF)' + "`r`n"
|
||||
$p = Join-Path $scratch 'loop_probe.bat'
|
||||
Set-Content $p $body -Encoding Ascii
|
||||
$r = & cmd.exe /c $p
|
||||
" {0,-42} {1}" -f $label, $r
|
||||
}
|
||||
|
||||
'=== C: nothing running (REGRESSION GUARD -- must sign off) ==='
|
||||
Probe 'no btl4 anywhere' $true
|
||||
|
||||
$env:BT_LOG='loopprobe.log'; $env:BT_START_INSIDE='1'
|
||||
foreach ($v in 'BT_RELAY','BT_FE_LOOP') { Remove-Item -LiteralPath "Env:\$v" -ErrorAction SilentlyContinue }
|
||||
$decoy = Start-Process 'C:\git\bt411\build\Release\btl4.exe' -ArgumentList '-egg','ARENA1.EGG' `
|
||||
-WorkingDirectory 'C:\git\bt411\content' -PassThru
|
||||
"decoy btl4 pid $($decoy.Id)"
|
||||
Start-Sleep -Seconds 6
|
||||
|
||||
'=== A: someone ELSE''s btl4 (in the snapshot) -- must sign off ==='
|
||||
Probe 'unrelated instance alive' $true
|
||||
|
||||
'=== B: OUR generation (not in the snapshot) -- must keep waiting ==='
|
||||
Probe 'our own generation alive' $false
|
||||
|
||||
Stop-Process -Id $decoy.Id -Force -ErrorAction SilentlyContinue
|
||||
Start-Sleep -Seconds 2
|
||||
'=== C2: decoy gone, re-check the guard ==='
|
||||
Probe 'nothing running again' $true
|
||||
Reference in New Issue
Block a user