diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index 0a73526e35..ab60a04a3f 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,15 +96,10 @@ 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: | $exitCode = 0 - type minimize-log-window.ahk & "${env:RUNNER_TEMP}\ahk\AutoHotKey64.exe" /ErrorStdOut /force minimize-log-window.ahk "$PWD\minimize-log-window" 2>&1 | Out-Default if (!$?) { $exitCode = 1; echo "::error::Failed to minimize Log window!" } else { echo "::notice::Minimized Log window" } exit $exitCode @@ -119,11 +111,11 @@ jobs: $exitCode = 0 & "${env:RUNNER_TEMP}\ahk\AutoHotKey64.exe" /ErrorStdOut /force background-hook.ahk "$PWD\bg-hook" 2>&1 | Out-Default if (!$?) { $exitCode = 1; echo "::error::Test failed!" } else { echo "::notice::Test log" } - type bg-hook.log $env:LARGE_FILES_DIRECTORY = "${env:RUNNER_TEMP}\large" & "${env:RUNNER_TEMP}\ahk\AutoHotKey64.exe" /ErrorStdOut /force ctrl-c.ahk "$PWD\ctrl-c" 2>&1 | Out-Default if (!$?) { $exitCode = 1; echo "::error::Ctrl+C Test failed!" } else { echo "::notice::Ctrl+C Test log" } - type ctrl-c.log + & "${env:RUNNER_TEMP}\ahk\AutoHotKey64.exe" /ErrorStdOut /force keystroke-order.ahk "$PWD\keystroke-order" 2>&1 | Out-Default + if (!$?) { $exitCode = 1; echo "::error::Keystroke-order Test failed!" } else { echo "::notice::Keystroke-order Test log" } exit $exitCode - name: Show logs if: always() @@ -131,6 +123,7 @@ jobs: run: | type bg-hook.log type ctrl-c.log + type keystroke-order.log - name: Take screenshot, if canceled id: take-screenshot if: cancelled() || failure() diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..e05270ae56 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,341 @@ +# 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 + +### Cygwin Commit Message Format + +Commits that modify code under `winsup/cygwin/` should follow the Cygwin project's commit message conventions, as established by the upstream maintainers (Corinna Vinschen, Takashi Yano, et al.): + +- **Subject prefix**: `Cygwin: : `, where `` is the subsystem (e.g. `pty`, `flock`, `termios`, `uinfo`, `path`, `spawn`). Example: `Cygwin: pty: Fix jumbled keystrokes by removing the per-keystroke pipe transfer`. Both upper-case and lower-case after the prefix are used upstream; there is no strict rule. +- **`Fixes:` trailer**: When a commit fixes a bug introduced by a specific earlier commit, reference it with `Fixes: <12-char-hash> ("")`. Example: `Fixes: acc44e09d1d0 ("Cygwin: pty: Add missing input transfer when switch_to_pcon_in state.")` +- **`Addresses:` trailer**: Reference the user-visible bug report URL. Example: `Addresses: https://github.com/git-for-windows/git/issues/5632` +- **Trailer ordering**: `Addresses:`, then `Fixes:`, then `Assisted-by:` / `Reviewed-by:` / `Reported-by:`, then `Signed-off-by:` last — following the pattern seen in upstream Cygwin commits. + +## 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 Designed Keystroke Lifecycle + +Understanding the full lifecycle of a keystroke is essential. The design intent is that **no keystroke is ever lost**, regardless of what the foreground process does with it. The lifecycle differs between Cygwin and native foreground processes: + +**When a Cygwin process is in the foreground (e.g., bash):** +1. Terminal emulator writes keystroke via `master::write()` +2. `master::write()` calls `line_edit()` which applies POSIX line discipline +3. `accept_input()` writes processed bytes to the cyg pipe +4. The Cygwin slave (bash) reads from the cyg pipe + +**When a native process is in the foreground with pcon active:** +1. Terminal emulator writes keystroke via `master::write()` +2. `master::write()` fast path writes directly to `to_slave_nat` (nat pipe) +3. Conhost (the pseudo console host) reads from the nat pipe, converts the byte stream to `INPUT_RECORD` events, and stores them in its console input buffer +4. If the native process reads stdin: it gets `INPUT_RECORD` events via `ReadConsoleInput()` +5. If the native process does NOT read stdin (common for background tasks): the `INPUT_RECORD` events accumulate in conhost's buffer +6. When the native process exits: `cleanup_for_non_cygwin_app()` calls `transfer_input(to_cyg)`, which reads all pending `INPUT_RECORD` events from the console buffer via `ReadConsoleInputA()`, converts them back to bytes, and writes them to the cyg pipe +7. Bash's readline then receives these bytes as if they had been typed directly + +**Step 6 is critical and easy to overlook.** Keystrokes that go to the nat pipe during a native process's lifetime are NOT consumed by the native app (unless it explicitly reads them). They accumulate in conhost's input buffer and are transferred back to bash at cleanup. The transfer happens via `ReadConsoleInputA()` (raw event reads, not cooked/line-edited), so backspaces, escape sequences, and control characters are preserved as-is. + +**Consequence for debugging:** If keystrokes appear reordered at bash's readline after a native process exits, the problem is that some bytes went to the cyg pipe (directly to readline) while others went to the nat pipe (and were transferred back later). The bytes that went directly arrive first; the transferred bytes arrive second. This split delivery causes reordering. The fix must ensure that ALL keystrokes go through the same pipe during the native process's lifetime. + +### 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 (CSI6n exchange). +- **`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()`**, 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. + +**When transferred input goes to the cyg pipe (to_cyg direction),** it must pass through `line_edit()` to apply POSIX line discipline. This is handled by the `input_transferred_to_cyg` event: the slave signals this event after the transfer, and the master's forward thread wakes up, reads the transferred bytes from the cyg pipe, and processes them through `line_edit()`. This ensures consistent line discipline regardless of whether input arrived via direct typing or via transfer. + +### 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. It stays true for the entire duration of the native session, including during brief transitions when `pcon_activated` may flicker. +- **`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 determines whether keystroke input should go to the nat pipe. Its design intent is simple: return true whenever a native process session is active (`switch_to_nat_pipe` is set) and no Cygwin process is actively reading from the slave (the `TTY_SLAVE_READING` event does not exist). + +The function is synchronized with `pipe_sw_mutex` to avoid reading inconsistent state during pipe switching. If the mutex cannot be acquired and `pcon_start` is set (meaning pseudo console initialization is in progress), the function returns false so that the CSI6n response bytes go through `line_edit()` to the cyg pipe where the initialization code expects them. + +**Important design principle:** This function should NOT check `nat_fg()` (whether the native process is still in the foreground process group). Such a check creates a gap between native process exit and cleanup where keystrokes fall through to `line_edit()` (cyg pipe) instead of going to the nat pipe. This gap causes keystroke reordering: bytes that go directly to the cyg pipe during the gap arrive at readline before bytes that are transferred from the nat pipe at cleanup. The correct approach is to keep routing to the nat pipe as long as `switch_to_nat_pipe` is set, regardless of the native process's foreground status. The `switch_to_nat_pipe` flag is only cleared during cleanup, after `transfer_input(to_cyg)` has moved all pending data back to the cyg pipe. + +### Mutexes and Synchronization + +Three cross-process named mutexes protect different aspects of the PTY state: + +- **`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`. Also acquired by `to_be_read_from_nat_pipe()` to read consistent state. The consistent lock ordering is: `pipe_sw_mutex` first, then `input_mutex`. +- **`attach_mutex`**: Protects console attachment/detachment operations. Used during `transfer_input()` to prevent races when reading console input records via `ReadConsoleInputA()`, and in `get_winpid_to_hand_over()` to prevent the master process from being misidentified during temporary console attachment. + +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 + +When the terminal emulator (mintty) sends a keystroke, it calls `master::write()`. The function has three code paths: + +1. **pcon_start handler**: Active during pseudo console initialization (CSI6n exchange). Accumulates ESC sequence bytes and routes the CSI6n response to the slave. Non-response bytes go through `line_edit()`. This path is only active during the brief initialization window. + +2. **Fast path** (pcon+nat): Active when `to_be_read_from_nat_pipe()` AND `pcon_activated` AND `pty_input_state == to_nat`. Writes directly to `to_slave_nat` via `WriteFile()`, with signal processing and charset conversion. This is the steady-state path for native apps. + +3. **Fallthrough** (`line_edit`): All other cases. Input goes through POSIX line discipline and `accept_input()` routes to the appropriate pipe based on `pty_input_state`. + +### Pseudo Console Oscillation + +When a native process spawns short-lived Cygwin children (e.g. `git.exe` calling `cygpath` via `--format`), the pseudo console activates and deactivates in rapid succession: + +1. Native process in foreground: `pcon_activated=true`, `pty_input_state=to_nat` +2. Cygwin child starts: `setpgid_aux()` fires, transfers data to cyg pipe, `pcon_activated=false`, `pty_input_state=to_cyg` +3. Cygwin child exits (milliseconds later): native process regains foreground, pcon reactivates + +**The key design principle for handling oscillation:** keystrokes must always go to the nat pipe while `switch_to_nat_pipe` is true, regardless of `pcon_activated` or foreground status flickering. When keystrokes reach the nat pipe while the pcon is temporarily deactivated, they go through the raw pipe (not via conhost). When `transfer_input` runs at cleanup, it moves them back. This is safe because the keystrokes stay in the nat pipe in chronological order. + +The bugs that cause keystroke reordering are always of the form: some bytes go to the cyg pipe (via `line_edit` fallthrough) while others go to the nat pipe (via the fast path), and the two sets arrive at bash's readline in the wrong order. The fix is to prevent the split: either ALL bytes go to one pipe, or the routing decision is properly synchronized so that no bytes leak to the wrong pipe. + +### Key Functions for State Transitions + +- **`setup_for_non_cygwin_app()`**: Called when a non-Cygwin process is about to be spawned. Sets up the pseudo console and switches input to nat pipe. Holds `pipe_sw_mutex` during the entire setup to prevent the master from seeing inconsistent state. +- **`cleanup_for_non_cygwin_app()`**: Called when the non-Cygwin process exits. First calls `transfer_input(to_cyg)` to move all pending input from the nat pipe (conhost's console buffer) back to the cyg pipe. Then tears down the pcon via `close_pseudoconsole()`. The transfer must happen BEFORE the pcon is closed (while the console is still accessible). +- **`reset_switch_to_nat_pipe()`**: Cleanup function called from `bg_check()` and `setpgid_aux()`. Detects when the nat pipe owner has exited and resets state. Only performs cleanup when no other process owns the nat pipe and the owner is dead. Does NOT clean up when the owner is self (bash) or alive, to avoid tearing down active sessions. +- **`transfer_input()`**: Moves pending data between the cyg and nat pipes. When transferring to cyg with pcon active, reads `INPUT_RECORD` events from the console via `ReadConsoleInputA()`. When transferring to cyg, signals `input_transferred_to_cyg` so the master's forward thread can apply `line_edit()` to the transferred bytes. +- **`setpgid_aux()`**: Called when the foreground process group changes. Triggers `transfer_input` in the appropriate direction. Releases `pipe_sw_mutex` before acquiring `input_mutex` to maintain consistent lock ordering. + +### Debugging Tips + +When investigating PTY-related bugs, keep these patterns in mind: + +- **Trace the full keystroke lifecycle**: Do not stop at "the keystroke goes to pipe X." Follow it all the way to where bash's readline receives it, including any `transfer_input` calls at cleanup. The most common bugs involve bytes being split across the two pipes and arriving at readline out of order. +- **Check the routing decision in `to_be_read_from_nat_pipe()`**: This function is the gatekeeper for all routing decisions. If it returns the wrong value, keystrokes go to the wrong pipe. Verify that it holds `pipe_sw_mutex` while reading state, and that it does not have unnecessary checks (like `nat_fg()`) that create gaps during transitions. +- **Study existing upstream patches before writing fixes**: Takashi Yano is the upstream PTY maintainer and understands the state machine deeply. When he proposes patches on cygwin-patches@, apply and test his full series before attempting alternative fixes. His patches form cohesive sets where individual patches depend on each other for correct behavior. Cherry-picking individual patches from his series will break invariants. +- **Never remove `transfer_input()` calls without understanding what they transfer**: The transfers at `setpgid_aux()`, `cleanup_for_non_cygwin_app()`, and the pcon_start completion block each serve specific purposes. Removing them loses data. The correct fix for reordering bugs is to ensure keystrokes consistently go to one pipe (typically by fixing the routing decision), not to remove the transfer that reunites split data. +- **The `pcon_start` handler is only for CSI6n**: During pcon initialization, `pcon_start=true` tells `master::write()` to enter a special handler that accumulates the CSI6n response. Non-CSI bytes in this handler go through `line_edit()` to the cyg pipe. This is correct and intentional: during the brief CSI6n exchange, the pcon is not yet ready to receive user input, so `line_edit()` buffers it for bash. The pcon_start handler is NOT a general-purpose input router and should not be modified to route bytes to the nat pipe. +- **Tracing**: For timing-sensitive bugs, use a memory-mapped ring buffer (not per-event file I/O, which changes timings). The master process (mintty) is a MinGW program; C++ static destructors in msys-2.0.dll do NOT fire when it exits. Use `CreateFileMapping` + `MapViewOfFile` for trace buffers that persist after process termination. Use `QueryPerformanceCounter` for microsecond timestamps. Trace across both master and slave processes using separate per-PID files. + +## 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 +- **Cygwin mailing lists**: https://inbox.sourceware.org/cygwin/ (general), https://inbox.sourceware.org/cygwin-patches/ (patches), https://inbox.sourceware.org/cygwin-developers/ (internals) — essential for understanding why specific code was added; commit messages often reference these discussions +- **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. 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/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!') diff --git a/ui-tests/cpu-stress.ps1 b/ui-tests/cpu-stress.ps1 new file mode 100644 index 0000000000..4f1dbff8c0 --- /dev/null +++ b/ui-tests/cpu-stress.ps1 @@ -0,0 +1,6 @@ +$sleepExe = & cygpath.exe -aw /usr/bin/sleep.exe +$procs = 1..[Environment]::ProcessorCount | ForEach-Object { + Start-Process -NoNewWindow -PassThru cmd.exe -ArgumentList '/c','for /L %i in (1,1,999999) do @echo . >NUL' +} +& $sleepExe 1 +$procs | Stop-Process -Force -ErrorAction SilentlyContinue 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/keystroke-order.ahk b/ui-tests/keystroke-order.ahk new file mode 100644 index 0000000000..790710e5dd --- /dev/null +++ b/ui-tests/keystroke-order.ahk @@ -0,0 +1,272 @@ +#Requires AutoHotkey v2.0 +#Include ui-test-library.ahk + +; Reproducer for https://github.com/git-for-windows/git/issues/5632 +; +; Keystroke reordering: when a non-MSYS2 process runs in the foreground +; of a PTY, keystrokes typed into bash arrive out of order because the +; MSYS2 runtime's transfer_input() can reorder bytes across pipe buffers. +; +; The test types characters interleaved with backspaces while a non-MSYS +; foreground process (powershell launching MSYS sleep) runs under CPU +; stress. If backspace bytes get reordered relative to the characters +; they should delete, readline produces wrong output. +; +; The test runs in two phases: +; Phase 1 (pcon enabled): the default mode, exercises the pseudo +; console oscillation code paths in master::write(). +; Phase 2 (disable_pcon): sets MSYS=disable_pcon so that pseudo +; console is never created, exercising the non-pcon input routing +; and verifying that typeahead is preserved correctly. + +SetWorkTree('git-test-keystroke-order') + +testString := 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' + +hwnd := LaunchMintty() +winId := 'ahk_id ' hwnd + +; Wait for bash prompt via HTML export (Ctrl+F5). +deadline := A_TickCount + 60000 +while A_TickCount < deadline +{ + capture := CaptureBufferFromMintty(winId) + if InStr(capture, '$ ') + break + Sleep 500 +} +if !InStr(capture, '$ ') + ExitWithError 'Timed out waiting for bash prompt' +Info 'Bash prompt appeared' + +; === cmd.exe input verification === +; Verify that input typed into cmd.exe (a native Win32 console app) is not +; silently lost. This catches the regression where removing the pcon_start +; post-loop block also removed the pty_input_state = to_nat transition, +; causing keystrokes to go to the wrong pipe. +Info '=== cmd.exe input verification ===' +WinActivate(winId) +SetKeyDelay 20, 20 +SendEvent('{Text}cmd.exe') +SendEvent('{Enter}') +; Type immediately without waiting for cmd.exe to fully start. +Sleep 200 +SendEvent('{Text}echo ' testString) +SendEvent('{Enter}') + +; Wait for the test string to appear in cmd.exe output. +deadline := A_TickCount + 10000 +cmdOk := false +while A_TickCount < deadline +{ + text := CaptureBufferFromMintty(winId) + ; Look for the echoed string (cmd.exe prints the command AND its output) + ; Count occurrences: the echo command line itself plus the output = at least 2 + count := 0 + searchPos := 1 + while searchPos := InStr(text, testString, , searchPos) + { + count++ + searchPos += StrLen(testString) + } + if count >= 2 + { + Info 'cmd.exe echoed the test string correctly' + cmdOk := true + break + } + Sleep 500 +} +if !cmdOk +{ + Info 'Captured text:' + Info text + ExitWithError 'cmd.exe did not echo the test string (input lost?)' +} + +; === Ctrl+H single-character delete verification === +; When pseudo console is enabled, conhost.exe may translate Ctrl+H (0x08) +; into Ctrl+Backspace, which performs word-wise deletion instead of +; single-character deletion. Verify that Ctrl+H deletes only one char. +; See: https://inbox.sourceware.org/cygwin-patches/463c3df7-3810-ed9a-9f7c-c2cf4fd6a7b7@gmx.de/ +Info '=== Ctrl+H single-character delete verification ===' +WinActivate(winId) +SetKeyDelay 20, 20 +SendEvent('{Text}echo Expresso') +Sleep 200 +Send '{Ctrl down}h{Ctrl up}' +Sleep 200 +SendEvent('{Enter}') + +deadline := A_TickCount + 10000 +ctrlHOk := false +while A_TickCount < deadline +{ + text := CaptureBufferFromMintty(winId) + ; If Ctrl+H correctly deleted only 'o', the command executed was + ; "echo Express" and cmd.exe printed "Express" as output. If Ctrl+H + ; did a word-wise delete, "Expresso" was fully erased and cmd.exe + ; ran "echo " which prints "ECHO is on." instead. + if InStr(text, 'Express') && !InStr(text, 'Expresso') + { + Info 'Ctrl+H correctly deleted only the last character' + ctrlHOk := true + break + } + Sleep 500 +} +if !ctrlHOk +{ + Info 'Captured text:' + Info text + ExitWithError 'Ctrl+H did not delete a single character (word-wise delete?)' +} + +; Exit cmd.exe and verify we return to bash. +WinActivate(winId) +SetKeyDelay 20, 20 +SendEvent('{Text}exit') +SendEvent('{Enter}') +Sleep 1000 + +text := CaptureBufferFromMintty(winId) +; After exiting cmd.exe we should see a bash prompt again. +; Find the last "$ " -- it should come after the cmd.exe session. +lastPrompt := 0 +pos := 1 +while pos := InStr(text, '$ ', , pos) +{ + lastPrompt := pos + pos += 2 +} +after := (lastPrompt > 0) ? Trim(SubStr(text, lastPrompt + 2)) : '' +if after != '' +{ + Info 'WARNING: unexpected text after prompt: ' after +} +Info 'Back at bash prompt after cmd.exe' + +stressCmd := 'powershell.exe -File ' StrReplace(A_ScriptDir, '\', '/') '/cpu-stress.ps1' +Info 'Foreground command: ' stressCmd + +; === Phase 1: pcon enabled (default) === +Info '=== Phase 1: pcon enabled ===' +mismatch := RunKeystrokeTest(winId, stressCmd, testString, 5) + +if !mismatch +{ + ; === Phase 2: disable_pcon === + Info '=== Phase 2: disable_pcon ===' + WinActivate(winId) + SetKeyDelay 20, 20 + SendEvent('{Text}export MSYS=disable_pcon') + SendEvent('{Enter}') + Sleep 500 + + mismatch := RunKeystrokeTest(winId, stressCmd, testString, 5) +} + +WinActivate(winId) +SetKeyDelay 20, 20 +Send '{Ctrl down}c{Ctrl up}' +Sleep 500 +SendEvent('{Text}exit') +SendEvent('{Enter}') +Sleep 1000 +ExitApp mismatch ? 1 : 0 + +; Run the keystroke reordering test for a given number of iterations. +; Returns true if a mismatch was detected, false if all iterations passed. +RunKeystrokeTest(winId, stressCmd, testString, maxIterations) { + mismatch := false + chunkSize := 2 + + Loop maxIterations + { + iteration := A_Index + Info 'Iteration ' iteration ' of ' maxIterations + + WinActivate(winId) + + ; 1. Launch foreground stress process + SetKeyDelay 20, 20 + SendEvent('{Text}' stressCmd) + SendEvent('{Enter}') + + ; 2. Type with backspaces: send chunkSize chars + ",;" + BS*2 at a time. + SetKeyDelay 1, 1 + Sleep 500 + offset := 1 + while offset <= StrLen(testString) + { + chunk := SubStr(testString, offset, chunkSize) + SendEvent('{Text}' chunk ',;') + SendEvent('{Backspace}{Backspace}') + offset += chunkSize + } + + ; 3. Poll the HTML export for what readline rendered after "$ ". + ; The HTML shows the final screen state (backspaces already applied). + Sleep 2000 + deadline := A_TickCount + 30000 + while A_TickCount < deadline + { + text := CaptureBufferFromMintty(winId) + + ; Find the last "$ " and extract the text after it + lastPrompt := 0 + pos := 1 + while pos := InStr(text, '$ ', , pos) + { + lastPrompt := pos + pos += 2 + } + if lastPrompt > 0 + { + after := Trim(SubStr(text, lastPrompt + 2)) + ; Take first "word" (up to whitespace or end) + spPos := InStr(after, ' ') + if spPos > 0 + after := SubStr(after, 1, spPos - 1) + + if after = testString + { + Info 'Iteration ' iteration ': OK' + break + } + if InStr(after, 'powershell') || InStr(after, 'sleep') || after = '' + { + ; Stress command or bare prompt -- keep waiting + } + else if SubStr(testString, 1, StrLen(after)) != after + { + Info 'MISMATCH in iteration ' iteration '!' + Info 'Expected: ' testString + Info 'Got: ' after + mismatch := true + break + } + } + Sleep 500 + } + + if A_TickCount >= deadline + { + Info 'TIMEOUT in iteration ' iteration + mismatch := true + break + } + if mismatch + break + + ; Clear readline buffer for next iteration + SetKeyDelay 20, 20 + Send '{Ctrl down}u{Ctrl up}' + Sleep 300 + } + + if !mismatch + Info 'All ' maxIterations ' iterations passed' + + return mismatch +} 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..645bcd0d82 100644 --- a/ui-tests/ui-test-library.ahk +++ b/ui-tests/ui-test-library.ahk @@ -40,7 +40,19 @@ CleanUpWorkTree() { } Info(text) { + global workTree, cannotWriteToStdout FileAppend text '`n', workTree '.log' + if !IsSet(cannotWriteToStdout) + { + try + FileAppend text '`n', '*' + catch as e { + if e.__Class == 'OSError' && e.Number == 6 + cannotWriteToStdout:= false + else + throw e + } + } } closeWindow := false @@ -67,41 +79,24 @@ 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') +; 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. +CaptureBufferFromWindowsTerminal(winTitle := '') { + static exportFile := A_ScriptDir . '\wt-buffer-export.txt' + if FileExist(exportFile) + FileDelete exportFile 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 + 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 := '') { @@ -109,17 +104,82 @@ WaitForRegExInWindowsTerminal(regex, errorMessage, successMessage, timeout := 50 ; Wait for the regex to match in the terminal output while true { - capturedText := CaptureTextFromWindowsTerminal(winTitle) - if RegExMatch(capturedText, regex) - break + capturedText := CaptureBufferFromWindowsTerminal(winTitle) + if RegExMatch(capturedText, regex, &matchObj) + { + Info(successMessage) + return matchObj + } Sleep 100 if A_TickCount > timeout { Info('Captured text:`n' . capturedText) ExitWithError errorMessage } - if winTitle != '' - WinActivate winTitle - MouseClick 'WheelDown', , , 20 } - Info(successMessage) +} + +; 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