From f6c383341d76a331941f22c1a046a3d4bb1f8ce3 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 20 Feb 2026 20:55:12 +0100 Subject: [PATCH 01/15] ui-tests: offer a more robust way to capture the terminal text The existing method to capture text from Windows Terminal emulates mouse movements, dragging across the entire window with finicky pixel calculations for title bar height, scroll bar width and padding, then right-clicks to copy. This is fragile: if the window geometry changes, if another window gets focus, or if the title bar height differs between OS versions, the capture silently gets the wrong text. Windows Terminal's exportBuffer action avoids all of that by writing the complete scrollback buffer to a file on a keybinding, with no dependence on pixel positions or window focus. To use it, WT must run in portable mode with a settings.json that defines the action and keybinding. Add setup-portable-wt.ps1, which downloads WT (when not already present), creates the .portable marker and writes settings.json with Ctrl+Shift+F12 bound to exportBuffer. It accepts a -DestDir parameter so CI can use $RUNNER_TEMP while local development uses $TEMP. When running inside GitHub Actions it also appends the WT directory to $GITHUB_PATH. In the CI workflow, replace the inline "Install Windows Terminal" step with a call to the setup script (which is available after checkout). In the AHK test library, add CaptureBufferFromWindowsTerminal() which triggers the keybinding, waits for the export file, and returns the buffer contents. The export file is written into the script directory so it gets uploaded as a build artifact on failure. Assisted-by: Claude Opus 4.6 Signed-off-by: Johannes Schindelin --- .github/workflows/ui-tests.yml | 23 +++----- ui-tests/.gitattributes | 1 + ui-tests/setup-portable-wt.ps1 | 96 ++++++++++++++++++++++++++++++++++ ui-tests/ui-test-library.ahk | 21 ++++++++ 4 files changed, 126 insertions(+), 15 deletions(-) create mode 100644 ui-tests/setup-portable-wt.ps1 diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index 0a73526e35..8208dbfad8 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -32,28 +32,25 @@ jobs: $p = Get-ChildItem -Recurse "${env:RUNNER_TEMP}\artifacts" | where {$_.Name -eq "msys-2.0.dll"} | Select -ExpandProperty VersionInfo | Select -First 1 -ExpandProperty FileName cp $p "c:/Program Files/Git/usr/bin/msys-2.0.dll" + - uses: actions/checkout@v6 + with: + sparse-checkout: | + ui-tests + - uses: actions/cache/restore@v5 id: restore-wt with: key: wt-${{ env.WT_VERSION }} path: ${{ runner.temp }}/wt.zip - - name: Download Windows Terminal - if: steps.restore-wt.outputs.cache-hit != 'true' - shell: bash + - name: Install and configure portable Windows Terminal + working-directory: ui-tests run: | - curl -fLo "$RUNNER_TEMP/wt.zip" \ - https://github.com/microsoft/terminal/releases/download/v$WT_VERSION/Microsoft.WindowsTerminal_${WT_VERSION}_x64.zip + powershell -File setup-portable-wt.ps1 -WtVersion $env:WT_VERSION -DestDir $env:RUNNER_TEMP - uses: actions/cache/save@v5 if: steps.restore-wt.outputs.cache-hit != 'true' with: key: wt-${{ env.WT_VERSION }} path: ${{ runner.temp }}/wt.zip - - name: Install Windows Terminal - shell: bash - working-directory: ${{ runner.temp }} - run: | - "$WINDIR/system32/tar.exe" -xf "$RUNNER_TEMP/wt.zip" && - cygpath -aw terminal-$WT_VERSION >>$GITHUB_PATH - uses: actions/cache/restore@v5 id: restore-ahk with: @@ -99,10 +96,6 @@ jobs: "$WINDIR/system32/tar.exe" -C "$RUNNER_TEMP" -xvf "$RUNNER_TEMP/win32-openssh.zip" && echo "OPENSSH_FOR_WINDOWS_DIRECTORY=$(cygpath -aw "$RUNNER_TEMP/OpenSSH-Win64")" >>$GITHUB_ENV - - uses: actions/checkout@v6 - with: - sparse-checkout: | - ui-tests - name: Minimize existing Log window working-directory: ui-tests run: | diff --git a/ui-tests/.gitattributes b/ui-tests/.gitattributes index 4dd1b9375b..7d5ccef0ca 100644 --- a/ui-tests/.gitattributes +++ b/ui-tests/.gitattributes @@ -1 +1,2 @@ *.ahk eol=lf +*.ps1 eol=lf diff --git a/ui-tests/setup-portable-wt.ps1 b/ui-tests/setup-portable-wt.ps1 new file mode 100644 index 0000000000..572e6bc119 --- /dev/null +++ b/ui-tests/setup-portable-wt.ps1 @@ -0,0 +1,96 @@ +# Configures a portable Windows Terminal for the UI tests. +# +# Downloads WT if needed, then creates .portable marker and settings.json +# with exportBuffer bound to Ctrl+Shift+F12. The export file lands in the +# script's own directory (ui-tests/) so it gets uploaded as build artifact. +# +# The portable WT uses its own settings directory (next to the executable) +# so it never touches the user's installed Windows Terminal configuration. + +param( + [string]$WtVersion = $env:WT_VERSION, + [string]$DestDir = $env:TEMP +) + +if (-not $WtVersion) { $WtVersion = '1.22.11141.0' } + +$wtDir = "$DestDir\terminal-$WtVersion" +$wtExe = "$wtDir\wt.exe" + +# Download if the directory doesn't contain wt.exe yet +if (-not (Test-Path $wtExe)) { + $wtZip = "$DestDir\wt.zip" + if (-not (Test-Path $wtZip)) { + $url = "https://github.com/microsoft/terminal/releases/download/v$WtVersion/Microsoft.WindowsTerminal_${WtVersion}_x64.zip" + Write-Host "Downloading Windows Terminal $WtVersion ..." + curl.exe -fLo $wtZip $url + if ($LASTEXITCODE -ne 0) { throw "Download failed" } + } + Write-Host "Extracting ..." + & "$env:WINDIR\system32\tar.exe" -C $DestDir -xf $wtZip + if ($LASTEXITCODE -ne 0) { throw "Extract failed" } +} + +# Create .portable marker so WT reads settings from settings\ next to wt.exe +$portableMarker = "$wtDir\.portable" +if (-not (Test-Path $portableMarker)) { + Set-Content -Path $portableMarker -Value "" +} + +# Write settings.json with exportBuffer action +$settingsDir = "$wtDir\settings" +if (-not (Test-Path $settingsDir)) { New-Item -ItemType Directory -Path $settingsDir -Force | Out-Null } + +$bufferExportPath = ($PSScriptRoot + '\wt-buffer-export.txt') -replace '\\', '/' + +$settings = @" +{ + "`$schema": "https://aka.ms/terminal-profiles-schema", + "actions": [ + { + "command": { + "action": "exportBuffer", + "path": "$bufferExportPath" + }, + "id": "User.TestExportBuffer" + }, + { + "command": { "action": "copy", "singleLine": false }, + "id": "User.copy" + }, + { "command": "paste", "id": "User.paste" } + ], + "copyFormatting": "none", + "copyOnSelect": false, + "defaultProfile": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}", + "keybindings": [ + { "id": "User.TestExportBuffer", "keys": "ctrl+shift+f12" }, + { "id": null, "keys": "ctrl+v" }, + { "id": null, "keys": "ctrl+c" } + ], + "profiles": { + "defaults": {}, + "list": [ + { + "commandline": "%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", + "guid": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}", + "hidden": false, + "name": "Windows PowerShell" + } + ] + }, + "schemes": [], + "themes": [] +} +"@ + +Set-Content -Path "$settingsDir\settings.json" -Value $settings + +# Add WT to PATH if running in GitHub Actions +if ($env:GITHUB_PATH) { + $wtDir | Out-File -Append -FilePath $env:GITHUB_PATH +} + +Write-Host "Portable WT ready at: $wtDir" +Write-Host " exportBuffer path: $bufferExportPath" +Write-Host " exportBuffer key: Ctrl+Shift+F12" diff --git a/ui-tests/ui-test-library.ahk b/ui-tests/ui-test-library.ahk index 120e85ed76..eea0570ebd 100644 --- a/ui-tests/ui-test-library.ahk +++ b/ui-tests/ui-test-library.ahk @@ -104,6 +104,27 @@ CaptureTextFromWindowsTerminal(winTitle := '') { return Result } +; Capture the Windows Terminal buffer via the exportBuffer action (Ctrl+Shift+F12). +; Requires a portable WT with settings.json that maps Ctrl+Shift+F12 to exportBuffer +; writing to /wt-buffer-export.txt. Unlike CaptureTextFromWindowsTerminal(), +; this does not depend on mouse position or window focus quirks. +CaptureBufferFromWindowsTerminal(winTitle := '') { + static exportFile := A_ScriptDir . '\wt-buffer-export.txt' + if FileExist(exportFile) + FileDelete exportFile + if winTitle != '' + WinActivate winTitle + Sleep 200 + Send '^+{F12}' + deadline := A_TickCount + 3000 + while !FileExist(exportFile) && A_TickCount < deadline + Sleep 50 + if !FileExist(exportFile) + return '' + Sleep 100 + return FileRead(exportFile) +} + WaitForRegExInWindowsTerminal(regex, errorMessage, successMessage, timeout := 5000, winTitle := '') { timeout := timeout + A_TickCount ; Wait for the regex to match in the terminal output From 9e561bdeb64f22cfab27ea914f6f05c42788797a Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 20 Feb 2026 21:35:47 +0100 Subject: [PATCH 02/15] fixup! Start implementing UI-based tests by adding an AutoHotKey library Now that CaptureBufferFromWindowsTerminal() is available, switch WaitForRegExInWindowsTerminal() to use it instead of the mouse-drag based CaptureTextFromWindowsTerminal(). This also lets us drop the WheelDown scrolling that was needed because the mouse-drag method could only capture the visible portion of the terminal: exportBuffer writes the entire scrollback, so there is nothing to scroll to. Assisted-by: Claude Opus 4.6 Signed-off-by: Johannes Schindelin --- ui-tests/ui-test-library.ahk | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/ui-tests/ui-test-library.ahk b/ui-tests/ui-test-library.ahk index eea0570ebd..d62ea2eff4 100644 --- a/ui-tests/ui-test-library.ahk +++ b/ui-tests/ui-test-library.ahk @@ -130,7 +130,7 @@ WaitForRegExInWindowsTerminal(regex, errorMessage, successMessage, timeout := 50 ; Wait for the regex to match in the terminal output while true { - capturedText := CaptureTextFromWindowsTerminal(winTitle) + capturedText := CaptureBufferFromWindowsTerminal(winTitle) if RegExMatch(capturedText, regex) break Sleep 100 @@ -138,9 +138,6 @@ WaitForRegExInWindowsTerminal(regex, errorMessage, successMessage, timeout := 50 Info('Captured text:`n' . capturedText) ExitWithError errorMessage } - if winTitle != '' - WinActivate winTitle - MouseClick 'WheelDown', , , 20 } Info(successMessage) } \ No newline at end of file From 47c3440f69ecfa1a2bc41bbed2def340c6826f36 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 26 Feb 2026 14:10:27 +0100 Subject: [PATCH 03/15] Cygwin: pty: Fix jumbled keystrokes by removing the per-keystroke pipe transfer When rapidly typing at a Cygwin terminal while a native Windows program runs and exits (e.g. typing while a short-lived git command prints output in a mintty terminal), keystrokes can arrive at bash in the wrong order: typing "git log" may display "igt olg" or similar scrambled output. Background: the pseudo console's two-pipe architecture ------------------------------------------------------ The Cygwin PTY maintains two independent pairs of input pipes: "cyg" pipe (to_slave / from_master): For Cygwin processes. The master calls line_edit(), which implements POSIX line discipline (handling backspace, Ctrl-C, canonical-mode buffering, echo), then accept_input() writes the processed bytes to this pipe. The Cygwin slave (e.g. bash) reads from the other end. "nat" pipe (to_slave_nat / from_master_nat): For native Windows console programs. When the "pseudo console" (abbreviated "pcon" in the code) is active, Windows' conhost.exe wraps this pipe and provides the Win32 Console API (ReadConsoleInput, etc.) to the native app. The master writes raw bytes directly to this pipe. Both pipes are needed because Cygwin processes and native Windows programs have incompatible expectations. Cygwin processes read POSIX byte streams after line discipline processing. Native programs call ReadConsoleInput() for structured key-down/key-up events -- something a plain pipe cannot provide. A shared-memory variable `pty_input_state` (values: to_cyg or to_nat) tracks which pipe currently receives new input. The function transfer_input() moves pending data between the two pipes when the state changes. The flag `pcon_activated` indicates whether a Windows pseudo console is currently running. The problem: oscillation ------------------------ Each time a native program starts or exits in a PTY, the pseudo console activates or deactivates. Even a single such transition -- running git.exe and then returning to the bash prompt -- can trigger the bug. The effect is amplified when transitions happen in quick succession (e.g. a shell script calling several short-lived native commands), creating many oscillation cycles: (1) native program starts: pcon_activated=true, state=to_nat (2) native program exits: pcon deactivated, state=to_cyg (3) next native program: pcon reactivated, state=to_nat Git for Windows' AutoHotKey-based UI tests create this pattern deliberately by having PowerShell invoke Cygwin utilities in a tight loop to achieve near-100% reproduction. During each transition, master::write() -- which routes every keystroke from the terminal emulator -- must decide which pipe to use. How the transfer steals readline's data --------------------------------------- In master::write(), after the pcon+nat fast code path and before calling line_edit(), there was a code block that runs when: to_be_read_from_nat_pipe() is true (a native app is "in charge") && pcon_activated is false (pcon momentarily OFF) && pty_input_state is to_cyg (input going to cyg pipe) When all three conditions hold, it calls transfer_input(to_nat) to move ALL pending data from the cyg pipe to the nat pipe. The intent was to handle a specific scenario where, with pseudo console disabled, input lingered in the wrong pipe after a Cygwin child exited. The problem is that during oscillation step (2), these conditions are also true -- and the cyg pipe contains bash's readline buffer with the partially-typed command line. On every keystroke during the gap, this code reads ALL of readline's buffered input out of the cyg pipe and pushes it into the nat pipe: Keystroke 'g' arrives during oscillation gap (step 2): | v transfer_input(to_nat) <--- reads readline's prior "it" | from cyg pipe, writes to nat v line_edit('g') | v accept_input() ---> nat pipe <--- 'g' also goes to nat pipe | v pcon reactivates (step 3): | v readline reads cyg pipe, but "it" is gone -- it was moved to the nat pipe. Later keystrokes that went correctly to the cyg pipe appear before the stolen ones. Result: "git" arrives at bash as "tgi" or similar scramble. The fix ------- Remove the transfer_input() call entirely. The original commit's comment says it was needed "when cygwin-app which is started from non-cygwin app is terminated if pseudo console is disabled." That scenario is already handled by setpgid_aux() in the slave process, which performs the transfer at the correct moment: the process-group change when the Cygwin child exits. The per-keystroke transfer in master::write() was redundant for that use case and catastrophic during oscillation. This single change addresses the majority of the reordering (from "virtually every character scrambled" to roughly one stray character per five iterations in testing). Subsequent commits in this series address the remaining code paths that can still displace readline data during oscillation. Regression note: the removed code was added in response to a ghost-typing report where vim's ANSI escape responses appeared at the bash prompt after exiting vim with MSYS=disable_pcon: https://inbox.sourceware.org/cygwin-patches/nycvar.QRO.7.76.6.2112092345060.90@tvgsbejvaqbjf.bet/ Since setpgid_aux() still handles the pipe transfer at process-group boundaries, the disable_pcon scenario is covered. Tested with MSYS=disable_pcon and Git for Windows' AutoHotKey-based UI tests without regressions. Addresses: https://github.com/git-for-windows/git/issues/5632 Fixes: acc44e09d1d0 ("Cygwin: pty: Add missing input transfer when switch_to_pcon_in state.") Assisted-by: Claude Opus 4.6 Signed-off-by: Johannes Schindelin --- winsup/cygwin/fhandler/pty.cc | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/winsup/cygwin/fhandler/pty.cc b/winsup/cygwin/fhandler/pty.cc index 9c3ef2192a..6cc46fcb27 100644 --- a/winsup/cygwin/fhandler/pty.cc +++ b/winsup/cygwin/fhandler/pty.cc @@ -2268,17 +2268,6 @@ fhandler_pty_master::write (const void *ptr, size_t len) or cygwin process is foreground even though pseudo console is activated. */ - /* This input transfer is needed when cygwin-app which is started from - non-cygwin app is terminated if pseudo console is disabled. */ - if (to_be_read_from_nat_pipe () && !get_ttyp ()->pcon_activated - && get_ttyp ()->pty_input_state == tty::to_cyg) - { - acquire_attach_mutex (mutex_timeout); - fhandler_pty_slave::transfer_input (tty::to_nat, from_master, - get_ttyp (), input_available_event); - release_attach_mutex (); - } - line_edit_status status = line_edit (p, len, ti, &ret); ReleaseMutex (input_mutex); From d97d93629b3a7af0c2c2462b01dad1472f5f63f5 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 20 Feb 2026 21:41:27 +0100 Subject: [PATCH 04/15] amend! ci: add an AutoHotKey-based integration test ci: add an AutoHotKey-based integration test The issue reported in https://github.com/microsoft/git/issues/730 was fixed, but due to missing tests for the issue a regression slipped in within mere weeks. Let's add an integration test that will (hopefully) prevent this issue from regressing again. This integration test is implement as an AutoHotKey script. It might look unnatural to use a script language designed to implement global keyboard shortcuts, but it is a quite powerful approach. While there are miles between the ease of developing AutoHotKey scripts and developing, say, Playwright tests, there is a decent integration into VS Code (including single-step debugging), and AutoHotKey's own development and community are quite vibrant and friendly. I had looked at alternatives to AutoHotKey, such as WinAppDriver, SikuliX, nut.js and AutoIt, in particular searching for a solution that would have a powerful recording feature similar to Playwright, but did not find any that is 1) mature, 2) well-maintained, 3) open source and 4) would be easy to integrate into a GitHub workflow. In the end, AutoHotKey appeared my clearest preference. So how is the test implemented? It lives in `ui-test/` and requires AutoHotKey v2 as well as Windows Terminal (the Legacy Prompt would not reproduce the problem). It then follows the reproducer I gave to the Cygwin team: 1. initialize a Git repository 2. install a `pre-commit` hook 3. this hook shall spawn a non-Cygwin/MSYS2 process in the background 4. that background process shall print to the console after Git exits 5. open a Command Prompt in Windows Terminal 6. run `git commit` 7. wait until the background process is done printing 8. press the Cursor Up key 9. observe that the Command Prompt does not react (in the test, it _does_ expect a reaction: the previous command in the command history should be shown, i.e. `git commit`) In my reproducer, I then also suggested to press the Enter key and to observe that now the "More ?" prompt is shown, but no input is accepted, until Ctrl+Z is pressed. Naturally, the test should not expect _that_ ;-) There were a couple of complications I needed to face when developing this test: - I did not find any easy macro recorder for AutoHotKey that I liked. It would not have helped much, anyway, because intentions are hard to record. - Before I realized that there is excellent AutoHotKey support in VS Code via the AutoHotKey++ and AutoHotKey Debug extensions, I struggled quite a bit to get the syntax right. - Windows Terminal does not use classical Win32 controls that AutoHotKey knows well. To capture the terminal text, we use Windows Terminal's exportBuffer action, which writes the entire scrollback to a file on a keybinding (Ctrl+Shift+F12). This requires running WT in portable mode with a settings.json that defines the action, which the setup script takes care of. - Despite my expectations, `ExitApp` would not actually exit AutoHotKey before the spawned process exits and/or the associated window is closed. For good measure, run this test both on windows-2022 (corresponding to Windows 10) and on windows-2025 (corresponding to Windows 11). Co-authored-by: Eu-Pin Tien Assisted-by: Claude Opus 4.6 Signed-off-by: Johannes Schindelin --- ui-tests/background-hook.ahk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-tests/background-hook.ahk b/ui-tests/background-hook.ahk index af7c27d313..76d04708d2 100755 --- a/ui-tests/background-hook.ahk +++ b/ui-tests/background-hook.ahk @@ -43,7 +43,7 @@ WaitForRegExInWindowsTerminal('`n49$', 'Timed out waiting for commit to finish', ; Verify that CursorUp shows the previous command Send('{Up}') Sleep 150 -Text := CaptureTextFromWindowsTerminal() +Text := CaptureBufferFromWindowsTerminal() if not RegExMatch(Text, 'git commit --allow-empty -m zOMG *$') ExitWithError 'Cursor Up did not work: ' Text Info('Match!') From c9c17c2959f49410ce930442476670cdf0001351 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 26 Feb 2026 14:10:56 +0100 Subject: [PATCH 05/15] Cygwin: pty: Remove pcon_start readahead flush that displaces readline data After the previous commit addressed the worst data-stealing code path, roughly one in five test iterations still shows a stray character. Another transfer code path in master::write() is responsible. When a native process becomes the PTY foreground, the pseudo console must be initialized. During this "pcon_start" phase, master::write() enters a polling loop that feeds keystrokes to the nascent pseudo console. When the loop completes (pcon_start becomes false), there was a block of code that: 1. Flushed the master's readahead buffer via accept_input() 2. Called transfer_input(to_nat) to move all cyg pipe data to the nat pipe The intent was to preserve typeahead: characters typed during pcon initialization should eventually reach the native process. But during pseudo console oscillation (the rapid pcon on/off cycles described in the previous commit), this fires on every pcon re-initialization -- and the "typeahead" it transfers includes readline's entire editing buffer. Worse, if the terminal emulator was mid-way through sending a rapid editing sequence like "XY" (type two characters, then erase them with backspace), the readahead flush fires after buffering "X" but before the backspaces arrive. The orphaned "X" gets pushed to the cyg pipe via accept_input(), where readline sees it as genuine input -- producing a stray character that the user never intended. Remove the accept_input() and transfer_input() calls entirely. Keep `pcon_start_pid = 0`, which marks the end of initialization. The readahead data belongs to the Cygwin process (bash is in canonical mode during command entry) and will be delivered naturally when line_edit() encounters a newline or when readline switches the terminal to raw mode after the foreground command exits. The setpgid_aux() code path in the slave process still handles the steady-state cyg-to-nat transfer at process-group boundaries. Combined with the previous commit, Git for Windows' AutoHotKey-based UI tests now pass cleanly in the vast majority of iterations. Regression note: the removed code was motivated by a 2020 bug report about lost typeahead with native processes: https://inbox.sourceware.org/cygwin/7e3d947e-b178-30a3-589f-b48e6003fbb3@googlemail.com/ Since the pcon_start window is brief (a few milliseconds) and setpgid_aux() handles the steady-state transfer, the risk of typeahead loss is low. Addresses: https://github.com/git-for-windows/git/issues/5632 Fixes: 10d083c745dd ("Cygwin: pty: Inherit typeahead data between two input pipes.") Fixes: f20641789427 ("Cygwin: pty: Reduce unecessary input transfer.") Assisted-by: Claude Opus 4.6 Signed-off-by: Johannes Schindelin --- winsup/cygwin/fhandler/pty.cc | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/winsup/cygwin/fhandler/pty.cc b/winsup/cygwin/fhandler/pty.cc index 6cc46fcb27..767e26f75d 100644 --- a/winsup/cygwin/fhandler/pty.cc +++ b/winsup/cygwin/fhandler/pty.cc @@ -2204,21 +2204,6 @@ fhandler_pty_master::write (const void *ptr, size_t len) if (!get_ttyp ()->pcon_start) { /* Pseudo console initialization has been done in above code. */ pinfo pp (get_ttyp ()->pcon_start_pid); - if (get_ttyp ()->switch_to_nat_pipe - && get_ttyp ()->pty_input_state_eq (tty::to_cyg)) - { - /* This accept_input() call is needed in order to transfer input - which is not accepted yet to non-cygwin pipe. */ - WaitForSingleObject (input_mutex, mutex_timeout); - if (get_readahead_valid ()) - accept_input (); - acquire_attach_mutex (mutex_timeout); - fhandler_pty_slave::transfer_input (tty::to_nat, from_master, - get_ttyp (), - input_available_event); - release_attach_mutex (); - ReleaseMutex (input_mutex); - } get_ttyp ()->pcon_start_pid = 0; } From c10cd37265fc6f39c8e9404edc65bbdf9a67b5b9 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 20 Feb 2026 22:01:18 +0100 Subject: [PATCH 06/15] fixup! Start implementing UI-based tests by adding an AutoHotKey library Now that all callers have been switched to CaptureBufferFromWindowsTerminal(), remove the old CaptureTextFromWindowsTerminal() function entirely. It relied on emulating mouse movements to drag-select the visible portion of the terminal and copy it via right-click, which was fragile and required hard-coded pixel offsets for the title bar height, scroll bar width, and padding. None of that complexity is needed anymore. Assisted-by: Claude Opus 4.6 Signed-off-by: Johannes Schindelin --- ui-tests/ui-test-library.ahk | 40 +----------------------------------- 1 file changed, 1 insertion(+), 39 deletions(-) diff --git a/ui-tests/ui-test-library.ahk b/ui-tests/ui-test-library.ahk index d62ea2eff4..d1a240e331 100644 --- a/ui-tests/ui-test-library.ahk +++ b/ui-tests/ui-test-library.ahk @@ -67,47 +67,9 @@ RunWaitOne(command) { return Result } -; This function is quite the hack. It assumes that the Windows Terminal is the active window, -; then drags the mouse diagonally across the window to select all text and then copies it. -; -; This is fragile! If any other window becomes active, or if the mouse is moved, -; the function will not work as intended. -; -; An alternative would be to use `ControlSend`, e.g. -; `ControlSend '+^a', 'Windows.UI.Input.InputSite.WindowClass1', 'ahk_id ' . hwnd -; This _kinda_ works, the text is selected (all text, in fact), but the PowerShell itself -; _also_ processes the keyboard events and therefore they leave ugly and unintended -; `^Ac` characters in the prompt. So that alternative is not really usable. -CaptureTextFromWindowsTerminal(winTitle := '') { - if winTitle != '' - WinActivate winTitle - ControlGetPos &cx, &cy, &cw, &ch, 'Windows.UI.Composition.DesktopWindowContentBridge1', "A" - titleBarHeight := 54 - scrollBarWidth := 28 - pad := 8 - - SavedClipboard := ClipboardAll - A_Clipboard := '' - SendMode('Event') - if winTitle != '' - WinActivate winTitle - MouseMove cx + pad, cy + titleBarHeight + pad - if winTitle != '' - WinActivate winTitle - MouseClickDrag 'Left', , , cx + cw - scrollBarWidth, cy + ch - pad, , '' - if winTitle != '' - WinActivate winTitle - MouseClick 'Right' - ClipWait() - Result := A_Clipboard - Clipboard := SavedClipboard - return Result -} - ; Capture the Windows Terminal buffer via the exportBuffer action (Ctrl+Shift+F12). ; Requires a portable WT with settings.json that maps Ctrl+Shift+F12 to exportBuffer -; writing to /wt-buffer-export.txt. Unlike CaptureTextFromWindowsTerminal(), -; this does not depend on mouse position or window focus quirks. +; writing to /wt-buffer-export.txt. CaptureBufferFromWindowsTerminal(winTitle := '') { static exportFile := A_ScriptDir . '\wt-buffer-export.txt' if FileExist(exportFile) From 86a76c19438e4b29aeddbd8f661d02376c767ad6 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 26 Feb 2026 14:13:44 +0100 Subject: [PATCH 07/15] Cygwin: pty: Prevent premature pseudo console teardown that amplifies oscillation The two preceding commits removed transfer code paths that stole readline's data during pseudo console oscillation. This commit addresses the oscillation itself: a guard function that tears down active pseudo console sessions prematurely, causing more frequent oscillation cycles and thus more opportunities for the remaining (less harmful) timing issues to manifest. The function reset_switch_to_nat_pipe() runs from bg_check() in the slave process. Its purpose is to clean up the nat pipe state when no native process is using the pseudo console anymore. Its guard logic was: if (!nat_pipe_owner_self(pid) && process_alive(pid)) return; /* someone else owns it, don't reset */ /* fall through to destructive cleanup: clear pty_input_state, nat_pipe_owner_pid, switch_to_nat_pipe, pcon_activated */ The nat_pipe_owner_pid is set to bash's own PID during setup_for_non_cygwin_app() (because bash is the process that calls exec() to launch the native program). When bg_check() runs and calls reset_switch_to_nat_pipe(), nat_pipe_owner_self() returns true -- the first condition becomes false, the && short-circuits, and the function falls through to the destructive cleanup. It clears pcon_activated, switch_to_nat_pipe, pty_input_state, and nat_pipe_owner_pid -- even though the native process is still alive and actively using the pseudo console. This forced every subsequent code path to re-initialize the pseudo console from scratch, creating exactly the rapid oscillation described in the earlier commits. Restructure the guard into two separate checks: if (process_alive(pid)) { if (!nat_pipe_owner_self(pid)) return; /* someone else owns it */ if (pcon_activated || switch_to_nat_pipe) return; /* we own it, but session is still active */ } /* fall through: owner died or session ended */ When a different process owns the nat pipe, the behavior is unchanged. When bash itself is the owner, the function now also returns early if pcon_activated or switch_to_nat_pipe is still set. Both flags are checked because during pseudo console handovers between parent and child native processes, pcon_activated is briefly false while switch_to_nat_pipe remains true. The cleanup still runs when it should: when the owner process has exited or when both flags indicate the session has truly ended. Regression note: this change is strictly more conservative -- it adds conditions that prevent cleanup, never removes them. Every scenario where the original code returned early still returns early. Addresses: https://github.com/git-for-windows/git/issues/5632 Fixes: 919dea66d3ca ("Cygwin: pty: Fix a race issue in startup of pseudo console.") Assisted-by: Claude Opus 4.6 Signed-off-by: Johannes Schindelin --- winsup/cygwin/fhandler/pty.cc | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/winsup/cygwin/fhandler/pty.cc b/winsup/cygwin/fhandler/pty.cc index 767e26f75d..84e4f6f86d 100644 --- a/winsup/cygwin/fhandler/pty.cc +++ b/winsup/cygwin/fhandler/pty.cc @@ -1179,12 +1179,23 @@ fhandler_pty_slave::reset_switch_to_nat_pipe (void) DWORD wait_ret = WaitForSingleObject (pipe_sw_mutex, mutex_timeout); if (wait_ret == WAIT_TIMEOUT) return; - if (!nat_pipe_owner_self (get_ttyp ()->nat_pipe_owner_pid) - && process_alive (get_ttyp ()->nat_pipe_owner_pid)) + if (process_alive (get_ttyp ()->nat_pipe_owner_pid)) { - /* There is a process which owns nat pipe. */ - ReleaseMutex (pipe_sw_mutex); - return; + if (!nat_pipe_owner_self (get_ttyp ()->nat_pipe_owner_pid)) + { + /* There is a process which owns nat pipe. */ + ReleaseMutex (pipe_sw_mutex); + return; + } + /* We are the nat pipe owner. Don't reset while a native process + is still using the nat pipe -- check both pcon_activated and + switch_to_nat_pipe since the latter stays true during pcon + handovers when pcon_activated is briefly false. */ + if (get_ttyp ()->pcon_activated || get_ttyp ()->switch_to_nat_pipe) + { + ReleaseMutex (pipe_sw_mutex); + return; + } } /* Clean up nat pipe state */ get_ttyp ()->pty_input_state = tty::to_cyg; From cf891a0a233e5e7eeabadd61f4500aecadff92f3 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 20 Feb 2026 12:17:50 +0100 Subject: [PATCH 08/15] Add AGENTS.md with comprehensive project context for AI agents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This file documents the layered fork structure of this repository (Cygwin → MSYS2 → Git for Windows), the merging-rebase strategy that keeps the main branch fast-forwarding, the build system and its bootstrap chicken-and-egg nature (msys-2.0.dll is the POSIX emulation layer that its own GCC depends on), the CI pipeline, key directories and files, development guidelines, and external resources. The intent is to give AI coding agents enough context to work competently on this codebase without hallucinating about its structure or purpose. Assisted-by: Claude Opus 4.6 Signed-off-by: Johannes Schindelin --- AGENTS.md | 298 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 298 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..177fb5ef44 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,298 @@ +# Guidelines for AI Agents Working on This Codebase + +## Project Overview + +This repository is the **Git for Windows fork** of the **MSYS2 runtime**, which is itself a fork of the **Cygwin runtime**. The runtime provides a POSIX emulation layer on Windows, producing `msys-2.0.dll` (analogous to Cygwin's `cygwin1.dll`). It is the foundational component that allows Unix-style programs (bash, coreutils, etc.) to run on Windows within the MSYS2 and Git for Windows ecosystems. + +### The Layered Fork Structure + +There are three layers of this project, each building on the one below: + +1. **Cygwin** (`git://sourceware.org/git/newlib-cygwin.git`, releases at https://cygwin.com): The upstream project. Cygwin is a POSIX-compatible environment for Windows consisting of a DLL (`cygwin1.dll`) that provides substantial POSIX API functionality, plus a collection of GNU and Open Source tools. The Cygwin project releases versioned tags (e.g., `cygwin-3.6.6`) from the `cygwin/cygwin` GitHub mirror. + +2. **MSYS2** (`https://github.com/msys2/msys2-runtime`): The MSYS2 project rebases its own patches on top of each Cygwin release. MSYS2 maintains branches named `msys2-X.Y.Z` (e.g., `msys2-3.6.6`) where the Cygwin code is the base and MSYS2-specific patches are applied on top. These patches implement features like POSIX-to-Windows path conversion (`msys2_path_conv.cc`), the `MSYS` environment variable for controlling runtime behavior, pseudo-console support toggling, and adaptations needed for MSYS2's focus on building native Windows software (as opposed to Cygwin's focus on running Unix software on Windows as-is). + +3. **Git for Windows** (`https://github.com/git-for-windows/msys2-runtime`, this repository): Git for Windows maintains a "merging rebase" on top of the MSYS2 patches. The `main` branch uses a special strategy where it always fast-forwards. Each rebase to a new upstream version starts with a "fake merge" commit (message: `Start the merging-rebase to cygwin-X.Y.Z`) that merges previous `main` using the `-s ours` strategy. This ensures the branch always fast-forwards despite being rebased. Git for Windows' own patches (on top of MSYS2's patches) address issues specific to Git's usage patterns, such as Ctrl+C signal handling, SSH hang fixes, and console output correctness. + +### Key Relationships + +- **Cygwin → MSYS2**: MSYS2 rebases onto each Cygwin release. When Cygwin releases version X.Y.Z, an `msys2-X.Y.Z` branch is created with MSYS2 patches rebased on top. +- **MSYS2 → Git for Windows**: Git for Windows performs a merging rebase that first merges in the MSYS2 patches, then rebases its own patches on top. +- The `main` branch in this repository (git-for-windows/msys2-runtime) is the Git for Windows branch, not Cygwin's or MSYS2's. + +## Repository Structure + +### Key Directories + +- **`winsup/cygwin/`**: The core of the Cygwin/MSYS2 runtime. This is where `msys-2.0.dll` (the POSIX emulation DLL) is built. Most development work happens here. Key files include: + - `dcrt0.cc`: Runtime initialization + - `spawn.cc`: Process spawning + - `path.cc`: Path handling + - `fork.cc`: fork() implementation + - `exceptions.cc`: Signal handling + - `msys2_path_conv.cc` / `msys2_path_conv.h`: MSYS2-specific POSIX-to-Windows path conversion (CC0-licensed) + - `environ.cc`: Environment variable handling, including the `MSYS` environment variable + - `fhandler/`: File handler implementations for various device types + - `local_includes/`: Internal headers + - `release/`: Version history files (one per Cygwin release version) +- **`winsup/utils/`**: Cygwin/MSYS2 utility programs (mount, cygpath, etc.) +- **`newlib/`**: The C library (newlib) used by the runtime +- **`ui-tests/`**: AutoHotKey-based integration tests that test the runtime in real terminal scenarios +- **`.github/workflows/`**: CI configuration + +## Build System + +### The Chicken-and-Egg Problem + +The MSYS2 runtime (`msys-2.0.dll`) is itself the POSIX emulation layer that the MSYS2 toolchain (GCC, binutils, etc.) depends on. The MSYS2 environment's own GCC links against `msys-2.0.dll` to provide POSIX semantics. This means you need a working MSYS2 runtime to compile a new MSYS2 runtime — a classic bootstrap problem. + +In practice, this is resolved by using an existing MSYS2 installation to build the new version. The CI workflow (`.github/workflows/build.yaml`) installs MSYS2 via the `msys2/setup-msys2` action, then builds the new runtime within that environment. + +### Build Dependencies + +Building requires MSYS2 packages: `msys2-devel`, `base-devel`, `autotools`, `cocom`, `gcc`, `gettext-devel`, `libiconv-devel`, `make`, `mingw-w64-cross-crt`, `mingw-w64-cross-gcc`, `mingw-w64-cross-zlib`, `perl`, `zlib-devel`. These are all **msys** packages (they link against `msys-2.0.dll`), not native MinGW packages. + +### Building in the Git for Windows SDK + +The Git for Windows SDK provides a complete MSYS2 environment with all necessary build dependencies pre-installed. The source tree is typically located at `/usr/src/MSYS2-packages/msys2-runtime/src/msys2-runtime` inside the SDK. + +**Critical: PATH ordering.** The build must use the MSYS2 toolchain, not any MinGW toolchain that might be on the PATH. Before building, ensure: + +```bash +export PATH=/usr/bin:/mingw64/bin:/mingw32/bin:$PATH +``` + +If MinGW's GCC is found first, the build will fail because MinGW tools do not link against `msys-2.0.dll` and cannot produce the runtime DLL. + +### Build Commands + +```bash +# Generate autotools files +(cd winsup && ./autogen.sh) + +# Configure (the --with-msys2-runtime-commit flag embeds the commit hash) +./configure --disable-dependency-tracking --with-msys2-runtime-commit="$(git rev-parse HEAD)" + +# Build +make -j8 +``` + +For quick rebuilds of just the DLL during development: +```bash +# Rebuild only msys-2.0.dll +make -C ../build-x86_64-pc-msys/x*/winsup/cygwin -j15 new-msys-2.0.dll +``` + +The build output is `new-msys-2.0.dll` in the build directory. This is a staging name to avoid overwriting the running DLL. + +### Testing a Locally-Built DLL + +You cannot replace the SDK's own `msys-2.0.dll` while running inside the SDK — the DLL is loaded by every MSYS2 process including your shell. Instead, copy the built DLL into a separate installation such as a Portable Git: + +```bash +cp new-msys-2.0.dll /path/to/PortableGit/usr/bin/msys-2.0.dll +``` + +Then run tests using that Portable Git's mintty/bash. Back up the original DLL first. + +The `build-and-copy.sh` helper script in the repository root can reconfigure, rebuild, and copy `msys-2.0.dll` to a target location. + +### Internal API Constraints + +Code inside `msys-2.0.dll` cannot use the full C runtime or C++ standard library freely. Key limitations: + +- **`__small_sprintf`** is used instead of `sprintf`. It does NOT support `%lld` (64-bit integers) or floating-point format specifiers. For 64-bit values, split into high/low 32-bit halves and print as two `%u` values. +- **Memory allocation** in low-level code (e.g., DLL initialization, atexit handlers) should use `HeapAlloc(GetProcessHeap(), ...)` to avoid circular dependencies with the Cygwin malloc. + +### CI Pipeline + +The CI (`.github/workflows/build.yaml`) does the following: +1. **Build**: Compiles the runtime on `windows-latest` using MSYS2 +2. **Minimal SDK artifact**: Creates a minimal Git for Windows SDK with the just-built runtime, used for testing Git itself +3. **Test minimal SDK**: Runs Git's test suite against the new runtime +4. **UI tests**: AutoHotKey-based integration tests for terminal behavior (Ctrl+C interrupts, SSH operations, etc.) +5. **MSYS2 tests**: Runs the MSYS2 project's own test suite across multiple environments and compilers + +## Git Branch and Rebase Workflow + +### The Merging Rebase Strategy + +Git for Windows uses a "merging rebase" to maintain a fast-forwarding `main` branch. The key insight is a "fake merge" commit that: + +1. Starts from the new upstream commit (Cygwin tag) +2. Merges in the previous `main` using `-s ours` (takes NO changes from previous main, only the tree from upstream) +3. This makes `main` a parent of the new commit, so the result is a fast-forward from previous `main` +4. Patches are then rebased on top of this fake merge + +The commit message follows a strict format: `Start the merging-rebase to cygwin-X.Y.Z`. This is machine-parseable — `git rev-parse 'main^{/^Start.the.merging-rebase}'` finds the most recent such commit. + +### History of Merging Rebases + +The repository has been continuously rebased through Cygwin versions from 3.3.x through the current 3.6.6. Each rebase is visible as a `Start the merging-rebase to cygwin-X.Y.Z` commit on `main`. + +### Key Branches + +- `main`: Git for Windows' branch (fast-forwarding, contains merging-rebase commits) +- `cygwin-X_Y-branch` (e.g., `cygwin-3_6-branch`): Tracking branches for upstream Cygwin +- `cygwin/main`: Upstream Cygwin's main branch +- Various feature branches for specific fixes (e.g., `fix-ctrl+c-again`, `fix-ssh-hangs-reloaded`) + +### Key Remotes + +- `cygwin`: The upstream Cygwin repository (`git://sourceware.org/git/newlib-cygwin.git`) +- `msys2`: The MSYS2 fork (`https://github.com/msys2/msys2-runtime`) +- `git-for-windows`: This repository (`https://github.com/git-for-windows/msys2-runtime`) +- `dscho`: Johannes Schindelin's fork (primary maintainer) + +## Development Guidelines + +### Language and Style + +The runtime is written in **C++** (with some C). The code uses Cygwin's existing coding conventions. When modifying files under `winsup/cygwin/`: +- Follow the existing indentation and brace style of each file +- Cygwin code uses 8-space tabs in many files +- MSYS2-specific additions (like `msys2_path_conv.cc`) may use different conventions + +### Making Changes + +Most changes for Git for Windows purposes are in `winsup/cygwin/`. Common areas of modification: +- Signal handling (`exceptions.cc`, `sigproc.cc`) +- Process spawning (`spawn.cc`) +- PTY/console handling (`fhandler/` directory, `termios.cc`) +- Path conversion (`msys2_path_conv.cc`, `path.cc`) +- Environment handling (`environ.cc`) + +### Testing + +- The CI builds the runtime and runs Git's entire test suite against it +- UI tests in `ui-tests/` test real terminal scenarios using AutoHotKey +- MSYS2's own test suite is run across multiple compiler/environment combinations +- For local testing, build the DLL and copy it to replace `msys-2.0.dll` in an MSYS2 installation + +### Commit Discipline + +- One logical change per commit +- Commit messages should explain context, intent, and justification in prose (not bullet points) +- For the rebase workflow, commit messages follow specific patterns (e.g., `Start the merging-rebase to ...`) that tooling depends on — do not alter these patterns + +## PTY Architecture — Pipes, State Machine, and Input Routing + +This section documents the internal architecture of the pseudo-terminal (PTY) implementation in `winsup/cygwin/fhandler/pty.cc`. Understanding this is essential for debugging any issue involving terminal input/output, keystroke handling, signal delivery, and process foreground/background transitions. + +### Background: Why This Matters + +The pseudo console support in the Cygwin runtime is one of the most intricate subsystems in this codebase. It bridges two fundamentally different models of terminal I/O — POSIX and Win32 console — across multiple processes that share state through shared memory. The implementation is ambitious and evolving; the complexity of the interactions between pipe switching, pseudo console lifecycle, cross-process mutexes, and foreground process detection means that changes in one area can have subtle, hard-to-diagnose effects elsewhere. Historically, bug fixes in this area have occasionally introduced new regressions, which is simply a reflection of how difficult the problem space is. Any AI agent working on PTY-related issues should take the time to understand the full picture before proposing changes, and should be especially careful about mutex acquisition order, state transitions that span process boundaries, and the distinction between the two pipe pairs described below. + +### The Two Pipe Pairs + +Each PTY has **two independent pipe pairs** for input, serving different consumers: + +1. **Cygwin (cyg) pipe**: `to_slave` / `from_master` + - Used when a **Cygwin/MSYS2 process** (e.g., bash) is in the foreground. + - Input goes through `line_edit()` (in `termios.cc`) which handles line discipline (echo, canonical mode, special characters) before being written via `accept_input()`. + - The slave reads from `from_master` (aliased as `get_handle()` on the slave side). + +2. **Native (nat) pipe**: `to_slave_nat` / `from_master_nat` + - Used when a **non-Cygwin (native Windows) process** (e.g., `powershell.exe`, `cmd.exe`, a MinGW program) is in the foreground. + - When the pseudo console (pcon) is active, `CreatePseudoConsole()` wraps this pipe pair. The Windows `conhost.exe` process reads from `from_master_nat` and provides console input semantics to the native app. + - The master writes directly to `to_slave_nat` via `WriteFile()`, bypassing `line_edit()`. + +For **output**, there is a corresponding pair (`to_master` / `to_master_nat`) plus a forwarding thread (`master_fwd_thread`) that copies output from the nat pipe's slave side (`from_slave_nat`) to the cyg pipe's master side (`to_master`), so the terminal emulator (mintty) always reads from one place. + +### The Pseudo Console (pcon) + +When `MSYS=disable_pcon` is NOT set (the default), the runtime uses Windows' `CreatePseudoConsole()` API to give native console applications a real console to talk to. The pseudo console is created on demand when a non-Cygwin process becomes the foreground process, and torn down when it exits. This is what allows programs like `cmd.exe`, `powershell.exe`, or any MinGW-built program to work correctly inside a mintty terminal, which has no native Win32 console of its own. + +The pcon lifecycle is managed across process boundaries: the slave process (running the non-Cygwin app) and the master process (the terminal emulator) both participate. This cross-process coordination is the source of much of the complexity. + +Key state fields in the `tty` structure (shared memory, in `tty.h`): + +- **`pcon_activated`** (`bool`): True when a pseudo console is currently active. +- **`pcon_start`** (`bool`): True during pseudo console initialization. +- **`pcon_start_pid`** (`pid_t`): PID of the process that initiated pcon setup. + +### The Input State Machine + +The field **`pty_input_state`** (type `xfer_dir`, in `tty.h:137`) tracks which pipe pair currently "owns" the input. It has two values: + +- **`to_cyg`**: Input is flowing to the Cygwin pipe. The master's `write()` uses the `line_edit()` → `accept_input()` path, which writes to `to_slave` (cyg pipe). +- **`to_nat`**: Input is flowing to the native pipe. The master's `write()` writes directly to `to_slave_nat` (nat pipe), or through the pseudo console. + +The state transitions happen via **`transfer_input()`** (pty.cc, around line 3905), which: +1. Reads all pending data from the "source" pipe (the one being abandoned). +2. Writes that data into the "destination" pipe (the one being switched to). +3. Sets `pty_input_state` to the new direction. + +This ensures data already buffered in one pipe is not lost when switching. **Any code that changes `pty_input_state` without calling `transfer_input()` risks losing or reordering data.** This invariant is critical and has been the root cause of past bugs. + +### Related State Fields + +- **`switch_to_nat_pipe`** (`bool`): Set to true when a non-Cygwin process is detected in the foreground. This is a prerequisite for `to_be_read_from_nat_pipe()` returning true. +- **`nat_pipe_owner_pid`** (`DWORD`): PID of the process that "owns" the nat pipe setup. Used to detect when the owner has exited (for cleanup). + +### The `to_be_read_from_nat_pipe()` Function + +This function (pty.cc, around line 1288) determines whether the current foreground process is a native (non-Cygwin) app. It checks: + +1. `switch_to_nat_pipe` must be true. +2. A named event `TTY_SLAVE_READING` must NOT exist (its existence means a Cygwin process is actively reading from the slave, indicating a Cygwin foreground). +3. `nat_fg(pgid)` returns true (the foreground process group contains a native process). + +**This function reads shared state without holding any mutex.** Its return value can therefore change between consecutive calls within the same function, which is an important consideration for callers that make multiple decisions based on the foreground state. + +### Mutexes and Synchronization + +Two cross-process named mutexes protect different aspects of the PTY state. Understanding which mutex protects what — and the fact that they are independent — is essential for diagnosing race conditions. + +- **`input_mutex`**: Protects the input data path. Held by `master::write()` while routing input to a pipe, by `transfer_input()` while moving data between pipes, and by `line_edit()` / `accept_input()`. +- **`pipe_sw_mutex`**: Protects pipe switching state — creation/destruction of the pseudo console, changes to `switch_to_nat_pipe`, `nat_pipe_owner_pid`. This is a DIFFERENT mutex from `input_mutex`. + +Because these are separate mutexes, it is possible for one process to modify the pipe switching state (under `pipe_sw_mutex`) while another process is in the middle of writing input (under `input_mutex`). Any code that modifies `pty_input_state` or `pcon_activated` must carefully consider whether it also needs `input_mutex` to avoid creating a window where the master's write path makes inconsistent decisions. + +Additionally, because these are **cross-process** named mutexes, they are shared via the kernel between the master (terminal emulator) and slave (bash and its children) processes. Operations that look local in the source code actually have system-wide synchronization effects. + +### The `master::write()` Input Routing (pty.cc, around line 2240) + +When the terminal emulator (mintty) sends a keystroke, it calls `master::write()`. After acquiring `input_mutex`, the function decides which path to take: + +1. **Path 1 — pcon+nat** (line ~2245): If `to_be_read_from_nat_pipe()` AND `pcon_activated` AND `pty_input_state == to_nat` → write directly to `to_slave_nat`. This is the fast path for native apps with pcon. + +2. **Path 2 — non-pcon transfer** (line ~2288): If `to_be_read_from_nat_pipe()` AND NOT `pcon_activated` AND `pty_input_state == to_cyg` → call `transfer_input(to_nat)` to move cyg pipe data to nat pipe, then fall through to line_edit. + +3. **Path 3 — line_edit** (line ~2300): The default/fallthrough path. Calls `line_edit()` which processes the input through terminal line discipline and then calls `accept_input()`, which writes to either the cyg or nat pipe based on the current `pty_input_state`. + +The conditions checked at each step involve multiple shared-memory fields (`to_be_read_from_nat_pipe()`, `pcon_activated`, `pty_input_state`). If any of these fields changes between consecutive calls to `master::write()` — or worse, between the check and the write within a single call — input can end up in the wrong pipe. + +### Key Functions for State Transitions + +- **`setup_for_non_cygwin_app()`** (~line 4150): Called when a non-Cygwin process becomes foreground. Sets up the pseudo console and switches input to nat pipe. +- **`cleanup_for_non_cygwin_app()`** (~line 4184): Called when the non-Cygwin process exits. Tears down pcon, transfers input back to cyg pipe. +- **`reset_switch_to_nat_pipe()`** (~line 1091): Cleanup function called from various slave-side operations (e.g., `bg_check()`, `setpgid_aux()`). Detects when the nat pipe owner has exited and resets state. This function is particularly subtle because it runs in the slave process and modifies shared state that the master relies on. +- **`mask_switch_to_nat_pipe()`** (~line 1249): Temporarily masks/unmasks the nat pipe switching. Used when a Cygwin process starts/stops reading from the slave. +- **`setpgid_aux()`** (~line 4214): Called when the foreground process group changes. May trigger pipe switching. + +### Debugging Tips + +When investigating PTY-related bugs, keep these patterns in mind: + +- **Data in two pipes**: If characters are lost, duplicated, or reordered, check whether data ended up split across the cyg and nat pipes due to a state transition during input. +- **Cross-process state changes**: The master and slave processes share state through the `tty` structure in shared memory. A state change in the slave (e.g., `reset_switch_to_nat_pipe()`) is immediately visible to the master, without any notification. Look for races where the master reads state, acts on it, but the state changed between the read and the action. +- **Mutex coverage gaps**: Check whether every modification of `pty_input_state`, `pcon_activated`, and `switch_to_nat_pipe` is protected by the appropriate mutex. The existence of two separate mutexes (`input_mutex` and `pipe_sw_mutex`) means that holding one does not protect against changes guarded by the other. +- **`transfer_input()` must accompany state changes**: Whenever `pty_input_state` is changed, any data buffered in the old pipe must be transferred to the new one. Forgetting this step causes data loss or reordering. +- **Tracing**: For timing-sensitive bugs, in-process tracing with lock-free per-thread buffers (using Windows TLS and `QueryPerformanceCounter`) is effective. Avoid file I/O during reproduction — accumulate in memory and dump at process exit. See the `ui-tests/` directory for AutoHotKey-based reproducers that can drive mintty programmatically. + +## Packaging + +The MSYS2 runtime is packaged as an **msys** package (`msys2-runtime`) using `makepkg` with a `PKGBUILD` recipe in the `msys2/MSYS2-packages` repository. The package definition lives at `msys2-runtime/PKGBUILD` in that repository. + +## External Resources + +- **Cygwin project**: https://cygwin.com — upstream source, FAQ, user's guide +- **Cygwin source**: https://github.com/cygwin/cygwin (mirror of `sourceware.org/git/newlib-cygwin.git`) +- **Cygwin announcements**: https://inbox.sourceware.org/cygwin-announce — release announcements +- **MSYS2 project**: https://www.msys2.org — documentation, package management +- **MSYS2 runtime source**: https://github.com/msys2/msys2-runtime +- **MSYS2 packages**: https://github.com/msys2/MSYS2-packages — package recipes including `msys2-runtime` +- **Git for Windows**: https://gitforwindows.org +- **Git for Windows runtime**: https://github.com/git-for-windows/msys2-runtime (this repository) +- **MSYS2 environments**: https://www.msys2.org/docs/environments/ — explains MSYS vs UCRT64 vs CLANG64 etc. From 0b3a7f6f168bc4c614612527b3560a2ea2496b6a Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 26 Feb 2026 16:12:30 +0100 Subject: [PATCH 09/15] fixup! Start implementing UI-based tests by adding an AutoHotKey library The SSH clone in the ctrl-c test occasionally fails with "early EOF" / "unexpected disconnect while reading sideband packet" on CI runners. This is a transient SSH/network issue unrelated to the MSYS2 runtime, but it causes the entire test to fail. Wrap the second clone (the one that verifies cloning completes successfully, as opposed to the one that tests Ctrl+C interruption) in a retry loop with up to five attempts. On each failure, clean up the partial clone directory and restart sshd (which may have exited after the broken connection), then try again. The regex pattern now accepts either "Receiving objects: .*, done." (success) or "fatal: early EOF" (transient failure) followed by the PowerShell prompt. Assisted-by: Claude Opus 4.6 Signed-off-by: Johannes Schindelin --- ui-tests/ctrl-c.ahk | 26 +++++++++++++++++++++----- ui-tests/ui-test-library.ahk | 8 +++++--- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/ui-tests/ctrl-c.ahk b/ui-tests/ctrl-c.ahk index 3ca8873de7..432c0b58c0 100644 --- a/ui-tests/ctrl-c.ahk +++ b/ui-tests/ctrl-c.ahk @@ -151,11 +151,27 @@ if (openSSHPath != '' and FileExist(openSSHPath . '\sshd.exe')) { Info('Started SSH server: ' sshdPID) Info('Starting clone') - Send('git -c core.sshCommand="ssh ' . sshOptions . '" clone ' . cloneOptions . '{Enter}') - Sleep 500 - Info('Waiting for clone to finish') - WinActivate('ahk_id ' . hwnd) - WaitForRegExInWindowsTerminal('Receiving objects: .*, done\.`r?`nPS .*>[ `n`r]*$', 'Timed out waiting for clone to finish', 'Clone finished', 15000, 'ahk_id ' . hwnd) + retries := 5 + Loop retries { + Send('git -c core.sshCommand="ssh ' . sshOptions . '" clone ' . cloneOptions . '{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) + + if InStr(matchObj[1], 'done.') + break + if A_Index == retries + ExitWithError('Clone failed after ' . retries . ' attempts (early EOF)') + Info('Clone failed (early EOF), 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 not DirExist(largeGitClonePath) ExitWithError('`large-clone` did not work?!?') diff --git a/ui-tests/ui-test-library.ahk b/ui-tests/ui-test-library.ahk index d1a240e331..5a1b54bbd5 100644 --- a/ui-tests/ui-test-library.ahk +++ b/ui-tests/ui-test-library.ahk @@ -93,13 +93,15 @@ WaitForRegExInWindowsTerminal(regex, errorMessage, successMessage, timeout := 50 while true { capturedText := CaptureBufferFromWindowsTerminal(winTitle) - if RegExMatch(capturedText, regex) - break + if RegExMatch(capturedText, regex, &matchObj) + { + Info(successMessage) + return matchObj + } Sleep 100 if A_TickCount > timeout { Info('Captured text:`n' . capturedText) ExitWithError errorMessage } } - Info(successMessage) } \ No newline at end of file From 53163b1e90e1cbcbf33a316e93b2d22cae328a39 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 26 Feb 2026 14:14:55 +0100 Subject: [PATCH 10/15] Cygwin: pty: Guard accept_input routing and flush stale readahead in fast path This final commit in the series addresses two remaining edge cases where characters can escape through unintended routing during pseudo console oscillation. Part 1: accept_input() routing guard ------------------------------------- accept_input() writes data from the readahead buffer to one of the two PTY input pipes. Its routing condition was: if (to_be_read_from_nat_pipe() && pty_input_state == to_nat) write_to = to_slave_nat; /* nat pipe */ else write_to = to_slave; /* cyg pipe */ A comment in the code documents the intention: "This code is reached if non-cygwin app is foreground and pseudo console is NOT enabled." But the condition does not actually check pcon_activated. During pseudo console oscillation, accept_input() can route data to the nat pipe even while the pseudo console IS active. When pcon is active, input for native processes flows through conhost.exe, not through direct pipe writes. Routing data to the nat pipe via accept_input() during pcon activation either duplicates what conhost already delivers or displaces data that should have stayed in the cyg pipe. Add `&& !pcon_activated` to make the code match its own documented invariant. Part 2: readahead flush in the pcon+nat fast code path ------------------------------------------------------ The pcon+nat fast code path in master::write() handles the common case where a native app is in the foreground with pcon active. It writes keystrokes directly to the nat pipe via WriteFile(), bypassing line_edit() entirely. If a previous call to master::write() went through line_edit() (because pcon was momentarily inactive during oscillation), line_edit() may have left data in the readahead buffer via unget_readahead(). Without flushing this stale readahead, it persists until the next line_edit() call, at which point accept_input() emits it -- potentially after newer characters that went through the fast code path, breaking chronological order. Add an accept_input() call at the top of the pcon+nat fast code path to flush any stale readahead before the current keystroke is written via WriteFile(). Together with the three preceding commits, this eliminates the character reordering reported in git-for-windows/git#5632. Addresses: https://github.com/git-for-windows/git/issues/5632 Fixes: 3d46583d4fa8 ("Cygwin: pty: Update some comments in pty code.") Assisted-by: Claude Opus 4.6 Signed-off-by: Johannes Schindelin --- winsup/cygwin/fhandler/pty.cc | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/winsup/cygwin/fhandler/pty.cc b/winsup/cygwin/fhandler/pty.cc index 84e4f6f86d..693e1a8062 100644 --- a/winsup/cygwin/fhandler/pty.cc +++ b/winsup/cygwin/fhandler/pty.cc @@ -489,6 +489,7 @@ fhandler_pty_master::accept_input () HANDLE write_to = get_output_handle (); tmp_pathbuf tp; if (to_be_read_from_nat_pipe () + && !get_ttyp ()->pcon_activated && get_ttyp ()->pty_input_state == tty::to_nat) { /* This code is reached if non-cygwin app is foreground and @@ -2226,8 +2227,18 @@ fhandler_pty_master::write (const void *ptr, size_t len) WaitForSingleObject (input_mutex, mutex_timeout); if (to_be_read_from_nat_pipe () && get_ttyp ()->pcon_activated && get_ttyp ()->pty_input_state == tty::to_nat) - { /* Reaches here when non-cygwin app is foreground and pseudo console - is activated. */ + { + /* Flush any stale readahead data from a prior line_edit call that + ran while pty_input_state was temporarily to_cyg (e.g. during a + setpgid_aux transition when a cygwin child of the native process + started or exited). Without this, the readahead contents would + be stranded and emitted after the direct WriteFile below, + breaking chronological order. */ + if (get_readahead_valid ()) + { + accept_input (); + } + tmp_pathbuf tp; char *buf = (char *) ptr; size_t nlen = len; From 467ed0c1cb0ee79bee6f3cc86d024470e666d24c Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 20 Feb 2026 23:51:07 +0100 Subject: [PATCH 11/15] ui-tests: add mintty launch and capture helpers to the library The existing UI test infrastructure only supports Windows Terminal, but the keystroke reordering bug reported in https://github.com/git-for-windows/git/issues/5632 manifests most reliably in mintty, which uses a different PTY code path. To write a reproducer for that bug, we need library functions that can launch mintty and read back what it displayed. An initial attempt used mintty's `-l` flag to write a terminal log file, then read back that log with ANSI escape sequences stripped. This approach turned out to be unreliable: mintty buffers its log output, so content that is already visible on screen (such as the `$ ` prompt) may not have been flushed to the log file yet. Polling for a prompt that is already displayed but not yet logged leads to an indefinite wait. Instead, LaunchMintty() configures mintty's Ctrl+F5 keybinding to trigger the `export-html` action, which writes an HTML snapshot of the current screen to a file. This is instantaneous and always reflects exactly what is on screen. The function uses window-class enumeration to identify the newly-created mintty window among any pre-existing instances and returns its handle. CaptureBufferFromMintty() sends Ctrl+F5 to trigger the export, reads the resulting HTML file, extracts the `` content, strips HTML tags, and decodes common entities to return plain text suitable for substring matching. It accepts an optional window title to activate the correct mintty instance before sending the keystroke. Note that AHK's ControlSend cannot be used here because mintty passes the raw keycodes through to the terminal session rather than interpreting them as window-level shortcuts, so WinActivate followed by Send is the only way to trigger the export action. Assisted-by: Claude Opus 4.6 Signed-off-by: Johannes Schindelin --- ui-tests/ui-test-library.ahk | 66 ++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/ui-tests/ui-test-library.ahk b/ui-tests/ui-test-library.ahk index 5a1b54bbd5..a559be56db 100644 --- a/ui-tests/ui-test-library.ahk +++ b/ui-tests/ui-test-library.ahk @@ -104,4 +104,70 @@ WaitForRegExInWindowsTerminal(regex, errorMessage, successMessage, timeout := 50 ExitWithError errorMessage } } +} + +; Launch mintty with HTML export support. Returns the window handle. +; Ctrl+F5 is bound to export-html; the file is written to /mintty-export.html. +LaunchMintty(extraArgs := '') { + exportFile := A_ScriptDir . '\mintty-export.html' + savePattern := StrReplace(A_ScriptDir, '\', '/') '/mintty-export' + minttyClass := 'ahk_class mintty' + existing := Map() + for h in WinGetList(minttyClass) + existing[h] := true + + cmd := 'mintty.exe -o "KeyFunctions=C+F5:export-html" -o "SaveFilename=' savePattern '"' + if extraArgs != '' + cmd .= ' ' extraArgs + cmd .= ' -' + Run cmd, , , &childPid + Info 'Launched mintty, PID: ' childPid + + hwnd := 0 + deadline := A_TickCount + 10000 + while A_TickCount < deadline + { + for h in WinGetList(minttyClass) + { + if !existing.Has(h) + { + hwnd := h + break 2 + } + } + Sleep 100 + } + if !hwnd + ExitWithError 'New mintty window did not appear' + WinActivate('ahk_id ' hwnd) + Info 'Found new mintty: ' hwnd + return hwnd +} + +; Trigger Ctrl+F5 to export mintty's screen as HTML, read it, strip tags, +; and return the plain text. +CaptureBufferFromMintty(winTitle := '') { + static exportFile := A_ScriptDir . '\mintty-export.html' + if FileExist(exportFile) + FileDelete exportFile + if winTitle != '' + WinActivate winTitle + Send '^{F5}' + deadline := A_TickCount + 3000 + while !FileExist(exportFile) && A_TickCount < deadline + Sleep 50 + if !FileExist(exportFile) + return '' + Sleep 100 + html := FileRead(exportFile) + ; Extract body content only (skip CSS in