Skip to content

Commit 9346519

Browse files
psmuxcatoxliu
andcommitted
fix(platform): sleep before FreeConsole in send_ctrl_c_event (#307)
Move the 5ms sleep from after FreeConsole() to before it, so psmux remains attached to the child's console while the async Ctrl+C dispatch completes. GenerateConsoleCtrlEvent returns as soon as the signal is queued, not when it is handled. Detaching too early could cause the console subsystem to lose the signal context. psmux is protected during the sleep by the preceding SetConsoleCtrlHandler(None, 1) which ignores Ctrl+C. Adds E2E test with 50 iterations of Ctrl+C signal delivery verification. Co-authored-by: Shijian Liu <catoxliu@gmail.com>
1 parent 5a10bac commit 9346519

2 files changed

Lines changed: 255 additions & 6 deletions

File tree

src/platform.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1118,17 +1118,18 @@ pub mod mouse_inject {
11181118

11191119
log(&format!("GenerateConsoleCtrlEvent => ok={} err={}", ok, err));
11201120

1121+
// GenerateConsoleCtrlEvent dispatches asynchronously via a system
1122+
// thread pool. Sleep while still attached so the signal has time
1123+
// to propagate through the console subsystem before we detach.
1124+
// psmux is protected by the preceding SetConsoleCtrlHandler(None, 1).
1125+
std::thread::sleep(std::time::Duration::from_millis(5));
1126+
11211127
// Detach from the child's console BEFORE restoring Ctrl+C handling.
1122-
// GenerateConsoleCtrlEvent dispatches asynchronously via a new thread;
1123-
// if we restore the default handler while still attached, the async
1128+
// If we restore the default handler while still attached, the async
11241129
// handler thread might terminate psmux. Detaching first ensures the
11251130
// event only targets processes that remain on the console.
11261131
FreeConsole();
11271132

1128-
// Brief sleep to let the async CTRL_C_EVENT handler thread finish
1129-
// before we re-enable default handling.
1130-
std::thread::sleep(std::time::Duration::from_millis(5));
1131-
11321133
// Restore default Ctrl+C handling now that we're detached
11331134
SetConsoleCtrlHandler(None, 0);
11341135

tests/test_pr307_ctrlc_signal.ps1

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
# PR #307: Ctrl+C signal delivery reliability test
2+
# Tests whether send-keys C-c reliably interrupts a running process
3+
# The PR claims GenerateConsoleCtrlEvent can lose signals because FreeConsole
4+
# is called before the async dispatch completes.
5+
6+
$ErrorActionPreference = "Continue"
7+
$PSMUX = (Get-Command psmux -EA Stop).Source
8+
$SESSION = "pr307_ctrlc"
9+
$psmuxDir = "$env:USERPROFILE\.psmux"
10+
$script:TestsPassed = 0
11+
$script:TestsFailed = 0
12+
13+
function Write-Pass($msg) { Write-Host " [PASS] $msg" -ForegroundColor Green; $script:TestsPassed++ }
14+
function Write-Fail($msg) { Write-Host " [FAIL] $msg" -ForegroundColor Red; $script:TestsFailed++ }
15+
16+
function Cleanup {
17+
& $PSMUX kill-session -t $SESSION 2>&1 | Out-Null
18+
Start-Sleep -Milliseconds 500
19+
Remove-Item "$psmuxDir\$SESSION.*" -Force -EA SilentlyContinue
20+
}
21+
22+
function Wait-Session {
23+
param([string]$Name, [int]$TimeoutMs = 15000)
24+
$pf = "$psmuxDir\$Name.port"
25+
$sw = [System.Diagnostics.Stopwatch]::StartNew()
26+
while ($sw.ElapsedMilliseconds -lt $TimeoutMs) {
27+
if (Test-Path $pf) {
28+
$port = (Get-Content $pf -Raw -EA SilentlyContinue)
29+
if ($port -and $port.Trim() -match '^\d+$') {
30+
try {
31+
$tcp = [System.Net.Sockets.TcpClient]::new("127.0.0.1", [int]$port.Trim())
32+
$tcp.Close()
33+
return $true
34+
} catch {}
35+
}
36+
}
37+
Start-Sleep -Milliseconds 100
38+
}
39+
return $false
40+
}
41+
42+
# === SETUP ===
43+
Cleanup
44+
Start-Sleep -Seconds 1
45+
& $PSMUX new-session -d -s $SESSION
46+
if (-not (Wait-Session $SESSION)) {
47+
Write-Host "FATAL: Session creation failed" -ForegroundColor Red
48+
exit 1
49+
}
50+
Write-Host "Session $SESSION created" -ForegroundColor Green
51+
52+
# Wait for shell prompt
53+
Start-Sleep -Seconds 3
54+
55+
Write-Host "`n=== PR #307: Ctrl+C Signal Delivery Test ===" -ForegroundColor Cyan
56+
57+
# ================================================================
58+
# TEST 1: Basic Ctrl+C delivery - single attempt
59+
# ================================================================
60+
Write-Host "`n[Test 1] Basic Ctrl+C delivery" -ForegroundColor Yellow
61+
62+
& $PSMUX send-keys -t $SESSION "ping -n 100 127.0.0.1" Enter 2>&1 | Out-Null
63+
Start-Sleep -Seconds 2
64+
65+
$capBefore = & $PSMUX capture-pane -t $SESSION -p 2>&1 | Out-String
66+
$pingStarted = $capBefore -match "Reply from"
67+
if ($pingStarted) { Write-Host " ping is running..." }
68+
else { Write-Host " WARNING: ping may not have started yet" -ForegroundColor DarkYellow }
69+
70+
& $PSMUX send-keys -t $SESSION C-c 2>&1 | Out-Null
71+
Start-Sleep -Seconds 1
72+
73+
$capAfter = & $PSMUX capture-pane -t $SESSION -p 2>&1 | Out-String
74+
$interrupted = ($capAfter -match "Control-C") -or ($capAfter -match "Ping statistics") -or ($capAfter -match "PS [A-Z]:\\")
75+
76+
if ($interrupted) { Write-Pass "Ctrl+C interrupted ping" }
77+
else { Write-Fail "Ctrl+C may not have interrupted ping" }
78+
79+
# Clear
80+
& $PSMUX send-keys -t $SESSION "" Enter 2>&1 | Out-Null
81+
Start-Sleep -Milliseconds 300
82+
& $PSMUX send-keys -t $SESSION "cls" Enter 2>&1 | Out-Null
83+
Start-Sleep -Milliseconds 500
84+
85+
# ================================================================
86+
# TEST 2: Rapid-fire Ctrl+C reliability (stress test)
87+
# N iterations of: start process -> send C-c -> check interrupted
88+
# ================================================================
89+
Write-Host "`n[Test 2] Rapid Ctrl+C reliability ($iterations iterations)" -ForegroundColor Yellow
90+
$iterations = 30
91+
$delivered = 0
92+
$dropped = 0
93+
94+
for ($i = 1; $i -le $iterations; $i++) {
95+
# Start a blocking command
96+
& $PSMUX send-keys -t $SESSION "ping -n 50 127.0.0.1" Enter 2>&1 | Out-Null
97+
Start-Sleep -Milliseconds 1200
98+
99+
# Send Ctrl+C
100+
& $PSMUX send-keys -t $SESSION C-c 2>&1 | Out-Null
101+
Start-Sleep -Milliseconds 800
102+
103+
# Check if interrupted
104+
$cap = & $PSMUX capture-pane -t $SESSION -p 2>&1 | Out-String
105+
$ok = ($cap -match "Control-C") -or ($cap -match "Ping statistics") -or ($cap -match "PS [A-Z]:\\.*>\s*$")
106+
107+
if ($ok) {
108+
$delivered++
109+
} else {
110+
$dropped++
111+
Write-Host " Iteration ${i}: SIGNAL MAY HAVE BEEN DROPPED" -ForegroundColor Red
112+
# Recovery: send another C-c
113+
& $PSMUX send-keys -t $SESSION C-c 2>&1 | Out-Null
114+
Start-Sleep -Milliseconds 500
115+
}
116+
117+
# Clear for next iteration
118+
& $PSMUX send-keys -t $SESSION "" Enter 2>&1 | Out-Null
119+
Start-Sleep -Milliseconds 200
120+
& $PSMUX send-keys -t $SESSION "cls" Enter 2>&1 | Out-Null
121+
Start-Sleep -Milliseconds 300
122+
}
123+
124+
$rate = [math]::Round(($delivered / $iterations) * 100, 1)
125+
Write-Host " Delivered: $delivered/$iterations ($rate%)"
126+
127+
if ($dropped -eq 0) { Write-Pass "All $iterations Ctrl+C signals delivered (100%)" }
128+
elseif ($dropped -le 2) { Write-Fail "Some signals dropped: $dropped/$iterations ($rate% delivery). PR #307 fix may help." }
129+
else { Write-Fail "Significant signal loss: $dropped/$iterations ($rate% delivery). PR #307 fix NEEDED." }
130+
131+
# ================================================================
132+
# TEST 3: Ctrl+C with very fast timing (minimal delay)
133+
# This is the scenario most likely to trigger the race condition
134+
# ================================================================
135+
Write-Host "`n[Test 3] Fast Ctrl+C (minimal delay, 20 iterations)" -ForegroundColor Yellow
136+
$fastDelivered = 0
137+
$fastDropped = 0
138+
139+
for ($i = 1; $i -le 20; $i++) {
140+
& $PSMUX send-keys -t $SESSION "ping -n 50 127.0.0.1" Enter 2>&1 | Out-Null
141+
Start-Sleep -Milliseconds 600 # Less wait time = more pressure on async dispatch
142+
143+
& $PSMUX send-keys -t $SESSION C-c 2>&1 | Out-Null
144+
Start-Sleep -Milliseconds 500
145+
146+
$cap = & $PSMUX capture-pane -t $SESSION -p 2>&1 | Out-String
147+
$ok = ($cap -match "Control-C") -or ($cap -match "Ping statistics") -or ($cap -match "PS [A-Z]:\\.*>\s*$")
148+
149+
if ($ok) { $fastDelivered++ }
150+
else {
151+
$fastDropped++
152+
Write-Host " Fast iteration ${i}: DROPPED" -ForegroundColor Red
153+
& $PSMUX send-keys -t $SESSION C-c 2>&1 | Out-Null
154+
Start-Sleep -Milliseconds 500
155+
}
156+
157+
& $PSMUX send-keys -t $SESSION "" Enter 2>&1 | Out-Null
158+
Start-Sleep -Milliseconds 100
159+
& $PSMUX send-keys -t $SESSION "cls" Enter 2>&1 | Out-Null
160+
Start-Sleep -Milliseconds 300
161+
}
162+
163+
$fastRate = [math]::Round(($fastDelivered / 20) * 100, 1)
164+
Write-Host " Fast delivery: $fastDelivered/20 ($fastRate%)"
165+
166+
if ($fastDropped -eq 0) { Write-Pass "All fast Ctrl+C signals delivered" }
167+
else { Write-Fail "Fast signal loss: $fastDropped/20. Race condition confirmed." }
168+
169+
# ================================================================
170+
# TEST 4: Ctrl+C with Python script (different process type)
171+
# ================================================================
172+
Write-Host "`n[Test 4] Ctrl+C with Python process" -ForegroundColor Yellow
173+
174+
# Check if python is available
175+
$pythonCmd = if (Get-Command python -EA SilentlyContinue) { "python" }
176+
elseif (Get-Command python3 -EA SilentlyContinue) { "python3" }
177+
else { $null }
178+
179+
if ($pythonCmd) {
180+
& $PSMUX send-keys -t $SESSION "$pythonCmd -c ""import time; [print(f'tick {i}') or time.sleep(0.5) for i in range(100)]""" Enter 2>&1 | Out-Null
181+
Start-Sleep -Seconds 2
182+
183+
& $PSMUX send-keys -t $SESSION C-c 2>&1 | Out-Null
184+
Start-Sleep -Seconds 1
185+
186+
$cap = & $PSMUX capture-pane -t $SESSION -p 2>&1 | Out-String
187+
$pyInterrupted = ($cap -match "KeyboardInterrupt") -or ($cap -match "PS [A-Z]:\\")
188+
189+
if ($pyInterrupted) { Write-Pass "Ctrl+C interrupted Python script" }
190+
else { Write-Fail "Ctrl+C may not have interrupted Python" }
191+
192+
& $PSMUX send-keys -t $SESSION "" Enter 2>&1 | Out-Null
193+
Start-Sleep -Milliseconds 200
194+
& $PSMUX send-keys -t $SESSION "cls" Enter 2>&1 | Out-Null
195+
Start-Sleep -Milliseconds 500
196+
} else {
197+
Write-Host " [SKIP] Python not available" -ForegroundColor DarkGray
198+
}
199+
200+
# ================================================================
201+
# TEST 5: Win32 TUI Visual Verification
202+
# ================================================================
203+
Write-Host "`n[Test 5] TUI Visual Verification" -ForegroundColor Yellow
204+
205+
$TUI_SESSION = "pr307_tui"
206+
& $PSMUX kill-session -t $TUI_SESSION 2>&1 | Out-Null
207+
Start-Sleep -Milliseconds 500
208+
209+
$proc = Start-Process -FilePath $PSMUX -ArgumentList "new-session","-s",$TUI_SESSION -PassThru
210+
Start-Sleep -Seconds 4
211+
212+
& $PSMUX has-session -t $TUI_SESSION 2>$null
213+
if ($LASTEXITCODE -eq 0) { Write-Pass "TUI session alive" }
214+
else { Write-Fail "TUI session failed to start" }
215+
216+
# Send a ping in the TUI session and Ctrl+C it
217+
& $PSMUX send-keys -t $TUI_SESSION "ping -n 20 127.0.0.1" Enter 2>&1 | Out-Null
218+
Start-Sleep -Seconds 2
219+
& $PSMUX send-keys -t $TUI_SESSION C-c 2>&1 | Out-Null
220+
Start-Sleep -Seconds 1
221+
222+
$tuiCap = & $PSMUX capture-pane -t $TUI_SESSION -p 2>&1 | Out-String
223+
$tuiOk = ($tuiCap -match "Control-C") -or ($tuiCap -match "Ping statistics") -or ($tuiCap -match "PS [A-Z]:\\")
224+
225+
if ($tuiOk) { Write-Pass "TUI: Ctrl+C interrupted process in attached session" }
226+
else { Write-Fail "TUI: Ctrl+C may not have worked in attached session" }
227+
228+
# Cleanup TUI
229+
& $PSMUX kill-session -t $TUI_SESSION 2>&1 | Out-Null
230+
try { Stop-Process -Id $proc.Id -Force -EA SilentlyContinue } catch {}
231+
232+
# === TEARDOWN ===
233+
Cleanup
234+
235+
Write-Host "`n=== Summary ===" -ForegroundColor Cyan
236+
Write-Host " Total Ctrl+C signals tested: $($iterations + 20 + 2) (approx)"
237+
Write-Host " Passed: $($script:TestsPassed)" -ForegroundColor Green
238+
Write-Host " Failed: $($script:TestsFailed)" -ForegroundColor $(if ($script:TestsFailed -gt 0) { "Red" } else { "Green" })
239+
Write-Host ""
240+
if ($dropped -gt 0 -or $fastDropped -gt 0) {
241+
Write-Host " VERDICT: Signal loss detected. PR #307 fix IS NEEDED." -ForegroundColor Red
242+
} else {
243+
Write-Host " VERDICT: No signal loss detected in $($iterations + 20) iterations." -ForegroundColor Green
244+
Write-Host " However, PR #307 is a correctness fix for a RACE CONDITION." -ForegroundColor Yellow
245+
Write-Host " The current code sleeps AFTER FreeConsole which is logically wrong." -ForegroundColor Yellow
246+
Write-Host " Moving sleep BEFORE FreeConsole is the correct sequence." -ForegroundColor Yellow
247+
}
248+
exit $script:TestsFailed

0 commit comments

Comments
 (0)