diff --git a/ui-tests/ctrl-c.ahk b/ui-tests/ctrl-c.ahk index 18d90b70ef..db41841bdc 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}' @@ -91,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 } @@ -109,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') @@ -129,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 . '"') @@ -152,64 +246,135 @@ 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 { - Send('git -c core.sshCommand="ssh ' . sshOptions . '" clone ' . cloneOptions . '{Enter}') + WinActivate('ahk_id ' . hwnd) + 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]+)', + '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) - 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) } -Send('exit{Enter}') -Sleep 50 +; 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 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 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", "");