From 0eae379a25cf2e393845ba0b7181a5a9d9a7731e Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 16 Jul 2026 15:18:45 +0200 Subject: [PATCH 1/7] Cygwin: pty: keep interactive console input for native programs via Cygwin Currently, when a native Windows program starts a Cygwin program while a pseudo console is active, and the Cygwin program then starts another native Windows program, the final program can lose access to console input. It then behaves as though its standard input were redirected instead of remaining interactive. For example, a native `git.exe` may invoke shell aliases (i.e. execute a shell command) that would in turn call interactive Git commands who would no longer work because their standard input appeared to be redirected. This can be demonstrated as follows: git -c 'alias.console-probe=!powershell.exe -NoLogo -NoProfile -Command " Write-Output ([Console]::IsInputRedirected) try { [void][Console]::KeyAvailable exit 0 } catch { exit 1 } "' console-probe Running this command with a Win32 version of `git.exe` currently prints `True` and exits with exit code 1. In the latest official release, where this bug is not present, it prints `False` and results in exit code 0. The reason is to be fonud in the archetype code. Reminder: For each pseudo terminal (pty), the archetype is the shared pty fhandler that owns the underlying native handles and supplies them to every per-file-descriptor fhandler for that pty. `open_with_arch()` calls `open()`, copies the first pty fhandler's state into the archetype, and then calls `open_setup()`. At that stage, pcon handle adoption already took place in `open_setup()`. This was not anticipated by 60a88896dc (Cygwin: pty: do not leak nat handles when adopting the pcon's in open_setup(), 2026-06-25), which tried to fix a leak by closing the superseded native handles as they were replaced in `open_setup()`. Because `open_with_arch()` had already copied those handle values into the archetype, closing them invalidated the archetype's copies. The archetype therefore retained stale values for those closed handles, which later pty fd fhandlers would inherit. If Windows reuses one of those values for a newly duplicated pcon handle, closing the stale value closes the new handle instead. The nested native program then receives unusable console input. Preserve usable console input by moving the unchanged transactional pcon handle adoption to `open()`, before the archetype snapshot. The archetype then receives valid pcon handles, all pty fd fhandlers inherit live handles, and the superseded raw pipe handles are closed exactly once. This commit is best viewed with `--color-moved`. Fixes: 60a88896dce0 ("Cygwin: pty: do not leak nat handles when adopting the pcon's in open_setup()") Assisted-by: GPT-5.6 Sol Signed-off-by: Johannes Schindelin --- winsup/cygwin/fhandler/pty.cc | 44 +++++++++++++++++------------------ 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/winsup/cygwin/fhandler/pty.cc b/winsup/cygwin/fhandler/pty.cc index 2c28e7d8eb..ba726a061c 100644 --- a/winsup/cygwin/fhandler/pty.cc +++ b/winsup/cygwin/fhandler/pty.cc @@ -1059,26 +1059,6 @@ fhandler_pty_slave::open (int flags, mode_t) release_attach_mutex (); } - set_open_status (); - return 1; - -err: - if (GetLastError () == ERROR_FILE_NOT_FOUND) - set_errno (ENXIO); - else - __seterrno (); -err_no_errno: - termios_printf (errmsg); -err_no_msg: - for (HANDLE **h = handles; *h; h++) - if (**h && **h != INVALID_HANDLE_VALUE) - CloseHandle (**h); - return 0; -} - -bool -fhandler_pty_slave::open_setup (int flags) -{ if (get_ttyp ()->pcon_activated) { HANDLE pcon_owner = OpenProcess (PROCESS_DUP_HANDLE, FALSE, @@ -1094,8 +1074,8 @@ fhandler_pty_slave::open_setup (int flags) 0, TRUE, DUPLICATE_SAME_ACCESS); if (ok_in && ok_out) { - /* Close the cyg master-side handles open() installed before - replacing them, so they do not leak. */ + /* Replace these before open_with_arch() copies them into the + archetype shared by all pty slave fhandlers. */ CloseHandle (get_handle_nat ()); CloseHandle (get_output_handle_nat ()); set_handle_nat (new_in); @@ -1112,6 +1092,26 @@ fhandler_pty_slave::open_setup (int flags) } } + set_open_status (); + return 1; + +err: + if (GetLastError () == ERROR_FILE_NOT_FOUND) + set_errno (ENXIO); + else + __seterrno (); +err_no_errno: + termios_printf (errmsg); +err_no_msg: + for (HANDLE **h = handles; *h; h++) + if (**h && **h != INVALID_HANDLE_VALUE) + CloseHandle (**h); + return 0; +} + +bool +fhandler_pty_slave::open_setup (int flags) +{ set_flags ((flags & ~O_TEXT) | O_BINARY); myself->set_ctty (this, flags); report_tty_counts (this, "opened", ""); From 82b89f8b04b964c814572731e697d690bdb6ef7f Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 16 Jul 2026 18:32:56 +0200 Subject: [PATCH 2/7] ui-tests: catch redirected console input through native Git aliases When native `git.exe` runs a `!` alias under `mintty`, it starts `sh.exe`, which may in turn launch another native program. A pseudo console regression made that program see standard input as redirected, causing console APIs such as `[Console]::KeyAvailable` to fail. The existing `mintty` grandchild-input test is the right home for a regression test to catch such a bug because it already launches an interactive `mintty` session and exercises native/Cygwin/native process chains, while the Git invocation itself exercises the relevant alias path. Keep the process chain attached to `mintty` and use PowerShell's file APIs to persist the observations. Shell redirection would replace Git's standard handles before pseudo console setup and mask the regression. Discard stale results so repeated local runs cannot pass spuriously. Assisted-by: GPT 5.6 Sol Signed-off-by: Johannes Schindelin --- ui-tests/pcon-grandchild-input.ahk | 36 ++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/ui-tests/pcon-grandchild-input.ahk b/ui-tests/pcon-grandchild-input.ahk index 890d4d44fe..7d1b331cb1 100644 --- a/ui-tests/pcon-grandchild-input.ahk +++ b/ui-tests/pcon-grandchild-input.ahk @@ -37,6 +37,42 @@ if !InStr(capture, '$ ') ExitWithError 'Timed out waiting for bash prompt' Info 'Bash prompt appeared' +; A native Git process starts sh.exe for the alias, which in turn starts +; native PowerShell. Verify that PowerShell still sees console input and +; can query Console.KeyAvailable. Do not use shell redirection here: it +; changes Git's handles before pcon setup and masks the regression. +probeOutput := 'git-alias-console.out' +probeStatus := 'git-alias-console.status' +if FileExist(probeOutput) + FileDelete probeOutput +if FileExist(probeStatus) + FileDelete probeStatus +probeCommand := "git -c 'alias.console-probe=!powershell.exe " . + "-NoLogo -NoProfile -Command `"\$redirected = " . + "[Console]::IsInputRedirected; try { " . + "[void][Console]::KeyAvailable; \$status = 0 } catch { \$status = 1 }; " . + "Set-Content -NoNewline -Path " probeOutput " -Value \$redirected; " . + "Set-Content -NoNewline -Path " probeStatus " -Value \$status; " . + "exit \$status`"' console-probe" +WinActivate(winId) +SendEvent('{Text}' probeCommand) +SendEvent('{Enter}') + +deadline := A_TickCount + 10000 +while !FileExist(probeStatus) && A_TickCount < deadline + Sleep 100 +if !FileExist(probeStatus) + ExitWithError 'Timed out waiting for Git alias console probe' + +probeRedirected := Trim(FileRead(probeOutput), ' `t`r`n') +probeExitCode := Trim(FileRead(probeStatus), ' `t`r`n') +Info 'Git alias console probe output: ' probeRedirected +Info 'Git alias console probe exit code: ' probeExitCode +if probeRedirected != 'False' + ExitWithError 'PowerShell stdin is redirected through the Git alias' +if probeExitCode != '0' + ExitWithError 'PowerShell could not query Console.KeyAvailable' + ; Launch cmd.exe to activate pseudo console WinActivate(winId) SetKeyDelay 20, 20 From ea9f8c4c78c1e805f22b67a1f4aec93a85ed78b5 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 17 Jul 2026 12:03:25 +0200 Subject: [PATCH 3/7] fixup! ui-tests: do verify the SSH hang fix Use a more robust way to verify the long clone. Assisted-by: GPT-5.6 Sol Signed-off-by: Johannes Schindelin --- ui-tests/ctrl-c.ahk | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/ui-tests/ctrl-c.ahk b/ui-tests/ctrl-c.ahk index 18d90b70ef..8521a74ce2 100644 --- a/ui-tests/ctrl-c.ahk +++ b/ui-tests/ctrl-c.ahk @@ -175,18 +175,26 @@ if (openSSHPath != '' and FileExist(openSSHPath . '\sshd.exe')) { Info('Starting clone') retries := 5 + cloneResultMarker := 'GIT_CLONE_EXIT_CODE=' Loop retries { - Send('git -c core.sshCommand="ssh ' . sshOptions . '" clone ' . cloneOptions . '{Enter}') + Send('git -c core.sshCommand="ssh ' . sshOptions . '" clone ' . + cloneOptions . '; Write-Output "' . cloneResultMarker . + '$LASTEXITCODE"{Enter}') Sleep 500 Info('Waiting for clone to finish (attempt ' . A_Index . '/' . retries . ')') WinActivate('ahk_id ' . hwnd) - matchObj := WaitForRegExInWindowsTerminal('(Receiving objects: .*, done\.|fatal: early EOF)`r?`nPS .*>[ `n`r]*$', 'Timed out waiting for clone to finish', 'Clone command completed', 15000, 'ahk_id ' . hwnd) + matchObj := WaitForRegExInWindowsTerminal( + cloneResultMarker . '([0-9]+)`r?`nPS .*>[ `n`r]*$', + 'Timed out waiting for clone to finish', + 'Clone command completed', 15000, 'ahk_id ' . hwnd) - if InStr(matchObj[1], 'done.') + if matchObj[1] == '0' break if A_Index == retries - ExitWithError('Clone failed after ' . retries . ' attempts (early EOF)') - Info('Clone failed (early EOF), restarting SSH server and retrying...') + ExitWithError('Clone failed after ' . retries . + ' attempts (exit code ' . matchObj[1] . ')') + Info('Clone failed with exit code ' . matchObj[1] . + ', restarting SSH server and retrying...') if DirExist(largeGitClonePath) DirDelete(largeGitClonePath, true) ; Restart sshd for the next attempt (it may have exited after the failed connection) From a001d7d26e3e2ea563536cfe266af5f886302ab6 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 17 Jul 2026 16:11:59 +0200 Subject: [PATCH 4/7] fixup! ui-tests: do verify the SSH hang fix Prevent cleanup from hanging by directing `exit` to the test's recorded PowerShell window and waiting for it to close before removing the worktree. Assisted-by: GPT-5.6 Sol Signed-off-by: Johannes Schindelin --- ui-tests/ctrl-c.ahk | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ui-tests/ctrl-c.ahk b/ui-tests/ctrl-c.ahk index 8521a74ce2..3153b0dc73 100644 --- a/ui-tests/ctrl-c.ahk +++ b/ui-tests/ctrl-c.ahk @@ -218,6 +218,9 @@ if (openSSHPath != '' and FileExist(openSSHPath . '\sshd.exe')) { } } +WinActivate('ahk_id ' . hwnd) Send('exit{Enter}') -Sleep 50 +if !WinWaitClose('ahk_id ' . hwnd, , 10) + ExitWithError 'PowerShell window did not close' +Info 'PowerShell window closed' CleanUpWorkTree() \ No newline at end of file From 66105ea37f2f87db9b45280c4e20a2d610ab1ba8 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 17 Jul 2026 16:37:55 +0200 Subject: [PATCH 5/7] fixup! ui-tests: do verify the SSH hang fix Avoid false interrupt timeouts by waiting until the shell alias confirms `sleep` is running, then reactivating the recorded PowerShell window before sending Ctrl+C. Assisted-by: GPT-5.6 Sol Signed-off-by: Johannes Schindelin --- ui-tests/ctrl-c.ahk | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/ui-tests/ctrl-c.ahk b/ui-tests/ctrl-c.ahk index 3153b0dc73..8f3299a0cf 100644 --- a/ui-tests/ctrl-c.ahk +++ b/ui-tests/ctrl-c.ahk @@ -32,10 +32,15 @@ WaitForRegExInWindowsTerminal('PS [A-Z]:.*>[ `n`r]*$', 'Timed out waiting for Po ; sleep test Sleep 1500 ; The `:;` is needed to force Git to call this via the shell, otherwise `/usr/bin/` would not resolve. -Send('git -c alias.sleep="{!}:;/usr/bin/sleep" sleep 15{Enter}') -Sleep 500 +Send('git -c alias.sleep="{!}:;echo __SLEEP_STARTED__;' . + '/usr/bin/sleep" sleep 15{Enter}') +WaitForRegExInWindowsTerminal( + '(^|`n)__SLEEP_STARTED__`r?`n', + 'Timed out waiting for sleep to start', 'Sleep started', + 10000, 'ahk_id ' . hwnd) ; interrupt sleep; Ideally we'd call `Send('^C')` but that would too quick on GitHub Actions' runners. ; The idea for this work-around comes from https://www.reddit.com/r/AutoHotkey/comments/aok10s/comment/eg57e81/. +WinActivate('ahk_id ' . hwnd) Send '{Ctrl down}{c down}' Sleep 50 Send '{c up}{Ctrl up}' From ca2f3947cbaa43175c9ec74370c9b45a5daa3955 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 17 Jul 2026 16:57:55 +0200 Subject: [PATCH 6/7] fixup! ui-tests: do verify the SSH hang fix Avoid false clone timeouts by treating the explicit exit-code marker as sufficient completion evidence instead of requiring a clean PowerShell prompt, because the buffer-export hotkey can append `[24~` to it. Assisted-by: GPT-5.6 Sol Signed-off-by: Johannes Schindelin --- ui-tests/ctrl-c.ahk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-tests/ctrl-c.ahk b/ui-tests/ctrl-c.ahk index 8f3299a0cf..f290d759bf 100644 --- a/ui-tests/ctrl-c.ahk +++ b/ui-tests/ctrl-c.ahk @@ -189,7 +189,7 @@ if (openSSHPath != '' and FileExist(openSSHPath . '\sshd.exe')) { Info('Waiting for clone to finish (attempt ' . A_Index . '/' . retries . ')') WinActivate('ahk_id ' . hwnd) matchObj := WaitForRegExInWindowsTerminal( - cloneResultMarker . '([0-9]+)`r?`nPS .*>[ `n`r]*$', + cloneResultMarker . '([0-9]+)', 'Timed out waiting for clone to finish', 'Clone command completed', 15000, 'ahk_id ' . hwnd) From 55abf7622f3a1760817e31401c9dbf381246a8e5 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sun, 19 Jul 2026 22:03:04 +0200 Subject: [PATCH 7/7] fixup! ui-tests: do verify the SSH hang fix Windows 2025 runners intermittently drop a single synthesized Ctrl+C or `exit`. Screen-buffer polling can also miss the short-lived clone `ssh.exe`, or be polluted by terminal key sequences. A dropped clone interrupt lets a fast localhost clone complete normally. Interrupted Git cleanup may also leave harmless empty `.git/{objects,refs}` scaffolding because Windows delete-pending handles can make `rmdir` lose a race. Use exact process ownership, rather than ambient process names or screen contents, as the invariant. Subscribe before clone launch and identify only the `ssh.exe` carrying the test's unique key path. Track and revalidate the exact owned PIDs. Restart and clean up only `sshd` PIDs launched by the test. Do not assume that unrelated Git, SSH, or `sshd` processes are absent, and never stop any process by name. Refocus and reissue Ctrl+C until the exact clone `ssh.exe` exits. Treat surviving files as failures, but tolerate and remove empty scaffolding. Reissue `exit` until the recorded window closes. Use an explicit exit-code marker for the successful clone instead of fragile terminal-output ordering. Claude Opus 4.8 instrumented dropped input: quick Ctrl+C was delivered in 9/12 attempts and dropped in 3/12. Deliberate key-down/up still dropped 1/12. Its second internal stress attempt passed 20/20. The committed debug-branch source then independently passed the initial Windows 2025 20-run gate. Copilot steps were skipped, and no error text appeared in any iteration log. The ported feature-branch source matches the tested source except for one behavior-neutral line wrap. It passes the AutoHotkey parser and hygiene checks. Assisted-by: Claude Opus 4.8 Assisted-by: GPT-5.6 Sol Signed-off-by: Johannes Schindelin --- ui-tests/ctrl-c.ahk | 223 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 186 insertions(+), 37 deletions(-) diff --git a/ui-tests/ctrl-c.ahk b/ui-tests/ctrl-c.ahk index f290d759bf..db41841bdc 100644 --- a/ui-tests/ctrl-c.ahk +++ b/ui-tests/ctrl-c.ahk @@ -96,14 +96,14 @@ if (openSSHPath != '' and FileExist(openSSHPath . '\sshd.exe')) { ExitWithError 'Could not add admin read permission from ' . path . ': ' A_LastError } - WaitForSshd() { + WaitForSshd(expectedPID) { deadline := A_TickCount + 60000 while true { if FileExist('sshd.pid') { content := '' try content := Trim(FileRead('sshd.pid'), ' `t`r`n') - if content != '' { + if content == expectedPID && ProcessExist(expectedPID) { Info('sshd is accepting connections (PID ' . content . ')') return } @@ -114,6 +114,94 @@ if (openSSHPath != '' and FileExist(openSSHPath . '\sshd.exe')) { } } + StartSshd(openSSHPath, sshdOptions, sshdPIDs) { + try FileDelete('sshd.pid') + Run(openSSHPath . '\sshd.exe ' . sshdOptions, '', 'Hide', &pid) + if A_LastError + ExitWithError 'Error starting SSH server: ' A_LastError + sshdPIDs.Push(pid) + Info('Started SSH server: ' . pid) + WaitForSshd(pid) + return pid + } + + StopSshd(pid, openSSHPath, sshdPIDs) { + if !pid + return true + proc := FindProcess(pid) + executablePath := '' + if proc + try executablePath := proc.ExecutablePath + if executablePath == openSSHPath . '\sshd.exe' { + Info('Stopping sshd.exe (PID ' . pid . ')') + try ProcessClose(pid) + try ProcessWaitClose(pid, 5) + } + if !ProcessExist(pid) { + loop sshdPIDs.Length { + if sshdPIDs[A_Index] == pid { + sshdPIDs.RemoveAt(A_Index) + break + } + } + } + return !ProcessExist(pid) + } + + CleanUpSshdProcesses(sshdPIDs, openSSHPath, *) { + for pid in sshdPIDs.Clone() + StopSshd(pid, openSSHPath, sshdPIDs) + } + + FindProcess(pid) { + query := 'SELECT ProcessId, ParentProcessId, Name, CommandLine, ' . + 'ExecutablePath FROM Win32_Process WHERE ProcessId = ' . pid + for proc in ComObjGet('winmgmts:').ExecQuery(query) + return proc + return 0 + } + + ProcessMatches(pid, name, marker) { + proc := FindProcess(pid) + if !proc || proc.Name != name + return false + commandLine := '' + try commandLine := proc.CommandLine + return InStr(commandLine, marker) + } + + ; Count the regular files (not directories) below `dir`, recursing into + ; subdirectories and hidden entries such as a `.git` folder. + CountFilesRecursively(dir) { + count := 0 + Loop Files, dir . '\*', 'FR' + count++ + return count + } + + WatchSshStarts() { + query := 'SELECT * FROM Win32_ProcessStartTrace ' . + 'WHERE ProcessName = "ssh.exe"' + return ComObjGet('winmgmts:').ExecNotificationQuery(query) + } + + WaitForCloneSsh(events, keyPath) { + deadline := A_TickCount + 15000 + while A_TickCount < deadline { + try event := events.NextEvent(deadline - A_TickCount) + catch + break + ssh := FindProcess(event.ProcessID) + if !ssh + continue + sshCommandLine := '' + try sshCommandLine := ssh.CommandLine + if InStr(sshCommandLine, keyPath) + return ssh.ProcessId + } + return 0 + } + ; Set up SSH server Info('Generating host key') RunWait('git -c alias.c="!ssh-keygen -b 4096 -f ssh_host_rsa_key -N \"\"" c', '', 'Hide') @@ -134,13 +222,14 @@ if (openSSHPath != '' and FileExist(openSSHPath . '\sshd.exe')) { 'PidFile "' . workTree . '\sshd.pid"`n', 'sshd_config') sshdOptions := '-f "' . workTree . '\sshd_config" -D -E "' . workTree . '\sshd.log"' + sshdPIDs := [] + sshdCleanup := CleanUpSshdProcesses.Bind( + sshdPIDs, openSSHPath) + OnExit(sshdCleanup) ; Start SSH server Info('Starting SSH server') - Run(openSSHPath . '\sshd.exe ' . sshdOptions, '', 'Hide', &sshdPID) - if A_LastError - ExitWithError 'Error starting SSH server: ' A_LastError - Info('Started SSH server: ' sshdPID) + sshdPID := StartSshd(openSSHPath, sshdOptions, sshdPIDs) Info('Starting clone') workTreeMSYS := RunWaitOne('git -c alias.cygpath="!cygpath" cygpath -u "' . workTree . '"') @@ -157,31 +246,86 @@ if (openSSHPath != '' and FileExist(openSSHPath . '\sshd.exe')) { ; `ssh.exe` prefixes the username with the domain name. cloneOptions := '--upload-pack="powershell git upload-pack" "' . EnvGet('USERNAME') . '@localhost:' . largeGitRepoPath . '" "' . largeGitClonePath . '"' - WaitForSshd() - Send('git -c core.sshCommand="ssh ' . sshOptions . '" clone ' . cloneOptions . '{Enter}') - Sleep 50 - Info('Waiting for clone to start') + sshStartEvents := WatchSshStarts() WinActivate('ahk_id ' . hwnd) - WaitForRegExInWindowsTerminal('remote: ', 'Timed out waiting for clone to start', 'Clone started', 15000, 'ahk_id ' . hwnd) + Send('git -c core.sshCommand="ssh ' . sshOptions . '" clone ' . + cloneOptions . '{Enter}') + cloneSshPID := WaitForCloneSsh( + sshStartEvents, workTreeMSYS . '/id_rsa') + if !cloneSshPID + ExitWithError 'Timed out waiting for clone ssh.exe' + Info('Clone ssh.exe started: ' . cloneSshPID) Info('Trying to interrupt clone') - Send('^C') ; interrupt clone - Sleep 150 - WaitForRegExInWindowsTerminal('`nfatal: (.*`r?`n){1,3}PS .*>[ `n`r]*$', 'Timed out waiting for clone to be interrupted', 'clone was interrupted as desired') + if !ProcessMatches(cloneSshPID, 'ssh.exe', workTreeMSYS . '/id_rsa') + ExitWithError 'Clone completed before Ctrl+C could be sent' + ; Interrupt the clone. A bare `Send('^C')` is too quick to be delivered + ; reliably on GitHub Actions' runners (see the sleep interrupt above), and + ; even the deliberate key-down/up sequence is occasionally lost to a + ; focus/scheduling race. A missed interrupt lets the clone run to completion + ; (its ssh.exe only exits once the ~26M transfer finishes), so keep + ; re-issuing the Ctrl+C, re-focusing the window each time, until ssh.exe + ; actually exits. + deadline := A_TickCount + 15000 + while ProcessMatches( + cloneSshPID, 'ssh.exe', workTreeMSYS . '/id_rsa') && + A_TickCount < deadline { + WinActivate('ahk_id ' . hwnd) + Send '{Ctrl down}{c down}' + Sleep 50 + Send '{c up}{Ctrl up}' + checkDeadline := A_TickCount + 600 + while ProcessExist(cloneSshPID) && A_TickCount < checkDeadline + Sleep 20 + } + if ProcessMatches(cloneSshPID, 'ssh.exe', workTreeMSYS . '/id_rsa') + ExitWithError 'Clone ssh.exe did not exit after Ctrl+C' + Info('clone was interrupted as desired') - if DirExist(largeGitClonePath) - ExitWithError('`large-clone` was unexpectedly not deleted on interrupt') + ; Interrupting `git clone` makes it run its `remove_junk` cleanup, which + ; unlinks every file of the partial clone. On Windows that cleanup races + ; with the still-terminating child processes: their delete-pending file + ; handles (and CWDs) keep the now file-less directories busy, so git's + ; `rmdir` of the empty scaffolding fails and it gives up, permanently + ; leaving behind an empty `large-clone\.git\{objects,refs}` skeleton. That + ; benign leftover is not a completed clone, so it must not fail the test: + ; the interrupt is already proven by the clone's `ssh.exe` having exited + ; (above) and by the clone content being gone. Wait for every file to + ; disappear (tolerating empty directories), fail only if actual clone + ; content survives (i.e. the clone was not aborted), then remove any empty + ; scaffolding ourselves so the verification clone below starts clean. + deadline := A_TickCount + 5000 + while DirExist(largeGitClonePath) && + CountFilesRecursively(largeGitClonePath) > 0 && + A_TickCount < deadline + Sleep 10 + if DirExist(largeGitClonePath) { + remainingFiles := CountFilesRecursively(largeGitClonePath) + if remainingFiles > 0 + ExitWithError('`large-clone` still contained ' . remainingFiles . + ' file(s) after interrupt (clone was not aborted)') + ; Only empty scaffolding remains; drop it so the verification clone + ; below can create the target afresh. The directories may stay briefly + ; busy while the interrupted clone's children finish exiting, so retry. + deadline := A_TickCount + 5000 + while DirExist(largeGitClonePath) && A_TickCount < deadline { + try DirDelete(largeGitClonePath, true) + if !DirExist(largeGitClonePath) + break + Sleep 50 + } + } ; Now verify that the SSH-based clone actually works and does not hang Info('Re-starting SSH server') - Run(openSSHPath . '\sshd.exe ' . sshdOptions, '', 'Hide', &sshdPID) - if A_LastError - ExitWithError 'Error starting SSH server: ' A_LastError - Info('Started SSH server: ' sshdPID) + if !StopSshd(sshdPID, openSSHPath, sshdPIDs) + ExitWithError 'Could not stop SSH server before restart' + sshdPID := StartSshd(openSSHPath, sshdOptions, sshdPIDs) Info('Starting clone') retries := 5 cloneResultMarker := 'GIT_CLONE_EXIT_CODE=' Loop retries { + WinActivate('ahk_id ' . hwnd) Send('git -c core.sshCommand="ssh ' . sshOptions . '" clone ' . cloneOptions . '; Write-Output "' . cloneResultMarker . '$LASTEXITCODE"{Enter}') @@ -202,30 +346,35 @@ if (openSSHPath != '' and FileExist(openSSHPath . '\sshd.exe')) { ', restarting SSH server and retrying...') if DirExist(largeGitClonePath) DirDelete(largeGitClonePath, true) - ; Restart sshd for the next attempt (it may have exited after the failed connection) - Run(openSSHPath . '\sshd.exe ' . sshdOptions, '', 'Hide', &sshdPID) - if A_LastError - ExitWithError 'Error restarting SSH server: ' A_LastError - Info('Restarted SSH server: ' sshdPID) + if !StopSshd(sshdPID, openSSHPath, sshdPIDs) + ExitWithError 'Could not stop SSH server before retry' + sshdPID := StartSshd(openSSHPath, sshdOptions, sshdPIDs) + Info('Restarted SSH server: ' . sshdPID) } if not DirExist(largeGitClonePath) ExitWithError('`large-clone` did not work?!?') - for proc in ComObjGet('winmgmts:').ExecQuery('SELECT ProcessId, Name, ExecutablePath FROM Win32_Process WHERE Name LIKE "sshd%.exe"') { - if (proc.ExecutablePath != '' and InStr(proc.ExecutablePath, openSSHPath) > 0) { - Info('Stopping ' . proc.Name . ' (PID ' . proc.ProcessId . ')') - try { - ProcessClose proc.ProcessId - ProcessWaitClose proc.ProcessId, 5 - } - } - } + CleanUpSshdProcesses(sshdPIDs, openSSHPath) + if sshdPIDs.Length + ExitWithError 'Could not stop all SSH servers' + OnExit(sshdCleanup, 0) } -WinActivate('ahk_id ' . hwnd) -Send('exit{Enter}') -if !WinWaitClose('ahk_id ' . hwnd, , 10) +; Close the PowerShell window. As with the Ctrl+C interrupts above, a single +; `Send('exit{Enter}')` is occasionally lost to a focus/scheduling race on +; GitHub Actions' runners, which would leave the Windows Terminal window (and +; its OpenConsole/PowerShell processes) behind. Re-issue the exit, re-focusing +; the window each time, until it actually closes; the leading `{Enter}` flushes +; any partial command a half-delivered attempt might have left on the prompt. +deadline := A_TickCount + 20000 +while WinExist('ahk_id ' . hwnd) && A_TickCount < deadline { + try WinActivate('ahk_id ' . hwnd) + if WinExist('ahk_id ' . hwnd) + Send('{Enter}exit{Enter}') + WinWaitClose('ahk_id ' . hwnd, , 3) +} +if WinExist('ahk_id ' . hwnd) ExitWithError 'PowerShell window did not close' Info 'PowerShell window closed' CleanUpWorkTree() \ No newline at end of file