diff --git a/.github/actions/install-node-dependencies/action.yml b/.github/actions/install-node-dependencies/action.yml index 6aecef6e1524..14517bfd55e6 100644 --- a/.github/actions/install-node-dependencies/action.yml +++ b/.github/actions/install-node-dependencies/action.yml @@ -146,8 +146,7 @@ runs: path: ${{ steps.electron-package-cache.outputs.cache-root }} key: electron-package-${{ runner.os }}-${{ runner.arch }}-${{ steps.electron-package-cache.outputs.version }} - # Why cached: `--ignore-scripts` leaves node-pty without build/Release, so - # ensure-native-runtime node-gyp-compiles it in every job that asks for a runtime. + # Cache Windows registry and process-tree addons built by ensure-native-runtime. # The artifacts are ABI-bound, so the key carries the target runtime, the resolved # Node version, and the patch whose contents the build has to match. # Windows extra globs are empty on Linux. No restore-keys: a partial-match key is @@ -173,10 +172,9 @@ runs: uses: actions/cache@v5 with: path: | - node_modules/.pnpm/node-pty@*/node_modules/node-pty/build native/windows-registry/build node_modules/.pnpm/@vscode+windows-process-tre*/node_modules/@vscode/windows-process-tree/build - key: native-modules-${{ runner.os }}-${{ steps.native-cache-scope.outputs.scope }}-${{ runner.arch }}-${{ inputs.native-runtime }}-node${{ steps.requested-node.outputs.node-version || steps.default-node.outputs.node-version }}-${{ hashFiles('pnpm-lock.yaml', '.github/actions/install-node-dependencies/action.yml', 'config/scripts/ensure-native-runtime.mjs', 'config/scripts/rebuild-native-deps.mjs', 'config/patches/node-pty@1.1.0.patch', 'config/patches/@vscode__windows-process-tree@0.8.0.patch', 'native/windows-registry/src/addon.cc', 'native/windows-registry/binding.gyp', 'native/windows-registry/package.json') }} + key: native-modules-${{ runner.os }}-${{ steps.native-cache-scope.outputs.scope }}-${{ runner.arch }}-${{ inputs.native-runtime }}-node${{ steps.requested-node.outputs.node-version || steps.default-node.outputs.node-version }}-${{ hashFiles('pnpm-lock.yaml', '.github/actions/install-node-dependencies/action.yml', 'config/scripts/ensure-native-runtime.mjs', 'config/scripts/rebuild-native-deps.mjs', 'config/patches/@vscode__windows-process-tree@0.8.0.patch', 'native/windows-registry/src/addon.cc', 'native/windows-registry/binding.gyp', 'native/windows-registry/package.json') }} - name: Restore compiled native modules without saving id: native-cache-restore-only @@ -184,10 +182,9 @@ runs: uses: actions/cache/restore@v5 with: path: | - node_modules/.pnpm/node-pty@*/node_modules/node-pty/build native/windows-registry/build node_modules/.pnpm/@vscode+windows-process-tre*/node_modules/@vscode/windows-process-tree/build - key: native-modules-${{ runner.os }}-${{ steps.native-cache-scope.outputs.scope }}-${{ runner.arch }}-${{ inputs.native-runtime }}-node${{ steps.requested-node.outputs.node-version || steps.default-node.outputs.node-version }}-${{ hashFiles('pnpm-lock.yaml', '.github/actions/install-node-dependencies/action.yml', 'config/scripts/ensure-native-runtime.mjs', 'config/scripts/rebuild-native-deps.mjs', 'config/patches/node-pty@1.1.0.patch', 'config/patches/@vscode__windows-process-tree@0.8.0.patch', 'native/windows-registry/src/addon.cc', 'native/windows-registry/binding.gyp', 'native/windows-registry/package.json') }} + key: native-modules-${{ runner.os }}-${{ steps.native-cache-scope.outputs.scope }}-${{ runner.arch }}-${{ inputs.native-runtime }}-node${{ steps.requested-node.outputs.node-version || steps.default-node.outputs.node-version }}-${{ hashFiles('pnpm-lock.yaml', '.github/actions/install-node-dependencies/action.yml', 'config/scripts/ensure-native-runtime.mjs', 'config/scripts/rebuild-native-deps.mjs', 'config/patches/@vscode__windows-process-tree@0.8.0.patch', 'native/windows-registry/src/addon.cc', 'native/windows-registry/binding.gyp', 'native/windows-registry/package.json') }} # pnpm's bundled gyp_main.py is not executable on fresh Linux runners. - name: Use external node-gyp diff --git a/.github/workflows/bun-profile-tests.yml b/.github/workflows/bun-profile-tests.yml index 39eb8d42b7ad..ca461def8240 100644 --- a/.github/workflows/bun-profile-tests.yml +++ b/.github/workflows/bun-profile-tests.yml @@ -20,11 +20,18 @@ on: - '.github/actions/load-windows-watcher-artifacts/**' workflow_dispatch: inputs: + linux_package: + description: Package and test Linux x64 and ARM64 without publishing a release + type: boolean + default: false signed_mac_package: description: Sign, notarize and test macOS packages without publishing a release type: boolean default: false +env: + ORCA_TEST_REQUIRE_ORCAD_ARTIFACTS: '1' + permissions: contents: read @@ -183,7 +190,7 @@ jobs: - name: Verify native Alpine artifact and persistence run: | docker run --rm --init -i \ - -e ORCA_BACKGROUND_LAUNCH=1 \ + -e ORCA_BACKGROUND_LAUNCH=1 -e ORCA_TEST_REQUIRE_ORCAD_ARTIFACTS=1 \ -v "$GITHUB_WORKSPACE:/work" -w /work \ node:24-alpine3.23 sh -s <<'BUN_QUALIFICATION' set -eu @@ -200,6 +207,73 @@ jobs: node node_modules/vitest/vitest.mjs run --config config/vitest.config.ts config/scripts/orcad-companion-artifacts.test.mjs BUN_QUALIFICATION + linux_package: + needs: windows_watcher + if: ${{ !cancelled() && github.event_name == 'workflow_dispatch' && inputs.linux_package && needs.windows_watcher.result == 'success' }} + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-22.04 + arch: x64 + unpacked_dir: dist/linux-unpacked + - os: ubuntu-24.04-arm + arch: arm64 + unpacked_dir: dist/linux-arm64-unpacked + runs-on: ${{ matrix.os }} + timeout-minutes: 90 + env: + ORCA_BACKGROUND_LAUNCH: '1' + NODE_OPTIONS: --max-old-space-size=4096 + PACKAGE_ARCH: ${{ matrix.arch }} + PACKAGE_DIRECTORY: ${{ matrix.unpacked_dir }} + ORCA_LINUX_ARM64_RELEASE: ${{ matrix.arch == 'arm64' && '1' || '0' }} + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ github.sha }} + persist-credentials: false + - uses: ./.github/actions/load-windows-watcher-artifacts + - uses: ./.github/actions/install-node-dependencies + with: + native-runtime: electron + cache-dependency-path: | + pnpm-lock.yaml + mobile/pnpm-lock.yaml + - uses: ./.github/actions/install-mobile-dependencies + - name: Install Linux package and computer-use dependencies + run: sudo apt-get update && sudo apt-get install -y python3-gi gir1.2-atspi-2.0 at-spi2-core xclip xdotool rpm + - run: pnpm build:release + - name: Package without release publication + run: | + node config/scripts/ensure-native-runtime.mjs --runtime=electron + pnpm exec electron-builder --config config/electron-builder.config.cjs --linux AppImage deb rpm --"$PACKAGE_ARCH" --publish never + - name: Preserve exact package and provenance + run: | + set -euo pipefail + mkdir -p "$RUNNER_TEMP/linux-package-evidence" + git rev-parse HEAD > "$RUNNER_TEMP/linux-package-evidence/source-commit.txt" + uname -a > "$RUNNER_TEMP/linux-package-evidence/host-kernel.txt" + tar -czf "$RUNNER_TEMP/linux-package-evidence/unpacked-$PACKAGE_ARCH.tar.gz" -C "$PACKAGE_DIRECTORY" . + (cd "$PACKAGE_DIRECTORY" && find . -type f -print0 | sort -z | xargs -0 sha256sum) > "$RUNNER_TEMP/linux-package-evidence/payload-sha256.txt" + sha256sum dist/*.AppImage dist/*.deb dist/*.rpm > "$RUNNER_TEMP/linux-package-evidence/installers-sha256.txt" + (cd "$RUNNER_TEMP/linux-package-evidence" && sha256sum "unpacked-$PACKAGE_ARCH.tar.gz") > "$RUNNER_TEMP/linux-package-evidence/archive-sha256.txt" + - name: Qualify packaged terminal on Ubuntu 20.04 userspace + run: | + set -euo pipefail + node config/scripts/run-linux-packaged-terminal-floor-smoke.mjs --app-dir "$PACKAGE_DIRECTORY" 2>&1 | tee "$RUNNER_TEMP/linux-package-evidence/floor-smoke.log" + - uses: actions/upload-artifact@v7 + if: ${{ always() }} + with: + name: bun-linux-package-${{ matrix.arch }} + path: | + ${{ runner.temp }}/linux-package-evidence/ + dist/*.AppImage + dist/*.deb + dist/*.rpm + if-no-files-found: error + retention-days: 7 + signed_mac_package: needs: windows_watcher if: ${{ !cancelled() && github.event_name == 'workflow_dispatch' && inputs.signed_mac_package && github.repository == 'stablyai/orca' && startsWith(github.ref, 'refs/heads/') && needs.windows_watcher.result == 'success' }} diff --git a/.github/workflows/ci-cache-warmup.yml b/.github/workflows/ci-cache-warmup.yml index 3bd497e22359..87a61a2d3104 100644 --- a/.github/workflows/ci-cache-warmup.yml +++ b/.github/workflows/ci-cache-warmup.yml @@ -19,7 +19,6 @@ on: - 'config/tsconfig*.json' - 'config/scripts/ensure-native-runtime.mjs' - 'config/scripts/rebuild-native-deps.mjs' - - 'config/patches/node-pty@1.1.0.patch' - 'config/patches/@vscode__windows-process-tree@0.8.0.patch' - 'native/windows-registry/**' pull_request: diff --git a/.github/workflows/computer-e2e.yml b/.github/workflows/computer-e2e.yml index 325ce6cb61cd..0aa55e5f2b7a 100644 --- a/.github/workflows/computer-e2e.yml +++ b/.github/workflows/computer-e2e.yml @@ -26,13 +26,21 @@ on: - 'config/scripts/daemon-endpoint-handover-smoke.mjs' - 'config/scripts/windows-daemon-workspace-close-repro.mjs' - 'config/scripts/verify-computer-native.mjs' - # Why: the native-smoke job boots the built terminal daemon under plain - # Node, so any change to the daemon bundle graph or the main build must - # re-run it (the v1.4.129-rc.1 daemon outage shipped with green CI). + # Runtime and standalone bundle changes must exercise actual daemon startup. + - 'config/scripts/daemon-smoke-launch*' + - 'config/scripts/build-terminal-daemon.mjs' + - 'config/scripts/orcad-entry-build.mjs' + - 'config/scripts/build-cli-runtime.mjs' + - 'config/scripts/build-orcad-bun.mjs' + - 'config/scripts/build-windows-conpty.mjs' + - 'config/licenses/windows-conpty-LICENSE.txt' + - 'resources/licenses/bun/**' + - 'config/bundled-cli-runtime.cjs' + - 'src/shared/orcad-bun-runtime.ts' + - 'src/shared/windows-conpty-release.ts' - 'electron.vite.config.ts' - 'config/build-plugins/**' - 'src/main/daemon/**' - - 'src/main/providers/local-pty-provider.ts' - 'src/main/providers/windows-shell-args.ts' - 'src/main/providers/windows-shell-preflight-runtime.windows.test.ts' - 'native/computer-use-macos/**' @@ -81,6 +89,11 @@ jobs: - uses: ./.github/actions/install-node-dependencies with: native-runtime: node + - name: Prepare pinned Bun for Windows shell fixtures + if: runner.os == 'Windows' + run: node --input-type=module -e "import { buildCliRuntime } from './config/scripts/build-cli-runtime.mjs'; await buildCliRuntime(process.platform, process.arch)" + - name: Build terminal service and Windows gate for shell fixtures + run: pnpm run build:terminal-daemon # Why: without --config, bare vitest ignores config/vitest.config.ts (there # is no root config) and falls back to the 5s default timeout with no # Windows worker cap, so the real csc.exe launcher-compile tests time out @@ -126,10 +139,8 @@ jobs: - run: pnpm verify:computer-native - run: pnpm build:cli - run: pnpm run build:electron-vite:parallel - # Why: boot the BUILT daemon-entry under plain Node the way production - # forks it. v1.4.129-rc.1 shipped a daemon that exited at module load - # (leaked electron require) while every other check passed; this fails - # the PR when the built daemon cannot start on ubuntu-22.04 / windows. + - run: pnpm exec vitest run --config config/vitest.config.ts config/scripts/daemon-smoke-launch.test.mjs + # Exercise the shipped Bun runtime and standalone daemon bundle together. - name: Daemon boot smoke run: node config/scripts/daemon-boot-smoke.mjs # Why: a daemon that lost its endpoint name used to delete the replacement's diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 72edb50b0af0..6f5bfbce752e 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -57,7 +57,7 @@ jobs: with: ref: ${{ inputs.ref || github.ref }} - # Why: the build's plain-Node daemon smoke load resolves node-pty. + # Build checks require the Electron runtime and Windows native addons. - uses: ./.github/actions/install-node-dependencies with: native-runtime: node @@ -77,6 +77,8 @@ jobs: VITE_EXPOSE_STORE: 'true' run: | status=0 + pnpm run build:cli || status=1 + pnpm run build:terminal-daemon || status=1 pnpm run build:relay & relay_pid=$! npx electron-vite build --mode e2e || status=1 diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 833b08995913..0485da13ccbb 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -490,7 +490,7 @@ jobs: # present, so without this the fish barrier is only covered by config-shape # assertions and never actually exercised. # Why release-4: DECSET 2031 arming lives in the fish 4.0 Rust tty_handoff, and - # fish-color-scheme-child-stdin.node-pty.test.ts (#9993) needs it. Noble ships + # fish-color-scheme-child-stdin.bun.test.ts (#9993) needs it. Noble ships # 3.7, so the PPA is what makes that lane real. - name: Install zsh and fish run: | @@ -560,25 +560,28 @@ jobs: with: native-runtime: node + - name: Prepare pinned Bun for real terminal tests + run: node config/scripts/build-orcad-bun.mjs --runtime-only --out-dir out/orcad + - name: Test real shell contracts run: | node config/scripts/run-bun-profile-tests.mjs --maxWorkers=1 src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec.test.ts pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 \ src/main/daemon/shell-ready.test.ts \ - src/main/daemon/node-pty-fd-leak.test.ts \ + src/main/daemon/bun-pty-fd-lifecycle.integration.test.ts \ src/main/providers/local-pty-shell-ready-zsh-launch-environment.test.ts \ src/main/providers/__tests__/shell-ready-framework-example.test.ts \ src/main/pty/codex-shell-launch-preflight.test.ts \ src/main/pty/omp-shell-wrapper-alias-safety.test.ts \ - src/main/pty/omp-shell-wrapper.node-pty.test.ts \ + src/main/pty/omp-shell-wrapper.bun.test.ts \ src/main/shell-startup-feature-channel.test.ts \ - src/main/terminal-history-fish-session.node-pty.test.ts \ + src/main/terminal-history-fish-session.bun.test.ts \ src/main/zsh-scoped-histfile.live-shell.test.ts \ src/main/zsh-startup-hook-user-config-equivalence.live-shell.test.ts \ src/main/zsh-wrapper-version-mismatch.live-shell.test.ts \ - src/renderer/src/components/terminal-pane/fish-color-scheme-child-stdin.node-pty.test.ts \ - src/shared/fish-query-reply-child-stdin.node-pty.test.ts \ - src/shared/pty-reply-echo-shapes.node-pty.test.ts \ + src/main/daemon/pty-subprocess/bun-tests/fish-color-scheme-child-stdin.bun.test.ts \ + src/main/daemon/pty-subprocess/bun-tests/fish-query-reply-child-stdin.bun.test.ts \ + src/main/daemon/pty-subprocess/bun-tests/pty-reply-echo-shapes.bun.test.ts \ src/shared/startup-shell-portability.live-shell.test.ts \ src/shared/posix-command-path-lookup.test.ts @@ -629,7 +632,7 @@ jobs: persist-credentials: false # Why no native-runtime: the provider drives the prebuilt agent-browser binary - # shipped in node_modules and never touches node-pty. + # shipped in node_modules without loading native terminal code. - uses: ./.github/actions/install-node-dependencies # Why the runner's Google Chrome and not its chromium: Ubuntu 24.04 only ships an @@ -668,7 +671,7 @@ jobs: persist-credentials: false # Why no native-runtime: the builder is esbuild and the render check is a browser. Nothing - # in this job loads node-pty. + # in this job loads native terminal code. - uses: ./.github/actions/install-node-dependencies with: native-runtime: node @@ -843,7 +846,7 @@ jobs: status=0 pnpm run build:cli || status=1 - scripts=(build:relay build:electron-vite:parallel) + scripts=(build:relay build:terminal-daemon build:electron-vite:parallel) pids=() for script in "${scripts[@]}"; do pnpm run "$script" & @@ -967,16 +970,16 @@ jobs: uses: actions/cache/save@v5 with: path: | - node_modules/.pnpm/node-pty@*/node_modules/node-pty/build native/windows-registry/build node_modules/.pnpm/@vscode+windows-process-tre*/node_modules/@vscode/windows-process-tree/build - key: native-modules-${{ runner.os }}-${{ steps.deps.outputs.native-cache-scope }}-${{ runner.arch }}-node-node${{ steps.deps.outputs.node-version }}-${{ hashFiles('pnpm-lock.yaml', '.github/actions/install-node-dependencies/action.yml', 'config/scripts/ensure-native-runtime.mjs', 'config/scripts/rebuild-native-deps.mjs', 'config/patches/node-pty@1.1.0.patch', 'config/patches/@vscode__windows-process-tree@0.8.0.patch', 'native/windows-registry/src/addon.cc', 'native/windows-registry/binding.gyp', 'native/windows-registry/package.json') }} + key: native-modules-${{ runner.os }}-${{ steps.deps.outputs.native-cache-scope }}-${{ runner.arch }}-node-node${{ steps.deps.outputs.node-version }}-${{ hashFiles('pnpm-lock.yaml', '.github/actions/install-node-dependencies/action.yml', 'config/scripts/ensure-native-runtime.mjs', 'config/scripts/rebuild-native-deps.mjs', 'config/patches/@vscode__windows-process-tree@0.8.0.patch', 'native/windows-registry/src/addon.cc', 'native/windows-registry/binding.gyp', 'native/windows-registry/package.json') }} # Keep cold PowerShell probes out of concurrent native and renderer tests. - name: Test installer PowerShell capability run: >- - pnpm exec vitest run --config config/vitest.config.ts + pnpm exec vitest run --config config/vitest.config.ts --fileParallelism=false config/scripts/nsis-process-check.test.mjs + config/scripts/nsis-bun-terminal-uninstall.test.mjs - name: Prepare pinned Bun for CLI boundary tests run: | @@ -985,14 +988,14 @@ jobs: # vitest runs here directly rather than through `pnpm test`, so the addon # assertions only hold once install-node-dependencies has rebuilt natives. + - name: Prepare pinned Bun for real terminal tests + run: node config/scripts/build-orcad-bun.mjs --runtime-only --out-dir out/orcad + - name: Test Windows-specific boundaries run: >- pnpm exec vitest run --config config/vitest.config.ts config/scripts/rebuild-native-deps.test.mjs config/scripts/rebuild-native-deps-windows-process-tree.test.mjs - config/scripts/rebuild-native-deps-node-pty.test.mjs - config/scripts/ensure-native-runtime-job-ownership.test.mjs - config/scripts/verify-packaged-node-pty-job-ownership.test.mjs config/scripts/windows-pe-machine.test.mjs config/scripts/script-module-dependencies.test.mjs src/main/windows-registry-addon.test.ts @@ -1005,8 +1008,8 @@ jobs: src/main/browser/browser-route-webrtc-egress.electron.test.ts src/main/browser/browser-route-h3-egress.electron.test.ts src/main/browser/browser-route-dns-prefetch.electron.test.ts - src/main/providers/windows-conpty-wide-char-duplication.node-pty.test.ts - src/main/providers/pty-repaint-wide-char-buffer.node-pty.test.ts + src/main/providers/windows-conpty-wide-char-duplication.bun.test.ts + src/main/providers/pty-repaint-wide-char-buffer.bun.test.ts src/shared/child-process/windows-command-line.win32.test.ts src/shared/child-process/windows-cmd-shim-resolution.test.ts src/shared/child-process/windows-cmd-shim-resolution.win32.test.ts @@ -1015,6 +1018,7 @@ jobs: src/main/codex/windows-hook-command.test.ts src/main/codex/windows-hook-upgrade.test.ts src/main/windows/windows-pty-job.win32.test.ts + src/main/daemon/bun-pty-windows-io-failure.win32.test.ts src/main/windows/windows-msys-job.win32.test.ts src/main/providers/agent-foreground-process-git-bash.win32.test.ts src/main/windows/windows-host-job.win32.test.ts @@ -1060,10 +1064,9 @@ jobs: uses: actions/cache@v5 with: path: | - node_modules/.pnpm/node-pty@*/node_modules/node-pty/build native/windows-registry/build node_modules/.pnpm/@vscode+windows-process-tre*/node_modules/@vscode/windows-process-tree/build - key: native-modules-${{ runner.os }}-${{ steps.deps.outputs.native-cache-scope }}-${{ runner.arch }}-electron-node${{ steps.deps.outputs.node-version }}-${{ hashFiles('pnpm-lock.yaml', '.github/actions/install-node-dependencies/action.yml', 'config/scripts/ensure-native-runtime.mjs', 'config/scripts/rebuild-native-deps.mjs', 'config/patches/node-pty@1.1.0.patch', 'config/patches/@vscode__windows-process-tree@0.8.0.patch', 'native/windows-registry/src/addon.cc', 'native/windows-registry/binding.gyp', 'native/windows-registry/package.json') }} + key: native-modules-${{ runner.os }}-${{ steps.deps.outputs.native-cache-scope }}-${{ runner.arch }}-electron-node${{ steps.deps.outputs.node-version }}-${{ hashFiles('pnpm-lock.yaml', '.github/actions/install-node-dependencies/action.yml', 'config/scripts/ensure-native-runtime.mjs', 'config/scripts/rebuild-native-deps.mjs', 'config/patches/@vscode__windows-process-tree@0.8.0.patch', 'native/windows-registry/src/addon.cc', 'native/windows-registry/binding.gyp', 'native/windows-registry/package.json') }} - name: Prepare Electron native runtime run: node config/scripts/ensure-native-runtime.mjs --runtime=electron diff --git a/.github/workflows/release-cut.yml b/.github/workflows/release-cut.yml index 2bd95518e09c..e080112ad3f9 100644 --- a/.github/workflows/release-cut.yml +++ b/.github/workflows/release-cut.yml @@ -1506,7 +1506,7 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Load packaged node-pty on the Linux floor + - name: Qualify packaged Bun terminal on the Linux floor if: matrix.platform == 'linux-x64' || matrix.platform == 'linux-arm64' uses: nick-fields/retry@v4 with: @@ -1514,7 +1514,7 @@ jobs: max_attempts: 3 retry_wait_seconds: 30 command: >- - node config/scripts/run-linux-packaged-node-pty-floor-smoke.mjs + node config/scripts/run-linux-packaged-terminal-floor-smoke.mjs --app-dir ${{ matrix.unpacked_dir }} # Why: SignPath signs GitHub workflow artifacts, so Windows builds must @@ -1541,22 +1541,12 @@ jobs: # app.asar - the exact defect this chain exists to remove. ORCA_WIN_UNINSTALLER_EXPORT_PATH: ${{ runner.temp }}\uninstaller-signing\unsigned\orca-uninstaller.exe - - name: Verify Windows node-pty ConPTY runtime + - name: Verify Windows Bun and ConPTY runtime if: matrix.platform == 'win' && github.run_attempt == 1 shell: pwsh run: | - $runtimeDir = 'dist/win-unpacked/resources/node_modules/node-pty/build/Release' - $requiredFiles = @( - "$runtimeDir/conpty.node", - "$runtimeDir/conpty/conpty.dll", - "$runtimeDir/conpty/OpenConsole.exe" - ) - foreach ($file in $requiredFiles) { - if (-not (Test-Path -LiteralPath $file -PathType Leaf)) { - throw "Missing Windows node-pty runtime file: $file" - } - Get-Item -LiteralPath $file - } + node -e "require('./config/bundled-cli-runtime.cjs').verifyCliRuntimeDirectory('dist/win-unpacked/resources/cli-runtime', 'win32', 'x64')" + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - name: Install SignPath PowerShell module if: matrix.platform == 'win' && github.run_attempt == 1 @@ -1564,7 +1554,7 @@ jobs: # ── Windows inner-binary signing (issue #7785) ───────────────────── # Why: SignPath cannot deep-sign inside NSIS installers, so inner PE - # files (Orca.exe, node-pty *.node, DLLs) are signed via a separate zip + # files (Orca.exe, Bun, native addons, DLLs) are signed via a separate zip # request, then the installer is rebuilt from the signed tree before the # existing installer signing request below. The NSIS uninstaller rides # this same request (it is the MDE update cluster: old-uninstaller.exe / @@ -1606,8 +1596,8 @@ jobs: if (-not ($list -contains 'Orca.exe')) { throw 'Orca.exe was not staged for signing; unpacked layout changed?' } - if (-not ($list | Where-Object { $_ -like '*conpty_console_list.node' })) { - throw 'node-pty conpty_console_list.node was not staged; this is the file from issue #7785.' + if (-not ($list | Where-Object { $_ -like '*windows_process_tree.node' })) { + throw 'Windows process inspection addon was not staged for signing.' } Set-Content -Path 'inner-signing-list.txt' -Value ($list -join "`n") @@ -1789,6 +1779,8 @@ jobs: $failures | ForEach-Object { Write-Host "::error::$_" } throw "Signed inner artifact did not round-trip cleanly ($($failures.Count) failures)." } + node config/scripts/finalize-signed-cli-runtime.mjs $root 'signing-stage' + if ($LASTEXITCODE -ne 0) { throw 'Signed runtime identity finalization failed.' } # Why gated separately from the inner restore above: if SignPath's # windows-inner-binaries-zip artifact configuration does not (yet) cover the diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index ceb1485e237e..a209ca6ea860 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -52,6 +52,9 @@ jobs: - name: Install Electron package binary for tests run: node config/scripts/install-electron-package-binary.mjs + - name: Prepare pinned Bun for real terminal tests + run: node config/scripts/build-orcad-bun.mjs --runtime-only --out-dir out/orcad + - name: Test shard env: ORCA_BALANCE_UNIT_SHARDS: '1' @@ -61,19 +64,19 @@ jobs: pnpm exec vitest run --config config/vitest.config.ts \ --exclude=src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec.test.ts \ --exclude=src/main/daemon/shell-ready.test.ts \ - --exclude=src/main/daemon/node-pty-fd-leak.test.ts \ + --exclude=src/main/daemon/bun-pty-fd-lifecycle.integration.test.ts \ --exclude=src/main/providers/local-pty-shell-ready-zsh-launch-environment.test.ts \ --exclude=src/main/providers/__tests__/shell-ready-framework-example.test.ts \ --exclude=src/main/pty/omp-shell-wrapper-alias-safety.test.ts \ - --exclude=src/main/pty/omp-shell-wrapper.node-pty.test.ts \ + --exclude=src/main/pty/omp-shell-wrapper.bun.test.ts \ --exclude=src/main/shell-startup-feature-channel.test.ts \ - --exclude=src/main/terminal-history-fish-session.node-pty.test.ts \ + --exclude=src/main/terminal-history-fish-session.bun.test.ts \ --exclude=src/main/zsh-scoped-histfile.live-shell.test.ts \ --exclude=src/main/zsh-startup-hook-user-config-equivalence.live-shell.test.ts \ --exclude=src/main/zsh-wrapper-version-mismatch.live-shell.test.ts \ - --exclude=src/renderer/src/components/terminal-pane/fish-color-scheme-child-stdin.node-pty.test.ts \ - --exclude=src/shared/fish-query-reply-child-stdin.node-pty.test.ts \ - --exclude=src/shared/pty-reply-echo-shapes.node-pty.test.ts \ + --exclude=src/main/daemon/pty-subprocess/bun-tests/fish-color-scheme-child-stdin.bun.test.ts \ + --exclude=src/main/daemon/pty-subprocess/bun-tests/fish-query-reply-child-stdin.bun.test.ts \ + --exclude=src/main/daemon/pty-subprocess/bun-tests/pty-reply-echo-shapes.bun.test.ts \ --exclude=src/shared/startup-shell-portability.live-shell.test.ts \ --exclude=src/shared/posix-command-path-lookup.test.ts \ --exclude=tests/e2e/relay-region-compatibility.unit.test.ts \ diff --git a/.github/workflows/windows-signing-rehearsal.yml b/.github/workflows/windows-signing-rehearsal.yml index a5d194c1113c..7ad43f376f5f 100644 --- a/.github/workflows/windows-signing-rehearsal.yml +++ b/.github/workflows/windows-signing-rehearsal.yml @@ -1,7 +1,7 @@ # Windows inner-binary signing rehearsal. # # Why: SignPath cannot deep-sign inside NSIS installers, so shipping signed -# inner binaries (Orca.exe, node-pty *.node, DLLs — see issue #7785) requires +# inner binaries (Orca.exe, Bun, native addons, DLLs — see issue #7785) requires # a two-request flow: sign the unpacked PE files first, then build the NSIS # installer from the signed tree, then sign the installer. The NSIS uninstaller # rides that same first request — it is captured through electron-builder's sign @@ -164,8 +164,8 @@ jobs: if (-not ($list -contains 'Orca.exe')) { throw 'Orca.exe was not staged for signing; unpacked layout changed?' } - if (-not ($list | Where-Object { $_ -like '*conpty_console_list.node' })) { - throw 'node-pty conpty_console_list.node was not staged; this is the file from issue #7785.' + if (-not ($list | Where-Object { $_ -like '*windows_process_tree.node' })) { + throw 'Windows process inspection addon was not staged for signing.' } Set-Content -Path 'inner-signing-list.txt' -Value ($list -join "`n") @@ -252,6 +252,8 @@ jobs: $failures | ForEach-Object { Write-Host "::error::$_" } throw "Signed inner artifact did not round-trip cleanly ($($failures.Count) failures)." } + node config/scripts/finalize-signed-cli-runtime.mjs $root 'signing-stage' + if ($LASTEXITCODE -ne 0) { throw 'Signed runtime identity finalization failed.' } - name: Restore signed uninstaller for the installer rebuild shell: pwsh diff --git a/config/bundled-cli-runtime.cjs b/config/bundled-cli-runtime.cjs index 2c31878d8523..04bfea963317 100644 --- a/config/bundled-cli-runtime.cjs +++ b/config/bundled-cli-runtime.cjs @@ -18,7 +18,12 @@ function cliRuntimeExtraResource(platform) { return { from: `out/cli-runtime/${platform}-\${arch}`, to: 'cli-runtime', - filter: [cliRuntimeFilename(platform), 'runtime.json', 'LICENSE.md'] + filter: [ + cliRuntimeFilename(platform), + 'runtime.json', + 'LICENSE.md', + ...(platform === 'win32' ? ['conpty/**'] : []) + ] } } @@ -40,12 +45,52 @@ function verifyCliRuntimeDirectory(directory, platform, arch) { if (!license.equals(expectedLicense)) { throw new Error(`CLI runtime license notice mismatch for ${expectedTarget}`) } + if (platform === 'win32') { + verifyWindowsConptyDirectory(join(directory, 'conpty'), arch) + } const binary = readFileSync(join(directory, cliRuntimeFilename(platform))) if (createHash('sha256').update(binary).digest('hex') !== manifest.sha256) { throw new Error(`CLI runtime checksum mismatch for ${expectedTarget}`) } } +function verifyWindowsConptyDirectory(directory, arch) { + const { + WINDOWS_CONPTY_VERSION, + WINDOWS_CONPTY_ARCHIVE, + WINDOWS_CONPTY_FILES + } = require('../src/shared/windows-conpty-release.ts') + const files = WINDOWS_CONPTY_FILES[arch] + if (!files) { + throw new Error(`Unsupported ConPTY architecture: ${arch}`) + } + const manifest = JSON.parse(readFileSync(join(directory, 'conpty.json'), 'utf8')) + if ( + manifest.version !== WINDOWS_CONPTY_VERSION || + manifest.arch !== arch || + manifest.source !== WINDOWS_CONPTY_ARCHIVE.url || + manifest.archiveSha256 !== WINDOWS_CONPTY_ARCHIVE.sha256 + ) { + throw new Error(`Invalid ConPTY manifest for ${arch}`) + } + for (const [filename, expected] of Object.entries(files)) { + const actual = createHash('sha256') + .update(readFileSync(join(directory, filename))) + .digest('hex') + if (manifest.files?.[filename] !== expected || actual !== expected) { + throw new Error(`ConPTY checksum mismatch: ${arch}/${filename}`) + } + } + const license = readFileSync(join(directory, 'LICENSE.txt')) + if (!license.equals(readFileSync(join(__dirname, 'licenses', 'windows-conpty-LICENSE.txt')))) { + throw new Error('ConPTY license notice mismatch') + } + const nuspec = readFileSync(join(directory, 'Microsoft.Windows.Console.ConPTY.nuspec')) + if (createHash('sha256').update(nuspec).digest('hex') !== WINDOWS_CONPTY_ARCHIVE.nuspecSha256) { + throw new Error('ConPTY package metadata checksum mismatch') + } +} + function assertBundledCliRuntimeBuilt(platform, archEnum, projectDir = join(__dirname, '..')) { const arch = { 1: 'x64', 3: 'arm64' }[archEnum] const target = cliRuntimeTarget(platform, arch) @@ -68,5 +113,6 @@ module.exports = { cliRuntimeFilename, cliRuntimeExtraResource, verifyCliRuntimeDirectory, + verifyWindowsConptyDirectory, assertBundledCliRuntimeBuilt } diff --git a/config/docker/daemon-shutdown-descendants/Dockerfile b/config/docker/daemon-shutdown-descendants/Dockerfile index b012d5738e3d..ecedf97749d3 100644 --- a/config/docker/daemon-shutdown-descendants/Dockerfile +++ b/config/docker/daemon-shutdown-descendants/Dockerfile @@ -1,13 +1,13 @@ -FROM node:22-bookworm-slim +FROM debian:bookworm-slim ENV DEBIAN_FRONTEND=noninteractive -ENV NODE_PATH=/usr/local/lib/node_modules +ENV ORCA_BACKGROUND_LAUNCH=1 RUN apt-get update \ - && apt-get install -y --no-install-recommends build-essential python3 procps util-linux \ - && rm -rf /var/lib/apt/lists/* \ - && npm install --global --no-fund --no-audit node-pty@1.1.0 + && apt-get install -y --no-install-recommends libstdc++6 procps util-linux \ + && rm -rf /var/lib/apt/lists/* +COPY --chmod=755 bun-runtime /usr/local/bin/bun-runtime COPY run-case.sh /usr/local/bin/run-daemon-shutdown-descendants COPY fixture.cjs /opt/daemon-shutdown-descendants/fixture.cjs diff --git a/config/docker/daemon-shutdown-descendants/README.md b/config/docker/daemon-shutdown-descendants/README.md index 80a24a8f64e2..29761520a70e 100644 --- a/config/docker/daemon-shutdown-descendants/README.md +++ b/config/docker/daemon-shutdown-descendants/README.md @@ -6,7 +6,7 @@ Run from the repository root with Docker running: ORCA_BACKGROUND_LAUNCH=1 node config/scripts/run-daemon-shutdown-descendants-docker.mjs ``` -The runner bundles the production `TerminalHost` and native PTY wrapper from the working tree. By default it runs the candidate only. Pass `--baseline ` to add a red/green run; the baseline loads the five teardown modules from that ref without changing the checkout. Both run in Linux with `node-pty` 1.1.0. +The runner bundles the production `TerminalHost` and native PTY wrapper from the working tree. By default it runs the candidate only. Pass `--baseline ` to add a red/green run; the baseline loads the five teardown modules from that ref without changing the checkout. Both run in Linux with the pinned bundled Bun runtime and the current Bun PTY backend. The baseline changes teardown logic only; this is not a comparison between terminal engines. A live PTY shell owns a child that called `setsid` and ignores `SIGTERM`. The fixture verifies those relationships, calls `host.dispose()`, and immediately exits its process. A separate supervisor checks that the child has stopped, an unrelated canary is still running, and shutdown finished within the daemon's five-second limit. The baseline must leave the child running; the candidate must stop it. diff --git a/config/docker/daemon-shutdown-descendants/bundle-entry.ts b/config/docker/daemon-shutdown-descendants/bundle-entry.ts index 14a9d7026078..a132632148ea 100644 --- a/config/docker/daemon-shutdown-descendants/bundle-entry.ts +++ b/config/docker/daemon-shutdown-descendants/bundle-entry.ts @@ -1,2 +1,4 @@ export { TerminalHost } from '../../../src/main/daemon/terminal-host' export { createDaemonPtySubprocessHandle } from '../../../src/main/daemon/pty-subprocess/subprocess-handle' +export { spawnBunPty } from '../../../src/main/daemon/pty-subprocess/bun-pty-process' +export { spawnProcess } from '../../../src/shared/child-process/run-process' diff --git a/config/docker/daemon-shutdown-descendants/fixture.cjs b/config/docker/daemon-shutdown-descendants/fixture.cjs index 409d21d20e55..0c9752a3d766 100644 --- a/config/docker/daemon-shutdown-descendants/fixture.cjs +++ b/config/docker/daemon-shutdown-descendants/fixture.cjs @@ -1,20 +1,24 @@ const fs = require('node:fs') -const { spawn } = require('node:child_process') -const pty = require('node-pty') const bundle = process.argv[2] const resultPath = process.argv[3] async function main() { - const { TerminalHost, createDaemonPtySubprocessHandle } = require(bundle) + const { TerminalHost, createDaemonPtySubprocessHandle, spawnBunPty, spawnProcess } = require( + bundle + ) let output = '' - const canary = spawn('/bin/sh', ['-c', 'while :; do sleep 1; done'], { + const canary = spawnProcess({ + program: '/bin/sh', + args: ['-c', 'while :; do sleep 1; done'], stdio: 'ignore' }) canary.unref() const childScript = 'setsid /bin/sh -c \'trap "" TERM; while :; do sleep 1; done\' & echo ORCA_DESCENDANT:$!; while :; do sleep 1; done' - const native = pty.spawn('/bin/sh', ['-c', childScript], { + const native = spawnBunPty({ + file: '/bin/sh', + args: ['-c', childScript], cols: 80, rows: 24, cwd: '/tmp', diff --git a/config/docker/daemon-shutdown-descendants/run-case.sh b/config/docker/daemon-shutdown-descendants/run-case.sh index a8ce8414d745..0278d1d90dea 100755 --- a/config/docker/daemon-shutdown-descendants/run-case.sh +++ b/config/docker/daemon-shutdown-descendants/run-case.sh @@ -3,16 +3,17 @@ set -eu bundle=${1:?bundle path is required} mode=${2:?mode is required} -result_dir=/tmp/daemon-shutdown-descendants -mkdir -p "$result_dir" +fixture=${3:-/opt/daemon-shutdown-descendants/fixture.cjs} +result_dir=$(mktemp -d /tmp/daemon-shutdown-descendants.XXXXXX) +trap 'rm -rf "$result_dir"' EXIT # The fixture exits like daemon-entry after dispose(). This shell remains as a # supervisor so a detached descendant can be inspected after the daemon exits. -timeout --kill-after=2s 20s node /opt/daemon-shutdown-descendants/fixture.cjs "$bundle" "$result_dir/fixture.json" +timeout --kill-after=2s 20s bun-runtime --no-env-file "$fixture" "$bundle" "$result_dir/fixture.json" -child_pid=$(node -e "const r=require(process.argv[1]); process.stdout.write(String(r.childPid))" "$result_dir/fixture.json") -canary_pid=$(node -e "const r=require(process.argv[1]); process.stdout.write(String(r.canaryPid))" "$result_dir/fixture.json") -shutdown_ms=$(node -e "const r=require(process.argv[1]); process.stdout.write(String(r.shutdownMs))" "$result_dir/fixture.json") +child_pid=$(bun-runtime --no-env-file -e "const r=require(process.argv[1]); process.stdout.write(String(r.childPid))" "$result_dir/fixture.json") +canary_pid=$(bun-runtime --no-env-file -e "const r=require(process.argv[1]); process.stdout.write(String(r.canaryPid))" "$result_dir/fixture.json") +shutdown_ms=$(bun-runtime --no-env-file -e "const r=require(process.argv[1]); process.stdout.write(String(r.shutdownMs))" "$result_dir/fixture.json") process_state() { awk '{ print $3 }' "/proc/$1/stat" 2>/dev/null || true diff --git a/config/docker/headless-serve-shutdown/run-appimage-desktop-startup-case.sh b/config/docker/headless-serve-shutdown/run-appimage-desktop-startup-case.sh index 59a6bef0e5cc..4cd873faa020 100755 --- a/config/docker/headless-serve-shutdown/run-appimage-desktop-startup-case.sh +++ b/config/docker/headless-serve-shutdown/run-appimage-desktop-startup-case.sh @@ -47,7 +47,13 @@ declare -A tree_start_ticks=() read_start_ticks() { local pid=$1 [[ -r "/proc/$pid/stat" ]] || return 1 - awk '{print $22}' "/proc/$pid/stat" + local stat + local -a fields + stat=$(<"/proc/$pid/stat") || return 1 + # comm is parenthesized and may contain spaces or parentheses. + read -r -a fields <<< "${stat##*) }" + ((${#fields[@]} >= 20)) || return 1 + printf '%s\n' "${fields[19]}" } identity_alive() { @@ -55,7 +61,7 @@ identity_alive() { local expected_ticks=$2 [[ -n "$expected_ticks" ]] || return 1 [[ -r "/proc/$pid/stat" ]] || return 1 - [[ $(awk '{print $22}' "/proc/$pid/stat" 2>/dev/null || true) == "$expected_ticks" ]] || return 1 + [[ $(read_start_ticks "$pid" 2>/dev/null || true) == "$expected_ticks" ]] || return 1 local process_state process_state=$(ps -o stat= -p "$pid" 2>/dev/null | tr -d '[:space:]' || true) [[ -n "$process_state" && "$process_state" != Z* ]] @@ -85,6 +91,27 @@ collect_process_tree() { done } +# Detached services can outlive every ancestor visible in the startup tree. +collect_state_processes() { + local signal=$1 stat_file pid ticks + for stat_file in /proc/[0-9]*/stat; do + pid=${stat_file#/proc/} + pid=${pid%/stat} + [[ "$pid" != "$$" && -O "$stat_file" ]] || continue + ticks=$(read_start_ticks "$pid" 2>/dev/null || true) + [[ -n "$ticks" ]] || continue + if ! grep -zFxq -- "HOME=$state_dir/home" "/proc/$pid/environ" 2>/dev/null; then + continue + fi + identity_alive "$pid" "$ticks" || continue + if [[ "${tree_start_ticks[$pid]-}" != "$ticks" ]]; then + tree_pids+=("$pid") + tree_start_ticks["$pid"]=$ticks + kill -s "$signal" "$pid" 2>/dev/null || true + fi + done +} + process_is_xvfb() { local pid=$1 local command_name @@ -117,9 +144,12 @@ signal_owned_processes() { wait_for_owned_exit() { local timeout_seconds=$1 + local signal=$2 + local empty_passes=0 local deadline=$((SECONDS + timeout_seconds)) local pid ticks alive while ((SECONDS < deadline)); do + collect_state_processes "$signal" alive=0 for pid in "${tree_pids[@]}"; do ticks=${tree_start_ticks[$pid]-} @@ -129,7 +159,10 @@ wait_for_owned_exit() { fi done if ((alive == 0)); then - return 0 + empty_passes=$((empty_passes + 1)) + ((empty_passes < 2)) || return 0 + else + empty_passes=0 fi sleep 0.2 done @@ -179,11 +212,12 @@ cleanup() { local status=$? trap - EXIT signal_process_group TERM || true + collect_state_processes TERM signal_owned_processes TERM || true - if ! wait_for_owned_exit 10; then + if ! wait_for_owned_exit 10 TERM; then signal_process_group KILL || true signal_owned_processes KILL || true - wait_for_owned_exit 5 || status=1 + wait_for_owned_exit 5 KILL || status=1 fi capture_launcher_status || true if ((status != 0)); then diff --git a/config/docker/headless-serve-shutdown/run-signal-case.sh b/config/docker/headless-serve-shutdown/run-signal-case.sh index 2d561629170c..bfe948cce4d4 100755 --- a/config/docker/headless-serve-shutdown/run-signal-case.sh +++ b/config/docker/headless-serve-shutdown/run-signal-case.sh @@ -22,6 +22,9 @@ esac state_dir=$(mktemp -d "/tmp/orca-shutdown-${signal_name}.XXXXXX") stdout_log="$state_dir/stdout.log" stderr_log="$state_dir/stderr.log" +# The parent can poll before the background shell opens its redirections. +: >"$stdout_log" +: >"$stderr_log" ulimit -c 0 sleep 300 & diff --git a/config/electron-builder.config.cjs b/config/electron-builder.config.cjs index 68c4bc9a99f9..c44b1cf6e215 100644 --- a/config/electron-builder.config.cjs +++ b/config/electron-builder.config.cjs @@ -30,9 +30,6 @@ const { bundledRipgrepMacSignIgnore, finalizePackagedRipgrep } = require('./bundled-ripgrep-resources.cjs') -const { - verifyPackagedWindowsNodePty -} = require('./scripts/verify-packaged-node-pty-job-ownership.cjs') const { verifySkillsCliRuntime } = require('./scripts/verify-skills-cli-runtime.cjs') const { verifyStaticAppImagePackage } = require('./scripts/static-appimage-package-contract.cjs') const { signWindowsUninstallerViaSignPath } = require('./scripts/windows-uninstaller-signing.cjs') @@ -116,6 +113,11 @@ const emojiShortcodeDatasetResource = { } const commonExtraResources = [ relayExtraResource, + { + from: 'out/terminal-daemon', + to: 'terminal-daemon', + filter: ['daemon-entry.js', 'windows-bun-pty-gate-entry.js'] + }, ...bundledRipgrepExtraResources, bundledPluginResources, skillFreshnessResources, @@ -192,10 +194,12 @@ module.exports = { '!**/.vscode/*', // Why: these repo-only inputs are either bundled into out/ or copied via // extraResources. Shipping them in app.asar bloats the desktop bundle. + '!.build{,/**/*}', '!src{,/**/*}', '!out/orcad{,/**/*}', '!out/orcad-template{,/**/*}', '!out/cli-runtime{,/**/*}', + '!out/terminal-daemon{,/**/*}', '!out/.orcad-*{,/**/*}', '!config{,/**/*}', '!docs{,/**/*}', @@ -318,12 +322,32 @@ module.exports = { assertPackagedNativeVariantsInstalled(context.electronPlatformName, context.arch) assertBundledRipgrepInstalled() assertMobileWebBundleBuilt(mobileWebBundleDir) + assertPackagedDaemonEntryExists( + join(context.packager?.projectDir ?? join(__dirname, '..'), 'out'), + context.electronPlatformName + ) assertBundledCliRuntimeBuilt( context.electronPlatformName, context.arch, context.packager?.projectDir ) }, + afterSign: (context) => { + if (context.electronPlatformName !== 'darwin') { + return + } + verifyCliRuntimeDirectory( + join( + context.appOutDir, + `${context.packager.appInfo.productFilename}.app`, + 'Contents', + 'Resources', + 'cli-runtime' + ), + 'darwin', + { 1: 'x64', 3: 'arm64' }[context.arch] + ) + }, afterPack: async (context) => { const resourcesDir = context.electronPlatformName === 'darwin' @@ -381,17 +405,10 @@ module.exports = { }) } verifyPackagedMainRuntimeDeps(resourcesDir) - // Why: boot the packaged daemon-entry under plain Node, but only for the - // slice matching the packaging host's arch — daemon-entry.js is JS, yet it - // require()s the native (N-API) node-pty for the TARGET arch, which the host - // Node cannot load cross-arch. `Arch` enum: ia32=0, x64=1, armv7l=2, - // arm64=3, universal=4 (universal contains the host slice, so run it). + // The bundled runtime and native dependencies can only boot on the matching host architecture. const archEnumByNodeArch = { ia32: 0, x64: 1, armv7l: 2, arm64: 3 } const hostArchEnum = archEnumByNodeArch[process.arch] const canExecuteTargetArch = context.arch === hostArchEnum || context.arch === 4 - if (context.electronPlatformName === 'win32') { - verifyPackagedWindowsNodePty(resourcesDir, context.arch, { canExecuteTargetArch }) - } verifySkillsCliRuntime(join(resourcesDir, 'app.asar.unpacked', 'out'), resourcesDir, { executeCommands: canExecuteTargetArch }) @@ -401,12 +418,10 @@ module.exports = { ) } if (canExecuteTargetArch) { - verifyPackagedDaemonEntryBoots(resourcesDir) + verifyPackagedDaemonEntryBoots(resourcesDir, { platform: context.electronPlatformName }) } else { - // Why: a cross-arch slice can't be booted by the host Node, but the - // unpacked entry must still exist — its absence is a layout regression - // regardless of arch, so only the boot is skipped, not the check. - assertPackagedDaemonEntryExists(resourcesDir) + // Cross-architecture packages still require the complete daemon entry layout. + assertPackagedDaemonEntryExists(resourcesDir, context.electronPlatformName) console.log( `[verify-packaged-daemon-entry] skipped boot on cross-arch slice (target ${context.arch}, host ${process.arch})` ) @@ -514,7 +529,8 @@ module.exports = { icon: 'resources/build/icon.icns', entitlements: 'resources/build/entitlements.mac.plist', entitlementsInherit: 'resources/build/entitlements.mac.plist', - signIgnore: bundledRipgrepMacSignIgnore, + // Keep Bun's upstream Developer ID signature and pinned executable bytes. + signIgnore: [...bundledRipgrepMacSignIgnore, '/cli-runtime/bun-runtime$'], extendInfo: { NSAppleEventsUsageDescription: 'Orca allows terminal-launched developer tools to automate local apps when you request it.', @@ -686,12 +702,7 @@ module.exports = { afterRemove: 'resources/linux/packaging/after-remove.sh' }, beforeBuild: electronBuilderNativeRebuild, - // Why: must be true so that electron-builder rebuilds native modules - // (node-pty) for each target architecture when producing dual-arch macOS - // builds (x64 + arm64). With npmRebuild disabled, CI on an arm64 runner - // packages arm64 binaries into the x64 DMG, causing "posix_spawnp failed" - // on Intel Macs. The beforeBuild hook performs Orca's targeted rebuild and - // returns false so electron-builder does not rebuild optional cpu-features. + // beforeBuild performs the target-architecture rebuild and skips optional cpu-features. npmRebuild: true, publish: { provider: 'github', diff --git a/config/licenses/windows-conpty-LICENSE.txt b/config/licenses/windows-conpty-LICENSE.txt new file mode 100644 index 000000000000..8cb179cdb694 --- /dev/null +++ b/config/licenses/windows-conpty-LICENSE.txt @@ -0,0 +1,21 @@ +Copyright (c) Microsoft Corporation. All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/config/nsis/orca-bun-terminal-uninstall.nsh b/config/nsis/orca-bun-terminal-uninstall.nsh new file mode 100644 index 000000000000..933efe3d3e27 --- /dev/null +++ b/config/nsis/orca-bun-terminal-uninstall.nsh @@ -0,0 +1,12 @@ +; This root is outside legacy profile-scoped pruning; runtime copies retain their shipped name. +!macro ORCA_UNINSTALL_BUN_TERMINAL_HOST + ; Query and pin each process handle before termination; uncertainty retains the runtime files. + StrCpy $0 "$SYSDIR\WindowsPowerShell\v1.0\powershell.exe" + IfFileExists "$WINDIR\Sysnative\WindowsPowerShell\v1.0\powershell.exe" 0 +2 + StrCpy $0 "$WINDIR\Sysnative\WindowsPowerShell\v1.0\powershell.exe" + nsExec::Exec /TIMEOUT=15000 `"$0" -NoProfile -NonInteractive -Command "$$ErrorActionPreference='Stop';try{if(!$$env:LOCALAPPDATA){throw 1};$$r=[IO.Path]::Combine($$env:LOCALAPPDATA,'Orca','terminal-daemon-host')+'\';$$s=[Security.Principal.WindowsIdentity]::GetCurrent().User.Value;$$owned=@(Get-CimInstance Win32_Process|? Name -eq 'bun-runtime.exe'|%{if(!$$_.ExecutablePath){throw 1};if($$_.ExecutablePath.StartsWith($$r,[StringComparison]::OrdinalIgnoreCase)){if((Invoke-CimMethod -InputObject $$_ -MethodName GetOwnerSid).Sid -ne $$s){throw 1};$$p=Get-Process -Id $$_.ProcessId;$$null=$$p.Handle;if($$p.StartTime.ToUniversalTime().ToString('yyyyMMddHHmmssffffff') -ne $$_.CreationDate.ToUniversalTime().ToString('yyyyMMddHHmmssffffff') -or !$$p.Path.StartsWith($$r,[StringComparison]::OrdinalIgnoreCase)){throw 1};$$p}});foreach($$p in $$owned){if(!$$p.HasExited){$$p.Kill();if(!$$p.WaitForExit(5000)){throw 1}};$$p.Dispose()};exit 0}catch{exit 1}"` + Pop $0 + ${if} $0 == 0 + RMDir /r "$LOCALAPPDATA\Orca\terminal-daemon-host" + ${endIf} +!macroend diff --git a/config/nsis/orca-installer-hooks.nsh b/config/nsis/orca-installer-hooks.nsh index a6fb31f9aec9..ab4c1660253a 100644 --- a/config/nsis/orca-installer-hooks.nsh +++ b/config/nsis/orca-installer-hooks.nsh @@ -4,6 +4,7 @@ ; customUnInstall hook Orca needs lives here. !include "${__FILEDIR__}\orca-process-check.nsh" +!include "${__FILEDIR__}\orca-bun-terminal-uninstall.nsh" ; --------------------------------------------------------------------------- ; Markdown "Open with Orca" (issue #10138) @@ -90,6 +91,7 @@ Pop $0 nsExec::Exec 'taskkill /F /IM "orca-terminal-daemon.exe" $2' Pop $0 + !insertmacro ORCA_UNINSTALL_BUN_TERMINAL_HOST Pop $2 Pop $1 Pop $0 diff --git a/config/oxlint-anti-slop.json b/config/oxlint-anti-slop.json index 4728c38a7e9d..434ddd365f04 100644 --- a/config/oxlint-anti-slop.json +++ b/config/oxlint-anti-slop.json @@ -47,8 +47,8 @@ "overrides": [ { "files": [ - "**/*.test.{ts,tsx}", - "**/*.spec.{ts,tsx}", + "**/*.test.{ts,tsx,mjs}", + "**/*.spec.{ts,tsx,mjs}", "tests/**/*.{ts,tsx}", "**/__mocks__/**" ], @@ -56,11 +56,10 @@ "anti-slop/no-module-mocking": "off" } }, - // mock-descendant-sweep.ts (daemon and relay) is a test-only side-effect shim: its whole body - // is one vi.mock that keeps mock PTY PIDs away from the host process table, and it exists so - // 60 suites do not each inline the same hoisted factory. It is never imported by product code. + // These test-only shims keep fake PTYs away from native runtime calls. + // Product code never imports them. { - "files": ["**/mock-descendant-sweep.ts"], + "files": ["**/mock-descendant-sweep.ts", "**/pty-ipc-native-runtime-mock.ts"], "rules": { "anti-slop/no-module-mocking": "off" } diff --git a/config/packaged-runtime-node-modules.cjs b/config/packaged-runtime-node-modules.cjs index c88d70350eb0..e569bd04a752 100644 --- a/config/packaged-runtime-node-modules.cjs +++ b/config/packaged-runtime-node-modules.cjs @@ -1,15 +1,6 @@ -const { - copyFileSync, - existsSync, - mkdirSync, - readFileSync, - readdirSync, - realpathSync, - rmSync -} = require('node:fs') +const { existsSync, readFileSync, readdirSync, realpathSync, rmSync } = require('node:fs') const { dirname, join, resolve } = require('node:path') const { builtinModules, createRequire } = require('node:module') -const { PE_MACHINE, readPeMachine } = require('./scripts/windows-pe-machine.cjs') const projectDir = resolve(__dirname, '..') const requireFromProject = createRequire(join(projectDir, 'package.json')) @@ -22,7 +13,6 @@ const PACKAGED_RUNTIME_PACKAGE_ROOTS = [ 'electron-updater', 'i18next', 'jsonc-parser', - 'node-pty', 'posthog-node', 'proper-lockfile', // serve-sim (for CLI JS entry + closure + state/middleware + to make packaged require('serve-sim') + its internal relatives work; mirrors other runtime JS like ws/yaml/zod. Natives/dylibs still via extraResources + the node_modules/serve-sim copy in resources from builder. Client if added too. @@ -39,12 +29,6 @@ const WINDOWS_PACKAGED_RUNTIME_PACKAGE_ROOTS = [ '@orca/windows-registry' ] -const NODE_PTY_PREBUILD_PREFIX_BY_PLATFORM = { - darwin: 'darwin-', - linux: 'linux-', - win32: 'win32-' -} -const NODE_PTY_CONPTY_RUNTIME_FILES = ['conpty.dll', 'OpenConsole.exe'] const PARCEL_WATCHER_PLATFORM_PREFIX_BY_PLATFORM = { darwin: 'watcher-darwin', linux: 'watcher-linux', @@ -282,14 +266,6 @@ function verifyPackagedMainRuntimeDeps(resourcesDir, asar = require('@electron/a } } -function normalizeNodePtyWindowsArch(electronArch) { - const architecture = normalizeElectronArchitecture(electronArch) - if (architecture !== 'x64' && architecture !== 'arm64') { - throw new Error(`Unsupported packaged node-pty Windows architecture: ${architecture}`) - } - return architecture -} - function normalizeElectronArchitecture(electronArch) { const architecture = typeof electronArch === 'number' @@ -303,143 +279,6 @@ function normalizeElectronArchitecture(electronArch) { return architecture } -function pruneNodePtyNativeDirectories(directory, platformPrefix, electronArch, allowsSuffix) { - if (!existsSync(directory)) { - return - } - const architecture = normalizeElectronArchitecture(electronArch) - const targetPrefix = `${platformPrefix}${architecture}` - const platformPrefixes = Object.values(NODE_PTY_PREBUILD_PREFIX_BY_PLATFORM) - for (const entry of readdirSync(directory, { withFileTypes: true })) { - if (!entry.isDirectory() || !platformPrefixes.some((prefix) => entry.name.startsWith(prefix))) { - continue - } - const matchesTarget = - entry.name.startsWith(platformPrefix) && - (entry.name === targetPrefix || (allowsSuffix && entry.name.startsWith(`${targetPrefix}-`))) - if (!matchesTarget) { - rmSync(join(directory, entry.name), { recursive: true, force: true }) - } - } -} - -function findNodePtyConptySourceDir(nodePtyDir, windowsArch) { - const conptyRoot = join(nodePtyDir, 'third_party', 'conpty') - if (!existsSync(conptyRoot)) { - throw new Error(`Packaged node-pty is missing ${conptyRoot}`) - } - for (const entry of readdirSync(conptyRoot, { withFileTypes: true })) { - if (!entry.isDirectory()) { - continue - } - const sourceDir = join(conptyRoot, entry.name, `win10-${windowsArch}`) - if (existsSync(sourceDir)) { - return sourceDir - } - } - throw new Error(`Packaged node-pty has no ConPTY payload for win10-${windowsArch}`) -} - -function ensurePackagedNodePtyConptyRuntime(nodePtyDir, electronArch) { - const releaseDir = join(nodePtyDir, 'build', 'Release') - if (!existsSync(join(releaseDir, 'conpty.node'))) { - return - } - - const runtimeDir = join(releaseDir, 'conpty') - const missingRuntimeFiles = NODE_PTY_CONPTY_RUNTIME_FILES.filter( - (filename) => !existsSync(join(runtimeDir, filename)) - ) - if (missingRuntimeFiles.length === 0) { - return - } - - const windowsArch = normalizeNodePtyWindowsArch(electronArch) - const sourceDir = findNodePtyConptySourceDir(nodePtyDir, windowsArch) - mkdirSync(runtimeDir, { recursive: true }) - for (const filename of missingRuntimeFiles) { - const sourceFile = join(sourceDir, filename) - if (!existsSync(sourceFile)) { - throw new Error(`Packaged node-pty is missing ${sourceFile}`) - } - // Why: node-pty's Windows addon loads conpty.dll relative to conpty.node, - // but its install script can run before electron-builder gathers resources. - copyFileSync(sourceFile, join(runtimeDir, filename)) - } -} - -/** Whether node-pty's source build holds a conpty.node the `electronArch` slice could load. */ -function conptyTargetsArch(nodePtyDir, electronArch) { - const releaseAddon = join(nodePtyDir, 'build', 'Release', 'conpty.node') - if (!existsSync(releaseAddon)) { - return false - } - // Null (not a PE) counts as unloadable, so a truncated or quarantined build keeps the fallback. - return readPeMachine(releaseAddon) === PE_MACHINE[normalizeNodePtyWindowsArch(electronArch)] -} - -function prunePackagedNodePty(resourcesDir, electronPlatformName, electronArch) { - const nodePtyDir = join(resourcesDir, 'node_modules', 'node-pty') - if (!existsSync(nodePtyDir)) { - return - } - - // Why delete only conpty.node: node-pty's loader tries build/Release, then - // build/Debug, then prebuilds/-, swallowing every failure in - // between. Only the source build carries Orca's job-object exports, so an ABI - // mismatch or an AV quarantine of build/Release/conpty.node would silently - // fall through to the UNPATCHED prebuild -- teardown back to guessing by PID - // ancestry, with no error anywhere. - // - // Why NOT the whole prebuilds/ tree: Orca's own patch deletes the - // `conpty_console_list` and winpty `pty` gyp targets, so a Windows source - // build emits conpty.node and nothing else. conpty_console_list.node, - // pty.node, winpty.dll and winpty-agent.exe exist ONLY here. Removing them - // silently kills console-membership probing (the forked agent throws at - // require, and its caller resolves null with silent: true), and removes the - // winpty backend that node-pty still selects below Windows build 18309. - // - // Why the arch check: a cross-HOST package can copy a build/Release that is not a Windows - // binary at all, so its mere presence does not mean the target can load it -- deleting the - // target-arch prebuild would then remove the only loadable binary. This used to approximate - // that with `electronArch === process.arch`, which also skipped the arm64 slice cross-built on - // an x64 Windows host -- a rebuild that DOES emit a correct arm64 addon. That slice kept the - // unpatched prebuild as a reachable fallback for any later load failure of build/Release. - // Read the PE header instead of guessing. - if (electronPlatformName === 'win32' && conptyTargetsArch(nodePtyDir, electronArch)) { - const prebuildDir = join(nodePtyDir, 'prebuilds', `win32-${electronArch}`) - for (const staleFallback of ['conpty.node', 'conpty.pdb']) { - rmSync(join(prebuildDir, staleFallback), { force: true }) - } - } - - const allowedPrebuildPrefix = NODE_PTY_PREBUILD_PREFIX_BY_PLATFORM[electronPlatformName] - if (allowedPrebuildPrefix) { - pruneNodePtyNativeDirectories( - join(nodePtyDir, 'prebuilds'), - allowedPrebuildPrefix, - electronArch, - false - ) - // Why: sequential cross-arch rebuilds accumulate ABI-tagged outputs here. - pruneNodePtyNativeDirectories( - join(nodePtyDir, 'bin'), - allowedPrebuildPrefix, - electronArch, - true - ) - } - - if (electronPlatformName === 'win32') { - ensurePackagedNodePtyConptyRuntime(nodePtyDir, electronArch) - } else { - // Why: conpty is Windows-only and node-pty resolves runtime binaries from - // build/Release or prebuilds/-, not third_party/conpty. - rmSync(join(nodePtyDir, 'third_party', 'conpty'), { recursive: true, force: true }) - rmSync(join(nodePtyDir, 'deps', 'winpty'), { recursive: true, force: true }) - } -} - function prunePackagedParcelWatcher(resourcesDir, electronPlatformName, electronArch) { const parcelDir = join(resourcesDir, 'node_modules', '@parcel') if (!existsSync(parcelDir)) { @@ -593,7 +432,6 @@ function assertPackagedNativeVariantsInstalled(electronPlatformName, electronArc function prunePackagedRuntimeNodeModules(resourcesDir, electronPlatformName, electronArch) { const architecture = normalizeElectronArchitecture(electronArch) - prunePackagedNodePty(resourcesDir, electronPlatformName, architecture) prunePackagedParcelWatcher(resourcesDir, electronPlatformName, architecture) // Why before the filename walk: zod/src is deleted wholesale, so walking it first is wasted work. prunePackagedZodSources(resourcesDir) @@ -618,9 +456,7 @@ module.exports = { createPackagedRuntimeNodeModuleResources, findAsarEntry, isPackagedExternalSpecifier, - normalizeNodePtyWindowsArch, packageNameFromSpecifier, - prunePackagedNodePty, prunePackagedParcelWatcher, prunePackagedRuntimeNodeModules, prunePackagedRuntimeTypeAndSourceMapArtifacts, diff --git a/config/patches/@xterm__headless@6.1.0-beta.302.patch b/config/patches/@xterm__headless@6.1.0-beta.302.patch new file mode 100644 index 000000000000..2c219e5c3838 --- /dev/null +++ b/config/patches/@xterm__headless@6.1.0-beta.302.patch @@ -0,0 +1,44 @@ +diff --git a/lib-headless/xterm-headless.js b/lib-headless/xterm-headless.js +index 5bf9fa6b9d108903fbe65f1e46df4bcaf3fec2bb..b9ac44d16a92f789e19575bb366f57f14371f743 100644 +--- a/lib-headless/xterm-headless.js ++++ b/lib-headless/xterm-headless.js +@@ -1,2 +1,2 @@ +-(()=>{"use strict";var e={132(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.IntervalTimer=t.MicrotaskTimer=t.TimeoutTimer=void 0,t.timeout=function(e){return new Promise(t=>setTimeout(t,e))},t.disposableTimeout=function(e,t=0,i){const r=setTimeout(()=>{e(),i&&n.dispose()},t),n=(0,s.toDisposable)(()=>{clearTimeout(r)});return i?.add(n),n};const s=i(812);t.TimeoutTimer=class{constructor(){this._token=-1,this._isDisposed=!1}dispose(){this.cancel(),this._isDisposed=!0}cancel(){-1!==this._token&&(clearTimeout(this._token),this._token=-1)}cancelAndSet(e,t){if(this._isDisposed)throw new Error("Calling cancelAndSet on a disposed TimeoutTimer");this.cancel(),this._token=setTimeout(()=>{this._token=-1,e()},t)}setIfNotSet(e,t){if(this._isDisposed)throw new Error("Calling setIfNotSet on a disposed TimeoutTimer");-1===this._token&&(this._token=setTimeout(()=>{this._token=-1,e()},t))}},t.MicrotaskTimer=class{constructor(){this._isScheduled=!1,this._isDisposed=!1}dispose(){this.cancel(),this._isDisposed=!0}cancel(){this._isScheduled=!1}set(e){if(this._isDisposed)throw new Error("Calling set on a disposed MicrotaskTimer");this._isScheduled||(this._isScheduled=!0,queueMicrotask(()=>{this._isScheduled&&(this._isScheduled=!1,e())}))}},t.IntervalTimer=class{constructor(){this._isDisposed=!1}cancel(){this._disposable?.dispose(),this._disposable=void 0}cancelAndSet(e,t,i=globalThis){if(this._isDisposed)throw new Error("Calling cancelAndSet on a disposed IntervalTimer");this.cancel();const s=i.setInterval(()=>{e()},t);this._disposable={dispose:()=>{i.clearInterval(s),this._disposable=void 0}}}dispose(){this.cancel(),this._isDisposed=!0}}},639(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.CircularList=void 0;const s=i(812),r=i(636);class n extends s.Disposable{constructor(e){super(),this._maxLength=e,this.onDeleteEmitter=this._register(new r.Emitter),this.onDelete=this.onDeleteEmitter.event,this.onInsertEmitter=this._register(new r.Emitter),this.onInsert=this.onInsertEmitter.event,this.onTrimEmitter=this._register(new r.Emitter),this.onTrim=this.onTrimEmitter.event,this._array=new Array(this._maxLength),this._startIndex=0,this._length=0}get maxLength(){return this._maxLength}set maxLength(e){if(this._maxLength===e)return;const t=new Array(e);for(let i=0;ithis._length)for(let t=this._length;t=e;t--)this._array[this._getCyclicIndex(t+i.length)]=this._array[this._getCyclicIndex(t)];for(let t=0;tthis._maxLength){const e=this._length+i.length-this._maxLength;this._startIndex+=e,this._length=this._maxLength,this.onTrimEmitter.fire(e)}else this._length+=i.length}trimStart(e){e>this._length&&(e=this._length),this._startIndex+=e,this._length-=e,this.onTrimEmitter.fire(e)}shiftElements(e,t,i){if(!(t<=0)){if(e<0||e>=this._length)throw new Error("start argument out of range");if(e+i<0)throw new Error("Cannot shift elements in list beyond index 0");if(i>0){for(let s=t-1;s>=0;s--)this.set(e+s+i,this.get(e+s));const s=e+t+i-this._length;if(s>0)for(this._length+=s;this._length>this._maxLength;)this._length--,this._startIndex++,this.onTrimEmitter.fire(1)}else for(let s=0;s{this._onScrollApi?.fire(e.position)})),this._onScrollApi.event}get cols(){return this._bufferService.cols}get rows(){return this._bufferService.rows}get buffers(){return this._bufferService.buffers}get options(){return this.optionsService.options}set options(e){for(const t in e)this.optionsService.options[t]=e[t]}constructor(e){super(),this._windowsWrappingHeuristics=this._register(new b.MutableDisposable),this._onBinary=this._register(new v.Emitter),this.onBinary=this._onBinary.event,this._onData=this._register(new v.Emitter),this.onData=this._onData.event,this._onLineFeed=this._register(new v.Emitter),this.onLineFeed=this._onLineFeed.event,this._onRender=this._register(new v.Emitter),this.onRender=this._onRender.event,this._onResize=this._register(new v.Emitter),this.onResize=this._onResize.event,this._onWriteParsed=this._register(new v.Emitter),this.onWriteParsed=this._onWriteParsed.event,this._onScroll=this._register(new v.Emitter),this._instantiationService=new r.InstantiationService,this.optionsService=this._register(new o.OptionsService(e)),this._instantiationService.setService(s.IOptionsService,this.optionsService),this._logService=this._register(this._instantiationService.createInstance(n.LogService)),this._instantiationService.setService(s.ILogService,this._logService),this._bufferService=this._register(this._instantiationService.createInstance(a.BufferService)),this._instantiationService.setService(s.IBufferService,this._bufferService),this.coreService=this._register(this._instantiationService.createInstance(h.CoreService)),this._instantiationService.setService(s.ICoreService,this.coreService),this.mouseStateService=this._register(this._instantiationService.createInstance(c.MouseStateService)),this._instantiationService.setService(s.IMouseStateService,this.mouseStateService),this.unicodeService=this._register(this._instantiationService.createInstance(_.UnicodeService)),this.unicodeService.register(new l.UnicodeV6),this._instantiationService.setService(s.IUnicodeService,this.unicodeService),this._charsetService=this._instantiationService.createInstance(d.CharsetService),this._instantiationService.setService(s.ICharsetService,this._charsetService),this._oscLinkService=this._instantiationService.createInstance(g.OscLinkService),this._instantiationService.setService(s.IOscLinkService,this._oscLinkService),this._inputHandler=this._register(new f.InputHandler(this._bufferService,this._charsetService,this.coreService,this._logService,this.optionsService,this._oscLinkService,this.mouseStateService,this.unicodeService)),this._register(v.EventUtils.forward(this._inputHandler.onLineFeed,this._onLineFeed)),this._register(v.EventUtils.forward(this._bufferService.onResize,this._onResize)),this._register(v.EventUtils.forward(this.coreService.onData,this._onData)),this._register(v.EventUtils.forward(this.coreService.onBinary,this._onBinary)),this._register(this.coreService.onRequestScrollToBottom(()=>this.scrollToBottom(!0))),this._register(this.coreService.onUserInput(()=>this._writeBuffer.handleUserInput())),this._register(this.optionsService.onMultipleOptionChange(["windowsPty"],()=>this._handleWindowsPtyOptionChange())),this._register(this._bufferService.onScroll(()=>{this._onScroll.fire({position:this._bufferService.buffer.ydisp}),this._inputHandler.markRangeDirty(this._bufferService.buffer.scrollTop,this._bufferService.buffer.scrollBottom)})),this._writeBuffer=this._register(new p.WriteBuffer((e,t)=>this._inputHandler.parse(e,t))),this._register(v.EventUtils.forward(this._writeBuffer.onWriteParsed,this._onWriteParsed))}write(e,t){this._writeBuffer.write(e,t)}writeSync(e,t){this._logService.logLevel<=s.LogLevelEnum.WARN&&!S&&(this._logService.warn("writeSync is unreliable and will be removed soon."),S=!0),this._writeBuffer.writeSync(e,t)}input(e,t=!0){this.coreService.triggerDataEvent(e,t)}resize(e,t){isNaN(e)||isNaN(t)||(e=Math.max(e,2),t=Math.max(t,1),this._writeBuffer.flushSync(),this._bufferService.resize(e,t))}scroll(e,t=!1){this._bufferService.scroll(e,t)}scrollLines(e,t){this._bufferService.scrollLines(e,t)}scrollPages(e){this.scrollLines(e*(this.rows-1))}scrollToTop(){this.scrollLines(-this._bufferService.buffer.ydisp)}scrollToBottom(e){this.scrollLines(this._bufferService.buffer.ybase-this._bufferService.buffer.ydisp)}scrollToLine(e){const t=e-this._bufferService.buffer.ydisp;0!==t&&this.scrollLines(t)}registerEscHandler(e,t){return this._inputHandler.registerEscHandler(e,t)}registerDcsHandler(e,t){return this._inputHandler.registerDcsHandler(e,t)}registerCsiHandler(e,t){return this._inputHandler.registerCsiHandler(e,t)}registerOscHandler(e,t){return this._inputHandler.registerOscHandler(e,t)}registerApcHandler(e,t){return this._inputHandler.registerApcHandler(e,t)}_setup(){this._handleWindowsPtyOptionChange()}reset(){this._inputHandler.reset(),this._bufferService.reset(),this._charsetService.reset(),this.coreService.reset(),this.mouseStateService.reset()}_handleWindowsPtyOptionChange(){let e=!1;const t=this.optionsService.rawOptions.windowsPty;t&&void 0!==t.backend&&void 0!==t.buildNumber&&(e=!!("conpty"===t.backend&&t.buildNumber<21376)),e?this._enableWindowsWrappingHeuristics():this._windowsWrappingHeuristics.clear()}_enableWindowsWrappingHeuristics(){if(!this._windowsWrappingHeuristics.value){const e=[];e.push(this.onLineFeed(u.updateWindowsModeWrappedState.bind(null,this._bufferService))),e.push(this.registerCsiHandler({final:"H"},()=>((0,u.updateWindowsModeWrappedState)(this._bufferService),!1))),this._windowsWrappingHeuristics.value=(0,b.toDisposable)(()=>{for(const t of e)t.dispose()})}}}t.CoreTerminal=m},636(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.EventUtils=t.Emitter=void 0;const s=i(812);var r;t.Emitter=class{constructor(){this._listeners=[],this._disposed=!1}get event(){return this._event||(this._event=(e,t,i)=>{if(this._disposed)return(0,s.toDisposable)(()=>{});const r={fn:e,thisArgs:t};this._listeners=this._listeners.slice(),this._listeners.push(r);const n=(0,s.toDisposable)(()=>{const e=this._listeners.indexOf(r);-1!==e&&(this._listeners=this._listeners.slice(),this._listeners.splice(e,1))});return i&&(Array.isArray(i)?i.push(n):i.add(n)),n}),this._event}fire(e){if(this._disposed||!this._listeners.length)return;if(1===this._listeners.length)return void this._listeners[0].fn.call(this._listeners[0].thisArgs,e);const t=this._listeners;for(let i=0,s=t.length;it.fire(e))},e.map=function(e,t){return(i,s,r)=>e(e=>i.call(s,t(e)),void 0,r)},e.any=function(...e){return(t,i,r)=>{const n=new s.DisposableStore;for(const s of e)n.add(s(e=>t.call(i,e)));return r&&(Array.isArray(r)?r.push(n):r.add(n)),n}},e.runAndSubscribe=function(e,t,i){return t(i),e(e=>t(e))}}(r||(t.EventUtils=r={}))},486(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,a=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)a=Reflect.decorate(e,t,i,s);else for(var o=e.length-1;o>=0;o--)(r=e[o])&&(a=(n<3?r(a):n>3?r(t,i,a):r(t,i))||a);return n>3&&a&&Object.defineProperty(t,i,a),a},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.InputHandler=t.WindowsOptionsReportType=void 0,t.isValidColorIndex=E;const n=i(760),a=i(717),o=i(812),h=i(726),c=i(107),l=i(938),_=i(55),d=i(451),u=i(501),f=i(415),p=i(346),g=i(823),v=i(607),b=i(693),S=i(636),m=i(804),y={"(":0,")":1,"*":2,"+":3,"-":1,".":2};function C(e,t){if(e>24)return t.setWinLines||!1;switch(e){case 1:return!!t.restoreWin;case 2:return!!t.minimizeWin;case 3:return!!t.setWinPosition;case 4:return!!t.setWinSizePixels;case 5:return!!t.raiseWin;case 6:return!!t.lowerWin;case 7:return!!t.refreshWin;case 8:return!!t.setWinSizeChars;case 9:return!!t.maximizeWin;case 10:return!!t.fullscreenWin;case 11:return!!t.getWinState;case 13:return!!t.getWinPosition;case 14:return!!t.getWinSizePixels;case 15:return!!t.getScreenSizePixels;case 16:return!!t.getCellSizePixels;case 18:return!!t.getWinSizeChars;case 19:return!!t.getScreenSizeChars;case 20:return!!t.getIconTitle;case 21:return!!t.getWinTitle;case 22:return!!t.pushTitle;case 23:return!!t.popTitle;case 24:return!!t.setWinLines}return!1}var w;!function(e){e[e.GET_WIN_SIZE_PIXELS=0]="GET_WIN_SIZE_PIXELS",e[e.GET_CELL_SIZE_PIXELS=1]="GET_CELL_SIZE_PIXELS"}(w||(t.WindowsOptionsReportType=w={}));let B=0;class k extends o.Disposable{getAttrData(){return this._curAttrData}constructor(e,t,i,s,r,o,l,_,d=new a.EscapeSequenceParser){super(),this._bufferService=e,this._charsetService=t,this._coreService=i,this._logService=s,this._optionsService=r,this._oscLinkService=o,this._mouseStateService=l,this._unicodeService=_,this._parser=d,this._parseBuffer=new Uint32Array(4096),this._stringDecoder=new h.StringToUtf32,this._utf8Decoder=new h.Utf8ToUtf32,this._windowTitle="",this._iconName="",this._windowTitleStack=[],this._iconNameStack=[],this._curAttrData=c.DEFAULT_ATTR_DATA.clone(),this._eraseAttrDataInternal=c.DEFAULT_ATTR_DATA.clone(),this._onRequestBell=this._register(new S.Emitter),this.onRequestBell=this._onRequestBell.event,this._onRequestRefreshRows=this._register(new S.Emitter),this.onRequestRefreshRows=this._onRequestRefreshRows.event,this._onRequestReset=this._register(new S.Emitter),this.onRequestReset=this._onRequestReset.event,this._onRequestSendFocus=this._register(new S.Emitter),this.onRequestSendFocus=this._onRequestSendFocus.event,this._onRequestSyncScrollBar=this._register(new S.Emitter),this.onRequestSyncScrollBar=this._onRequestSyncScrollBar.event,this._onRequestWindowsOptionsReport=this._register(new S.Emitter),this.onRequestWindowsOptionsReport=this._onRequestWindowsOptionsReport.event,this._onA11yChar=this._register(new S.Emitter),this.onA11yChar=this._onA11yChar.event,this._onA11yTab=this._register(new S.Emitter),this.onA11yTab=this._onA11yTab.event,this._onCursorMove=this._register(new S.Emitter),this.onCursorMove=this._onCursorMove.event,this._onLineFeed=this._register(new S.Emitter),this.onLineFeed=this._onLineFeed.event,this._onScroll=this._register(new S.Emitter),this.onScroll=this._onScroll.event,this._onTitleChange=this._register(new S.Emitter),this.onTitleChange=this._onTitleChange.event,this._onColor=this._register(new S.Emitter),this.onColor=this._onColor.event,this._onRequestColorSchemeQuery=this._register(new S.Emitter),this.onRequestColorSchemeQuery=this._onRequestColorSchemeQuery.event,this._parseStack={paused:!1,cursorStartX:0,cursorStartY:0,decodedLength:0,position:0},this._specialColors=[256,257,258],this._register(this._parser),this._dirtyRowTracker=new A(this._bufferService),this._activeBuffer=this._bufferService.buffer,this._register(this._bufferService.buffers.onBufferActivate(e=>this._activeBuffer=e.activeBuffer)),this._parser.setCsiHandlerFallback((e,t)=>{this._logService.debug("Unknown CSI code: ",{identifier:this._parser.identToString(e),params:t.toArray()})}),this._parser.setEscHandlerFallback(e=>{this._logService.debug("Unknown ESC code: ",{identifier:this._parser.identToString(e)})}),this._parser.setExecuteHandlerFallback(e=>{this._logService.debug("Unknown EXECUTE code: ",{code:e})}),this._parser.setOscHandlerFallback((e,t,i)=>{this._logService.debug("Unknown OSC code: ",{identifier:e,action:t,data:i})}),this._parser.setDcsHandlerFallback((e,t,i)=>{"HOOK"===t&&(i=i.toArray()),this._logService.debug("Unknown DCS code: ",{identifier:this._parser.identToString(e),action:t,payload:i})}),this._parser.setApcHandlerFallback((e,t,i)=>{this._logService.debug("Unknown APC code: ",{identifier:this._parser.identToString(e),action:t,payload:i})}),this._parser.setPrintHandler((e,t,i)=>this.print(e,t,i)),this._parser.registerCsiHandler({final:"@"},e=>this.insertChars(e)),this._parser.registerCsiHandler({intermediates:" ",final:"@"},e=>this.scrollLeft(e)),this._parser.registerCsiHandler({final:"A"},e=>this.cursorUp(e)),this._parser.registerCsiHandler({intermediates:" ",final:"A"},e=>this.scrollRight(e)),this._parser.registerCsiHandler({final:"B"},e=>this.cursorDown(e)),this._parser.registerCsiHandler({final:"C"},e=>this.cursorForward(e)),this._parser.registerCsiHandler({final:"D"},e=>this.cursorBackward(e)),this._parser.registerCsiHandler({final:"E"},e=>this.cursorNextLine(e)),this._parser.registerCsiHandler({final:"F"},e=>this.cursorPrecedingLine(e)),this._parser.registerCsiHandler({final:"G"},e=>this.cursorCharAbsolute(e)),this._parser.registerCsiHandler({final:"H"},e=>this.cursorPosition(e)),this._parser.registerCsiHandler({final:"I"},e=>this.cursorForwardTab(e)),this._parser.registerCsiHandler({final:"J"},e=>this.eraseInDisplay(e,!1)),this._parser.registerCsiHandler({prefix:"?",final:"J"},e=>this.eraseInDisplay(e,!0)),this._parser.registerCsiHandler({final:"K"},e=>this.eraseInLine(e,!1)),this._parser.registerCsiHandler({prefix:"?",final:"K"},e=>this.eraseInLine(e,!0)),this._parser.registerCsiHandler({final:"L"},e=>this.insertLines(e)),this._parser.registerCsiHandler({final:"M"},e=>this.deleteLines(e)),this._parser.registerCsiHandler({final:"P"},e=>this.deleteChars(e)),this._parser.registerCsiHandler({final:"S"},e=>this.scrollUp(e)),this._parser.registerCsiHandler({final:"T"},e=>this.scrollDown(e)),this._parser.registerCsiHandler({final:"X"},e=>this.eraseChars(e)),this._parser.registerCsiHandler({final:"Z"},e=>this.cursorBackwardTab(e)),this._parser.registerCsiHandler({final:"^"},e=>this.scrollDown(e)),this._parser.registerCsiHandler({final:"`"},e=>this.charPosAbsolute(e)),this._parser.registerCsiHandler({final:"a"},e=>this.hPositionRelative(e)),this._parser.registerCsiHandler({final:"b"},e=>this.repeatPrecedingCharacter(e)),this._parser.registerCsiHandler({final:"c"},e=>this.sendDeviceAttributesPrimary(e)),this._parser.registerCsiHandler({prefix:">",final:"c"},e=>this.sendDeviceAttributesSecondary(e)),this._parser.registerCsiHandler({final:"d"},e=>this.linePosAbsolute(e)),this._parser.registerCsiHandler({final:"e"},e=>this.vPositionRelative(e)),this._parser.registerCsiHandler({final:"f"},e=>this.hVPosition(e)),this._parser.registerCsiHandler({final:"g"},e=>this.tabClear(e)),this._parser.registerCsiHandler({final:"h"},e=>this.setMode(e)),this._parser.registerCsiHandler({prefix:"?",final:"h"},e=>this.setModePrivate(e)),this._parser.registerCsiHandler({final:"l"},e=>this.resetMode(e)),this._parser.registerCsiHandler({prefix:"?",final:"l"},e=>this.resetModePrivate(e)),this._parser.registerCsiHandler({final:"m"},e=>this.charAttributes(e)),this._parser.registerCsiHandler({final:"n"},e=>this.deviceStatus(e)),this._parser.registerCsiHandler({prefix:"?",final:"n"},e=>this.deviceStatusPrivate(e)),this._parser.registerCsiHandler({intermediates:"!",final:"p"},e=>this.softReset(e)),this._parser.registerCsiHandler({prefix:">",final:"q"},e=>this.sendXtVersion(e)),this._parser.registerCsiHandler({intermediates:" ",final:"q"},e=>this.setCursorStyle(e)),this._parser.registerCsiHandler({final:"r"},e=>this.setScrollRegion(e)),this._parser.registerCsiHandler({final:"s"},e=>this.saveCursor(e)),this._parser.registerCsiHandler({final:"t"},e=>this.windowOptions(e)),this._parser.registerCsiHandler({final:"u"},e=>this.restoreCursor(e)),this._parser.registerCsiHandler({intermediates:"'",final:"}"},e=>this.insertColumns(e)),this._parser.registerCsiHandler({intermediates:"'",final:"~"},e=>this.deleteColumns(e)),this._parser.registerCsiHandler({intermediates:'"',final:"q"},e=>this.selectProtected(e)),this._parser.registerCsiHandler({intermediates:"$",final:"p"},e=>this.requestMode(e,!0)),this._parser.registerCsiHandler({prefix:"?",intermediates:"$",final:"p"},e=>this.requestMode(e,!1)),this._parser.registerCsiHandler({prefix:"=",final:"u"},e=>this.kittyKeyboardSet(e)),this._parser.registerCsiHandler({prefix:"?",final:"u"},e=>this.kittyKeyboardQuery(e)),this._parser.registerCsiHandler({prefix:">",final:"u"},e=>this.kittyKeyboardPush(e)),this._parser.registerCsiHandler({prefix:"<",final:"u"},e=>this.kittyKeyboardPop(e)),this._parser.setExecuteHandler("",()=>this.bell()),this._parser.setExecuteHandler("\n",()=>this.lineFeed()),this._parser.setExecuteHandler("\v",()=>this.lineFeed()),this._parser.setExecuteHandler("\f",()=>this.lineFeed()),this._parser.setExecuteHandler("\r",()=>this.carriageReturn()),this._parser.setExecuteHandler("\b",()=>this.backspace()),this._parser.setExecuteHandler("\t",()=>this.tab()),this._parser.setExecuteHandler("",()=>this.shiftOut()),this._parser.setExecuteHandler("",()=>this.shiftIn()),this._parser.setExecuteHandler("„",()=>this.index()),this._parser.setExecuteHandler("…",()=>this.nextLine()),this._parser.setExecuteHandler("ˆ",()=>this.tabSet()),this._parser.registerOscHandler(0,new p.OscHandler(e=>(this.setTitle(e),this.setIconName(e),!0))),this._parser.registerOscHandler(1,new p.OscHandler(e=>this.setIconName(e))),this._parser.registerOscHandler(2,new p.OscHandler(e=>this.setTitle(e))),this._parser.registerOscHandler(4,new p.OscHandler(e=>this.setOrReportIndexedColor(e))),this._parser.registerOscHandler(8,new p.OscHandler(e=>this.setHyperlink(e))),this._parser.registerOscHandler(10,new p.OscHandler(e=>this.setOrReportFgColor(e))),this._parser.registerOscHandler(11,new p.OscHandler(e=>this.setOrReportBgColor(e))),this._parser.registerOscHandler(12,new p.OscHandler(e=>this.setOrReportCursorColor(e))),this._parser.registerOscHandler(104,new p.OscHandler(e=>this.restoreIndexedColor(e))),this._parser.registerOscHandler(110,new p.OscHandler(e=>this.restoreFgColor(e))),this._parser.registerOscHandler(111,new p.OscHandler(e=>this.restoreBgColor(e))),this._parser.registerOscHandler(112,new p.OscHandler(e=>this.restoreCursorColor(e))),this._parser.registerEscHandler({final:"7"},()=>this.saveCursor()),this._parser.registerEscHandler({final:"8"},()=>this.restoreCursor()),this._parser.registerEscHandler({final:"D"},()=>this.index()),this._parser.registerEscHandler({final:"E"},()=>this.nextLine()),this._parser.registerEscHandler({final:"H"},()=>this.tabSet()),this._parser.registerEscHandler({final:"M"},()=>this.reverseIndex()),this._parser.registerEscHandler({final:"="},()=>this.keypadApplicationMode()),this._parser.registerEscHandler({final:">"},()=>this.keypadNumericMode()),this._parser.registerEscHandler({final:"c"},()=>this.fullReset()),this._parser.registerEscHandler({final:"n"},()=>this.setgLevel(2)),this._parser.registerEscHandler({final:"o"},()=>this.setgLevel(3)),this._parser.registerEscHandler({final:"|"},()=>this.setgLevel(3)),this._parser.registerEscHandler({final:"}"},()=>this.setgLevel(2)),this._parser.registerEscHandler({final:"~"},()=>this.setgLevel(1)),this._parser.registerEscHandler({intermediates:"%",final:"@"},()=>this.selectDefaultCharset()),this._parser.registerEscHandler({intermediates:"%",final:"G"},()=>this.selectDefaultCharset());for(const e in n.CHARSETS)this._parser.registerEscHandler({intermediates:"(",final:e},()=>this.selectCharset("("+e)),this._parser.registerEscHandler({intermediates:")",final:e},()=>this.selectCharset(")"+e)),this._parser.registerEscHandler({intermediates:"*",final:e},()=>this.selectCharset("*"+e)),this._parser.registerEscHandler({intermediates:"+",final:e},()=>this.selectCharset("+"+e)),this._parser.registerEscHandler({intermediates:"-",final:e},()=>this.selectCharset("-"+e)),this._parser.registerEscHandler({intermediates:".",final:e},()=>this.selectCharset("."+e)),this._parser.registerEscHandler({intermediates:"/",final:e},()=>this.selectCharset("/"+e));this._parser.registerEscHandler({intermediates:"#",final:"8"},()=>this.screenAlignmentPattern()),this._parser.setErrorHandler(e=>(this._logService.error("Parsing error: ",e),e)),this._parser.registerDcsHandler({intermediates:"$",final:"q"},new g.DcsHandler((e,t)=>this.requestStatusString(e,t)))}_preserveStack(e,t,i,s){this._parseStack.paused=!0,this._parseStack.cursorStartX=e,this._parseStack.cursorStartY=t,this._parseStack.decodedLength=i,this._parseStack.position=s}_logSlowResolvingAsync(e){if(this._logService.logLevel<=u.LogLevelEnum.WARN){let t;const i=new Promise((e,i)=>{t=setTimeout(()=>i("#SLOW_TIMEOUT"),5e3)});Promise.race([e,i]).then(()=>{void 0!==t&&clearTimeout(t)},e=>{if(void 0!==t&&clearTimeout(t),"#SLOW_TIMEOUT"!==e)throw e;console.warn("async parser handler taking longer than 5000 ms")})}}_getCurrentLinkId(){return this._curAttrData.extended.urlId}parse(e,t){let i,s=this._activeBuffer.x,r=this._activeBuffer.y,n=0;const a=this._parseStack.paused;if(a){if(i=this._parser.parse(this._parseBuffer,this._parseStack.decodedLength,t))return this._logSlowResolvingAsync(i),i;s=this._parseStack.cursorStartX,r=this._parseStack.cursorStartY,this._parseStack.paused=!1,e.length>131072&&(n=this._parseStack.position+131072)}if(this._logService.logLevel<=u.LogLevelEnum.DEBUG&&this._logService.debug("parsing data "+("string"==typeof e?` "${e}"`:` "${Array.prototype.map.call(e,e=>String.fromCharCode(e)).join("")}"`)),this._logService.logLevel===u.LogLevelEnum.TRACE&&this._logService.trace("parsing data (codes)","string"==typeof e?e.split("").map(e=>e.charCodeAt(0)):e),this._parseBuffer.length131072)for(let t=n;t0&&2===p.getWidth(this._activeBuffer.x-1)&&p.setCellFromCodepoint(this._activeBuffer.x-1,0,1,u);let g=this._parser.precedingJoinState;for(let v=t;vo)if(_){const e=p;let t=this._activeBuffer.x-b;if(this._activeBuffer.x=b,this._activeBuffer.y++,this._activeBuffer.y===this._activeBuffer.scrollBottom+1?(this._activeBuffer.y--,this._bufferService.scroll(this._eraseAttrData(),!0)):(this._activeBuffer.y>=this._bufferService.rows&&(this._activeBuffer.y=this._bufferService.rows-1),this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y).isWrapped=!0),p=this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y),!p)return;for(b>0&&p instanceof c.BufferLine&&p.copyCellsFrom(e,t,0,b,!1);t=0;)p.setCellFromCodepoint(this._activeBuffer.x++,0,0,u);continue}if(d&&(p.insertCells(this._activeBuffer.x,r-b,this._activeBuffer.getNullCell(u)),2===p.getWidth(o-1)&&p.setCellFromCodepoint(o-1,l.NULL_CELL_CODE,l.NULL_CELL_WIDTH,u)),p.setCellFromCodepoint(this._activeBuffer.x++,s,r,u),r>0)for(;--r;)p.setCellFromCodepoint(this._activeBuffer.x++,0,0,u)}this._parser.precedingJoinState=g,this._activeBuffer.x0&&0===p.getWidth(this._activeBuffer.x)&&!p.hasContent(this._activeBuffer.x)&&p.setCellFromCodepoint(this._activeBuffer.x,0,1,u),this._dirtyRowTracker.markDirty(this._activeBuffer.y)}registerCsiHandler(e,t){return"t"!==e.final||e.prefix||e.intermediates?this._parser.registerCsiHandler(e,t):this._parser.registerCsiHandler(e,e=>!C(e.params[0],this._optionsService.rawOptions.windowOptions)||t(e))}registerDcsHandler(e,t){return this._parser.registerDcsHandler(e,new g.DcsHandler(t))}registerEscHandler(e,t){return this._parser.registerEscHandler(e,t)}registerOscHandler(e,t){return this._parser.registerOscHandler(e,new p.OscHandler(t))}registerApcHandler(e,t){return this._parser.registerApcHandler(e,new v.ApcHandler(t))}bell(){return this._onRequestBell.fire(),!0}lineFeed(){return this._dirtyRowTracker.markDirty(this._activeBuffer.y),this._optionsService.rawOptions.convertEol&&(this._activeBuffer.x=0),this._activeBuffer.y++,this._activeBuffer.y===this._activeBuffer.scrollBottom+1?(this._activeBuffer.y--,this._bufferService.scroll(this._eraseAttrData())):this._activeBuffer.y>=this._bufferService.rows?this._activeBuffer.y=this._bufferService.rows-1:this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y).isWrapped=!1,this._activeBuffer.x>=this._bufferService.cols&&this._activeBuffer.x--,this._dirtyRowTracker.markDirty(this._activeBuffer.y),this._onLineFeed.fire(),!0}carriageReturn(){return this._activeBuffer.x=0,!0}backspace(){if(!this._coreService.decPrivateModes.reverseWraparound)return this._restrictCursor(),this._activeBuffer.x>0&&this._activeBuffer.x--,!0;if(this._restrictCursor(this._bufferService.cols),this._activeBuffer.x>0)this._activeBuffer.x--;else if(0===this._activeBuffer.x&&this._activeBuffer.y>this._activeBuffer.scrollTop&&this._activeBuffer.y<=this._activeBuffer.scrollBottom&&this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y)?.isWrapped){this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y).isWrapped=!1,this._activeBuffer.y--,this._activeBuffer.x=this._bufferService.cols-1;const e=this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y);e.hasWidth(this._activeBuffer.x)&&!e.hasContent(this._activeBuffer.x)&&this._activeBuffer.x--}return this._restrictCursor(),!0}tab(){if(this._activeBuffer.x>=this._bufferService.cols)return!0;const e=this._activeBuffer.x;return this._activeBuffer.x=this._activeBuffer.nextStop(),this._optionsService.rawOptions.screenReaderMode&&this._onA11yTab.fire(this._activeBuffer.x-e),!0}shiftOut(){return this._charsetService.setgLevel(1),!0}shiftIn(){return this._charsetService.setgLevel(0),!0}_restrictCursor(e=this._bufferService.cols-1){this._activeBuffer.x=Math.min(e,Math.max(0,this._activeBuffer.x)),this._activeBuffer.y=this._coreService.decPrivateModes.origin?Math.min(this._activeBuffer.scrollBottom,Math.max(this._activeBuffer.scrollTop,this._activeBuffer.y)):Math.min(this._bufferService.rows-1,Math.max(0,this._activeBuffer.y)),this._dirtyRowTracker.markDirty(this._activeBuffer.y)}_setCursor(e,t){this._dirtyRowTracker.markDirty(this._activeBuffer.y),this._coreService.decPrivateModes.origin?(this._activeBuffer.x=e,this._activeBuffer.y=this._activeBuffer.scrollTop+t):(this._activeBuffer.x=e,this._activeBuffer.y=t),this._restrictCursor(),this._dirtyRowTracker.markDirty(this._activeBuffer.y)}_moveCursor(e,t){this._restrictCursor(),this._setCursor(this._activeBuffer.x+e,this._activeBuffer.y+t)}cursorUp(e){const t=this._activeBuffer.y-this._activeBuffer.scrollTop;return t>=0?this._moveCursor(0,-Math.min(t,e.params[0]||1)):this._moveCursor(0,-(e.params[0]||1)),!0}cursorDown(e){const t=this._activeBuffer.scrollBottom-this._activeBuffer.y;return t>=0?this._moveCursor(0,Math.min(t,e.params[0]||1)):this._moveCursor(0,e.params[0]||1),!0}cursorForward(e){return this._moveCursor(e.params[0]||1,0),!0}cursorBackward(e){return this._moveCursor(-(e.params[0]||1),0),!0}cursorNextLine(e){return this.cursorDown(e),this._activeBuffer.x=0,!0}cursorPrecedingLine(e){return this.cursorUp(e),this._activeBuffer.x=0,!0}cursorCharAbsolute(e){return this._setCursor((e.params[0]||1)-1,this._activeBuffer.y),!0}cursorPosition(e){return this._setCursor(e.length>=2?(e.params[1]||1)-1:0,(e.params[0]||1)-1),!0}charPosAbsolute(e){return this._setCursor((e.params[0]||1)-1,this._activeBuffer.y),!0}hPositionRelative(e){return this._moveCursor(e.params[0]||1,0),!0}linePosAbsolute(e){return this._setCursor(this._activeBuffer.x,(e.params[0]||1)-1),!0}vPositionRelative(e){return this._moveCursor(0,e.params[0]||1),!0}hVPosition(e){return this.cursorPosition(e),!0}tabClear(e){const t=e.params[0];return 0===t?delete this._activeBuffer.tabs[this._activeBuffer.x]:3===t&&(this._activeBuffer.tabs={}),!0}cursorForwardTab(e){if(this._activeBuffer.x>=this._bufferService.cols)return!0;let t=e.params[0]||1;for(;t--;)this._activeBuffer.x=this._activeBuffer.nextStop();return!0}cursorBackwardTab(e){if(this._activeBuffer.x>=this._bufferService.cols)return!0;let t=e.params[0]||1;for(;t--;)this._activeBuffer.x=this._activeBuffer.prevStop();return!0}selectProtected(e){const t=e.params[0];return 1===t&&(this._curAttrData.bg|=536870912),2!==t&&0!==t||(this._curAttrData.bg&=-536870913),!0}_eraseInBufferLine(e,t,i,s=!1,r=!1){const n=this._activeBuffer.lines.get(this._activeBuffer.ybase+e);n&&(n.replaceCells(t,i,this._activeBuffer.getNullCell(this._eraseAttrData()),r),s&&(n.isWrapped=!1))}_resetBufferLine(e,t=!1){const i=this._activeBuffer.lines.get(this._activeBuffer.ybase+e);i&&(i.fill(this._activeBuffer.getNullCell(this._eraseAttrData()),t),this._bufferService.buffer.clearMarkers(this._activeBuffer.ybase+e),i.isWrapped=!1)}eraseInDisplay(e,t=!1){let i;switch(this._restrictCursor(this._bufferService.cols),e.params[0]){case 0:for(i=this._activeBuffer.y,this._dirtyRowTracker.markDirty(i),this._eraseInBufferLine(i++,this._activeBuffer.x,this._bufferService.cols,0===this._activeBuffer.x,t);i=this._bufferService.cols){const e=this._activeBuffer.lines.get(i+1);e&&(e.isWrapped=!1)}for(;i--;)this._resetBufferLine(i,t);this._dirtyRowTracker.markDirty(0);break;case 2:if(this._optionsService.rawOptions.scrollOnEraseInDisplay){for(i=this._bufferService.rows,this._dirtyRowTracker.markRangeDirty(0,i-1);i--;){const e=this._activeBuffer.lines.get(this._activeBuffer.ybase+i);if(e?.getTrimmedLength())break}for(;i>=0;i--)this._bufferService.scroll(this._eraseAttrData())}else{for(i=this._bufferService.rows,this._dirtyRowTracker.markDirty(i-1);i--;)this._resetBufferLine(i,t);this._dirtyRowTracker.markDirty(0)}break;case 3:const e=this._activeBuffer.lines.length-this._bufferService.rows;e>0&&(this._activeBuffer.lines.trimStart(e),this._activeBuffer.ybase=Math.max(this._activeBuffer.ybase-e,0),this._activeBuffer.ydisp=Math.max(this._activeBuffer.ydisp-e,0),this._activeBuffer===this._bufferService.buffers.normal&&(this._bufferService.isUserScrolling=!1),this._onScroll.fire(0))}return!0}eraseInLine(e,t=!1){switch(this._restrictCursor(this._bufferService.cols),e.params[0]){case 0:this._eraseInBufferLine(this._activeBuffer.y,this._activeBuffer.x,this._bufferService.cols,0===this._activeBuffer.x,t);break;case 1:this._eraseInBufferLine(this._activeBuffer.y,0,this._activeBuffer.x+1,!1,t);break;case 2:this._eraseInBufferLine(this._activeBuffer.y,0,this._bufferService.cols,!0,t)}return this._dirtyRowTracker.markDirty(this._activeBuffer.y),!0}insertLines(e){this._restrictCursor();let t=e.params[0]||1;if(this._activeBuffer.y>this._activeBuffer.scrollBottom||this._activeBuffer.ythis._activeBuffer.scrollBottom||this._activeBuffer.ythis._activeBuffer.scrollBottom||this._activeBuffer.ythis._activeBuffer.scrollBottom||this._activeBuffer.ythis._activeBuffer.scrollBottom||this._activeBuffer.ythis._activeBuffer.scrollBottom||this._activeBuffer.y65535?2:1}let h=o;for(let e=1;e0||(this._is("xterm")||this._is("rxvt-unicode")||this._is("screen")?this._coreService.triggerDataEvent("[?1;2c"):this._is("linux")&&this._coreService.triggerDataEvent("[?6c")),!0}sendDeviceAttributesSecondary(e){return e.params[0]>0||(this._is("xterm")?this._coreService.triggerDataEvent("[>0;276;0c"):this._is("rxvt-unicode")?this._coreService.triggerDataEvent("[>85;95;0c"):this._is("linux")?this._coreService.triggerDataEvent(e.params[0]+"c"):this._is("screen")&&this._coreService.triggerDataEvent("[>83;40003;0c")),!0}sendXtVersion(e){return e.params[0]>0||this._coreService.triggerDataEvent(`P>|xterm.js(${m.XTERM_VERSION})\\`),!0}_is(e){return(this._optionsService.rawOptions.termName+"").startsWith(e)}setMode(e){for(let t=0;t(n.triggerDataEvent(`[${t?"":"?"}${e};${i}$y`),!0),d=e=>e?1:2,u=e.params[0];return t?_(u,2===u?4:4===u?d(n.modes.insertMode):12===u?3:20===u?d(l.convertEol):0):1===u?_(u,d(i.applicationCursorKeys)):3===u?_(u,l.windowOptions.setWinLines?80===o?2:132===o?1:0:0):6===u?_(u,d(i.origin)):7===u?_(u,d(i.wraparound)):8===u?_(u,3):9===u?_(u,d("X10"===s)):12===u?_(u,d(l.cursorBlink)):25===u?_(u,d(!n.isCursorHidden)):45===u?_(u,d(i.reverseWraparound)):66===u?_(u,d(i.applicationKeypad)):67===u?_(u,4):1e3===u?_(u,d("VT200"===s)):1002===u?_(u,d("DRAG"===s)):1003===u?_(u,d("ANY"===s)):1004===u?_(u,d(i.sendFocus)):1005===u?_(u,4):1006===u?_(u,d("SGR"===r)):1015===u?_(u,4):1016===u?_(u,d("SGR_PIXELS"===r)):1048===u?_(u,1):47===u||1047===u||1049===u?_(u,d(h===c)):2004===u?_(u,d(i.bracketedPasteMode)):2026===u?_(u,d(i.synchronizedOutput)):9001===u&&this._optionsService.rawOptions.vtExtensions?.win32InputMode?_(u,d(i.win32InputMode)):_(u,0)}_updateAttrColor(e,t,i,s,r){return 2===t?(e|=50331648,e&=-16777216,e|=d.AttributeData.fromColorRGB([i,s,r])):5===t&&(e&=-67108864,e|=33554432|255&i),e}_extractColor(e,t,i){const s=[0,0,-1,0,0,0];let r=0,n=0;do{if(s[n+r]=e.params[t+n],e.hasSubParams(t+n)){const i=e.getSubParams(t+n);let a=0;do{5===s[1]&&(r=1),s[n+a+1+r]=i[a]}while(++a=2||2===s[1]&&n+r>=5)break;s[1]&&(r=1)}while(++n+t5)&&(e=1),t.extended.underlineStyle=e,t.fg|=268435456,0===e&&(t.fg&=-268435457),t.updateExtended()}_processSGR0(e){e.fg=c.DEFAULT_ATTR_DATA.fg,e.bg=c.DEFAULT_ATTR_DATA.bg,e.extended=e.extended.clone(),e.extended.underlineStyle=0,e.extended.underlineColor&=-67108864,e.updateExtended()}charAttributes(e){if(1===e.length&&0===e.params[0])return this._processSGR0(this._curAttrData),!0;const t=e.length;let i;const s=this._curAttrData;for(let r=0;r=30&&i<=37?(s.fg&=-67108864,s.fg|=16777216|i-30):i>=40&&i<=47?(s.bg&=-67108864,s.bg|=16777216|i-40):i>=90&&i<=97?(s.fg&=-67108864,s.fg|=16777224|i-90):i>=100&&i<=107?(s.bg&=-67108864,s.bg|=16777224|i-100):0===i?this._processSGR0(s):1===i?s.fg|=134217728:3===i?s.bg|=67108864:4===i?(s.fg|=268435456,this._processUnderline(e.hasSubParams(r)?e.getSubParams(r)[0]:1,s)):5===i?s.fg|=536870912:7===i?s.fg|=67108864:8===i?s.fg|=1073741824:9===i?s.fg|=2147483648:2===i?s.bg|=134217728:21===i?this._processUnderline(2,s):22===i?(s.fg&=-134217729,s.bg&=-134217729):23===i?s.bg&=-67108865:24===i?(s.fg&=-268435457,this._processUnderline(0,s)):25===i?s.fg&=-536870913:27===i?s.fg&=-67108865:28===i?s.fg&=-1073741825:29===i?s.fg&=2147483647:39===i?(s.fg&=-67108864,s.fg|=16777215&c.DEFAULT_ATTR_DATA.fg):49===i?(s.bg&=-67108864,s.bg|=16777215&c.DEFAULT_ATTR_DATA.bg):38===i||48===i||58===i?r+=this._extractColor(e,r,s):53===i?s.bg|=1073741824:55===i?s.bg&=-1073741825:221===i&&(this._optionsService.rawOptions.vtExtensions?.kittySgrBoldFaintControl??1)?s.fg&=-134217729:222===i&&(this._optionsService.rawOptions.vtExtensions?.kittySgrBoldFaintControl??1)?s.bg&=-134217729:59===i?(s.extended=s.extended.clone(),s.extended.underlineColor=-1,s.updateExtended()):this._logService.debug("Unknown SGR attribute: %d.",i);return!0}deviceStatus(e){switch(e.params[0]){case 5:this._coreService.triggerDataEvent("");break;case 6:const e=this._activeBuffer.y+1,t=this._activeBuffer.x+1;this._coreService.triggerDataEvent(`[${e};${t}R`)}return!0}deviceStatusPrivate(e){switch(e.params[0]){case 6:const e=this._activeBuffer.y+1,t=this._activeBuffer.x+1;this._coreService.triggerDataEvent(`[?${e};${t}R`);break;case 15:case 25:case 26:case 53:break;case 996:(this._optionsService.rawOptions.vtExtensions?.colorSchemeQuery??1)&&this._onRequestColorSchemeQuery.fire()}return!0}softReset(e){return this._coreService.isCursorHidden=!1,this._onRequestSyncScrollBar.fire(),this._activeBuffer.scrollTop=0,this._activeBuffer.scrollBottom=this._bufferService.rows-1,this._curAttrData=c.DEFAULT_ATTR_DATA.clone(),this._coreService.reset(),this._charsetService.reset(),this._activeBuffer.savedX=0,this._activeBuffer.savedY=this._activeBuffer.ybase,this._activeBuffer.savedCurAttrData.fg=this._curAttrData.fg,this._activeBuffer.savedCurAttrData.bg=this._curAttrData.bg,this._activeBuffer.savedCharset=this._charsetService.charset,this._coreService.decPrivateModes.origin=!1,!0}setCursorStyle(e){const t=0===e.length?1:e.params[0];if(0===t)this._coreService.decPrivateModes.cursorStyle=void 0,this._coreService.decPrivateModes.cursorBlink=void 0;else{switch(t){case 1:case 2:this._coreService.decPrivateModes.cursorStyle="block";break;case 3:case 4:this._coreService.decPrivateModes.cursorStyle="underline";break;case 5:case 6:this._coreService.decPrivateModes.cursorStyle="bar"}const e=t%2==1;this._coreService.decPrivateModes.cursorBlink=e}return!0}setScrollRegion(e){const t=e.params[0]||1;let i;return(e.length<2||(i=e.params[1])>this._bufferService.rows||0===i)&&(i=this._bufferService.rows),i>t&&(this._activeBuffer.scrollTop=t-1,this._activeBuffer.scrollBottom=i-1,this._setCursor(0,0)),!0}windowOptions(e){if(!C(e.params[0],this._optionsService.rawOptions.windowOptions))return!0;const t=e.length>1?e.params[1]:0;switch(e.params[0]){case 14:2!==t&&this._onRequestWindowsOptionsReport.fire(w.GET_WIN_SIZE_PIXELS);break;case 16:this._onRequestWindowsOptionsReport.fire(w.GET_CELL_SIZE_PIXELS);break;case 18:this._bufferService&&this._coreService.triggerDataEvent(`[8;${this._bufferService.rows};${this._bufferService.cols}t`);break;case 22:0!==t&&2!==t||(this._windowTitleStack.push(this._windowTitle),this._windowTitleStack.length>10&&this._windowTitleStack.shift()),0!==t&&1!==t||(this._iconNameStack.push(this._iconName),this._iconNameStack.length>10&&this._iconNameStack.shift());break;case 23:0!==t&&2!==t||this._windowTitleStack.length&&this.setTitle(this._windowTitleStack.pop()),0!==t&&1!==t||this._iconNameStack.length&&this.setIconName(this._iconNameStack.pop())}return!0}saveCursor(e){return this._activeBuffer.savedX=this._activeBuffer.x,this._activeBuffer.savedY=this._activeBuffer.ybase+this._activeBuffer.y,this._activeBuffer.savedCurAttrData.fg=this._curAttrData.fg,this._activeBuffer.savedCurAttrData.bg=this._curAttrData.bg,this._activeBuffer.savedCharset=this._charsetService.charset,this._activeBuffer.savedCharsets=this._charsetService.charsets.slice(),this._activeBuffer.savedGlevel=this._charsetService.glevel,this._activeBuffer.savedOriginMode=this._coreService.decPrivateModes.origin,this._activeBuffer.savedWraparoundMode=this._coreService.decPrivateModes.wraparound,!0}restoreCursor(e){this._activeBuffer.x=this._activeBuffer.savedX||0,this._activeBuffer.y=Math.max(this._activeBuffer.savedY-this._activeBuffer.ybase,0),this._curAttrData.fg=this._activeBuffer.savedCurAttrData.fg,this._curAttrData.bg=this._activeBuffer.savedCurAttrData.bg;for(let e=0;e1;){const e=i.shift(),s=i.shift();if(/^\d+$/.exec(e)){const i=parseInt(e,10);if(E(i))if("?"===s)t.push({type:0,index:i});else{const e=(0,b.parseColor)(s);e&&t.push({type:1,index:i,color:e})}}}return t.length&&this._onColor.fire(t),!0}setHyperlink(e){const t=e.indexOf(";");if(-1===t)return!0;const i=e.slice(0,t).trim(),s=e.slice(t+1);return s?this._createHyperlink(i,s):!i.trim()&&this._finishHyperlink()}_createHyperlink(e,t){this._getCurrentLinkId()&&this._finishHyperlink();const i=e.split(":");let s;const r=i.findIndex(e=>e.startsWith("id="));return-1!==r&&(s=i[r].slice(3)||void 0),this._curAttrData.extended=this._curAttrData.extended.clone(),this._curAttrData.extended.urlId=this._oscLinkService.registerLink({id:s,uri:t}),this._curAttrData.updateExtended(),!0}_finishHyperlink(){return this._curAttrData.extended=this._curAttrData.extended.clone(),this._curAttrData.extended.urlId=0,this._curAttrData.updateExtended(),!0}_setOrReportSpecialColor(e,t){const i=e.split(";");for(let e=0;e=this._specialColors.length);++e,++t)if("?"===i[e])this._onColor.fire([{type:0,index:this._specialColors[t]}]);else{const s=(0,b.parseColor)(i[e]);s&&this._onColor.fire([{type:1,index:this._specialColors[t],color:s}])}return!0}setOrReportFgColor(e){return this._setOrReportSpecialColor(e,0)}setOrReportBgColor(e){return this._setOrReportSpecialColor(e,1)}setOrReportCursorColor(e){return this._setOrReportSpecialColor(e,2)}restoreIndexedColor(e){if(!e)return this._onColor.fire([{type:2}]),!0;const t=[],i=e.split(";");for(let e=0;e=this._bufferService.rows&&(this._activeBuffer.y=this._bufferService.rows-1),this._restrictCursor(),!0}tabSet(){return this._activeBuffer.tabs[this._activeBuffer.x]=!0,!0}reverseIndex(){if(this._restrictCursor(),this._activeBuffer.y===this._activeBuffer.scrollTop){const e=this._activeBuffer.scrollBottom-this._activeBuffer.scrollTop;this._activeBuffer.lines.shiftElements(this._activeBuffer.ybase+this._activeBuffer.y,e,1),this._activeBuffer.lines.set(this._activeBuffer.ybase+this._activeBuffer.y,this._activeBuffer.getBlankLine(this._eraseAttrData())),this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop,this._activeBuffer.scrollBottom)}else this._activeBuffer.y--,this._restrictCursor();return!0}fullReset(){return this._parser.reset(),this._onRequestReset.fire(),!0}reset(){this._curAttrData=c.DEFAULT_ATTR_DATA.clone(),this._eraseAttrDataInternal=c.DEFAULT_ATTR_DATA.clone()}_eraseAttrData(){return this._eraseAttrDataInternal.bg&=-67108864,this._eraseAttrDataInternal.bg|=67108863&this._curAttrData.bg,this._eraseAttrDataInternal}setgLevel(e){return this._charsetService.setgLevel(e),!0}screenAlignmentPattern(){const e=new _.CellData;e.content=1<<22|"E".charCodeAt(0),e.fg=this._curAttrData.fg,e.bg=this._curAttrData.bg,this._setCursor(0,0);for(let t=0;t(this._coreService.triggerDataEvent(`${e}\\`),!0))('"q'===e?`P1$r${this._curAttrData.isProtected()?1:0}"q`:'"p'===e?'P1$r61;1"p':"r"===e?`P1$r${i.scrollTop+1};${i.scrollBottom+1}r`:"m"===e?"P1$r0m":" q"===e?`P1$r${{block:2,underline:4,bar:6}[s.cursorStyle]-(s.cursorBlink?1:0)} q`:"P0$r")}markRangeDirty(e,t){this._dirtyRowTracker.markRangeDirty(e,t)}kittyKeyboardSet(e){if(!this._optionsService.rawOptions.vtExtensions?.kittyKeyboard)return!0;const t=e.params[0]||0,i=e.length>1&&e.params[1]||1,s=this._coreService.kittyKeyboard;switch(i){case 1:s.flags=t;break;case 2:s.flags|=t;break;case 3:s.flags&=~t}return!0}kittyKeyboardQuery(e){if(!this._optionsService.rawOptions.vtExtensions?.kittyKeyboard)return!0;const t=this._coreService.kittyKeyboard.flags;return this._coreService.triggerDataEvent(`[?${t}u`),!0}kittyKeyboardPush(e){if(!this._optionsService.rawOptions.vtExtensions?.kittyKeyboard)return!0;const t=e.params[0]||0,i=this._coreService.kittyKeyboard,s=this._bufferService.buffer===this._bufferService.buffers.alt?i.altStack:i.mainStack;return s.length>=16&&s.shift(),s.push(i.flags),i.flags=t,!0}kittyKeyboardPop(e){if(!this._optionsService.rawOptions.vtExtensions?.kittyKeyboard)return!0;const t=Math.max(1,e.params[0]||1),i=this._coreService.kittyKeyboard,s=this._bufferService.buffer===this._bufferService.buffers.alt?i.altStack:i.mainStack;for(let e=0;e0;e++)i.flags=s.pop();return 0===s.length&&t>0&&(i.flags=0),!0}}t.InputHandler=k;let A=class{constructor(e){this._bufferService=e,this.clearRange()}clearRange(){this.start=this._bufferService.buffer.y,this.end=this._bufferService.buffer.y}markDirty(e){ethis.end&&(this.end=e)}markRangeDirty(e,t){e>t&&(B=e,e=t,t=B),ethis.end&&(this.end=t)}markAllDirty(){this.markRangeDirty(0,this._bufferService.rows-1)}};function E(e){return 0<=e&&e<256}A=s([r(0,u.IBufferService)],A)},812(e,t){function i(e){return{dispose:e}}function s(e){if(!e)return e;if(Array.isArray(e)){for(const t of e)t.dispose();return[]}return e.dispose(),e}Object.defineProperty(t,"__esModule",{value:!0}),t.MutableDisposable=t.Disposable=t.DisposableStore=void 0,t.toDisposable=i,t.dispose=s,t.combinedDisposable=function(...e){return i(()=>s(e))};class r{constructor(){this._disposables=new Set,this._isDisposed=!1}get isDisposed(){return this._isDisposed}add(e){return this._isDisposed?e.dispose():this._disposables.add(e),e}dispose(){if(!this._isDisposed){this._isDisposed=!0;for(const e of this._disposables)e.dispose();this._disposables.clear()}}clear(){for(const e of this._disposables)e.dispose();this._disposables.clear()}}t.DisposableStore=r;class n{constructor(){this._store=new r}dispose(){this._store.dispose()}_register(e){return this._store.add(e)}}t.Disposable=n,n.None=Object.freeze({dispose(){}}),t.MutableDisposable=class{constructor(){this._isDisposed=!1}get value(){return this._isDisposed?void 0:this._value}set value(e){this._isDisposed||e===this._value||(this._value?.dispose(),this._value=e)}clear(){this.value=void 0}dispose(){this._isDisposed=!0,this._value?.dispose(),this._value=void 0}}},701(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.isChromeOS=t.isLinux=t.isWindows=t.isMac=t.isSafari=t.isLegacyEdge=t.isChrome=t.isFirefox=t.isNode=void 0,t.getZoomFactor=function(e){return 1},t.getSafariVersion=function(){if(!t.isSafari)return 0;const e=i.match(/Version\/(\d+)/);return null===e||e.length<2?0:parseInt(e[1],10)},t.isNode=!("undefined"==typeof process||!("title"in process)||"undefined"!=typeof navigator&&!navigator.userAgent.startsWith("Node.js/"));const i=t.isNode?"node":navigator.userAgent,s=t.isNode?"node":navigator.platform;t.isFirefox=i.includes("Firefox"),t.isChrome=i.includes("Chrome"),t.isLegacyEdge=i.includes("Edge"),t.isSafari=/^((?!chrome|android).)*safari/i.test(i),t.isMac=["Macintosh","MacIntel","MacPPC","Mac68K"].includes(s),t.isWindows=["Windows","Win16","Win32","WinCE"].includes(s),t.isLinux=s.indexOf("Linux")>=0,t.isChromeOS=/\bCrOS\b/.test(i)},220(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.LimitedStringBuilder=t.StringBuilder=void 0;class i{constructor(){this._chunks=[],this._length=0}get length(){return this._length}reset(){this._chunks.length=0,this._length=0}append(e){this._chunks.push(e),this._length+=e.length}toString(){return this._chunks.join("")}}t.StringBuilder=i,t.LimitedStringBuilder=class{constructor(e){this._limit=e,this._builder=new i}get length(){return this._builder.length}get limit(){return this._limit}reset(){this._builder.reset()}append(e){return this._builder.append(e),this._builder.length>this._limit&&(this._builder.reset(),!0)}toString(){return this._builder.toString()}}},168(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.DebouncedIdleTask=t.IdleTaskQueue=t.PriorityTaskQueue=void 0;class i{constructor(e){this._tasks=[],this._i=0,this._logService=e}enqueue(e){this._tasks.push(e),this._start()}flush(){for(;this._ii)return r-t<-20&&this._logService.warn(`task queue exceeded allotted deadline by ${Math.abs(Math.round(r-t))}ms`),void this._start();r=i}this.clear()}}class s extends i{_requestCallback(e){return setTimeout(()=>e(this._createDeadline(16)))}_cancelCallback(e){clearTimeout(e)}_createDeadline(e){const t=performance.now()+e;return{timeRemaining:()=>Math.max(0,t-performance.now())}}}t.PriorityTaskQueue=s,t.IdleTaskQueue="requestIdleCallback"in globalThis?class extends i{_requestCallback(e){return requestIdleCallback(e)}_cancelCallback(e){cancelIdleCallback(e)}}:s,t.DebouncedIdleTask=class{constructor(e){this._queue=new t.IdleTaskQueue(e)}set(e){this._queue.clear(),this._queue.enqueue(e)}flush(){this._queue.flush()}dispose(){this._queue.clear()}}},804(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.XTERM_VERSION=void 0,t.XTERM_VERSION="6.0.0"},882(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.updateWindowsModeWrappedState=function(e){const t=e.buffer.lines.get(e.buffer.ybase+e.buffer.y-1),i=t?.get(e.cols-1),r=e.buffer.lines.get(e.buffer.ybase+e.buffer.y);r&&i&&(r.isWrapped=i[s.CHAR_DATA_CODE_INDEX]!==s.NULL_CELL_CODE&&i[s.CHAR_DATA_CODE_INDEX]!==s.WHITESPACE_CELL_CODE)};const s=i(938)},451(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.ExtendedAttrs=t.AttributeData=void 0;class i{constructor(){this.fg=0,this.bg=0,this.extended=new s}static toColorRGB(e){return[e>>>16&255,e>>>8&255,255&e]}static fromColorRGB(e){return(255&e[0])<<16|(255&e[1])<<8|255&e[2]}clone(){const e=new i;return e.fg=this.fg,e.bg=this.bg,e.extended=this.extended.clone(),e}isInverse(){return 67108864&this.fg}isBold(){return 134217728&this.fg}isUnderline(){return this.hasExtendedAttrs()&&0!==this.extended.underlineStyle?1:268435456&this.fg}isBlink(){return 536870912&this.fg}isInvisible(){return 1073741824&this.fg}isItalic(){return 67108864&this.bg}isDim(){return 134217728&this.bg}isStrikethrough(){return 2147483648&this.fg}isProtected(){return 536870912&this.bg}isOverline(){return 1073741824&this.bg}getFgColorMode(){return 50331648&this.fg}getBgColorMode(){return 50331648&this.bg}isFgRGB(){return!(50331648&~this.fg)}isBgRGB(){return!(50331648&~this.bg)}isFgPalette(){return 16777216==(50331648&this.fg)||33554432==(50331648&this.fg)}isBgPalette(){return 16777216==(50331648&this.bg)||33554432==(50331648&this.bg)}isFgDefault(){return!(50331648&this.fg)}isBgDefault(){return!(50331648&this.bg)}isAttributeDefault(){return 0===this.fg&&0===this.bg}getFgColor(){switch(50331648&this.fg){case 16777216:case 33554432:return 255&this.fg;case 50331648:return 16777215&this.fg;default:return-1}}getBgColor(){switch(50331648&this.bg){case 16777216:case 33554432:return 255&this.bg;case 50331648:return 16777215&this.bg;default:return-1}}hasExtendedAttrs(){return 268435456&this.bg}updateExtended(){this.extended.isEmpty()?this.bg&=-268435457:this.bg|=268435456}getUnderlineColor(){if(268435456&this.bg&&~this.extended.underlineColor)switch(50331648&this.extended.underlineColor){case 16777216:case 33554432:return 255&this.extended.underlineColor;case 50331648:return 16777215&this.extended.underlineColor;default:return this.getFgColor()}return this.getFgColor()}getUnderlineColorMode(){return 268435456&this.bg&&~this.extended.underlineColor?50331648&this.extended.underlineColor:this.getFgColorMode()}isUnderlineColorRGB(){return 268435456&this.bg&&~this.extended.underlineColor?!(50331648&~this.extended.underlineColor):this.isFgRGB()}isUnderlineColorPalette(){return 268435456&this.bg&&~this.extended.underlineColor?16777216==(50331648&this.extended.underlineColor)||33554432==(50331648&this.extended.underlineColor):this.isFgPalette()}isUnderlineColorDefault(){return 268435456&this.bg&&~this.extended.underlineColor?!(50331648&this.extended.underlineColor):this.isFgDefault()}getUnderlineStyle(){return 268435456&this.fg?268435456&this.bg?this.extended.underlineStyle:1:0}getUnderlineVariantOffset(){return this.extended.underlineVariantOffset}}t.AttributeData=i;class s{get ext(){return this._urlId?-469762049&this._ext|this.underlineStyle<<26:this._ext}set ext(e){this._ext=e}get underlineStyle(){return this._urlId?5:(469762048&this._ext)>>26}set underlineStyle(e){this._ext&=-469762049,this._ext|=e<<26&469762048}get underlineColor(){return 67108863&this._ext}set underlineColor(e){this._ext&=-67108864,this._ext|=67108863&e}get urlId(){return this._urlId}set urlId(e){this._urlId=e}get underlineVariantOffset(){const e=(3758096384&this._ext)>>29;return e<0?4294967288^e:e}set underlineVariantOffset(e){this._ext&=536870911,this._ext|=e<<29&3758096384}constructor(e=0,t=0){this._ext=0,this._urlId=0,this._ext=e,this._urlId=t}clone(){return new s(this._ext,this._urlId)}isEmpty(){return 0===this.underlineStyle&&0===this._urlId}}t.ExtendedAttrs=s},73(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.Buffer=t.MAX_BUFFER_SIZE=void 0;const s=i(639),r=i(812),n=i(168),a=i(451),o=i(107),h=i(732),c=i(55),l=i(938),_=i(158),d=i(760);t.MAX_BUFFER_SIZE=4294967295;class u extends r.Disposable{constructor(e,t,i,a){super(),this._hasScrollback=e,this._optionsService=t,this._bufferService=i,this._logService=a,this.ydisp=0,this.ybase=0,this.y=0,this.x=0,this.tabs={},this.savedY=0,this.savedX=0,this.savedCurAttrData=o.DEFAULT_ATTR_DATA.clone(),this.savedCharset=d.DEFAULT_CHARSET,this.savedCharsets=[],this.savedGlevel=0,this.savedOriginMode=!1,this.savedWraparoundMode=!0,this.markers=[],this._nullCell=c.CellData.fromCharData([0,l.NULL_CELL_CHAR,l.NULL_CELL_WIDTH,l.NULL_CELL_CODE]),this._whitespaceCell=c.CellData.fromCharData([0,l.WHITESPACE_CELL_CHAR,l.WHITESPACE_CELL_WIDTH,l.WHITESPACE_CELL_CODE]),this._isClearing=!1,this._memoryCleanupPosition=0,this._cols=this._bufferService.cols,this._rows=this._bufferService.rows,this.lines=new s.CircularList(this._getCorrectBufferLength(this._rows)),this.scrollTop=0,this.scrollBottom=this._rows-1,this.setupTabStops(),this._memoryCleanupQueue=new n.IdleTaskQueue(this._logService),this._register((0,r.toDisposable)(()=>this._memoryCleanupQueue.clear())),this._register((0,r.toDisposable)(()=>this.clearAllMarkers()))}getNullCell(e){return e?(this._nullCell.fg=e.fg,this._nullCell.bg=e.bg,this._nullCell.extended=e.extended):(this._nullCell.fg=0,this._nullCell.bg=0,this._nullCell.extended=new a.ExtendedAttrs),this._nullCell}getWhitespaceCell(e){return e?(this._whitespaceCell.fg=e.fg,this._whitespaceCell.bg=e.bg,this._whitespaceCell.extended=e.extended):(this._whitespaceCell.fg=0,this._whitespaceCell.bg=0,this._whitespaceCell.extended=new a.ExtendedAttrs),this._whitespaceCell}getBlankLine(e,t){return new o.BufferLine(this._bufferService.cols,this.getNullCell(e),t)}get hasScrollback(){return this._hasScrollback&&this.lines.maxLength>this._rows}get isCursorInViewport(){const e=this.ybase+this.y-this.ydisp;return e>=0&&et.MAX_BUFFER_SIZE?t.MAX_BUFFER_SIZE:i}fillViewportRows(e){if(0===this.lines.length){e??=o.DEFAULT_ATTR_DATA;let t=this._rows;for(;t--;)this.lines.push(this.getBlankLine(e))}}clear(){this.ydisp=0,this.ybase=0,this.y=0,this.x=0,this.lines=new s.CircularList(this._getCorrectBufferLength(this._rows)),this.scrollTop=0,this.scrollBottom=this._rows-1,this.setupTabStops()}resize(e,t){const i=this.getNullCell(o.DEFAULT_ATTR_DATA);let s=0;const r=this._getCorrectBufferLength(t);if(r>this.lines.maxLength&&(this.lines.maxLength=r),this.lines.length>0){if(this._cols0&&this.lines.length<=this.ybase+this.y+n+1?(this.ybase--,n++,this.ydisp>0&&this.ydisp--):this.lines.push(new o.BufferLine(e,i,!1)));else for(let e=this._rows;e>t;e--)this.lines.length>t+this.ybase&&(this.lines.length>this.ybase+this.y+1?this.lines.pop():(this.ybase++,this.ydisp++));if(r0&&(this.lines.trimStart(e),this.ybase=Math.max(this.ybase-e,0),this.ydisp=Math.max(this.ydisp-e,0),this.savedY=Math.max(this.savedY-e,0)),this.lines.maxLength=r}this.x=Math.min(this.x,e-1),this.y=Math.min(this.y,t-1),n&&(this.y+=n),this.savedX=Math.min(this.savedX,e-1),this.scrollTop=0}if(this.scrollBottom=t-1,this._isReflowEnabled&&(this._reflow(e,t),this._cols>e))for(let t=0;t0){const e=Math.max(0,this.lines.length-this.ybase-1);this.y=Math.min(this.y,e)}this._memoryCleanupQueue.clear(),s>.1*this.lines.length&&(this._memoryCleanupPosition=0,this._memoryCleanupQueue.enqueue(()=>this._batchedMemoryCleanup()))}_batchedMemoryCleanup(){let e=!0;this._memoryCleanupPosition>=this.lines.length&&(this._memoryCleanupPosition=0,e=!1);let t=0;for(;this._memoryCleanupPosition100)return!0;return e}get _isReflowEnabled(){const e=this._optionsService.rawOptions.windowsPty;return e&&e.buildNumber?this._hasScrollback&&"conpty"===e.backend&&e.buildNumber>=21376:this._hasScrollback}_reflow(e,t){this._cols!==e&&(e>this._cols?this._reflowLarger(e,t):this._reflowSmaller(e,t))}_reflowLarger(e,t){const i=this._optionsService.rawOptions.reflowCursorLine,s=(0,h.reflowLargerGetLinesToRemove)(this.lines,this._cols,e,this.ybase+this.y,this.getNullCell(o.DEFAULT_ATTR_DATA),i);if(s.length>0){const i=(0,h.reflowLargerCreateNewLayout)(this.lines,s);(0,h.reflowLargerApplyNewLayout)(this.lines,i.layout),this._reflowLargerAdjustViewport(e,t,i.countRemoved)}}_reflowLargerAdjustViewport(e,t,i){const s=this.getNullCell(o.DEFAULT_ATTR_DATA);let r=i;for(;r-- >0;)0===this.ybase?(this.y>0&&this.y--,this.lines.length=0;a--){let c=this.lines.get(a);if(!c||!c.isWrapped&&c.getTrimmedLength()<=e)continue;const l=[c];for(;c.isWrapped&&a>0;)c=this.lines.get(--a),l.unshift(c);if(!i){const e=this.ybase+this.y;if(e>=a&&e0&&(r.push({start:a+l.length+n,newLines:p}),n+=p.length),l.push(...p);let g=d.length-1,v=d[g];0===v&&(g--,v=d[g]);let b=l.length-u-1,S=_;for(;b>=0;){const e=Math.min(S,v);if(void 0===l[g])break;if(l[g].copyCellsFrom(l[b],S-e,v-e,e,!0),v-=e,0===v&&(g--,v=d[g]),S-=e,0===S){b--;const e=Math.max(b,0);S=(0,h.getWrappedLineTrimmedLength)(l,e,this._cols)}}for(let t=0;t0;)0===this.ybase?this.y0){const e=[],t=[];for(let e=0;e=0;c--)if(o&&o.start>s+h){for(let e=o.newLines.length-1;e>=0;e--)this.lines.set(c--,o.newLines[e]);c++,e.push({index:s+1,amount:o.newLines.length}),h+=o.newLines.length,o=r[++a]}else this.lines.set(c,t[s--]);let c=0;for(let t=e.length-1;t>=0;t--)e[t].index+=c,this.lines.onInsertEmitter.fire(e[t]),c+=e[t].amount;const l=Math.max(0,i+n-this.lines.maxLength);l>0&&this.lines.onTrimEmitter.fire(l)}}translateBufferLineToString(e,t,i=0,s){const r=this.lines.get(e);return r?r.translateToString(t,i,s):""}getWrappedRangeForLine(e){let t=e,i=e;for(;t>0&&this.lines.get(t).isWrapped;)t--;for(;i+10;);return e>=this._cols?this._cols-1:e<0?0:e}nextStop(e){for(e??=this.x;!this.tabs[++e]&&e=this._cols?this._cols-1:e<0?0:e}clearMarkers(e){this._isClearing=!0;for(let t=0;t{t.line-=e,t.line<0&&t.dispose()})),t.register(this.lines.onInsert(e=>{t.line>=e.index&&(t.line+=e.amount)})),t.register(this.lines.onDelete(e=>{t.line>=e.index&&t.linee.index&&(t.line-=e.amount)})),t.register(t.onDispose(()=>this._removeMarker(t))),t}_removeMarker(e){this._isClearing||this.markers.splice(this.markers.indexOf(e),1)}}t.Buffer=u},107(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.BufferLine=t.DEFAULT_ATTR_DATA=void 0;const s=i(451),r=i(55),n=i(938),a=i(726);t.DEFAULT_ATTR_DATA=Object.freeze(new s.AttributeData);let o=0;const h=new r.CellData,c=t.DEFAULT_ATTR_DATA.extended.clone();class l{constructor(e,t,i=!1){this.isWrapped=i,this._combined={},this._extendedAttrs={},this._cacheValid=!1,this._cache="",this._cacheTrimmed=!1,this._data=new Uint32Array(3*e);const s=t??r.CellData.fromCharData([0,n.NULL_CELL_CHAR,n.NULL_CELL_WIDTH,n.NULL_CELL_CODE]);for(let t=0;t>22,2097152&t?this._combined[e].charCodeAt(this._combined[e].length-1):i]}set(e,t){this._cacheValid=!1,this._data[3*e+1]=t[n.CHAR_DATA_ATTR_INDEX],t[n.CHAR_DATA_CHAR_INDEX].length>1?(this._combined[e]=t[1],this._data[3*e+0]=2097152|e|t[n.CHAR_DATA_WIDTH_INDEX]<<22):this._data[3*e+0]=t[n.CHAR_DATA_CHAR_INDEX].charCodeAt(0)|t[n.CHAR_DATA_WIDTH_INDEX]<<22}getWidth(e){return this._data[3*e+0]>>22}hasWidth(e){return 12582912&this._data[3*e+0]}getFg(e){return this._data[3*e+1]}getBg(e){return this._data[3*e+2]}hasContent(e){return 4194303&this._data[3*e+0]}getCodePoint(e){const t=this._data[3*e+0];return 2097152&t?this._combined[e].charCodeAt(this._combined[e].length-1):2097151&t}isCombined(e){return 2097152&this._data[3*e+0]}getString(e){const t=this._data[3*e+0];return 2097152&t?this._combined[e]:2097151&t?(0,a.stringFromCodePoint)(2097151&t):""}isProtected(e){return 536870912&this._data[3*e+2]}loadCell(e,t){return o=3*e,t.content=this._data[o+0],t.fg=this._data[o+1],t.bg=this._data[o+2],2097152&t.content?t.combinedData=this._combined[e]:t.combinedData="",268435456&t.bg?t.extended=this._extendedAttrs[e]:(c._ext=0,c._urlId=0,t.extended=c),t}setCell(e,t){this._cacheValid=!1,2097152&t.content&&(this._combined[e]=t.combinedData),268435456&t.bg&&(this._extendedAttrs[e]=t.extended),this._data[3*e+0]=t.content,this._data[3*e+1]=t.fg,this._data[3*e+2]=t.bg}setCellFromCodepoint(e,t,i,s){this._cacheValid=!1,268435456&s.bg&&(this._extendedAttrs[e]=s.extended);const r=3*e;this._data[r+0]=t|i<<22,this._data[r+1]=s.fg,this._data[r+2]=s.bg}addCodepointToCell(e,t,i){this._cacheValid=!1;let s=this._data[3*e+0];2097152&s?this._combined[e]+=(0,a.stringFromCodePoint)(t):2097151&s?(this._combined[e]=(0,a.stringFromCodePoint)(2097151&s)+(0,a.stringFromCodePoint)(t),s&=-2097152,s|=2097152):s=t|1<<22,i&&(s&=-12582913,s|=i<<22),this._data[3*e+0]=s}insertCells(e,t,i){if(this._cacheValid=!1,(e%=this.length)&&2===this.getWidth(e-1)&&this.setCellFromCodepoint(e-1,0,1,i),t=0;--i)this.setCell(e+t+i,this.loadCell(e+i,h));for(let s=0;sthis.length){if(this._data.buffer.byteLength>=4*i)this._data=new Uint32Array(this._data.buffer,0,i);else{const e=new Uint32Array(i);e.set(this._data),this._data=e}for(let i=this.length;i=e&&delete this._combined[s]}const s=Object.keys(this._extendedAttrs);for(let t=0;t=e&&delete this._extendedAttrs[i]}}return this.length=e,4*i*2=0;--e)if(4194303&this._data[3*e+0])return e+(this._data[3*e+0]>>22);return 0}getNoBgTrimmedLength(){for(let e=this.length-1;e>=0;--e)if(4194303&this._data[3*e+0]||50331648&this._data[3*e+2])return e+(this._data[3*e+0]>>22);return 0}copyCellsFrom(e,t,i,s,r){this._cacheValid=!1;const n=e._data;if(r)for(let r=s-1;r>=0;r--){for(let e=0;e<3;e++)this._data[3*(i+r)+e]=n[3*(t+r)+e];this._copyCellMapsFrom(e,t+r,i+r)}else for(let r=0;r>22||1}s&&s.push(t);const h=o.join("");return r&&(this._cache=h,this._cacheValid=!0,this._cacheTrimmed=!!e),h}_copyCellMapsFrom(e,t,i){const s=3*t;2097152&e._data[s+0]&&(this._combined[i]=e._combined[t]),268435456&e._data[s+2]&&(this._extendedAttrs[i]=e._extendedAttrs[t])}_copySparseMapsFrom(e){this._combined={},this._extendedAttrs={};for(let t=0;t=h&&r0&&(e>d||0===_[e].getTrimmedLength());e--)g++;g>0&&(o.push(h+_.length-g),o.push(g)),h+=_.length-1}return o},t.reflowLargerCreateNewLayout=function(e,t){const i=[];let s=0,r=t[s],n=0;for(let a=0;ac&&(a-=c,o++);const l=2===e[o].getWidth(a-1);l&&a--;const _=l?s-1:s;r.push(_),h+=_}return r},t.getWrappedLineTrimmedLength=i},478(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.BufferSet=void 0;const s=i(812),r=i(73),n=i(636);class a extends s.Disposable{constructor(e,t,i){super(),this._optionsService=e,this._bufferService=t,this._logService=i,this._normalBuffer=this._register(new s.MutableDisposable),this._altBuffer=this._register(new s.MutableDisposable),this._onBufferActivate=this._register(new n.Emitter),this.onBufferActivate=this._onBufferActivate.event,this.reset(),this._register(this._optionsService.onSpecificOptionChange("scrollback",()=>this.resize(this._bufferService.cols,this._bufferService.rows))),this._register(this._optionsService.onSpecificOptionChange("tabStopWidth",()=>this.setupTabStops()))}reset(){this._normal=new r.Buffer(!0,this._optionsService,this._bufferService,this._logService),this._normalBuffer.value=this._normal,this._normal.fillViewportRows(),this._alt=new r.Buffer(!1,this._optionsService,this._bufferService,this._logService),this._altBuffer.value=this._alt,this._activeBuffer=this._normal,this._onBufferActivate.fire({activeBuffer:this._normal,inactiveBuffer:this._alt}),this.setupTabStops()}get alt(){return this._alt}get active(){return this._activeBuffer}get normal(){return this._normal}activateNormalBuffer(){this._activeBuffer!==this._normal&&(this._normal.x=this._alt.x,this._normal.y=this._alt.y,this._alt.clearAllMarkers(),this._alt.clear(),this._activeBuffer=this._normal,this._onBufferActivate.fire({activeBuffer:this._normal,inactiveBuffer:this._alt}))}activateAltBuffer(e){this._activeBuffer!==this._alt&&(this._alt.fillViewportRows(e),this._alt.x=this._normal.x,this._alt.y=this._normal.y,this._activeBuffer=this._alt,this._onBufferActivate.fire({activeBuffer:this._alt,inactiveBuffer:this._normal}))}resize(e,t){this._normal.resize(e,t),this._alt.resize(e,t),this.setupTabStops(e)}setupTabStops(e){this._normal.setupTabStops(e),this._alt.setupTabStops(e)}}t.BufferSet=a},55(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.CellData=void 0;const s=i(726),r=i(938),n=i(451);class a extends n.AttributeData{constructor(){super(...arguments),this.content=0,this.fg=0,this.bg=0,this.extended=new n.ExtendedAttrs,this.combinedData=""}static fromCharData(e){const t=new a;return t.setFromCharData(e),t}isCombined(){return 2097152&this.content}getWidth(){return this.content>>22}getChars(){return 2097152&this.content?this.combinedData:2097151&this.content?(0,s.stringFromCodePoint)(2097151&this.content):""}getCode(){return this.isCombined()?this.combinedData.charCodeAt(this.combinedData.length-1):2097151&this.content}setFromCharData(e){this.fg=e[r.CHAR_DATA_ATTR_INDEX],this.bg=0;let t=!1;if(e[r.CHAR_DATA_CHAR_INDEX].length>2)t=!0;else if(2===e[r.CHAR_DATA_CHAR_INDEX].length){const i=e[r.CHAR_DATA_CHAR_INDEX].charCodeAt(0);if(55296<=i&&i<=56319){const s=e[r.CHAR_DATA_CHAR_INDEX].charCodeAt(1);56320<=s&&s<=57343?this.content=1024*(i-55296)+s-56320+65536|e[r.CHAR_DATA_WIDTH_INDEX]<<22:t=!0}else t=!0}else this.content=e[r.CHAR_DATA_CHAR_INDEX].charCodeAt(0)|e[r.CHAR_DATA_WIDTH_INDEX]<<22;t&&(this.combinedData=e[r.CHAR_DATA_CHAR_INDEX],this.content=2097152|e[r.CHAR_DATA_WIDTH_INDEX]<<22)}getAsCharData(){return[this.fg,this.getChars(),this.getWidth(),this.getCode()]}attributesEquals(e){if(this.getFgColorMode()!==e.getFgColorMode()||this.getFgColor()!==e.getFgColor())return!1;if(this.getBgColorMode()!==e.getBgColorMode()||this.getBgColor()!==e.getBgColor())return!1;if(this.isInverse()!==e.isInverse())return!1;if(this.isBold()!==e.isBold())return!1;if(this.isUnderline()!==e.isUnderline())return!1;if(this.isUnderline()){if(this.getUnderlineStyle()!==e.getUnderlineStyle())return!1;const t=this.isUnderlineColorDefault(),i=e.isUnderlineColorDefault();if(!t||!i){if(t!==i)return!1;if(this.getUnderlineColor()!==e.getUnderlineColor())return!1;if(this.getUnderlineColorMode()!==e.getUnderlineColorMode())return!1}}return this.isOverline()===e.isOverline()&&this.isBlink()===e.isBlink()&&this.isInvisible()===e.isInvisible()&&this.isItalic()===e.isItalic()&&this.isDim()===e.isDim()&&this.isStrikethrough()===e.isStrikethrough()}}t.CellData=a},938(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.WHITESPACE_CELL_CODE=t.WHITESPACE_CELL_WIDTH=t.WHITESPACE_CELL_CHAR=t.NULL_CELL_CODE=t.NULL_CELL_WIDTH=t.NULL_CELL_CHAR=t.CHAR_DATA_CODE_INDEX=t.CHAR_DATA_WIDTH_INDEX=t.CHAR_DATA_CHAR_INDEX=t.CHAR_DATA_ATTR_INDEX=t.DEFAULT_EXT=t.DEFAULT_ATTR=t.DEFAULT_COLOR=void 0,t.DEFAULT_COLOR=0,t.DEFAULT_ATTR=t.DEFAULT_COLOR<<9|256,t.DEFAULT_EXT=0,t.CHAR_DATA_ATTR_INDEX=0,t.CHAR_DATA_CHAR_INDEX=1,t.CHAR_DATA_WIDTH_INDEX=2,t.CHAR_DATA_CODE_INDEX=3,t.NULL_CELL_CHAR="",t.NULL_CELL_WIDTH=1,t.NULL_CELL_CODE=0,t.WHITESPACE_CELL_CHAR=" ",t.WHITESPACE_CELL_WIDTH=1,t.WHITESPACE_CELL_CODE=32},158(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.Marker=void 0;const s=i(812),r=i(636);class n{get id(){return this._id}constructor(e){this.line=e,this.isDisposed=!1,this._disposables=[],this._id=n._nextId++,this._onDispose=this.register(new r.Emitter),this.onDispose=this._onDispose.event}dispose(){this.isDisposed||(this.isDisposed=!0,this.line=-1,this._onDispose.fire(),(0,s.dispose)(this._disposables),this._disposables.length=0)}register(e){return this._disposables.push(e),e}}t.Marker=n,n._nextId=1},760(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.DEFAULT_CHARSET=t.CHARSETS=void 0,t.CHARSETS={},t.DEFAULT_CHARSET=t.CHARSETS.B,t.CHARSETS[0]={"`":"◆",a:"▒",b:"␉",c:"␌",d:"␍",e:"␊",f:"°",g:"±",h:"␤",i:"␋",j:"┘",k:"┐",l:"┌",m:"└",n:"┼",o:"⎺",p:"⎻",q:"─",r:"⎼",s:"⎽",t:"├",u:"┤",v:"┴",w:"┬",x:"│",y:"≤",z:"≥","{":"π","|":"≠","}":"£","~":"·"},t.CHARSETS.A={"#":"£"},t.CHARSETS.B=void 0,t.CHARSETS[4]={"#":"£","@":"¾","[":"ij","\\":"½","]":"|","{":"¨","|":"f","}":"¼","~":"´"},t.CHARSETS.C=t.CHARSETS[5]={"[":"Ä","\\":"Ö","]":"Å","^":"Ü","`":"é","{":"ä","|":"ö","}":"å","~":"ü"},t.CHARSETS.R={"#":"£","@":"à","[":"°","\\":"ç","]":"§","{":"é","|":"ù","}":"è","~":"¨"},t.CHARSETS.Q={"@":"à","[":"â","\\":"ç","]":"ê","^":"î","`":"ô","{":"é","|":"ù","}":"è","~":"û"},t.CHARSETS.K={"@":"§","[":"Ä","\\":"Ö","]":"Ü","{":"ä","|":"ö","}":"ü","~":"ß"},t.CHARSETS.Y={"#":"£","@":"§","[":"°","\\":"ç","]":"é","`":"ù","{":"à","|":"ò","}":"è","~":"ì"},t.CHARSETS.E=t.CHARSETS[6]={"@":"Ä","[":"Æ","\\":"Ø","]":"Å","^":"Ü","`":"ä","{":"æ","|":"ø","}":"å","~":"ü"},t.CHARSETS.Z={"#":"£","@":"§","[":"¡","\\":"Ñ","]":"¿","{":"°","|":"ñ","}":"ç"},t.CHARSETS.H=t.CHARSETS[7]={"@":"É","[":"Ä","\\":"Ö","]":"Å","^":"Ü","`":"é","{":"ä","|":"ö","}":"å","~":"ü"},t.CHARSETS["="]={"#":"ù","@":"à","[":"é","\\":"ç","]":"ê","^":"î",_:"è","`":"ô","{":"ä","|":"ö","}":"ü","~":"û"}},726(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.Utf8ToUtf32=t.StringToUtf32=void 0,t.stringFromCodePoint=function(e){return e>65535?(e-=65536,String.fromCharCode(55296+(e>>10))+String.fromCharCode(e%1024+56320)):String.fromCharCode(e)},t.utf32ToString=function(e,t=0,i=e.length){let s="";for(let r=t;r65535?(t-=65536,s+=String.fromCharCode(55296+(t>>10))+String.fromCharCode(t%1024+56320)):s+=String.fromCharCode(t)}return s},t.StringToUtf32=class{constructor(){this._interim=0}clear(){this._interim=0}decode(e,t){const i=e.length;if(!i)return 0;let s=0,r=0;if(this._interim){const i=e.charCodeAt(r++);56320<=i&&i<=57343?t[s++]=1024*(this._interim-55296)+i-56320+65536:(t[s++]=this._interim,t[s++]=i),this._interim=0}for(let n=r;n=i)return this._interim=r,s;const a=e.charCodeAt(n);56320<=a&&a<=57343?t[s++]=1024*(r-55296)+a-56320+65536:(t[s++]=r,t[s++]=a);continue}65279!==r&&(t[s++]=r)}return s}},t.Utf8ToUtf32=class{constructor(){this.interim=new Uint8Array(3)}clear(){this.interim.fill(0)}decode(e,t){const i=e.length;if(!i)return 0;let s,r,n,a,o,h=0,c=0;if(this.interim[0]){let s=!1,r=this.interim[0];r&=192==(224&r)?31:224==(240&r)?15:7;let n,a=0;for(;(n=this.interim[++a])&&a<4;)r<<=6,r|=63&n;const o=192==(224&this.interim[0])?2:224==(240&this.interim[0])?3:4,l=o-a;for(;c=i)return 0;if(n=e[c++],128!=(192&n)){c--,s=!0;break}this.interim[a++]=n,r<<=6,r|=63&n}s||(2===o?r<128?c--:t[h++]=r:3===o?r<2048||r>=55296&&r<=57343||65279===r||(t[h++]=r):r<65536||r>1114111||(t[h++]=r)),this.interim.fill(0)}const l=i-4;let _=c;for(;_=i)return this.interim[0]=s,h;if(r=e[_++],128!=(192&r)){_--;continue}if(o=(31&s)<<6|63&r,o<128){_--;continue}t[h++]=o}else if(224==(240&s)){if(_>=i)return this.interim[0]=s,h;if(r=e[_++],128!=(192&r)){_--;continue}if(_>=i)return this.interim[0]=s,this.interim[1]=r,h;if(n=e[_++],128!=(192&n)){_--;continue}if(o=(15&s)<<12|(63&r)<<6|63&n,o<2048||o>=55296&&o<=57343||65279===o)continue;t[h++]=o}else if(240==(248&s)){if(_>=i)return this.interim[0]=s,h;if(r=e[_++],128!=(192&r)){_--;continue}if(_>=i)return this.interim[0]=s,this.interim[1]=r,h;if(n=e[_++],128!=(192&n)){_--;continue}if(_>=i)return this.interim[0]=s,this.interim[1]=r,this.interim[2]=n,h;if(a=e[_++],128!=(192&a)){_--;continue}if(o=(7&s)<<18|(63&r)<<12|(63&n)<<6|63&a,o<65536||o>1114111)continue;t[h++]=o}}return h}}},428(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.UnicodeV6=void 0;const s=i(415),r=[[768,879],[1155,1158],[1160,1161],[1425,1469],[1471,1471],[1473,1474],[1476,1477],[1479,1479],[1536,1539],[1552,1557],[1611,1630],[1648,1648],[1750,1764],[1767,1768],[1770,1773],[1807,1807],[1809,1809],[1840,1866],[1958,1968],[2027,2035],[2305,2306],[2364,2364],[2369,2376],[2381,2381],[2385,2388],[2402,2403],[2433,2433],[2492,2492],[2497,2500],[2509,2509],[2530,2531],[2561,2562],[2620,2620],[2625,2626],[2631,2632],[2635,2637],[2672,2673],[2689,2690],[2748,2748],[2753,2757],[2759,2760],[2765,2765],[2786,2787],[2817,2817],[2876,2876],[2879,2879],[2881,2883],[2893,2893],[2902,2902],[2946,2946],[3008,3008],[3021,3021],[3134,3136],[3142,3144],[3146,3149],[3157,3158],[3260,3260],[3263,3263],[3270,3270],[3276,3277],[3298,3299],[3393,3395],[3405,3405],[3530,3530],[3538,3540],[3542,3542],[3633,3633],[3636,3642],[3655,3662],[3761,3761],[3764,3769],[3771,3772],[3784,3789],[3864,3865],[3893,3893],[3895,3895],[3897,3897],[3953,3966],[3968,3972],[3974,3975],[3984,3991],[3993,4028],[4038,4038],[4141,4144],[4146,4146],[4150,4151],[4153,4153],[4184,4185],[4448,4607],[4959,4959],[5906,5908],[5938,5940],[5970,5971],[6002,6003],[6068,6069],[6071,6077],[6086,6086],[6089,6099],[6109,6109],[6155,6157],[6313,6313],[6432,6434],[6439,6440],[6450,6450],[6457,6459],[6679,6680],[6912,6915],[6964,6964],[6966,6970],[6972,6972],[6978,6978],[7019,7027],[7616,7626],[7678,7679],[8203,8207],[8234,8238],[8288,8291],[8298,8303],[8400,8431],[12330,12335],[12441,12442],[43014,43014],[43019,43019],[43045,43046],[64286,64286],[65024,65039],[65056,65059],[65279,65279],[65529,65531]],n=[[68097,68099],[68101,68102],[68108,68111],[68152,68154],[68159,68159],[119143,119145],[119155,119170],[119173,119179],[119210,119213],[119362,119364],[917505,917505],[917536,917631],[917760,917999]];let a;t.UnicodeV6=class{constructor(){if(this.version="6",!a){a=new Uint8Array(65536),a.fill(1),a[0]=0,a.fill(0,1,32),a.fill(0,127,160),a.fill(2,4352,4448),a[9001]=2,a[9002]=2,a.fill(2,11904,42192),a[12351]=1,a.fill(2,44032,55204),a.fill(2,63744,64256),a.fill(2,65040,65050),a.fill(2,65072,65136),a.fill(2,65280,65377),a.fill(2,65504,65511);for(let e=0;et[r][1])return!1;for(;r>=s;)if(i=s+r>>1,e>t[i][1])s=i+1;else{if(!(e=131072&&e<=196605||e>=196608&&e<=262141?2:1}charProperties(e,t){let i=this.wcwidth(e),r=0===i&&0!==t;if(r){const e=s.UnicodeService.extractWidth(t);0===e?r=!1:e>i&&(i=e)}return s.UnicodeService.createPropertyValue(0,i,r)}}},562(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.WriteBuffer=void 0;const s=i(132),r=i(812),n=i(636);class a extends r.Disposable{constructor(e){super(),this._action=e,this._writeBuffer=[],this._callbacks=[],this._pendingData=0,this._bufferOffset=0,this._isSyncWriting=!1,this._syncCalls=0,this._didUserInput=!1,this._innerWriteTimer=this._register(new s.TimeoutTimer),this._onWriteParsed=this._register(new n.Emitter),this.onWriteParsed=this._onWriteParsed.event,this._register((0,r.toDisposable)(()=>{this._writeBuffer.length=0,this._callbacks.length=0,this._pendingData=0,this._bufferOffset=0}))}handleUserInput(){this._didUserInput=!0}flushSync(){if(this._store.isDisposed)return;if(this._isSyncWriting)return;let e;this._isSyncWriting=!0;let t=!1;for(;e=this._writeBuffer.shift();){t=!0,this._action(e);const i=this._callbacks.shift();i&&i()}this._pendingData=0,this._bufferOffset=2147483647,this._writeBuffer.length=0,this._callbacks.length=0,this._isSyncWriting=!1,t&&this._onWriteParsed.fire()}writeSync(e,t){if(this._store.isDisposed)return;if(void 0!==t&&this._syncCalls>t)return void(this._syncCalls=0);if(this._pendingData+=e.length,this._writeBuffer.push(e),this._callbacks.push(void 0),this._syncCalls++,this._isSyncWriting)return;let i;for(this._isSyncWriting=!0;i=this._writeBuffer.shift();){this._action(i);const e=this._callbacks.shift();e&&e()}this._pendingData=0,this._bufferOffset=2147483647,this._isSyncWriting=!1,this._syncCalls=0}write(e,t){if(!this._store.isDisposed){if(this._pendingData>5e7)throw new Error("write data discarded, use flow control to avoid losing data");if(!this._writeBuffer.length){if(this._bufferOffset=0,this._didUserInput)return this._didUserInput=!1,this._pendingData+=e.length,this._writeBuffer.push(e),this._callbacks.push(t),void this._innerWrite();this._scheduleInnerWrite()}this._pendingData+=e.length,this._writeBuffer.push(e),this._callbacks.push(t)}}_scheduleInnerWrite(e=0,t=!0){this._store.isDisposed||this._innerWriteTimer.cancelAndSet(()=>this._innerWrite(e,t),0)}_innerWrite(e=0,t=!0){if(this._store.isDisposed)return;const i=e||performance.now();for(;this._writeBuffer.length>this._bufferOffset;){const e=this._writeBuffer[this._bufferOffset],s=this._action(e,t);if(s){const e=e=>{this._store.isDisposed||(performance.now()-i>=12?this._scheduleInnerWrite(0,e):this._innerWrite(i,e))};return void s.catch(e=>(queueMicrotask(()=>{throw e}),Promise.resolve(!1))).then(e)}const r=this._callbacks[this._bufferOffset];if(r&&r(),this._bufferOffset++,this._pendingData-=e.length,performance.now()-i>=12)break}this._writeBuffer.length>this._bufferOffset?(this._bufferOffset>50&&(this._writeBuffer=this._writeBuffer.slice(this._bufferOffset),this._callbacks=this._callbacks.slice(this._bufferOffset),this._bufferOffset=0),this._scheduleInnerWrite()):(this._writeBuffer.length=0,this._callbacks.length=0,this._pendingData=0,this._bufferOffset=0),this._onWriteParsed.fire()}}t.WriteBuffer=a},693(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.parseColor=function(e){if(!e)return;let t=e.toLowerCase();if(t.startsWith("rgb:")){t=t.slice(4);const e=i.exec(t);if(e){const t=e[1]?15:e[4]?255:e[7]?4095:65535;return[Math.round(parseInt(e[1]||e[4]||e[7]||e[10],16)/t*255),Math.round(parseInt(e[2]||e[5]||e[8]||e[11],16)/t*255),Math.round(parseInt(e[3]||e[6]||e[9]||e[12],16)/t*255)]}}else if(t.startsWith("#")&&(t=t.slice(1),s.exec(t)&&[3,6,9,12].includes(t.length))){const e=t.length/3,i=[0,0,0];for(let s=0;s<3;++s){const r=parseInt(t.slice(e*s,e*s+e),16);i[s]=1===e?r<<4:2===e?r:3===e?r>>4:r>>8}return i}},t.toRgbString=function(e,t=16){const[i,s,n]=e;return`rgb:${r(i,t)}/${r(s,t)}/${r(n,t)}`};const i=/^([\da-f])\/([\da-f])\/([\da-f])$|^([\da-f]{2})\/([\da-f]{2})\/([\da-f]{2})$|^([\da-f]{3})\/([\da-f]{3})\/([\da-f]{3})$|^([\da-f]{4})\/([\da-f]{4})\/([\da-f]{4})$/,s=/^[\da-f]+$/;function r(e,t){const i=e.toString(16),s=i.length<2?"0"+i:i;switch(t){case 4:return i[0];case 8:return s;case 12:return(s+s).slice(0,3);default:return s+s}}},607(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.ApcHandler=t.ApcParser=void 0;const s=i(726),r=i(220),n=[];t.ApcParser=class{constructor(){this._handlers=Object.create(null),this._active=n,this._ident=0,this._handlerFb=()=>{},this._stack={paused:!1,loopPosition:0,fallThrough:!1}}registerHandler(e,t){this._handlers[e]??=[];const i=this._handlers[e];return i.push(t),{dispose:()=>{const e=i.indexOf(t);-1!==e&&i.splice(e,1)}}}clearHandler(e){this._handlers[e]&&delete this._handlers[e]}setHandlerFallback(e){this._handlerFb=e}dispose(){this._handlers=Object.create(null),this._handlerFb=()=>{},this._active=n}reset(){if(this._active.length)for(let e=this._stack.paused?this._stack.loopPosition-1:this._active.length-1;e>=0;--e)this._active[e].end(!1);this._stack.paused=!1,this._active=n,this._ident=0}start(e){if(this.reset(),this._ident=e,this._active=this._handlers[e]||n,this._active.length)for(let e=this._active.length-1;e>=0;e--)this._active[e].start();else this._handlerFb(this._ident,"START")}put(e,t,i){if(this._active.length)for(let s=this._active.length-1;s>=0;s--)this._active[s].put(e,t,i);else this._handlerFb(this._ident,"PUT",(0,s.utf32ToString)(e,t,i))}end(e,t=!0){if(this._active.length){let i=!1,s=this._active.length-1,r=!1;if(this._stack.paused&&(s=this._stack.loopPosition-1,i=t,r=this._stack.fallThrough,this._stack.paused=!1),!r&&!1===i){for(;s>=0&&(i=this._active[s].end(e),!0!==i);s--)if(i instanceof Promise)return this._stack.paused=!0,this._stack.loopPosition=s,this._stack.fallThrough=!1,i;s--}for(;s>=0;s--)if(i=this._active[s].end(!1),i instanceof Promise)return this._stack.paused=!0,this._stack.loopPosition=s,this._stack.fallThrough=!0,i}else this._handlerFb(this._ident,"END",e);this._active=n,this._ident=0}};class a{constructor(e){this._handler=e,this._data=new r.LimitedStringBuilder(a._payloadLimit),this._hitLimit=!1}start(){this._data.reset(),this._hitLimit=!1}put(e,t,i){this._hitLimit||this._data.append((0,s.utf32ToString)(e,t,i))&&(this._hitLimit=!0)}end(e){let t=!1;if(this._hitLimit)t=!1;else if(e&&(t=this._handler(this._data.toString()),t instanceof Promise))return t.then(e=>(this._data.reset(),this._hitLimit=!1,e));return this._data.reset(),this._hitLimit=!1,t}}t.ApcHandler=a,a._payloadLimit=1e7},823(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.DcsHandler=t.DcsParser=void 0;const s=i(726),r=i(262),n=i(220),a=[];t.DcsParser=class{constructor(){this._handlers=Object.create(null),this._active=a,this._ident=0,this._handlerFb=()=>{},this._stack={paused:!1,loopPosition:0,fallThrough:!1}}dispose(){this._handlers=Object.create(null),this._handlerFb=()=>{},this._active=a}registerHandler(e,t){this._handlers[e]??=[];const i=this._handlers[e];return i.push(t),{dispose:()=>{const e=i.indexOf(t);-1!==e&&i.splice(e,1)}}}clearHandler(e){this._handlers[e]&&delete this._handlers[e]}setHandlerFallback(e){this._handlerFb=e}reset(){if(this._active.length)for(let e=this._stack.paused?this._stack.loopPosition-1:this._active.length-1;e>=0;--e)this._active[e].unhook(!1);this._stack.paused=!1,this._active=a,this._ident=0}hook(e,t){if(this.reset(),this._ident=e,this._active=this._handlers[e]||a,this._active.length)for(let e=this._active.length-1;e>=0;e--)this._active[e].hook(t);else this._handlerFb(this._ident,"HOOK",t)}put(e,t,i){if(this._active.length)for(let s=this._active.length-1;s>=0;s--)this._active[s].put(e,t,i);else this._handlerFb(this._ident,"PUT",(0,s.utf32ToString)(e,t,i))}unhook(e,t=!0){if(this._active.length){let i=!1,s=this._active.length-1,r=!1;if(this._stack.paused&&(s=this._stack.loopPosition-1,i=t,r=this._stack.fallThrough,this._stack.paused=!1),!r&&!1===i){for(;s>=0&&(i=this._active[s].unhook(e),!0!==i);s--)if(i instanceof Promise)return this._stack.paused=!0,this._stack.loopPosition=s,this._stack.fallThrough=!1,i;s--}for(;s>=0;s--)if(i=this._active[s].unhook(!1),i instanceof Promise)return this._stack.paused=!0,this._stack.loopPosition=s,this._stack.fallThrough=!0,i}else this._handlerFb(this._ident,"UNHOOK",e);this._active=a,this._ident=0}};const o=new r.Params;o.addParam(0);class h{constructor(e){this._handler=e,this._data=new n.LimitedStringBuilder(h._payloadLimit),this._params=o,this._hitLimit=!1}hook(e){this._params=e.length>1||e.params[0]?e.clone():o,this._data.reset(),this._hitLimit=!1}put(e,t,i){this._hitLimit||this._data.append((0,s.utf32ToString)(e,t,i))&&(this._hitLimit=!0)}unhook(e){let t=!1;if(this._hitLimit)t=!1;else if(e&&(t=this._handler(this._data.toString(),this._params),t instanceof Promise))return t.then(e=>(this._params=o,this._data.reset(),this._hitLimit=!1,e));return this._params=o,this._data.reset(),this._hitLimit=!1,t}}t.DcsHandler=h,h._payloadLimit=1e7},717(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.EscapeSequenceParser=t.VT500_TRANSITION_TABLE=t.TransitionTable=void 0;const s=i(812),r=i(262),n=i(346),a=i(823),o=i(607);class h{constructor(e){this.table=new Uint16Array(e)}setDefault(e,t){this.table.fill(e<<8|t)}add(e,t,i,s){this.table[t<<8|e]=i<<8|s}addMany(e,t,i,s){for(let r=0;rt),i=(e,i)=>t.slice(e,i),s=i(32,127),r=i(0,24);r.push(25),r.push.apply(r,i(28,32));const n=i(0,17);e.setDefault(1,0),e.addMany(s,0,2,0);for(const t of n)e.addMany([24,26,153,154],t,3,0),e.addMany(i(128,144),t,3,0),e.addMany(i(144,152),t,3,0),e.add(156,t,0,0),e.add(27,t,11,1),e.add(157,t,4,8),e.addMany([152,158],t,0,7),e.add(159,t,11,14),e.add(155,t,11,3),e.add(144,t,11,9);return e.addMany(r,0,3,0),e.addMany(r,1,3,1),e.add(127,1,0,1),e.addMany(r,8,0,8),e.addMany(r,3,3,3),e.add(127,3,0,3),e.addMany(r,4,3,4),e.add(127,4,0,4),e.addMany(r,6,3,6),e.addMany(r,5,3,5),e.add(127,5,0,5),e.addMany(r,2,3,2),e.add(127,2,0,2),e.add(93,1,4,8),e.addMany(s,8,5,8),e.add(127,8,5,8),e.addMany([156,27,24,26,7],8,6,0),e.addMany(i(28,32),8,0,8),e.addMany([88,94],1,0,7),e.addMany(s,7,0,7),e.addMany(r,7,0,7),e.add(156,7,0,0),e.add(127,7,0,7),e.add(95,1,11,14),e.addMany(r,14,0,14),e.add(127,14,0,14),e.addMany(i(32,48),14,9,15),e.addMany(i(48,127),14,15,16),e.addMany(i(48,127),15,15,16),e.addMany(r,15,0,15),e.addMany(i(32,48),15,9,15),e.add(127,15,0,15),e.addMany(s,16,16,16),e.addMany(r,16,0,16),e.addMany(i(8,14),16,16,16),e.add(127,16,0,16),e.addMany([27,156,24,26],16,17,0),e.add(91,1,11,3),e.addMany(i(64,127),3,7,0),e.addMany(i(48,60),3,8,4),e.addMany([60,61,62,63],3,9,4),e.addMany(i(48,60),4,8,4),e.addMany(i(64,127),4,7,0),e.addMany([60,61,62,63],4,0,6),e.addMany(i(32,64),6,0,6),e.add(127,6,0,6),e.addMany(i(64,127),6,0,0),e.addMany(i(32,48),3,9,5),e.addMany(i(32,48),5,9,5),e.addMany(i(48,64),5,0,6),e.addMany(i(64,127),5,7,0),e.addMany(i(32,48),4,9,5),e.addMany(i(32,48),1,9,2),e.addMany(i(32,48),2,9,2),e.addMany(i(48,127),2,10,0),e.addMany(i(48,80),1,10,0),e.addMany(i(81,88),1,10,0),e.addMany([89,90,92],1,10,0),e.addMany(i(96,127),1,10,0),e.add(80,1,11,9),e.addMany(r,9,0,9),e.add(127,9,0,9),e.addMany(i(32,48),9,9,12),e.addMany(i(48,60),9,8,10),e.addMany([60,61,62,63],9,9,10),e.addMany(r,11,0,11),e.addMany(i(32,128),11,0,11),e.addMany(r,10,0,10),e.add(127,10,0,10),e.addMany(i(48,60),10,8,10),e.addMany([60,61,62,63],10,0,11),e.addMany(i(32,48),10,9,12),e.addMany(r,12,0,12),e.add(127,12,0,12),e.addMany(i(32,48),12,9,12),e.addMany(i(48,64),12,0,11),e.addMany(i(64,127),12,12,13),e.addMany(i(64,127),10,12,13),e.addMany(i(64,127),9,12,13),e.addMany(r,13,13,13),e.addMany(s,13,13,13),e.add(127,13,0,13),e.addMany([27,156,24,26],13,14,0),e.add(c,0,2,0),e.add(c,8,5,8),e.add(c,6,0,6),e.add(c,11,0,11),e.add(c,13,13,13),e.add(c,16,16,16),e}();class l extends s.Disposable{constructor(e=t.VT500_TRANSITION_TABLE){super(),this._transitions=e,this._parseStack={state:0,handlers:[],handlerPos:0,transition:0,chunkPos:0},this.initialState=0,this.currentState=this.initialState,this._params=new r.Params,this._params.addParam(0),this._collect=0,this.precedingJoinState=0,this._printHandlerFb=(e,t,i)=>{},this._executeHandlerFb=e=>{},this._csiHandlerFb=(e,t)=>{},this._escHandlerFb=e=>{},this._errorHandlerFb=e=>e,this._printHandler=this._printHandlerFb,this._executeHandlers=Object.create(null),this._executeHandlersArr=new Array(24).fill(void 0),this._csiHandlers=Object.create(null),this._escHandlers=Object.create(null),this._register((0,s.toDisposable)(()=>{this._csiHandlers=Object.create(null),this._executeHandlers=Object.create(null),this._executeHandlersArr=new Array(24).fill(void 0),this._escHandlers=Object.create(null)})),this._oscParser=this._register(new n.OscParser),this._dcsParser=this._register(new a.DcsParser),this._apcParser=this._register(new o.ApcParser),this._errorHandler=this._errorHandlerFb,this.registerEscHandler({final:"\\"},()=>!0)}_identifier(e,t=[64,126]){let i=0;if(e.prefix){if(e.prefix.length>1)throw new Error("only one byte as prefix supported");if(i=e.prefix.charCodeAt(0),i<60||i>63)throw new Error("prefix must be in range 0x3c .. 0x3f")}if(e.intermediates){if(e.intermediates.length>2)throw new Error("only two bytes as intermediates are supported");for(let t=0;ts||s>47)throw new Error("intermediate must be in range 0x20 .. 0x2f");i<<=8,i|=s}}if(1!==e.final.length)throw new Error("final must be a single byte");const s=e.final.charCodeAt(0);if(t[0]>s||s>t[1])throw new Error(`final must be in range ${t[0]} .. ${t[1]}`);return i<<=8,i|=s,i}identToString(e){const t=[];for(;e;)t.push(String.fromCharCode(255&e)),e>>=8;return t.reverse().join("")}setPrintHandler(e){this._printHandler=e}clearPrintHandler(){this._printHandler=this._printHandlerFb}registerEscHandler(e,t){const i=this._identifier(e,[48,126]);this._escHandlers[i]??=[];const s=this._escHandlers[i];return s.push(t),{dispose:()=>{const e=s.indexOf(t);-1!==e&&s.splice(e,1)}}}clearEscHandler(e){this._escHandlers[this._identifier(e,[48,126])]&&delete this._escHandlers[this._identifier(e,[48,126])]}setEscHandlerFallback(e){this._escHandlerFb=e}setExecuteHandler(e,t){const i=e.charCodeAt(0);this._executeHandlers[i]=t,i<24&&(this._executeHandlersArr[i]=t)}clearExecuteHandler(e){const t=e.charCodeAt(0);this._executeHandlers[t]&&delete this._executeHandlers[t],t<24&&(this._executeHandlersArr[t]=void 0)}setExecuteHandlerFallback(e){this._executeHandlerFb=e}registerCsiHandler(e,t){const i=this._identifier(e);this._csiHandlers[i]??=[];const s=this._csiHandlers[i];return s.push(t),{dispose:()=>{const e=s.indexOf(t);-1!==e&&s.splice(e,1)}}}clearCsiHandler(e){this._csiHandlers[this._identifier(e)]&&delete this._csiHandlers[this._identifier(e)]}setCsiHandlerFallback(e){this._csiHandlerFb=e}registerDcsHandler(e,t){return this._dcsParser.registerHandler(this._identifier(e),t)}clearDcsHandler(e){this._dcsParser.clearHandler(this._identifier(e))}setDcsHandlerFallback(e){this._dcsParser.setHandlerFallback(e)}registerOscHandler(e,t){return this._oscParser.registerHandler(e,t)}clearOscHandler(e){this._oscParser.clearHandler(e)}setOscHandlerFallback(e){this._oscParser.setHandlerFallback(e)}registerApcHandler(e,t){return e.prefix=void 0,this._apcParser.registerHandler(this._identifier(e,[48,126]),t)}clearApcHandler(e){e.prefix=void 0,this._apcParser.clearHandler(this._identifier(e,[48,126]))}setApcHandlerFallback(e){this._apcParser.setHandlerFallback(e)}setErrorHandler(e){this._errorHandler=e}clearErrorHandler(){this._errorHandler=this._errorHandlerFb}reset(){this.currentState=this.initialState,this._oscParser.reset(),this._dcsParser.reset(),this._apcParser.reset(),this._params.resetZdm(),this._collect=0,this.precedingJoinState=0,0!==this._parseStack.state&&(this._parseStack.state=2,this._parseStack.handlers=[])}_preserveStack(e,t,i,s,r){this._parseStack.state=e,this._parseStack.handlers=t,this._parseStack.handlerPos=i,this._parseStack.transition=s,this._parseStack.chunkPos=r}parse(e,t,i){let s,r,n,a=0;if(this._parseStack.state)if(2===this._parseStack.state)this._parseStack.state=0,a=this._parseStack.chunkPos+1;else{if(void 0===i||1===this._parseStack.state)throw this._parseStack.state=1,new Error("improper continuation due to previous async handler, giving up parsing");const t=this._parseStack.handlers;let r=this._parseStack.handlerPos-1;switch(this._parseStack.state){case 3:if(!1===i&&r>-1)for(;r>=0&&(n=t[r](this._params),!0!==n);r--)if(n instanceof Promise)return this._parseStack.handlerPos=r,n;this._parseStack.handlers=[];break;case 4:if(!1===i&&r>-1)for(;r>=0&&(n=t[r](),!0!==n);r--)if(n instanceof Promise)return this._parseStack.handlerPos=r,n;this._parseStack.handlers=[];break;case 6:if(s=e[this._parseStack.chunkPos],n=this._dcsParser.unhook(24!==s&&26!==s,i),n)return n;27===s&&(this._parseStack.transition|=1),this._params.resetZdm(),this._collect=0;break;case 5:if(s=e[this._parseStack.chunkPos],n=this._oscParser.end(24!==s&&26!==s,i),n)return n;27===s&&(this._parseStack.transition|=1),this._params.resetZdm(),this._collect=0;break;case 7:if(s=e[this._parseStack.chunkPos],n=this._apcParser.end(24!==s&&26!==s,i),n)return n;27===s&&(this._parseStack.transition|=1),this._params.resetZdm(),this._collect=0}this._parseStack.state=0,a=this._parseStack.chunkPos+1,this.precedingJoinState=0,this.currentState=255&this._parseStack.transition}for(let i=a;i=60&&a<=63&&(this._collect=a,s++);let o=!1;for(;s=48&&a<=57)this._params.addDigit(a-48);else if(59===a)this._params.addParam(0);else{if(58!==a){if(a>=64&&a<=126){const e=this._csiHandlers[this._collect<<8|a];let t=e?e.length-1:-1;for(;t>=0&&(n=e[t](this._params),!0!==n);t--)if(n instanceof Promise)return r=1792,this._preserveStack(3,e,t,r,s),n;t<0&&this._csiHandlerFb(this._collect<<8|a,this._params),this.precedingJoinState=0,i=s,this.currentState=0,o=!0;break}break}this._params.addSubParam(-1)}o||(i=s-1,this.currentState=4);continue}switch(r=this._transitions.table[this.currentState<<8|(s>8){case 2:let a=i;const o=t-4;for(;a=32&&(e[a]<=126||e[a]>=c)&&e[++a]>=32&&(e[a]<=126||e[a]>=c)&&e[++a]>=32&&(e[a]<=126||e[a]>=c)&&e[++a]>=32&&(e[a]<=126||e[a]>=c););if(a>=o)for(;a=32&&(e[a]<=126||e[a]>=c);)a++;this._printHandler(e,i,a),i=a-1;break;case 3:this._executeHandlers[s]?this._executeHandlers[s]():this._executeHandlerFb(s),this.precedingJoinState=0;break;case 0:break;case 1:if(this._errorHandler({position:i,code:s,currentState:this.currentState,collect:this._collect,params:this._params,abort:!1}).abort)return;break;case 7:const h=this._csiHandlers[this._collect<<8|s];let l=h?h.length-1:-1;for(;l>=0&&(n=h[l](this._params),!0!==n);l--)if(n instanceof Promise)return this._preserveStack(3,h,l,r,i),n;l<0&&this._csiHandlerFb(this._collect<<8|s,this._params),this.precedingJoinState=0;break;case 8:do{switch(s){case 59:this._params.addParam(0);break;case 58:this._params.addSubParam(-1);break;default:this._params.addDigit(s-48)}}while(++i47&&s<60);i--;break;case 9:this._collect<<=8,this._collect|=s;break;case 10:const _=this._escHandlers[this._collect<<8|s];let d=_?_.length-1:-1;for(;d>=0&&(n=_[d](),!0!==n);d--)if(n instanceof Promise)return this._preserveStack(4,_,d,r,i),n;d<0&&this._escHandlerFb(this._collect<<8|s),this.precedingJoinState=0;break;case 11:this._params.resetZdm(),this._collect=0;break;case 12:this._dcsParser.hook(this._collect<<8|s,this._params);break;case 13:for(let r=i+1;;++r)if(r>=t||24===(s=e[r])||26===s||27===s||s>127&&s=t||(s=e[r])<32||s>127&&s=32&&e[s]<127||e[s]>=8&&e[s]<14||e[s]>=c))){this._apcParser.put(e,i,s),i=s-1;break}break;case 17:if(n=this._apcParser.end(24!==s&&26!==s),n)return this._preserveStack(7,[],0,r,i),n;27===s&&(r|=1),this._params.resetZdm(),this._collect=0,this.precedingJoinState=0}this.currentState=255&r}}}t.EscapeSequenceParser=l},346(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.OscHandler=t.OscParser=void 0;const s=i(726),r=i(220),n=[];t.OscParser=class{constructor(){this._state=0,this._active=n,this._id=-1,this._handlers=Object.create(null),this._handlerFb=()=>{},this._stack={paused:!1,loopPosition:0,fallThrough:!1}}registerHandler(e,t){this._handlers[e]??=[];const i=this._handlers[e];return i.push(t),{dispose:()=>{const e=i.indexOf(t);-1!==e&&i.splice(e,1)}}}clearHandler(e){this._handlers[e]&&delete this._handlers[e]}setHandlerFallback(e){this._handlerFb=e}dispose(){this._handlers=Object.create(null),this._handlerFb=()=>{},this._active=n}reset(){if(2===this._state)for(let e=this._stack.paused?this._stack.loopPosition-1:this._active.length-1;e>=0;--e)this._active[e].end(!1);this._stack.paused=!1,this._active=n,this._id=-1,this._state=0}_start(){if(this._active=this._handlers[this._id]||n,this._active.length)for(let e=this._active.length-1;e>=0;e--)this._active[e].start();else this._handlerFb(this._id,"START")}_put(e,t,i){if(this._active.length)for(let s=this._active.length-1;s>=0;s--)this._active[s].put(e,t,i);else this._handlerFb(this._id,"PUT",(0,s.utf32ToString)(e,t,i))}start(){this.reset(),this._state=1}put(e,t,i){if(3!==this._state){if(1===this._state)for(;t0&&this._put(e,t,i)}}end(e,t=!0){if(0!==this._state){if(3!==this._state)if(1===this._state&&this._start(),this._active.length){let i=!1,s=this._active.length-1,r=!1;if(this._stack.paused&&(s=this._stack.loopPosition-1,i=t,r=this._stack.fallThrough,this._stack.paused=!1),!r&&!1===i){for(;s>=0&&(i=this._active[s].end(e),!0!==i);s--)if(i instanceof Promise)return this._stack.paused=!0,this._stack.loopPosition=s,this._stack.fallThrough=!1,i;s--}for(;s>=0;s--)if(i=this._active[s].end(!1),i instanceof Promise)return this._stack.paused=!0,this._stack.loopPosition=s,this._stack.fallThrough=!0,i}else this._handlerFb(this._id,"END",e);this._active=n,this._id=-1,this._state=0}}};class a{constructor(e){this._handler=e,this._data=new r.LimitedStringBuilder(a._payloadLimit),this._hitLimit=!1}start(){this._data.reset(),this._hitLimit=!1}put(e,t,i){this._hitLimit||this._data.append((0,s.utf32ToString)(e,t,i))&&(this._hitLimit=!0)}end(e){let t=!1;if(this._hitLimit)t=!1;else if(e&&(t=this._handler(this._data.toString()),t instanceof Promise))return t.then(e=>(this._data.reset(),this._hitLimit=!1,e));return this._data.reset(),this._hitLimit=!1,t}}t.OscHandler=a,a._payloadLimit=1e7},262(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.Params=void 0;class i{static fromArray(e){const t=new i;if(!e.length)return t;for(let i=Array.isArray(e[0])?1:0;i256)throw new Error("maxSubParamsLength must not be greater than 256");this.params=new Int32Array(e),this.length=0,this._subParams=new Int32Array(t),this._subParamsLength=0,this._subParamsIdx=new Uint16Array(e),this._rejectDigits=!1,this._rejectSubDigits=!1,this._digitIsSub=!1}clone(){const e=new i(this.maxLength,this.maxSubParamsLength);return e.params.set(this.params),e.length=this.length,e._subParams.set(this._subParams),e._subParamsLength=this._subParamsLength,e._subParamsIdx.set(this._subParamsIdx),e._rejectDigits=this._rejectDigits,e._rejectSubDigits=this._rejectSubDigits,e._digitIsSub=this._digitIsSub,e}toArray(){const e=[];for(let t=0;t>8,s=255&this._subParamsIdx[t];s-i>0&&e.push(Array.prototype.slice.call(this._subParams,i,s))}return e}reset(){this.length=0,this._subParamsLength=0,this._rejectDigits=!1,this._rejectSubDigits=!1,this._digitIsSub=!1}resetZdm(){this.length=1,this._subParamsLength=0,this._rejectDigits=!1,this._rejectSubDigits=!1,this._digitIsSub=!1,this._subParamsIdx[0]=0,this.params[0]=0}addParam(e){if(this._digitIsSub=!1,this.length>=this.maxLength)this._rejectDigits=!0;else{if(e<-1)throw new Error("values less than -1 are not allowed");this._subParamsIdx[this.length]=this._subParamsLength<<8|this._subParamsLength,this.params[this.length++]=e>2147483647?2147483647:e}}addSubParam(e){if(this._digitIsSub=!0,this.length)if(this._rejectDigits||this._subParamsLength>=this.maxSubParamsLength)this._rejectSubDigits=!0;else{if(e<-1)throw new Error("values less than -1 are not allowed");this._subParams[this._subParamsLength++]=e>2147483647?2147483647:e,this._subParamsIdx[this.length-1]++}}hasSubParams(e){return(255&this._subParamsIdx[e])-(this._subParamsIdx[e]>>8)>0}getSubParams(e){const t=this._subParamsIdx[e]>>8,i=255&this._subParamsIdx[e];return i-t>0?this._subParams.subarray(t,i):null}getSubParamsAll(){const e={};for(let t=0;t>8,s=255&this._subParamsIdx[t];s-i>0&&(e[t]=this._subParams.slice(i,s))}return e}addDigit(e){let t;if(this._rejectDigits||!(t=this._digitIsSub?this._subParamsLength:this.length)||this._digitIsSub&&this._rejectSubDigits)return;const i=this._digitIsSub?this._subParams:this.params,s=i[t-1];i[t-1]=~s?Math.min(10*s+e,2147483647):e}}t.Params=i},27(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.AddonManager=void 0,t.AddonManager=class{constructor(){this._addons=[]}dispose(){for(let e=this._addons.length-1;e>=0;e--)this._addons[e].instance.dispose()}loadAddon(e,t){const i={instance:t,dispose:t.dispose,isDisposed:!1};this._addons.push(i),t.dispose=()=>this._wrappedAddonDispose(i),t.activate(e)}_wrappedAddonDispose(e){if(e.isDisposed)return;let t=-1;for(let i=0;i=this._line.length))return t?(this._line.loadCell(e,t),t):this._line.loadCell(e,new s.CellData)}translateToString(e,t,i){return this._line.translateToString(e,t,i)}}},101(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.BufferNamespaceApi=void 0;const s=i(235),r=i(812),n=i(636);class a extends r.Disposable{constructor(e){super(),this._core=e,this._onBufferChange=this._register(new n.Emitter),this.onBufferChange=this._onBufferChange.event,this._normal=new s.BufferApiView(this._core.buffers.normal,"normal"),this._alternate=new s.BufferApiView(this._core.buffers.alt,"alternate"),this._register(this._core.buffers.onBufferActivate(()=>this._onBufferChange.fire(this.active)))}get active(){if(this._core.buffers.active===this._core.buffers.normal)return this.normal;if(this._core.buffers.active===this._core.buffers.alt)return this.alternate;throw new Error("Active buffer is neither normal nor alternate")}get normal(){return this._normal.init(this._core.buffers.normal)}get alternate(){return this._alternate.init(this._core.buffers.alt)}}t.BufferNamespaceApi=a},97(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.ParserApi=void 0,t.ParserApi=class{constructor(e){this._core=e}registerCsiHandler(e,t){return this._core.registerCsiHandler(e,e=>t(e.toArray()))}addCsiHandler(e,t){return this.registerCsiHandler(e,t)}registerDcsHandler(e,t){return this._core.registerDcsHandler(e,(e,i)=>t(e,i.toArray()))}addDcsHandler(e,t){return this.registerDcsHandler(e,t)}registerEscHandler(e,t){return this._core.registerEscHandler(e,t)}addEscHandler(e,t){return this.registerEscHandler(e,t)}registerOscHandler(e,t){return this._core.registerOscHandler(e,t)}addOscHandler(e,t){return this.registerOscHandler(e,t)}registerApcHandler(e,t){return this._core.registerApcHandler(e,t)}}},335(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.UnicodeApi=void 0,t.UnicodeApi=class{constructor(e){this._core=e}register(e){this._core.unicodeService.register(e)}get versions(){return this._core.unicodeService.versions}get activeVersion(){return this._core.unicodeService.activeVersion}set activeVersion(e){this._core.unicodeService.activeVersion=e}}},640(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,a=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)a=Reflect.decorate(e,t,i,s);else for(var o=e.length-1;o>=0;o--)(r=e[o])&&(a=(n<3?r(a):n>3?r(t,i,a):r(t,i))||a);return n>3&&a&&Object.defineProperty(t,i,a),a},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.BufferService=void 0;const n=i(812),a=i(478),o=i(501),h=i(636);let c=class extends n.Disposable{get buffer(){return this.buffers.active}constructor(e,t){super(),this.isUserScrolling=!1,this._onResize=this._register(new h.Emitter),this.onResize=this._onResize.event,this._onScroll=this._register(new h.Emitter),this.onScroll=this._onScroll.event,this.cols=Math.max(e.rawOptions.cols||0,2),this.rows=Math.max(e.rawOptions.rows||0,1),this.buffers=this._register(new a.BufferSet(e,this,t)),this._register(this.buffers.onBufferActivate(e=>{this._onScroll.fire(e.activeBuffer.ydisp)}))}resize(e,t){const i=this.cols!==e,s=this.rows!==t;this.cols=e,this.rows=t,this.buffers.resize(e,t),this._onResize.fire({cols:e,rows:t,colsChanged:i,rowsChanged:s})}reset(){this.buffers.reset(),this.isUserScrolling=!1}scroll(e,t=!1){const i=this.buffer;let s;s=this._cachedBlankLine,s&&s.length===this.cols&&s.getFg(0)===e.fg&&s.getBg(0)===e.bg||(s=i.getBlankLine(e,t),this._cachedBlankLine=s),s.isWrapped=t;const r=i.ybase+i.scrollTop,n=i.ybase+i.scrollBottom;if(0===i.scrollTop){const e=i.lines.isFull;n===i.lines.length-1?e?i.lines.recycle().copyFrom(s,!0):i.lines.push(s.clone(!0)):i.lines.splice(n+1,0,s.clone(!0)),e?this.isUserScrolling&&(i.ydisp=Math.max(i.ydisp-1,0)):(i.ybase++,this.isUserScrolling||i.ydisp++)}else{const e=n-r+1;i.lines.shiftElements(r+1,e-1,-1),i.lines.set(n,s.clone(!0))}this.isUserScrolling||(i.ydisp=i.ybase),this._onScroll.fire(i.ydisp)}scrollLines(e,t){const i=this.buffer;if(e<0){if(0===i.ydisp)return;this.isUserScrolling=!0}else e+i.ydisp>=i.ybase&&(this.isUserScrolling=!1);const s=i.ydisp;i.ydisp=Math.max(Math.min(i.ydisp+e,i.ybase),0),s!==i.ydisp&&(t||this._onScroll.fire(i.ydisp))}};t.BufferService=c,t.BufferService=c=s([r(0,o.IOptionsService),r(1,o.ILogService)],c)},746(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.CharsetService=void 0,t.CharsetService=class{constructor(){this.glevel=0,this._charsets=[]}get charsets(){return this._charsets}reset(){this.charset=void 0,this._charsets=[],this.glevel=0}setgLevel(e){this.glevel=e,this.charset=this._charsets[e]}setgCharset(e,t){this._charsets[e]=t,this.glevel===e&&(this.charset=t)}}},71(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,a=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)a=Reflect.decorate(e,t,i,s);else for(var o=e.length-1;o>=0;o--)(r=e[o])&&(a=(n<3?r(a):n>3?r(t,i,a):r(t,i))||a);return n>3&&a&&Object.defineProperty(t,i,a),a},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.CoreService=void 0;const n=i(812),a=i(501),o=i(636),h=Object.freeze({insertMode:!1}),c=Object.freeze({applicationCursorKeys:!1,applicationKeypad:!1,bracketedPasteMode:!1,colorSchemeUpdates:!1,cursorBlink:void 0,cursorStyle:void 0,origin:!1,reverseWraparound:!1,sendFocus:!1,synchronizedOutput:!1,win32InputMode:!1,wraparound:!0});let l=class extends n.Disposable{constructor(e,t,i){super(),this._bufferService=e,this._logService=t,this._optionsService=i,this.isCursorHidden=!1,this._onData=this._register(new o.Emitter),this.onData=this._onData.event,this._onUserInput=this._register(new o.Emitter),this.onUserInput=this._onUserInput.event,this._onBinary=this._register(new o.Emitter),this.onBinary=this._onBinary.event,this._onRequestScrollToBottom=this._register(new o.Emitter),this.onRequestScrollToBottom=this._onRequestScrollToBottom.event,this.isCursorInitialized=i.rawOptions.showCursorImmediately??!1,this.modes=structuredClone(h),this.decPrivateModes=structuredClone(c),this.kittyKeyboard={flags:0,mainFlags:0,altFlags:0,mainStack:[],altStack:[]}}reset(){this.modes=structuredClone(h),this.decPrivateModes=structuredClone(c),this.kittyKeyboard={flags:0,mainFlags:0,altFlags:0,mainStack:[],altStack:[]}}triggerDataEvent(e,t=!1){if(this._optionsService.rawOptions.disableStdin)return;const i=this._bufferService.buffer;t&&this._optionsService.rawOptions.scrollOnUserInput&&i.ybase!==i.ydisp&&this._onRequestScrollToBottom.fire(),t&&this._onUserInput.fire(),this._logService.debug(`sending data "${e}"`),this._logService.trace("sending data (codes)",()=>e.split("").map(e=>e.charCodeAt(0))),this._onData.fire(e)}triggerBinaryEvent(e){this._optionsService.rawOptions.disableStdin||(this._logService.debug(`sending binary "${e}"`),this._logService.trace("sending binary (codes)",()=>e.split("").map(e=>e.charCodeAt(0))),this._onBinary.fire(e))}};t.CoreService=l,t.CoreService=l=s([r(0,a.IBufferService),r(1,a.ILogService),r(2,a.IOptionsService)],l)},25(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.InstantiationService=t.ServiceCollection=void 0;const s=i(501),r=i(201);class n{constructor(...e){this._entries=new Map;for(const[t,i]of e)this.set(t,i)}set(e,t){const i=this._entries.get(e);return this._entries.set(e,t),i}forEach(e){for(const[t,i]of this._entries.entries())e(t,i)}has(e){return this._entries.has(e)}get(e){return this._entries.get(e)}}t.ServiceCollection=n,t.InstantiationService=class{constructor(){this._services=new n,this._services.set(s.IInstantiationService,this)}setService(e,t){this._services.set(e,t)}getService(e){return this._services.get(e)}createInstance(e,...t){const i=(0,r.getServiceDependencies)(e).sort((e,t)=>e.index-t.index),s=[];for(const t of i){const i=this._services.get(t.id);if(!i)throw new Error(`[createInstance] ${e.name} depends on UNKNOWN service ${t.id._id}.`);s.push(i)}const n=i.length>0?i[0].index:t.length;if(t.length!==n)throw new Error(`[createInstance] First service dependency of ${e.name} at position ${n+1} conflicts with ${t.length} static arguments`);return new e(...[...t,...s])}}},276(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,a=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)a=Reflect.decorate(e,t,i,s);else for(var o=e.length-1;o>=0;o--)(r=e[o])&&(a=(n<3?r(a):n>3?r(t,i,a):r(t,i))||a);return n>3&&a&&Object.defineProperty(t,i,a),a},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.LogService=void 0;const n=i(812),a=i(501),o={trace:a.LogLevelEnum.TRACE,debug:a.LogLevelEnum.DEBUG,info:a.LogLevelEnum.INFO,warn:a.LogLevelEnum.WARN,error:a.LogLevelEnum.ERROR,off:a.LogLevelEnum.OFF};let h=class extends n.Disposable{get logLevel(){return this._logLevel}constructor(e){super(),this._optionsService=e,this._logLevel=a.LogLevelEnum.OFF,this._updateLogLevel(),this._register(this._optionsService.onSpecificOptionChange("logLevel",()=>this._updateLogLevel()))}_updateLogLevel(){this._logLevel=o[this._optionsService.rawOptions.logLevel]}_evalLazyOptionalParams(e){for(let t=0;t!1},X10:{events:1,restrict:e=>4!==e.button&&1===e.action&&(e.ctrl=!1,e.alt=!1,e.shift=!1,!0)},VT200:{events:19,restrict:e=>32!==e.action},DRAG:{events:23,restrict:e=>32!==e.action||3!==e.button},ANY:{events:31,restrict:e=>!0}};function a(e,t){let i=(e.ctrl?16:0)|(e.shift?4:0)|(e.alt?8:0);return 4===e.button?(i|=64,i|=e.action):(i|=3&e.button,4&e.button&&(i|=64),8&e.button&&(i|=128),32===e.action?i|=32:0!==e.action||t||(i|=3)),i}const o=String.fromCharCode,h={DEFAULT:e=>{const t=[a(e,!1)+32,e.col+32,e.row+32];return t[0]>255||t[1]>255||t[2]>255?"":`${o(t[0])}${o(t[1])}${o(t[2])}`},SGR:e=>{const t=0===e.action&&4!==e.button?"m":"M";return`[<${a(e,!0)};${e.col};${e.row}${t}`},SGR_PIXELS:e=>{const t=0===e.action&&4!==e.button?"m":"M";return`[<${a(e,!0)};${e.x};${e.y}${t}`}};class c extends s.Disposable{constructor(){super(),this._protocols={},this._encodings={},this._activeProtocol="",this._activeEncoding="",this._onProtocolChange=this._register(new r.Emitter),this.onProtocolChange=this._onProtocolChange.event;for(const e of Object.keys(n))this.addProtocol(e,n[e]);for(const e of Object.keys(h))this.addEncoding(e,h[e]);this.reset()}addProtocol(e,t){this._protocols[e]=t}addEncoding(e,t){this._encodings[e]=t}get activeProtocol(){return this._activeProtocol}get areMouseEventsActive(){return 0!==this._protocols[this._activeProtocol].events}set activeProtocol(e){if(!this._protocols[e])throw new Error(`unknown protocol "${e}"`);this._activeProtocol=e,this._onProtocolChange.fire(this._protocols[e].events)}get activeEncoding(){return this._activeEncoding}set activeEncoding(e){if(!this._encodings[e])throw new Error(`unknown encoding "${e}"`);this._activeEncoding=e}reset(){this.activeProtocol="NONE",this.activeEncoding="DEFAULT"}setCustomWheelEventHandler(e){this._customWheelEventHandler=e}allowCustomWheelEvent(e){return!this._customWheelEventHandler||!1!==this._customWheelEventHandler(e)}restrictMouseEvent(e){return this._protocols[this._activeProtocol].restrict(e)}encodeMouseEvent(e){return this._encodings[this._activeEncoding](e)}get isDefaultEncoding(){return"DEFAULT"===this._activeEncoding}get isPixelEncoding(){return"SGR_PIXELS"===this._activeEncoding}}t.MouseStateService=c},56(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.OptionsService=t.DEFAULT_OPTIONS=void 0;const s=i(812),r=i(701),n=i(636);t.DEFAULT_OPTIONS={cols:80,rows:24,showCursorImmediately:!1,cursorBlink:!1,blinkIntervalDuration:0,cursorStyle:"block",cursorWidth:1,cursorInactiveStyle:"outline",drawBoldTextInBrightColors:!0,documentOverride:null,fastScrollSensitivity:5,fontFamily:"monospace",fontSize:15,fontWeight:"normal",fontWeightBold:"bold",ignoreBracketedPasteMode:!1,lineHeight:1,letterSpacing:0,linkHandler:null,logLevel:"info",logger:null,scrollback:1e3,scrollbar:{showScrollbar:!0},scrollOnEraseInDisplay:!1,scrollOnUserInput:!0,scrollSensitivity:1,screenReaderMode:!1,smoothScrollDuration:0,macOptionIsMeta:!1,macOptionClickForcesSelection:!1,minimumContrastRatio:1,mouseEventsRequireAlt:!1,disableStdin:!1,allowProposedApi:!1,allowTransparency:!1,tabStopWidth:8,theme:{},reflowCursorLine:!1,rescaleOverlappingGlyphs:!1,rightClickSelectsWord:r.isMac,windowOptions:{},windowsPty:{},wordSeparator:" ()[]{}',\"`",altClickMovesCursor:!0,convertEol:!1,termName:"xterm",quirks:{},vtExtensions:{}};const a=["normal","bold","100","200","300","400","500","600","700","800","900"];class o extends s.Disposable{constructor(e){super(),this._onOptionChange=this._register(new n.Emitter),this.onOptionChange=this._onOptionChange.event;const i={...t.DEFAULT_OPTIONS};for(const t in e)if(t in i)try{const s=e[t];i[t]=this._sanitizeAndValidateOption(t,s)}catch(e){console.error(e)}this.rawOptions=i,this.options={...i},this._setupOptions(),this._register((0,s.toDisposable)(()=>{this.rawOptions.linkHandler=null,this.rawOptions.documentOverride=null}))}onSpecificOptionChange(e,t){return this.onOptionChange(i=>{i===e&&t(this.rawOptions[e])})}onMultipleOptionChange(e,t){return this.onOptionChange(i=>{-1!==e.indexOf(i)&&t()})}_setupOptions(){const e=e=>{if(!(e in t.DEFAULT_OPTIONS))throw new Error(`No option with key "${e}"`);return this.rawOptions[e]},i=(e,i)=>{if(!(e in t.DEFAULT_OPTIONS))throw new Error(`No option with key "${e}"`);i=this._sanitizeAndValidateOption(e,i),this.rawOptions[e]!==i&&(this.rawOptions[e]=i,this._onOptionChange.fire(e))};for(const t in this.rawOptions){const s={get:e.bind(this,t),set:i.bind(this,t)};Object.defineProperty(this.options,t,s)}}_sanitizeAndValidateOption(e,i){switch(e){case"cursorStyle":if(i||(i=t.DEFAULT_OPTIONS[e]),!function(e){return"block"===e||"underline"===e||"bar"===e}(i))throw new Error(`"${i}" is not a valid value for ${e}`);break;case"wordSeparator":i||(i=t.DEFAULT_OPTIONS[e]);break;case"fontWeight":case"fontWeightBold":if("number"==typeof i&&1<=i&&i<=1e3)break;i=a.includes(i)?i:t.DEFAULT_OPTIONS[e];break;case"blinkIntervalDuration":if((i=Math.floor(i))<0)throw new Error(`${e} cannot be less than 0, value: ${i}`);break;case"cursorWidth":i=Math.floor(i);case"lineHeight":case"tabStopWidth":if(i<1)throw new Error(`${e} cannot be less than 1, value: ${i}`);break;case"minimumContrastRatio":i=Math.max(1,Math.min(21,Math.round(10*i)/10));break;case"scrollback":if((i=Math.min(i,4294967295))<0)throw new Error(`${e} cannot be less than 0, value: ${i}`);break;case"fastScrollSensitivity":case"scrollSensitivity":if(i<=0)throw new Error(`${e} cannot be less than or equal to 0, value: ${i}`);break;case"rows":case"cols":if(!i&&0!==i)throw new Error(`${e} must be numeric, value: ${i}`);break;case"windowsPty":i=i??{}}return i}}t.OptionsService=o},811(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,a=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)a=Reflect.decorate(e,t,i,s);else for(var o=e.length-1;o>=0;o--)(r=e[o])&&(a=(n<3?r(a):n>3?r(t,i,a):r(t,i))||a);return n>3&&a&&Object.defineProperty(t,i,a),a},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.OscLinkService=void 0;const n=i(501);let a=class{constructor(e){this._bufferService=e,this._nextId=1,this._entriesWithId=new Map,this._dataByLinkId=new Map}registerLink(e){const t=this._bufferService.buffer;if(void 0===e.id){const i=t.addMarker(t.ybase+t.y),s={data:e,id:this._nextId++,lines:[i]};return i.onDispose(()=>this._removeMarkerFromLink(s,i)),this._dataByLinkId.set(s.id,s),s.id}const i=e,s=this._getEntryIdKey(i),r=this._entriesWithId.get(s);if(r)return this.addLineToLink(r.id,t.ybase+t.y),r.id;const n=t.addMarker(t.ybase+t.y),a={id:this._nextId++,key:this._getEntryIdKey(i),data:i,lines:[n]};return n.onDispose(()=>this._removeMarkerFromLink(a,n)),this._entriesWithId.set(a.key,a),this._dataByLinkId.set(a.id,a),a.id}addLineToLink(e,t){const i=this._dataByLinkId.get(e);if(i&&i.lines.every(e=>e.line!==t)){const e=this._bufferService.buffer.addMarker(t);i.lines.push(e),e.onDispose(()=>this._removeMarkerFromLink(i,e))}}getLinkData(e){return this._dataByLinkId.get(e)?.data}_getEntryIdKey(e){return`${e.id};;${e.uri}`}_removeMarkerFromLink(e,t){const i=e.lines.indexOf(t);-1!==i&&(e.lines.splice(i,1),0===e.lines.length&&(void 0!==e.data.id&&this._entriesWithId.delete(e.key),this._dataByLinkId.delete(e.id)))}};t.OscLinkService=a,t.OscLinkService=a=s([r(0,n.IBufferService)],a)},201(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.serviceRegistry=void 0,t.getServiceDependencies=function(e){return e.di$dependencies||[]},t.createDecorator=function(e){if(t.serviceRegistry.has(e))return t.serviceRegistry.get(e);const i=function(e,t,s){if(3!==arguments.length)throw new Error("@IServiceName-decorator can only be used to decorate a parameter");!function(e,t,i){t.di$target===t?t.di$dependencies.push({id:e,index:i}):(t.di$dependencies=[{id:e,index:i}],t.di$target=t)}(i,e,s)};return i._id=e,t.serviceRegistry.set(e,i),i},t.serviceRegistry=new Map},501(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.IDecorationService=t.IUnicodeService=t.IOscLinkService=t.IOptionsService=t.ILogService=t.LogLevelEnum=t.IInstantiationService=t.ICharsetService=t.ICoreService=t.IMouseStateService=t.IBufferService=void 0;const s=i(201);var r;t.IBufferService=(0,s.createDecorator)("BufferService"),t.IMouseStateService=(0,s.createDecorator)("MouseStateService"),t.ICoreService=(0,s.createDecorator)("CoreService"),t.ICharsetService=(0,s.createDecorator)("CharsetService"),t.IInstantiationService=(0,s.createDecorator)("InstantiationService"),function(e){e[e.TRACE=0]="TRACE",e[e.DEBUG=1]="DEBUG",e[e.INFO=2]="INFO",e[e.WARN=3]="WARN",e[e.ERROR=4]="ERROR",e[e.OFF=5]="OFF"}(r||(t.LogLevelEnum=r={})),t.ILogService=(0,s.createDecorator)("LogService"),t.IOptionsService=(0,s.createDecorator)("OptionsService"),t.IOscLinkService=(0,s.createDecorator)("OscLinkService"),t.IUnicodeService=(0,s.createDecorator)("UnicodeService"),t.IDecorationService=(0,s.createDecorator)("DecorationService")},415(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.UnicodeService=void 0;const s=i(636);class r{constructor(){this._providers=Object.create(null),this._active="",this._onChange=new s.Emitter,this.onChange=this._onChange.event}static extractShouldJoin(e){return!!(1&e)}static extractWidth(e){return e>>1&3}static extractCharKind(e){return e>>3}static createPropertyValue(e,t,i=!1){return(16777215&e)<<3|(3&t)<<1|(i?1:0)}dispose(){this._onChange.dispose()}get versions(){return Object.keys(this._providers)}get activeVersion(){return this._active}set activeVersion(e){if(!this._providers[e])throw new Error(`unknown Unicode version "${e}"`);this._active=e,this._activeProvider=this._providers[e],this._onChange.fire(e)}register(e){this._providers[e.version]=e,this._active||(this.activeVersion=e.version)}wcwidth(e){return this._activeProvider.wcwidth(e)}getStringCellWidth(e){let t=0,i=0;const s=e.length;for(let n=0;n=s)return t+this.wcwidth(a);const i=e.charCodeAt(n);56320<=i&&i<=57343?a=1024*(a-55296)+i-56320+65536:t+=this.wcwidth(i)}const o=this.charProperties(a,i);let h=r.extractWidth(o);r.extractShouldJoin(o)&&(h-=r.extractWidth(i)),t+=h,i=o}return t}charProperties(e,t){return this._activeProvider.charProperties(e,t)}}t.UnicodeService=r},856(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.Terminal=void 0;const s=i(107),r=i(777),n=i(636);class a extends r.CoreTerminal{constructor(e={}){super(e),this._onBell=this._register(new n.Emitter),this.onBell=this._onBell.event,this._onCursorMove=this._register(new n.Emitter),this.onCursorMove=this._onCursorMove.event,this._onTitleChange=this._register(new n.Emitter),this.onTitleChange=this._onTitleChange.event,this._onA11yCharEmitter=this._register(new n.Emitter),this.onA11yChar=this._onA11yCharEmitter.event,this._onA11yTabEmitter=this._register(new n.Emitter),this.onA11yTab=this._onA11yTabEmitter.event,this._setup(),this._register(this._inputHandler.onRequestBell(()=>this.bell())),this._register(this._inputHandler.onRequestReset(()=>this.reset())),this._register(n.EventUtils.forward(this._inputHandler.onCursorMove,this._onCursorMove)),this._register(n.EventUtils.forward(this._inputHandler.onTitleChange,this._onTitleChange)),this._register(n.EventUtils.forward(this._inputHandler.onA11yChar,this._onA11yCharEmitter)),this._register(n.EventUtils.forward(this._inputHandler.onA11yTab,this._onA11yTabEmitter)),this._register(n.EventUtils.forward(n.EventUtils.map(this._inputHandler.onRequestRefreshRows,e=>({start:e?.start??0,end:e?.end??this.rows-1})),this._onRender))}get buffer(){return this.buffers.active}get markers(){return this.buffer.markers}registerMarker(e){return this.buffer.addMarker(this.buffer.ybase+this.buffer.y+e)}bell(){this._onBell.fire()}input(e,t=!0){this.coreService.triggerDataEvent(e,t)}resize(e,t){e===this.cols&&t===this.rows||super.resize(e,t)}clear(){this.buffer.clearAllMarkers(),this.buffer.lines.set(0,this.buffer.lines.get(this.buffer.ybase+this.buffer.y)),this.buffer.lines.length=1,this.buffer.ydisp=0,this.buffer.ybase=0,this.buffer.y=0;for(let e=1;e{var e=s;Object.defineProperty(e,"__esModule",{value:!0}),e.Terminal=void 0;const t=i(101),r=i(97),n=i(335),a=i(856),o=i(27),h=i(812),c=["cols","rows"];class l extends h.Disposable{constructor(e){super(),this._core=this._register(new a.Terminal(e)),this._addonManager=this._register(new o.AddonManager),this._publicOptions={...this._core.options};const t=e=>this._core.options[e],i=(e,t)=>{this._checkReadonlyOptions(e),this._core.options[e]=t};for(const e in this._core.options){const s={get:t.bind(this,e),set:i.bind(this,e)};Object.defineProperty(this._publicOptions,e,s)}}_checkReadonlyOptions(e){if(c.includes(e))throw new Error(`Option "${e}" can only be set in the constructor`)}_checkProposedApi(){if(!this._core.optionsService.options.allowProposedApi)throw new Error("You must set the allowProposedApi option to true to use proposed API")}get onBell(){return this._core.onBell}get onBinary(){return this._core.onBinary}get onCursorMove(){return this._core.onCursorMove}get onData(){return this._core.onData}get onLineFeed(){return this._core.onLineFeed}get onRender(){return this._core.onRender}get onResize(){return this._core.onResize}get onScroll(){return this._core.onScroll}get onTitleChange(){return this._core.onTitleChange}get onWriteParsed(){return this._core.onWriteParsed}get parser(){return this._parser??=new r.ParserApi(this._core),this._parser}get unicode(){return this._checkProposedApi(),new n.UnicodeApi(this._core)}get rows(){return this._core.rows}get cols(){return this._core.cols}get buffer(){return this._buffer??=this._register(new t.BufferNamespaceApi(this._core)),this._buffer}get markers(){return this._core.markers}get modes(){const e=this._core.coreService.decPrivateModes;let t="none";switch(this._core.mouseStateService.activeProtocol){case"X10":t="x10";break;case"VT200":t="vt200";break;case"DRAG":t="drag";break;case"ANY":t="any"}return{applicationCursorKeysMode:e.applicationCursorKeys,applicationKeypadMode:e.applicationKeypad,bracketedPasteMode:e.bracketedPasteMode,insertMode:this._core.coreService.modes.insertMode,mouseTrackingMode:t,originMode:e.origin,reverseWraparoundMode:e.reverseWraparound,sendFocusMode:e.sendFocus,showCursor:!this._core.coreService.isCursorHidden,synchronizedOutputMode:e.synchronizedOutput,win32InputMode:e.win32InputMode,wraparoundMode:e.wraparound}}get options(){return this._publicOptions}set options(e){for(const t in e)this._publicOptions[t]=e[t]}input(e,t=!0){this._core.input(e,t)}resize(e,t){this._verifyIntegers(e,t),this._core.resize(e,t)}registerMarker(e=0){return this._verifyIntegers(e),this._core.registerMarker(e)}addMarker(e){return this.registerMarker(e)}dispose(){super.dispose()}scrollLines(e){this._verifyIntegers(e),this._core.scrollLines(e)}scrollPages(e){this._verifyIntegers(e),this._core.scrollPages(e)}scrollToTop(){this._core.scrollToTop()}scrollToBottom(){this._core.scrollToBottom()}scrollToLine(e){this._verifyIntegers(e),this._core.scrollToLine(e)}clear(){this._core.clear()}write(e,t){this._core.write(e,t)}writeln(e,t){this._core.write(e),this._core.write("\r\n",t)}reset(){this._core.reset()}loadAddon(e){this._addonManager.loadAddon(this,e)}_verifyIntegers(...e){for(const t of e)if(t===1/0||isNaN(t)||t%1!=0)throw new Error("This API only accepts integers")}}e.Terminal=l})();var r=exports;for(var n in s)r[n]=s[n];s.__esModule&&Object.defineProperty(r,"__esModule",{value:!0})})(); ++(()=>{"use strict";var e={132(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.IntervalTimer=t.MicrotaskTimer=t.TimeoutTimer=void 0,t.timeout=function(e){return new Promise(t=>setTimeout(t,e))},t.disposableTimeout=function(e,t=0,i){const r=setTimeout(()=>{e(),i&&n.dispose()},t),n=(0,s.toDisposable)(()=>{clearTimeout(r)});return i?.add(n),n};const s=i(812);t.TimeoutTimer=class{constructor(){this._token=-1,this._isDisposed=!1}dispose(){this.cancel(),this._isDisposed=!0}cancel(){-1!==this._token&&(clearTimeout(this._token),this._token=-1)}cancelAndSet(e,t){if(this._isDisposed)throw new Error("Calling cancelAndSet on a disposed TimeoutTimer");this.cancel(),this._token=setTimeout(()=>{this._token=-1,e()},t)}setIfNotSet(e,t){if(this._isDisposed)throw new Error("Calling setIfNotSet on a disposed TimeoutTimer");-1===this._token&&(this._token=setTimeout(()=>{this._token=-1,e()},t))}},t.MicrotaskTimer=class{constructor(){this._isScheduled=!1,this._isDisposed=!1}dispose(){this.cancel(),this._isDisposed=!0}cancel(){this._isScheduled=!1}set(e){if(this._isDisposed)throw new Error("Calling set on a disposed MicrotaskTimer");this._isScheduled||(this._isScheduled=!0,queueMicrotask(()=>{this._isScheduled&&(this._isScheduled=!1,e())}))}},t.IntervalTimer=class{constructor(){this._isDisposed=!1}cancel(){this._disposable?.dispose(),this._disposable=void 0}cancelAndSet(e,t,i=globalThis){if(this._isDisposed)throw new Error("Calling cancelAndSet on a disposed IntervalTimer");this.cancel();const s=i.setInterval(()=>{e()},t);this._disposable={dispose:()=>{i.clearInterval(s),this._disposable=void 0}}}dispose(){this.cancel(),this._isDisposed=!0}}},639(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.CircularList=void 0;const s=i(812),r=i(636);class n extends s.Disposable{constructor(e){super(),this._maxLength=e,this.onDeleteEmitter=this._register(new r.Emitter),this.onDelete=this.onDeleteEmitter.event,this.onInsertEmitter=this._register(new r.Emitter),this.onInsert=this.onInsertEmitter.event,this.onTrimEmitter=this._register(new r.Emitter),this.onTrim=this.onTrimEmitter.event,this._array=new Array(this._maxLength),this._startIndex=0,this._length=0}get maxLength(){return this._maxLength}set maxLength(e){if(this._maxLength===e)return;const t=new Array(e);for(let i=0;ithis._length)for(let t=this._length;t=e;t--)this._array[this._getCyclicIndex(t+i.length)]=this._array[this._getCyclicIndex(t)];for(let t=0;tthis._maxLength){const e=this._length+i.length-this._maxLength;this._startIndex+=e,this._length=this._maxLength,this.onTrimEmitter.fire(e)}else this._length+=i.length}trimStart(e){e>this._length&&(e=this._length),this._startIndex+=e,this._length-=e,this.onTrimEmitter.fire(e)}shiftElements(e,t,i){if(!(t<=0)){if(e<0||e>=this._length)throw new Error("start argument out of range");if(e+i<0)throw new Error("Cannot shift elements in list beyond index 0");if(i>0){for(let s=t-1;s>=0;s--)this.set(e+s+i,this.get(e+s));const s=e+t+i-this._length;if(s>0)for(this._length+=s;this._length>this._maxLength;)this._length--,this._startIndex++,this.onTrimEmitter.fire(1)}else for(let s=0;s{this._onScrollApi?.fire(e.position)})),this._onScrollApi.event}get cols(){return this._bufferService.cols}get rows(){return this._bufferService.rows}get buffers(){return this._bufferService.buffers}get options(){return this.optionsService.options}set options(e){for(const t in e)this.optionsService.options[t]=e[t]}constructor(e){super(),this._windowsWrappingHeuristics=this._register(new b.MutableDisposable),this._onBinary=this._register(new v.Emitter),this.onBinary=this._onBinary.event,this._onData=this._register(new v.Emitter),this.onData=this._onData.event,this._onLineFeed=this._register(new v.Emitter),this.onLineFeed=this._onLineFeed.event,this._onRender=this._register(new v.Emitter),this.onRender=this._onRender.event,this._onResize=this._register(new v.Emitter),this.onResize=this._onResize.event,this._onWriteParsed=this._register(new v.Emitter),this.onWriteParsed=this._onWriteParsed.event,this._onScroll=this._register(new v.Emitter),this._instantiationService=new r.InstantiationService,this.optionsService=this._register(new o.OptionsService(e)),this._instantiationService.setService(s.IOptionsService,this.optionsService),this._logService=this._register(this._instantiationService.createInstance(n.LogService)),this._instantiationService.setService(s.ILogService,this._logService),this._bufferService=this._register(this._instantiationService.createInstance(a.BufferService)),this._instantiationService.setService(s.IBufferService,this._bufferService),this.coreService=this._register(this._instantiationService.createInstance(h.CoreService)),this._instantiationService.setService(s.ICoreService,this.coreService),this.mouseStateService=this._register(this._instantiationService.createInstance(c.MouseStateService)),this._instantiationService.setService(s.IMouseStateService,this.mouseStateService),this.unicodeService=this._register(this._instantiationService.createInstance(_.UnicodeService)),this.unicodeService.register(new l.UnicodeV6),this._instantiationService.setService(s.IUnicodeService,this.unicodeService),this._charsetService=this._instantiationService.createInstance(d.CharsetService),this._instantiationService.setService(s.ICharsetService,this._charsetService),this._oscLinkService=this._instantiationService.createInstance(g.OscLinkService),this._instantiationService.setService(s.IOscLinkService,this._oscLinkService),this._inputHandler=this._register(new f.InputHandler(this._bufferService,this._charsetService,this.coreService,this._logService,this.optionsService,this._oscLinkService,this.mouseStateService,this.unicodeService)),this._register(v.EventUtils.forward(this._inputHandler.onLineFeed,this._onLineFeed)),this._register(v.EventUtils.forward(this._bufferService.onResize,this._onResize)),this._register(v.EventUtils.forward(this.coreService.onData,this._onData)),this._register(v.EventUtils.forward(this.coreService.onBinary,this._onBinary)),this._register(this.coreService.onRequestScrollToBottom(()=>this.scrollToBottom(!0))),this._register(this.coreService.onUserInput(()=>this._writeBuffer.handleUserInput())),this._register(this.optionsService.onMultipleOptionChange(["windowsPty"],()=>this._handleWindowsPtyOptionChange())),this._register(this._bufferService.onScroll(()=>{this._onScroll.fire({position:this._bufferService.buffer.ydisp}),this._inputHandler.markRangeDirty(this._bufferService.buffer.scrollTop,this._bufferService.buffer.scrollBottom)})),this._writeBuffer=this._register(new p.WriteBuffer((e,t)=>this._inputHandler.parse(e,t))),this._register(v.EventUtils.forward(this._writeBuffer.onWriteParsed,this._onWriteParsed))}write(e,t){this._writeBuffer.write(e,t)}writeSync(e,t){this._logService.logLevel<=s.LogLevelEnum.WARN&&!S&&(this._logService.warn("writeSync is unreliable and will be removed soon."),S=!0),this._writeBuffer.writeSync(e,t)}input(e,t=!0){this.coreService.triggerDataEvent(e,t)}resize(e,t){isNaN(e)||isNaN(t)||(e=Math.max(e,2),t=Math.max(t,1),this._writeBuffer.flushSync(),this._bufferService.resize(e,t))}scroll(e,t=!1){this._bufferService.scroll(e,t)}scrollLines(e,t){this._bufferService.scrollLines(e,t)}scrollPages(e){this.scrollLines(e*(this.rows-1))}scrollToTop(){this.scrollLines(-this._bufferService.buffer.ydisp)}scrollToBottom(e){this.scrollLines(this._bufferService.buffer.ybase-this._bufferService.buffer.ydisp)}scrollToLine(e){const t=e-this._bufferService.buffer.ydisp;0!==t&&this.scrollLines(t)}registerEscHandler(e,t){return this._inputHandler.registerEscHandler(e,t)}registerDcsHandler(e,t){return this._inputHandler.registerDcsHandler(e,t)}registerCsiHandler(e,t){return this._inputHandler.registerCsiHandler(e,t)}registerOscHandler(e,t){return this._inputHandler.registerOscHandler(e,t)}registerApcHandler(e,t){return this._inputHandler.registerApcHandler(e,t)}_setup(){this._handleWindowsPtyOptionChange()}reset(){this._inputHandler.reset(),this._bufferService.reset(),this._charsetService.reset(),this.coreService.reset(),this.mouseStateService.reset()}_handleWindowsPtyOptionChange(){let e=!1;const t=this.optionsService.rawOptions.windowsPty;t&&void 0!==t.backend&&void 0!==t.buildNumber&&(e=!!("conpty"===t.backend&&t.buildNumber<21376)),e?this._enableWindowsWrappingHeuristics():this._windowsWrappingHeuristics.clear()}_enableWindowsWrappingHeuristics(){if(!this._windowsWrappingHeuristics.value){const e=[];e.push(this.onLineFeed(u.updateWindowsModeWrappedState.bind(null,this._bufferService))),e.push(this.registerCsiHandler({final:"H"},()=>((0,u.updateWindowsModeWrappedState)(this._bufferService),!1))),this._windowsWrappingHeuristics.value=(0,b.toDisposable)(()=>{for(const t of e)t.dispose()})}}}t.CoreTerminal=m},636(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.EventUtils=t.Emitter=void 0;const s=i(812);var r;t.Emitter=class{constructor(){this._listeners=[],this._disposed=!1}get event(){return this._event||(this._event=(e,t,i)=>{if(this._disposed)return(0,s.toDisposable)(()=>{});const r={fn:e,thisArgs:t};this._listeners=this._listeners.slice(),this._listeners.push(r);const n=(0,s.toDisposable)(()=>{const e=this._listeners.indexOf(r);-1!==e&&(this._listeners=this._listeners.slice(),this._listeners.splice(e,1))});return i&&(Array.isArray(i)?i.push(n):i.add(n)),n}),this._event}fire(e){if(this._disposed||!this._listeners.length)return;if(1===this._listeners.length)return void this._listeners[0].fn.call(this._listeners[0].thisArgs,e);const t=this._listeners;for(let i=0,s=t.length;it.fire(e))},e.map=function(e,t){return(i,s,r)=>e(e=>i.call(s,t(e)),void 0,r)},e.any=function(...e){return(t,i,r)=>{const n=new s.DisposableStore;for(const s of e)n.add(s(e=>t.call(i,e)));return r&&(Array.isArray(r)?r.push(n):r.add(n)),n}},e.runAndSubscribe=function(e,t,i){return t(i),e(e=>t(e))}}(r||(t.EventUtils=r={}))},486(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,a=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)a=Reflect.decorate(e,t,i,s);else for(var o=e.length-1;o>=0;o--)(r=e[o])&&(a=(n<3?r(a):n>3?r(t,i,a):r(t,i))||a);return n>3&&a&&Object.defineProperty(t,i,a),a},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.InputHandler=t.WindowsOptionsReportType=void 0,t.isValidColorIndex=E;const n=i(760),a=i(717),o=i(812),h=i(726),c=i(107),l=i(938),_=i(55),d=i(451),u=i(501),f=i(415),p=i(346),g=i(823),v=i(607),b=i(693),S=i(636),m=i(804),y={"(":0,")":1,"*":2,"+":3,"-":1,".":2};function C(e,t){if(e>24)return t.setWinLines||!1;switch(e){case 1:return!!t.restoreWin;case 2:return!!t.minimizeWin;case 3:return!!t.setWinPosition;case 4:return!!t.setWinSizePixels;case 5:return!!t.raiseWin;case 6:return!!t.lowerWin;case 7:return!!t.refreshWin;case 8:return!!t.setWinSizeChars;case 9:return!!t.maximizeWin;case 10:return!!t.fullscreenWin;case 11:return!!t.getWinState;case 13:return!!t.getWinPosition;case 14:return!!t.getWinSizePixels;case 15:return!!t.getScreenSizePixels;case 16:return!!t.getCellSizePixels;case 18:return!!t.getWinSizeChars;case 19:return!!t.getScreenSizeChars;case 20:return!!t.getIconTitle;case 21:return!!t.getWinTitle;case 22:return!!t.pushTitle;case 23:return!!t.popTitle;case 24:return!!t.setWinLines}return!1}var w;!function(e){e[e.GET_WIN_SIZE_PIXELS=0]="GET_WIN_SIZE_PIXELS",e[e.GET_CELL_SIZE_PIXELS=1]="GET_CELL_SIZE_PIXELS"}(w||(t.WindowsOptionsReportType=w={}));let B=0;class k extends o.Disposable{getAttrData(){return this._curAttrData}constructor(e,t,i,s,r,o,l,_,d=new a.EscapeSequenceParser){super(),this._bufferService=e,this._charsetService=t,this._coreService=i,this._logService=s,this._optionsService=r,this._oscLinkService=o,this._mouseStateService=l,this._unicodeService=_,this._parser=d,this._parseBuffer=new Uint32Array(4096),this._stringDecoder=new h.StringToUtf32,this._utf8Decoder=new h.Utf8ToUtf32,this._windowTitle="",this._iconName="",this._windowTitleStack=[],this._iconNameStack=[],this._curAttrData=c.DEFAULT_ATTR_DATA.clone(),this._eraseAttrDataInternal=c.DEFAULT_ATTR_DATA.clone(),this._onRequestBell=this._register(new S.Emitter),this.onRequestBell=this._onRequestBell.event,this._onRequestRefreshRows=this._register(new S.Emitter),this.onRequestRefreshRows=this._onRequestRefreshRows.event,this._onRequestReset=this._register(new S.Emitter),this.onRequestReset=this._onRequestReset.event,this._onRequestSendFocus=this._register(new S.Emitter),this.onRequestSendFocus=this._onRequestSendFocus.event,this._onRequestSyncScrollBar=this._register(new S.Emitter),this.onRequestSyncScrollBar=this._onRequestSyncScrollBar.event,this._onRequestWindowsOptionsReport=this._register(new S.Emitter),this.onRequestWindowsOptionsReport=this._onRequestWindowsOptionsReport.event,this._onA11yChar=this._register(new S.Emitter),this.onA11yChar=this._onA11yChar.event,this._onA11yTab=this._register(new S.Emitter),this.onA11yTab=this._onA11yTab.event,this._onCursorMove=this._register(new S.Emitter),this.onCursorMove=this._onCursorMove.event,this._onLineFeed=this._register(new S.Emitter),this.onLineFeed=this._onLineFeed.event,this._onScroll=this._register(new S.Emitter),this.onScroll=this._onScroll.event,this._onTitleChange=this._register(new S.Emitter),this.onTitleChange=this._onTitleChange.event,this._onColor=this._register(new S.Emitter),this.onColor=this._onColor.event,this._onRequestColorSchemeQuery=this._register(new S.Emitter),this.onRequestColorSchemeQuery=this._onRequestColorSchemeQuery.event,this._parseStack={paused:!1,cursorStartX:0,cursorStartY:0,decodedLength:0,position:0},this._specialColors=[256,257,258],this._register(this._parser),this._dirtyRowTracker=new A(this._bufferService),this._activeBuffer=this._bufferService.buffer,this._register(this._bufferService.buffers.onBufferActivate(e=>this._activeBuffer=e.activeBuffer)),this._parser.setCsiHandlerFallback((e,t)=>{this._logService.debug("Unknown CSI code: ",{identifier:this._parser.identToString(e),params:t.toArray()})}),this._parser.setEscHandlerFallback(e=>{this._logService.debug("Unknown ESC code: ",{identifier:this._parser.identToString(e)})}),this._parser.setExecuteHandlerFallback(e=>{this._logService.debug("Unknown EXECUTE code: ",{code:e})}),this._parser.setOscHandlerFallback((e,t,i)=>{this._logService.debug("Unknown OSC code: ",{identifier:e,action:t,data:i})}),this._parser.setDcsHandlerFallback((e,t,i)=>{"HOOK"===t&&(i=i.toArray()),this._logService.debug("Unknown DCS code: ",{identifier:this._parser.identToString(e),action:t,payload:i})}),this._parser.setApcHandlerFallback((e,t,i)=>{this._logService.debug("Unknown APC code: ",{identifier:this._parser.identToString(e),action:t,payload:i})}),this._parser.setPrintHandler((e,t,i)=>this.print(e,t,i)),this._parser.registerCsiHandler({final:"@"},e=>this.insertChars(e)),this._parser.registerCsiHandler({intermediates:" ",final:"@"},e=>this.scrollLeft(e)),this._parser.registerCsiHandler({final:"A"},e=>this.cursorUp(e)),this._parser.registerCsiHandler({intermediates:" ",final:"A"},e=>this.scrollRight(e)),this._parser.registerCsiHandler({final:"B"},e=>this.cursorDown(e)),this._parser.registerCsiHandler({final:"C"},e=>this.cursorForward(e)),this._parser.registerCsiHandler({final:"D"},e=>this.cursorBackward(e)),this._parser.registerCsiHandler({final:"E"},e=>this.cursorNextLine(e)),this._parser.registerCsiHandler({final:"F"},e=>this.cursorPrecedingLine(e)),this._parser.registerCsiHandler({final:"G"},e=>this.cursorCharAbsolute(e)),this._parser.registerCsiHandler({final:"H"},e=>this.cursorPosition(e)),this._parser.registerCsiHandler({final:"I"},e=>this.cursorForwardTab(e)),this._parser.registerCsiHandler({final:"J"},e=>this.eraseInDisplay(e,!1)),this._parser.registerCsiHandler({prefix:"?",final:"J"},e=>this.eraseInDisplay(e,!0)),this._parser.registerCsiHandler({final:"K"},e=>this.eraseInLine(e,!1)),this._parser.registerCsiHandler({prefix:"?",final:"K"},e=>this.eraseInLine(e,!0)),this._parser.registerCsiHandler({final:"L"},e=>this.insertLines(e)),this._parser.registerCsiHandler({final:"M"},e=>this.deleteLines(e)),this._parser.registerCsiHandler({final:"P"},e=>this.deleteChars(e)),this._parser.registerCsiHandler({final:"S"},e=>this.scrollUp(e)),this._parser.registerCsiHandler({final:"T"},e=>this.scrollDown(e)),this._parser.registerCsiHandler({final:"X"},e=>this.eraseChars(e)),this._parser.registerCsiHandler({final:"Z"},e=>this.cursorBackwardTab(e)),this._parser.registerCsiHandler({final:"^"},e=>this.scrollDown(e)),this._parser.registerCsiHandler({final:"`"},e=>this.charPosAbsolute(e)),this._parser.registerCsiHandler({final:"a"},e=>this.hPositionRelative(e)),this._parser.registerCsiHandler({final:"b"},e=>this.repeatPrecedingCharacter(e)),this._parser.registerCsiHandler({final:"c"},e=>this.sendDeviceAttributesPrimary(e)),this._parser.registerCsiHandler({prefix:">",final:"c"},e=>this.sendDeviceAttributesSecondary(e)),this._parser.registerCsiHandler({final:"d"},e=>this.linePosAbsolute(e)),this._parser.registerCsiHandler({final:"e"},e=>this.vPositionRelative(e)),this._parser.registerCsiHandler({final:"f"},e=>this.hVPosition(e)),this._parser.registerCsiHandler({final:"g"},e=>this.tabClear(e)),this._parser.registerCsiHandler({final:"h"},e=>this.setMode(e)),this._parser.registerCsiHandler({prefix:"?",final:"h"},e=>this.setModePrivate(e)),this._parser.registerCsiHandler({final:"l"},e=>this.resetMode(e)),this._parser.registerCsiHandler({prefix:"?",final:"l"},e=>this.resetModePrivate(e)),this._parser.registerCsiHandler({final:"m"},e=>this.charAttributes(e)),this._parser.registerCsiHandler({final:"n"},e=>this.deviceStatus(e)),this._parser.registerCsiHandler({prefix:"?",final:"n"},e=>this.deviceStatusPrivate(e)),this._parser.registerCsiHandler({intermediates:"!",final:"p"},e=>this.softReset(e)),this._parser.registerCsiHandler({prefix:">",final:"q"},e=>this.sendXtVersion(e)),this._parser.registerCsiHandler({intermediates:" ",final:"q"},e=>this.setCursorStyle(e)),this._parser.registerCsiHandler({final:"r"},e=>this.setScrollRegion(e)),this._parser.registerCsiHandler({final:"s"},e=>this.saveCursor(e)),this._parser.registerCsiHandler({final:"t"},e=>this.windowOptions(e)),this._parser.registerCsiHandler({final:"u"},e=>this.restoreCursor(e)),this._parser.registerCsiHandler({intermediates:"'",final:"}"},e=>this.insertColumns(e)),this._parser.registerCsiHandler({intermediates:"'",final:"~"},e=>this.deleteColumns(e)),this._parser.registerCsiHandler({intermediates:'"',final:"q"},e=>this.selectProtected(e)),this._parser.registerCsiHandler({intermediates:"$",final:"p"},e=>this.requestMode(e,!0)),this._parser.registerCsiHandler({prefix:"?",intermediates:"$",final:"p"},e=>this.requestMode(e,!1)),this._parser.registerCsiHandler({prefix:"=",final:"u"},e=>this.kittyKeyboardSet(e)),this._parser.registerCsiHandler({prefix:"?",final:"u"},e=>this.kittyKeyboardQuery(e)),this._parser.registerCsiHandler({prefix:">",final:"u"},e=>this.kittyKeyboardPush(e)),this._parser.registerCsiHandler({prefix:"<",final:"u"},e=>this.kittyKeyboardPop(e)),this._parser.setExecuteHandler("",()=>this.bell()),this._parser.setExecuteHandler("\n",()=>this.lineFeed()),this._parser.setExecuteHandler("\v",()=>this.lineFeed()),this._parser.setExecuteHandler("\f",()=>this.lineFeed()),this._parser.setExecuteHandler("\r",()=>this.carriageReturn()),this._parser.setExecuteHandler("\b",()=>this.backspace()),this._parser.setExecuteHandler("\t",()=>this.tab()),this._parser.setExecuteHandler("",()=>this.shiftOut()),this._parser.setExecuteHandler("",()=>this.shiftIn()),this._parser.setExecuteHandler("„",()=>this.index()),this._parser.setExecuteHandler("…",()=>this.nextLine()),this._parser.setExecuteHandler("ˆ",()=>this.tabSet()),this._parser.registerOscHandler(0,new p.OscHandler(e=>(this.setTitle(e),this.setIconName(e),!0))),this._parser.registerOscHandler(1,new p.OscHandler(e=>this.setIconName(e))),this._parser.registerOscHandler(2,new p.OscHandler(e=>this.setTitle(e))),this._parser.registerOscHandler(4,new p.OscHandler(e=>this.setOrReportIndexedColor(e))),this._parser.registerOscHandler(8,new p.OscHandler(e=>this.setHyperlink(e))),this._parser.registerOscHandler(10,new p.OscHandler(e=>this.setOrReportFgColor(e))),this._parser.registerOscHandler(11,new p.OscHandler(e=>this.setOrReportBgColor(e))),this._parser.registerOscHandler(12,new p.OscHandler(e=>this.setOrReportCursorColor(e))),this._parser.registerOscHandler(104,new p.OscHandler(e=>this.restoreIndexedColor(e))),this._parser.registerOscHandler(110,new p.OscHandler(e=>this.restoreFgColor(e))),this._parser.registerOscHandler(111,new p.OscHandler(e=>this.restoreBgColor(e))),this._parser.registerOscHandler(112,new p.OscHandler(e=>this.restoreCursorColor(e))),this._parser.registerEscHandler({final:"7"},()=>this.saveCursor()),this._parser.registerEscHandler({final:"8"},()=>this.restoreCursor()),this._parser.registerEscHandler({final:"D"},()=>this.index()),this._parser.registerEscHandler({final:"E"},()=>this.nextLine()),this._parser.registerEscHandler({final:"H"},()=>this.tabSet()),this._parser.registerEscHandler({final:"M"},()=>this.reverseIndex()),this._parser.registerEscHandler({final:"="},()=>this.keypadApplicationMode()),this._parser.registerEscHandler({final:">"},()=>this.keypadNumericMode()),this._parser.registerEscHandler({final:"c"},()=>this.fullReset()),this._parser.registerEscHandler({final:"n"},()=>this.setgLevel(2)),this._parser.registerEscHandler({final:"o"},()=>this.setgLevel(3)),this._parser.registerEscHandler({final:"|"},()=>this.setgLevel(3)),this._parser.registerEscHandler({final:"}"},()=>this.setgLevel(2)),this._parser.registerEscHandler({final:"~"},()=>this.setgLevel(1)),this._parser.registerEscHandler({intermediates:"%",final:"@"},()=>this.selectDefaultCharset()),this._parser.registerEscHandler({intermediates:"%",final:"G"},()=>this.selectDefaultCharset());for(const e in n.CHARSETS)this._parser.registerEscHandler({intermediates:"(",final:e},()=>this.selectCharset("("+e)),this._parser.registerEscHandler({intermediates:")",final:e},()=>this.selectCharset(")"+e)),this._parser.registerEscHandler({intermediates:"*",final:e},()=>this.selectCharset("*"+e)),this._parser.registerEscHandler({intermediates:"+",final:e},()=>this.selectCharset("+"+e)),this._parser.registerEscHandler({intermediates:"-",final:e},()=>this.selectCharset("-"+e)),this._parser.registerEscHandler({intermediates:".",final:e},()=>this.selectCharset("."+e)),this._parser.registerEscHandler({intermediates:"/",final:e},()=>this.selectCharset("/"+e));this._parser.registerEscHandler({intermediates:"#",final:"8"},()=>this.screenAlignmentPattern()),this._parser.setErrorHandler(e=>(this._logService.error("Parsing error: ",e),e)),this._parser.registerDcsHandler({intermediates:"$",final:"q"},new g.DcsHandler((e,t)=>this.requestStatusString(e,t)))}_preserveStack(e,t,i,s){this._parseStack.paused=!0,this._parseStack.cursorStartX=e,this._parseStack.cursorStartY=t,this._parseStack.decodedLength=i,this._parseStack.position=s}_logSlowResolvingAsync(e){if(this._logService.logLevel<=u.LogLevelEnum.WARN){let t;const i=new Promise((e,i)=>{t=setTimeout(()=>i("#SLOW_TIMEOUT"),5e3)});Promise.race([e,i]).then(()=>{void 0!==t&&clearTimeout(t)},e=>{if(void 0!==t&&clearTimeout(t),"#SLOW_TIMEOUT"!==e)throw e;console.warn("async parser handler taking longer than 5000 ms")})}}_getCurrentLinkId(){return this._curAttrData.extended.urlId}parse(e,t){let i,s=this._activeBuffer.x,r=this._activeBuffer.y,n=0;const a=this._parseStack.paused;if(a){if(i=this._parser.parse(this._parseBuffer,this._parseStack.decodedLength,t))return this._logSlowResolvingAsync(i),i;s=this._parseStack.cursorStartX,r=this._parseStack.cursorStartY,this._parseStack.paused=!1,e.length>131072&&(n=this._parseStack.position+131072)}if(this._logService.logLevel<=u.LogLevelEnum.DEBUG&&this._logService.debug("parsing data "+("string"==typeof e?` "${e}"`:` "${Array.prototype.map.call(e,e=>String.fromCharCode(e)).join("")}"`)),this._logService.logLevel===u.LogLevelEnum.TRACE&&this._logService.trace("parsing data (codes)","string"==typeof e?e.split("").map(e=>e.charCodeAt(0)):e),this._parseBuffer.length131072)for(let t=n;t0&&2===p.getWidth(this._activeBuffer.x-1)&&p.setCellFromCodepoint(this._activeBuffer.x-1,0,1,u);let g=this._parser.precedingJoinState;for(let v=t;v=32&&e[v]<127){do{p.setCellFromCodepoint(this._activeBuffer.x++,e[v],1,u)}while(++v=32&&e[v]<127);v--;continue}if(s=e[v],173===s)continue;if(s<127&&n){const e=n[String.fromCharCode(s)];e&&(s=e.charCodeAt(0))}const t=this._unicodeService.charProperties(s,g);r=f.UnicodeService.extractWidth(t);const b=f.UnicodeService.extractShouldJoin(t),S=b?f.UnicodeService.extractWidth(g):0;g=t,a&&this._onA11yChar.fire((0,h.stringFromCodePoint)(s));const m=this._getCurrentLinkId();if(m&&this._oscLinkService.addLineToLink(m,this._activeBuffer.ybase+this._activeBuffer.y),this._activeBuffer.x+r-S>o)if(_){const e=p;let t=this._activeBuffer.x-S;if(this._activeBuffer.x=S,this._activeBuffer.y++,this._activeBuffer.y===this._activeBuffer.scrollBottom+1?(this._activeBuffer.y--,this._bufferService.scroll(this._eraseAttrData(),!0)):(this._activeBuffer.y>=this._bufferService.rows&&(this._activeBuffer.y=this._bufferService.rows-1),this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y).isWrapped=!0),p=this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y),!p)return;for(S>0&&p instanceof c.BufferLine&&p.copyCellsFrom(e,t,0,S,!1);t=0;)p.setCellFromCodepoint(this._activeBuffer.x++,0,0,u);continue}if(d&&(p.insertCells(this._activeBuffer.x,r-S,this._activeBuffer.getNullCell(u)),2===p.getWidth(o-1)&&p.setCellFromCodepoint(o-1,l.NULL_CELL_CODE,l.NULL_CELL_WIDTH,u)),p.setCellFromCodepoint(this._activeBuffer.x++,s,r,u),r>0)for(;--r;)p.setCellFromCodepoint(this._activeBuffer.x++,0,0,u)}this._parser.precedingJoinState=g,this._activeBuffer.x0&&0===p.getWidth(this._activeBuffer.x)&&!p.hasContent(this._activeBuffer.x)&&p.setCellFromCodepoint(this._activeBuffer.x,0,1,u),this._dirtyRowTracker.markDirty(this._activeBuffer.y)}registerCsiHandler(e,t){return"t"!==e.final||e.prefix||e.intermediates?this._parser.registerCsiHandler(e,t):this._parser.registerCsiHandler(e,e=>!C(e.params[0],this._optionsService.rawOptions.windowOptions)||t(e))}registerDcsHandler(e,t){return this._parser.registerDcsHandler(e,new g.DcsHandler(t))}registerEscHandler(e,t){return this._parser.registerEscHandler(e,t)}registerOscHandler(e,t){return this._parser.registerOscHandler(e,new p.OscHandler(t))}registerApcHandler(e,t){return this._parser.registerApcHandler(e,new v.ApcHandler(t))}bell(){return this._onRequestBell.fire(),!0}lineFeed(){return this._dirtyRowTracker.markDirty(this._activeBuffer.y),this._optionsService.rawOptions.convertEol&&(this._activeBuffer.x=0),this._activeBuffer.y++,this._activeBuffer.y===this._activeBuffer.scrollBottom+1?(this._activeBuffer.y--,this._bufferService.scroll(this._eraseAttrData())):this._activeBuffer.y>=this._bufferService.rows?this._activeBuffer.y=this._bufferService.rows-1:this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y).isWrapped=!1,this._activeBuffer.x>=this._bufferService.cols&&this._activeBuffer.x--,this._dirtyRowTracker.markDirty(this._activeBuffer.y),this._onLineFeed.fire(),!0}carriageReturn(){return this._activeBuffer.x=0,!0}backspace(){if(!this._coreService.decPrivateModes.reverseWraparound)return this._restrictCursor(),this._activeBuffer.x>0&&this._activeBuffer.x--,!0;if(this._restrictCursor(this._bufferService.cols),this._activeBuffer.x>0)this._activeBuffer.x--;else if(0===this._activeBuffer.x&&this._activeBuffer.y>this._activeBuffer.scrollTop&&this._activeBuffer.y<=this._activeBuffer.scrollBottom&&this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y)?.isWrapped){this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y).isWrapped=!1,this._activeBuffer.y--,this._activeBuffer.x=this._bufferService.cols-1;const e=this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y);e.hasWidth(this._activeBuffer.x)&&!e.hasContent(this._activeBuffer.x)&&this._activeBuffer.x--}return this._restrictCursor(),!0}tab(){if(this._activeBuffer.x>=this._bufferService.cols)return!0;const e=this._activeBuffer.x;return this._activeBuffer.x=this._activeBuffer.nextStop(),this._optionsService.rawOptions.screenReaderMode&&this._onA11yTab.fire(this._activeBuffer.x-e),!0}shiftOut(){return this._charsetService.setgLevel(1),!0}shiftIn(){return this._charsetService.setgLevel(0),!0}_restrictCursor(e=this._bufferService.cols-1){this._activeBuffer.x=Math.min(e,Math.max(0,this._activeBuffer.x)),this._activeBuffer.y=this._coreService.decPrivateModes.origin?Math.min(this._activeBuffer.scrollBottom,Math.max(this._activeBuffer.scrollTop,this._activeBuffer.y)):Math.min(this._bufferService.rows-1,Math.max(0,this._activeBuffer.y)),this._dirtyRowTracker.markDirty(this._activeBuffer.y)}_setCursor(e,t){this._dirtyRowTracker.markDirty(this._activeBuffer.y),this._coreService.decPrivateModes.origin?(this._activeBuffer.x=e,this._activeBuffer.y=this._activeBuffer.scrollTop+t):(this._activeBuffer.x=e,this._activeBuffer.y=t),this._restrictCursor(),this._dirtyRowTracker.markDirty(this._activeBuffer.y)}_moveCursor(e,t){this._restrictCursor(),this._setCursor(this._activeBuffer.x+e,this._activeBuffer.y+t)}cursorUp(e){const t=this._activeBuffer.y-this._activeBuffer.scrollTop;return t>=0?this._moveCursor(0,-Math.min(t,e.params[0]||1)):this._moveCursor(0,-(e.params[0]||1)),!0}cursorDown(e){const t=this._activeBuffer.scrollBottom-this._activeBuffer.y;return t>=0?this._moveCursor(0,Math.min(t,e.params[0]||1)):this._moveCursor(0,e.params[0]||1),!0}cursorForward(e){return this._moveCursor(e.params[0]||1,0),!0}cursorBackward(e){return this._moveCursor(-(e.params[0]||1),0),!0}cursorNextLine(e){return this.cursorDown(e),this._activeBuffer.x=0,!0}cursorPrecedingLine(e){return this.cursorUp(e),this._activeBuffer.x=0,!0}cursorCharAbsolute(e){return this._setCursor((e.params[0]||1)-1,this._activeBuffer.y),!0}cursorPosition(e){return this._setCursor(e.length>=2?(e.params[1]||1)-1:0,(e.params[0]||1)-1),!0}charPosAbsolute(e){return this._setCursor((e.params[0]||1)-1,this._activeBuffer.y),!0}hPositionRelative(e){return this._moveCursor(e.params[0]||1,0),!0}linePosAbsolute(e){return this._setCursor(this._activeBuffer.x,(e.params[0]||1)-1),!0}vPositionRelative(e){return this._moveCursor(0,e.params[0]||1),!0}hVPosition(e){return this.cursorPosition(e),!0}tabClear(e){const t=e.params[0];return 0===t?delete this._activeBuffer.tabs[this._activeBuffer.x]:3===t&&(this._activeBuffer.tabs={}),!0}cursorForwardTab(e){if(this._activeBuffer.x>=this._bufferService.cols)return!0;let t=e.params[0]||1;for(;t--;)this._activeBuffer.x=this._activeBuffer.nextStop();return!0}cursorBackwardTab(e){if(this._activeBuffer.x>=this._bufferService.cols)return!0;let t=e.params[0]||1;for(;t--;)this._activeBuffer.x=this._activeBuffer.prevStop();return!0}selectProtected(e){const t=e.params[0];return 1===t&&(this._curAttrData.bg|=536870912),2!==t&&0!==t||(this._curAttrData.bg&=-536870913),!0}_eraseInBufferLine(e,t,i,s=!1,r=!1){const n=this._activeBuffer.lines.get(this._activeBuffer.ybase+e);n&&(n.replaceCells(t,i,this._activeBuffer.getNullCell(this._eraseAttrData()),r),s&&(n.isWrapped=!1))}_resetBufferLine(e,t=!1){const i=this._activeBuffer.lines.get(this._activeBuffer.ybase+e);i&&(i.fill(this._activeBuffer.getNullCell(this._eraseAttrData()),t),this._bufferService.buffer.clearMarkers(this._activeBuffer.ybase+e),i.isWrapped=!1)}eraseInDisplay(e,t=!1){let i;switch(this._restrictCursor(this._bufferService.cols),e.params[0]){case 0:for(i=this._activeBuffer.y,this._dirtyRowTracker.markDirty(i),this._eraseInBufferLine(i++,this._activeBuffer.x,this._bufferService.cols,0===this._activeBuffer.x,t);i=this._bufferService.cols){const e=this._activeBuffer.lines.get(i+1);e&&(e.isWrapped=!1)}for(;i--;)this._resetBufferLine(i,t);this._dirtyRowTracker.markDirty(0);break;case 2:if(this._optionsService.rawOptions.scrollOnEraseInDisplay){for(i=this._bufferService.rows,this._dirtyRowTracker.markRangeDirty(0,i-1);i--;){const e=this._activeBuffer.lines.get(this._activeBuffer.ybase+i);if(e?.getTrimmedLength())break}for(;i>=0;i--)this._bufferService.scroll(this._eraseAttrData())}else{for(i=this._bufferService.rows,this._dirtyRowTracker.markDirty(i-1);i--;)this._resetBufferLine(i,t);this._dirtyRowTracker.markDirty(0)}break;case 3:const e=this._activeBuffer.lines.length-this._bufferService.rows;e>0&&(this._activeBuffer.lines.trimStart(e),this._activeBuffer.ybase=Math.max(this._activeBuffer.ybase-e,0),this._activeBuffer.ydisp=Math.max(this._activeBuffer.ydisp-e,0),this._activeBuffer===this._bufferService.buffers.normal&&(this._bufferService.isUserScrolling=!1),this._onScroll.fire(0))}return!0}eraseInLine(e,t=!1){switch(this._restrictCursor(this._bufferService.cols),e.params[0]){case 0:this._eraseInBufferLine(this._activeBuffer.y,this._activeBuffer.x,this._bufferService.cols,0===this._activeBuffer.x,t);break;case 1:this._eraseInBufferLine(this._activeBuffer.y,0,this._activeBuffer.x+1,!1,t);break;case 2:this._eraseInBufferLine(this._activeBuffer.y,0,this._bufferService.cols,!0,t)}return this._dirtyRowTracker.markDirty(this._activeBuffer.y),!0}insertLines(e){this._restrictCursor();let t=e.params[0]||1;if(this._activeBuffer.y>this._activeBuffer.scrollBottom||this._activeBuffer.ythis._activeBuffer.scrollBottom||this._activeBuffer.ythis._activeBuffer.scrollBottom||this._activeBuffer.ythis._activeBuffer.scrollBottom||this._activeBuffer.ythis._activeBuffer.scrollBottom||this._activeBuffer.ythis._activeBuffer.scrollBottom||this._activeBuffer.y65535?2:1}let h=o;for(let e=1;e0||(this._is("xterm")||this._is("rxvt-unicode")||this._is("screen")?this._coreService.triggerDataEvent("[?1;2c"):this._is("linux")&&this._coreService.triggerDataEvent("[?6c")),!0}sendDeviceAttributesSecondary(e){return e.params[0]>0||(this._is("xterm")?this._coreService.triggerDataEvent("[>0;276;0c"):this._is("rxvt-unicode")?this._coreService.triggerDataEvent("[>85;95;0c"):this._is("linux")?this._coreService.triggerDataEvent(e.params[0]+"c"):this._is("screen")&&this._coreService.triggerDataEvent("[>83;40003;0c")),!0}sendXtVersion(e){return e.params[0]>0||this._coreService.triggerDataEvent(`P>|xterm.js(${m.XTERM_VERSION})\\`),!0}_is(e){return(this._optionsService.rawOptions.termName+"").startsWith(e)}setMode(e){for(let t=0;t(n.triggerDataEvent(`[${t?"":"?"}${e};${i}$y`),!0),d=e=>e?1:2,u=e.params[0];return t?_(u,2===u?4:4===u?d(n.modes.insertMode):12===u?3:20===u?d(l.convertEol):0):1===u?_(u,d(i.applicationCursorKeys)):3===u?_(u,l.windowOptions.setWinLines?80===o?2:132===o?1:0:0):6===u?_(u,d(i.origin)):7===u?_(u,d(i.wraparound)):8===u?_(u,3):9===u?_(u,d("X10"===s)):12===u?_(u,d(l.cursorBlink)):25===u?_(u,d(!n.isCursorHidden)):45===u?_(u,d(i.reverseWraparound)):66===u?_(u,d(i.applicationKeypad)):67===u?_(u,4):1e3===u?_(u,d("VT200"===s)):1002===u?_(u,d("DRAG"===s)):1003===u?_(u,d("ANY"===s)):1004===u?_(u,d(i.sendFocus)):1005===u?_(u,4):1006===u?_(u,d("SGR"===r)):1015===u?_(u,4):1016===u?_(u,d("SGR_PIXELS"===r)):1048===u?_(u,1):47===u||1047===u||1049===u?_(u,d(h===c)):2004===u?_(u,d(i.bracketedPasteMode)):2026===u?_(u,d(i.synchronizedOutput)):9001===u&&this._optionsService.rawOptions.vtExtensions?.win32InputMode?_(u,d(i.win32InputMode)):_(u,0)}_updateAttrColor(e,t,i,s,r){return 2===t?(e|=50331648,e&=-16777216,e|=d.AttributeData.fromColorRGB([i,s,r])):5===t&&(e&=-67108864,e|=33554432|255&i),e}_extractColor(e,t,i){const s=[0,0,-1,0,0,0];let r=0,n=0;do{if(s[n+r]=e.params[t+n],e.hasSubParams(t+n)){const i=e.getSubParams(t+n);let a=0;do{5===s[1]&&(r=1),s[n+a+1+r]=i[a]}while(++a=2||2===s[1]&&n+r>=5)break;s[1]&&(r=1)}while(++n+t5)&&(e=1),t.extended.underlineStyle=e,t.fg|=268435456,0===e&&(t.fg&=-268435457),t.updateExtended()}_processSGR0(e){e.fg=c.DEFAULT_ATTR_DATA.fg,e.bg=c.DEFAULT_ATTR_DATA.bg,e.extended=e.extended.clone(),e.extended.underlineStyle=0,e.extended.underlineColor&=-67108864,e.updateExtended()}charAttributes(e){if(1===e.length&&0===e.params[0])return this._processSGR0(this._curAttrData),!0;const t=e.length;let i;const s=this._curAttrData;for(let r=0;r=30&&i<=37?(s.fg&=-67108864,s.fg|=16777216|i-30):i>=40&&i<=47?(s.bg&=-67108864,s.bg|=16777216|i-40):i>=90&&i<=97?(s.fg&=-67108864,s.fg|=16777224|i-90):i>=100&&i<=107?(s.bg&=-67108864,s.bg|=16777224|i-100):0===i?this._processSGR0(s):1===i?s.fg|=134217728:3===i?s.bg|=67108864:4===i?(s.fg|=268435456,this._processUnderline(e.hasSubParams(r)?e.getSubParams(r)[0]:1,s)):5===i?s.fg|=536870912:7===i?s.fg|=67108864:8===i?s.fg|=1073741824:9===i?s.fg|=2147483648:2===i?s.bg|=134217728:21===i?this._processUnderline(2,s):22===i?(s.fg&=-134217729,s.bg&=-134217729):23===i?s.bg&=-67108865:24===i?(s.fg&=-268435457,this._processUnderline(0,s)):25===i?s.fg&=-536870913:27===i?s.fg&=-67108865:28===i?s.fg&=-1073741825:29===i?s.fg&=2147483647:39===i?(s.fg&=-67108864,s.fg|=16777215&c.DEFAULT_ATTR_DATA.fg):49===i?(s.bg&=-67108864,s.bg|=16777215&c.DEFAULT_ATTR_DATA.bg):38===i||48===i||58===i?r+=this._extractColor(e,r,s):53===i?s.bg|=1073741824:55===i?s.bg&=-1073741825:221===i&&(this._optionsService.rawOptions.vtExtensions?.kittySgrBoldFaintControl??1)?s.fg&=-134217729:222===i&&(this._optionsService.rawOptions.vtExtensions?.kittySgrBoldFaintControl??1)?s.bg&=-134217729:59===i?(s.extended=s.extended.clone(),s.extended.underlineColor=-1,s.updateExtended()):this._logService.debug("Unknown SGR attribute: %d.",i);return!0}deviceStatus(e){switch(e.params[0]){case 5:this._coreService.triggerDataEvent("");break;case 6:const e=this._activeBuffer.y+1,t=this._activeBuffer.x+1;this._coreService.triggerDataEvent(`[${e};${t}R`)}return!0}deviceStatusPrivate(e){switch(e.params[0]){case 6:const e=this._activeBuffer.y+1,t=this._activeBuffer.x+1;this._coreService.triggerDataEvent(`[?${e};${t}R`);break;case 15:case 25:case 26:case 53:break;case 996:(this._optionsService.rawOptions.vtExtensions?.colorSchemeQuery??1)&&this._onRequestColorSchemeQuery.fire()}return!0}softReset(e){return this._coreService.isCursorHidden=!1,this._onRequestSyncScrollBar.fire(),this._activeBuffer.scrollTop=0,this._activeBuffer.scrollBottom=this._bufferService.rows-1,this._curAttrData=c.DEFAULT_ATTR_DATA.clone(),this._coreService.reset(),this._charsetService.reset(),this._activeBuffer.savedX=0,this._activeBuffer.savedY=this._activeBuffer.ybase,this._activeBuffer.savedCurAttrData.fg=this._curAttrData.fg,this._activeBuffer.savedCurAttrData.bg=this._curAttrData.bg,this._activeBuffer.savedCharset=this._charsetService.charset,this._coreService.decPrivateModes.origin=!1,!0}setCursorStyle(e){const t=0===e.length?1:e.params[0];if(0===t)this._coreService.decPrivateModes.cursorStyle=void 0,this._coreService.decPrivateModes.cursorBlink=void 0;else{switch(t){case 1:case 2:this._coreService.decPrivateModes.cursorStyle="block";break;case 3:case 4:this._coreService.decPrivateModes.cursorStyle="underline";break;case 5:case 6:this._coreService.decPrivateModes.cursorStyle="bar"}const e=t%2==1;this._coreService.decPrivateModes.cursorBlink=e}return!0}setScrollRegion(e){const t=e.params[0]||1;let i;return(e.length<2||(i=e.params[1])>this._bufferService.rows||0===i)&&(i=this._bufferService.rows),i>t&&(this._activeBuffer.scrollTop=t-1,this._activeBuffer.scrollBottom=i-1,this._setCursor(0,0)),!0}windowOptions(e){if(!C(e.params[0],this._optionsService.rawOptions.windowOptions))return!0;const t=e.length>1?e.params[1]:0;switch(e.params[0]){case 14:2!==t&&this._onRequestWindowsOptionsReport.fire(w.GET_WIN_SIZE_PIXELS);break;case 16:this._onRequestWindowsOptionsReport.fire(w.GET_CELL_SIZE_PIXELS);break;case 18:this._bufferService&&this._coreService.triggerDataEvent(`[8;${this._bufferService.rows};${this._bufferService.cols}t`);break;case 22:0!==t&&2!==t||(this._windowTitleStack.push(this._windowTitle),this._windowTitleStack.length>10&&this._windowTitleStack.shift()),0!==t&&1!==t||(this._iconNameStack.push(this._iconName),this._iconNameStack.length>10&&this._iconNameStack.shift());break;case 23:0!==t&&2!==t||this._windowTitleStack.length&&this.setTitle(this._windowTitleStack.pop()),0!==t&&1!==t||this._iconNameStack.length&&this.setIconName(this._iconNameStack.pop())}return!0}saveCursor(e){return this._activeBuffer.savedX=this._activeBuffer.x,this._activeBuffer.savedY=this._activeBuffer.ybase+this._activeBuffer.y,this._activeBuffer.savedCurAttrData.fg=this._curAttrData.fg,this._activeBuffer.savedCurAttrData.bg=this._curAttrData.bg,this._activeBuffer.savedCharset=this._charsetService.charset,this._activeBuffer.savedCharsets=this._charsetService.charsets.slice(),this._activeBuffer.savedGlevel=this._charsetService.glevel,this._activeBuffer.savedOriginMode=this._coreService.decPrivateModes.origin,this._activeBuffer.savedWraparoundMode=this._coreService.decPrivateModes.wraparound,!0}restoreCursor(e){this._activeBuffer.x=this._activeBuffer.savedX||0,this._activeBuffer.y=Math.max(this._activeBuffer.savedY-this._activeBuffer.ybase,0),this._curAttrData.fg=this._activeBuffer.savedCurAttrData.fg,this._curAttrData.bg=this._activeBuffer.savedCurAttrData.bg;for(let e=0;e1;){const e=i.shift(),s=i.shift();if(/^\d+$/.exec(e)){const i=parseInt(e,10);if(E(i))if("?"===s)t.push({type:0,index:i});else{const e=(0,b.parseColor)(s);e&&t.push({type:1,index:i,color:e})}}}return t.length&&this._onColor.fire(t),!0}setHyperlink(e){const t=e.indexOf(";");if(-1===t)return!0;const i=e.slice(0,t).trim(),s=e.slice(t+1);return s?this._createHyperlink(i,s):!i.trim()&&this._finishHyperlink()}_createHyperlink(e,t){this._getCurrentLinkId()&&this._finishHyperlink();const i=e.split(":");let s;const r=i.findIndex(e=>e.startsWith("id="));return-1!==r&&(s=i[r].slice(3)||void 0),this._curAttrData.extended=this._curAttrData.extended.clone(),this._curAttrData.extended.urlId=this._oscLinkService.registerLink({id:s,uri:t}),this._curAttrData.updateExtended(),!0}_finishHyperlink(){return this._curAttrData.extended=this._curAttrData.extended.clone(),this._curAttrData.extended.urlId=0,this._curAttrData.updateExtended(),!0}_setOrReportSpecialColor(e,t){const i=e.split(";");for(let e=0;e=this._specialColors.length);++e,++t)if("?"===i[e])this._onColor.fire([{type:0,index:this._specialColors[t]}]);else{const s=(0,b.parseColor)(i[e]);s&&this._onColor.fire([{type:1,index:this._specialColors[t],color:s}])}return!0}setOrReportFgColor(e){return this._setOrReportSpecialColor(e,0)}setOrReportBgColor(e){return this._setOrReportSpecialColor(e,1)}setOrReportCursorColor(e){return this._setOrReportSpecialColor(e,2)}restoreIndexedColor(e){if(!e)return this._onColor.fire([{type:2}]),!0;const t=[],i=e.split(";");for(let e=0;e=this._bufferService.rows&&(this._activeBuffer.y=this._bufferService.rows-1),this._restrictCursor(),!0}tabSet(){return this._activeBuffer.tabs[this._activeBuffer.x]=!0,!0}reverseIndex(){if(this._restrictCursor(),this._activeBuffer.y===this._activeBuffer.scrollTop){const e=this._activeBuffer.scrollBottom-this._activeBuffer.scrollTop;this._activeBuffer.lines.shiftElements(this._activeBuffer.ybase+this._activeBuffer.y,e,1),this._activeBuffer.lines.set(this._activeBuffer.ybase+this._activeBuffer.y,this._activeBuffer.getBlankLine(this._eraseAttrData())),this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop,this._activeBuffer.scrollBottom)}else this._activeBuffer.y--,this._restrictCursor();return!0}fullReset(){return this._parser.reset(),this._onRequestReset.fire(),!0}reset(){this._curAttrData=c.DEFAULT_ATTR_DATA.clone(),this._eraseAttrDataInternal=c.DEFAULT_ATTR_DATA.clone()}_eraseAttrData(){return this._eraseAttrDataInternal.bg&=-67108864,this._eraseAttrDataInternal.bg|=67108863&this._curAttrData.bg,this._eraseAttrDataInternal}setgLevel(e){return this._charsetService.setgLevel(e),!0}screenAlignmentPattern(){const e=new _.CellData;e.content=1<<22|"E".charCodeAt(0),e.fg=this._curAttrData.fg,e.bg=this._curAttrData.bg,this._setCursor(0,0);for(let t=0;t(this._coreService.triggerDataEvent(`${e}\\`),!0))('"q'===e?`P1$r${this._curAttrData.isProtected()?1:0}"q`:'"p'===e?'P1$r61;1"p':"r"===e?`P1$r${i.scrollTop+1};${i.scrollBottom+1}r`:"m"===e?"P1$r0m":" q"===e?`P1$r${{block:2,underline:4,bar:6}[s.cursorStyle]-(s.cursorBlink?1:0)} q`:"P0$r")}markRangeDirty(e,t){this._dirtyRowTracker.markRangeDirty(e,t)}kittyKeyboardSet(e){if(!this._optionsService.rawOptions.vtExtensions?.kittyKeyboard)return!0;const t=e.params[0]||0,i=e.length>1&&e.params[1]||1,s=this._coreService.kittyKeyboard;switch(i){case 1:s.flags=t;break;case 2:s.flags|=t;break;case 3:s.flags&=~t}return!0}kittyKeyboardQuery(e){if(!this._optionsService.rawOptions.vtExtensions?.kittyKeyboard)return!0;const t=this._coreService.kittyKeyboard.flags;return this._coreService.triggerDataEvent(`[?${t}u`),!0}kittyKeyboardPush(e){if(!this._optionsService.rawOptions.vtExtensions?.kittyKeyboard)return!0;const t=e.params[0]||0,i=this._coreService.kittyKeyboard,s=this._bufferService.buffer===this._bufferService.buffers.alt?i.altStack:i.mainStack;return s.length>=16&&s.shift(),s.push(i.flags),i.flags=t,!0}kittyKeyboardPop(e){if(!this._optionsService.rawOptions.vtExtensions?.kittyKeyboard)return!0;const t=Math.max(1,e.params[0]||1),i=this._coreService.kittyKeyboard,s=this._bufferService.buffer===this._bufferService.buffers.alt?i.altStack:i.mainStack;for(let e=0;e0;e++)i.flags=s.pop();return 0===s.length&&t>0&&(i.flags=0),!0}}t.InputHandler=k;let A=class{constructor(e){this._bufferService=e,this.clearRange()}clearRange(){this.start=this._bufferService.buffer.y,this.end=this._bufferService.buffer.y}markDirty(e){ethis.end&&(this.end=e)}markRangeDirty(e,t){e>t&&(B=e,e=t,t=B),ethis.end&&(this.end=t)}markAllDirty(){this.markRangeDirty(0,this._bufferService.rows-1)}};function E(e){return 0<=e&&e<256}A=s([r(0,u.IBufferService)],A)},812(e,t){function i(e){return{dispose:e}}function s(e){if(!e)return e;if(Array.isArray(e)){for(const t of e)t.dispose();return[]}return e.dispose(),e}Object.defineProperty(t,"__esModule",{value:!0}),t.MutableDisposable=t.Disposable=t.DisposableStore=void 0,t.toDisposable=i,t.dispose=s,t.combinedDisposable=function(...e){return i(()=>s(e))};class r{constructor(){this._disposables=new Set,this._isDisposed=!1}get isDisposed(){return this._isDisposed}add(e){return this._isDisposed?e.dispose():this._disposables.add(e),e}dispose(){if(!this._isDisposed){this._isDisposed=!0;for(const e of this._disposables)e.dispose();this._disposables.clear()}}clear(){for(const e of this._disposables)e.dispose();this._disposables.clear()}}t.DisposableStore=r;class n{constructor(){this._store=new r}dispose(){this._store.dispose()}_register(e){return this._store.add(e)}}t.Disposable=n,n.None=Object.freeze({dispose(){}}),t.MutableDisposable=class{constructor(){this._isDisposed=!1}get value(){return this._isDisposed?void 0:this._value}set value(e){this._isDisposed||e===this._value||(this._value?.dispose(),this._value=e)}clear(){this.value=void 0}dispose(){this._isDisposed=!0,this._value?.dispose(),this._value=void 0}}},701(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.isChromeOS=t.isLinux=t.isWindows=t.isMac=t.isSafari=t.isLegacyEdge=t.isChrome=t.isFirefox=t.isNode=void 0,t.getZoomFactor=function(e){return 1},t.getSafariVersion=function(){if(!t.isSafari)return 0;const e=i.match(/Version\/(\d+)/);return null===e||e.length<2?0:parseInt(e[1],10)},t.isNode=!("undefined"==typeof process||!("title"in process)||"undefined"!=typeof navigator&&!navigator.userAgent.startsWith("Node.js/"));const i=t.isNode?"node":navigator.userAgent,s=t.isNode?"node":navigator.platform;t.isFirefox=i.includes("Firefox"),t.isChrome=i.includes("Chrome"),t.isLegacyEdge=i.includes("Edge"),t.isSafari=/^((?!chrome|android).)*safari/i.test(i),t.isMac=["Macintosh","MacIntel","MacPPC","Mac68K"].includes(s),t.isWindows=["Windows","Win16","Win32","WinCE"].includes(s),t.isLinux=s.indexOf("Linux")>=0,t.isChromeOS=/\bCrOS\b/.test(i)},220(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.LimitedStringBuilder=t.StringBuilder=void 0;class i{constructor(){this._chunks=[],this._length=0}get length(){return this._length}reset(){this._chunks.length=0,this._length=0}append(e){this._chunks.push(e),this._length+=e.length}toString(){return this._chunks.join("")}}t.StringBuilder=i,t.LimitedStringBuilder=class{constructor(e){this._limit=e,this._builder=new i}get length(){return this._builder.length}get limit(){return this._limit}reset(){this._builder.reset()}append(e){return this._builder.append(e),this._builder.length>this._limit&&(this._builder.reset(),!0)}toString(){return this._builder.toString()}}},168(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.DebouncedIdleTask=t.IdleTaskQueue=t.PriorityTaskQueue=void 0;class i{constructor(e){this._tasks=[],this._i=0,this._logService=e}enqueue(e){this._tasks.push(e),this._start()}flush(){for(;this._ii)return r-t<-20&&this._logService.warn(`task queue exceeded allotted deadline by ${Math.abs(Math.round(r-t))}ms`),void this._start();r=i}this.clear()}}class s extends i{_requestCallback(e){return setTimeout(()=>e(this._createDeadline(16)))}_cancelCallback(e){clearTimeout(e)}_createDeadline(e){const t=performance.now()+e;return{timeRemaining:()=>Math.max(0,t-performance.now())}}}t.PriorityTaskQueue=s,t.IdleTaskQueue="requestIdleCallback"in globalThis?class extends i{_requestCallback(e){return requestIdleCallback(e)}_cancelCallback(e){cancelIdleCallback(e)}}:s,t.DebouncedIdleTask=class{constructor(e){this._queue=new t.IdleTaskQueue(e)}set(e){this._queue.clear(),this._queue.enqueue(e)}flush(){this._queue.flush()}dispose(){this._queue.clear()}}},804(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.XTERM_VERSION=void 0,t.XTERM_VERSION="6.0.0"},882(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.updateWindowsModeWrappedState=function(e){const t=e.buffer.lines.get(e.buffer.ybase+e.buffer.y-1),i=t?.get(e.cols-1),r=e.buffer.lines.get(e.buffer.ybase+e.buffer.y);r&&i&&(r.isWrapped=i[s.CHAR_DATA_CODE_INDEX]!==s.NULL_CELL_CODE&&i[s.CHAR_DATA_CODE_INDEX]!==s.WHITESPACE_CELL_CODE)};const s=i(938)},451(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.ExtendedAttrs=t.AttributeData=void 0;class i{constructor(){this.fg=0,this.bg=0,this.extended=new s}static toColorRGB(e){return[e>>>16&255,e>>>8&255,255&e]}static fromColorRGB(e){return(255&e[0])<<16|(255&e[1])<<8|255&e[2]}clone(){const e=new i;return e.fg=this.fg,e.bg=this.bg,e.extended=this.extended.clone(),e}isInverse(){return 67108864&this.fg}isBold(){return 134217728&this.fg}isUnderline(){return this.hasExtendedAttrs()&&0!==this.extended.underlineStyle?1:268435456&this.fg}isBlink(){return 536870912&this.fg}isInvisible(){return 1073741824&this.fg}isItalic(){return 67108864&this.bg}isDim(){return 134217728&this.bg}isStrikethrough(){return 2147483648&this.fg}isProtected(){return 536870912&this.bg}isOverline(){return 1073741824&this.bg}getFgColorMode(){return 50331648&this.fg}getBgColorMode(){return 50331648&this.bg}isFgRGB(){return!(50331648&~this.fg)}isBgRGB(){return!(50331648&~this.bg)}isFgPalette(){return 16777216==(50331648&this.fg)||33554432==(50331648&this.fg)}isBgPalette(){return 16777216==(50331648&this.bg)||33554432==(50331648&this.bg)}isFgDefault(){return!(50331648&this.fg)}isBgDefault(){return!(50331648&this.bg)}isAttributeDefault(){return 0===this.fg&&0===this.bg}getFgColor(){switch(50331648&this.fg){case 16777216:case 33554432:return 255&this.fg;case 50331648:return 16777215&this.fg;default:return-1}}getBgColor(){switch(50331648&this.bg){case 16777216:case 33554432:return 255&this.bg;case 50331648:return 16777215&this.bg;default:return-1}}hasExtendedAttrs(){return 268435456&this.bg}updateExtended(){this.extended.isEmpty()?this.bg&=-268435457:this.bg|=268435456}getUnderlineColor(){if(268435456&this.bg&&~this.extended.underlineColor)switch(50331648&this.extended.underlineColor){case 16777216:case 33554432:return 255&this.extended.underlineColor;case 50331648:return 16777215&this.extended.underlineColor;default:return this.getFgColor()}return this.getFgColor()}getUnderlineColorMode(){return 268435456&this.bg&&~this.extended.underlineColor?50331648&this.extended.underlineColor:this.getFgColorMode()}isUnderlineColorRGB(){return 268435456&this.bg&&~this.extended.underlineColor?!(50331648&~this.extended.underlineColor):this.isFgRGB()}isUnderlineColorPalette(){return 268435456&this.bg&&~this.extended.underlineColor?16777216==(50331648&this.extended.underlineColor)||33554432==(50331648&this.extended.underlineColor):this.isFgPalette()}isUnderlineColorDefault(){return 268435456&this.bg&&~this.extended.underlineColor?!(50331648&this.extended.underlineColor):this.isFgDefault()}getUnderlineStyle(){return 268435456&this.fg?268435456&this.bg?this.extended.underlineStyle:1:0}getUnderlineVariantOffset(){return this.extended.underlineVariantOffset}}t.AttributeData=i;class s{get ext(){return this._urlId?-469762049&this._ext|this.underlineStyle<<26:this._ext}set ext(e){this._ext=e}get underlineStyle(){return this._urlId?5:(469762048&this._ext)>>26}set underlineStyle(e){this._ext&=-469762049,this._ext|=e<<26&469762048}get underlineColor(){return 67108863&this._ext}set underlineColor(e){this._ext&=-67108864,this._ext|=67108863&e}get urlId(){return this._urlId}set urlId(e){this._urlId=e}get underlineVariantOffset(){const e=(3758096384&this._ext)>>29;return e<0?4294967288^e:e}set underlineVariantOffset(e){this._ext&=536870911,this._ext|=e<<29&3758096384}constructor(e=0,t=0){this._ext=0,this._urlId=0,this._ext=e,this._urlId=t}clone(){return new s(this._ext,this._urlId)}isEmpty(){return 0===this.underlineStyle&&0===this._urlId}}t.ExtendedAttrs=s},73(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.Buffer=t.MAX_BUFFER_SIZE=void 0;const s=i(639),r=i(812),n=i(168),a=i(451),o=i(107),h=i(732),c=i(55),l=i(938),_=i(158),d=i(760);t.MAX_BUFFER_SIZE=4294967295;class u extends r.Disposable{constructor(e,t,i,a){super(),this._hasScrollback=e,this._optionsService=t,this._bufferService=i,this._logService=a,this.ydisp=0,this.ybase=0,this.y=0,this.x=0,this.tabs={},this.savedY=0,this.savedX=0,this.savedCurAttrData=o.DEFAULT_ATTR_DATA.clone(),this.savedCharset=d.DEFAULT_CHARSET,this.savedCharsets=[],this.savedGlevel=0,this.savedOriginMode=!1,this.savedWraparoundMode=!0,this.markers=[],this._nullCell=c.CellData.fromCharData([0,l.NULL_CELL_CHAR,l.NULL_CELL_WIDTH,l.NULL_CELL_CODE]),this._whitespaceCell=c.CellData.fromCharData([0,l.WHITESPACE_CELL_CHAR,l.WHITESPACE_CELL_WIDTH,l.WHITESPACE_CELL_CODE]),this._isClearing=!1,this._memoryCleanupPosition=0,this._cols=this._bufferService.cols,this._rows=this._bufferService.rows,this.lines=new s.CircularList(this._getCorrectBufferLength(this._rows)),this.scrollTop=0,this.scrollBottom=this._rows-1,this.setupTabStops(),this._memoryCleanupQueue=new n.IdleTaskQueue(this._logService),this._register((0,r.toDisposable)(()=>this._memoryCleanupQueue.clear())),this._register((0,r.toDisposable)(()=>this.clearAllMarkers()))}getNullCell(e){return e?(this._nullCell.fg=e.fg,this._nullCell.bg=e.bg,this._nullCell.extended=e.extended):(this._nullCell.fg=0,this._nullCell.bg=0,this._nullCell.extended=new a.ExtendedAttrs),this._nullCell}getWhitespaceCell(e){return e?(this._whitespaceCell.fg=e.fg,this._whitespaceCell.bg=e.bg,this._whitespaceCell.extended=e.extended):(this._whitespaceCell.fg=0,this._whitespaceCell.bg=0,this._whitespaceCell.extended=new a.ExtendedAttrs),this._whitespaceCell}getBlankLine(e,t){return new o.BufferLine(this._bufferService.cols,this.getNullCell(e),t)}get hasScrollback(){return this._hasScrollback&&this.lines.maxLength>this._rows}get isCursorInViewport(){const e=this.ybase+this.y-this.ydisp;return e>=0&&et.MAX_BUFFER_SIZE?t.MAX_BUFFER_SIZE:i}fillViewportRows(e){if(0===this.lines.length){e??=o.DEFAULT_ATTR_DATA;let t=this._rows;for(;t--;)this.lines.push(this.getBlankLine(e))}}clear(){this.ydisp=0,this.ybase=0,this.y=0,this.x=0,this.lines=new s.CircularList(this._getCorrectBufferLength(this._rows)),this.scrollTop=0,this.scrollBottom=this._rows-1,this.setupTabStops()}resize(e,t){const i=this.getNullCell(o.DEFAULT_ATTR_DATA);let s=0;const r=this._getCorrectBufferLength(t);if(r>this.lines.maxLength&&(this.lines.maxLength=r),this.lines.length>0){if(this._cols0&&this.lines.length<=this.ybase+this.y+n+1?(this.ybase--,n++,this.ydisp>0&&this.ydisp--):this.lines.push(new o.BufferLine(e,i,!1)));else for(let e=this._rows;e>t;e--)this.lines.length>t+this.ybase&&(this.lines.length>this.ybase+this.y+1?this.lines.pop():(this.ybase++,this.ydisp++));if(r0&&(this.lines.trimStart(e),this.ybase=Math.max(this.ybase-e,0),this.ydisp=Math.max(this.ydisp-e,0),this.savedY=Math.max(this.savedY-e,0)),this.lines.maxLength=r}this.x=Math.min(this.x,e-1),this.y=Math.min(this.y,t-1),n&&(this.y+=n),this.savedX=Math.min(this.savedX,e-1),this.scrollTop=0}if(this.scrollBottom=t-1,this._isReflowEnabled&&(this._reflow(e,t),this._cols>e))for(let t=0;t0){const e=Math.max(0,this.lines.length-this.ybase-1);this.y=Math.min(this.y,e)}this._memoryCleanupQueue.clear(),s>.1*this.lines.length&&(this._memoryCleanupPosition=0,this._memoryCleanupQueue.enqueue(()=>this._batchedMemoryCleanup()))}_batchedMemoryCleanup(){let e=!0;this._memoryCleanupPosition>=this.lines.length&&(this._memoryCleanupPosition=0,e=!1);let t=0;for(;this._memoryCleanupPosition100)return!0;return e}get _isReflowEnabled(){const e=this._optionsService.rawOptions.windowsPty;return e&&e.buildNumber?this._hasScrollback&&"conpty"===e.backend&&e.buildNumber>=21376:this._hasScrollback}_reflow(e,t){this._cols!==e&&(e>this._cols?this._reflowLarger(e,t):this._reflowSmaller(e,t))}_reflowLarger(e,t){const i=this._optionsService.rawOptions.reflowCursorLine,s=(0,h.reflowLargerGetLinesToRemove)(this.lines,this._cols,e,this.ybase+this.y,this.getNullCell(o.DEFAULT_ATTR_DATA),i);if(s.length>0){const i=(0,h.reflowLargerCreateNewLayout)(this.lines,s);(0,h.reflowLargerApplyNewLayout)(this.lines,i.layout),this._reflowLargerAdjustViewport(e,t,i.countRemoved)}}_reflowLargerAdjustViewport(e,t,i){const s=this.getNullCell(o.DEFAULT_ATTR_DATA);let r=i;for(;r-- >0;)0===this.ybase?(this.y>0&&this.y--,this.lines.length=0;a--){let c=this.lines.get(a);if(!c||!c.isWrapped&&c.getTrimmedLength()<=e)continue;const l=[c];for(;c.isWrapped&&a>0;)c=this.lines.get(--a),l.unshift(c);if(!i){const e=this.ybase+this.y;if(e>=a&&e0&&(r.push({start:a+l.length+n,newLines:p}),n+=p.length),l.push(...p);let g=d.length-1,v=d[g];0===v&&(g--,v=d[g]);let b=l.length-u-1,S=_;for(;b>=0;){const e=Math.min(S,v);if(void 0===l[g])break;if(l[g].copyCellsFrom(l[b],S-e,v-e,e,!0),v-=e,0===v&&(g--,v=d[g]),S-=e,0===S){b--;const e=Math.max(b,0);S=(0,h.getWrappedLineTrimmedLength)(l,e,this._cols)}}for(let t=0;t0;)0===this.ybase?this.y0){const e=[],t=[];for(let e=0;e=0;c--)if(o&&o.start>s+h){for(let e=o.newLines.length-1;e>=0;e--)this.lines.set(c--,o.newLines[e]);c++,e.push({index:s+1,amount:o.newLines.length}),h+=o.newLines.length,o=r[++a]}else this.lines.set(c,t[s--]);let c=0;for(let t=e.length-1;t>=0;t--)e[t].index+=c,this.lines.onInsertEmitter.fire(e[t]),c+=e[t].amount;const l=Math.max(0,i+n-this.lines.maxLength);l>0&&this.lines.onTrimEmitter.fire(l)}}translateBufferLineToString(e,t,i=0,s){const r=this.lines.get(e);return r?r.translateToString(t,i,s):""}getWrappedRangeForLine(e){let t=e,i=e;for(;t>0&&this.lines.get(t).isWrapped;)t--;for(;i+10;);return e>=this._cols?this._cols-1:e<0?0:e}nextStop(e){for(e??=this.x;!this.tabs[++e]&&e=this._cols?this._cols-1:e<0?0:e}clearMarkers(e){this._isClearing=!0;for(let t=0;t{t.line-=e,t.line<0&&t.dispose()})),t.register(this.lines.onInsert(e=>{t.line>=e.index&&(t.line+=e.amount)})),t.register(this.lines.onDelete(e=>{t.line>=e.index&&t.linee.index&&(t.line-=e.amount)})),t.register(t.onDispose(()=>this._removeMarker(t))),t}_removeMarker(e){this._isClearing||this.markers.splice(this.markers.indexOf(e),1)}}t.Buffer=u},107(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.BufferLine=t.DEFAULT_ATTR_DATA=void 0;const s=i(451),r=i(55),n=i(938),a=i(726);t.DEFAULT_ATTR_DATA=Object.freeze(new s.AttributeData);let o=0;const h=new r.CellData,c=t.DEFAULT_ATTR_DATA.extended.clone();class l{constructor(e,t,i=!1){this.isWrapped=i,this._combined={},this._extendedAttrs={},this._cacheValid=!1,this._cache="",this._cacheTrimmed=!1,this._data=new Uint32Array(3*e);const s=t??r.CellData.fromCharData([0,n.NULL_CELL_CHAR,n.NULL_CELL_WIDTH,n.NULL_CELL_CODE]);for(let t=0;t>22,2097152&t?this._combined[e].charCodeAt(this._combined[e].length-1):i]}set(e,t){this._cacheValid=!1,this._data[3*e+1]=t[n.CHAR_DATA_ATTR_INDEX],t[n.CHAR_DATA_CHAR_INDEX].length>1?(this._combined[e]=t[1],this._data[3*e+0]=2097152|e|t[n.CHAR_DATA_WIDTH_INDEX]<<22):this._data[3*e+0]=t[n.CHAR_DATA_CHAR_INDEX].charCodeAt(0)|t[n.CHAR_DATA_WIDTH_INDEX]<<22}getWidth(e){return this._data[3*e+0]>>22}hasWidth(e){return 12582912&this._data[3*e+0]}getFg(e){return this._data[3*e+1]}getBg(e){return this._data[3*e+2]}hasContent(e){return 4194303&this._data[3*e+0]}getCodePoint(e){const t=this._data[3*e+0];return 2097152&t?this._combined[e].charCodeAt(this._combined[e].length-1):2097151&t}isCombined(e){return 2097152&this._data[3*e+0]}getString(e){const t=this._data[3*e+0];return 2097152&t?this._combined[e]:2097151&t?(0,a.stringFromCodePoint)(2097151&t):""}isProtected(e){return 536870912&this._data[3*e+2]}loadCell(e,t){return o=3*e,t.content=this._data[o+0],t.fg=this._data[o+1],t.bg=this._data[o+2],2097152&t.content?t.combinedData=this._combined[e]:t.combinedData="",268435456&t.bg?t.extended=this._extendedAttrs[e]:(c._ext=0,c._urlId=0,t.extended=c),t}setCell(e,t){this._cacheValid=!1,2097152&t.content&&(this._combined[e]=t.combinedData),268435456&t.bg&&(this._extendedAttrs[e]=t.extended),this._data[3*e+0]=t.content,this._data[3*e+1]=t.fg,this._data[3*e+2]=t.bg}setCellFromCodepoint(e,t,i,s){this._cacheValid=!1,268435456&s.bg&&(this._extendedAttrs[e]=s.extended);const r=3*e;this._data[r+0]=t|i<<22,this._data[r+1]=s.fg,this._data[r+2]=s.bg}addCodepointToCell(e,t,i){this._cacheValid=!1;let s=this._data[3*e+0];2097152&s?this._combined[e]+=(0,a.stringFromCodePoint)(t):2097151&s?(this._combined[e]=(0,a.stringFromCodePoint)(2097151&s)+(0,a.stringFromCodePoint)(t),s&=-2097152,s|=2097152):s=t|1<<22,i&&(s&=-12582913,s|=i<<22),this._data[3*e+0]=s}insertCells(e,t,i){if(this._cacheValid=!1,(e%=this.length)&&2===this.getWidth(e-1)&&this.setCellFromCodepoint(e-1,0,1,i),t=0;--i)this.setCell(e+t+i,this.loadCell(e+i,h));for(let s=0;sthis.length){if(this._data.buffer.byteLength>=4*i)this._data=new Uint32Array(this._data.buffer,0,i);else{const e=new Uint32Array(i);e.set(this._data),this._data=e}for(let i=this.length;i=e&&delete this._combined[s]}const s=Object.keys(this._extendedAttrs);for(let t=0;t=e&&delete this._extendedAttrs[i]}}return this.length=e,4*i*2=0;--e)if(4194303&this._data[3*e+0])return e+(this._data[3*e+0]>>22);return 0}getNoBgTrimmedLength(){for(let e=this.length-1;e>=0;--e)if(4194303&this._data[3*e+0]||50331648&this._data[3*e+2])return e+(this._data[3*e+0]>>22);return 0}copyCellsFrom(e,t,i,s,r){this._cacheValid=!1;const n=e._data;if(r)for(let r=s-1;r>=0;r--){for(let e=0;e<3;e++)this._data[3*(i+r)+e]=n[3*(t+r)+e];this._copyCellMapsFrom(e,t+r,i+r)}else for(let r=0;r>22||1}s&&s.push(t);const h=o.join("");return r&&(this._cache=h,this._cacheValid=!0,this._cacheTrimmed=!!e),h}_copyCellMapsFrom(e,t,i){const s=3*t;2097152&e._data[s+0]&&(this._combined[i]=e._combined[t]),268435456&e._data[s+2]&&(this._extendedAttrs[i]=e._extendedAttrs[t])}_copySparseMapsFrom(e){this._combined={},this._extendedAttrs={};for(let t=0;t=h&&r0&&(e>d||0===_[e].getTrimmedLength());e--)g++;g>0&&(o.push(h+_.length-g),o.push(g)),h+=_.length-1}return o},t.reflowLargerCreateNewLayout=function(e,t){const i=[];let s=0,r=t[s],n=0;for(let a=0;ac&&(a-=c,o++);const l=2===e[o].getWidth(a-1);l&&a--;const _=l?s-1:s;r.push(_),h+=_}return r},t.getWrappedLineTrimmedLength=i},478(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.BufferSet=void 0;const s=i(812),r=i(73),n=i(636);class a extends s.Disposable{constructor(e,t,i){super(),this._optionsService=e,this._bufferService=t,this._logService=i,this._normalBuffer=this._register(new s.MutableDisposable),this._altBuffer=this._register(new s.MutableDisposable),this._onBufferActivate=this._register(new n.Emitter),this.onBufferActivate=this._onBufferActivate.event,this.reset(),this._register(this._optionsService.onSpecificOptionChange("scrollback",()=>this.resize(this._bufferService.cols,this._bufferService.rows))),this._register(this._optionsService.onSpecificOptionChange("tabStopWidth",()=>this.setupTabStops()))}reset(){this._normal=new r.Buffer(!0,this._optionsService,this._bufferService,this._logService),this._normalBuffer.value=this._normal,this._normal.fillViewportRows(),this._alt=new r.Buffer(!1,this._optionsService,this._bufferService,this._logService),this._altBuffer.value=this._alt,this._activeBuffer=this._normal,this._onBufferActivate.fire({activeBuffer:this._normal,inactiveBuffer:this._alt}),this.setupTabStops()}get alt(){return this._alt}get active(){return this._activeBuffer}get normal(){return this._normal}activateNormalBuffer(){this._activeBuffer!==this._normal&&(this._normal.x=this._alt.x,this._normal.y=this._alt.y,this._alt.clearAllMarkers(),this._alt.clear(),this._activeBuffer=this._normal,this._onBufferActivate.fire({activeBuffer:this._normal,inactiveBuffer:this._alt}))}activateAltBuffer(e){this._activeBuffer!==this._alt&&(this._alt.fillViewportRows(e),this._alt.x=this._normal.x,this._alt.y=this._normal.y,this._activeBuffer=this._alt,this._onBufferActivate.fire({activeBuffer:this._alt,inactiveBuffer:this._normal}))}resize(e,t){this._normal.resize(e,t),this._alt.resize(e,t),this.setupTabStops(e)}setupTabStops(e){this._normal.setupTabStops(e),this._alt.setupTabStops(e)}}t.BufferSet=a},55(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.CellData=void 0;const s=i(726),r=i(938),n=i(451);class a extends n.AttributeData{constructor(){super(...arguments),this.content=0,this.fg=0,this.bg=0,this.extended=new n.ExtendedAttrs,this.combinedData=""}static fromCharData(e){const t=new a;return t.setFromCharData(e),t}isCombined(){return 2097152&this.content}getWidth(){return this.content>>22}getChars(){return 2097152&this.content?this.combinedData:2097151&this.content?(0,s.stringFromCodePoint)(2097151&this.content):""}getCode(){return this.isCombined()?this.combinedData.charCodeAt(this.combinedData.length-1):2097151&this.content}setFromCharData(e){this.fg=e[r.CHAR_DATA_ATTR_INDEX],this.bg=0;let t=!1;if(e[r.CHAR_DATA_CHAR_INDEX].length>2)t=!0;else if(2===e[r.CHAR_DATA_CHAR_INDEX].length){const i=e[r.CHAR_DATA_CHAR_INDEX].charCodeAt(0);if(55296<=i&&i<=56319){const s=e[r.CHAR_DATA_CHAR_INDEX].charCodeAt(1);56320<=s&&s<=57343?this.content=1024*(i-55296)+s-56320+65536|e[r.CHAR_DATA_WIDTH_INDEX]<<22:t=!0}else t=!0}else this.content=e[r.CHAR_DATA_CHAR_INDEX].charCodeAt(0)|e[r.CHAR_DATA_WIDTH_INDEX]<<22;t&&(this.combinedData=e[r.CHAR_DATA_CHAR_INDEX],this.content=2097152|e[r.CHAR_DATA_WIDTH_INDEX]<<22)}getAsCharData(){return[this.fg,this.getChars(),this.getWidth(),this.getCode()]}attributesEquals(e){if(this.getFgColorMode()!==e.getFgColorMode()||this.getFgColor()!==e.getFgColor())return!1;if(this.getBgColorMode()!==e.getBgColorMode()||this.getBgColor()!==e.getBgColor())return!1;if(this.isInverse()!==e.isInverse())return!1;if(this.isBold()!==e.isBold())return!1;if(this.isUnderline()!==e.isUnderline())return!1;if(this.isUnderline()){if(this.getUnderlineStyle()!==e.getUnderlineStyle())return!1;const t=this.isUnderlineColorDefault(),i=e.isUnderlineColorDefault();if(!t||!i){if(t!==i)return!1;if(this.getUnderlineColor()!==e.getUnderlineColor())return!1;if(this.getUnderlineColorMode()!==e.getUnderlineColorMode())return!1}}return this.isOverline()===e.isOverline()&&this.isBlink()===e.isBlink()&&this.isInvisible()===e.isInvisible()&&this.isItalic()===e.isItalic()&&this.isDim()===e.isDim()&&this.isStrikethrough()===e.isStrikethrough()}}t.CellData=a},938(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.WHITESPACE_CELL_CODE=t.WHITESPACE_CELL_WIDTH=t.WHITESPACE_CELL_CHAR=t.NULL_CELL_CODE=t.NULL_CELL_WIDTH=t.NULL_CELL_CHAR=t.CHAR_DATA_CODE_INDEX=t.CHAR_DATA_WIDTH_INDEX=t.CHAR_DATA_CHAR_INDEX=t.CHAR_DATA_ATTR_INDEX=t.DEFAULT_EXT=t.DEFAULT_ATTR=t.DEFAULT_COLOR=void 0,t.DEFAULT_COLOR=0,t.DEFAULT_ATTR=t.DEFAULT_COLOR<<9|256,t.DEFAULT_EXT=0,t.CHAR_DATA_ATTR_INDEX=0,t.CHAR_DATA_CHAR_INDEX=1,t.CHAR_DATA_WIDTH_INDEX=2,t.CHAR_DATA_CODE_INDEX=3,t.NULL_CELL_CHAR="",t.NULL_CELL_WIDTH=1,t.NULL_CELL_CODE=0,t.WHITESPACE_CELL_CHAR=" ",t.WHITESPACE_CELL_WIDTH=1,t.WHITESPACE_CELL_CODE=32},158(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.Marker=void 0;const s=i(812),r=i(636);class n{get id(){return this._id}constructor(e){this.line=e,this.isDisposed=!1,this._disposables=[],this._id=n._nextId++,this._onDispose=this.register(new r.Emitter),this.onDispose=this._onDispose.event}dispose(){this.isDisposed||(this.isDisposed=!0,this.line=-1,this._onDispose.fire(),(0,s.dispose)(this._disposables),this._disposables.length=0)}register(e){return this._disposables.push(e),e}}t.Marker=n,n._nextId=1},760(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.DEFAULT_CHARSET=t.CHARSETS=void 0,t.CHARSETS={},t.DEFAULT_CHARSET=t.CHARSETS.B,t.CHARSETS[0]={"`":"◆",a:"▒",b:"␉",c:"␌",d:"␍",e:"␊",f:"°",g:"±",h:"␤",i:"␋",j:"┘",k:"┐",l:"┌",m:"└",n:"┼",o:"⎺",p:"⎻",q:"─",r:"⎼",s:"⎽",t:"├",u:"┤",v:"┴",w:"┬",x:"│",y:"≤",z:"≥","{":"π","|":"≠","}":"£","~":"·"},t.CHARSETS.A={"#":"£"},t.CHARSETS.B=void 0,t.CHARSETS[4]={"#":"£","@":"¾","[":"ij","\\":"½","]":"|","{":"¨","|":"f","}":"¼","~":"´"},t.CHARSETS.C=t.CHARSETS[5]={"[":"Ä","\\":"Ö","]":"Å","^":"Ü","`":"é","{":"ä","|":"ö","}":"å","~":"ü"},t.CHARSETS.R={"#":"£","@":"à","[":"°","\\":"ç","]":"§","{":"é","|":"ù","}":"è","~":"¨"},t.CHARSETS.Q={"@":"à","[":"â","\\":"ç","]":"ê","^":"î","`":"ô","{":"é","|":"ù","}":"è","~":"û"},t.CHARSETS.K={"@":"§","[":"Ä","\\":"Ö","]":"Ü","{":"ä","|":"ö","}":"ü","~":"ß"},t.CHARSETS.Y={"#":"£","@":"§","[":"°","\\":"ç","]":"é","`":"ù","{":"à","|":"ò","}":"è","~":"ì"},t.CHARSETS.E=t.CHARSETS[6]={"@":"Ä","[":"Æ","\\":"Ø","]":"Å","^":"Ü","`":"ä","{":"æ","|":"ø","}":"å","~":"ü"},t.CHARSETS.Z={"#":"£","@":"§","[":"¡","\\":"Ñ","]":"¿","{":"°","|":"ñ","}":"ç"},t.CHARSETS.H=t.CHARSETS[7]={"@":"É","[":"Ä","\\":"Ö","]":"Å","^":"Ü","`":"é","{":"ä","|":"ö","}":"å","~":"ü"},t.CHARSETS["="]={"#":"ù","@":"à","[":"é","\\":"ç","]":"ê","^":"î",_:"è","`":"ô","{":"ä","|":"ö","}":"ü","~":"û"}},726(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.Utf8ToUtf32=t.StringToUtf32=void 0,t.stringFromCodePoint=function(e){return e>65535?(e-=65536,String.fromCharCode(55296+(e>>10))+String.fromCharCode(e%1024+56320)):String.fromCharCode(e)},t.utf32ToString=function(e,t=0,i=e.length){let s="";for(let r=t;r65535?(t-=65536,s+=String.fromCharCode(55296+(t>>10))+String.fromCharCode(t%1024+56320)):s+=String.fromCharCode(t)}return s},t.StringToUtf32=class{constructor(){this._interim=0}clear(){this._interim=0}decode(e,t){const i=e.length;if(!i)return 0;let s=0,r=0;if(this._interim){const i=e.charCodeAt(r++);56320<=i&&i<=57343?t[s++]=1024*(this._interim-55296)+i-56320+65536:(t[s++]=this._interim,t[s++]=i),this._interim=0}for(let n=r;n=i)return this._interim=r,s;const a=e.charCodeAt(n);56320<=a&&a<=57343?t[s++]=1024*(r-55296)+a-56320+65536:(t[s++]=r,t[s++]=a);continue}65279!==r&&(t[s++]=r)}return s}},t.Utf8ToUtf32=class{constructor(){this.interim=new Uint8Array(3)}clear(){this.interim.fill(0)}decode(e,t){const i=e.length;if(!i)return 0;let s,r,n,a,o,h=0,c=0;if(this.interim[0]){let s=!1,r=this.interim[0];r&=192==(224&r)?31:224==(240&r)?15:7;let n,a=0;for(;(n=this.interim[++a])&&a<4;)r<<=6,r|=63&n;const o=192==(224&this.interim[0])?2:224==(240&this.interim[0])?3:4,l=o-a;for(;c=i)return 0;if(n=e[c++],128!=(192&n)){c--,s=!0;break}this.interim[a++]=n,r<<=6,r|=63&n}s||(2===o?r<128?c--:t[h++]=r:3===o?r<2048||r>=55296&&r<=57343||65279===r||(t[h++]=r):r<65536||r>1114111||(t[h++]=r)),this.interim.fill(0)}const l=i-4;let _=c;for(;_=i)return this.interim[0]=s,h;if(r=e[_++],128!=(192&r)){_--;continue}if(o=(31&s)<<6|63&r,o<128){_--;continue}t[h++]=o}else if(224==(240&s)){if(_>=i)return this.interim[0]=s,h;if(r=e[_++],128!=(192&r)){_--;continue}if(_>=i)return this.interim[0]=s,this.interim[1]=r,h;if(n=e[_++],128!=(192&n)){_--;continue}if(o=(15&s)<<12|(63&r)<<6|63&n,o<2048||o>=55296&&o<=57343||65279===o)continue;t[h++]=o}else if(240==(248&s)){if(_>=i)return this.interim[0]=s,h;if(r=e[_++],128!=(192&r)){_--;continue}if(_>=i)return this.interim[0]=s,this.interim[1]=r,h;if(n=e[_++],128!=(192&n)){_--;continue}if(_>=i)return this.interim[0]=s,this.interim[1]=r,this.interim[2]=n,h;if(a=e[_++],128!=(192&a)){_--;continue}if(o=(7&s)<<18|(63&r)<<12|(63&n)<<6|63&a,o<65536||o>1114111)continue;t[h++]=o}}return h}}},428(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.UnicodeV6=void 0;const s=i(415),r=[[768,879],[1155,1158],[1160,1161],[1425,1469],[1471,1471],[1473,1474],[1476,1477],[1479,1479],[1536,1539],[1552,1557],[1611,1630],[1648,1648],[1750,1764],[1767,1768],[1770,1773],[1807,1807],[1809,1809],[1840,1866],[1958,1968],[2027,2035],[2305,2306],[2364,2364],[2369,2376],[2381,2381],[2385,2388],[2402,2403],[2433,2433],[2492,2492],[2497,2500],[2509,2509],[2530,2531],[2561,2562],[2620,2620],[2625,2626],[2631,2632],[2635,2637],[2672,2673],[2689,2690],[2748,2748],[2753,2757],[2759,2760],[2765,2765],[2786,2787],[2817,2817],[2876,2876],[2879,2879],[2881,2883],[2893,2893],[2902,2902],[2946,2946],[3008,3008],[3021,3021],[3134,3136],[3142,3144],[3146,3149],[3157,3158],[3260,3260],[3263,3263],[3270,3270],[3276,3277],[3298,3299],[3393,3395],[3405,3405],[3530,3530],[3538,3540],[3542,3542],[3633,3633],[3636,3642],[3655,3662],[3761,3761],[3764,3769],[3771,3772],[3784,3789],[3864,3865],[3893,3893],[3895,3895],[3897,3897],[3953,3966],[3968,3972],[3974,3975],[3984,3991],[3993,4028],[4038,4038],[4141,4144],[4146,4146],[4150,4151],[4153,4153],[4184,4185],[4448,4607],[4959,4959],[5906,5908],[5938,5940],[5970,5971],[6002,6003],[6068,6069],[6071,6077],[6086,6086],[6089,6099],[6109,6109],[6155,6157],[6313,6313],[6432,6434],[6439,6440],[6450,6450],[6457,6459],[6679,6680],[6912,6915],[6964,6964],[6966,6970],[6972,6972],[6978,6978],[7019,7027],[7616,7626],[7678,7679],[8203,8207],[8234,8238],[8288,8291],[8298,8303],[8400,8431],[12330,12335],[12441,12442],[43014,43014],[43019,43019],[43045,43046],[64286,64286],[65024,65039],[65056,65059],[65279,65279],[65529,65531]],n=[[68097,68099],[68101,68102],[68108,68111],[68152,68154],[68159,68159],[119143,119145],[119155,119170],[119173,119179],[119210,119213],[119362,119364],[917505,917505],[917536,917631],[917760,917999]];let a;t.UnicodeV6=class{constructor(){if(this.version="6",!a){a=new Uint8Array(65536),a.fill(1),a[0]=0,a.fill(0,1,32),a.fill(0,127,160),a.fill(2,4352,4448),a[9001]=2,a[9002]=2,a.fill(2,11904,42192),a[12351]=1,a.fill(2,44032,55204),a.fill(2,63744,64256),a.fill(2,65040,65050),a.fill(2,65072,65136),a.fill(2,65280,65377),a.fill(2,65504,65511);for(let e=0;et[r][1])return!1;for(;r>=s;)if(i=s+r>>1,e>t[i][1])s=i+1;else{if(!(e=131072&&e<=196605||e>=196608&&e<=262141?2:1}charProperties(e,t){let i=this.wcwidth(e),r=0===i&&0!==t;if(r){const e=s.UnicodeService.extractWidth(t);0===e?r=!1:e>i&&(i=e)}return s.UnicodeService.createPropertyValue(0,i,r)}}},562(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.WriteBuffer=void 0;const s=i(132),r=i(812),n=i(636);class a extends r.Disposable{constructor(e){super(),this._action=e,this._writeBuffer=[],this._callbacks=[],this._pendingData=0,this._bufferOffset=0,this._isSyncWriting=!1,this._syncCalls=0,this._didUserInput=!1,this._innerWriteTimer=this._register(new s.TimeoutTimer),this._onWriteParsed=this._register(new n.Emitter),this.onWriteParsed=this._onWriteParsed.event,this._register((0,r.toDisposable)(()=>{this._writeBuffer.length=0,this._callbacks.length=0,this._pendingData=0,this._bufferOffset=0}))}handleUserInput(){this._didUserInput=!0}flushSync(){if(this._store.isDisposed)return;if(this._isSyncWriting)return;let e;this._isSyncWriting=!0;let t=!1;for(;e=this._writeBuffer.shift();){t=!0,this._action(e);const i=this._callbacks.shift();i&&i()}this._pendingData=0,this._bufferOffset=2147483647,this._writeBuffer.length=0,this._callbacks.length=0,this._isSyncWriting=!1,t&&this._onWriteParsed.fire()}writeSync(e,t){if(this._store.isDisposed)return;if(void 0!==t&&this._syncCalls>t)return void(this._syncCalls=0);if(this._pendingData+=e.length,this._writeBuffer.push(e),this._callbacks.push(void 0),this._syncCalls++,this._isSyncWriting)return;let i;for(this._isSyncWriting=!0;i=this._writeBuffer.shift();){this._action(i);const e=this._callbacks.shift();e&&e()}this._pendingData=0,this._bufferOffset=2147483647,this._isSyncWriting=!1,this._syncCalls=0}write(e,t){if(!this._store.isDisposed){if(this._pendingData>5e7)throw new Error("write data discarded, use flow control to avoid losing data");if(!this._writeBuffer.length){if(this._bufferOffset=0,this._didUserInput)return this._didUserInput=!1,this._pendingData+=e.length,this._writeBuffer.push(e),this._callbacks.push(t),void this._innerWrite();this._scheduleInnerWrite()}this._pendingData+=e.length,this._writeBuffer.push(e),this._callbacks.push(t)}}_scheduleInnerWrite(e=0,t=!0){this._store.isDisposed||this._innerWriteTimer.cancelAndSet(()=>this._innerWrite(e,t),0)}_innerWrite(e=0,t=!0){if(this._store.isDisposed)return;const i=e||performance.now();for(;this._writeBuffer.length>this._bufferOffset;){const e=this._writeBuffer[this._bufferOffset],s=this._action(e,t);if(s){const e=e=>{this._store.isDisposed||(performance.now()-i>=12?this._scheduleInnerWrite(0,e):this._innerWrite(i,e))};return void s.catch(e=>(queueMicrotask(()=>{throw e}),Promise.resolve(!1))).then(e)}const r=this._callbacks[this._bufferOffset];if(r&&r(),this._bufferOffset++,this._pendingData-=e.length,performance.now()-i>=12)break}this._writeBuffer.length>this._bufferOffset?(this._bufferOffset>50&&(this._writeBuffer=this._writeBuffer.slice(this._bufferOffset),this._callbacks=this._callbacks.slice(this._bufferOffset),this._bufferOffset=0),this._scheduleInnerWrite()):(this._writeBuffer.length=0,this._callbacks.length=0,this._pendingData=0,this._bufferOffset=0),this._onWriteParsed.fire()}}t.WriteBuffer=a},693(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.parseColor=function(e){if(!e)return;let t=e.toLowerCase();if(t.startsWith("rgb:")){t=t.slice(4);const e=i.exec(t);if(e){const t=e[1]?15:e[4]?255:e[7]?4095:65535;return[Math.round(parseInt(e[1]||e[4]||e[7]||e[10],16)/t*255),Math.round(parseInt(e[2]||e[5]||e[8]||e[11],16)/t*255),Math.round(parseInt(e[3]||e[6]||e[9]||e[12],16)/t*255)]}}else if(t.startsWith("#")&&(t=t.slice(1),s.exec(t)&&[3,6,9,12].includes(t.length))){const e=t.length/3,i=[0,0,0];for(let s=0;s<3;++s){const r=parseInt(t.slice(e*s,e*s+e),16);i[s]=1===e?r<<4:2===e?r:3===e?r>>4:r>>8}return i}},t.toRgbString=function(e,t=16){const[i,s,n]=e;return`rgb:${r(i,t)}/${r(s,t)}/${r(n,t)}`};const i=/^([\da-f])\/([\da-f])\/([\da-f])$|^([\da-f]{2})\/([\da-f]{2})\/([\da-f]{2})$|^([\da-f]{3})\/([\da-f]{3})\/([\da-f]{3})$|^([\da-f]{4})\/([\da-f]{4})\/([\da-f]{4})$/,s=/^[\da-f]+$/;function r(e,t){const i=e.toString(16),s=i.length<2?"0"+i:i;switch(t){case 4:return i[0];case 8:return s;case 12:return(s+s).slice(0,3);default:return s+s}}},607(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.ApcHandler=t.ApcParser=void 0;const s=i(726),r=i(220),n=[];t.ApcParser=class{constructor(){this._handlers=Object.create(null),this._active=n,this._ident=0,this._handlerFb=()=>{},this._stack={paused:!1,loopPosition:0,fallThrough:!1}}registerHandler(e,t){this._handlers[e]??=[];const i=this._handlers[e];return i.push(t),{dispose:()=>{const e=i.indexOf(t);-1!==e&&i.splice(e,1)}}}clearHandler(e){this._handlers[e]&&delete this._handlers[e]}setHandlerFallback(e){this._handlerFb=e}dispose(){this._handlers=Object.create(null),this._handlerFb=()=>{},this._active=n}reset(){if(this._active.length)for(let e=this._stack.paused?this._stack.loopPosition-1:this._active.length-1;e>=0;--e)this._active[e].end(!1);this._stack.paused=!1,this._active=n,this._ident=0}start(e){if(this.reset(),this._ident=e,this._active=this._handlers[e]||n,this._active.length)for(let e=this._active.length-1;e>=0;e--)this._active[e].start();else this._handlerFb(this._ident,"START")}put(e,t,i){if(this._active.length)for(let s=this._active.length-1;s>=0;s--)this._active[s].put(e,t,i);else this._handlerFb(this._ident,"PUT",(0,s.utf32ToString)(e,t,i))}end(e,t=!0){if(this._active.length){let i=!1,s=this._active.length-1,r=!1;if(this._stack.paused&&(s=this._stack.loopPosition-1,i=t,r=this._stack.fallThrough,this._stack.paused=!1),!r&&!1===i){for(;s>=0&&(i=this._active[s].end(e),!0!==i);s--)if(i instanceof Promise)return this._stack.paused=!0,this._stack.loopPosition=s,this._stack.fallThrough=!1,i;s--}for(;s>=0;s--)if(i=this._active[s].end(!1),i instanceof Promise)return this._stack.paused=!0,this._stack.loopPosition=s,this._stack.fallThrough=!0,i}else this._handlerFb(this._ident,"END",e);this._active=n,this._ident=0}};class a{constructor(e){this._handler=e,this._data=new r.LimitedStringBuilder(a._payloadLimit),this._hitLimit=!1}start(){this._data.reset(),this._hitLimit=!1}put(e,t,i){this._hitLimit||this._data.append((0,s.utf32ToString)(e,t,i))&&(this._hitLimit=!0)}end(e){let t=!1;if(this._hitLimit)t=!1;else if(e&&(t=this._handler(this._data.toString()),t instanceof Promise))return t.then(e=>(this._data.reset(),this._hitLimit=!1,e));return this._data.reset(),this._hitLimit=!1,t}}t.ApcHandler=a,a._payloadLimit=1e7},823(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.DcsHandler=t.DcsParser=void 0;const s=i(726),r=i(262),n=i(220),a=[];t.DcsParser=class{constructor(){this._handlers=Object.create(null),this._active=a,this._ident=0,this._handlerFb=()=>{},this._stack={paused:!1,loopPosition:0,fallThrough:!1}}dispose(){this._handlers=Object.create(null),this._handlerFb=()=>{},this._active=a}registerHandler(e,t){this._handlers[e]??=[];const i=this._handlers[e];return i.push(t),{dispose:()=>{const e=i.indexOf(t);-1!==e&&i.splice(e,1)}}}clearHandler(e){this._handlers[e]&&delete this._handlers[e]}setHandlerFallback(e){this._handlerFb=e}reset(){if(this._active.length)for(let e=this._stack.paused?this._stack.loopPosition-1:this._active.length-1;e>=0;--e)this._active[e].unhook(!1);this._stack.paused=!1,this._active=a,this._ident=0}hook(e,t){if(this.reset(),this._ident=e,this._active=this._handlers[e]||a,this._active.length)for(let e=this._active.length-1;e>=0;e--)this._active[e].hook(t);else this._handlerFb(this._ident,"HOOK",t)}put(e,t,i){if(this._active.length)for(let s=this._active.length-1;s>=0;s--)this._active[s].put(e,t,i);else this._handlerFb(this._ident,"PUT",(0,s.utf32ToString)(e,t,i))}unhook(e,t=!0){if(this._active.length){let i=!1,s=this._active.length-1,r=!1;if(this._stack.paused&&(s=this._stack.loopPosition-1,i=t,r=this._stack.fallThrough,this._stack.paused=!1),!r&&!1===i){for(;s>=0&&(i=this._active[s].unhook(e),!0!==i);s--)if(i instanceof Promise)return this._stack.paused=!0,this._stack.loopPosition=s,this._stack.fallThrough=!1,i;s--}for(;s>=0;s--)if(i=this._active[s].unhook(!1),i instanceof Promise)return this._stack.paused=!0,this._stack.loopPosition=s,this._stack.fallThrough=!0,i}else this._handlerFb(this._ident,"UNHOOK",e);this._active=a,this._ident=0}};const o=new r.Params;o.addParam(0);class h{constructor(e){this._handler=e,this._data=new n.LimitedStringBuilder(h._payloadLimit),this._params=o,this._hitLimit=!1}hook(e){this._params=e.length>1||e.params[0]?e.clone():o,this._data.reset(),this._hitLimit=!1}put(e,t,i){this._hitLimit||this._data.append((0,s.utf32ToString)(e,t,i))&&(this._hitLimit=!0)}unhook(e){let t=!1;if(this._hitLimit)t=!1;else if(e&&(t=this._handler(this._data.toString(),this._params),t instanceof Promise))return t.then(e=>(this._params=o,this._data.reset(),this._hitLimit=!1,e));return this._params=o,this._data.reset(),this._hitLimit=!1,t}}t.DcsHandler=h,h._payloadLimit=1e7},717(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.EscapeSequenceParser=t.VT500_TRANSITION_TABLE=t.TransitionTable=void 0;const s=i(812),r=i(262),n=i(346),a=i(823),o=i(607);class h{constructor(e){this.table=new Uint16Array(e)}setDefault(e,t){this.table.fill(e<<8|t)}add(e,t,i,s){this.table[t<<8|e]=i<<8|s}addMany(e,t,i,s){for(let r=0;rt),i=(e,i)=>t.slice(e,i),s=i(32,127),r=i(0,24);r.push(25),r.push.apply(r,i(28,32));const n=i(0,17);e.setDefault(1,0),e.addMany(s,0,2,0);for(const t of n)e.addMany([24,26,153,154],t,3,0),e.addMany(i(128,144),t,3,0),e.addMany(i(144,152),t,3,0),e.add(156,t,0,0),e.add(27,t,11,1),e.add(157,t,4,8),e.addMany([152,158],t,0,7),e.add(159,t,11,14),e.add(155,t,11,3),e.add(144,t,11,9);return e.addMany(r,0,3,0),e.addMany(r,1,3,1),e.add(127,1,0,1),e.addMany(r,8,0,8),e.addMany(r,3,3,3),e.add(127,3,0,3),e.addMany(r,4,3,4),e.add(127,4,0,4),e.addMany(r,6,3,6),e.addMany(r,5,3,5),e.add(127,5,0,5),e.addMany(r,2,3,2),e.add(127,2,0,2),e.add(93,1,4,8),e.addMany(s,8,5,8),e.add(127,8,5,8),e.addMany([156,27,24,26,7],8,6,0),e.addMany(i(28,32),8,0,8),e.addMany([88,94],1,0,7),e.addMany(s,7,0,7),e.addMany(r,7,0,7),e.add(156,7,0,0),e.add(127,7,0,7),e.add(95,1,11,14),e.addMany(r,14,0,14),e.add(127,14,0,14),e.addMany(i(32,48),14,9,15),e.addMany(i(48,127),14,15,16),e.addMany(i(48,127),15,15,16),e.addMany(r,15,0,15),e.addMany(i(32,48),15,9,15),e.add(127,15,0,15),e.addMany(s,16,16,16),e.addMany(r,16,0,16),e.addMany(i(8,14),16,16,16),e.add(127,16,0,16),e.addMany([27,156,24,26],16,17,0),e.add(91,1,11,3),e.addMany(i(64,127),3,7,0),e.addMany(i(48,60),3,8,4),e.addMany([60,61,62,63],3,9,4),e.addMany(i(48,60),4,8,4),e.addMany(i(64,127),4,7,0),e.addMany([60,61,62,63],4,0,6),e.addMany(i(32,64),6,0,6),e.add(127,6,0,6),e.addMany(i(64,127),6,0,0),e.addMany(i(32,48),3,9,5),e.addMany(i(32,48),5,9,5),e.addMany(i(48,64),5,0,6),e.addMany(i(64,127),5,7,0),e.addMany(i(32,48),4,9,5),e.addMany(i(32,48),1,9,2),e.addMany(i(32,48),2,9,2),e.addMany(i(48,127),2,10,0),e.addMany(i(48,80),1,10,0),e.addMany(i(81,88),1,10,0),e.addMany([89,90,92],1,10,0),e.addMany(i(96,127),1,10,0),e.add(80,1,11,9),e.addMany(r,9,0,9),e.add(127,9,0,9),e.addMany(i(32,48),9,9,12),e.addMany(i(48,60),9,8,10),e.addMany([60,61,62,63],9,9,10),e.addMany(r,11,0,11),e.addMany(i(32,128),11,0,11),e.addMany(r,10,0,10),e.add(127,10,0,10),e.addMany(i(48,60),10,8,10),e.addMany([60,61,62,63],10,0,11),e.addMany(i(32,48),10,9,12),e.addMany(r,12,0,12),e.add(127,12,0,12),e.addMany(i(32,48),12,9,12),e.addMany(i(48,64),12,0,11),e.addMany(i(64,127),12,12,13),e.addMany(i(64,127),10,12,13),e.addMany(i(64,127),9,12,13),e.addMany(r,13,13,13),e.addMany(s,13,13,13),e.add(127,13,0,13),e.addMany([27,156,24,26],13,14,0),e.add(c,0,2,0),e.add(c,8,5,8),e.add(c,6,0,6),e.add(c,11,0,11),e.add(c,13,13,13),e.add(c,16,16,16),e}();class l extends s.Disposable{constructor(e=t.VT500_TRANSITION_TABLE){super(),this._transitions=e,this._parseStack={state:0,handlers:[],handlerPos:0,transition:0,chunkPos:0},this.initialState=0,this.currentState=this.initialState,this._params=new r.Params,this._params.addParam(0),this._collect=0,this.precedingJoinState=0,this._printHandlerFb=(e,t,i)=>{},this._executeHandlerFb=e=>{},this._csiHandlerFb=(e,t)=>{},this._escHandlerFb=e=>{},this._errorHandlerFb=e=>e,this._printHandler=this._printHandlerFb,this._executeHandlers=Object.create(null),this._executeHandlersArr=new Array(24).fill(void 0),this._csiHandlers=Object.create(null),this._escHandlers=Object.create(null),this._register((0,s.toDisposable)(()=>{this._csiHandlers=Object.create(null),this._executeHandlers=Object.create(null),this._executeHandlersArr=new Array(24).fill(void 0),this._escHandlers=Object.create(null)})),this._oscParser=this._register(new n.OscParser),this._dcsParser=this._register(new a.DcsParser),this._apcParser=this._register(new o.ApcParser),this._errorHandler=this._errorHandlerFb,this.registerEscHandler({final:"\\"},()=>!0)}_identifier(e,t=[64,126]){let i=0;if(e.prefix){if(e.prefix.length>1)throw new Error("only one byte as prefix supported");if(i=e.prefix.charCodeAt(0),i<60||i>63)throw new Error("prefix must be in range 0x3c .. 0x3f")}if(e.intermediates){if(e.intermediates.length>2)throw new Error("only two bytes as intermediates are supported");for(let t=0;ts||s>47)throw new Error("intermediate must be in range 0x20 .. 0x2f");i<<=8,i|=s}}if(1!==e.final.length)throw new Error("final must be a single byte");const s=e.final.charCodeAt(0);if(t[0]>s||s>t[1])throw new Error(`final must be in range ${t[0]} .. ${t[1]}`);return i<<=8,i|=s,i}identToString(e){const t=[];for(;e;)t.push(String.fromCharCode(255&e)),e>>=8;return t.reverse().join("")}setPrintHandler(e){this._printHandler=e}clearPrintHandler(){this._printHandler=this._printHandlerFb}registerEscHandler(e,t){const i=this._identifier(e,[48,126]);this._escHandlers[i]??=[];const s=this._escHandlers[i];return s.push(t),{dispose:()=>{const e=s.indexOf(t);-1!==e&&s.splice(e,1)}}}clearEscHandler(e){this._escHandlers[this._identifier(e,[48,126])]&&delete this._escHandlers[this._identifier(e,[48,126])]}setEscHandlerFallback(e){this._escHandlerFb=e}setExecuteHandler(e,t){const i=e.charCodeAt(0);this._executeHandlers[i]=t,i<24&&(this._executeHandlersArr[i]=t)}clearExecuteHandler(e){const t=e.charCodeAt(0);this._executeHandlers[t]&&delete this._executeHandlers[t],t<24&&(this._executeHandlersArr[t]=void 0)}setExecuteHandlerFallback(e){this._executeHandlerFb=e}registerCsiHandler(e,t){const i=this._identifier(e);this._csiHandlers[i]??=[];const s=this._csiHandlers[i];return s.push(t),{dispose:()=>{const e=s.indexOf(t);-1!==e&&s.splice(e,1)}}}clearCsiHandler(e){this._csiHandlers[this._identifier(e)]&&delete this._csiHandlers[this._identifier(e)]}setCsiHandlerFallback(e){this._csiHandlerFb=e}registerDcsHandler(e,t){return this._dcsParser.registerHandler(this._identifier(e),t)}clearDcsHandler(e){this._dcsParser.clearHandler(this._identifier(e))}setDcsHandlerFallback(e){this._dcsParser.setHandlerFallback(e)}registerOscHandler(e,t){return this._oscParser.registerHandler(e,t)}clearOscHandler(e){this._oscParser.clearHandler(e)}setOscHandlerFallback(e){this._oscParser.setHandlerFallback(e)}registerApcHandler(e,t){return e.prefix=void 0,this._apcParser.registerHandler(this._identifier(e,[48,126]),t)}clearApcHandler(e){e.prefix=void 0,this._apcParser.clearHandler(this._identifier(e,[48,126]))}setApcHandlerFallback(e){this._apcParser.setHandlerFallback(e)}setErrorHandler(e){this._errorHandler=e}clearErrorHandler(){this._errorHandler=this._errorHandlerFb}reset(){this.currentState=this.initialState,this._oscParser.reset(),this._dcsParser.reset(),this._apcParser.reset(),this._params.resetZdm(),this._collect=0,this.precedingJoinState=0,0!==this._parseStack.state&&(this._parseStack.state=2,this._parseStack.handlers=[])}_preserveStack(e,t,i,s,r){this._parseStack.state=e,this._parseStack.handlers=t,this._parseStack.handlerPos=i,this._parseStack.transition=s,this._parseStack.chunkPos=r}parse(e,t,i){let s,r,n,a=0;if(this._parseStack.state)if(2===this._parseStack.state)this._parseStack.state=0,a=this._parseStack.chunkPos+1;else{if(void 0===i||1===this._parseStack.state)throw this._parseStack.state=1,new Error("improper continuation due to previous async handler, giving up parsing");const t=this._parseStack.handlers;let r=this._parseStack.handlerPos-1;switch(this._parseStack.state){case 3:if(!1===i&&r>-1)for(;r>=0&&(n=t[r](this._params),!0!==n);r--)if(n instanceof Promise)return this._parseStack.handlerPos=r,n;this._parseStack.handlers=[];break;case 4:if(!1===i&&r>-1)for(;r>=0&&(n=t[r](),!0!==n);r--)if(n instanceof Promise)return this._parseStack.handlerPos=r,n;this._parseStack.handlers=[];break;case 6:if(s=e[this._parseStack.chunkPos],n=this._dcsParser.unhook(24!==s&&26!==s,i),n)return n;27===s&&(this._parseStack.transition|=1),this._params.resetZdm(),this._collect=0;break;case 5:if(s=e[this._parseStack.chunkPos],n=this._oscParser.end(24!==s&&26!==s,i),n)return n;27===s&&(this._parseStack.transition|=1),this._params.resetZdm(),this._collect=0;break;case 7:if(s=e[this._parseStack.chunkPos],n=this._apcParser.end(24!==s&&26!==s,i),n)return n;27===s&&(this._parseStack.transition|=1),this._params.resetZdm(),this._collect=0}this._parseStack.state=0,a=this._parseStack.chunkPos+1,this.precedingJoinState=0,this.currentState=255&this._parseStack.transition}for(let i=a;i=60&&a<=63&&(this._collect=a,s++);let o=!1;for(;s=48&&a<=57)this._params.addDigit(a-48);else if(59===a)this._params.addParam(0);else{if(58!==a){if(a>=64&&a<=126){const e=this._csiHandlers[this._collect<<8|a];let t=e?e.length-1:-1;for(;t>=0&&(n=e[t](this._params),!0!==n);t--)if(n instanceof Promise)return r=1792,this._preserveStack(3,e,t,r,s),n;t<0&&this._csiHandlerFb(this._collect<<8|a,this._params),this.precedingJoinState=0,i=s,this.currentState=0,o=!0;break}break}this._params.addSubParam(-1)}o||(i=s-1,this.currentState=4);continue}switch(r=this._transitions.table[this.currentState<<8|(s>8){case 2:let a=i;const o=t-4;for(;a=32&&(e[a]<=126||e[a]>=c)&&e[++a]>=32&&(e[a]<=126||e[a]>=c)&&e[++a]>=32&&(e[a]<=126||e[a]>=c)&&e[++a]>=32&&(e[a]<=126||e[a]>=c););if(a>=o)for(;a=32&&(e[a]<=126||e[a]>=c);)a++;this._printHandler(e,i,a),i=a-1;break;case 3:this._executeHandlers[s]?this._executeHandlers[s]():this._executeHandlerFb(s),this.precedingJoinState=0;break;case 0:break;case 1:if(this._errorHandler({position:i,code:s,currentState:this.currentState,collect:this._collect,params:this._params,abort:!1}).abort)return;break;case 7:const h=this._csiHandlers[this._collect<<8|s];let l=h?h.length-1:-1;for(;l>=0&&(n=h[l](this._params),!0!==n);l--)if(n instanceof Promise)return this._preserveStack(3,h,l,r,i),n;l<0&&this._csiHandlerFb(this._collect<<8|s,this._params),this.precedingJoinState=0;break;case 8:do{switch(s){case 59:this._params.addParam(0);break;case 58:this._params.addSubParam(-1);break;default:this._params.addDigit(s-48)}}while(++i47&&s<60);i--;break;case 9:this._collect<<=8,this._collect|=s;break;case 10:const _=this._escHandlers[this._collect<<8|s];let d=_?_.length-1:-1;for(;d>=0&&(n=_[d](),!0!==n);d--)if(n instanceof Promise)return this._preserveStack(4,_,d,r,i),n;d<0&&this._escHandlerFb(this._collect<<8|s),this.precedingJoinState=0;break;case 11:this._params.resetZdm(),this._collect=0;break;case 12:this._dcsParser.hook(this._collect<<8|s,this._params);break;case 13:for(let r=i+1;;++r)if(r>=t||24===(s=e[r])||26===s||27===s||s>127&&s=t||(s=e[r])<32||s>127&&s=32&&e[s]<127||e[s]>=8&&e[s]<14||e[s]>=c))){this._apcParser.put(e,i,s),i=s-1;break}break;case 17:if(n=this._apcParser.end(24!==s&&26!==s),n)return this._preserveStack(7,[],0,r,i),n;27===s&&(r|=1),this._params.resetZdm(),this._collect=0,this.precedingJoinState=0}this.currentState=255&r}}}t.EscapeSequenceParser=l},346(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.OscHandler=t.OscParser=void 0;const s=i(726),r=i(220),n=[];t.OscParser=class{constructor(){this._state=0,this._active=n,this._id=-1,this._handlers=Object.create(null),this._handlerFb=()=>{},this._stack={paused:!1,loopPosition:0,fallThrough:!1}}registerHandler(e,t){this._handlers[e]??=[];const i=this._handlers[e];return i.push(t),{dispose:()=>{const e=i.indexOf(t);-1!==e&&i.splice(e,1)}}}clearHandler(e){this._handlers[e]&&delete this._handlers[e]}setHandlerFallback(e){this._handlerFb=e}dispose(){this._handlers=Object.create(null),this._handlerFb=()=>{},this._active=n}reset(){if(2===this._state)for(let e=this._stack.paused?this._stack.loopPosition-1:this._active.length-1;e>=0;--e)this._active[e].end(!1);this._stack.paused=!1,this._active=n,this._id=-1,this._state=0}_start(){if(this._active=this._handlers[this._id]||n,this._active.length)for(let e=this._active.length-1;e>=0;e--)this._active[e].start();else this._handlerFb(this._id,"START")}_put(e,t,i){if(this._active.length)for(let s=this._active.length-1;s>=0;s--)this._active[s].put(e,t,i);else this._handlerFb(this._id,"PUT",(0,s.utf32ToString)(e,t,i))}start(){this.reset(),this._state=1}put(e,t,i){if(3!==this._state){if(1===this._state)for(;t0&&this._put(e,t,i)}}end(e,t=!0){if(0!==this._state){if(3!==this._state)if(1===this._state&&this._start(),this._active.length){let i=!1,s=this._active.length-1,r=!1;if(this._stack.paused&&(s=this._stack.loopPosition-1,i=t,r=this._stack.fallThrough,this._stack.paused=!1),!r&&!1===i){for(;s>=0&&(i=this._active[s].end(e),!0!==i);s--)if(i instanceof Promise)return this._stack.paused=!0,this._stack.loopPosition=s,this._stack.fallThrough=!1,i;s--}for(;s>=0;s--)if(i=this._active[s].end(!1),i instanceof Promise)return this._stack.paused=!0,this._stack.loopPosition=s,this._stack.fallThrough=!0,i}else this._handlerFb(this._id,"END",e);this._active=n,this._id=-1,this._state=0}}};class a{constructor(e){this._handler=e,this._data=new r.LimitedStringBuilder(a._payloadLimit),this._hitLimit=!1}start(){this._data.reset(),this._hitLimit=!1}put(e,t,i){this._hitLimit||this._data.append((0,s.utf32ToString)(e,t,i))&&(this._hitLimit=!0)}end(e){let t=!1;if(this._hitLimit)t=!1;else if(e&&(t=this._handler(this._data.toString()),t instanceof Promise))return t.then(e=>(this._data.reset(),this._hitLimit=!1,e));return this._data.reset(),this._hitLimit=!1,t}}t.OscHandler=a,a._payloadLimit=1e7},262(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.Params=void 0;class i{static fromArray(e){const t=new i;if(!e.length)return t;for(let i=Array.isArray(e[0])?1:0;i256)throw new Error("maxSubParamsLength must not be greater than 256");this.params=new Int32Array(e),this.length=0,this._subParams=new Int32Array(t),this._subParamsLength=0,this._subParamsIdx=new Uint16Array(e),this._rejectDigits=!1,this._rejectSubDigits=!1,this._digitIsSub=!1}clone(){const e=new i(this.maxLength,this.maxSubParamsLength);return e.params.set(this.params),e.length=this.length,e._subParams.set(this._subParams),e._subParamsLength=this._subParamsLength,e._subParamsIdx.set(this._subParamsIdx),e._rejectDigits=this._rejectDigits,e._rejectSubDigits=this._rejectSubDigits,e._digitIsSub=this._digitIsSub,e}toArray(){const e=[];for(let t=0;t>8,s=255&this._subParamsIdx[t];s-i>0&&e.push(Array.prototype.slice.call(this._subParams,i,s))}return e}reset(){this.length=0,this._subParamsLength=0,this._rejectDigits=!1,this._rejectSubDigits=!1,this._digitIsSub=!1}resetZdm(){this.length=1,this._subParamsLength=0,this._rejectDigits=!1,this._rejectSubDigits=!1,this._digitIsSub=!1,this._subParamsIdx[0]=0,this.params[0]=0}addParam(e){if(this._digitIsSub=!1,this.length>=this.maxLength)this._rejectDigits=!0;else{if(e<-1)throw new Error("values less than -1 are not allowed");this._subParamsIdx[this.length]=this._subParamsLength<<8|this._subParamsLength,this.params[this.length++]=e>2147483647?2147483647:e}}addSubParam(e){if(this._digitIsSub=!0,this.length)if(this._rejectDigits||this._subParamsLength>=this.maxSubParamsLength)this._rejectSubDigits=!0;else{if(e<-1)throw new Error("values less than -1 are not allowed");this._subParams[this._subParamsLength++]=e>2147483647?2147483647:e,this._subParamsIdx[this.length-1]++}}hasSubParams(e){return(255&this._subParamsIdx[e])-(this._subParamsIdx[e]>>8)>0}getSubParams(e){const t=this._subParamsIdx[e]>>8,i=255&this._subParamsIdx[e];return i-t>0?this._subParams.subarray(t,i):null}getSubParamsAll(){const e={};for(let t=0;t>8,s=255&this._subParamsIdx[t];s-i>0&&(e[t]=this._subParams.slice(i,s))}return e}addDigit(e){let t;if(this._rejectDigits||!(t=this._digitIsSub?this._subParamsLength:this.length)||this._digitIsSub&&this._rejectSubDigits)return;const i=this._digitIsSub?this._subParams:this.params,s=i[t-1];i[t-1]=~s?Math.min(10*s+e,2147483647):e}}t.Params=i},27(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.AddonManager=void 0,t.AddonManager=class{constructor(){this._addons=[]}dispose(){for(let e=this._addons.length-1;e>=0;e--)this._addons[e].instance.dispose()}loadAddon(e,t){const i={instance:t,dispose:t.dispose,isDisposed:!1};this._addons.push(i),t.dispose=()=>this._wrappedAddonDispose(i),t.activate(e)}_wrappedAddonDispose(e){if(e.isDisposed)return;let t=-1;for(let i=0;i=this._line.length))return t?(this._line.loadCell(e,t),t):this._line.loadCell(e,new s.CellData)}translateToString(e,t,i){return this._line.translateToString(e,t,i)}}},101(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.BufferNamespaceApi=void 0;const s=i(235),r=i(812),n=i(636);class a extends r.Disposable{constructor(e){super(),this._core=e,this._onBufferChange=this._register(new n.Emitter),this.onBufferChange=this._onBufferChange.event,this._normal=new s.BufferApiView(this._core.buffers.normal,"normal"),this._alternate=new s.BufferApiView(this._core.buffers.alt,"alternate"),this._register(this._core.buffers.onBufferActivate(()=>this._onBufferChange.fire(this.active)))}get active(){if(this._core.buffers.active===this._core.buffers.normal)return this.normal;if(this._core.buffers.active===this._core.buffers.alt)return this.alternate;throw new Error("Active buffer is neither normal nor alternate")}get normal(){return this._normal.init(this._core.buffers.normal)}get alternate(){return this._alternate.init(this._core.buffers.alt)}}t.BufferNamespaceApi=a},97(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.ParserApi=void 0,t.ParserApi=class{constructor(e){this._core=e}registerCsiHandler(e,t){return this._core.registerCsiHandler(e,e=>t(e.toArray()))}addCsiHandler(e,t){return this.registerCsiHandler(e,t)}registerDcsHandler(e,t){return this._core.registerDcsHandler(e,(e,i)=>t(e,i.toArray()))}addDcsHandler(e,t){return this.registerDcsHandler(e,t)}registerEscHandler(e,t){return this._core.registerEscHandler(e,t)}addEscHandler(e,t){return this.registerEscHandler(e,t)}registerOscHandler(e,t){return this._core.registerOscHandler(e,t)}addOscHandler(e,t){return this.registerOscHandler(e,t)}registerApcHandler(e,t){return this._core.registerApcHandler(e,t)}}},335(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.UnicodeApi=void 0,t.UnicodeApi=class{constructor(e){this._core=e}register(e){this._core.unicodeService.register(e)}get versions(){return this._core.unicodeService.versions}get activeVersion(){return this._core.unicodeService.activeVersion}set activeVersion(e){this._core.unicodeService.activeVersion=e}}},640(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,a=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)a=Reflect.decorate(e,t,i,s);else for(var o=e.length-1;o>=0;o--)(r=e[o])&&(a=(n<3?r(a):n>3?r(t,i,a):r(t,i))||a);return n>3&&a&&Object.defineProperty(t,i,a),a},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.BufferService=void 0;const n=i(812),a=i(478),o=i(501),h=i(636);let c=class extends n.Disposable{get buffer(){return this.buffers.active}constructor(e,t){super(),this.isUserScrolling=!1,this._onResize=this._register(new h.Emitter),this.onResize=this._onResize.event,this._onScroll=this._register(new h.Emitter),this.onScroll=this._onScroll.event,this.cols=Math.max(e.rawOptions.cols||0,2),this.rows=Math.max(e.rawOptions.rows||0,1),this.buffers=this._register(new a.BufferSet(e,this,t)),this._register(this.buffers.onBufferActivate(e=>{this._onScroll.fire(e.activeBuffer.ydisp)}))}resize(e,t){const i=this.cols!==e,s=this.rows!==t;this.cols=e,this.rows=t,this.buffers.resize(e,t),this._onResize.fire({cols:e,rows:t,colsChanged:i,rowsChanged:s})}reset(){this.buffers.reset(),this.isUserScrolling=!1}scroll(e,t=!1){const i=this.buffer;let s;s=this._cachedBlankLine,s&&s.length===this.cols&&s.getFg(0)===e.fg&&s.getBg(0)===e.bg||(s=i.getBlankLine(e,t),this._cachedBlankLine=s),s.isWrapped=t;const r=i.ybase+i.scrollTop,n=i.ybase+i.scrollBottom;if(0===i.scrollTop){const e=i.lines.isFull;n===i.lines.length-1?e?i.lines.recycle().copyFrom(s,!0):i.lines.push(s.clone(!0)):i.lines.splice(n+1,0,s.clone(!0)),e?this.isUserScrolling&&(i.ydisp=Math.max(i.ydisp-1,0)):(i.ybase++,this.isUserScrolling||i.ydisp++)}else{const e=n-r+1;i.lines.shiftElements(r+1,e-1,-1),i.lines.set(n,s.clone(!0))}this.isUserScrolling||(i.ydisp=i.ybase),this._onScroll.fire(i.ydisp)}scrollLines(e,t){const i=this.buffer;if(e<0){if(0===i.ydisp)return;this.isUserScrolling=!0}else e+i.ydisp>=i.ybase&&(this.isUserScrolling=!1);const s=i.ydisp;i.ydisp=Math.max(Math.min(i.ydisp+e,i.ybase),0),s!==i.ydisp&&(t||this._onScroll.fire(i.ydisp))}};t.BufferService=c,t.BufferService=c=s([r(0,o.IOptionsService),r(1,o.ILogService)],c)},746(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.CharsetService=void 0,t.CharsetService=class{constructor(){this.glevel=0,this._charsets=[]}get charsets(){return this._charsets}reset(){this.charset=void 0,this._charsets=[],this.glevel=0}setgLevel(e){this.glevel=e,this.charset=this._charsets[e]}setgCharset(e,t){this._charsets[e]=t,this.glevel===e&&(this.charset=t)}}},71(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,a=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)a=Reflect.decorate(e,t,i,s);else for(var o=e.length-1;o>=0;o--)(r=e[o])&&(a=(n<3?r(a):n>3?r(t,i,a):r(t,i))||a);return n>3&&a&&Object.defineProperty(t,i,a),a},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.CoreService=void 0;const n=i(812),a=i(501),o=i(636),h=Object.freeze({insertMode:!1}),c=Object.freeze({applicationCursorKeys:!1,applicationKeypad:!1,bracketedPasteMode:!1,colorSchemeUpdates:!1,cursorBlink:void 0,cursorStyle:void 0,origin:!1,reverseWraparound:!1,sendFocus:!1,synchronizedOutput:!1,win32InputMode:!1,wraparound:!0});let l=class extends n.Disposable{constructor(e,t,i){super(),this._bufferService=e,this._logService=t,this._optionsService=i,this.isCursorHidden=!1,this._onData=this._register(new o.Emitter),this.onData=this._onData.event,this._onUserInput=this._register(new o.Emitter),this.onUserInput=this._onUserInput.event,this._onBinary=this._register(new o.Emitter),this.onBinary=this._onBinary.event,this._onRequestScrollToBottom=this._register(new o.Emitter),this.onRequestScrollToBottom=this._onRequestScrollToBottom.event,this.isCursorInitialized=i.rawOptions.showCursorImmediately??!1,this.modes=structuredClone(h),this.decPrivateModes=structuredClone(c),this.kittyKeyboard={flags:0,mainFlags:0,altFlags:0,mainStack:[],altStack:[]}}reset(){this.modes=structuredClone(h),this.decPrivateModes=structuredClone(c),this.kittyKeyboard={flags:0,mainFlags:0,altFlags:0,mainStack:[],altStack:[]}}triggerDataEvent(e,t=!1){if(this._optionsService.rawOptions.disableStdin)return;const i=this._bufferService.buffer;t&&this._optionsService.rawOptions.scrollOnUserInput&&i.ybase!==i.ydisp&&this._onRequestScrollToBottom.fire(),t&&this._onUserInput.fire(),this._logService.debug(`sending data "${e}"`),this._logService.trace("sending data (codes)",()=>e.split("").map(e=>e.charCodeAt(0))),this._onData.fire(e)}triggerBinaryEvent(e){this._optionsService.rawOptions.disableStdin||(this._logService.debug(`sending binary "${e}"`),this._logService.trace("sending binary (codes)",()=>e.split("").map(e=>e.charCodeAt(0))),this._onBinary.fire(e))}};t.CoreService=l,t.CoreService=l=s([r(0,a.IBufferService),r(1,a.ILogService),r(2,a.IOptionsService)],l)},25(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.InstantiationService=t.ServiceCollection=void 0;const s=i(501),r=i(201);class n{constructor(...e){this._entries=new Map;for(const[t,i]of e)this.set(t,i)}set(e,t){const i=this._entries.get(e);return this._entries.set(e,t),i}forEach(e){for(const[t,i]of this._entries.entries())e(t,i)}has(e){return this._entries.has(e)}get(e){return this._entries.get(e)}}t.ServiceCollection=n,t.InstantiationService=class{constructor(){this._services=new n,this._services.set(s.IInstantiationService,this)}setService(e,t){this._services.set(e,t)}getService(e){return this._services.get(e)}createInstance(e,...t){const i=(0,r.getServiceDependencies)(e).sort((e,t)=>e.index-t.index),s=[];for(const t of i){const i=this._services.get(t.id);if(!i)throw new Error(`[createInstance] ${e.name} depends on UNKNOWN service ${t.id._id}.`);s.push(i)}const n=i.length>0?i[0].index:t.length;if(t.length!==n)throw new Error(`[createInstance] First service dependency of ${e.name} at position ${n+1} conflicts with ${t.length} static arguments`);return new e(...[...t,...s])}}},276(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,a=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)a=Reflect.decorate(e,t,i,s);else for(var o=e.length-1;o>=0;o--)(r=e[o])&&(a=(n<3?r(a):n>3?r(t,i,a):r(t,i))||a);return n>3&&a&&Object.defineProperty(t,i,a),a},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.LogService=void 0;const n=i(812),a=i(501),o={trace:a.LogLevelEnum.TRACE,debug:a.LogLevelEnum.DEBUG,info:a.LogLevelEnum.INFO,warn:a.LogLevelEnum.WARN,error:a.LogLevelEnum.ERROR,off:a.LogLevelEnum.OFF};let h=class extends n.Disposable{get logLevel(){return this._logLevel}constructor(e){super(),this._optionsService=e,this._logLevel=a.LogLevelEnum.OFF,this._updateLogLevel(),this._register(this._optionsService.onSpecificOptionChange("logLevel",()=>this._updateLogLevel()))}_updateLogLevel(){this._logLevel=o[this._optionsService.rawOptions.logLevel]}_evalLazyOptionalParams(e){for(let t=0;t!1},X10:{events:1,restrict:e=>4!==e.button&&1===e.action&&(e.ctrl=!1,e.alt=!1,e.shift=!1,!0)},VT200:{events:19,restrict:e=>32!==e.action},DRAG:{events:23,restrict:e=>32!==e.action||3!==e.button},ANY:{events:31,restrict:e=>!0}};function a(e,t){let i=(e.ctrl?16:0)|(e.shift?4:0)|(e.alt?8:0);return 4===e.button?(i|=64,i|=e.action):(i|=3&e.button,4&e.button&&(i|=64),8&e.button&&(i|=128),32===e.action?i|=32:0!==e.action||t||(i|=3)),i}const o=String.fromCharCode,h={DEFAULT:e=>{const t=[a(e,!1)+32,e.col+32,e.row+32];return t[0]>255||t[1]>255||t[2]>255?"":`${o(t[0])}${o(t[1])}${o(t[2])}`},SGR:e=>{const t=0===e.action&&4!==e.button?"m":"M";return`[<${a(e,!0)};${e.col};${e.row}${t}`},SGR_PIXELS:e=>{const t=0===e.action&&4!==e.button?"m":"M";return`[<${a(e,!0)};${e.x};${e.y}${t}`}};class c extends s.Disposable{constructor(){super(),this._protocols={},this._encodings={},this._activeProtocol="",this._activeEncoding="",this._onProtocolChange=this._register(new r.Emitter),this.onProtocolChange=this._onProtocolChange.event;for(const e of Object.keys(n))this.addProtocol(e,n[e]);for(const e of Object.keys(h))this.addEncoding(e,h[e]);this.reset()}addProtocol(e,t){this._protocols[e]=t}addEncoding(e,t){this._encodings[e]=t}get activeProtocol(){return this._activeProtocol}get areMouseEventsActive(){return 0!==this._protocols[this._activeProtocol].events}set activeProtocol(e){if(!this._protocols[e])throw new Error(`unknown protocol "${e}"`);this._activeProtocol=e,this._onProtocolChange.fire(this._protocols[e].events)}get activeEncoding(){return this._activeEncoding}set activeEncoding(e){if(!this._encodings[e])throw new Error(`unknown encoding "${e}"`);this._activeEncoding=e}reset(){this.activeProtocol="NONE",this.activeEncoding="DEFAULT"}setCustomWheelEventHandler(e){this._customWheelEventHandler=e}allowCustomWheelEvent(e){return!this._customWheelEventHandler||!1!==this._customWheelEventHandler(e)}restrictMouseEvent(e){return this._protocols[this._activeProtocol].restrict(e)}encodeMouseEvent(e){return this._encodings[this._activeEncoding](e)}get isDefaultEncoding(){return"DEFAULT"===this._activeEncoding}get isPixelEncoding(){return"SGR_PIXELS"===this._activeEncoding}}t.MouseStateService=c},56(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.OptionsService=t.DEFAULT_OPTIONS=void 0;const s=i(812),r=i(701),n=i(636);t.DEFAULT_OPTIONS={cols:80,rows:24,showCursorImmediately:!1,cursorBlink:!1,blinkIntervalDuration:0,cursorStyle:"block",cursorWidth:1,cursorInactiveStyle:"outline",drawBoldTextInBrightColors:!0,documentOverride:null,fastScrollSensitivity:5,fontFamily:"monospace",fontSize:15,fontWeight:"normal",fontWeightBold:"bold",ignoreBracketedPasteMode:!1,lineHeight:1,letterSpacing:0,linkHandler:null,logLevel:"info",logger:null,scrollback:1e3,scrollbar:{showScrollbar:!0},scrollOnEraseInDisplay:!1,scrollOnUserInput:!0,scrollSensitivity:1,screenReaderMode:!1,smoothScrollDuration:0,macOptionIsMeta:!1,macOptionClickForcesSelection:!1,minimumContrastRatio:1,mouseEventsRequireAlt:!1,disableStdin:!1,allowProposedApi:!1,allowTransparency:!1,tabStopWidth:8,theme:{},reflowCursorLine:!1,rescaleOverlappingGlyphs:!1,rightClickSelectsWord:r.isMac,windowOptions:{},windowsPty:{},wordSeparator:" ()[]{}',\"`",altClickMovesCursor:!0,convertEol:!1,termName:"xterm",quirks:{},vtExtensions:{}};const a=["normal","bold","100","200","300","400","500","600","700","800","900"];class o extends s.Disposable{constructor(e){super(),this._onOptionChange=this._register(new n.Emitter),this.onOptionChange=this._onOptionChange.event;const i={...t.DEFAULT_OPTIONS};for(const t in e)if(t in i)try{const s=e[t];i[t]=this._sanitizeAndValidateOption(t,s)}catch(e){console.error(e)}this.rawOptions=i,this.options={...i},this._setupOptions(),this._register((0,s.toDisposable)(()=>{this.rawOptions.linkHandler=null,this.rawOptions.documentOverride=null}))}onSpecificOptionChange(e,t){return this.onOptionChange(i=>{i===e&&t(this.rawOptions[e])})}onMultipleOptionChange(e,t){return this.onOptionChange(i=>{-1!==e.indexOf(i)&&t()})}_setupOptions(){const e=e=>{if(!(e in t.DEFAULT_OPTIONS))throw new Error(`No option with key "${e}"`);return this.rawOptions[e]},i=(e,i)=>{if(!(e in t.DEFAULT_OPTIONS))throw new Error(`No option with key "${e}"`);i=this._sanitizeAndValidateOption(e,i),this.rawOptions[e]!==i&&(this.rawOptions[e]=i,this._onOptionChange.fire(e))};for(const t in this.rawOptions){const s={get:e.bind(this,t),set:i.bind(this,t)};Object.defineProperty(this.options,t,s)}}_sanitizeAndValidateOption(e,i){switch(e){case"cursorStyle":if(i||(i=t.DEFAULT_OPTIONS[e]),!function(e){return"block"===e||"underline"===e||"bar"===e}(i))throw new Error(`"${i}" is not a valid value for ${e}`);break;case"wordSeparator":i||(i=t.DEFAULT_OPTIONS[e]);break;case"fontWeight":case"fontWeightBold":if("number"==typeof i&&1<=i&&i<=1e3)break;i=a.includes(i)?i:t.DEFAULT_OPTIONS[e];break;case"blinkIntervalDuration":if((i=Math.floor(i))<0)throw new Error(`${e} cannot be less than 0, value: ${i}`);break;case"cursorWidth":i=Math.floor(i);case"lineHeight":case"tabStopWidth":if(i<1)throw new Error(`${e} cannot be less than 1, value: ${i}`);break;case"minimumContrastRatio":i=Math.max(1,Math.min(21,Math.round(10*i)/10));break;case"scrollback":if((i=Math.min(i,4294967295))<0)throw new Error(`${e} cannot be less than 0, value: ${i}`);break;case"fastScrollSensitivity":case"scrollSensitivity":if(i<=0)throw new Error(`${e} cannot be less than or equal to 0, value: ${i}`);break;case"rows":case"cols":if(!i&&0!==i)throw new Error(`${e} must be numeric, value: ${i}`);break;case"windowsPty":i=i??{}}return i}}t.OptionsService=o},811(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,a=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)a=Reflect.decorate(e,t,i,s);else for(var o=e.length-1;o>=0;o--)(r=e[o])&&(a=(n<3?r(a):n>3?r(t,i,a):r(t,i))||a);return n>3&&a&&Object.defineProperty(t,i,a),a},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.OscLinkService=void 0;const n=i(501);let a=class{constructor(e){this._bufferService=e,this._nextId=1,this._entriesWithId=new Map,this._dataByLinkId=new Map}registerLink(e){const t=this._bufferService.buffer;if(void 0===e.id){const i=t.addMarker(t.ybase+t.y),s={data:e,id:this._nextId++,lines:[i]};return i.onDispose(()=>this._removeMarkerFromLink(s,i)),this._dataByLinkId.set(s.id,s),s.id}const i=e,s=this._getEntryIdKey(i),r=this._entriesWithId.get(s);if(r)return this.addLineToLink(r.id,t.ybase+t.y),r.id;const n=t.addMarker(t.ybase+t.y),a={id:this._nextId++,key:this._getEntryIdKey(i),data:i,lines:[n]};return n.onDispose(()=>this._removeMarkerFromLink(a,n)),this._entriesWithId.set(a.key,a),this._dataByLinkId.set(a.id,a),a.id}addLineToLink(e,t){const i=this._dataByLinkId.get(e);if(i&&i.lines.every(e=>e.line!==t)){const e=this._bufferService.buffer.addMarker(t);i.lines.push(e),e.onDispose(()=>this._removeMarkerFromLink(i,e))}}getLinkData(e){return this._dataByLinkId.get(e)?.data}_getEntryIdKey(e){return`${e.id};;${e.uri}`}_removeMarkerFromLink(e,t){const i=e.lines.indexOf(t);-1!==i&&(e.lines.splice(i,1),0===e.lines.length&&(void 0!==e.data.id&&this._entriesWithId.delete(e.key),this._dataByLinkId.delete(e.id)))}};t.OscLinkService=a,t.OscLinkService=a=s([r(0,n.IBufferService)],a)},201(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.serviceRegistry=void 0,t.getServiceDependencies=function(e){return e.di$dependencies||[]},t.createDecorator=function(e){if(t.serviceRegistry.has(e))return t.serviceRegistry.get(e);const i=function(e,t,s){if(3!==arguments.length)throw new Error("@IServiceName-decorator can only be used to decorate a parameter");!function(e,t,i){t.di$target===t?t.di$dependencies.push({id:e,index:i}):(t.di$dependencies=[{id:e,index:i}],t.di$target=t)}(i,e,s)};return i._id=e,t.serviceRegistry.set(e,i),i},t.serviceRegistry=new Map},501(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.IDecorationService=t.IUnicodeService=t.IOscLinkService=t.IOptionsService=t.ILogService=t.LogLevelEnum=t.IInstantiationService=t.ICharsetService=t.ICoreService=t.IMouseStateService=t.IBufferService=void 0;const s=i(201);var r;t.IBufferService=(0,s.createDecorator)("BufferService"),t.IMouseStateService=(0,s.createDecorator)("MouseStateService"),t.ICoreService=(0,s.createDecorator)("CoreService"),t.ICharsetService=(0,s.createDecorator)("CharsetService"),t.IInstantiationService=(0,s.createDecorator)("InstantiationService"),function(e){e[e.TRACE=0]="TRACE",e[e.DEBUG=1]="DEBUG",e[e.INFO=2]="INFO",e[e.WARN=3]="WARN",e[e.ERROR=4]="ERROR",e[e.OFF=5]="OFF"}(r||(t.LogLevelEnum=r={})),t.ILogService=(0,s.createDecorator)("LogService"),t.IOptionsService=(0,s.createDecorator)("OptionsService"),t.IOscLinkService=(0,s.createDecorator)("OscLinkService"),t.IUnicodeService=(0,s.createDecorator)("UnicodeService"),t.IDecorationService=(0,s.createDecorator)("DecorationService")},415(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.UnicodeService=void 0;const s=i(636);class r{constructor(){this._providers=Object.create(null),this._active="",this._onChange=new s.Emitter,this.onChange=this._onChange.event}static extractShouldJoin(e){return!!(1&e)}static extractWidth(e){return e>>1&3}static extractCharKind(e){return e>>3}static createPropertyValue(e,t,i=!1){return(16777215&e)<<3|(3&t)<<1|(i?1:0)}dispose(){this._onChange.dispose()}get versions(){return Object.keys(this._providers)}get activeVersion(){return this._active}set activeVersion(e){if(!this._providers[e])throw new Error(`unknown Unicode version "${e}"`);this._active=e,this._activeProvider=this._providers[e],this._onChange.fire(e)}register(e){this._providers[e.version]=e,this._active||(this.activeVersion=e.version)}wcwidth(e){return this._activeProvider.wcwidth(e)}getStringCellWidth(e){let t=0,i=0;const s=e.length;for(let n=0;n=s)return t+this.wcwidth(a);const i=e.charCodeAt(n);56320<=i&&i<=57343?a=1024*(a-55296)+i-56320+65536:t+=this.wcwidth(i)}const o=this.charProperties(a,i);let h=r.extractWidth(o);r.extractShouldJoin(o)&&(h-=r.extractWidth(i)),t+=h,i=o}return t}charProperties(e,t){return this._activeProvider.charProperties(e,t)}}t.UnicodeService=r},856(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.Terminal=void 0;const s=i(107),r=i(777),n=i(636);class a extends r.CoreTerminal{constructor(e={}){super(e),this._onBell=this._register(new n.Emitter),this.onBell=this._onBell.event,this._onCursorMove=this._register(new n.Emitter),this.onCursorMove=this._onCursorMove.event,this._onTitleChange=this._register(new n.Emitter),this.onTitleChange=this._onTitleChange.event,this._onA11yCharEmitter=this._register(new n.Emitter),this.onA11yChar=this._onA11yCharEmitter.event,this._onA11yTabEmitter=this._register(new n.Emitter),this.onA11yTab=this._onA11yTabEmitter.event,this._setup(),this._register(this._inputHandler.onRequestBell(()=>this.bell())),this._register(this._inputHandler.onRequestReset(()=>this.reset())),this._register(n.EventUtils.forward(this._inputHandler.onCursorMove,this._onCursorMove)),this._register(n.EventUtils.forward(this._inputHandler.onTitleChange,this._onTitleChange)),this._register(n.EventUtils.forward(this._inputHandler.onA11yChar,this._onA11yCharEmitter)),this._register(n.EventUtils.forward(this._inputHandler.onA11yTab,this._onA11yTabEmitter)),this._register(n.EventUtils.forward(n.EventUtils.map(this._inputHandler.onRequestRefreshRows,e=>({start:e?.start??0,end:e?.end??this.rows-1})),this._onRender))}get buffer(){return this.buffers.active}get markers(){return this.buffer.markers}registerMarker(e){return this.buffer.addMarker(this.buffer.ybase+this.buffer.y+e)}bell(){this._onBell.fire()}input(e,t=!0){this.coreService.triggerDataEvent(e,t)}resize(e,t){e===this.cols&&t===this.rows||super.resize(e,t)}clear(){this.buffer.clearAllMarkers(),this.buffer.lines.set(0,this.buffer.lines.get(this.buffer.ybase+this.buffer.y)),this.buffer.lines.length=1,this.buffer.ydisp=0,this.buffer.ybase=0,this.buffer.y=0;for(let e=1;e{var e=s;Object.defineProperty(e,"__esModule",{value:!0}),e.Terminal=void 0;const t=i(101),r=i(97),n=i(335),a=i(856),o=i(27),h=i(812),c=["cols","rows"];class l extends h.Disposable{constructor(e){super(),this._core=this._register(new a.Terminal(e)),this._addonManager=this._register(new o.AddonManager),this._publicOptions={...this._core.options};const t=e=>this._core.options[e],i=(e,t)=>{this._checkReadonlyOptions(e),this._core.options[e]=t};for(const e in this._core.options){const s={get:t.bind(this,e),set:i.bind(this,e)};Object.defineProperty(this._publicOptions,e,s)}}_checkReadonlyOptions(e){if(c.includes(e))throw new Error(`Option "${e}" can only be set in the constructor`)}_checkProposedApi(){if(!this._core.optionsService.options.allowProposedApi)throw new Error("You must set the allowProposedApi option to true to use proposed API")}get onBell(){return this._core.onBell}get onBinary(){return this._core.onBinary}get onCursorMove(){return this._core.onCursorMove}get onData(){return this._core.onData}get onLineFeed(){return this._core.onLineFeed}get onRender(){return this._core.onRender}get onResize(){return this._core.onResize}get onScroll(){return this._core.onScroll}get onTitleChange(){return this._core.onTitleChange}get onWriteParsed(){return this._core.onWriteParsed}get parser(){return this._parser??=new r.ParserApi(this._core),this._parser}get unicode(){return this._checkProposedApi(),new n.UnicodeApi(this._core)}get rows(){return this._core.rows}get cols(){return this._core.cols}get buffer(){return this._buffer??=this._register(new t.BufferNamespaceApi(this._core)),this._buffer}get markers(){return this._core.markers}get modes(){const e=this._core.coreService.decPrivateModes;let t="none";switch(this._core.mouseStateService.activeProtocol){case"X10":t="x10";break;case"VT200":t="vt200";break;case"DRAG":t="drag";break;case"ANY":t="any"}return{applicationCursorKeysMode:e.applicationCursorKeys,applicationKeypadMode:e.applicationKeypad,bracketedPasteMode:e.bracketedPasteMode,insertMode:this._core.coreService.modes.insertMode,mouseTrackingMode:t,originMode:e.origin,reverseWraparoundMode:e.reverseWraparound,sendFocusMode:e.sendFocus,showCursor:!this._core.coreService.isCursorHidden,synchronizedOutputMode:e.synchronizedOutput,win32InputMode:e.win32InputMode,wraparoundMode:e.wraparound}}get options(){return this._publicOptions}set options(e){for(const t in e)this._publicOptions[t]=e[t]}input(e,t=!0){this._core.input(e,t)}resize(e,t){this._verifyIntegers(e,t),this._core.resize(e,t)}registerMarker(e=0){return this._verifyIntegers(e),this._core.registerMarker(e)}addMarker(e){return this.registerMarker(e)}dispose(){super.dispose()}scrollLines(e){this._verifyIntegers(e),this._core.scrollLines(e)}scrollPages(e){this._verifyIntegers(e),this._core.scrollPages(e)}scrollToTop(){this._core.scrollToTop()}scrollToBottom(){this._core.scrollToBottom()}scrollToLine(e){this._verifyIntegers(e),this._core.scrollToLine(e)}clear(){this._core.clear()}write(e,t){this._core.write(e,t)}writeln(e,t){this._core.write(e),this._core.write("\r\n",t)}reset(){this._core.reset()}loadAddon(e){this._addonManager.loadAddon(this,e)}_verifyIntegers(...e){for(const t of e)if(t===1/0||isNaN(t)||t%1!=0)throw new Error("This API only accepts integers")}}e.Terminal=l})();var r=exports;for(var n in s)r[n]=s[n];s.__esModule&&Object.defineProperty(r,"__esModule",{value:!0})})(); + //# sourceMappingURL=xterm-headless.js.map +\ No newline at end of file +diff --git a/lib-headless/xterm-headless.js.map b/lib-headless/xterm-headless.js.map +index 84b5526bfe137fb4932f1dd5a58ac8d657721bcd..8dbf0d37123e802e2e91a635a4006db85c4c7c7a 100644 +--- a/lib-headless/xterm-headless.js.map ++++ b/lib-headless/xterm-headless.js.map +@@ -1 +1 @@ +-{"version":3,"file":"xterm-headless.js","mappings":"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","sources":["webpack://@xterm/xterm/./src/common/Async.ts","webpack://@xterm/xterm/./src/common/CircularList.ts","webpack://@xterm/xterm/./src/common/CoreTerminal.ts","webpack://@xterm/xterm/./src/common/Event.ts","webpack://@xterm/xterm/./src/common/InputHandler.ts","webpack://@xterm/xterm/./src/common/Lifecycle.ts","webpack://@xterm/xterm/./src/common/Platform.ts","webpack://@xterm/xterm/./src/common/StringBuilder.ts","webpack://@xterm/xterm/./src/common/TaskQueue.ts","webpack://@xterm/xterm/./src/common/Version.ts","webpack://@xterm/xterm/./src/common/WindowsMode.ts","webpack://@xterm/xterm/./src/common/buffer/AttributeData.ts","webpack://@xterm/xterm/./src/common/buffer/Buffer.ts","webpack://@xterm/xterm/./src/common/buffer/BufferLine.ts","webpack://@xterm/xterm/./src/common/buffer/BufferReflow.ts","webpack://@xterm/xterm/./src/common/buffer/BufferSet.ts","webpack://@xterm/xterm/./src/common/buffer/CellData.ts","webpack://@xterm/xterm/./src/common/buffer/Constants.ts","webpack://@xterm/xterm/./src/common/buffer/Marker.ts","webpack://@xterm/xterm/./src/common/data/Charsets.ts","webpack://@xterm/xterm/./src/common/input/TextDecoder.ts","webpack://@xterm/xterm/./src/common/input/UnicodeV6.ts","webpack://@xterm/xterm/./src/common/input/WriteBuffer.ts","webpack://@xterm/xterm/./src/common/input/XParseColor.ts","webpack://@xterm/xterm/./src/common/parser/ApcParser.ts","webpack://@xterm/xterm/./src/common/parser/DcsParser.ts","webpack://@xterm/xterm/./src/common/parser/EscapeSequenceParser.ts","webpack://@xterm/xterm/./src/common/parser/OscParser.ts","webpack://@xterm/xterm/./src/common/parser/Params.ts","webpack://@xterm/xterm/./src/common/public/AddonManager.ts","webpack://@xterm/xterm/./src/common/public/BufferApiView.ts","webpack://@xterm/xterm/./src/common/public/BufferLineApiView.ts","webpack://@xterm/xterm/./src/common/public/BufferNamespaceApi.ts","webpack://@xterm/xterm/./src/common/public/ParserApi.ts","webpack://@xterm/xterm/./src/common/public/UnicodeApi.ts","webpack://@xterm/xterm/./src/common/services/BufferService.ts","webpack://@xterm/xterm/./src/common/services/CharsetService.ts","webpack://@xterm/xterm/./src/common/services/CoreService.ts","webpack://@xterm/xterm/./src/common/services/InstantiationService.ts","webpack://@xterm/xterm/./src/common/services/LogService.ts","webpack://@xterm/xterm/./src/common/services/MouseStateService.ts","webpack://@xterm/xterm/./src/common/services/OptionsService.ts","webpack://@xterm/xterm/./src/common/services/OscLinkService.ts","webpack://@xterm/xterm/./src/common/services/ServiceRegistry.ts","webpack://@xterm/xterm/./src/common/services/Services.ts","webpack://@xterm/xterm/./src/common/services/UnicodeService.ts","webpack://@xterm/xterm/./src/headless/Terminal.ts","webpack://@xterm/xterm/webpack/bootstrap","webpack://@xterm/xterm/./src/headless/public/Terminal.ts"],"sourcesContent":["/**\n * Copyright (c) 2026 The xterm.js authors. All rights reserved.\n * @license MIT\n *\n * Minimal async helpers for xterm.js core.\n */\n\nimport { DisposableStore, IDisposable, toDisposable } from './Lifecycle';\n\nexport function timeout(millis: number): Promise {\n return new Promise(resolve => setTimeout(resolve, millis));\n}\n\n/**\n * Creates a timeout that can be disposed using its returned value.\n * @param handler The timeout handler.\n * @param timeout An optional timeout in milliseconds.\n * @param store An optional {@link DisposableStore} that will have the timeout disposable managed\n * automatically.\n */\nexport function disposableTimeout(handler: () => void, timeout = 0, store?: DisposableStore): IDisposable {\n const timer = setTimeout(() => {\n handler();\n if (store) {\n disposable.dispose();\n }\n }, timeout);\n const disposable = toDisposable(() => {\n clearTimeout(timer);\n });\n store?.add(disposable);\n return disposable;\n}\n\nexport class TimeoutTimer implements IDisposable {\n private _token: any = -1;\n private _isDisposed = false;\n\n public dispose(): void {\n this.cancel();\n this._isDisposed = true;\n }\n\n public cancel(): void {\n if (this._token !== -1) {\n clearTimeout(this._token);\n this._token = -1;\n }\n }\n\n public cancelAndSet(runner: () => void, timeout: number): void {\n if (this._isDisposed) {\n throw new Error('Calling cancelAndSet on a disposed TimeoutTimer');\n }\n this.cancel();\n this._token = setTimeout(() => {\n this._token = -1;\n runner();\n }, timeout);\n }\n\n public setIfNotSet(runner: () => void, timeout: number): void {\n if (this._isDisposed) {\n throw new Error('Calling setIfNotSet on a disposed TimeoutTimer');\n }\n if (this._token !== -1) {\n return;\n }\n this._token = setTimeout(() => {\n this._token = -1;\n runner();\n }, timeout);\n }\n}\n\n/**\n * Schedules a single runner on the microtask queue. Unlike {@link TimeoutTimer}, a scheduled\n * microtask cannot be unqueued; {@link cancel} prevents the runner from executing if it has not\n * run yet.\n */\nexport class MicrotaskTimer implements IDisposable {\n private _isScheduled = false;\n private _isDisposed = false;\n\n public dispose(): void {\n this.cancel();\n this._isDisposed = true;\n }\n\n public cancel(): void {\n this._isScheduled = false;\n }\n\n public set(runner: () => void): void {\n if (this._isDisposed) {\n throw new Error('Calling set on a disposed MicrotaskTimer');\n }\n if (this._isScheduled) {\n return;\n }\n this._isScheduled = true;\n queueMicrotask(() => {\n if (!this._isScheduled) {\n return;\n }\n this._isScheduled = false;\n runner();\n });\n }\n}\n\nexport class IntervalTimer implements IDisposable {\n private _disposable: IDisposable | undefined;\n private _isDisposed = false;\n\n public cancel(): void {\n this._disposable?.dispose();\n this._disposable = undefined;\n }\n\n public cancelAndSet(runner: () => void, interval: number, context: Window | typeof globalThis = globalThis): void {\n if (this._isDisposed) {\n throw new Error('Calling cancelAndSet on a disposed IntervalTimer');\n }\n this.cancel();\n const handle = context.setInterval(() => {\n runner();\n }, interval);\n this._disposable = {\n dispose: () => {\n context.clearInterval(handle as any);\n this._disposable = undefined;\n }\n };\n }\n\n public dispose(): void {\n this.cancel();\n this._isDisposed = true;\n }\n}\n","/**\n * Copyright (c) 2016 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { Disposable } from './Lifecycle';\nimport { Emitter, type IEvent } from './Event';\n\nexport interface IInsertEvent {\n index: number;\n amount: number;\n}\n\nexport interface IDeleteEvent {\n index: number;\n amount: number;\n}\n\nexport interface ICircularList {\n length: number;\n maxLength: number;\n isFull: boolean;\n\n onDeleteEmitter: Emitter;\n onDelete: IEvent;\n onInsertEmitter: Emitter;\n onInsert: IEvent;\n onTrimEmitter: Emitter;\n onTrim: IEvent;\n\n get(index: number): T | undefined;\n set(index: number, value: T): void;\n push(value: T): void;\n recycle(): T;\n pop(): T | undefined;\n splice(start: number, deleteCount: number, ...items: T[]): void;\n trimStart(count: number): void;\n shiftElements(start: number, count: number, offset: number): void;\n}\n\n/**\n * Represents a circular list; a list with a maximum size that wraps around when push is called,\n * overriding values at the start of the list.\n */\nexport class CircularList extends Disposable implements ICircularList {\n protected _array: (T | undefined)[];\n private _startIndex: number;\n private _length: number;\n\n public readonly onDeleteEmitter = this._register(new Emitter());\n public readonly onDelete = this.onDeleteEmitter.event;\n public readonly onInsertEmitter = this._register(new Emitter());\n public readonly onInsert = this.onInsertEmitter.event;\n public readonly onTrimEmitter = this._register(new Emitter());\n public readonly onTrim = this.onTrimEmitter.event;\n\n constructor(\n private _maxLength: number\n ) {\n super();\n this._array = new Array(this._maxLength);\n this._startIndex = 0;\n this._length = 0;\n }\n\n public get maxLength(): number {\n return this._maxLength;\n }\n\n public set maxLength(newMaxLength: number) {\n // There was no change in maxLength, return early.\n if (this._maxLength === newMaxLength) {\n return;\n }\n\n // Reconstruct array, starting at index 0. Only transfer values from the\n // indexes 0 to length.\n const newArray = new Array(newMaxLength);\n for (let i = 0; i < Math.min(newMaxLength, this.length); i++) {\n newArray[i] = this._array[this._getCyclicIndex(i)];\n }\n this._array = newArray;\n this._maxLength = newMaxLength;\n this._startIndex = 0;\n }\n\n public get length(): number {\n return this._length;\n }\n\n public set length(newLength: number) {\n if (newLength > this._length) {\n for (let i = this._length; i < newLength; i++) {\n this._array[i] = undefined;\n }\n }\n this._length = newLength;\n }\n\n /**\n * Gets the value at an index.\n *\n * Note that for performance reasons there is no bounds checking here, the index reference is\n * circular so this should always return a value and never throw.\n * @param index The index of the value to get.\n * @returns The value corresponding to the index.\n */\n public get(index: number): T | undefined {\n return this._array[this._getCyclicIndex(index)];\n }\n\n /**\n * Sets the value at an index.\n *\n * Note that for performance reasons there is no bounds checking here, the index reference is\n * circular so this should always return a value and never throw.\n * @param index The index to set.\n * @param value The value to set.\n */\n public set(index: number, value: T | undefined): void {\n this._array[this._getCyclicIndex(index)] = value;\n }\n\n /**\n * Pushes a new value onto the list, wrapping around to the start of the array, overriding index 0\n * if the maximum length is reached.\n * @param value The value to push onto the list.\n */\n public push(value: T): void {\n this._array[this._getCyclicIndex(this._length)] = value;\n if (this._length === this._maxLength) {\n this._startIndex = ++this._startIndex % this._maxLength;\n this.onTrimEmitter.fire(1);\n } else {\n this._length++;\n }\n }\n\n /**\n * Advance ringbuffer index and return current element for recycling.\n * Note: The buffer must be full for this method to work.\n * @throws When the buffer is not full.\n */\n public recycle(): T {\n if (this._length !== this._maxLength) {\n throw new Error('Can only recycle when the buffer is full');\n }\n this._startIndex = ++this._startIndex % this._maxLength;\n this.onTrimEmitter.fire(1);\n return this._array[this._getCyclicIndex(this._length - 1)]!;\n }\n\n /**\n * Ringbuffer is at max length.\n */\n public get isFull(): boolean {\n return this._length === this._maxLength;\n }\n\n /**\n * Removes and returns the last value on the list.\n * @returns The popped value.\n */\n public pop(): T | undefined {\n return this._array[this._getCyclicIndex(this._length-- - 1)];\n }\n\n /**\n * Deletes and/or inserts items at a particular index (in that order). Unlike\n * Array.prototype.splice, this operation does not return the deleted items as a new array in\n * order to save creating a new array. Note that this operation may shift all values in the list\n * in the worst case.\n * @param start The index to delete and/or insert.\n * @param deleteCount The number of elements to delete.\n * @param items The items to insert.\n */\n public splice(start: number, deleteCount: number, ...items: T[]): void {\n // Delete items\n if (deleteCount) {\n for (let i = start; i < this._length - deleteCount; i++) {\n this._array[this._getCyclicIndex(i)] = this._array[this._getCyclicIndex(i + deleteCount)];\n }\n this._length -= deleteCount;\n this.onDeleteEmitter.fire({ index: start, amount: deleteCount });\n }\n\n // Add items\n for (let i = this._length - 1; i >= start; i--) {\n this._array[this._getCyclicIndex(i + items.length)] = this._array[this._getCyclicIndex(i)];\n }\n for (let i = 0; i < items.length; i++) {\n this._array[this._getCyclicIndex(start + i)] = items[i];\n }\n if (items.length) {\n this.onInsertEmitter.fire({ index: start, amount: items.length });\n }\n\n // Adjust length as needed\n if (this._length + items.length > this._maxLength) {\n const countToTrim = (this._length + items.length) - this._maxLength;\n this._startIndex += countToTrim;\n this._length = this._maxLength;\n this.onTrimEmitter.fire(countToTrim);\n } else {\n this._length += items.length;\n }\n }\n\n /**\n * Trims a number of items from the start of the list.\n * @param count The number of items to remove.\n */\n public trimStart(count: number): void {\n if (count > this._length) {\n count = this._length;\n }\n this._startIndex += count;\n this._length -= count;\n this.onTrimEmitter.fire(count);\n }\n\n public shiftElements(start: number, count: number, offset: number): void {\n if (count <= 0) {\n return;\n }\n if (start < 0 || start >= this._length) {\n throw new Error('start argument out of range');\n }\n if (start + offset < 0) {\n throw new Error('Cannot shift elements in list beyond index 0');\n }\n\n if (offset > 0) {\n for (let i = count - 1; i >= 0; i--) {\n this.set(start + i + offset, this.get(start + i));\n }\n const expandListBy = (start + count + offset) - this._length;\n if (expandListBy > 0) {\n this._length += expandListBy;\n while (this._length > this._maxLength) {\n this._length--;\n this._startIndex++;\n this.onTrimEmitter.fire(1);\n }\n }\n } else {\n for (let i = 0; i < count; i++) {\n this.set(start + i + offset, this.get(start + i));\n }\n }\n }\n\n /**\n * Gets the cyclic index for the specified regular index. The cyclic index can then be used on the\n * backing array to get the element associated with the regular index.\n * @param index The regular index.\n * @returns The cyclic index.\n */\n private _getCyclicIndex(index: number): number {\n return (this._startIndex + index) % this._maxLength;\n }\n}\n","/**\n * Copyright (c) 2014-2020 The xterm.js authors. All rights reserved.\n * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)\n * @license MIT\n *\n * Originally forked from (with the author's permission):\n * Fabrice Bellard's javascript vt100 for jslinux:\n * http://bellard.org/jslinux/\n * Copyright (c) 2011 Fabrice Bellard\n * The original design remains. The terminal itself\n * has been extended to include xterm CSI codes, among\n * other features.\n *\n * Terminal Emulation References:\n * http://vt100.net/\n * http://invisible-island.net/xterm/ctlseqs/ctlseqs.txt\n * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html\n * http://invisible-island.net/vttest/\n * http://www.inwap.com/pdp10/ansicode.txt\n * http://linux.die.net/man/4/console_codes\n * http://linux.die.net/man/7/urxvt\n */\n\nimport { IInstantiationService, IOptionsService, IBufferService, ILogService, ICharsetService, ICoreService, IMouseStateService, IUnicodeService, LogLevelEnum, IOscLinkService } from './services/Services';\nimport { InstantiationService } from './services/InstantiationService';\nimport { LogService } from './services/LogService';\nimport { BufferService, BufferServiceConstants } from './services/BufferService';\nimport { OptionsService } from './services/OptionsService';\nimport { IDisposable, IScrollEvent, ITerminalOptions, IParams } from './Types';\nimport { IAttributeData, IBufferSet } from './buffer/Types';\nimport { CoreService } from './services/CoreService';\nimport { MouseStateService } from './services/MouseStateService';\nimport { UnicodeV6 } from './input/UnicodeV6';\nimport { UnicodeService } from './services/UnicodeService';\nimport { CharsetService } from './services/CharsetService';\nimport { updateWindowsModeWrappedState } from './WindowsMode';\nimport { IFunctionIdentifier } from './parser/Types';\nimport { InputHandler } from './InputHandler';\nimport { WriteBuffer } from './input/WriteBuffer';\nimport { OscLinkService } from './services/OscLinkService';\nimport { Emitter, EventUtils, type IEvent } from './Event';\nimport { Disposable, MutableDisposable, toDisposable } from './Lifecycle';\n\n// Only trigger this warning a single time per session\nlet hasWriteSyncWarnHappened = false;\n\nexport interface ICoreTerminal {\n mouseStateService: IMouseStateService;\n coreService: ICoreService;\n optionsService: IOptionsService;\n unicodeService: IUnicodeService;\n buffers: IBufferSet;\n options: Required;\n registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise): IDisposable;\n registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise): IDisposable;\n registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise): IDisposable;\n registerOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable;\n registerApcHandler(id: IFunctionIdentifier, callback: (data: string) => boolean | Promise): IDisposable;\n}\n\nexport abstract class CoreTerminal extends Disposable implements ICoreTerminal {\n protected readonly _instantiationService: IInstantiationService;\n protected readonly _bufferService: IBufferService;\n protected readonly _logService: ILogService;\n protected readonly _charsetService: ICharsetService;\n protected readonly _oscLinkService: IOscLinkService;\n\n public readonly mouseStateService: IMouseStateService;\n public readonly coreService: ICoreService;\n public readonly unicodeService: IUnicodeService;\n public readonly optionsService: IOptionsService;\n\n protected _inputHandler: InputHandler;\n private _writeBuffer: WriteBuffer;\n private _windowsWrappingHeuristics = this._register(new MutableDisposable());\n\n private readonly _onBinary = this._register(new Emitter());\n public readonly onBinary = this._onBinary.event;\n private readonly _onData = this._register(new Emitter());\n public readonly onData = this._onData.event;\n protected _onLineFeed = this._register(new Emitter());\n public readonly onLineFeed = this._onLineFeed.event;\n protected readonly _onRender = this._register(new Emitter<{ start: number, end: number }>());\n public readonly onRender = this._onRender.event;\n private readonly _onResize = this._register(new Emitter<{ cols: number, rows: number }>());\n public readonly onResize = this._onResize.event;\n protected readonly _onWriteParsed = this._register(new Emitter());\n public readonly onWriteParsed = this._onWriteParsed.event;\n\n /**\n * Internally we track the source of the scroll but this is meaningless outside the library so\n * it's filtered out.\n */\n protected _onScrollApi?: Emitter;\n protected _onScroll = this._register(new Emitter());\n public get onScroll(): IEvent {\n if (!this._onScrollApi) {\n this._onScrollApi = this._register(new Emitter());\n this._onScroll.event(ev => {\n this._onScrollApi?.fire(ev.position);\n });\n }\n return this._onScrollApi.event;\n }\n\n public get cols(): number { return this._bufferService.cols; }\n public get rows(): number { return this._bufferService.rows; }\n public get buffers(): IBufferSet { return this._bufferService.buffers; }\n public get options(): Required { return this.optionsService.options; }\n public set options(options: ITerminalOptions) {\n for (const key in options) {\n this.optionsService.options[key] = options[key];\n }\n }\n\n constructor(\n options: Partial\n ) {\n super();\n\n // Setup and initialize services\n this._instantiationService = new InstantiationService();\n this.optionsService = this._register(new OptionsService(options));\n this._instantiationService.setService(IOptionsService, this.optionsService);\n this._logService = this._register(this._instantiationService.createInstance(LogService));\n this._instantiationService.setService(ILogService, this._logService);\n this._bufferService = this._register(this._instantiationService.createInstance(BufferService));\n this._instantiationService.setService(IBufferService, this._bufferService);\n this.coreService = this._register(this._instantiationService.createInstance(CoreService));\n this._instantiationService.setService(ICoreService, this.coreService);\n this.mouseStateService = this._register(this._instantiationService.createInstance(MouseStateService));\n this._instantiationService.setService(IMouseStateService, this.mouseStateService);\n this.unicodeService = this._register(this._instantiationService.createInstance(UnicodeService));\n this.unicodeService.register(new UnicodeV6());\n this._instantiationService.setService(IUnicodeService, this.unicodeService);\n this._charsetService = this._instantiationService.createInstance(CharsetService);\n this._instantiationService.setService(ICharsetService, this._charsetService);\n this._oscLinkService = this._instantiationService.createInstance(OscLinkService);\n this._instantiationService.setService(IOscLinkService, this._oscLinkService);\n\n\n // Register input handler and handle/forward events\n this._inputHandler = this._register(new InputHandler(this._bufferService, this._charsetService, this.coreService, this._logService, this.optionsService, this._oscLinkService, this.mouseStateService, this.unicodeService));\n this._register(EventUtils.forward(this._inputHandler.onLineFeed, this._onLineFeed));\n\n // Setup listeners\n this._register(EventUtils.forward(this._bufferService.onResize, this._onResize));\n this._register(EventUtils.forward(this.coreService.onData, this._onData));\n this._register(EventUtils.forward(this.coreService.onBinary, this._onBinary));\n this._register(this.coreService.onRequestScrollToBottom(() => this.scrollToBottom(true)));\n this._register(this.coreService.onUserInput(() => this._writeBuffer.handleUserInput()));\n this._register(this.optionsService.onMultipleOptionChange(['windowsPty'], () => this._handleWindowsPtyOptionChange()));\n this._register(this._bufferService.onScroll(() => {\n this._onScroll.fire({ position: this._bufferService.buffer.ydisp });\n this._inputHandler.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom);\n }));\n // Setup WriteBuffer\n this._writeBuffer = this._register(new WriteBuffer((data, promiseResult) => this._inputHandler.parse(data, promiseResult)));\n this._register(EventUtils.forward(this._writeBuffer.onWriteParsed, this._onWriteParsed));\n }\n\n public write(data: string | Uint8Array, callback?: () => void): void {\n this._writeBuffer.write(data, callback);\n }\n\n /**\n * Write data to terminal synchonously.\n *\n * This method is unreliable with async parser handlers, thus should not\n * be used anymore. If you need blocking semantics on data input consider\n * `write` with a callback instead.\n *\n * @deprecated Unreliable, will be removed soon.\n */\n public writeSync(data: string | Uint8Array, maxSubsequentCalls?: number): void {\n if (this._logService.logLevel <= LogLevelEnum.WARN && !hasWriteSyncWarnHappened) {\n this._logService.warn('writeSync is unreliable and will be removed soon.');\n hasWriteSyncWarnHappened = true;\n }\n this._writeBuffer.writeSync(data, maxSubsequentCalls);\n }\n\n public input(data: string, wasUserInput: boolean = true): void {\n this.coreService.triggerDataEvent(data, wasUserInput);\n }\n\n public resize(x: number, y: number): void {\n if (isNaN(x) || isNaN(y)) {\n return;\n }\n\n x = Math.max(x, BufferServiceConstants.MINIMUM_COLS);\n y = Math.max(y, BufferServiceConstants.MINIMUM_ROWS);\n\n // Flush pending writes before resize to avoid race conditions where async\n // writes are processed with incorrect dimensions\n this._writeBuffer.flushSync();\n\n this._bufferService.resize(x, y);\n }\n\n /**\n * Scroll the terminal down 1 row, creating a blank line.\n * @param eraseAttr The attribute data to use the for blank line.\n * @param isWrapped Whether the new line is wrapped from the previous line.\n */\n public scroll(eraseAttr: IAttributeData, isWrapped: boolean = false): void {\n this._bufferService.scroll(eraseAttr, isWrapped);\n }\n\n /**\n * Scroll the display of the terminal\n * @param disp The number of lines to scroll down (negative scroll up).\n * @param suppressScrollEvent Don't emit the scroll event as scrollLines. This is used to avoid\n * unwanted events being handled by the viewport when the event was triggered from the viewport\n * originally.\n */\n public scrollLines(disp: number, suppressScrollEvent?: boolean): void {\n this._bufferService.scrollLines(disp, suppressScrollEvent);\n }\n\n public scrollPages(pageCount: number): void {\n this.scrollLines(pageCount * (this.rows - 1));\n }\n\n public scrollToTop(): void {\n this.scrollLines(-this._bufferService.buffer.ydisp);\n }\n\n public scrollToBottom(disableSmoothScroll?: boolean): void {\n this.scrollLines(this._bufferService.buffer.ybase - this._bufferService.buffer.ydisp);\n }\n\n public scrollToLine(line: number): void {\n const scrollAmount = line - this._bufferService.buffer.ydisp;\n if (scrollAmount !== 0) {\n this.scrollLines(scrollAmount);\n }\n }\n\n /** Add handler for ESC escape sequence. See xterm.d.ts for details. */\n public registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise): IDisposable {\n return this._inputHandler.registerEscHandler(id, callback);\n }\n\n /** Add handler for DCS escape sequence. See xterm.d.ts for details. */\n public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise): IDisposable {\n return this._inputHandler.registerDcsHandler(id, callback);\n }\n\n /** Add handler for CSI escape sequence. See xterm.d.ts for details. */\n public registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise): IDisposable {\n return this._inputHandler.registerCsiHandler(id, callback);\n }\n\n /** Add handler for OSC escape sequence. See xterm.d.ts for details. */\n public registerOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable {\n return this._inputHandler.registerOscHandler(ident, callback);\n }\n\n /** Add handler for APC escape sequence. See xterm.d.ts for details. */\n public registerApcHandler(id: IFunctionIdentifier, callback: (data: string) => boolean | Promise): IDisposable {\n return this._inputHandler.registerApcHandler(id, callback);\n }\n\n protected _setup(): void {\n this._handleWindowsPtyOptionChange();\n }\n\n public reset(): void {\n this._inputHandler.reset();\n this._bufferService.reset();\n this._charsetService.reset();\n this.coreService.reset();\n this.mouseStateService.reset();\n }\n\n\n private _handleWindowsPtyOptionChange(): void {\n let value = false;\n const windowsPty = this.optionsService.rawOptions.windowsPty;\n if (windowsPty && windowsPty.backend !== undefined && windowsPty.buildNumber !== undefined) {\n value = !!(windowsPty.backend === 'conpty' && windowsPty.buildNumber < 21376);\n }\n if (value) {\n this._enableWindowsWrappingHeuristics();\n } else {\n this._windowsWrappingHeuristics.clear();\n }\n }\n\n protected _enableWindowsWrappingHeuristics(): void {\n if (!this._windowsWrappingHeuristics.value) {\n const disposables: IDisposable[] = [];\n disposables.push(this.onLineFeed(updateWindowsModeWrappedState.bind(null, this._bufferService)));\n disposables.push(this.registerCsiHandler({ final: 'H' }, () => {\n updateWindowsModeWrappedState(this._bufferService);\n return false;\n }));\n this._windowsWrappingHeuristics.value = toDisposable(() => {\n for (const d of disposables) {\n d.dispose();\n }\n });\n }\n }\n}\n","/**\n * Copyright (c) 2024-2026 The xterm.js authors. All rights reserved.\n * @license MIT\n *\n * Minimal event utilities for xterm.js core.\n * Simplified from VS Code's event.ts - no leak detection/profiling.\n */\n\nimport { IDisposable, DisposableStore, toDisposable } from './Lifecycle';\n\nexport interface IEvent {\n (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore): IDisposable;\n}\n\nexport class Emitter {\n private _listeners: { fn: (e: T) => any, thisArgs: any }[] = [];\n private _disposed = false;\n private _event: IEvent | undefined;\n\n public get event(): IEvent {\n if (this._event) {\n return this._event;\n }\n this._event = (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => {\n if (this._disposed) {\n return toDisposable(() => {});\n }\n\n const entry = { fn: listener, thisArgs };\n this._listeners = this._listeners.slice();\n this._listeners.push(entry);\n\n const result = toDisposable(() => {\n const idx = this._listeners.indexOf(entry);\n if (idx !== -1) {\n this._listeners = this._listeners.slice();\n this._listeners.splice(idx, 1);\n }\n });\n\n if (disposables) {\n if (Array.isArray(disposables)) {\n disposables.push(result);\n } else {\n disposables.add(result);\n }\n }\n\n return result;\n };\n return this._event;\n }\n\n public fire(event: T): void {\n if (this._disposed || !this._listeners.length) {\n return;\n }\n if (this._listeners.length === 1) {\n this._listeners[0].fn.call(this._listeners[0].thisArgs, event);\n return;\n }\n const listeners = this._listeners;\n for (let i = 0, len = listeners.length; i < len; ++i) {\n listeners[i].fn.call(listeners[i].thisArgs, event);\n }\n }\n\n public dispose(): void {\n if (this._disposed) {\n return;\n }\n this._disposed = true;\n this._listeners.length = 0;\n }\n}\n\nexport namespace EventUtils {\n export function forward(from: IEvent, to: Emitter): IDisposable {\n return from(e => to.fire(e));\n }\n\n export function map(event: IEvent, map: (i: I) => O): IEvent {\n return (listener: (e: O) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => {\n return event(i => listener.call(thisArgs, map(i)), undefined, disposables);\n };\n }\n\n export function any(...events: IEvent[]): IEvent;\n export function any(...events: IEvent[]): IEvent;\n export function any(...events: IEvent[]): IEvent {\n return (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => {\n const store = new DisposableStore();\n for (const event of events) {\n store.add(event(e => listener.call(thisArgs, e)));\n }\n if (disposables) {\n if (Array.isArray(disposables)) {\n disposables.push(store);\n } else {\n disposables.add(store);\n }\n }\n return store;\n };\n }\n\n export function runAndSubscribe(event: IEvent, handler: (e: T) => void, initial: T): IDisposable;\n export function runAndSubscribe(event: IEvent, handler: (e: T | undefined) => void): IDisposable;\n export function runAndSubscribe(event: IEvent, handler: (e: T | undefined) => void, initial?: T): IDisposable {\n handler(initial);\n return event(e => handler(e));\n }\n}\n","/**\n * Copyright (c) 2014 The xterm.js authors. All rights reserved.\n * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)\n * @license MIT\n */\n\nimport { IInputHandler, IDisposable, IWindowOptions, IColorEvent, IParseStack, ColorIndex, ColorRequestType, SpecialColorIndex } from './Types';\nimport { IAttributeData, IBuffer } from './buffer/Types';\nimport { C0, C1 } from './data/EscapeSequences';\nimport { CHARSETS, DEFAULT_CHARSET } from './data/Charsets';\nimport { EscapeSequenceParser } from './parser/EscapeSequenceParser';\nimport { Disposable } from './Lifecycle';\nimport { StringToUtf32, stringFromCodePoint, Utf8ToUtf32 } from './input/TextDecoder';\nimport { BufferLine, DEFAULT_ATTR_DATA } from './buffer/BufferLine';\nimport { IParsingState, IEscapeSequenceParser, IParams, IFunctionIdentifier } from './parser/Types';\nimport { NULL_CELL_CODE, NULL_CELL_WIDTH, Attributes, FgFlags, BgFlags, Content, UnderlineStyle } from './buffer/Constants';\nimport { CellData } from './buffer/CellData';\nimport { AttributeData } from './buffer/AttributeData';\nimport { ICoreService, IBufferService, IOptionsService, ILogService, IMouseStateService, ICharsetService, IUnicodeService, LogLevelEnum, IOscLinkService } from './services/Services';\nimport { UnicodeService } from './services/UnicodeService';\nimport { OscHandler } from './parser/OscParser';\nimport { DcsHandler } from './parser/DcsParser';\nimport { ApcHandler } from './parser/ApcParser';\nimport { parseColor } from './input/XParseColor';\nimport { Emitter } from './Event';\nimport { XTERM_VERSION } from './Version';\n\n/**\n * Map collect to glevel. Used in `selectCharset`.\n */\nconst GLEVEL: { [key: string]: number } = { '(': 0, ')': 1, '*': 2, '+': 3, '-': 1, '.': 2 };\n\n/**\n * Document xterm VT features here that are currently unsupported\n */\n// @vt: #N DCS DECUDK \"User Defined Keys\" \"DCS Ps ; Ps \\| Pt ST\" \"Definitions for user-defined keys.\"\n// @vt: #N DCS XTGETTCAP \"Request Terminfo String\" \"DCS + q Pt ST\" \"Request Terminfo String.\"\n// @vt: #N DCS XTSETTCAP \"Set Terminfo Data\" \"DCS + p Pt ST\" \"Set Terminfo Data.\"\n// @vt: #N OSC 1 \"Set Icon Name\" \"OSC 1 ; Pt BEL\" \"Set icon name.\"\n\n/**\n * Max length of the UTF32 input buffer. Real memory consumption is 4 times higher.\n */\nconst enum Constants {\n MAX_PARSEBUFFER_LENGTH = 131072,\n /** Limit length of title and icon name stacks. */\n STACK_LIMIT = 10,\n // create a warning log if an async handler takes longer than the limit (in ms)\n SLOW_ASYNC_LIMIT = 5000\n}\n\n// map params to window option\nfunction paramToWindowOption(n: number, opts: IWindowOptions): boolean {\n if (n > 24) {\n return opts.setWinLines || false;\n }\n switch (n) {\n case 1: return !!opts.restoreWin;\n case 2: return !!opts.minimizeWin;\n case 3: return !!opts.setWinPosition;\n case 4: return !!opts.setWinSizePixels;\n case 5: return !!opts.raiseWin;\n case 6: return !!opts.lowerWin;\n case 7: return !!opts.refreshWin;\n case 8: return !!opts.setWinSizeChars;\n case 9: return !!opts.maximizeWin;\n case 10: return !!opts.fullscreenWin;\n case 11: return !!opts.getWinState;\n case 13: return !!opts.getWinPosition;\n case 14: return !!opts.getWinSizePixels;\n case 15: return !!opts.getScreenSizePixels;\n case 16: return !!opts.getCellSizePixels;\n case 18: return !!opts.getWinSizeChars;\n case 19: return !!opts.getScreenSizeChars;\n case 20: return !!opts.getIconTitle;\n case 21: return !!opts.getWinTitle;\n case 22: return !!opts.pushTitle;\n case 23: return !!opts.popTitle;\n case 24: return !!opts.setWinLines;\n }\n return false;\n}\n\nexport enum WindowsOptionsReportType {\n GET_WIN_SIZE_PIXELS = 0,\n GET_CELL_SIZE_PIXELS = 1\n}\n\n// Work variables to avoid garbage collection\nlet $temp = 0;\n\n/**\n * The terminal's standard implementation of IInputHandler, this handles all\n * input from the Parser.\n *\n * Refer to http://invisible-island.net/xterm/ctlseqs/ctlseqs.html to understand\n * each function's header comment.\n */\nexport class InputHandler extends Disposable implements IInputHandler {\n private _parseBuffer: Uint32Array = new Uint32Array(4096);\n private _stringDecoder: StringToUtf32 = new StringToUtf32();\n private _utf8Decoder: Utf8ToUtf32 = new Utf8ToUtf32();\n private _windowTitle = '';\n private _iconName = '';\n private _dirtyRowTracker: IDirtyRowTracker;\n protected _windowTitleStack: string[] = [];\n protected _iconNameStack: string[] = [];\n\n private _curAttrData: IAttributeData = DEFAULT_ATTR_DATA.clone();\n public getAttrData(): IAttributeData { return this._curAttrData; }\n private _eraseAttrDataInternal: IAttributeData = DEFAULT_ATTR_DATA.clone();\n\n private _activeBuffer: IBuffer;\n\n private readonly _onRequestBell = this._register(new Emitter());\n public readonly onRequestBell = this._onRequestBell.event;\n private readonly _onRequestRefreshRows = this._register(new Emitter<{ start: number, end: number } | undefined>());\n public readonly onRequestRefreshRows = this._onRequestRefreshRows.event;\n private readonly _onRequestReset = this._register(new Emitter());\n public readonly onRequestReset = this._onRequestReset.event;\n private readonly _onRequestSendFocus = this._register(new Emitter());\n public readonly onRequestSendFocus = this._onRequestSendFocus.event;\n private readonly _onRequestSyncScrollBar = this._register(new Emitter());\n public readonly onRequestSyncScrollBar = this._onRequestSyncScrollBar.event;\n private readonly _onRequestWindowsOptionsReport = this._register(new Emitter());\n public readonly onRequestWindowsOptionsReport = this._onRequestWindowsOptionsReport.event;\n\n private readonly _onA11yChar = this._register(new Emitter());\n public readonly onA11yChar = this._onA11yChar.event;\n private readonly _onA11yTab = this._register(new Emitter());\n public readonly onA11yTab = this._onA11yTab.event;\n private readonly _onCursorMove = this._register(new Emitter());\n public readonly onCursorMove = this._onCursorMove.event;\n private readonly _onLineFeed = this._register(new Emitter());\n public readonly onLineFeed = this._onLineFeed.event;\n private readonly _onScroll = this._register(new Emitter());\n public readonly onScroll = this._onScroll.event;\n private readonly _onTitleChange = this._register(new Emitter());\n public readonly onTitleChange = this._onTitleChange.event;\n private readonly _onColor = this._register(new Emitter());\n public readonly onColor = this._onColor.event;\n private readonly _onRequestColorSchemeQuery = this._register(new Emitter());\n public readonly onRequestColorSchemeQuery = this._onRequestColorSchemeQuery.event;\n\n private _parseStack: IParseStack = {\n paused: false,\n cursorStartX: 0,\n cursorStartY: 0,\n decodedLength: 0,\n position: 0\n };\n\n constructor(\n private readonly _bufferService: IBufferService,\n private readonly _charsetService: ICharsetService,\n private readonly _coreService: ICoreService,\n private readonly _logService: ILogService,\n private readonly _optionsService: IOptionsService,\n private readonly _oscLinkService: IOscLinkService,\n private readonly _mouseStateService: IMouseStateService,\n private readonly _unicodeService: IUnicodeService,\n private readonly _parser: IEscapeSequenceParser = new EscapeSequenceParser()\n ) {\n super();\n this._register(this._parser);\n this._dirtyRowTracker = new DirtyRowTracker(this._bufferService);\n\n // Track properties used in performance critical code manually to avoid using slow getters\n this._activeBuffer = this._bufferService.buffer;\n this._register(this._bufferService.buffers.onBufferActivate(e => this._activeBuffer = e.activeBuffer));\n\n /**\n * custom fallback handlers\n */\n this._parser.setCsiHandlerFallback((ident, params) => {\n this._logService.debug('Unknown CSI code: ', { identifier: this._parser.identToString(ident), params: params.toArray() });\n });\n this._parser.setEscHandlerFallback(ident => {\n this._logService.debug('Unknown ESC code: ', { identifier: this._parser.identToString(ident) });\n });\n this._parser.setExecuteHandlerFallback(code => {\n this._logService.debug('Unknown EXECUTE code: ', { code });\n });\n this._parser.setOscHandlerFallback((identifier, action, data) => {\n this._logService.debug('Unknown OSC code: ', { identifier, action, data });\n });\n this._parser.setDcsHandlerFallback((ident, action, payload) => {\n if (action === 'HOOK') {\n payload = payload.toArray();\n }\n this._logService.debug('Unknown DCS code: ', { identifier: this._parser.identToString(ident), action, payload });\n });\n this._parser.setApcHandlerFallback((ident, action, payload) => {\n this._logService.debug('Unknown APC code: ', { identifier: this._parser.identToString(ident), action, payload });\n });\n\n /**\n * print handler\n */\n this._parser.setPrintHandler((data, start, end) => this.print(data, start, end));\n\n /**\n * CSI handler\n */\n this._parser.registerCsiHandler({ final: '@' }, params => this.insertChars(params));\n this._parser.registerCsiHandler({ intermediates: ' ', final: '@' }, params => this.scrollLeft(params));\n this._parser.registerCsiHandler({ final: 'A' }, params => this.cursorUp(params));\n this._parser.registerCsiHandler({ intermediates: ' ', final: 'A' }, params => this.scrollRight(params));\n this._parser.registerCsiHandler({ final: 'B' }, params => this.cursorDown(params));\n this._parser.registerCsiHandler({ final: 'C' }, params => this.cursorForward(params));\n this._parser.registerCsiHandler({ final: 'D' }, params => this.cursorBackward(params));\n this._parser.registerCsiHandler({ final: 'E' }, params => this.cursorNextLine(params));\n this._parser.registerCsiHandler({ final: 'F' }, params => this.cursorPrecedingLine(params));\n this._parser.registerCsiHandler({ final: 'G' }, params => this.cursorCharAbsolute(params));\n this._parser.registerCsiHandler({ final: 'H' }, params => this.cursorPosition(params));\n this._parser.registerCsiHandler({ final: 'I' }, params => this.cursorForwardTab(params));\n this._parser.registerCsiHandler({ final: 'J' }, params => this.eraseInDisplay(params, false));\n this._parser.registerCsiHandler({ prefix: '?', final: 'J' }, params => this.eraseInDisplay(params, true));\n this._parser.registerCsiHandler({ final: 'K' }, params => this.eraseInLine(params, false));\n this._parser.registerCsiHandler({ prefix: '?', final: 'K' }, params => this.eraseInLine(params, true));\n this._parser.registerCsiHandler({ final: 'L' }, params => this.insertLines(params));\n this._parser.registerCsiHandler({ final: 'M' }, params => this.deleteLines(params));\n this._parser.registerCsiHandler({ final: 'P' }, params => this.deleteChars(params));\n this._parser.registerCsiHandler({ final: 'S' }, params => this.scrollUp(params));\n this._parser.registerCsiHandler({ final: 'T' }, params => this.scrollDown(params));\n this._parser.registerCsiHandler({ final: 'X' }, params => this.eraseChars(params));\n this._parser.registerCsiHandler({ final: 'Z' }, params => this.cursorBackwardTab(params));\n this._parser.registerCsiHandler({ final: '^' }, params => this.scrollDown(params));\n this._parser.registerCsiHandler({ final: '`' }, params => this.charPosAbsolute(params));\n this._parser.registerCsiHandler({ final: 'a' }, params => this.hPositionRelative(params));\n this._parser.registerCsiHandler({ final: 'b' }, params => this.repeatPrecedingCharacter(params));\n this._parser.registerCsiHandler({ final: 'c' }, params => this.sendDeviceAttributesPrimary(params));\n this._parser.registerCsiHandler({ prefix: '>', final: 'c' }, params => this.sendDeviceAttributesSecondary(params));\n this._parser.registerCsiHandler({ final: 'd' }, params => this.linePosAbsolute(params));\n this._parser.registerCsiHandler({ final: 'e' }, params => this.vPositionRelative(params));\n this._parser.registerCsiHandler({ final: 'f' }, params => this.hVPosition(params));\n this._parser.registerCsiHandler({ final: 'g' }, params => this.tabClear(params));\n this._parser.registerCsiHandler({ final: 'h' }, params => this.setMode(params));\n this._parser.registerCsiHandler({ prefix: '?', final: 'h' }, params => this.setModePrivate(params));\n this._parser.registerCsiHandler({ final: 'l' }, params => this.resetMode(params));\n this._parser.registerCsiHandler({ prefix: '?', final: 'l' }, params => this.resetModePrivate(params));\n this._parser.registerCsiHandler({ final: 'm' }, params => this.charAttributes(params));\n this._parser.registerCsiHandler({ final: 'n' }, params => this.deviceStatus(params));\n this._parser.registerCsiHandler({ prefix: '?', final: 'n' }, params => this.deviceStatusPrivate(params));\n this._parser.registerCsiHandler({ intermediates: '!', final: 'p' }, params => this.softReset(params));\n this._parser.registerCsiHandler({ prefix: '>', final: 'q' }, params => this.sendXtVersion(params));\n this._parser.registerCsiHandler({ intermediates: ' ', final: 'q' }, params => this.setCursorStyle(params));\n this._parser.registerCsiHandler({ final: 'r' }, params => this.setScrollRegion(params));\n this._parser.registerCsiHandler({ final: 's' }, params => this.saveCursor(params));\n this._parser.registerCsiHandler({ final: 't' }, params => this.windowOptions(params));\n this._parser.registerCsiHandler({ final: 'u' }, params => this.restoreCursor(params));\n this._parser.registerCsiHandler({ intermediates: '\\'', final: '}' }, params => this.insertColumns(params));\n this._parser.registerCsiHandler({ intermediates: '\\'', final: '~' }, params => this.deleteColumns(params));\n this._parser.registerCsiHandler({ intermediates: '\"', final: 'q' }, params => this.selectProtected(params));\n this._parser.registerCsiHandler({ intermediates: '$', final: 'p' }, params => this.requestMode(params, true));\n this._parser.registerCsiHandler({ prefix: '?', intermediates: '$', final: 'p' }, params => this.requestMode(params, false));\n\n // Kitty keyboard protocol handlers\n this._parser.registerCsiHandler({ prefix: '=', final: 'u' }, params => this.kittyKeyboardSet(params));\n this._parser.registerCsiHandler({ prefix: '?', final: 'u' }, params => this.kittyKeyboardQuery(params));\n this._parser.registerCsiHandler({ prefix: '>', final: 'u' }, params => this.kittyKeyboardPush(params));\n this._parser.registerCsiHandler({ prefix: '<', final: 'u' }, params => this.kittyKeyboardPop(params));\n\n /**\n * execute handler\n */\n this._parser.setExecuteHandler(C0.BEL, () => this.bell());\n this._parser.setExecuteHandler(C0.LF, () => this.lineFeed());\n this._parser.setExecuteHandler(C0.VT, () => this.lineFeed());\n this._parser.setExecuteHandler(C0.FF, () => this.lineFeed());\n this._parser.setExecuteHandler(C0.CR, () => this.carriageReturn());\n this._parser.setExecuteHandler(C0.BS, () => this.backspace());\n this._parser.setExecuteHandler(C0.HT, () => this.tab());\n this._parser.setExecuteHandler(C0.SO, () => this.shiftOut());\n this._parser.setExecuteHandler(C0.SI, () => this.shiftIn());\n // FIXME: What do to with missing? Old code just added those to print.\n\n this._parser.setExecuteHandler(C1.IND, () => this.index());\n this._parser.setExecuteHandler(C1.NEL, () => this.nextLine());\n this._parser.setExecuteHandler(C1.HTS, () => this.tabSet());\n\n /**\n * OSC handler\n */\n // 0 - icon name + title\n this._parser.registerOscHandler(0, new OscHandler(data => { this.setTitle(data); this.setIconName(data); return true; }));\n // 1 - icon name\n this._parser.registerOscHandler(1, new OscHandler(data => this.setIconName(data)));\n // 2 - title\n this._parser.registerOscHandler(2, new OscHandler(data => this.setTitle(data)));\n // 3 - set property X in the form \"prop=value\"\n // 4 - Change Color Number\n this._parser.registerOscHandler(4, new OscHandler(data => this.setOrReportIndexedColor(data)));\n // 5 - Change Special Color Number\n // 6 - Enable/disable Special Color Number c\n // 7 - current directory? (not in xterm spec, see https://gitlab.com/gnachman/iterm2/issues/3939)\n // 8 - create hyperlink (not in xterm spec, see https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda)\n this._parser.registerOscHandler(8, new OscHandler(data => this.setHyperlink(data)));\n // 10 - Change VT100 text foreground color to Pt.\n this._parser.registerOscHandler(10, new OscHandler(data => this.setOrReportFgColor(data)));\n // 11 - Change VT100 text background color to Pt.\n this._parser.registerOscHandler(11, new OscHandler(data => this.setOrReportBgColor(data)));\n // 12 - Change text cursor color to Pt.\n this._parser.registerOscHandler(12, new OscHandler(data => this.setOrReportCursorColor(data)));\n // 13 - Change mouse foreground color to Pt.\n // 14 - Change mouse background color to Pt.\n // 15 - Change Tektronix foreground color to Pt.\n // 16 - Change Tektronix background color to Pt.\n // 17 - Change highlight background color to Pt.\n // 18 - Change Tektronix cursor color to Pt.\n // 19 - Change highlight foreground color to Pt.\n // 46 - Change Log File to Pt.\n // 50 - Set Font to Pt.\n // 51 - reserved for Emacs shell.\n // 52 - Manipulate Selection Data.\n // 104 ; c - Reset Color Number c.\n this._parser.registerOscHandler(104, new OscHandler(data => this.restoreIndexedColor(data)));\n // 105 ; c - Reset Special Color Number c.\n // 106 ; c; f - Enable/disable Special Color Number c.\n // 110 - Reset VT100 text foreground color.\n this._parser.registerOscHandler(110, new OscHandler(data => this.restoreFgColor(data)));\n // 111 - Reset VT100 text background color.\n this._parser.registerOscHandler(111, new OscHandler(data => this.restoreBgColor(data)));\n // 112 - Reset text cursor color.\n this._parser.registerOscHandler(112, new OscHandler(data => this.restoreCursorColor(data)));\n // 113 - Reset mouse foreground color.\n // 114 - Reset mouse background color.\n // 115 - Reset Tektronix foreground color.\n // 116 - Reset Tektronix background color.\n // 117 - Reset highlight color.\n // 118 - Reset Tektronix cursor color.\n // 119 - Reset highlight foreground color.\n\n /**\n * ESC handlers\n */\n this._parser.registerEscHandler({ final: '7' }, () => this.saveCursor());\n this._parser.registerEscHandler({ final: '8' }, () => this.restoreCursor());\n this._parser.registerEscHandler({ final: 'D' }, () => this.index());\n this._parser.registerEscHandler({ final: 'E' }, () => this.nextLine());\n this._parser.registerEscHandler({ final: 'H' }, () => this.tabSet());\n this._parser.registerEscHandler({ final: 'M' }, () => this.reverseIndex());\n this._parser.registerEscHandler({ final: '=' }, () => this.keypadApplicationMode());\n this._parser.registerEscHandler({ final: '>' }, () => this.keypadNumericMode());\n this._parser.registerEscHandler({ final: 'c' }, () => this.fullReset());\n this._parser.registerEscHandler({ final: 'n' }, () => this.setgLevel(2));\n this._parser.registerEscHandler({ final: 'o' }, () => this.setgLevel(3));\n this._parser.registerEscHandler({ final: '|' }, () => this.setgLevel(3));\n this._parser.registerEscHandler({ final: '}' }, () => this.setgLevel(2));\n this._parser.registerEscHandler({ final: '~' }, () => this.setgLevel(1));\n this._parser.registerEscHandler({ intermediates: '%', final: '@' }, () => this.selectDefaultCharset());\n this._parser.registerEscHandler({ intermediates: '%', final: 'G' }, () => this.selectDefaultCharset());\n for (const flag in CHARSETS) {\n this._parser.registerEscHandler({ intermediates: '(', final: flag }, () => this.selectCharset('(' + flag));\n this._parser.registerEscHandler({ intermediates: ')', final: flag }, () => this.selectCharset(')' + flag));\n this._parser.registerEscHandler({ intermediates: '*', final: flag }, () => this.selectCharset('*' + flag));\n this._parser.registerEscHandler({ intermediates: '+', final: flag }, () => this.selectCharset('+' + flag));\n this._parser.registerEscHandler({ intermediates: '-', final: flag }, () => this.selectCharset('-' + flag));\n this._parser.registerEscHandler({ intermediates: '.', final: flag }, () => this.selectCharset('.' + flag));\n this._parser.registerEscHandler({ intermediates: '/', final: flag }, () => this.selectCharset('/' + flag)); // TODO: supported?\n }\n this._parser.registerEscHandler({ intermediates: '#', final: '8' }, () => this.screenAlignmentPattern());\n\n /**\n * error handler\n */\n this._parser.setErrorHandler((state: IParsingState) => {\n this._logService.error('Parsing error: ', state);\n return state;\n });\n\n /**\n * DCS handler\n */\n this._parser.registerDcsHandler({ intermediates: '$', final: 'q' }, new DcsHandler((data, params) => this.requestStatusString(data, params)));\n }\n\n /**\n * Async parse support.\n */\n private _preserveStack(cursorStartX: number, cursorStartY: number, decodedLength: number, position: number): void {\n this._parseStack.paused = true;\n this._parseStack.cursorStartX = cursorStartX;\n this._parseStack.cursorStartY = cursorStartY;\n this._parseStack.decodedLength = decodedLength;\n this._parseStack.position = position;\n }\n\n private _logSlowResolvingAsync(p: Promise): void {\n // log a limited warning about an async handler taking too long\n if (this._logService.logLevel <= LogLevelEnum.WARN) {\n let slowTimeout: ReturnType | undefined;\n const slowPromise = new Promise((_res, rej) => {\n slowTimeout = setTimeout(() => rej('#SLOW_TIMEOUT'), Constants.SLOW_ASYNC_LIMIT);\n });\n Promise.race([p, slowPromise])\n .then(() => {\n if (slowTimeout !== undefined) {\n clearTimeout(slowTimeout);\n }\n }, err => {\n if (slowTimeout !== undefined) {\n clearTimeout(slowTimeout);\n }\n if (err !== '#SLOW_TIMEOUT') {\n throw err;\n }\n console.warn(`async parser handler taking longer than ${Constants.SLOW_ASYNC_LIMIT} ms`);\n });\n }\n }\n\n private _getCurrentLinkId(): number {\n return this._curAttrData.extended.urlId;\n }\n\n /**\n * Parse call with async handler support.\n *\n * Whether the stack state got preserved for the next call, is indicated by the return value:\n * - undefined (void):\n * all handlers were sync, no stack save, continue normally with next chunk\n * - Promise\\:\n * execution stopped at async handler, stack saved, continue with same chunk and the promise\n * resolve value as `promiseResult` until the method returns `undefined`\n *\n * Note: This method should only be called by `Terminal.write` to ensure correct execution order\n * and proper continuation of async parser handlers.\n */\n public parse(data: string | Uint8Array, promiseResult?: boolean): void | Promise {\n let result: void | Promise;\n let cursorStartX = this._activeBuffer.x;\n let cursorStartY = this._activeBuffer.y;\n let start = 0;\n const wasPaused = this._parseStack.paused;\n\n if (wasPaused) {\n // assumption: _parseBuffer never mutates between async calls\n if (result = this._parser.parse(this._parseBuffer, this._parseStack.decodedLength, promiseResult)) {\n this._logSlowResolvingAsync(result);\n return result;\n }\n cursorStartX = this._parseStack.cursorStartX;\n cursorStartY = this._parseStack.cursorStartY;\n this._parseStack.paused = false;\n if (data.length > Constants.MAX_PARSEBUFFER_LENGTH) {\n start = this._parseStack.position + Constants.MAX_PARSEBUFFER_LENGTH;\n }\n }\n\n // Log debug data, the log level gate is to prevent extra work in this hot path\n if (this._logService.logLevel <= LogLevelEnum.DEBUG) {\n this._logService.debug(`parsing data ${typeof data === 'string' ? ` \"${data}\"` : ` \"${Array.prototype.map.call(data, e => String.fromCharCode(e)).join('')}\"`}`);\n }\n if (this._logService.logLevel === LogLevelEnum.TRACE) {\n this._logService.trace(`parsing data (codes)`, typeof data === 'string'\n ? data.split('').map(e => e.charCodeAt(0))\n : data\n );\n }\n\n // resize input buffer if needed\n if (this._parseBuffer.length < data.length) {\n if (this._parseBuffer.length < Constants.MAX_PARSEBUFFER_LENGTH) {\n this._parseBuffer = new Uint32Array(Math.min(data.length, Constants.MAX_PARSEBUFFER_LENGTH));\n }\n }\n\n // Clear the dirty row service so we know which lines changed as a result of parsing\n // Important: do not clear between async calls, otherwise we lost pending update information.\n if (!wasPaused) {\n this._dirtyRowTracker.clearRange();\n }\n\n // process big data in smaller chunks\n if (data.length > Constants.MAX_PARSEBUFFER_LENGTH) {\n for (let i = start; i < data.length; i += Constants.MAX_PARSEBUFFER_LENGTH) {\n const end = i + Constants.MAX_PARSEBUFFER_LENGTH < data.length ? i + Constants.MAX_PARSEBUFFER_LENGTH : data.length;\n const len = (typeof data === 'string')\n ? this._stringDecoder.decode(data.substring(i, end), this._parseBuffer)\n : this._utf8Decoder.decode(data.subarray(i, end), this._parseBuffer);\n if (result = this._parser.parse(this._parseBuffer, len)) {\n this._preserveStack(cursorStartX, cursorStartY, len, i);\n this._logSlowResolvingAsync(result);\n return result;\n }\n }\n } else {\n if (!wasPaused) {\n const len = (typeof data === 'string')\n ? this._stringDecoder.decode(data, this._parseBuffer)\n : this._utf8Decoder.decode(data, this._parseBuffer);\n if (result = this._parser.parse(this._parseBuffer, len)) {\n this._preserveStack(cursorStartX, cursorStartY, len, 0);\n this._logSlowResolvingAsync(result);\n return result;\n }\n }\n }\n\n if (this._activeBuffer.x !== cursorStartX || this._activeBuffer.y !== cursorStartY) {\n this._onCursorMove.fire();\n }\n\n // Refresh any dirty rows accumulated as part of parsing, fire only for rows within the\n // _viewport_ which is relative to ydisp, not relative to ybase.\n const viewportEnd = this._dirtyRowTracker.end + (this._bufferService.buffer.ybase - this._bufferService.buffer.ydisp);\n const viewportStart = this._dirtyRowTracker.start + (this._bufferService.buffer.ybase - this._bufferService.buffer.ydisp);\n if (viewportStart < this._bufferService.rows) {\n this._onRequestRefreshRows.fire({\n start: Math.min(viewportStart, this._bufferService.rows - 1),\n end: Math.min(viewportEnd, this._bufferService.rows - 1)\n });\n }\n }\n\n public print(data: Uint32Array, start: number, end: number): void {\n let code: number;\n let chWidth: number;\n const charset = this._charsetService.charset;\n const screenReaderMode = this._optionsService.rawOptions.screenReaderMode;\n const cols = this._bufferService.cols;\n const wraparoundMode = this._coreService.decPrivateModes.wraparound;\n const insertMode = this._coreService.modes.insertMode;\n const curAttr = this._curAttrData;\n let bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y);\n\n // Defensive check: bufferRow can be undefined if a resize occurred mid-write due to async\n // scheduling gaps in WriteBuffer. See https://github.com/xtermjs/xterm.js/issues/5597\n if (!bufferRow) {\n return;\n }\n\n this._dirtyRowTracker.markDirty(this._activeBuffer.y);\n\n // handle wide chars: reset start_cell-1 if we would overwrite the second cell of a wide char\n if (this._activeBuffer.x && end - start > 0 && bufferRow.getWidth(this._activeBuffer.x - 1) === 2) {\n bufferRow.setCellFromCodepoint(this._activeBuffer.x - 1, 0, 1, curAttr);\n }\n\n let precedingJoinState = this._parser.precedingJoinState;\n for (let pos = start; pos < end; ++pos) {\n code = data[pos];\n\n // Soft hyphen's (U+00AD) behavior is ambiguous and differs across terminals. We opt to treat\n // it as a zero-width hint to text layout engines and simply ignore it.\n if (code === 0xAD) {\n continue;\n }\n\n // get charset replacement character\n // charset is only defined for ASCII, therefore we only\n // search for an replacement char if code < 127\n if (code < 127 && charset) {\n const ch = charset[String.fromCharCode(code)];\n if (ch) {\n code = ch.charCodeAt(0);\n }\n }\n\n const currentInfo = this._unicodeService.charProperties(code, precedingJoinState);\n chWidth = UnicodeService.extractWidth(currentInfo);\n const shouldJoin = UnicodeService.extractShouldJoin(currentInfo);\n const oldWidth = shouldJoin ? UnicodeService.extractWidth(precedingJoinState) : 0;\n precedingJoinState = currentInfo;\n\n if (screenReaderMode) {\n this._onA11yChar.fire(stringFromCodePoint(code));\n }\n const linkId = this._getCurrentLinkId();\n if (linkId) {\n this._oscLinkService.addLineToLink(linkId, this._activeBuffer.ybase + this._activeBuffer.y);\n }\n\n // goto next line if ch would overflow\n // NOTE: To avoid costly width checks here,\n // the terminal does not allow a cols < 2.\n if (this._activeBuffer.x + chWidth - oldWidth > cols) {\n // autowrap - DECAWM\n // automatically wraps to the beginning of the next line\n if (wraparoundMode) {\n const oldRow = bufferRow;\n let oldCol = this._activeBuffer.x - oldWidth;\n this._activeBuffer.x = oldWidth;\n this._activeBuffer.y++;\n if (this._activeBuffer.y === this._activeBuffer.scrollBottom + 1) {\n this._activeBuffer.y--;\n this._bufferService.scroll(this._eraseAttrData(), true);\n } else {\n if (this._activeBuffer.y >= this._bufferService.rows) {\n this._activeBuffer.y = this._bufferService.rows - 1;\n }\n // The line already exists (eg. the initial viewport), mark it as a\n // wrapped line\n this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!.isWrapped = true;\n }\n // row changed, get it again\n bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y);\n if (!bufferRow) {\n return;\n }\n if (oldWidth > 0 && bufferRow instanceof BufferLine) {\n // Combining character widens 1 column to 2.\n // Move old character to next line.\n bufferRow.copyCellsFrom(oldRow as BufferLine,\n oldCol, 0, oldWidth, false);\n }\n // clear left over cells to the right\n while (oldCol < cols) {\n oldRow.setCellFromCodepoint(oldCol++, 0, 1, curAttr);\n }\n } else {\n this._activeBuffer.x = cols - 1;\n if (chWidth === 2) {\n // FIXME: check for xterm behavior\n // What to do here? We got a wide char that does not fit into last cell\n continue;\n }\n }\n }\n\n // insert combining char at last cursor position\n // this._activeBuffer.x should never be 0 for a combining char\n // since they always follow a cell consuming char\n // therefore we can test for this._activeBuffer.x to avoid overflow left\n if (shouldJoin && this._activeBuffer.x) {\n const offset = bufferRow.getWidth(this._activeBuffer.x - 1) ? 1 : 2;\n // if empty cell after fullwidth, need to go 2 cells back\n // it is save to step 2 cells back here\n // since an empty cell is only set by fullwidth chars\n bufferRow.addCodepointToCell(this._activeBuffer.x - offset,\n code, chWidth);\n for (let delta = chWidth - oldWidth; --delta >= 0;) {\n bufferRow.setCellFromCodepoint(this._activeBuffer.x++, 0, 0, curAttr);\n }\n continue;\n }\n\n // insert mode: move characters to right\n if (insertMode) {\n // right shift cells according to the width\n bufferRow.insertCells(this._activeBuffer.x, chWidth - oldWidth, this._activeBuffer.getNullCell(curAttr));\n // test last cell - since the last cell has only room for\n // a halfwidth char any fullwidth shifted there is lost\n // and will be set to empty cell\n if (bufferRow.getWidth(cols - 1) === 2) {\n bufferRow.setCellFromCodepoint(cols - 1, NULL_CELL_CODE, NULL_CELL_WIDTH, curAttr);\n }\n }\n\n // write current char to buffer and advance cursor\n bufferRow.setCellFromCodepoint(this._activeBuffer.x++, code, chWidth, curAttr);\n\n // fullwidth char - also set next cell to placeholder stub and advance cursor\n // for graphemes bigger than fullwidth we can simply loop to zero\n // we already made sure above, that this._activeBuffer.x + chWidth will not overflow right\n if (chWidth > 0) {\n while (--chWidth) {\n // other than a regular empty cell a cell following a wide char has no width\n bufferRow.setCellFromCodepoint(this._activeBuffer.x++, 0, 0, curAttr);\n }\n }\n }\n\n this._parser.precedingJoinState = precedingJoinState;\n\n // handle wide chars: reset cell to the right if it is second cell of a wide char\n if (this._activeBuffer.x < cols && end - start > 0 && bufferRow.getWidth(this._activeBuffer.x) === 0 && !bufferRow.hasContent(this._activeBuffer.x)) {\n bufferRow.setCellFromCodepoint(this._activeBuffer.x, 0, 1, curAttr);\n }\n\n this._dirtyRowTracker.markDirty(this._activeBuffer.y);\n }\n\n /**\n * Forward registerCsiHandler from parser.\n */\n public registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise): IDisposable {\n if (id.final === 't' && !id.prefix && !id.intermediates) {\n // security: always check whether window option is allowed\n return this._parser.registerCsiHandler(id, params => {\n if (!paramToWindowOption(params.params[0], this._optionsService.rawOptions.windowOptions)) {\n return true;\n }\n return callback(params);\n });\n }\n return this._parser.registerCsiHandler(id, callback);\n }\n\n /**\n * Forward registerDcsHandler from parser.\n */\n public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise): IDisposable {\n return this._parser.registerDcsHandler(id, new DcsHandler(callback));\n }\n\n /**\n * Forward registerEscHandler from parser.\n */\n public registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise): IDisposable {\n return this._parser.registerEscHandler(id, callback);\n }\n\n /**\n * Forward registerOscHandler from parser.\n */\n public registerOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable {\n return this._parser.registerOscHandler(ident, new OscHandler(callback));\n }\n\n /**\n * Forward registerApcHandler from parser.\n */\n public registerApcHandler(id: IFunctionIdentifier, callback: (data: string) => boolean | Promise): IDisposable {\n return this._parser.registerApcHandler(id, new ApcHandler(callback));\n }\n\n /**\n * BEL\n * Bell (Ctrl-G).\n *\n * @vt: #Y C0 BEL \"Bell\" \"\\a, \\x07\" \"Ring the bell.\"\n * The behavior of the bell is further customizable with `ITerminalOptions.bellStyle`\n * and `ITerminalOptions.bellSound`.\n */\n public bell(): boolean {\n this._onRequestBell.fire();\n return true;\n }\n\n /**\n * LF\n * Line Feed or New Line (NL). (LF is Ctrl-J).\n *\n * @vt: #Y C0 LF \"Line Feed\" \"\\n, \\x0A\" \"Move the cursor one row down, scrolling if needed.\"\n * Scrolling is restricted to scroll margins and will only happen on the bottom line.\n *\n * @vt: #Y C0 VT \"Vertical Tabulation\" \"\\v, \\x0B\" \"Treated as LF.\"\n * @vt: #Y C0 FF \"Form Feed\" \"\\f, \\x0C\" \"Treated as LF.\"\n */\n public lineFeed(): boolean {\n this._dirtyRowTracker.markDirty(this._activeBuffer.y);\n if (this._optionsService.rawOptions.convertEol) {\n this._activeBuffer.x = 0;\n }\n this._activeBuffer.y++;\n if (this._activeBuffer.y === this._activeBuffer.scrollBottom + 1) {\n this._activeBuffer.y--;\n this._bufferService.scroll(this._eraseAttrData());\n } else if (this._activeBuffer.y >= this._bufferService.rows) {\n this._activeBuffer.y = this._bufferService.rows - 1;\n } else {\n // There was an explicit line feed (not just a carriage return), so clear the wrapped state of\n // the line. This is particularly important on conpty/Windows where revisiting lines to\n // reprint is common, especially on resize. Note that the windowsMode wrapped line heuristics\n // can mess with this so windowsMode should be disabled, which is recommended on Windows build\n // 21376 and above.\n this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!.isWrapped = false;\n }\n // If the end of the line is hit, prevent this action from wrapping around to the next line.\n if (this._activeBuffer.x >= this._bufferService.cols) {\n this._activeBuffer.x--;\n }\n this._dirtyRowTracker.markDirty(this._activeBuffer.y);\n\n this._onLineFeed.fire();\n return true;\n }\n\n /**\n * CR\n * Carriage Return (Ctrl-M).\n *\n * @vt: #Y C0 CR \"Carriage Return\" \"\\r, \\x0D\" \"Move the cursor to the beginning of the row.\"\n */\n public carriageReturn(): boolean {\n this._activeBuffer.x = 0;\n return true;\n }\n\n /**\n * BS\n * Backspace (Ctrl-H).\n *\n * @vt: #Y C0 BS \"Backspace\" \"\\b, \\x08\" \"Move the cursor one position to the left.\"\n * By default it is not possible to move the cursor past the leftmost position.\n * If `reverse wrap-around` (`CSI ? 45 h`) is set, a previous soft line wrap (DECAWM)\n * can be undone with BS within the scroll margins. In that case the cursor will wrap back\n * to the end of the previous row. Note that it is not possible to peek back into the scrollbuffer\n * with the cursor, thus at the home position (top-leftmost cell) this has no effect.\n */\n public backspace(): boolean {\n // reverse wrap-around is disabled\n if (!this._coreService.decPrivateModes.reverseWraparound) {\n this._restrictCursor();\n if (this._activeBuffer.x > 0) {\n this._activeBuffer.x--;\n }\n return true;\n }\n\n // reverse wrap-around is enabled\n // other than for normal operation mode, reverse wrap-around allows the cursor\n // to be at x=cols to be able to address the last cell of a row by BS\n this._restrictCursor(this._bufferService.cols);\n\n if (this._activeBuffer.x > 0) {\n this._activeBuffer.x--;\n } else {\n /**\n * reverse wrap-around handling:\n * Our implementation deviates from xterm on purpose. Details:\n * - only previous soft NLs can be reversed (isWrapped=true)\n * - only works within scrollborders (top/bottom, left/right not yet supported)\n * - cannot peek into scrollbuffer\n * - any cursor movement sequence keeps working as expected\n */\n if (this._activeBuffer.x === 0\n && this._activeBuffer.y > this._activeBuffer.scrollTop\n && this._activeBuffer.y <= this._activeBuffer.scrollBottom\n && this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)?.isWrapped) {\n this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!.isWrapped = false;\n this._activeBuffer.y--;\n this._activeBuffer.x = this._bufferService.cols - 1;\n // find last taken cell - last cell can have 3 different states:\n // - hasContent(true) + hasWidth(1): narrow char - we are done\n // - hasWidth(0): second part of wide char - we are done\n // - hasContent(false) + hasWidth(1): empty cell due to early wrapping wide char, go one\n // cell further back\n const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!;\n if (line.hasWidth(this._activeBuffer.x) && !line.hasContent(this._activeBuffer.x)) {\n this._activeBuffer.x--;\n // We do this only once, since width=1 + hasContent=false currently happens only once\n // before early wrapping of a wide char.\n // This needs to be fixed once we support graphemes taking more than 2 cells.\n }\n }\n }\n this._restrictCursor();\n return true;\n }\n\n /**\n * TAB\n * Horizontal Tab (HT) (Ctrl-I).\n *\n * @vt: #Y C0 HT \"Horizontal Tabulation\" \"\\t, \\x09\" \"Move the cursor to the next character tab stop.\"\n */\n public tab(): boolean {\n if (this._activeBuffer.x >= this._bufferService.cols) {\n return true;\n }\n const originalX = this._activeBuffer.x;\n this._activeBuffer.x = this._activeBuffer.nextStop();\n if (this._optionsService.rawOptions.screenReaderMode) {\n this._onA11yTab.fire(this._activeBuffer.x - originalX);\n }\n return true;\n }\n\n /**\n * SO\n * Shift Out (Ctrl-N) -> Switch to Alternate Character Set. This invokes the\n * G1 character set.\n *\n * @vt: #P[Only limited ISO-2022 charset support.] C0 SO \"Shift Out\" \"\\x0E\" \"Switch to an alternative character set.\"\n */\n public shiftOut(): boolean {\n this._charsetService.setgLevel(1);\n return true;\n }\n\n /**\n * SI\n * Shift In (Ctrl-O) -> Switch to Standard Character Set. This invokes the G0\n * character set (the default).\n *\n * @vt: #Y C0 SI \"Shift In\" \"\\x0F\" \"Return to regular character set after Shift Out.\"\n */\n public shiftIn(): boolean {\n this._charsetService.setgLevel(0);\n return true;\n }\n\n /**\n * Restrict cursor to viewport size / scroll margin (origin mode).\n */\n private _restrictCursor(maxCol: number = this._bufferService.cols - 1): void {\n this._activeBuffer.x = Math.min(maxCol, Math.max(0, this._activeBuffer.x));\n this._activeBuffer.y = this._coreService.decPrivateModes.origin\n ? Math.min(this._activeBuffer.scrollBottom, Math.max(this._activeBuffer.scrollTop, this._activeBuffer.y))\n : Math.min(this._bufferService.rows - 1, Math.max(0, this._activeBuffer.y));\n this._dirtyRowTracker.markDirty(this._activeBuffer.y);\n }\n\n /**\n * Set absolute cursor position.\n */\n private _setCursor(x: number, y: number): void {\n this._dirtyRowTracker.markDirty(this._activeBuffer.y);\n if (this._coreService.decPrivateModes.origin) {\n this._activeBuffer.x = x;\n this._activeBuffer.y = this._activeBuffer.scrollTop + y;\n } else {\n this._activeBuffer.x = x;\n this._activeBuffer.y = y;\n }\n this._restrictCursor();\n this._dirtyRowTracker.markDirty(this._activeBuffer.y);\n }\n\n /**\n * Set relative cursor position.\n */\n private _moveCursor(x: number, y: number): void {\n // for relative changes we have to make sure we are within 0 .. cols/rows - 1\n // before calculating the new position\n this._restrictCursor();\n this._setCursor(this._activeBuffer.x + x, this._activeBuffer.y + y);\n }\n\n /**\n * CSI Ps A\n * Cursor Up Ps Times (default = 1) (CUU).\n *\n * @vt: #Y CSI CUU \"Cursor Up\" \"CSI Ps A\" \"Move cursor `Ps` times up (default=1).\"\n * If the cursor would pass the top scroll margin, it will stop there.\n */\n public cursorUp(params: IParams): boolean {\n // stop at scrollTop\n const diffToTop = this._activeBuffer.y - this._activeBuffer.scrollTop;\n if (diffToTop >= 0) {\n this._moveCursor(0, -Math.min(diffToTop, params.params[0] || 1));\n } else {\n this._moveCursor(0, -(params.params[0] || 1));\n }\n return true;\n }\n\n /**\n * CSI Ps B\n * Cursor Down Ps Times (default = 1) (CUD).\n *\n * @vt: #Y CSI CUD \"Cursor Down\" \"CSI Ps B\" \"Move cursor `Ps` times down (default=1).\"\n * If the cursor would pass the bottom scroll margin, it will stop there.\n */\n public cursorDown(params: IParams): boolean {\n // stop at scrollBottom\n const diffToBottom = this._activeBuffer.scrollBottom - this._activeBuffer.y;\n if (diffToBottom >= 0) {\n this._moveCursor(0, Math.min(diffToBottom, params.params[0] || 1));\n } else {\n this._moveCursor(0, params.params[0] || 1);\n }\n return true;\n }\n\n /**\n * CSI Ps C\n * Cursor Forward Ps Times (default = 1) (CUF).\n *\n * @vt: #Y CSI CUF \"Cursor Forward\" \"CSI Ps C\" \"Move cursor `Ps` times forward (default=1).\"\n */\n public cursorForward(params: IParams): boolean {\n this._moveCursor(params.params[0] || 1, 0);\n return true;\n }\n\n /**\n * CSI Ps D\n * Cursor Backward Ps Times (default = 1) (CUB).\n *\n * @vt: #Y CSI CUB \"Cursor Backward\" \"CSI Ps D\" \"Move cursor `Ps` times backward (default=1).\"\n */\n public cursorBackward(params: IParams): boolean {\n this._moveCursor(-(params.params[0] || 1), 0);\n return true;\n }\n\n /**\n * CSI Ps E\n * Cursor Next Line Ps Times (default = 1) (CNL).\n * Other than cursorDown (CUD) also set the cursor to first column.\n *\n * @vt: #Y CSI CNL \"Cursor Next Line\" \"CSI Ps E\" \"Move cursor `Ps` times down (default=1) and to the first column.\"\n * Same as CUD, additionally places the cursor at the first column.\n */\n public cursorNextLine(params: IParams): boolean {\n this.cursorDown(params);\n this._activeBuffer.x = 0;\n return true;\n }\n\n /**\n * CSI Ps F\n * Cursor Previous Line Ps Times (default = 1) (CPL).\n * Other than cursorUp (CUU) also set the cursor to first column.\n *\n * @vt: #Y CSI CPL \"Cursor Backward\" \"CSI Ps F\" \"Move cursor `Ps` times up (default=1) and to the first column.\"\n * Same as CUU, additionally places the cursor at the first column.\n */\n public cursorPrecedingLine(params: IParams): boolean {\n this.cursorUp(params);\n this._activeBuffer.x = 0;\n return true;\n }\n\n /**\n * CSI Ps G\n * Cursor Character Absolute [column] (default = [row,1]) (CHA).\n *\n * @vt: #Y CSI CHA \"Cursor Horizontal Absolute\" \"CSI Ps G\" \"Move cursor to `Ps`-th column of the active row (default=1).\"\n */\n public cursorCharAbsolute(params: IParams): boolean {\n this._setCursor((params.params[0] || 1) - 1, this._activeBuffer.y);\n return true;\n }\n\n /**\n * CSI Ps ; Ps H\n * Cursor Position [row;column] (default = [1,1]) (CUP).\n *\n * @vt: #Y CSI CUP \"Cursor Position\" \"CSI Ps ; Ps H\" \"Set cursor to position [`Ps`, `Ps`] (default = [1, 1]).\"\n * If ORIGIN mode is set, places the cursor to the absolute position within the scroll margins.\n * If ORIGIN mode is not set, places the cursor to the absolute position within the viewport.\n * Note that the coordinates are 1-based, thus the top left position starts at `1 ; 1`.\n */\n public cursorPosition(params: IParams): boolean {\n this._setCursor(\n // col\n (params.length >= 2) ? (params.params[1] || 1) - 1 : 0,\n // row\n (params.params[0] || 1) - 1\n );\n return true;\n }\n\n /**\n * CSI Pm ` Character Position Absolute\n * [column] (default = [row,1]) (HPA).\n * Currently same functionality as CHA.\n *\n * @vt: #Y CSI HPA \"Horizontal Position Absolute\" \"CSI Ps ` \" \"Same as CHA.\"\n */\n public charPosAbsolute(params: IParams): boolean {\n this._setCursor((params.params[0] || 1) - 1, this._activeBuffer.y);\n return true;\n }\n\n /**\n * CSI Pm a Character Position Relative\n * [columns] (default = [row,col+1]) (HPR)\n *\n * @vt: #Y CSI HPR \"Horizontal Position Relative\" \"CSI Ps a\" \"Same as CUF.\"\n */\n public hPositionRelative(params: IParams): boolean {\n this._moveCursor(params.params[0] || 1, 0);\n return true;\n }\n\n /**\n * CSI Pm d Vertical Position Absolute (VPA)\n * [row] (default = [1,column])\n *\n * @vt: #Y CSI VPA \"Vertical Position Absolute\" \"CSI Ps d\" \"Move cursor to `Ps`-th row (default=1).\"\n */\n public linePosAbsolute(params: IParams): boolean {\n this._setCursor(this._activeBuffer.x, (params.params[0] || 1) - 1);\n return true;\n }\n\n /**\n * CSI Pm e Vertical Position Relative (VPR)\n * [rows] (default = [row+1,column])\n * reuse CSI Ps B ?\n *\n * @vt: #Y CSI VPR \"Vertical Position Relative\" \"CSI Ps e\" \"Move cursor `Ps` times down (default=1).\"\n */\n public vPositionRelative(params: IParams): boolean {\n this._moveCursor(0, params.params[0] || 1);\n return true;\n }\n\n /**\n * CSI Ps ; Ps f\n * Horizontal and Vertical Position [row;column] (default =\n * [1,1]) (HVP).\n * Same as CUP.\n *\n * @vt: #Y CSI HVP \"Horizontal and Vertical Position\" \"CSI Ps ; Ps f\" \"Same as CUP.\"\n */\n public hVPosition(params: IParams): boolean {\n this.cursorPosition(params);\n return true;\n }\n\n /**\n * CSI Ps g Tab Clear (TBC).\n * Ps = 0 -> Clear Current Column (default).\n * Ps = 3 -> Clear All.\n * Potentially:\n * Ps = 2 -> Clear Stops on Line.\n * http://vt100.net/annarbor/aaa-ug/section6.html\n *\n * @vt: #Y CSI TBC \"Tab Clear\" \"CSI Ps g\" \"Clear tab stops at current position (0) or all (3) (default=0).\"\n * Clearing tabstops off the active row (Ps = 2, VT100) is currently not supported.\n */\n public tabClear(params: IParams): boolean {\n const param = params.params[0];\n if (param === 0) {\n delete this._activeBuffer.tabs[this._activeBuffer.x];\n } else if (param === 3) {\n this._activeBuffer.tabs = {};\n }\n return true;\n }\n\n /**\n * CSI Ps I\n * Cursor Forward Tabulation Ps tab stops (default = 1) (CHT).\n *\n * @vt: #Y CSI CHT \"Cursor Horizontal Tabulation\" \"CSI Ps I\" \"Move cursor `Ps` times tabs forward (default=1).\"\n */\n public cursorForwardTab(params: IParams): boolean {\n if (this._activeBuffer.x >= this._bufferService.cols) {\n return true;\n }\n let param = params.params[0] || 1;\n while (param--) {\n this._activeBuffer.x = this._activeBuffer.nextStop();\n }\n return true;\n }\n\n /**\n * CSI Ps Z Cursor Backward Tabulation Ps tab stops (default = 1) (CBT).\n *\n * @vt: #Y CSI CBT \"Cursor Backward Tabulation\" \"CSI Ps Z\" \"Move cursor `Ps` tabs backward (default=1).\"\n */\n public cursorBackwardTab(params: IParams): boolean {\n if (this._activeBuffer.x >= this._bufferService.cols) {\n return true;\n }\n let param = params.params[0] || 1;\n\n while (param--) {\n this._activeBuffer.x = this._activeBuffer.prevStop();\n }\n return true;\n }\n\n /**\n * CSI Ps \" q Select Character Protection Attribute (DECSCA).\n *\n * @vt: #Y CSI DECSCA \"Select Character Protection Attribute\" \"CSI Ps \" q\" \"Whether DECSED and DECSEL can erase (0=default, 2) or not (1).\"\n */\n public selectProtected(params: IParams): boolean {\n const p = params.params[0];\n if (p === 1) this._curAttrData.bg |= BgFlags.PROTECTED;\n if (p === 2 || p === 0) this._curAttrData.bg &= ~BgFlags.PROTECTED;\n return true;\n }\n\n\n /**\n * Helper method to erase cells in a terminal row.\n * The cell gets replaced with the eraseChar of the terminal.\n * @param y The row index relative to the viewport.\n * @param start The start x index of the range to be erased.\n * @param end The end x index of the range to be erased (exclusive).\n * @param clearWrap clear the isWrapped flag\n * @param respectProtect Whether to respect the protection attribute (DECSCA).\n */\n private _eraseInBufferLine(y: number, start: number, end: number, clearWrap: boolean = false, respectProtect: boolean = false): void {\n const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y);\n if (!line) {\n return;\n }\n line.replaceCells(\n start,\n end,\n this._activeBuffer.getNullCell(this._eraseAttrData()),\n respectProtect\n );\n if (clearWrap) {\n line.isWrapped = false;\n }\n }\n\n /**\n * Helper method to reset cells in a terminal row. The cell gets replaced with the eraseChar of\n * the terminal and the isWrapped property is set to false.\n * @param y row index\n */\n private _resetBufferLine(y: number, respectProtect: boolean = false): void {\n const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y);\n if (line) {\n line.fill(this._activeBuffer.getNullCell(this._eraseAttrData()), respectProtect);\n this._bufferService.buffer.clearMarkers(this._activeBuffer.ybase + y);\n line.isWrapped = false;\n }\n }\n\n /**\n * CSI Ps J Erase in Display (ED).\n * Ps = 0 -> Erase Below (default).\n * Ps = 1 -> Erase Above.\n * Ps = 2 -> Erase All.\n * Ps = 3 -> Erase Saved Lines (xterm).\n * CSI ? Ps J\n * Erase in Display (DECSED).\n * Ps = 0 -> Selective Erase Below (default).\n * Ps = 1 -> Selective Erase Above.\n * Ps = 2 -> Selective Erase All.\n *\n * @vt: #Y CSI ED \"Erase In Display\" \"CSI Ps J\" \"Erase various parts of the viewport.\"\n * Supported param values:\n *\n * | Ps | Effect |\n * | -- | ------------------------------------------------------------ |\n * | 0 | Erase from the cursor through the end of the viewport. |\n * | 1 | Erase from the beginning of the viewport through the cursor. |\n * | 2 | Erase complete viewport. |\n * | 3 | Erase scrollback. |\n *\n * @vt: #Y CSI DECSED \"Selective Erase In Display\" \"CSI ? Ps J\" \"Same as ED with respecting protection flag.\"\n */\n public eraseInDisplay(params: IParams, respectProtect: boolean = false): boolean {\n this._restrictCursor(this._bufferService.cols);\n let j;\n switch (params.params[0]) {\n case 0:\n j = this._activeBuffer.y;\n this._dirtyRowTracker.markDirty(j);\n this._eraseInBufferLine(j++, this._activeBuffer.x, this._bufferService.cols, this._activeBuffer.x === 0, respectProtect);\n for (; j < this._bufferService.rows; j++) {\n this._resetBufferLine(j, respectProtect);\n }\n this._dirtyRowTracker.markDirty(j);\n break;\n case 1:\n j = this._activeBuffer.y;\n this._dirtyRowTracker.markDirty(j);\n // Deleted front part of line and everything before. This line will no longer be wrapped.\n this._eraseInBufferLine(j, 0, this._activeBuffer.x + 1, true, respectProtect);\n if (this._activeBuffer.x + 1 >= this._bufferService.cols) {\n // Deleted entire previous line. This next line can no longer be wrapped.\n const nextLine = this._activeBuffer.lines.get(j + 1);\n if (nextLine) {\n nextLine.isWrapped = false;\n }\n }\n while (j--) {\n this._resetBufferLine(j, respectProtect);\n }\n this._dirtyRowTracker.markDirty(0);\n break;\n case 2:\n if (this._optionsService.rawOptions.scrollOnEraseInDisplay) {\n j = this._bufferService.rows;\n this._dirtyRowTracker.markRangeDirty(0, j - 1);\n while (j--) {\n const currentLine = this._activeBuffer.lines.get(this._activeBuffer.ybase + j);\n if (currentLine?.getTrimmedLength()) {\n break;\n }\n }\n for (; j >= 0; j--) {\n this._bufferService.scroll(this._eraseAttrData());\n }\n }\n else {\n j = this._bufferService.rows;\n this._dirtyRowTracker.markDirty(j - 1);\n while (j--) {\n this._resetBufferLine(j, respectProtect);\n }\n this._dirtyRowTracker.markDirty(0);\n }\n break;\n case 3:\n // Clear scrollback (everything not in viewport)\n const scrollBackSize = this._activeBuffer.lines.length - this._bufferService.rows;\n if (scrollBackSize > 0) {\n this._activeBuffer.lines.trimStart(scrollBackSize);\n this._activeBuffer.ybase = Math.max(this._activeBuffer.ybase - scrollBackSize, 0);\n this._activeBuffer.ydisp = Math.max(this._activeBuffer.ydisp - scrollBackSize, 0);\n // isUserScrolling tracks the normal buffer's viewport, so ED3 on the alt\n // screen must not touch it\n if (this._activeBuffer === this._bufferService.buffers.normal) {\n this._bufferService.isUserScrolling = false;\n }\n // Force a scroll event to refresh viewport\n this._onScroll.fire(0);\n }\n break;\n }\n return true;\n }\n\n /**\n * CSI Ps K Erase in Line (EL).\n * Ps = 0 -> Erase to Right (default).\n * Ps = 1 -> Erase to Left.\n * Ps = 2 -> Erase All.\n * CSI ? Ps K\n * Erase in Line (DECSEL).\n * Ps = 0 -> Selective Erase to Right (default).\n * Ps = 1 -> Selective Erase to Left.\n * Ps = 2 -> Selective Erase All.\n *\n * @vt: #Y CSI EL \"Erase In Line\" \"CSI Ps K\" \"Erase various parts of the active row.\"\n * Supported param values:\n *\n * | Ps | Effect |\n * | -- | -------------------------------------------------------- |\n * | 0 | Erase from the cursor through the end of the row. |\n * | 1 | Erase from the beginning of the line through the cursor. |\n * | 2 | Erase complete line. |\n *\n * @vt: #Y CSI DECSEL \"Selective Erase In Line\" \"CSI ? Ps K\" \"Same as EL with respecting protecting flag.\"\n */\n public eraseInLine(params: IParams, respectProtect: boolean = false): boolean {\n this._restrictCursor(this._bufferService.cols);\n switch (params.params[0]) {\n case 0:\n this._eraseInBufferLine(this._activeBuffer.y, this._activeBuffer.x, this._bufferService.cols, this._activeBuffer.x === 0, respectProtect);\n break;\n case 1:\n this._eraseInBufferLine(this._activeBuffer.y, 0, this._activeBuffer.x + 1, false, respectProtect);\n break;\n case 2:\n this._eraseInBufferLine(this._activeBuffer.y, 0, this._bufferService.cols, true, respectProtect);\n break;\n }\n this._dirtyRowTracker.markDirty(this._activeBuffer.y);\n return true;\n }\n\n /**\n * CSI Ps L\n * Insert Ps Line(s) (default = 1) (IL).\n *\n * @vt: #Y CSI IL \"Insert Line\" \"CSI Ps L\" \"Insert `Ps` blank lines at active row (default=1).\"\n * For every inserted line at the scroll top one line at the scroll bottom gets removed.\n * The cursor is set to the first column.\n * IL has no effect if the cursor is outside the scroll margins.\n */\n public insertLines(params: IParams): boolean {\n this._restrictCursor();\n let param = params.params[0] || 1;\n\n if (this._activeBuffer.y > this._activeBuffer.scrollBottom || this._activeBuffer.y < this._activeBuffer.scrollTop) {\n return true;\n }\n\n const row: number = this._activeBuffer.ybase + this._activeBuffer.y;\n\n const scrollBottomRowsOffset = this._bufferService.rows - 1 - this._activeBuffer.scrollBottom;\n const scrollBottomAbsolute = this._bufferService.rows - 1 + this._activeBuffer.ybase - scrollBottomRowsOffset + 1;\n while (param--) {\n // test: echo -e '\\e[44m\\e[1L\\e[0m'\n // blankLine(true) - xterm/linux behavior\n this._activeBuffer.lines.splice(scrollBottomAbsolute - 1, 1);\n this._activeBuffer.lines.splice(row, 0, this._activeBuffer.getBlankLine(this._eraseAttrData()));\n }\n\n this._dirtyRowTracker.markRangeDirty(this._activeBuffer.y, this._activeBuffer.scrollBottom);\n this._activeBuffer.x = 0; // see https://vt100.net/docs/vt220-rm/chapter4.html - vt220 only?\n return true;\n }\n\n /**\n * CSI Ps M\n * Delete Ps Line(s) (default = 1) (DL).\n *\n * @vt: #Y CSI DL \"Delete Line\" \"CSI Ps M\" \"Delete `Ps` lines at active row (default=1).\"\n * For every deleted line at the scroll top one blank line at the scroll bottom gets appended.\n * The cursor is set to the first column.\n * DL has no effect if the cursor is outside the scroll margins.\n */\n public deleteLines(params: IParams): boolean {\n this._restrictCursor();\n let param = params.params[0] || 1;\n\n if (this._activeBuffer.y > this._activeBuffer.scrollBottom || this._activeBuffer.y < this._activeBuffer.scrollTop) {\n return true;\n }\n\n const row: number = this._activeBuffer.ybase + this._activeBuffer.y;\n\n let j: number;\n j = this._bufferService.rows - 1 - this._activeBuffer.scrollBottom;\n j = this._bufferService.rows - 1 + this._activeBuffer.ybase - j;\n while (param--) {\n // test: echo -e '\\e[44m\\e[1M\\e[0m'\n // blankLine(true) - xterm/linux behavior\n this._activeBuffer.lines.splice(row, 1);\n this._activeBuffer.lines.splice(j, 0, this._activeBuffer.getBlankLine(this._eraseAttrData()));\n }\n\n this._dirtyRowTracker.markRangeDirty(this._activeBuffer.y, this._activeBuffer.scrollBottom);\n this._activeBuffer.x = 0; // see https://vt100.net/docs/vt220-rm/chapter4.html - vt220 only?\n return true;\n }\n\n /**\n * CSI Ps @\n * Insert Ps (Blank) Character(s) (default = 1) (ICH).\n *\n * @vt: #Y CSI ICH \"Insert Characters\" \"CSI Ps @\" \"Insert `Ps` (blank) characters (default = 1).\"\n * The ICH sequence inserts `Ps` blank characters. The cursor remains at the beginning of the\n * blank characters. Text between the cursor and right margin moves to the right. Characters moved\n * past the right margin are lost.\n *\n *\n * FIXME: check against xterm - should not work outside of scroll margins (see VT520 manual)\n */\n public insertChars(params: IParams): boolean {\n this._restrictCursor();\n const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y);\n if (line) {\n line.insertCells(\n this._activeBuffer.x,\n params.params[0] || 1,\n this._activeBuffer.getNullCell(this._eraseAttrData())\n );\n this._dirtyRowTracker.markDirty(this._activeBuffer.y);\n }\n return true;\n }\n\n /**\n * CSI Ps P\n * Delete Ps Character(s) (default = 1) (DCH).\n *\n * @vt: #Y CSI DCH \"Delete Character\" \"CSI Ps P\" \"Delete `Ps` characters (default=1).\"\n * As characters are deleted, the remaining characters between the cursor and right margin move to\n * the left. Character attributes move with the characters. The terminal adds blank characters at\n * the right margin.\n *\n *\n * FIXME: check against xterm - should not work outside of scroll margins (see VT520 manual)\n */\n public deleteChars(params: IParams): boolean {\n this._restrictCursor();\n const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y);\n if (line) {\n line.deleteCells(\n this._activeBuffer.x,\n params.params[0] || 1,\n this._activeBuffer.getNullCell(this._eraseAttrData())\n );\n this._dirtyRowTracker.markDirty(this._activeBuffer.y);\n }\n return true;\n }\n\n /**\n * CSI Ps S Scroll up Ps lines (default = 1) (SU).\n *\n * @vt: #Y CSI SU \"Scroll Up\" \"CSI Ps S\" \"Scroll `Ps` lines up (default=1).\"\n *\n *\n * FIXME: scrolled out lines at top = 1 should add to scrollback (xterm)\n */\n public scrollUp(params: IParams): boolean {\n let param = params.params[0] || 1;\n\n while (param--) {\n this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollTop, 1);\n this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollBottom, 0, this._activeBuffer.getBlankLine(this._eraseAttrData()));\n }\n this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);\n return true;\n }\n\n /**\n * CSI Ps T Scroll down Ps lines (default = 1) (SD).\n *\n * @vt: #Y CSI SD \"Scroll Down\" \"CSI Ps T\" \"Scroll `Ps` lines down (default=1).\"\n */\n public scrollDown(params: IParams): boolean {\n let param = params.params[0] || 1;\n\n while (param--) {\n this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollBottom, 1);\n this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollTop, 0, this._activeBuffer.getBlankLine(DEFAULT_ATTR_DATA));\n }\n this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);\n return true;\n }\n\n /**\n * CSI Ps SP @ Scroll left Ps columns (default = 1) (SL) ECMA-48\n *\n * Notation: (Pn)\n * Representation: CSI Pn 02/00 04/00\n * Parameter default value: Pn = 1\n * SL causes the data in the presentation component to be moved by n character positions\n * if the line orientation is horizontal, or by n line positions if the line orientation\n * is vertical, such that the data appear to move to the left; where n equals the value of Pn.\n * The active presentation position is not affected by this control function.\n *\n * Supported:\n * - always left shift (no line orientation setting respected)\n *\n * @vt: #Y CSI SL \"Scroll Left\" \"CSI Ps SP @\" \"Scroll viewport `Ps` times to the left.\"\n * SL moves the content of all lines within the scroll margins `Ps` times to the left.\n * SL has no effect outside of the scroll margins.\n */\n public scrollLeft(params: IParams): boolean {\n if (this._activeBuffer.y > this._activeBuffer.scrollBottom || this._activeBuffer.y < this._activeBuffer.scrollTop) {\n return true;\n }\n const param = params.params[0] || 1;\n for (let y = this._activeBuffer.scrollTop; y <= this._activeBuffer.scrollBottom; ++y) {\n const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!;\n line.deleteCells(0, param, this._activeBuffer.getNullCell(this._eraseAttrData()));\n line.isWrapped = false;\n }\n this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);\n return true;\n }\n\n /**\n * CSI Ps SP A Scroll right Ps columns (default = 1) (SR) ECMA-48\n *\n * Notation: (Pn)\n * Representation: CSI Pn 02/00 04/01\n * Parameter default value: Pn = 1\n * SR causes the data in the presentation component to be moved by n character positions\n * if the line orientation is horizontal, or by n line positions if the line orientation\n * is vertical, such that the data appear to move to the right; where n equals the value of Pn.\n * The active presentation position is not affected by this control function.\n *\n * Supported:\n * - always right shift (no line orientation setting respected)\n *\n * @vt: #Y CSI SR \"Scroll Right\" \"CSI Ps SP A\" \"Scroll viewport `Ps` times to the right.\"\n * SL moves the content of all lines within the scroll margins `Ps` times to the right.\n * Content at the right margin is lost.\n * SL has no effect outside of the scroll margins.\n */\n public scrollRight(params: IParams): boolean {\n if (this._activeBuffer.y > this._activeBuffer.scrollBottom || this._activeBuffer.y < this._activeBuffer.scrollTop) {\n return true;\n }\n const param = params.params[0] || 1;\n for (let y = this._activeBuffer.scrollTop; y <= this._activeBuffer.scrollBottom; ++y) {\n const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!;\n line.insertCells(0, param, this._activeBuffer.getNullCell(this._eraseAttrData()));\n line.isWrapped = false;\n }\n this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);\n return true;\n }\n\n /**\n * CSI Pm ' }\n * Insert Ps Column(s) (default = 1) (DECIC), VT420 and up.\n *\n * @vt: #Y CSI DECIC \"Insert Columns\" \"CSI Ps ' }\" \"Insert `Ps` columns at cursor position.\"\n * DECIC inserts `Ps` times blank columns at the cursor position for all lines with the scroll\n * margins, moving content to the right. Content at the right margin is lost. DECIC has no effect\n * outside the scrolling margins.\n */\n public insertColumns(params: IParams): boolean {\n if (this._activeBuffer.y > this._activeBuffer.scrollBottom || this._activeBuffer.y < this._activeBuffer.scrollTop) {\n return true;\n }\n const param = params.params[0] || 1;\n for (let y = this._activeBuffer.scrollTop; y <= this._activeBuffer.scrollBottom; ++y) {\n const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!;\n line.insertCells(this._activeBuffer.x, param, this._activeBuffer.getNullCell(this._eraseAttrData()));\n line.isWrapped = false;\n }\n this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);\n return true;\n }\n\n /**\n * CSI Pm ' ~\n * Delete Ps Column(s) (default = 1) (DECDC), VT420 and up.\n *\n * @vt: #Y CSI DECDC \"Delete Columns\" \"CSI Ps ' ~\" \"Delete `Ps` columns at cursor position.\"\n * DECDC deletes `Ps` times columns at the cursor position for all lines with the scroll margins,\n * moving content to the left. Blank columns are added at the right margin.\n * DECDC has no effect outside the scrolling margins.\n */\n public deleteColumns(params: IParams): boolean {\n if (this._activeBuffer.y > this._activeBuffer.scrollBottom || this._activeBuffer.y < this._activeBuffer.scrollTop) {\n return true;\n }\n const param = params.params[0] || 1;\n for (let y = this._activeBuffer.scrollTop; y <= this._activeBuffer.scrollBottom; ++y) {\n const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!;\n line.deleteCells(this._activeBuffer.x, param, this._activeBuffer.getNullCell(this._eraseAttrData()));\n line.isWrapped = false;\n }\n this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);\n return true;\n }\n\n /**\n * CSI Ps X\n * Erase Ps Character(s) (default = 1) (ECH).\n *\n * @vt: #Y CSI ECH \"Erase Character\" \"CSI Ps X\" \"Erase `Ps` characters from current cursor position to the right (default=1).\"\n * ED erases `Ps` characters from current cursor position to the right.\n * ED works inside or outside the scrolling margins.\n */\n public eraseChars(params: IParams): boolean {\n this._restrictCursor();\n const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y);\n if (line) {\n line.replaceCells(\n this._activeBuffer.x,\n this._activeBuffer.x + (params.params[0] || 1),\n this._activeBuffer.getNullCell(this._eraseAttrData())\n );\n this._dirtyRowTracker.markDirty(this._activeBuffer.y);\n }\n return true;\n }\n\n /**\n * CSI Ps b Repeat the preceding graphic character Ps times (REP).\n * From ECMA 48 (@see http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-048.pdf)\n * Notation: (Pn)\n * Representation: CSI Pn 06/02\n * Parameter default value: Pn = 1\n * REP is used to indicate that the preceding character in the data stream,\n * if it is a graphic character (represented by one or more bit combinations) including SPACE,\n * is to be repeated n times, where n equals the value of Pn.\n * If the character preceding REP is a control function or part of a control function,\n * the effect of REP is not defined by this Standard.\n *\n * We extend xterm's behavior to allow repeating entire grapheme clusters.\n * This isn't 100% xterm-compatible, but it seems saner and more useful.\n * - text attrs are applied normally\n * - wrap around is respected\n * - any valid sequence resets the carried forward char\n *\n * Note: To get reset on a valid sequence working correctly without much runtime penalty, the\n * preceding codepoint is stored on the parser in `this.print` and reset during `parser.parse`.\n *\n * @vt: #Y CSI REP \"Repeat Preceding Character\" \"CSI Ps b\" \"Repeat preceding character `Ps` times (default=1).\"\n * REP repeats the previous character `Ps` times advancing the cursor, also wrapping if DECAWM is\n * set. REP has no effect if the sequence does not follow a printable ASCII character\n * (NOOP for any other sequence in between or NON ASCII characters).\n */\n public repeatPrecedingCharacter(params: IParams): boolean {\n const joinState = this._parser.precedingJoinState;\n if (!joinState) {\n return true;\n }\n // call print to insert the chars and handle correct wrapping\n const length = params.params[0] || 1;\n const chWidth = UnicodeService.extractWidth(joinState);\n const x = this._activeBuffer.x - chWidth;\n const bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!;\n const text = bufferRow.getString(x);\n const data = new Uint32Array(text.length * length);\n let idata = 0;\n for (let itext = 0; itext < text.length;) {\n const ch = text.codePointAt(itext) || 0;\n data[idata++] = ch;\n itext += ch > 0xffff ? 2 : 1;\n }\n let tlength = idata;\n for (let i = 1; i < length; ++i) {\n data.copyWithin(tlength, 0, idata);\n tlength += idata;\n }\n this.print(data, 0, tlength);\n return true;\n }\n\n /**\n * CSI Ps c Send Device Attributes (Primary DA).\n * Ps = 0 or omitted -> request attributes from terminal. The\n * response depends on the decTerminalID resource setting.\n * -> CSI ? 1 ; 2 c (``VT100 with Advanced Video Option'')\n * -> CSI ? 1 ; 0 c (``VT101 with No Options'')\n * -> CSI ? 6 c (``VT102'')\n * -> CSI ? 6 0 ; 1 ; 2 ; 6 ; 8 ; 9 ; 1 5 ; c (``VT220'')\n * The VT100-style response parameters do not mean anything by\n * themselves. VT220 parameters do, telling the host what fea-\n * tures the terminal supports:\n * Ps = 1 -> 132-columns.\n * Ps = 2 -> Printer.\n * Ps = 6 -> Selective erase.\n * Ps = 8 -> User-defined keys.\n * Ps = 9 -> National replacement character sets.\n * Ps = 1 5 -> Technical characters.\n * Ps = 2 2 -> ANSI color, e.g., VT525.\n * Ps = 2 9 -> ANSI text locator (i.e., DEC Locator mode).\n *\n * @vt: #Y CSI DA1 \"Primary Device Attributes\" \"CSI c\" \"Send primary device attributes.\"\n *\n *\n * TODO: fix and cleanup response\n */\n public sendDeviceAttributesPrimary(params: IParams): boolean {\n if (params.params[0] > 0) {\n return true;\n }\n if (this._is('xterm') || this._is('rxvt-unicode') || this._is('screen')) {\n this._coreService.triggerDataEvent(C0.ESC + '[?1;2c');\n } else if (this._is('linux')) {\n this._coreService.triggerDataEvent(C0.ESC + '[?6c');\n }\n return true;\n }\n\n /**\n * CSI > Ps c\n * Send Device Attributes (Secondary DA).\n * Ps = 0 or omitted -> request the terminal's identification\n * code. The response depends on the decTerminalID resource set-\n * ting. It should apply only to VT220 and up, but xterm extends\n * this to VT100.\n * -> CSI > Pp ; Pv ; Pc c\n * where Pp denotes the terminal type\n * Pp = 0 -> ``VT100''.\n * Pp = 1 -> ``VT220''.\n * and Pv is the firmware version (for xterm, this was originally\n * the XFree86 patch number, starting with 95). In a DEC termi-\n * nal, Pc indicates the ROM cartridge registration number and is\n * always zero.\n * More information:\n * xterm/charproc.c - line 2012, for more information.\n * vim responds with ^[[?0c or ^[[?1c after the terminal's response (?)\n *\n * @vt: #Y CSI DA2 \"Secondary Device Attributes\" \"CSI > c\" \"Send primary device attributes.\"\n *\n *\n * TODO: fix and cleanup response\n */\n public sendDeviceAttributesSecondary(params: IParams): boolean {\n if (params.params[0] > 0) {\n return true;\n }\n // xterm and urxvt\n // seem to spit this\n // out around ~370 times (?).\n if (this._is('xterm')) {\n this._coreService.triggerDataEvent(C0.ESC + '[>0;276;0c');\n } else if (this._is('rxvt-unicode')) {\n this._coreService.triggerDataEvent(C0.ESC + '[>85;95;0c');\n } else if (this._is('linux')) {\n // not supported by linux console.\n // linux console echoes parameters.\n this._coreService.triggerDataEvent(params.params[0] + 'c');\n } else if (this._is('screen')) {\n this._coreService.triggerDataEvent(C0.ESC + '[>83;40003;0c');\n }\n return true;\n }\n\n /**\n * CSI > Ps q\n * Ps = 0 => Report xterm name and version (XTVERSION).\n *\n * The response is a DCS sequence identifying the version: DCS > | text ST\n *\n * @vt: #Y CSI XTVERSION \"Report Xterm Version\" \"CSI > q\" \"Report the terminal name and version.\"\n */\n public sendXtVersion(params: IParams): boolean {\n if (params.params[0] > 0) {\n return true;\n }\n this._coreService.triggerDataEvent(`${C0.ESC}P>|xterm.js(${XTERM_VERSION})${C0.ESC}\\\\`);\n return true;\n }\n\n /**\n * Evaluate if the current terminal is the given argument.\n * @param term The terminal name to evaluate\n */\n private _is(term: string): boolean {\n return (this._optionsService.rawOptions.termName + '').startsWith(term);\n }\n\n /**\n * CSI Pm h Set Mode (SM).\n * Ps = 2 -> Keyboard Action Mode (AM).\n * Ps = 4 -> Insert Mode (IRM).\n * Ps = 1 2 -> Send/receive (SRM).\n * Ps = 2 0 -> Automatic Newline (LNM).\n *\n * @vt: #P[Only IRM is supported.] CSI SM \"Set Mode\" \"CSI Pm h\" \"Set various terminal modes.\"\n * Supported param values by SM:\n *\n * | Param | Action | Support |\n * | ----- | -------------------------------------- | ------- |\n * | 2 | Keyboard Action Mode (KAM). Always on. | #N |\n * | 4 | Insert Mode (IRM). | #Y |\n * | 12 | Send/receive (SRM). Always off. | #N |\n * | 20 | Automatic Newline (LNM). | #Y |\n */\n public setMode(params: IParams): boolean {\n for (let i = 0; i < params.length; i++) {\n switch (params.params[i]) {\n case 4:\n this._coreService.modes.insertMode = true;\n break;\n case 20:\n this._optionsService.options.convertEol = true;\n break;\n }\n }\n return true;\n }\n\n /**\n * CSI ? Pm h\n * DEC Private Mode Set (DECSET).\n * Ps = 1 -> Application Cursor Keys (DECCKM).\n * Ps = 2 -> Designate USASCII for character sets G0-G3\n * (DECANM), and set VT100 mode.\n * Ps = 3 -> 132 Column Mode (DECCOLM).\n * Ps = 4 -> Smooth (Slow) Scroll (DECSCLM).\n * Ps = 5 -> Reverse Video (DECSCNM).\n * Ps = 6 -> Origin Mode (DECOM).\n * Ps = 7 -> Wraparound Mode (DECAWM).\n * Ps = 8 -> Auto-repeat Keys (DECARM).\n * Ps = 9 -> Send Mouse X & Y on button press. See the sec-\n * tion Mouse Tracking.\n * Ps = 1 0 -> Show toolbar (rxvt).\n * Ps = 1 2 -> Start Blinking Cursor (att610).\n * Ps = 1 8 -> Print form feed (DECPFF).\n * Ps = 1 9 -> Set print extent to full screen (DECPEX).\n * Ps = 2 5 -> Show Cursor (DECTCEM).\n * Ps = 3 0 -> Show scrollbar (rxvt).\n * Ps = 3 5 -> Enable font-shifting functions (rxvt).\n * Ps = 3 8 -> Enter Tektronix Mode (DECTEK).\n * Ps = 4 0 -> Allow 80 -> 132 Mode.\n * Ps = 4 1 -> more(1) fix (see curses resource).\n * Ps = 4 2 -> Enable Nation Replacement Character sets (DECN-\n * RCM).\n * Ps = 4 4 -> Turn On Margin Bell.\n * Ps = 4 5 -> Reverse-wraparound Mode.\n * Ps = 4 6 -> Start Logging. This is normally disabled by a\n * compile-time option.\n * Ps = 4 7 -> Use Alternate Screen Buffer. (This may be dis-\n * abled by the titeInhibit resource).\n * Ps = 6 6 -> Application keypad (DECNKM).\n * Ps = 6 7 -> Backarrow key sends backspace (DECBKM).\n * Ps = 1 0 0 0 -> Send Mouse X & Y on button press and\n * release. See the section Mouse Tracking.\n * Ps = 1 0 0 1 -> Use Hilite Mouse Tracking.\n * Ps = 1 0 0 2 -> Use Cell Motion Mouse Tracking.\n * Ps = 1 0 0 3 -> Use All Motion Mouse Tracking.\n * Ps = 1 0 0 4 -> Send FocusIn/FocusOut events.\n * Ps = 1 0 0 5 -> Enable Extended Mouse Mode.\n * Ps = 1 0 1 0 -> Scroll to bottom on tty output (rxvt).\n * Ps = 1 0 1 1 -> Scroll to bottom on key press (rxvt).\n * Ps = 1 0 3 4 -> Interpret \"meta\" key, sets eighth bit.\n * (enables the eightBitInput resource).\n * Ps = 1 0 3 5 -> Enable special modifiers for Alt and Num-\n * Lock keys. (This enables the numLock resource).\n * Ps = 1 0 3 6 -> Send ESC when Meta modifies a key. (This\n * enables the metaSendsEscape resource).\n * Ps = 1 0 3 7 -> Send DEL from the editing-keypad Delete\n * key.\n * Ps = 1 0 3 9 -> Send ESC when Alt modifies a key. (This\n * enables the altSendsEscape resource).\n * Ps = 1 0 4 0 -> Keep selection even if not highlighted.\n * (This enables the keepSelection resource).\n * Ps = 1 0 4 1 -> Use the CLIPBOARD selection. (This enables\n * the selectToClipboard resource).\n * Ps = 1 0 4 2 -> Enable Urgency window manager hint when\n * Control-G is received. (This enables the bellIsUrgent\n * resource).\n * Ps = 1 0 4 3 -> Enable raising of the window when Control-G\n * is received. (enables the popOnBell resource).\n * Ps = 1 0 4 7 -> Use Alternate Screen Buffer. (This may be\n * disabled by the titeInhibit resource).\n * Ps = 1 0 4 8 -> Save cursor as in DECSC. (This may be dis-\n * abled by the titeInhibit resource).\n * Ps = 1 0 4 9 -> Save cursor as in DECSC and use Alternate\n * Screen Buffer, clearing it first. (This may be disabled by\n * the titeInhibit resource). This combines the effects of the 1\n * 0 4 7 and 1 0 4 8 modes. Use this with terminfo-based\n * applications rather than the 4 7 mode.\n * Ps = 1 0 5 0 -> Set terminfo/termcap function-key mode.\n * Ps = 1 0 5 1 -> Set Sun function-key mode.\n * Ps = 1 0 5 2 -> Set HP function-key mode.\n * Ps = 1 0 5 3 -> Set SCO function-key mode.\n * Ps = 1 0 6 0 -> Set legacy keyboard emulation (X11R6).\n * Ps = 1 0 6 1 -> Set VT220 keyboard emulation.\n * Ps = 2 0 0 4 -> Set bracketed paste mode.\n * Modes:\n * http: *vt100.net/docs/vt220-rm/chapter4.html\n *\n * @vt: #P[See below for supported modes.] CSI DECSET \"DEC Private Set Mode\" \"CSI ? Pm h\" \"Set various terminal attributes.\"\n * Supported param values by DECSET:\n *\n * | param | Action | Support |\n * | ----- | ------------------------------------------------------- | --------|\n * | 1 | Application Cursor Keys (DECCKM). | #Y |\n * | 2 | Designate US-ASCII for character sets G0-G3 (DECANM). | #Y |\n * | 3 | 132 Column Mode (DECCOLM). | #Y |\n * | 6 | Origin Mode (DECOM). | #Y |\n * | 7 | Auto-wrap Mode (DECAWM). | #Y |\n * | 8 | Auto-repeat Keys (DECARM). Always on. | #N |\n * | 9 | X10 xterm mouse protocol. | #Y |\n * | 12 | Start Blinking Cursor. | #P[Requires the allowSetCursorBlink quirk option enabled.] |\n * | 25 | Show Cursor (DECTCEM). | #Y |\n * | 45 | Reverse wrap-around. | #Y |\n * | 47 | Use Alternate Screen Buffer. | #Y |\n * | 66 | Application keypad (DECNKM). | #Y |\n * | 1000 | X11 xterm mouse protocol. | #Y |\n * | 1002 | Use Cell Motion Mouse Tracking. | #Y |\n * | 1003 | Use All Motion Mouse Tracking. | #Y |\n * | 1004 | Send FocusIn/FocusOut events | #Y |\n * | 1005 | Enable UTF-8 Mouse Mode. | #N |\n * | 1006 | Enable SGR Mouse Mode. | #Y |\n * | 1015 | Enable urxvt Mouse Mode. | #N |\n * | 1016 | Enable SGR-Pixels Mouse Mode. | #Y |\n * | 1047 | Use Alternate Screen Buffer. | #Y |\n * | 1048 | Save cursor as in DECSC. | #Y |\n * | 1049 | Save cursor and switch to alternate buffer clearing it. | #P[Does not clear the alternate buffer.] |\n * | 2004 | Set bracketed paste mode. | #Y |\n *\n *\n * FIXME: implement DECSCNM, 1049 should clear altbuffer\n */\n public setModePrivate(params: IParams): boolean {\n for (let i = 0; i < params.length; i++) {\n switch (params.params[i]) {\n case 1:\n this._coreService.decPrivateModes.applicationCursorKeys = true;\n break;\n case 2:\n this._charsetService.setgCharset(0, DEFAULT_CHARSET);\n this._charsetService.setgCharset(1, DEFAULT_CHARSET);\n this._charsetService.setgCharset(2, DEFAULT_CHARSET);\n this._charsetService.setgCharset(3, DEFAULT_CHARSET);\n // set VT100 mode here\n break;\n case 3:\n /**\n * DECCOLM - 132 column mode.\n * This is only active if 'SetWinLines' (24) is enabled\n * through `options.windowsOptions`.\n */\n if (this._optionsService.rawOptions.windowOptions.setWinLines) {\n this._bufferService.resize(132, this._bufferService.rows);\n this._onRequestReset.fire();\n }\n break;\n case 6:\n this._coreService.decPrivateModes.origin = true;\n this._setCursor(0, 0);\n break;\n case 7:\n this._coreService.decPrivateModes.wraparound = true;\n break;\n case 12:\n if (this._optionsService.rawOptions.quirks?.allowSetCursorBlink) {\n this._optionsService.options.cursorBlink = true;\n }\n break;\n case 45:\n this._coreService.decPrivateModes.reverseWraparound = true;\n break;\n case 66:\n this._logService.debug('Serial port requested application keypad.');\n this._coreService.decPrivateModes.applicationKeypad = true;\n this._onRequestSyncScrollBar.fire();\n break;\n case 9: // X10 Mouse\n // no release, no motion, no wheel, no modifiers.\n this._mouseStateService.activeProtocol = 'X10';\n break;\n case 1000: // vt200 mouse\n // no motion.\n this._mouseStateService.activeProtocol = 'VT200';\n break;\n case 1002: // button event mouse\n this._mouseStateService.activeProtocol = 'DRAG';\n break;\n case 1003: // any event mouse\n // any event - sends motion events,\n // even if there is no button held down.\n this._mouseStateService.activeProtocol = 'ANY';\n break;\n case 1004: // send focusin/focusout events\n // focusin: ^[[I\n // focusout: ^[[O\n this._coreService.decPrivateModes.sendFocus = true;\n this._onRequestSendFocus.fire();\n break;\n case 1005: // utf8 ext mode mouse - removed in #2507\n this._logService.debug('DECSET 1005 not supported (see #2507)');\n break;\n case 1006: // sgr ext mode mouse\n this._mouseStateService.activeEncoding = 'SGR';\n break;\n case 1015: // urxvt ext mode mouse - removed in #2507\n this._logService.debug('DECSET 1015 not supported (see #2507)');\n break;\n case 1016: // sgr pixels mode mouse\n this._mouseStateService.activeEncoding = 'SGR_PIXELS';\n break;\n case 25: // show cursor\n this._coreService.isCursorHidden = false;\n break;\n case 1048: // alt screen cursor\n this.saveCursor();\n break;\n case 1049: // alt screen buffer cursor\n this.saveCursor();\n // FALL-THROUGH\n case 47: // alt screen buffer\n case 1047: // alt screen buffer\n // Swap kitty keyboard flags: save main, restore alt\n if (this._optionsService.rawOptions.vtExtensions?.kittyKeyboard) {\n const state = this._coreService.kittyKeyboard;\n state.mainFlags = state.flags;\n state.flags = state.altFlags;\n }\n this._bufferService.buffers.activateAltBuffer(this._eraseAttrData());\n this._coreService.isCursorInitialized = true;\n this._onRequestRefreshRows.fire(undefined);\n this._onRequestSyncScrollBar.fire();\n break;\n case 2004: // bracketed paste mode (https://cirw.in/blog/bracketed-paste)\n this._coreService.decPrivateModes.bracketedPasteMode = true;\n break;\n case 2026: // synchronized output (https://github.com/contour-terminal/vt-extensions/blob/master/synchronized-output.md)\n this._coreService.decPrivateModes.synchronizedOutput = true;\n break;\n case 2031: // color scheme updates (https://contour-terminal.org/vt-extensions/color-palette-update-notifications/)\n if (this._optionsService.rawOptions.vtExtensions?.colorSchemeQuery ?? true) {\n this._coreService.decPrivateModes.colorSchemeUpdates = true;\n }\n break;\n case 9001: // win32-input-mode (https://github.com/microsoft/terminal/blob/main/doc/specs/%234999%20-%20Improved%20keyboard%20handling%20in%20Conpty.md)\n if (this._optionsService.rawOptions.vtExtensions?.win32InputMode) {\n this._coreService.decPrivateModes.win32InputMode = true;\n }\n break;\n }\n }\n return true;\n }\n\n\n /**\n * CSI Pm l Reset Mode (RM).\n * Ps = 2 -> Keyboard Action Mode (AM).\n * Ps = 4 -> Replace Mode (IRM).\n * Ps = 1 2 -> Send/receive (SRM).\n * Ps = 2 0 -> Normal Linefeed (LNM).\n *\n * @vt: #P[Only IRM is supported.] CSI RM \"Reset Mode\" \"CSI Pm l\" \"Set various terminal attributes.\"\n * Supported param values by RM:\n *\n * | Param | Action | Support |\n * | ----- | -------------------------------------- | ------- |\n * | 2 | Keyboard Action Mode (KAM). Always on. | #N |\n * | 4 | Replace Mode (IRM). (default) | #Y |\n * | 12 | Send/receive (SRM). Always off. | #N |\n * | 20 | Normal Linefeed (LNM). | #Y |\n *\n *\n * FIXME: why is LNM commented out?\n */\n public resetMode(params: IParams): boolean {\n for (let i = 0; i < params.length; i++) {\n switch (params.params[i]) {\n case 4:\n this._coreService.modes.insertMode = false;\n break;\n case 20:\n this._optionsService.options.convertEol = false;\n break;\n }\n }\n return true;\n }\n\n /**\n * CSI ? Pm l\n * DEC Private Mode Reset (DECRST).\n * Ps = 1 -> Normal Cursor Keys (DECCKM).\n * Ps = 2 -> Designate VT52 mode (DECANM).\n * Ps = 3 -> 80 Column Mode (DECCOLM).\n * Ps = 4 -> Jump (Fast) Scroll (DECSCLM).\n * Ps = 5 -> Normal Video (DECSCNM).\n * Ps = 6 -> Normal Cursor Mode (DECOM).\n * Ps = 7 -> No Wraparound Mode (DECAWM).\n * Ps = 8 -> No Auto-repeat Keys (DECARM).\n * Ps = 9 -> Don't send Mouse X & Y on button press.\n * Ps = 1 0 -> Hide toolbar (rxvt).\n * Ps = 1 2 -> Stop Blinking Cursor (att610).\n * Ps = 1 8 -> Don't print form feed (DECPFF).\n * Ps = 1 9 -> Limit print to scrolling region (DECPEX).\n * Ps = 2 5 -> Hide Cursor (DECTCEM).\n * Ps = 3 0 -> Don't show scrollbar (rxvt).\n * Ps = 3 5 -> Disable font-shifting functions (rxvt).\n * Ps = 4 0 -> Disallow 80 -> 132 Mode.\n * Ps = 4 1 -> No more(1) fix (see curses resource).\n * Ps = 4 2 -> Disable Nation Replacement Character sets (DEC-\n * NRCM).\n * Ps = 4 4 -> Turn Off Margin Bell.\n * Ps = 4 5 -> No Reverse-wraparound Mode.\n * Ps = 4 6 -> Stop Logging. (This is normally disabled by a\n * compile-time option).\n * Ps = 4 7 -> Use Normal Screen Buffer.\n * Ps = 6 6 -> Numeric keypad (DECNKM).\n * Ps = 6 7 -> Backarrow key sends delete (DECBKM).\n * Ps = 1 0 0 0 -> Don't send Mouse X & Y on button press and\n * release. See the section Mouse Tracking.\n * Ps = 1 0 0 1 -> Don't use Hilite Mouse Tracking.\n * Ps = 1 0 0 2 -> Don't use Cell Motion Mouse Tracking.\n * Ps = 1 0 0 3 -> Don't use All Motion Mouse Tracking.\n * Ps = 1 0 0 4 -> Don't send FocusIn/FocusOut events.\n * Ps = 1 0 0 5 -> Disable Extended Mouse Mode.\n * Ps = 1 0 1 0 -> Don't scroll to bottom on tty output\n * (rxvt).\n * Ps = 1 0 1 1 -> Don't scroll to bottom on key press (rxvt).\n * Ps = 1 0 3 4 -> Don't interpret \"meta\" key. (This disables\n * the eightBitInput resource).\n * Ps = 1 0 3 5 -> Disable special modifiers for Alt and Num-\n * Lock keys. (This disables the numLock resource).\n * Ps = 1 0 3 6 -> Don't send ESC when Meta modifies a key.\n * (This disables the metaSendsEscape resource).\n * Ps = 1 0 3 7 -> Send VT220 Remove from the editing-keypad\n * Delete key.\n * Ps = 1 0 3 9 -> Don't send ESC when Alt modifies a key.\n * (This disables the altSendsEscape resource).\n * Ps = 1 0 4 0 -> Do not keep selection when not highlighted.\n * (This disables the keepSelection resource).\n * Ps = 1 0 4 1 -> Use the PRIMARY selection. (This disables\n * the selectToClipboard resource).\n * Ps = 1 0 4 2 -> Disable Urgency window manager hint when\n * Control-G is received. (This disables the bellIsUrgent\n * resource).\n * Ps = 1 0 4 3 -> Disable raising of the window when Control-\n * G is received. (This disables the popOnBell resource).\n * Ps = 1 0 4 7 -> Use Normal Screen Buffer, clearing screen\n * first if in the Alternate Screen. (This may be disabled by\n * the titeInhibit resource).\n * Ps = 1 0 4 8 -> Restore cursor as in DECRC. (This may be\n * disabled by the titeInhibit resource).\n * Ps = 1 0 4 9 -> Use Normal Screen Buffer and restore cursor\n * as in DECRC. (This may be disabled by the titeInhibit\n * resource). This combines the effects of the 1 0 4 7 and 1 0\n * 4 8 modes. Use this with terminfo-based applications rather\n * than the 4 7 mode.\n * Ps = 1 0 5 0 -> Reset terminfo/termcap function-key mode.\n * Ps = 1 0 5 1 -> Reset Sun function-key mode.\n * Ps = 1 0 5 2 -> Reset HP function-key mode.\n * Ps = 1 0 5 3 -> Reset SCO function-key mode.\n * Ps = 1 0 6 0 -> Reset legacy keyboard emulation (X11R6).\n * Ps = 1 0 6 1 -> Reset keyboard emulation to Sun/PC style.\n * Ps = 2 0 0 4 -> Reset bracketed paste mode.\n *\n * @vt: #P[See below for supported modes.] CSI DECRST \"DEC Private Reset Mode\" \"CSI ? Pm l\" \"Reset various terminal attributes.\"\n * Supported param values by DECRST:\n *\n * | param | Action | Support |\n * | ----- | ------------------------------------------------------- | ------- |\n * | 1 | Normal Cursor Keys (DECCKM). | #Y |\n * | 2 | Designate VT52 mode (DECANM). | #N |\n * | 3 | 80 Column Mode (DECCOLM). | #B[Switches to old column width instead of 80.] |\n * | 6 | Normal Cursor Mode (DECOM). | #Y |\n * | 7 | No Wraparound Mode (DECAWM). | #Y |\n * | 8 | No Auto-repeat Keys (DECARM). | #N |\n * | 9 | Don't send Mouse X & Y on button press. | #Y |\n * | 12 | Stop Blinking Cursor. | #P[Requires the allowSetCursorBlink quirk option enabled.] |\n * | 25 | Hide Cursor (DECTCEM). | #Y |\n * | 45 | No reverse wrap-around. | #Y |\n * | 47 | Use Normal Screen Buffer. | #Y |\n * | 66 | Numeric keypad (DECNKM). | #Y |\n * | 1000 | Don't send Mouse reports. | #Y |\n * | 1002 | Don't use Cell Motion Mouse Tracking. | #Y |\n * | 1003 | Don't use All Motion Mouse Tracking. | #Y |\n * | 1004 | Don't send FocusIn/FocusOut events. | #Y |\n * | 1005 | Disable UTF-8 Mouse Mode. | #N |\n * | 1006 | Disable SGR Mouse Mode. | #Y |\n * | 1015 | Disable urxvt Mouse Mode. | #N |\n * | 1016 | Disable SGR-Pixels Mouse Mode. | #Y |\n * | 1047 | Use Normal Screen Buffer (clearing screen if in alt). | #Y |\n * | 1048 | Restore cursor as in DECRC. | #Y |\n * | 1049 | Use Normal Screen Buffer and restore cursor. | #Y |\n * | 2004 | Reset bracketed paste mode. | #Y |\n *\n *\n * FIXME: DECCOLM is currently broken (already fixed in window options PR)\n */\n public resetModePrivate(params: IParams): boolean {\n for (let i = 0; i < params.length; i++) {\n switch (params.params[i]) {\n case 1:\n this._coreService.decPrivateModes.applicationCursorKeys = false;\n break;\n case 3:\n /**\n * DECCOLM - 80 column mode.\n * This is only active if 'SetWinLines' (24) is enabled\n * through `options.windowsOptions`.\n */\n if (this._optionsService.rawOptions.windowOptions.setWinLines) {\n this._bufferService.resize(80, this._bufferService.rows);\n this._onRequestReset.fire();\n }\n break;\n case 6:\n this._coreService.decPrivateModes.origin = false;\n this._setCursor(0, 0);\n break;\n case 7:\n this._coreService.decPrivateModes.wraparound = false;\n break;\n case 12:\n if (this._optionsService.rawOptions.quirks?.allowSetCursorBlink) {\n this._optionsService.options.cursorBlink = false;\n }\n break;\n case 45:\n this._coreService.decPrivateModes.reverseWraparound = false;\n break;\n case 66:\n this._logService.debug('Switching back to normal keypad.');\n this._coreService.decPrivateModes.applicationKeypad = false;\n this._onRequestSyncScrollBar.fire();\n break;\n case 9: // X10 Mouse\n case 1000: // vt200 mouse\n case 1002: // button event mouse\n case 1003: // any event mouse\n this._mouseStateService.activeProtocol = 'NONE';\n break;\n case 1004: // send focusin/focusout events\n this._coreService.decPrivateModes.sendFocus = false;\n break;\n case 1005: // utf8 ext mode mouse - removed in #2507\n this._logService.debug('DECRST 1005 not supported (see #2507)');\n break;\n case 1006: // sgr ext mode mouse\n this._mouseStateService.activeEncoding = 'DEFAULT';\n break;\n case 1015: // urxvt ext mode mouse - removed in #2507\n this._logService.debug('DECRST 1015 not supported (see #2507)');\n break;\n case 1016: // sgr pixels mode mouse\n this._mouseStateService.activeEncoding = 'DEFAULT';\n break;\n case 25: // hide cursor\n this._coreService.isCursorHidden = true;\n break;\n case 1048: // alt screen cursor\n this.restoreCursor();\n break;\n case 1049: // alt screen buffer cursor\n // FALL-THROUGH\n case 47: // normal screen buffer\n case 1047: // normal screen buffer - clearing it first\n // Swap kitty keyboard flags: save alt, restore main\n if (this._optionsService.rawOptions.vtExtensions?.kittyKeyboard) {\n const state = this._coreService.kittyKeyboard;\n state.altFlags = state.flags;\n state.flags = state.mainFlags;\n }\n // Ensure the selection manager has the correct buffer\n this._bufferService.buffers.activateNormalBuffer();\n if (params.params[i] === 1049) {\n this.restoreCursor();\n }\n this._coreService.isCursorInitialized = true;\n this._onRequestRefreshRows.fire(undefined);\n this._onRequestSyncScrollBar.fire();\n break;\n case 2004: // bracketed paste mode (https://cirw.in/blog/bracketed-paste)\n this._coreService.decPrivateModes.bracketedPasteMode = false;\n break;\n case 2026: // synchronized output (https://github.com/contour-terminal/vt-extensions/blob/master/synchronized-output.md)\n this._coreService.decPrivateModes.synchronizedOutput = false;\n this._onRequestRefreshRows.fire(undefined);\n break;\n case 2031: // color scheme updates (https://contour-terminal.org/vt-extensions/color-palette-update-notifications/)\n if (this._optionsService.rawOptions.vtExtensions?.colorSchemeQuery ?? true) {\n this._coreService.decPrivateModes.colorSchemeUpdates = false;\n }\n break;\n case 9001: // win32-input-mode\n if (this._optionsService.rawOptions.vtExtensions?.win32InputMode) {\n this._coreService.decPrivateModes.win32InputMode = false;\n }\n break;\n }\n }\n return true;\n }\n\n /**\n * CSI Ps $ p Request ANSI Mode (DECRQM).\n *\n * Reports CSI Ps; Pm $ y (DECRPM), where Ps is the mode number as in SM/RM,\n * and Pm is the mode value:\n * 0 - not recognized\n * 1 - set\n * 2 - reset\n * 3 - permanently set\n * 4 - permanently reset\n *\n * @vt: #Y CSI DECRQM \"Request Mode\" \"CSI Ps $p\" \"Request mode state.\"\n * Returns a report as `CSI Ps; Pm $ y` (DECRPM), where `Ps` is the mode number as in SM/RM\n * or DECSET/DECRST, and `Pm` is the mode value:\n * - 0: not recognized\n * - 1: set\n * - 2: reset\n * - 3: permanently set\n * - 4: permanently reset\n *\n * For modes not understood xterm.js always returns `notRecognized`. In general this means,\n * that a certain operation mode is not implemented and cannot be used.\n *\n * Modes changing the active terminal buffer (47, 1047, 1049) are not subqueried\n * and only report, whether the alternate buffer is set.\n *\n * Mouse encodings and mouse protocols are handled mutual exclusive,\n * thus only one of each of those can be set at a given time.\n *\n * There is a chance, that some mode reports are not fully in line with xterm.js' behavior,\n * e.g. if the default implementation already exposes a certain behavior. If you find\n * discrepancies in the mode reports, please file a bug.\n */\n public requestMode(params: IParams, ansi: boolean): boolean {\n // return value as in DECRPM\n const enum V {\n NOT_RECOGNIZED = 0,\n SET = 1,\n RESET = 2,\n PERMANENTLY_SET = 3,\n PERMANENTLY_RESET = 4\n }\n\n // access helpers\n const dm = this._coreService.decPrivateModes;\n const { activeProtocol: mouseProtocol, activeEncoding: mouseEncoding } = this._mouseStateService;\n const cs = this._coreService;\n const { buffers, cols } = this._bufferService;\n const { active, alt } = buffers;\n const opts = this._optionsService.rawOptions;\n\n const f = (m: number, v: V): boolean => {\n cs.triggerDataEvent(`${C0.ESC}[${ansi ? '' : '?'}${m};${v}$y`);\n return true;\n };\n const b2v = (value: boolean): V => value ? V.SET : V.RESET;\n\n const p = params.params[0];\n\n if (ansi) {\n if (p === 2) return f(p, V.PERMANENTLY_RESET);\n if (p === 4) return f(p, b2v(cs.modes.insertMode));\n if (p === 12) return f(p, V.PERMANENTLY_SET);\n if (p === 20) return f(p, b2v(opts.convertEol));\n return f(p, V.NOT_RECOGNIZED);\n }\n\n if (p === 1) return f(p, b2v(dm.applicationCursorKeys));\n if (p === 3) return f(p, opts.windowOptions.setWinLines ? (cols === 80 ? V.RESET : cols === 132 ? V.SET : V.NOT_RECOGNIZED) : V.NOT_RECOGNIZED);\n if (p === 6) return f(p, b2v(dm.origin));\n if (p === 7) return f(p, b2v(dm.wraparound));\n if (p === 8) return f(p, V.PERMANENTLY_SET);\n if (p === 9) return f(p, b2v(mouseProtocol === 'X10'));\n if (p === 12) return f(p, b2v(opts.cursorBlink));\n if (p === 25) return f(p, b2v(!cs.isCursorHidden));\n if (p === 45) return f(p, b2v(dm.reverseWraparound));\n if (p === 66) return f(p, b2v(dm.applicationKeypad));\n if (p === 67) return f(p, V.PERMANENTLY_RESET);\n if (p === 1000) return f(p, b2v(mouseProtocol === 'VT200'));\n if (p === 1002) return f(p, b2v(mouseProtocol === 'DRAG'));\n if (p === 1003) return f(p, b2v(mouseProtocol === 'ANY'));\n if (p === 1004) return f(p, b2v(dm.sendFocus));\n if (p === 1005) return f(p, V.PERMANENTLY_RESET);\n if (p === 1006) return f(p, b2v(mouseEncoding === 'SGR'));\n if (p === 1015) return f(p, V.PERMANENTLY_RESET);\n if (p === 1016) return f(p, b2v(mouseEncoding === 'SGR_PIXELS'));\n if (p === 1048) return f(p, V.SET); // xterm always returns SET here\n if (p === 47 || p === 1047 || p === 1049) return f(p, b2v(active === alt));\n if (p === 2004) return f(p, b2v(dm.bracketedPasteMode));\n if (p === 2026) return f(p, b2v(dm.synchronizedOutput));\n if (p === 9001) return this._optionsService.rawOptions.vtExtensions?.win32InputMode ? f(p, b2v(dm.win32InputMode)) : f(p, V.NOT_RECOGNIZED);\n return f(p, V.NOT_RECOGNIZED);\n }\n\n /**\n * Helper to write color information packed with color mode.\n */\n private _updateAttrColor(color: number, mode: number, c1: number, c2: number, c3: number): number {\n if (mode === 2) {\n color |= Attributes.CM_RGB;\n color &= ~Attributes.RGB_MASK;\n color |= AttributeData.fromColorRGB([c1, c2, c3]);\n } else if (mode === 5) {\n color &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);\n color |= Attributes.CM_P256 | (c1 & 0xff);\n }\n return color;\n }\n\n /**\n * Helper to extract and apply color params/subparams.\n * Returns advance for params index.\n */\n private _extractColor(params: IParams, pos: number, attr: IAttributeData): number {\n // normalize params\n // meaning: [target, CM, ign, val, val, val]\n // RGB : [ 38/48, 2, ign, r, g, b]\n // P256 : [ 38/48, 5, ign, v, ign, ign]\n const accu = [0, 0, -1, 0, 0, 0];\n\n // alignment placeholder for non color space sequences\n let cSpace = 0;\n\n // return advance we took in params\n let advance = 0;\n\n do {\n accu[advance + cSpace] = params.params[pos + advance];\n if (params.hasSubParams(pos + advance)) {\n const subparams = params.getSubParams(pos + advance)!;\n let i = 0;\n do {\n if (accu[1] === 5) {\n cSpace = 1;\n }\n accu[advance + i + 1 + cSpace] = subparams[i];\n } while (++i < subparams.length && i + advance + 1 + cSpace < accu.length);\n break;\n }\n // exit early if can decide color mode with semicolons\n if ((accu[1] === 5 && advance + cSpace >= 2)\n || (accu[1] === 2 && advance + cSpace >= 5)) {\n break;\n }\n // offset colorSpace slot for semicolon mode\n if (accu[1]) {\n cSpace = 1;\n }\n } while (++advance + pos < params.length && advance + cSpace < accu.length);\n\n // set default values to 0\n for (let i = 2; i < accu.length; ++i) {\n if (accu[i] === -1) {\n accu[i] = 0;\n }\n }\n\n // apply colors\n switch (accu[0]) {\n case 38:\n attr.fg = this._updateAttrColor(attr.fg, accu[1], accu[3], accu[4], accu[5]);\n break;\n case 48:\n attr.bg = this._updateAttrColor(attr.bg, accu[1], accu[3], accu[4], accu[5]);\n break;\n case 58:\n attr.extended = attr.extended.clone();\n attr.extended.underlineColor = this._updateAttrColor(attr.extended.underlineColor, accu[1], accu[3], accu[4], accu[5]);\n }\n\n return advance;\n }\n\n /**\n * SGR 4 subparams:\n * 4:0 - equal to SGR 24 (turn off all underline)\n * 4:1 - equal to SGR 4 (single underline)\n * 4:2 - equal to SGR 21 (double underline)\n * 4:3 - curly underline\n * 4:4 - dotted underline\n * 4:5 - dashed underline\n */\n private _processUnderline(style: number, attr: IAttributeData): void {\n // treat extended attrs as immutable, thus always clone from old one\n // this is needed since the buffer only holds references to it\n attr.extended = attr.extended.clone();\n\n // default to 1 == single underline\n if (!~style || style > 5) {\n style = 1;\n }\n attr.extended.underlineStyle = style;\n attr.fg |= FgFlags.UNDERLINE;\n\n // 0 deactivates underline\n if (style === 0) {\n attr.fg &= ~FgFlags.UNDERLINE;\n }\n\n // update HAS_EXTENDED in BG\n attr.updateExtended();\n }\n\n private _processSGR0(attr: IAttributeData): void {\n attr.fg = DEFAULT_ATTR_DATA.fg;\n attr.bg = DEFAULT_ATTR_DATA.bg;\n attr.extended = attr.extended.clone();\n // Reset underline style and color. Note that we don't want to reset other\n // fields such as the url id.\n attr.extended.underlineStyle = UnderlineStyle.NONE;\n attr.extended.underlineColor &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);\n attr.updateExtended();\n }\n\n /**\n * CSI Pm m Character Attributes (SGR).\n *\n * @vt: #P[See below for supported attributes.] CSI SGR \"Select Graphic Rendition\" \"CSI Pm m\" \"Set/Reset various text attributes.\"\n * SGR selects one or more character attributes at the same time. Multiple params (up to 32)\n * are applied in order from left to right. The changed attributes are applied to all new\n * characters received. If you move characters in the viewport by scrolling or any other means,\n * then the attributes move with the characters.\n *\n * Supported param values by SGR:\n *\n * | Param | Meaning | Support |\n * | --------- | -------------------------------------------------------- | ------- |\n * | 0 | Normal (default). Resets any other preceding SGR. | #Y |\n * | 1 | Bold. (also see `options.drawBoldTextInBrightColors`) | #Y |\n * | 2 | Faint, decreased intensity. | #Y |\n * | 3 | Italic. | #Y |\n * | 4 | Underlined (see below for style support). | #Y |\n * | 5 | Slowly blinking. | #N |\n * | 6 | Rapidly blinking. | #N |\n * | 7 | Inverse. Flips foreground and background color. | #Y |\n * | 8 | Invisible (hidden). | #Y |\n * | 9 | Crossed-out characters (strikethrough). | #Y |\n * | 21 | Doubly underlined. | #Y |\n * | 22 | Normal (neither bold nor faint). | #Y |\n * | 23 | No italic. | #Y |\n * | 24 | Not underlined. | #Y |\n * | 25 | Steady (not blinking). | #Y |\n * | 27 | Positive (not inverse). | #Y |\n * | 28 | Visible (not hidden). | #Y |\n * | 29 | Not Crossed-out (strikethrough). | #Y |\n * | 30 | Foreground color: Black. | #Y |\n * | 31 | Foreground color: Red. | #Y |\n * | 32 | Foreground color: Green. | #Y |\n * | 33 | Foreground color: Yellow. | #Y |\n * | 34 | Foreground color: Blue. | #Y |\n * | 35 | Foreground color: Magenta. | #Y |\n * | 36 | Foreground color: Cyan. | #Y |\n * | 37 | Foreground color: White. | #Y |\n * | 38 | Foreground color: Extended color. | #P[Support for RGB and indexed colors, see below.] |\n * | 39 | Foreground color: Default (original). | #Y |\n * | 40 | Background color: Black. | #Y |\n * | 41 | Background color: Red. | #Y |\n * | 42 | Background color: Green. | #Y |\n * | 43 | Background color: Yellow. | #Y |\n * | 44 | Background color: Blue. | #Y |\n * | 45 | Background color: Magenta. | #Y |\n * | 46 | Background color: Cyan. | #Y |\n * | 47 | Background color: White. | #Y |\n * | 48 | Background color: Extended color. | #P[Support for RGB and indexed colors, see below.] |\n * | 49 | Background color: Default (original). | #Y |\n * | 53 | Overlined. | #Y |\n * | 55 | Not Overlined. | #Y |\n * | 58 | Underline color: Extended color. | #P[Support for RGB and indexed colors, see below.] |\n * | 221 | Not bold (kitty extension). | #Y |\n * | 222 | Not faint (kitty extension). | #Y |\n * | 90 - 97 | Bright foreground color (analogous to 30 - 37). | #Y |\n * | 100 - 107 | Bright background color (analogous to 40 - 47). | #Y |\n *\n * Underline supports subparams to denote the style in the form `4 : x`:\n *\n * | x | Meaning | Support |\n * | ------ | ------------------------------------------------------------- | ------- |\n * | 0 | No underline. Same as `SGR 24 m`. | #Y |\n * | 1 | Single underline. Same as `SGR 4 m`. | #Y |\n * | 2 | Double underline. | #Y |\n * | 3 | Curly underline. | #Y |\n * | 4 | Dotted underline. | #Y |\n * | 5 | Dashed underline. | #Y |\n * | other | Single underline. Same as `SGR 4 m`. | #Y |\n *\n * Extended colors are supported for foreground (Ps=38), background (Ps=48) and underline (Ps=58)\n * as follows:\n *\n * | Ps + 1 | Meaning | Support |\n * | ------ | ------------------------------------------------------------- | ------- |\n * | 0 | Implementation defined. | #N |\n * | 1 | Transparent. | #N |\n * | 2 | RGB color as `Ps ; 2 ; R ; G ; B` or `Ps : 2 : : R : G : B`. | #Y |\n * | 3 | CMY color. | #N |\n * | 4 | CMYK color. | #N |\n * | 5 | Indexed (256 colors) as `Ps ; 5 ; INDEX` or `Ps : 5 : INDEX`. | #Y |\n */\n public charAttributes(params: IParams): boolean {\n // Optimize a single SGR0.\n if (params.length === 1 && params.params[0] === 0) {\n this._processSGR0(this._curAttrData);\n return true;\n }\n\n const l = params.length;\n let p;\n const attr = this._curAttrData;\n\n for (let i = 0; i < l; i++) {\n p = params.params[i];\n if (p >= 30 && p <= 37) {\n // fg color 8\n attr.fg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);\n attr.fg |= Attributes.CM_P16 | (p - 30);\n } else if (p >= 40 && p <= 47) {\n // bg color 8\n attr.bg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);\n attr.bg |= Attributes.CM_P16 | (p - 40);\n } else if (p >= 90 && p <= 97) {\n // fg color 16\n attr.fg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);\n attr.fg |= Attributes.CM_P16 | (p - 90) | 8;\n } else if (p >= 100 && p <= 107) {\n // bg color 16\n attr.bg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);\n attr.bg |= Attributes.CM_P16 | (p - 100) | 8;\n } else if (p === 0) {\n // default\n this._processSGR0(attr);\n } else if (p === 1) {\n // bold text\n attr.fg |= FgFlags.BOLD;\n } else if (p === 3) {\n // italic text\n attr.bg |= BgFlags.ITALIC;\n } else if (p === 4) {\n // underlined text\n attr.fg |= FgFlags.UNDERLINE;\n this._processUnderline(params.hasSubParams(i) ? params.getSubParams(i)![0] : UnderlineStyle.SINGLE, attr);\n } else if (p === 5) {\n // blink\n attr.fg |= FgFlags.BLINK;\n } else if (p === 7) {\n // inverse and positive\n // test with: echo -e '\\e[31m\\e[42mhello\\e[7mworld\\e[27mhi\\e[m'\n attr.fg |= FgFlags.INVERSE;\n } else if (p === 8) {\n // invisible\n attr.fg |= FgFlags.INVISIBLE;\n } else if (p === 9) {\n // strikethrough\n attr.fg |= FgFlags.STRIKETHROUGH;\n } else if (p === 2) {\n // dimmed text\n attr.bg |= BgFlags.DIM;\n } else if (p === 21) {\n // double underline\n this._processUnderline(UnderlineStyle.DOUBLE, attr);\n } else if (p === 22) {\n // not bold nor faint\n attr.fg &= ~FgFlags.BOLD;\n attr.bg &= ~BgFlags.DIM;\n } else if (p === 23) {\n // not italic\n attr.bg &= ~BgFlags.ITALIC;\n } else if (p === 24) {\n // not underlined\n attr.fg &= ~FgFlags.UNDERLINE;\n this._processUnderline(UnderlineStyle.NONE, attr);\n } else if (p === 25) {\n // not blink\n attr.fg &= ~FgFlags.BLINK;\n } else if (p === 27) {\n // not inverse\n attr.fg &= ~FgFlags.INVERSE;\n } else if (p === 28) {\n // not invisible\n attr.fg &= ~FgFlags.INVISIBLE;\n } else if (p === 29) {\n // not strikethrough\n attr.fg &= ~FgFlags.STRIKETHROUGH;\n } else if (p === 39) {\n // reset fg\n attr.fg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);\n attr.fg |= DEFAULT_ATTR_DATA.fg & Attributes.RGB_MASK;\n } else if (p === 49) {\n // reset bg\n attr.bg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);\n attr.bg |= DEFAULT_ATTR_DATA.bg & Attributes.RGB_MASK;\n } else if (p === 38 || p === 48 || p === 58) {\n // fg color 256 and RGB\n i += this._extractColor(params, i, attr);\n } else if (p === 53) {\n // overline\n attr.bg |= BgFlags.OVERLINE;\n } else if (p === 55) {\n // not overline\n attr.bg &= ~BgFlags.OVERLINE;\n } else if (p === 221 && (this._optionsService.rawOptions.vtExtensions?.kittySgrBoldFaintControl ?? true)) {\n // not bold (kitty extension)\n attr.fg &= ~FgFlags.BOLD;\n } else if (p === 222 && (this._optionsService.rawOptions.vtExtensions?.kittySgrBoldFaintControl ?? true)) {\n // not faint (kitty extension)\n attr.bg &= ~BgFlags.DIM;\n } else if (p === 59) {\n attr.extended = attr.extended.clone();\n attr.extended.underlineColor = -1;\n attr.updateExtended();\n } else {\n this._logService.debug('Unknown SGR attribute: %d.', p);\n }\n }\n return true;\n }\n\n /**\n * CSI Ps n Device Status Report (DSR).\n * Ps = 5 -> Status Report. Result (``OK'') is\n * CSI 0 n\n * Ps = 6 -> Report Cursor Position (CPR) [row;column].\n * Result is\n * CSI r ; c R\n * CSI ? Ps n\n * Device Status Report (DSR, DEC-specific).\n * Ps = 6 -> Report Cursor Position (CPR) [row;column] as CSI\n * ? r ; c R (assumes page is zero).\n * Ps = 1 5 -> Report Printer status as CSI ? 1 0 n (ready).\n * or CSI ? 1 1 n (not ready).\n * Ps = 2 5 -> Report UDK status as CSI ? 2 0 n (unlocked)\n * or CSI ? 2 1 n (locked).\n * Ps = 2 6 -> Report Keyboard status as\n * CSI ? 2 7 ; 1 ; 0 ; 0 n (North American).\n * The last two parameters apply to VT400 & up, and denote key-\n * board ready and LK01 respectively.\n * Ps = 5 3 -> Report Locator status as\n * CSI ? 5 3 n Locator available, if compiled-in, or\n * CSI ? 5 0 n No Locator, if not.\n *\n * @vt: #Y CSI DSR \"Device Status Report\" \"CSI Ps n\" \"Request cursor position (CPR) with `Ps` = 6.\"\n */\n public deviceStatus(params: IParams): boolean {\n switch (params.params[0]) {\n case 5:\n // status report\n this._coreService.triggerDataEvent(`${C0.ESC}[0n`);\n break;\n case 6:\n // cursor position\n const y = this._activeBuffer.y + 1;\n const x = this._activeBuffer.x + 1;\n this._coreService.triggerDataEvent(`${C0.ESC}[${y};${x}R`);\n break;\n }\n return true;\n }\n\n // @vt: #P[Only CPR is supported.] CSI DECDSR \"DEC Device Status Report\" \"CSI ? Ps n\" \"Only CPR is supported (same as DSR).\"\n public deviceStatusPrivate(params: IParams): boolean {\n // modern xterm doesnt seem to\n // respond to any of these except ?6, 6, and 5\n switch (params.params[0]) {\n case 6:\n // cursor position\n const y = this._activeBuffer.y + 1;\n const x = this._activeBuffer.x + 1;\n this._coreService.triggerDataEvent(`${C0.ESC}[?${y};${x}R`);\n break;\n case 15:\n // no printer\n // this.handler(C0.ESC + '[?11n');\n break;\n case 25:\n // dont support user defined keys\n // this.handler(C0.ESC + '[?21n');\n break;\n case 26:\n // north american keyboard\n // this.handler(C0.ESC + '[?27;1;0;0n');\n break;\n case 53:\n // no dec locator/mouse\n // this.handler(C0.ESC + '[?50n');\n break;\n case 996:\n // color scheme query (https://contour-terminal.org/vt-extensions/color-palette-update-notifications/)\n if (this._optionsService.rawOptions.vtExtensions?.colorSchemeQuery ?? true) {\n this._onRequestColorSchemeQuery.fire();\n }\n break;\n }\n return true;\n }\n\n /**\n * CSI ! p Soft terminal reset (DECSTR).\n * http://vt100.net/docs/vt220-rm/table4-10.html\n *\n * @vt: #Y CSI DECSTR \"Soft Terminal Reset\" \"CSI ! p\" \"Reset several terminal attributes to initial state.\"\n * There are two terminal reset sequences - RIS and DECSTR. While RIS performs almost a full\n * terminal bootstrap, DECSTR only resets certain attributes. For most needs DECSTR should be\n * sufficient.\n *\n * The following terminal attributes are reset to default values:\n * - IRM is reset (dafault = false)\n * - scroll margins are reset (default = viewport size)\n * - erase attributes are reset to default\n * - charsets are reset\n * - DECSC data is reset to initial values\n * - DECOM is reset to absolute mode\n *\n *\n * FIXME: there are several more attributes missing (see VT520 manual)\n */\n public softReset(params: IParams): boolean {\n this._coreService.isCursorHidden = false;\n this._onRequestSyncScrollBar.fire();\n this._activeBuffer.scrollTop = 0;\n this._activeBuffer.scrollBottom = this._bufferService.rows - 1;\n this._curAttrData = DEFAULT_ATTR_DATA.clone();\n this._coreService.reset();\n this._charsetService.reset();\n\n // reset DECSC data\n this._activeBuffer.savedX = 0;\n this._activeBuffer.savedY = this._activeBuffer.ybase;\n this._activeBuffer.savedCurAttrData.fg = this._curAttrData.fg;\n this._activeBuffer.savedCurAttrData.bg = this._curAttrData.bg;\n this._activeBuffer.savedCharset = this._charsetService.charset;\n\n // reset DECOM\n this._coreService.decPrivateModes.origin = false;\n return true;\n }\n\n /**\n * CSI Ps SP q Set cursor style (DECSCUSR, VT520).\n * Ps = 0 -> reset to option.\n * Ps = 1 -> blinking block (default).\n * Ps = 2 -> steady block.\n * Ps = 3 -> blinking underline.\n * Ps = 4 -> steady underline.\n * Ps = 5 -> blinking bar (xterm).\n * Ps = 6 -> steady bar (xterm).\n *\n * @vt: #Y CSI DECSCUSR \"Set Cursor Style\" \"CSI Ps SP q\" \"Set cursor style.\"\n * Supported cursor styles:\n * - 0: reset to option\n * - empty, 1: blinking block\n * - 2: steady block\n * - 3: blinking underline\n * - 4: steady underline\n * - 5: blinking bar\n * - 6: steady bar\n */\n public setCursorStyle(params: IParams): boolean {\n const param = params.length === 0 ? 1 : params.params[0];\n if (param === 0) {\n this._coreService.decPrivateModes.cursorStyle = undefined;\n this._coreService.decPrivateModes.cursorBlink = undefined;\n } else {\n switch (param) {\n case 1:\n case 2:\n this._coreService.decPrivateModes.cursorStyle = 'block';\n break;\n case 3:\n case 4:\n this._coreService.decPrivateModes.cursorStyle = 'underline';\n break;\n case 5:\n case 6:\n this._coreService.decPrivateModes.cursorStyle = 'bar';\n break;\n }\n const isBlinking = param % 2 === 1;\n this._coreService.decPrivateModes.cursorBlink = isBlinking;\n }\n return true;\n }\n\n /**\n * CSI Ps ; Ps r\n * Set Scrolling Region [top;bottom] (default = full size of win-\n * dow) (DECSTBM).\n *\n * @vt: #Y CSI DECSTBM \"Set Top and Bottom Margin\" \"CSI Ps ; Ps r\" \"Set top and bottom margins of the viewport [top;bottom] (default = viewport size).\"\n */\n public setScrollRegion(params: IParams): boolean {\n const top = params.params[0] || 1;\n let bottom: number;\n\n if (params.length < 2 || (bottom = params.params[1]) > this._bufferService.rows || bottom === 0) {\n bottom = this._bufferService.rows;\n }\n\n if (bottom > top) {\n this._activeBuffer.scrollTop = top - 1;\n this._activeBuffer.scrollBottom = bottom - 1;\n this._setCursor(0, 0);\n }\n return true;\n }\n\n /**\n * CSI Ps ; Ps ; Ps t - Various window manipulations and reports (xterm)\n *\n * Note: Only those listed below are supported. All others are left to integrators and\n * need special treatment based on the embedding environment.\n *\n * Ps = 1 4 supported\n * Report xterm text area size in pixels.\n * Result is CSI 4 ; height ; width t\n * Ps = 14 ; 2 not implemented\n * Ps = 16 supported\n * Report xterm character cell size in pixels.\n * Result is CSI 6 ; height ; width t\n * Ps = 18 supported\n * Report the size of the text area in characters.\n * Result is CSI 8 ; height ; width t\n * Ps = 20 supported\n * Report xterm window's icon label.\n * Result is OSC L label ST\n * Ps = 21 supported\n * Report xterm window's title.\n * Result is OSC l label ST\n * Ps = 22 ; 0 -> Save xterm icon and window title on stack. supported\n * Ps = 22 ; 1 -> Save xterm icon title on stack. supported\n * Ps = 22 ; 2 -> Save xterm window title on stack. supported\n * Ps = 23 ; 0 -> Restore xterm icon and window title from stack. supported\n * Ps = 23 ; 1 -> Restore xterm icon title from stack. supported\n * Ps = 23 ; 2 -> Restore xterm window title from stack. supported\n * Ps >= 24 not implemented\n */\n public windowOptions(params: IParams): boolean {\n if (!paramToWindowOption(params.params[0], this._optionsService.rawOptions.windowOptions)) {\n return true;\n }\n const second = (params.length > 1) ? params.params[1] : 0;\n switch (params.params[0]) {\n case 14: // GetWinSizePixels, returns CSI 4 ; height ; width t\n if (second !== 2) {\n this._onRequestWindowsOptionsReport.fire(WindowsOptionsReportType.GET_WIN_SIZE_PIXELS);\n }\n break;\n case 16: // GetCellSizePixels, returns CSI 6 ; height ; width t\n this._onRequestWindowsOptionsReport.fire(WindowsOptionsReportType.GET_CELL_SIZE_PIXELS);\n break;\n case 18: // GetWinSizeChars, returns CSI 8 ; height ; width t\n if (this._bufferService) {\n this._coreService.triggerDataEvent(`${C0.ESC}[8;${this._bufferService.rows};${this._bufferService.cols}t`);\n }\n break;\n case 22: // PushTitle\n if (second === 0 || second === 2) {\n this._windowTitleStack.push(this._windowTitle);\n if (this._windowTitleStack.length > Constants.STACK_LIMIT) {\n this._windowTitleStack.shift();\n }\n }\n if (second === 0 || second === 1) {\n this._iconNameStack.push(this._iconName);\n if (this._iconNameStack.length > Constants.STACK_LIMIT) {\n this._iconNameStack.shift();\n }\n }\n break;\n case 23: // PopTitle\n if (second === 0 || second === 2) {\n if (this._windowTitleStack.length) {\n this.setTitle(this._windowTitleStack.pop()!);\n }\n }\n if (second === 0 || second === 1) {\n if (this._iconNameStack.length) {\n this.setIconName(this._iconNameStack.pop()!);\n }\n }\n break;\n }\n return true;\n }\n\n\n /**\n * CSI s\n * ESC 7\n * Save cursor (ANSI.SYS).\n *\n * @vt: #P[TODO...] CSI SCOSC \"Save Cursor\" \"CSI s\" \"Save cursor position, charmap and text attributes.\"\n * @vt: #Y ESC SC \"Save Cursor\" \"ESC 7\" \"Save cursor position, charmap and text attributes.\"\n */\n public saveCursor(params?: IParams): boolean {\n this._activeBuffer.savedX = this._activeBuffer.x;\n this._activeBuffer.savedY = this._activeBuffer.ybase + this._activeBuffer.y;\n this._activeBuffer.savedCurAttrData.fg = this._curAttrData.fg;\n this._activeBuffer.savedCurAttrData.bg = this._curAttrData.bg;\n this._activeBuffer.savedCharset = this._charsetService.charset;\n this._activeBuffer.savedCharsets = this._charsetService.charsets.slice();\n this._activeBuffer.savedGlevel = this._charsetService.glevel;\n this._activeBuffer.savedOriginMode = this._coreService.decPrivateModes.origin;\n this._activeBuffer.savedWraparoundMode = this._coreService.decPrivateModes.wraparound;\n return true;\n }\n\n\n /**\n * CSI u\n * ESC 8\n * Restore cursor (ANSI.SYS).\n *\n * @vt: #P[TODO...] CSI SCORC \"Restore Cursor\" \"CSI u\" \"Restore cursor position, charmap and text attributes.\"\n * @vt: #Y ESC RC \"Restore Cursor\" \"ESC 8\" \"Restore cursor position, charmap and text attributes.\"\n */\n public restoreCursor(params?: IParams): boolean {\n this._activeBuffer.x = this._activeBuffer.savedX || 0;\n this._activeBuffer.y = Math.max(this._activeBuffer.savedY - this._activeBuffer.ybase, 0);\n this._curAttrData.fg = this._activeBuffer.savedCurAttrData.fg;\n this._curAttrData.bg = this._activeBuffer.savedCurAttrData.bg;\n for (let i = 0; i < this._activeBuffer.savedCharsets.length; i++) {\n this._charsetService.setgCharset(i, this._activeBuffer.savedCharsets[i]);\n }\n this._charsetService.setgLevel(this._activeBuffer.savedGlevel);\n this._coreService.decPrivateModes.origin = this._activeBuffer.savedOriginMode;\n this._coreService.decPrivateModes.wraparound = this._activeBuffer.savedWraparoundMode;\n this._restrictCursor();\n return true;\n }\n\n /**\n * OSC 2; ST (set window title)\n * Proxy to set window title.\n *\n * @vt: #P[Icon name is not exposed.] OSC 0 \"Set Windows Title and Icon Name\" \"OSC 0 ; Pt BEL\" \"Set window title and icon name.\"\n * Icon name is not supported. For Window Title see below.\n *\n * @vt: #Y OSC 2 \"Set Windows Title\" \"OSC 2 ; Pt BEL\" \"Set window title.\"\n * xterm.js does not manipulate the title directly, instead exposes changes via the event\n * `Terminal.onTitleChange`.\n */\n public setTitle(data: string): boolean {\n this._windowTitle = data;\n this._onTitleChange.fire(data);\n return true;\n }\n\n /**\n * OSC 1; ST\n * Note: Icon name is not exposed.\n */\n public setIconName(data: string): boolean {\n this._iconName = data;\n return true;\n }\n\n /**\n * OSC 4; ; ST (set ANSI color to )\n *\n * @vt: #Y OSC 4 \"Set ANSI color\" \"OSC 4 ; c ; spec BEL\" \"Change color number `c` to the color specified by `spec`.\"\n * `c` is the color index between 0 and 255. The color format of `spec` is derived from\n * `XParseColor` (see OSC 10 for supported formats). There may be multipe `c ; spec` pairs present\n * in the same instruction. If `spec` contains `?` the terminal returns a sequence with the\n * currently set color.\n */\n public setOrReportIndexedColor(data: string): boolean {\n const event: IColorEvent = [];\n const slots = data.split(';');\n while (slots.length > 1) {\n const idx = slots.shift() as string;\n const spec = slots.shift() as string;\n if (/^\\d+$/.exec(idx)) {\n const index = parseInt(idx, 10);\n if (isValidColorIndex(index)) {\n if (spec === '?') {\n event.push({ type: ColorRequestType.REPORT, index });\n } else {\n const color = parseColor(spec);\n if (color) {\n event.push({ type: ColorRequestType.SET, index, color });\n }\n }\n }\n }\n }\n if (event.length) {\n this._onColor.fire(event);\n }\n return true;\n }\n\n /**\n * OSC 8 ; ; ST - create hyperlink\n * OSC 8 ; ; ST - finish hyperlink\n *\n * Test case:\n *\n * ```sh\n * printf '\\e]8;;http://example.com\\e\\\\This is a link\\e]8;;\\e\\\\\\n'\n * ```\n *\n * @vt: #Y OSC 8 \"Create hyperlink\" \"OSC 8 ; params ; uri BEL\" \"Create a hyperlink to `uri` using `params`.\"\n * `uri` is a hyperlink starting with `http://`, `https://`, `ftp://`, `file://` or `mailto://`. `params` is an\n * optional list of key=value assignments, separated by the : character.\n * Example: `id=xyz123:foo=bar:baz=quux`.\n * Currently only the id key is defined. Cells that share the same ID and URI share hover\n * feedback. Use `OSC 8 ; ; BEL` to finish the current hyperlink.\n */\n public setHyperlink(data: string): boolean {\n // Arg parsing is special cases to support unencoded semi-colons in the URIs (#4944)\n const idx = data.indexOf(';');\n if (idx === -1) {\n // malformed sequence, just return as handled\n return true;\n }\n const id = data.slice(0, idx).trim();\n const uri = data.slice(idx + 1);\n if (uri) {\n return this._createHyperlink(id, uri);\n }\n if (id.trim()) {\n return false;\n }\n return this._finishHyperlink();\n }\n\n private _createHyperlink(params: string, uri: string): boolean {\n // It's legal to open a new hyperlink without explicitly finishing the previous one\n if (this._getCurrentLinkId()) {\n this._finishHyperlink();\n }\n const parsedParams = params.split(':');\n let id: string | undefined;\n const idParamIndex = parsedParams.findIndex(e => e.startsWith('id='));\n if (idParamIndex !== -1) {\n id = parsedParams[idParamIndex].slice(3) || undefined;\n }\n this._curAttrData.extended = this._curAttrData.extended.clone();\n this._curAttrData.extended.urlId = this._oscLinkService.registerLink({ id, uri });\n this._curAttrData.updateExtended();\n return true;\n }\n\n private _finishHyperlink(): boolean {\n this._curAttrData.extended = this._curAttrData.extended.clone();\n this._curAttrData.extended.urlId = 0;\n this._curAttrData.updateExtended();\n return true;\n }\n\n // special colors - OSC 10 | 11 | 12\n private _specialColors = [SpecialColorIndex.FOREGROUND, SpecialColorIndex.BACKGROUND, SpecialColorIndex.CURSOR];\n\n /**\n * Apply colors requests for special colors in OSC 10 | 11 | 12.\n * Since these commands are stacking from multiple parameters,\n * we handle them in a loop with an entry offset to `_specialColors`.\n */\n private _setOrReportSpecialColor(data: string, offset: number): boolean {\n const slots = data.split(';');\n for (let i = 0; i < slots.length; ++i, ++offset) {\n if (offset >= this._specialColors.length) break;\n if (slots[i] === '?') {\n this._onColor.fire([{ type: ColorRequestType.REPORT, index: this._specialColors[offset] }]);\n } else {\n const color = parseColor(slots[i]);\n if (color) {\n this._onColor.fire([{ type: ColorRequestType.SET, index: this._specialColors[offset], color }]);\n }\n }\n }\n return true;\n }\n\n /**\n * OSC 10 ; | ST - set or query default foreground color\n *\n * @vt: #Y OSC 10 \"Set or query default foreground color\" \"OSC 10 ; Pt BEL\" \"Set or query default foreground color.\"\n * To set the color, the following color specification formats are supported:\n * - `rgb://` for `, , ` in `h | hh | hhh | hhhh`, where\n * `h` is a single hexadecimal digit (case insignificant). The different widths scale\n * from 4 bit (`h`) to 16 bit (`hhhh`) and get converted to 8 bit (`hh`).\n * - `#RGB` - 4 bits per channel, expanded to `#R0G0B0`\n * - `#RRGGBB` - 8 bits per channel\n * - `#RRRGGGBBB` - 12 bits per channel, truncated to `#RRGGBB`\n * - `#RRRRGGGGBBBB` - 16 bits per channel, truncated to `#RRGGBB`\n *\n * **Note:** X11 named colors are currently unsupported.\n *\n * If `Pt` contains `?` instead of a color specification, the terminal\n * returns a sequence with the current default foreground color\n * (use that sequence to restore the color after changes).\n *\n * **Note:** Other than xterm, xterm.js does not support OSC 12 - 19.\n * Therefore stacking multiple `Pt` separated by `;` only works for the first two entries.\n */\n public setOrReportFgColor(data: string): boolean {\n return this._setOrReportSpecialColor(data, 0);\n }\n\n /**\n * OSC 11 ; | ST - set or query default background color\n *\n * @vt: #Y OSC 11 \"Set or query default background color\" \"OSC 11 ; Pt BEL\" \"Same as OSC 10, but for default background.\"\n */\n public setOrReportBgColor(data: string): boolean {\n return this._setOrReportSpecialColor(data, 1);\n }\n\n /**\n * OSC 12 ; | ST - set or query default cursor color\n *\n * @vt: #Y OSC 12 \"Set or query default cursor color\" \"OSC 12 ; Pt BEL\" \"Same as OSC 10, but for default cursor color.\"\n */\n public setOrReportCursorColor(data: string): boolean {\n return this._setOrReportSpecialColor(data, 2);\n }\n\n /**\n * OSC 104 ; ST - restore ANSI color \n *\n * @vt: #Y OSC 104 \"Reset ANSI color\" \"OSC 104 ; c BEL\" \"Reset color number `c` to themed color.\"\n * `c` is the color index between 0 and 255. This function restores the default color for `c` as\n * specified by the loaded theme. Any number of `c` parameters may be given.\n * If no parameters are given, the entire indexed color table will be reset.\n */\n public restoreIndexedColor(data: string): boolean {\n if (!data) {\n this._onColor.fire([{ type: ColorRequestType.RESTORE }]);\n return true;\n }\n const event: IColorEvent = [];\n const slots = data.split(';');\n for (let i = 0; i < slots.length; ++i) {\n if (/^\\d+$/.exec(slots[i])) {\n const index = parseInt(slots[i], 10);\n if (isValidColorIndex(index)) {\n event.push({ type: ColorRequestType.RESTORE, index });\n }\n }\n }\n if (event.length) {\n this._onColor.fire(event);\n }\n return true;\n }\n\n /**\n * OSC 110 ST - restore default foreground color\n *\n * @vt: #Y OSC 110 \"Restore default foreground color\" \"OSC 110 BEL\" \"Restore default foreground to themed color.\"\n */\n public restoreFgColor(data: string): boolean {\n this._onColor.fire([{ type: ColorRequestType.RESTORE, index: SpecialColorIndex.FOREGROUND }]);\n return true;\n }\n\n /**\n * OSC 111 ST - restore default background color\n *\n * @vt: #Y OSC 111 \"Restore default background color\" \"OSC 111 BEL\" \"Restore default background to themed color.\"\n */\n public restoreBgColor(data: string): boolean {\n this._onColor.fire([{ type: ColorRequestType.RESTORE, index: SpecialColorIndex.BACKGROUND }]);\n return true;\n }\n\n /**\n * OSC 112 ST - restore default cursor color\n *\n * @vt: #Y OSC 112 \"Restore default cursor color\" \"OSC 112 BEL\" \"Restore default cursor to themed color.\"\n */\n public restoreCursorColor(data: string): boolean {\n this._onColor.fire([{ type: ColorRequestType.RESTORE, index: SpecialColorIndex.CURSOR }]);\n return true;\n }\n\n /**\n * ESC E\n * C1.NEL\n * DEC mnemonic: NEL (https://vt100.net/docs/vt510-rm/NEL)\n * Moves cursor to first position on next line.\n *\n * @vt: #Y C1 NEL \"Next Line\" \"\\x85\" \"Move the cursor to the beginning of the next row.\"\n * @vt: #Y ESC NEL \"Next Line\" \"ESC E\" \"Move the cursor to the beginning of the next row.\"\n */\n public nextLine(): boolean {\n this._activeBuffer.x = 0;\n this.index();\n return true;\n }\n\n /**\n * ESC =\n * DEC mnemonic: DECKPAM (https://vt100.net/docs/vt510-rm/DECKPAM.html)\n * Enables the numeric keypad to send application sequences to the host.\n */\n public keypadApplicationMode(): boolean {\n this._logService.debug('Serial port requested application keypad.');\n this._coreService.decPrivateModes.applicationKeypad = true;\n this._onRequestSyncScrollBar.fire();\n return true;\n }\n\n /**\n * ESC >\n * DEC mnemonic: DECKPNM (https://vt100.net/docs/vt510-rm/DECKPNM.html)\n * Enables the keypad to send numeric characters to the host.\n */\n public keypadNumericMode(): boolean {\n this._logService.debug('Switching back to normal keypad.');\n this._coreService.decPrivateModes.applicationKeypad = false;\n this._onRequestSyncScrollBar.fire();\n return true;\n }\n\n /**\n * ESC % @\n * ESC % G\n * Select default character set. UTF-8 is not supported (string are unicode anyways)\n * therefore ESC % G does the same.\n */\n public selectDefaultCharset(): boolean {\n this._charsetService.setgLevel(0);\n this._charsetService.setgCharset(0, DEFAULT_CHARSET); // US (default)\n return true;\n }\n\n /**\n * ESC ( C\n * Designate G0 Character Set, VT100, ISO 2022.\n * ESC ) C\n * Designate G1 Character Set (ISO 2022, VT100).\n * ESC * C\n * Designate G2 Character Set (ISO 2022, VT220).\n * ESC + C\n * Designate G3 Character Set (ISO 2022, VT220).\n * ESC - C\n * Designate G1 Character Set (VT300).\n * ESC . C\n * Designate G2 Character Set (VT300).\n * ESC / C\n * Designate G3 Character Set (VT300). C = A -> ISO Latin-1 Supplemental. - Supported?\n */\n public selectCharset(collectAndFlag: string): boolean {\n if (collectAndFlag.length !== 2) {\n this.selectDefaultCharset();\n return true;\n }\n if (collectAndFlag[0] === '/') {\n return true; // TODO: Is this supported?\n }\n this._charsetService.setgCharset(GLEVEL[collectAndFlag[0]], CHARSETS[collectAndFlag[1]] ?? DEFAULT_CHARSET);\n return true;\n }\n\n /**\n * ESC D\n * C1.IND\n * DEC mnemonic: IND (https://vt100.net/docs/vt510-rm/IND.html)\n * Moves the cursor down one line in the same column.\n *\n * @vt: #Y C1 IND \"Index\" \"\\x84\" \"Move the cursor one line down scrolling if needed.\"\n * @vt: #Y ESC IND \"Index\" \"ESC D\" \"Move the cursor one line down scrolling if needed.\"\n */\n public index(): boolean {\n this._restrictCursor();\n this._activeBuffer.y++;\n if (this._activeBuffer.y === this._activeBuffer.scrollBottom + 1) {\n this._activeBuffer.y--;\n this._bufferService.scroll(this._eraseAttrData());\n } else if (this._activeBuffer.y >= this._bufferService.rows) {\n this._activeBuffer.y = this._bufferService.rows - 1;\n }\n this._restrictCursor();\n return true;\n }\n\n /**\n * ESC H\n * C1.HTS\n * DEC mnemonic: HTS (https://vt100.net/docs/vt510-rm/HTS.html)\n * Sets a horizontal tab stop at the column position indicated by\n * the value of the active column when the terminal receives an HTS.\n *\n * @vt: #Y C1 HTS \"Horizontal Tabulation Set\" \"\\x88\" \"Places a tab stop at the current cursor position.\"\n * @vt: #Y ESC HTS \"Horizontal Tabulation Set\" \"ESC H\" \"Places a tab stop at the current cursor position.\"\n */\n public tabSet(): boolean {\n this._activeBuffer.tabs[this._activeBuffer.x] = true;\n return true;\n }\n\n /**\n * ESC M\n * C1.RI\n * DEC mnemonic: HTS\n * Moves the cursor up one line in the same column. If the cursor is at the top margin,\n * the page scrolls down.\n *\n * @vt: #Y ESC IR \"Reverse Index\" \"ESC M\" \"Move the cursor one line up scrolling if needed.\"\n */\n public reverseIndex(): boolean {\n this._restrictCursor();\n if (this._activeBuffer.y === this._activeBuffer.scrollTop) {\n // possibly move the code below to term.reverseScroll();\n // test: echo -ne '\\e[1;1H\\e[44m\\eM\\e[0m'\n // blankLine(true) is xterm/linux behavior\n const scrollRegionHeight = this._activeBuffer.scrollBottom - this._activeBuffer.scrollTop;\n this._activeBuffer.lines.shiftElements(this._activeBuffer.ybase + this._activeBuffer.y, scrollRegionHeight, 1);\n this._activeBuffer.lines.set(this._activeBuffer.ybase + this._activeBuffer.y, this._activeBuffer.getBlankLine(this._eraseAttrData()));\n this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);\n } else {\n this._activeBuffer.y--;\n this._restrictCursor(); // quickfix to not run out of bounds\n }\n return true;\n }\n\n /**\n * ESC c\n * DEC mnemonic: RIS (https://vt100.net/docs/vt510-rm/RIS.html)\n * Reset to initial state.\n *\n * @vt: #Y ESC RIS \"Full Reset\" \"ESC c\" \"Reset to initial state.\"\n */\n public fullReset(): boolean {\n this._parser.reset();\n this._onRequestReset.fire();\n return true;\n }\n\n public reset(): void {\n this._curAttrData = DEFAULT_ATTR_DATA.clone();\n this._eraseAttrDataInternal = DEFAULT_ATTR_DATA.clone();\n }\n\n /**\n * back_color_erase feature for xterm.\n */\n private _eraseAttrData(): IAttributeData {\n this._eraseAttrDataInternal.bg &= ~(Attributes.CM_MASK | 0xFFFFFF);\n this._eraseAttrDataInternal.bg |= this._curAttrData.bg & ~0xFC000000;\n return this._eraseAttrDataInternal;\n }\n\n /**\n * ESC n\n * ESC o\n * ESC |\n * ESC }\n * ESC ~\n * DEC mnemonic: LS (https://vt100.net/docs/vt510-rm/LS.html)\n * When you use a locking shift, the character set remains in GL or GR until\n * you use another locking shift. (partly supported)\n */\n public setgLevel(level: number): boolean {\n this._charsetService.setgLevel(level);\n return true;\n }\n\n /**\n * ESC # 8\n * DEC mnemonic: DECALN (https://vt100.net/docs/vt510-rm/DECALN.html)\n * This control function fills the complete screen area with\n * a test pattern (E) used for adjusting screen alignment.\n *\n * @vt: #Y ESC DECALN \"Screen Alignment Pattern\" \"ESC # 8\" \"Fill viewport with a test pattern (E).\"\n */\n public screenAlignmentPattern(): boolean {\n // prepare cell data\n const cell = new CellData();\n cell.content = 1 << Content.WIDTH_SHIFT | 'E'.charCodeAt(0);\n cell.fg = this._curAttrData.fg;\n cell.bg = this._curAttrData.bg;\n\n\n this._setCursor(0, 0);\n for (let yOffset = 0; yOffset < this._bufferService.rows; ++yOffset) {\n const row = this._activeBuffer.ybase + this._activeBuffer.y + yOffset;\n const line = this._activeBuffer.lines.get(row);\n if (line) {\n line.fill(cell);\n line.isWrapped = false;\n }\n }\n this._dirtyRowTracker.markAllDirty();\n this._setCursor(0, 0);\n return true;\n }\n\n\n /**\n * DCS $ q Pt ST\n * DECRQSS (https://vt100.net/docs/vt510-rm/DECRQSS.html)\n * Request Status String (DECRQSS), VT420 and up.\n * Response: DECRPSS (https://vt100.net/docs/vt510-rm/DECRPSS.html)\n *\n * @vt: #P[Limited support, see below.] DCS DECRQSS \"Request Selection or Setting\" \"DCS $ q Pt ST\" \"Request several terminal settings.\"\n * Response is in the form `ESC P 1 $ r Pt ST` for valid requests, where `Pt` contains the\n * corresponding CSI string, `ESC P 0 ST` for invalid requests.\n *\n * Supported requests and responses:\n *\n * | Type | Request | Response (`Pt`) |\n * | -------------------------------- | ----------------- | ----------------------------------------------------- |\n * | Graphic Rendition (SGR) | `DCS $ q m ST` | always reporting `0m` (currently broken) |\n * | Top and Bottom Margins (DECSTBM) | `DCS $ q r ST` | `Ps ; Ps r` |\n * | Cursor Style (DECSCUSR) | `DCS $ q SP q ST` | `Ps SP q` |\n * | Protection Attribute (DECSCA) | `DCS $ q \" q ST` | `Ps \" q` (DECSCA 2 is reported as Ps = 0) |\n * | Conformance Level (DECSCL) | `DCS $ q \" p ST` | always reporting `61 ; 1 \" p` (DECSCL is unsupported) |\n *\n *\n * TODO:\n * - fix SGR report\n * - either check which conformance is better suited or remove the report completely\n * --> we are currently a mixture of all up to VT400 but dont follow anyone strictly\n */\n public requestStatusString(data: string, params: IParams): boolean {\n const f = (s: string): boolean => {\n this._coreService.triggerDataEvent(`${C0.ESC}${s}${C0.ESC}\\\\`);\n return true;\n };\n\n // access helpers\n const b = this._bufferService.buffer;\n const opts = this._optionsService.rawOptions;\n const STYLES: { [key: string]: number } = { 'block': 2, 'underline': 4, 'bar': 6 };\n\n if (data === '\"q') return f(`P1$r${this._curAttrData.isProtected() ? 1 : 0}\"q`);\n if (data === '\"p') return f(`P1$r61;1\"p`);\n if (data === 'r') return f(`P1$r${b.scrollTop + 1};${b.scrollBottom + 1}r`);\n // FIXME: report real SGR settings instead of 0m\n if (data === 'm') return f(`P1$r0m`);\n if (data === ' q') return f(`P1$r${STYLES[opts.cursorStyle] - (opts.cursorBlink ? 1 : 0)} q`);\n return f(`P0$r`);\n }\n\n public markRangeDirty(y1: number, y2: number): void {\n this._dirtyRowTracker.markRangeDirty(y1, y2);\n }\n\n // #region Kitty keyboard\n\n /**\n * CSI = flags ; mode u\n * Set Kitty keyboard protocol flags.\n * mode: 1=set, 2=set-only-specified, 3=reset-only-specified\n *\n * @vt: #Y CSI KKBDSET \"Kitty Keyboard Set\" \"CSI = Ps ; Pm u\" \"Set Kitty keyboard protocol flags.\"\n */\n public kittyKeyboardSet(params: IParams): boolean {\n if (!this._optionsService.rawOptions.vtExtensions?.kittyKeyboard) {\n return true;\n }\n const flags = params.params[0] || 0;\n const mode = params.length > 1 ? (params.params[1] || 1) : 1;\n const state = this._coreService.kittyKeyboard;\n\n switch (mode) {\n case 1: // Set all flags\n state.flags = flags;\n break;\n case 2: // Set only specified flags (OR)\n state.flags |= flags;\n break;\n case 3: // Reset only specified flags (AND NOT)\n state.flags &= ~flags;\n break;\n }\n return true;\n }\n\n /**\n * CSI ? u\n * Query Kitty keyboard protocol flags.\n * Terminal responds with CSI ? flags u\n *\n * @vt: #Y CSI KKBDQUERY \"Kitty Keyboard Query\" \"CSI ? u\" \"Query Kitty keyboard protocol flags.\"\n */\n public kittyKeyboardQuery(params: IParams): boolean {\n if (!this._optionsService.rawOptions.vtExtensions?.kittyKeyboard) {\n return true;\n }\n const flags = this._coreService.kittyKeyboard.flags;\n this._coreService.triggerDataEvent(`${C0.ESC}[?${flags}u`);\n return true;\n }\n\n /**\n * CSI > flags u\n * Push Kitty keyboard flags onto stack and set new flags.\n *\n * @vt: #Y CSI KKBDPUSH \"Kitty Keyboard Push\" \"CSI > Ps u\" \"Push keyboard flags to stack and set new flags.\"\n */\n public kittyKeyboardPush(params: IParams): boolean {\n if (!this._optionsService.rawOptions.vtExtensions?.kittyKeyboard) {\n return true;\n }\n const flags = params.params[0] || 0;\n const state = this._coreService.kittyKeyboard;\n const isAlt = this._bufferService.buffer === this._bufferService.buffers.alt;\n const stack = isAlt ? state.altStack : state.mainStack;\n\n // Evict oldest entry if stack is full (DoS protection, limit of 16)\n if (stack.length >= 16) {\n stack.shift();\n }\n\n // Push current flags onto stack and set new flags\n stack.push(state.flags);\n state.flags = flags;\n return true;\n }\n\n /**\n * CSI < count u\n * Pop Kitty keyboard flags from stack.\n *\n * @vt: #Y CSI KKBDPOP \"Kitty Keyboard Pop\" \"CSI < Ps u\" \"Pop keyboard flags from stack.\"\n */\n public kittyKeyboardPop(params: IParams): boolean {\n if (!this._optionsService.rawOptions.vtExtensions?.kittyKeyboard) {\n return true;\n }\n const count = Math.max(1, params.params[0] || 1);\n const state = this._coreService.kittyKeyboard;\n const isAlt = this._bufferService.buffer === this._bufferService.buffers.alt;\n const stack = isAlt ? state.altStack : state.mainStack;\n\n // Pop specified number of entries from stack\n for (let i = 0; i < count && stack.length > 0; i++) {\n state.flags = stack.pop()!;\n }\n // If stack is empty after popping, reset to 0\n if (stack.length === 0 && count > 0) {\n state.flags = 0;\n }\n return true;\n }\n\n // #endregion\n}\n\nexport interface IDirtyRowTracker {\n readonly start: number;\n readonly end: number;\n\n clearRange(): void;\n markDirty(y: number): void;\n markRangeDirty(y1: number, y2: number): void;\n markAllDirty(): void;\n}\n\nclass DirtyRowTracker implements IDirtyRowTracker {\n public start!: number;\n public end!: number;\n\n constructor(\n @IBufferService private readonly _bufferService: IBufferService\n ) {\n this.clearRange();\n }\n\n public clearRange(): void {\n this.start = this._bufferService.buffer.y;\n this.end = this._bufferService.buffer.y;\n }\n\n public markDirty(y: number): void {\n if (y < this.start) {\n this.start = y;\n } else if (y > this.end) {\n this.end = y;\n }\n }\n\n public markRangeDirty(y1: number, y2: number): void {\n if (y1 > y2) {\n $temp = y1;\n y1 = y2;\n y2 = $temp;\n }\n if (y1 < this.start) {\n this.start = y1;\n }\n if (y2 > this.end) {\n this.end = y2;\n }\n }\n\n public markAllDirty(): void {\n this.markRangeDirty(0, this._bufferService.rows - 1);\n }\n}\n\nexport function isValidColorIndex(value: number): value is ColorIndex {\n return 0 <= value && value < 256;\n}\n","/**\n * Copyright (c) 2024-2026 The xterm.js authors. All rights reserved.\n * @license MIT\n *\n * Minimal lifecycle utilities for xterm.js core.\n * Simplified from VS Code's lifecycle.ts - no tracking/leak detection.\n */\n\nexport interface IDisposable {\n dispose(): void;\n}\n\nexport function toDisposable(fn: () => void): IDisposable {\n return { dispose: fn };\n}\n\nexport function dispose(disposable: T): T;\nexport function dispose(disposable: T | undefined): T | undefined;\nexport function dispose(disposables: T[]): T[];\nexport function dispose(arg: T | T[] | undefined): T | T[] | undefined {\n if (!arg) {\n return arg;\n }\n if (Array.isArray(arg)) {\n for (const d of arg) {\n d.dispose();\n }\n return [];\n }\n arg.dispose();\n return arg;\n}\n\nexport function combinedDisposable(...disposables: IDisposable[]): IDisposable {\n return toDisposable(() => dispose(disposables));\n}\n\nexport class DisposableStore implements IDisposable {\n private readonly _disposables = new Set();\n private _isDisposed = false;\n\n public get isDisposed(): boolean {\n return this._isDisposed;\n }\n\n public add(o: T): T {\n if (this._isDisposed) {\n o.dispose();\n } else {\n this._disposables.add(o);\n }\n return o;\n }\n\n public dispose(): void {\n if (this._isDisposed) {\n return;\n }\n this._isDisposed = true;\n for (const d of this._disposables) {\n d.dispose();\n }\n this._disposables.clear();\n }\n\n public clear(): void {\n for (const d of this._disposables) {\n d.dispose();\n }\n this._disposables.clear();\n }\n}\n\nexport abstract class Disposable implements IDisposable {\n public static readonly None: IDisposable = Object.freeze({ dispose() { } });\n\n protected readonly _store = new DisposableStore();\n\n public dispose(): void {\n this._store.dispose();\n }\n\n protected _register(o: T): T {\n return this._store.add(o);\n }\n}\n\nexport class MutableDisposable implements IDisposable {\n private _value: T | undefined;\n private _isDisposed = false;\n\n public get value(): T | undefined {\n return this._isDisposed ? undefined : this._value;\n }\n\n public set value(value: T | undefined) {\n if (this._isDisposed || value === this._value) {\n return;\n }\n this._value?.dispose();\n this._value = value;\n }\n\n public clear(): void {\n this.value = undefined;\n }\n\n public dispose(): void {\n this._isDisposed = true;\n this._value?.dispose();\n this._value = undefined;\n }\n}\n","/**\n * Copyright (c) 2016 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\ninterface INavigator {\n userAgent: string;\n language: string;\n platform: string;\n}\n\n// We're declaring a navigator global here as we expect it in all runtimes (node and browser), but\n// we want this module to live in common.\ndeclare const navigator: INavigator;\ndeclare const process: unknown;\n\n// navigator.userAgent is also checked here because bundling with the process module can cause\n// issues otherwise. Note that navigator exists in Node.js 21+ but the userAgent is\n// \"Node.js/\".\nexport const isNode = (typeof process !== 'undefined' && 'title' in (process as any) && (typeof navigator === 'undefined' || navigator.userAgent.startsWith('Node.js/'))) ? true : false;\nconst userAgent = (isNode) ? 'node' : navigator.userAgent;\nconst platform = (isNode) ? 'node' : navigator.platform;\n\nexport const isFirefox = userAgent.includes('Firefox');\nexport const isChrome = userAgent.includes('Chrome');\nexport const isLegacyEdge = userAgent.includes('Edge');\nexport const isSafari = /^((?!chrome|android).)*safari/i.test(userAgent);\n\ninterface IZoomWindow {\n devicePixelRatio?: number;\n}\n\nexport function getZoomFactor(_targetWindow: IZoomWindow): number {\n return 1;\n}\nexport function getSafariVersion(): number {\n if (!isSafari) {\n return 0;\n }\n const majorVersion = userAgent.match(/Version\\/(\\d+)/);\n if (majorVersion === null || majorVersion.length < 2) {\n return 0;\n }\n return parseInt(majorVersion[1], 10);\n}\n\n// Find the user's platform. We use this to interpret the meta key\n// and ISO third level shifts.\n// http://stackoverflow.com/q/19877924/577598\nexport const isMac = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'].includes(platform);\nexport const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].includes(platform);\nexport const isLinux = platform.indexOf('Linux') >= 0;\n// Note that when this is true, isLinux will also be true.\nexport const isChromeOS = /\\bCrOS\\b/.test(userAgent);\n","/**\n * Copyright (c) 2026 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\n/**\n * Accumulates string data from multiple chunks without O(n²) string concatenation.\n */\nexport class StringBuilder {\n private _chunks: string[] = [];\n private _length = 0;\n\n public get length(): number {\n return this._length;\n }\n\n public reset(): void {\n this._chunks.length = 0;\n this._length = 0;\n }\n\n public append(chunk: string): void {\n this._chunks.push(chunk);\n this._length += chunk.length;\n }\n\n public toString(): string {\n return this._chunks.join('');\n }\n}\n\n/**\n * String builder that rejects payloads larger than a fixed limit.\n */\nexport class LimitedStringBuilder {\n private readonly _builder = new StringBuilder();\n\n constructor(private readonly _limit: number) { }\n\n public get length(): number {\n return this._builder.length;\n }\n\n public get limit(): number {\n return this._limit;\n }\n\n public reset(): void {\n this._builder.reset();\n }\n\n /**\n * @returns true if the limit was exceeded (buffer is cleared in that case)\n */\n public append(chunk: string): boolean {\n this._builder.append(chunk);\n if (this._builder.length > this._limit) {\n this._builder.reset();\n return true;\n }\n return false;\n }\n\n public toString(): string {\n return this._builder.toString();\n }\n}\n","/**\n * Copyright (c) 2022 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport type { ILogService } from './services/Services';\n\ninterface ITaskQueue {\n /**\n * Adds a task to the queue which will run in a future idle callback.\n * To avoid perceivable stalls on the main thread, tasks with heavy workload\n * should split their work into smaller pieces and return `true` to get\n * called again until the work is done (on falsy return value).\n */\n enqueue(task: () => boolean | void): void;\n\n /**\n * Flushes the queue, running all remaining tasks synchronously.\n */\n flush(): void;\n\n /**\n * Clears any remaining tasks from the queue, these will not be run.\n */\n clear(): void;\n}\n\ninterface ITaskDeadline {\n timeRemaining(): number;\n}\ntype CallbackWithDeadline = (deadline: ITaskDeadline) => void;\n\nabstract class TaskQueue implements ITaskQueue {\n private _tasks: (() => boolean | void)[] = [];\n private _idleCallback?: number;\n private _i = 0;\n protected readonly _logService: ILogService;\n\n constructor(logService: ILogService) {\n this._logService = logService;\n }\n\n protected abstract _requestCallback(callback: CallbackWithDeadline): number;\n protected abstract _cancelCallback(identifier: number): void;\n\n public enqueue(task: () => boolean | void): void {\n this._tasks.push(task);\n this._start();\n }\n\n public flush(): void {\n while (this._i < this._tasks.length) {\n if (!this._tasks[this._i]()) {\n this._i++;\n }\n }\n this.clear();\n }\n\n public clear(): void {\n if (this._idleCallback) {\n this._cancelCallback(this._idleCallback);\n this._idleCallback = undefined;\n }\n this._i = 0;\n this._tasks.length = 0;\n }\n\n private _start(): void {\n if (!this._idleCallback) {\n this._idleCallback = this._requestCallback(this._process.bind(this));\n }\n }\n\n private _process(deadline: ITaskDeadline): void {\n this._idleCallback = undefined;\n let taskDuration: number;\n let longestTask = 0;\n let lastDeadlineRemaining = deadline.timeRemaining();\n let deadlineRemaining: number;\n while (this._i < this._tasks.length) {\n taskDuration = performance.now();\n if (!this._tasks[this._i]()) {\n this._i++;\n }\n // other than performance.now, performance.now might not be stable (changes on wall clock\n // changes), this is not an issue here as a clock change during a short running task is very\n // unlikely in case it still happened and leads to negative duration, simply assume 1 msec\n taskDuration = Math.max(1, performance.now() - taskDuration);\n longestTask = Math.max(taskDuration, longestTask);\n // Guess the following task will take a similar time to the longest task in this batch, allow\n // additional room to try avoid exceeding the deadline\n deadlineRemaining = deadline.timeRemaining();\n if (longestTask * 1.5 > deadlineRemaining) {\n // Warn when the time exceeding the deadline is over 20ms, if this happens in practice the\n // task should be split into sub-tasks to ensure the UI remains responsive.\n if (lastDeadlineRemaining - taskDuration < -20) {\n this._logService.warn(`task queue exceeded allotted deadline by ${Math.abs(Math.round(lastDeadlineRemaining - taskDuration))}ms`);\n }\n this._start();\n return;\n }\n lastDeadlineRemaining = deadlineRemaining;\n }\n this.clear();\n }\n}\n\n/**\n * A queue of that runs tasks over several tasks via setTimeout, trying to maintain above 60 frames\n * per second. The tasks will run in the order they are enqueued, but they will run some time later,\n * and care should be taken to ensure they're non-urgent and will not introduce race conditions.\n */\nexport class PriorityTaskQueue extends TaskQueue {\n protected _requestCallback(callback: CallbackWithDeadline): number {\n return setTimeout(() => callback(this._createDeadline(16)));\n }\n\n protected _cancelCallback(identifier: number): void {\n clearTimeout(identifier);\n }\n\n private _createDeadline(duration: number): ITaskDeadline {\n const end = performance.now() + duration;\n return {\n timeRemaining: () => Math.max(0, end - performance.now())\n };\n }\n}\n\nclass IdleTaskQueueInternal extends TaskQueue {\n protected _requestCallback(callback: IdleRequestCallback): number {\n return requestIdleCallback(callback);\n }\n\n protected _cancelCallback(identifier: number): void {\n cancelIdleCallback(identifier);\n }\n}\n\n/**\n * A queue of that runs tasks over several idle callbacks, trying to respect the idle callback's\n * deadline given by the environment. The tasks will run in the order they are enqueued, but they\n * will run some time later, and care should be taken to ensure they're non-urgent and will not\n * introduce race conditions.\n *\n * This reverts to a {@link PriorityTaskQueue} if the environment does not support idle callbacks.\n */\n// eslint-disable-next-line @typescript-eslint/naming-convention\nexport const IdleTaskQueue = ('requestIdleCallback' in globalThis) ? IdleTaskQueueInternal : PriorityTaskQueue;\n\n/**\n * An object that tracks a single debounced task that will run on the next idle frame. When called\n * multiple times, only the last set task will run.\n */\nexport class DebouncedIdleTask {\n private _queue: ITaskQueue;\n\n constructor(logService: ILogService) {\n this._queue = new IdleTaskQueue(logService);\n }\n\n public set(task: () => boolean | void): void {\n this._queue.clear();\n this._queue.enqueue(task);\n }\n\n public flush(): void {\n this._queue.flush();\n }\n\n public dispose(): void {\n this._queue.clear();\n }\n}\n","/**\n * Copyright (c) 2025 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\n/**\n * The xterm.js version. This is updated by the publish script from package.json.\n */\nexport const XTERM_VERSION = '6.0.0';\n","/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { CHAR_DATA_CODE_INDEX, NULL_CELL_CODE, WHITESPACE_CELL_CODE } from './buffer/Constants';\nimport { IBufferService } from './services/Services';\n\nexport function updateWindowsModeWrappedState(bufferService: IBufferService): void {\n // Winpty does not support wraparound mode which means that lines will never\n // be marked as wrapped. This causes issues for things like copying a line\n // retaining the wrapped new line characters or if consumers are listening\n // in on the data stream.\n //\n // The workaround for this is to listen to every incoming line feed and mark\n // the line as wrapped if the last character in the previous line is not a\n // space. This is certainly not without its problems, but generally on\n // Windows when text reaches the end of the terminal it's likely going to be\n // wrapped.\n const line = bufferService.buffer.lines.get(bufferService.buffer.ybase + bufferService.buffer.y - 1);\n const lastChar = line?.get(bufferService.cols - 1);\n\n const nextLine = bufferService.buffer.lines.get(bufferService.buffer.ybase + bufferService.buffer.y);\n if (nextLine && lastChar) {\n nextLine.isWrapped = (lastChar[CHAR_DATA_CODE_INDEX] !== NULL_CELL_CODE && lastChar[CHAR_DATA_CODE_INDEX] !== WHITESPACE_CELL_CODE);\n }\n}\n","/**\n * Copyright (c) 2018 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { IColorRGB } from '../Types';\nimport { IAttributeData, IExtendedAttrs } from './Types';\nimport { Attributes, FgFlags, BgFlags, UnderlineStyle, ExtFlags } from './Constants';\n\nexport class AttributeData implements IAttributeData {\n public static toColorRGB(value: number): IColorRGB {\n return [\n value >>> Attributes.RED_SHIFT & 255,\n value >>> Attributes.GREEN_SHIFT & 255,\n value & 255\n ];\n }\n\n public static fromColorRGB(value: IColorRGB): number {\n return (value[0] & 255) << Attributes.RED_SHIFT | (value[1] & 255) << Attributes.GREEN_SHIFT | value[2] & 255;\n }\n\n public clone(): IAttributeData {\n const newObj = new AttributeData();\n newObj.fg = this.fg;\n newObj.bg = this.bg;\n newObj.extended = this.extended.clone();\n return newObj;\n }\n\n // data\n public fg = 0;\n public bg = 0;\n public extended: IExtendedAttrs = new ExtendedAttrs();\n\n // flags\n public isInverse(): number { return this.fg & FgFlags.INVERSE; }\n public isBold(): number { return this.fg & FgFlags.BOLD; }\n public isUnderline(): number {\n if (this.hasExtendedAttrs() && this.extended.underlineStyle !== UnderlineStyle.NONE) {\n return 1;\n }\n return this.fg & FgFlags.UNDERLINE;\n }\n public isBlink(): number { return this.fg & FgFlags.BLINK; }\n public isInvisible(): number { return this.fg & FgFlags.INVISIBLE; }\n public isItalic(): number { return this.bg & BgFlags.ITALIC; }\n public isDim(): number { return this.bg & BgFlags.DIM; }\n public isStrikethrough(): number { return this.fg & FgFlags.STRIKETHROUGH; }\n public isProtected(): number { return this.bg & BgFlags.PROTECTED; }\n public isOverline(): number { return this.bg & BgFlags.OVERLINE; }\n\n // color modes\n public getFgColorMode(): number { return this.fg & Attributes.CM_MASK; }\n public getBgColorMode(): number { return this.bg & Attributes.CM_MASK; }\n public isFgRGB(): boolean { return (this.fg & Attributes.CM_MASK) === Attributes.CM_RGB; }\n public isBgRGB(): boolean { return (this.bg & Attributes.CM_MASK) === Attributes.CM_RGB; }\n public isFgPalette(): boolean { return (this.fg & Attributes.CM_MASK) === Attributes.CM_P16 || (this.fg & Attributes.CM_MASK) === Attributes.CM_P256; }\n public isBgPalette(): boolean { return (this.bg & Attributes.CM_MASK) === Attributes.CM_P16 || (this.bg & Attributes.CM_MASK) === Attributes.CM_P256; }\n public isFgDefault(): boolean { return (this.fg & Attributes.CM_MASK) === 0; }\n public isBgDefault(): boolean { return (this.bg & Attributes.CM_MASK) === 0; }\n public isAttributeDefault(): boolean { return this.fg === 0 && this.bg === 0; }\n\n // colors\n public getFgColor(): number {\n switch (this.fg & Attributes.CM_MASK) {\n case Attributes.CM_P16:\n case Attributes.CM_P256: return this.fg & Attributes.PCOLOR_MASK;\n case Attributes.CM_RGB: return this.fg & Attributes.RGB_MASK;\n default: return -1; // CM_DEFAULT defaults to -1\n }\n }\n public getBgColor(): number {\n switch (this.bg & Attributes.CM_MASK) {\n case Attributes.CM_P16:\n case Attributes.CM_P256: return this.bg & Attributes.PCOLOR_MASK;\n case Attributes.CM_RGB: return this.bg & Attributes.RGB_MASK;\n default: return -1; // CM_DEFAULT defaults to -1\n }\n }\n\n // extended attrs\n public hasExtendedAttrs(): number {\n return this.bg & BgFlags.HAS_EXTENDED;\n }\n public updateExtended(): void {\n if (this.extended.isEmpty()) {\n this.bg &= ~BgFlags.HAS_EXTENDED;\n } else {\n this.bg |= BgFlags.HAS_EXTENDED;\n }\n }\n public getUnderlineColor(): number {\n if ((this.bg & BgFlags.HAS_EXTENDED) && ~this.extended.underlineColor) {\n switch (this.extended.underlineColor & Attributes.CM_MASK) {\n case Attributes.CM_P16:\n case Attributes.CM_P256: return this.extended.underlineColor & Attributes.PCOLOR_MASK;\n case Attributes.CM_RGB: return this.extended.underlineColor & Attributes.RGB_MASK;\n default: return this.getFgColor();\n }\n }\n return this.getFgColor();\n }\n public getUnderlineColorMode(): number {\n return (this.bg & BgFlags.HAS_EXTENDED) && ~this.extended.underlineColor\n ? this.extended.underlineColor & Attributes.CM_MASK\n : this.getFgColorMode();\n }\n public isUnderlineColorRGB(): boolean {\n return (this.bg & BgFlags.HAS_EXTENDED) && ~this.extended.underlineColor\n ? (this.extended.underlineColor & Attributes.CM_MASK) === Attributes.CM_RGB\n : this.isFgRGB();\n }\n public isUnderlineColorPalette(): boolean {\n return (this.bg & BgFlags.HAS_EXTENDED) && ~this.extended.underlineColor\n ? (this.extended.underlineColor & Attributes.CM_MASK) === Attributes.CM_P16\n || (this.extended.underlineColor & Attributes.CM_MASK) === Attributes.CM_P256\n : this.isFgPalette();\n }\n public isUnderlineColorDefault(): boolean {\n return (this.bg & BgFlags.HAS_EXTENDED) && ~this.extended.underlineColor\n ? (this.extended.underlineColor & Attributes.CM_MASK) === 0\n : this.isFgDefault();\n }\n public getUnderlineStyle(): UnderlineStyle {\n return this.fg & FgFlags.UNDERLINE\n ? (this.bg & BgFlags.HAS_EXTENDED ? this.extended.underlineStyle : UnderlineStyle.SINGLE)\n : UnderlineStyle.NONE;\n }\n public getUnderlineVariantOffset(): number {\n return this.extended.underlineVariantOffset;\n }\n}\n\n\n/**\n * Extended attributes for a cell.\n * Holds information about different underline styles and color.\n */\nexport class ExtendedAttrs implements IExtendedAttrs {\n private _ext: number = 0;\n public get ext(): number {\n if (this._urlId) {\n return (\n (this._ext & ~ExtFlags.UNDERLINE_STYLE) |\n (this.underlineStyle << 26)\n );\n }\n return this._ext;\n }\n public set ext(value: number) { this._ext = value; }\n\n public get underlineStyle(): UnderlineStyle {\n // Always return the URL style if it has one\n if (this._urlId) {\n return UnderlineStyle.DASHED;\n }\n return (this._ext & ExtFlags.UNDERLINE_STYLE) >> 26;\n }\n public set underlineStyle(value: UnderlineStyle) {\n this._ext &= ~ExtFlags.UNDERLINE_STYLE;\n this._ext |= (value << 26) & ExtFlags.UNDERLINE_STYLE;\n }\n\n public get underlineColor(): number {\n return this._ext & (Attributes.CM_MASK | Attributes.RGB_MASK);\n }\n public set underlineColor(value: number) {\n this._ext &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);\n this._ext |= value & (Attributes.CM_MASK | Attributes.RGB_MASK);\n }\n\n private _urlId: number = 0;\n public get urlId(): number {\n return this._urlId;\n }\n public set urlId(value: number) {\n this._urlId = value;\n }\n\n public get underlineVariantOffset(): number {\n const val = (this._ext & ExtFlags.VARIANT_OFFSET) >> 29;\n if (val < 0) {\n return val ^ 0xFFFFFFF8;\n }\n return val;\n }\n public set underlineVariantOffset(value: number) {\n this._ext &= ~ExtFlags.VARIANT_OFFSET;\n this._ext |= (value << 29) & ExtFlags.VARIANT_OFFSET;\n }\n\n constructor(\n ext: number = 0,\n urlId: number = 0\n ) {\n this._ext = ext;\n this._urlId = urlId;\n }\n\n public clone(): IExtendedAttrs {\n return new ExtendedAttrs(this._ext, this._urlId);\n }\n\n /**\n * Convenient method to indicate whether the object holds no additional information,\n * that needs to be persistant in the buffer.\n */\n public isEmpty(): boolean {\n return this.underlineStyle === UnderlineStyle.NONE && this._urlId === 0;\n }\n}\n","/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { CircularList, IInsertEvent } from '../CircularList';\nimport { Disposable, toDisposable } from '../Lifecycle';\nimport { IdleTaskQueue } from '../TaskQueue';\nimport { ICharset } from '../Types';\nimport { IAttributeData, IBuffer, IBufferLine, ICellData } from './Types';\nimport { ExtendedAttrs } from './AttributeData';\nimport { BufferLine, DEFAULT_ATTR_DATA } from './BufferLine';\nimport { getWrappedLineTrimmedLength, reflowLargerApplyNewLayout, reflowLargerCreateNewLayout, reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths } from './BufferReflow';\nimport { CellData } from './CellData';\nimport { NULL_CELL_CHAR, NULL_CELL_CODE, NULL_CELL_WIDTH, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE, WHITESPACE_CELL_WIDTH } from './Constants';\nimport { Marker } from './Marker';\nimport { DEFAULT_CHARSET } from '../data/Charsets';\nimport { IBufferService, ILogService, IOptionsService } from '../services/Services';\n\nexport const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1\n\n/**\n * This class represents a terminal buffer (an internal state of the terminal), where the\n * following information is stored (in high-level):\n * - text content of this particular buffer\n * - cursor position\n * - scroll position\n */\nexport class Buffer extends Disposable implements IBuffer {\n public lines: CircularList;\n public ydisp: number = 0;\n public ybase: number = 0;\n public y: number = 0;\n public x: number = 0;\n public scrollBottom: number;\n public scrollTop: number;\n public tabs: { [column: number]: boolean | undefined } = {};\n public savedY: number = 0;\n public savedX: number = 0;\n public savedCurAttrData = DEFAULT_ATTR_DATA.clone();\n public savedCharset: ICharset | undefined = DEFAULT_CHARSET;\n public savedCharsets: (ICharset | undefined)[] = [];\n public savedGlevel: number = 0;\n public savedOriginMode: boolean = false;\n public savedWraparoundMode: boolean = true;\n public markers: Marker[] = [];\n private _nullCell: ICellData = CellData.fromCharData([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);\n private _whitespaceCell: ICellData = CellData.fromCharData([0, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_WIDTH, WHITESPACE_CELL_CODE]);\n private _cols: number;\n private _rows: number;\n private _isClearing: boolean = false;\n private _memoryCleanupQueue: InstanceType;\n private _memoryCleanupPosition = 0;\n\n constructor(\n private _hasScrollback: boolean,\n private _optionsService: IOptionsService,\n private _bufferService: IBufferService,\n private readonly _logService: ILogService\n ) {\n super();\n this._cols = this._bufferService.cols;\n this._rows = this._bufferService.rows;\n this.lines = new CircularList(this._getCorrectBufferLength(this._rows));\n this.scrollTop = 0;\n this.scrollBottom = this._rows - 1;\n this.setupTabStops();\n this._memoryCleanupQueue = new IdleTaskQueue(this._logService);\n this._register(toDisposable(() => this._memoryCleanupQueue.clear()));\n this._register(toDisposable(() => this.clearAllMarkers()));\n }\n\n public getNullCell(attr?: IAttributeData): ICellData {\n if (attr) {\n this._nullCell.fg = attr.fg;\n this._nullCell.bg = attr.bg;\n this._nullCell.extended = attr.extended;\n } else {\n this._nullCell.fg = 0;\n this._nullCell.bg = 0;\n this._nullCell.extended = new ExtendedAttrs();\n }\n return this._nullCell;\n }\n\n public getWhitespaceCell(attr?: IAttributeData): ICellData {\n if (attr) {\n this._whitespaceCell.fg = attr.fg;\n this._whitespaceCell.bg = attr.bg;\n this._whitespaceCell.extended = attr.extended;\n } else {\n this._whitespaceCell.fg = 0;\n this._whitespaceCell.bg = 0;\n this._whitespaceCell.extended = new ExtendedAttrs();\n }\n return this._whitespaceCell;\n }\n\n public getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine {\n return new BufferLine(this._bufferService.cols, this.getNullCell(attr), isWrapped);\n }\n\n public get hasScrollback(): boolean {\n return this._hasScrollback && this.lines.maxLength > this._rows;\n }\n\n public get isCursorInViewport(): boolean {\n const absoluteY = this.ybase + this.y;\n const relativeY = absoluteY - this.ydisp;\n return (relativeY >= 0 && relativeY < this._rows);\n }\n\n /**\n * Gets the correct buffer length based on the rows provided, the terminal's\n * scrollback and whether this buffer is flagged to have scrollback or not.\n * @param rows The terminal rows to use in the calculation.\n */\n private _getCorrectBufferLength(rows: number): number {\n if (!this._hasScrollback) {\n return rows;\n }\n\n const correctBufferLength = rows + this._optionsService.rawOptions.scrollback;\n\n return correctBufferLength > MAX_BUFFER_SIZE ? MAX_BUFFER_SIZE : correctBufferLength;\n }\n\n /**\n * Fills the buffer's viewport with blank lines.\n */\n public fillViewportRows(fillAttr?: IAttributeData): void {\n if (this.lines.length === 0) {\n fillAttr ??= DEFAULT_ATTR_DATA;\n let i = this._rows;\n while (i--) {\n this.lines.push(this.getBlankLine(fillAttr));\n }\n }\n }\n\n /**\n * Clears the buffer to its initial state, discarding all previous data.\n */\n public clear(): void {\n this.ydisp = 0;\n this.ybase = 0;\n this.y = 0;\n this.x = 0;\n this.lines = new CircularList(this._getCorrectBufferLength(this._rows));\n this.scrollTop = 0;\n this.scrollBottom = this._rows - 1;\n this.setupTabStops();\n }\n\n /**\n * Resizes the buffer, adjusting its data accordingly.\n * @param newCols The new number of columns.\n * @param newRows The new number of rows.\n */\n public resize(newCols: number, newRows: number): void {\n // store reference to null cell with default attrs\n const nullCell = this.getNullCell(DEFAULT_ATTR_DATA);\n\n // count bufferlines with overly big memory to be cleaned afterwards\n let dirtyMemoryLines = 0;\n\n // Increase max length if needed before adjustments to allow space to fill\n // as required.\n const newMaxLength = this._getCorrectBufferLength(newRows);\n if (newMaxLength > this.lines.maxLength) {\n this.lines.maxLength = newMaxLength;\n }\n\n // if (this._cols > newCols) {\n // console.log('increase!');\n // }\n\n // The following adjustments should only happen if the buffer has been\n // initialized/filled.\n if (this.lines.length > 0) {\n // Deal with columns increasing (reducing needs to happen after reflow)\n if (this._cols < newCols) {\n for (let i = 0; i < this.lines.length; i++) {\n // +boolean for fast 0 or 1 conversion\n dirtyMemoryLines += +this.lines.get(i)!.resize(newCols, nullCell);\n }\n }\n\n // Resize rows in both directions as needed\n let addToY = 0;\n if (this._rows < newRows) {\n for (let y = this._rows; y < newRows; y++) {\n if (this.lines.length < newRows + this.ybase) {\n if (this._optionsService.rawOptions.windowsPty.backend !== undefined || this._optionsService.rawOptions.windowsPty.buildNumber !== undefined) {\n // Just add the new missing rows on Windows as conpty reprints the screen with its\n // view of the world. Once a line enters scrollback for conpty it remains there\n this.lines.push(new BufferLine(newCols, nullCell, false));\n } else {\n if (this.ybase > 0 && this.lines.length <= this.ybase + this.y + addToY + 1) {\n // There is room above the buffer and there are no empty elements below the line,\n // scroll up\n this.ybase--;\n addToY++;\n if (this.ydisp > 0) {\n // Viewport is at the top of the buffer, must increase downwards\n this.ydisp--;\n }\n } else {\n // Add a blank line if there is no buffer left at the top to scroll to, or if there\n // are blank lines after the cursor\n this.lines.push(new BufferLine(newCols, nullCell, false));\n }\n }\n }\n }\n } else { // (this._rows >= newRows)\n for (let y = this._rows; y > newRows; y--) {\n if (this.lines.length > newRows + this.ybase) {\n if (this.lines.length > this.ybase + this.y + 1) {\n // The line is a blank line below the cursor, remove it\n this.lines.pop();\n } else {\n // The line is the cursor, scroll down\n this.ybase++;\n this.ydisp++;\n }\n }\n }\n }\n\n // Reduce max length if needed after adjustments, this is done after as it\n // would otherwise cut data from the bottom of the buffer.\n if (newMaxLength < this.lines.maxLength) {\n // Trim from the top of the buffer and adjust ybase and ydisp.\n const amountToTrim = this.lines.length - newMaxLength;\n if (amountToTrim > 0) {\n this.lines.trimStart(amountToTrim);\n this.ybase = Math.max(this.ybase - amountToTrim, 0);\n this.ydisp = Math.max(this.ydisp - amountToTrim, 0);\n this.savedY = Math.max(this.savedY - amountToTrim, 0);\n }\n this.lines.maxLength = newMaxLength;\n }\n\n // Make sure that the cursor stays on screen\n this.x = Math.min(this.x, newCols - 1);\n this.y = Math.min(this.y, newRows - 1);\n if (addToY) {\n this.y += addToY;\n }\n this.savedX = Math.min(this.savedX, newCols - 1);\n\n this.scrollTop = 0;\n }\n\n this.scrollBottom = newRows - 1;\n\n if (this._isReflowEnabled) {\n this._reflow(newCols, newRows);\n\n // Trim the end of the line off if cols shrunk\n if (this._cols > newCols) {\n for (let i = 0; i < this.lines.length; i++) {\n // +boolean for fast 0 or 1 conversion\n dirtyMemoryLines += +this.lines.get(i)!.resize(newCols, nullCell);\n }\n }\n }\n\n this._cols = newCols;\n this._rows = newRows;\n\n // Ensure the cursor position invariant: ybase + y must be within buffer bounds\n // This can be violated during reflow or when shrinking rows\n if (this.lines.length > 0) {\n const maxY = Math.max(0, this.lines.length - this.ybase - 1);\n this.y = Math.min(this.y, maxY);\n }\n\n this._memoryCleanupQueue.clear();\n // schedule memory cleanup only, if more than 10% of the lines are affected\n if (dirtyMemoryLines > 0.1 * this.lines.length) {\n this._memoryCleanupPosition = 0;\n this._memoryCleanupQueue.enqueue(() => this._batchedMemoryCleanup());\n }\n }\n\n private _batchedMemoryCleanup(): boolean {\n let normalRun = true;\n if (this._memoryCleanupPosition >= this.lines.length) {\n // cleanup made it once through all lines, thus rescan in loop below to also catch shifted\n // lines, which should finish rather quick if there are no more cleanups pending\n this._memoryCleanupPosition = 0;\n normalRun = false;\n }\n let counted = 0;\n while (this._memoryCleanupPosition < this.lines.length) {\n counted += this.lines.get(this._memoryCleanupPosition++)!.cleanupMemory();\n // cleanup max 100 lines per batch\n if (counted > 100) {\n return true;\n }\n }\n // normal runs always need another rescan afterwards\n // if we made it here with normalRun=false, we are in a final run\n // and can end the cleanup task for sure\n return normalRun;\n }\n\n private get _isReflowEnabled(): boolean {\n const windowsPty = this._optionsService.rawOptions.windowsPty;\n if (windowsPty && windowsPty.buildNumber) {\n return this._hasScrollback && windowsPty.backend === 'conpty' && windowsPty.buildNumber >= 21376;\n }\n return this._hasScrollback;\n }\n\n private _reflow(newCols: number, newRows: number): void {\n if (this._cols === newCols) {\n return;\n }\n\n // Iterate through rows, ignore the last one as it cannot be wrapped\n if (newCols > this._cols) {\n this._reflowLarger(newCols, newRows);\n } else {\n this._reflowSmaller(newCols, newRows);\n }\n }\n\n private _reflowLarger(newCols: number, newRows: number): void {\n const reflowCursorLine = this._optionsService.rawOptions.reflowCursorLine;\n const toRemove: number[] = reflowLargerGetLinesToRemove(this.lines, this._cols, newCols, this.ybase + this.y, this.getNullCell(DEFAULT_ATTR_DATA), reflowCursorLine);\n if (toRemove.length > 0) {\n const newLayoutResult = reflowLargerCreateNewLayout(this.lines, toRemove);\n reflowLargerApplyNewLayout(this.lines, newLayoutResult.layout);\n this._reflowLargerAdjustViewport(newCols, newRows, newLayoutResult.countRemoved);\n }\n }\n\n private _reflowLargerAdjustViewport(newCols: number, newRows: number, countRemoved: number): void {\n const nullCell = this.getNullCell(DEFAULT_ATTR_DATA);\n // Adjust viewport based on number of items removed\n let viewportAdjustments = countRemoved;\n while (viewportAdjustments-- > 0) {\n if (this.ybase === 0) {\n if (this.y > 0) {\n this.y--;\n }\n if (this.lines.length < newRows) {\n // Add an extra row at the bottom of the viewport\n this.lines.push(new BufferLine(newCols, nullCell, false));\n }\n } else {\n if (this.ydisp === this.ybase) {\n this.ydisp--;\n }\n this.ybase--;\n }\n }\n this.savedY = Math.max(this.savedY - countRemoved, 0);\n }\n\n private _reflowSmaller(newCols: number, newRows: number): void {\n const reflowCursorLine = this._optionsService.rawOptions.reflowCursorLine;\n const nullCell = this.getNullCell(DEFAULT_ATTR_DATA);\n // Gather all BufferLines that need to be inserted into the Buffer here so that they can be\n // batched up and only committed once\n const toInsert = [];\n let countToInsert = 0;\n // Go backwards as many lines may be trimmed and this will avoid considering them\n for (let y = this.lines.length - 1; y >= 0; y--) {\n // Check whether this line is a problem\n let nextLine = this.lines.get(y) as BufferLine;\n if (!nextLine || !nextLine.isWrapped && nextLine.getTrimmedLength() <= newCols) {\n continue;\n }\n\n // Gather wrapped lines and adjust y to be the starting line\n const wrappedLines: BufferLine[] = [nextLine];\n while (nextLine.isWrapped && y > 0) {\n nextLine = this.lines.get(--y) as BufferLine;\n wrappedLines.unshift(nextLine);\n }\n\n if (!reflowCursorLine) {\n // If these lines contain the cursor don't touch them, the program will handle fixing up\n // wrapped lines with the cursor\n const absoluteY = this.ybase + this.y;\n if (absoluteY >= y && absoluteY < y + wrappedLines.length) {\n continue;\n }\n }\n\n const lastLineLength = wrappedLines[wrappedLines.length - 1].getTrimmedLength();\n const destLineLengths = reflowSmallerGetNewLineLengths(wrappedLines, this._cols, newCols);\n const linesToAdd = destLineLengths.length - wrappedLines.length;\n let trimmedLines: number;\n if (this.ybase === 0 && this.y !== this.lines.length - 1) {\n // If the top section of the buffer is not yet filled\n trimmedLines = Math.max(0, this.y - this.lines.maxLength + linesToAdd);\n } else {\n trimmedLines = Math.max(0, this.lines.length - this.lines.maxLength + linesToAdd);\n }\n\n // Add the new lines\n const newLines: BufferLine[] = [];\n for (let i = 0; i < linesToAdd; i++) {\n const newLine = this.getBlankLine(DEFAULT_ATTR_DATA, true) as BufferLine;\n newLines.push(newLine);\n }\n if (newLines.length > 0) {\n toInsert.push({\n // countToInsert here gets the actual index, taking into account other inserted items.\n // using this we can iterate through the list forwards\n start: y + wrappedLines.length + countToInsert,\n newLines\n });\n countToInsert += newLines.length;\n }\n wrappedLines.push(...newLines);\n\n // Copy buffer data to new locations, this needs to happen backwards to do in-place\n let destLineIndex = destLineLengths.length - 1; // Math.floor(cellsNeeded / newCols);\n let destCol = destLineLengths[destLineIndex]; // cellsNeeded % newCols;\n if (destCol === 0) {\n destLineIndex--;\n destCol = destLineLengths[destLineIndex];\n }\n let srcLineIndex = wrappedLines.length - linesToAdd - 1;\n let srcCol = lastLineLength;\n while (srcLineIndex >= 0) {\n const cellsToCopy = Math.min(srcCol, destCol);\n if (wrappedLines[destLineIndex] === undefined) {\n // Sanity check that the line exists, this has been known to fail for an unknown reason\n // which would stop the reflow from happening if an exception would throw.\n break;\n }\n wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol - cellsToCopy, destCol - cellsToCopy, cellsToCopy, true);\n destCol -= cellsToCopy;\n if (destCol === 0) {\n destLineIndex--;\n destCol = destLineLengths[destLineIndex];\n }\n srcCol -= cellsToCopy;\n if (srcCol === 0) {\n srcLineIndex--;\n const wrappedLinesIndex = Math.max(srcLineIndex, 0);\n srcCol = getWrappedLineTrimmedLength(wrappedLines, wrappedLinesIndex, this._cols);\n }\n }\n\n // Null out the end of the line ends if a wide character wrapped to the following line\n for (let i = 0; i < wrappedLines.length; i++) {\n if (destLineLengths[i] < newCols) {\n wrappedLines[i].setCell(destLineLengths[i], nullCell);\n }\n }\n\n // Adjust viewport as needed\n let viewportAdjustments = linesToAdd - trimmedLines;\n while (viewportAdjustments-- > 0) {\n if (this.ybase === 0) {\n if (this.y < newRows - 1) {\n this.y++;\n this.lines.pop();\n } else {\n this.ybase++;\n this.ydisp++;\n }\n } else {\n // Ensure ybase does not exceed its maximum value\n if (this.ybase < Math.min(this.lines.maxLength, this.lines.length + countToInsert) - newRows) {\n if (this.ybase === this.ydisp) {\n this.ydisp++;\n }\n this.ybase++;\n }\n }\n }\n this.savedY = Math.min(this.savedY + linesToAdd, this.ybase + newRows - 1);\n }\n\n // Rearrange lines in the buffer if there are any insertions, this is done at the end rather\n // than earlier so that it's a single O(n) pass through the buffer, instead of O(n^2) from many\n // costly calls to CircularList.splice.\n if (toInsert.length > 0) {\n // Record buffer insert events and then play them back backwards so that the indexes are\n // correct\n const insertEvents: IInsertEvent[] = [];\n\n // Record original lines so they don't get overridden when we rearrange the list\n const originalLines: BufferLine[] = [];\n for (let i = 0; i < this.lines.length; i++) {\n originalLines.push(this.lines.get(i) as BufferLine);\n }\n const originalLinesLength = this.lines.length;\n\n let originalLineIndex = originalLinesLength - 1;\n let nextToInsertIndex = 0;\n let nextToInsert = toInsert[nextToInsertIndex];\n this.lines.length = Math.min(this.lines.maxLength, this.lines.length + countToInsert);\n let countInsertedSoFar = 0;\n for (let i = Math.min(this.lines.maxLength - 1, originalLinesLength + countToInsert - 1); i >= 0; i--) {\n if (nextToInsert && nextToInsert.start > originalLineIndex + countInsertedSoFar) {\n // Insert extra lines here, adjusting i as needed\n for (let nextI = nextToInsert.newLines.length - 1; nextI >= 0; nextI--) {\n this.lines.set(i--, nextToInsert.newLines[nextI]);\n }\n i++;\n\n // Create insert events for later\n insertEvents.push({\n index: originalLineIndex + 1,\n amount: nextToInsert.newLines.length\n });\n\n countInsertedSoFar += nextToInsert.newLines.length;\n nextToInsert = toInsert[++nextToInsertIndex];\n } else {\n this.lines.set(i, originalLines[originalLineIndex--]);\n }\n }\n\n // Update markers\n let insertCountEmitted = 0;\n for (let i = insertEvents.length - 1; i >= 0; i--) {\n insertEvents[i].index += insertCountEmitted;\n this.lines.onInsertEmitter.fire(insertEvents[i]);\n insertCountEmitted += insertEvents[i].amount;\n }\n const amountToTrim = Math.max(0, originalLinesLength + countToInsert - this.lines.maxLength);\n if (amountToTrim > 0) {\n this.lines.onTrimEmitter.fire(amountToTrim);\n }\n }\n }\n\n /**\n * Translates a buffer line to a string, with optional start and end columns.\n * Wide characters will count as two columns in the resulting string. This\n * function is useful for getting the actual text underneath the raw selection\n * position.\n * @param lineIndex The absolute index of the line being translated.\n * @param trimRight Whether to trim whitespace to the right.\n * @param startCol The column to start at.\n * @param endCol The column to end at.\n */\n public translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol: number = 0, endCol?: number): string {\n const line = this.lines.get(lineIndex);\n if (!line) {\n return '';\n }\n return line.translateToString(trimRight, startCol, endCol);\n }\n\n public getWrappedRangeForLine(y: number): { first: number, last: number } {\n let first = y;\n let last = y;\n // Scan upwards for wrapped lines\n while (first > 0 && this.lines.get(first)!.isWrapped) {\n first--;\n }\n // Scan downwards for wrapped lines\n while (last + 1 < this.lines.length && this.lines.get(last + 1)!.isWrapped) {\n last++;\n }\n return { first, last };\n }\n\n /**\n * Setup the tab stops.\n * @param i The index to start setting up tab stops from.\n */\n public setupTabStops(i?: number): void {\n if (i !== null && i !== undefined) {\n if (!this.tabs[i]) {\n i = this.prevStop(i);\n }\n } else {\n this.tabs = {};\n i = 0;\n }\n\n for (; i < this._cols; i += this._optionsService.rawOptions.tabStopWidth) {\n this.tabs[i] = true;\n }\n }\n\n /**\n * Move the cursor to the previous tab stop from the given position (default is current).\n * @param x The position to move the cursor to the previous tab stop.\n */\n public prevStop(x?: number): number {\n x ??= this.x;\n while (!this.tabs[--x] && x > 0);\n return x >= this._cols ? this._cols - 1 : x < 0 ? 0 : x;\n }\n\n /**\n * Move the cursor one tab stop forward from the given position (default is current).\n * @param x The position to move the cursor one tab stop forward.\n */\n public nextStop(x?: number): number {\n x ??= this.x;\n while (!this.tabs[++x] && x < this._cols);\n return x >= this._cols ? this._cols - 1 : x < 0 ? 0 : x;\n }\n\n /**\n * Clears markers on single line.\n * @param y The line to clear.\n */\n public clearMarkers(y: number): void {\n this._isClearing = true;\n for (let i = 0; i < this.markers.length; i++) {\n if (this.markers[i].line === y) {\n this.markers[i].dispose();\n this.markers.splice(i--, 1);\n }\n }\n this._isClearing = false;\n }\n\n /**\n * Clears markers on all lines\n */\n public clearAllMarkers(): void {\n this._isClearing = true;\n for (let i = 0; i < this.markers.length; i++) {\n this.markers[i].dispose();\n }\n this.markers.length = 0;\n this._isClearing = false;\n }\n\n public addMarker(y: number): Marker {\n const marker = new Marker(y);\n this.markers.push(marker);\n marker.register(this.lines.onTrim(amount => {\n marker.line -= amount;\n // The marker should be disposed when the line is trimmed from the buffer\n if (marker.line < 0) {\n marker.dispose();\n }\n }));\n marker.register(this.lines.onInsert(event => {\n if (marker.line >= event.index) {\n marker.line += event.amount;\n }\n }));\n marker.register(this.lines.onDelete(event => {\n // Delete the marker if it's within the range\n if (marker.line >= event.index && marker.line < event.index + event.amount) {\n marker.dispose();\n }\n\n // Shift the marker if it's after the deleted range\n if (marker.line > event.index) {\n marker.line -= event.amount;\n }\n }));\n marker.register(marker.onDispose(() => this._removeMarker(marker)));\n return marker;\n }\n\n private _removeMarker(marker: Marker): void {\n if (!this._isClearing) {\n this.markers.splice(this.markers.indexOf(marker), 1);\n }\n }\n}\n","/**\n * Copyright (c) 2018 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { CharData, IAttributeData, IBufferLine, ICellData, IExtendedAttrs } from './Types';\nimport { AttributeData } from './AttributeData';\nimport { CellData } from './CellData';\nimport { Attributes, BgFlags, CHAR_DATA_ATTR_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, Content, NULL_CELL_CHAR, NULL_CELL_CODE, NULL_CELL_WIDTH, WHITESPACE_CELL_CHAR } from './Constants';\nimport { stringFromCodePoint } from '../input/TextDecoder';\n\n// Buffer memory layout:\n//\n// [0]: content `uint32_t` - wcwidth(2) comb(1) codepoint(21)\n// [1]: fg `uint32_t` - flags(8) r(8) g(8) b(8)\n// [2]: bg `uint32_t` - flags(8) r(8) g(8) b(8)\n\nconst enum Constants {\n /** The number of 32 bit array indices taken by one cell. */\n CELL_INDICIES = 3,\n /** Factor when to cleanup underlying array buffer after shrinking. */\n CLEANUP_THRESHOLD = 2\n}\n\n/**\n * Cell member indices.\n *\n * Direct access:\n * `content = data[column * Constants.CELL_INDICIES + Cell.CONTENT];`\n * `fg = data[column * Constants.CELL_INDICIES + Cell.FG];`\n * `bg = data[column * Constants.CELL_INDICIES + Cell.BG];`\n */\nconst enum Cell {\n CONTENT = 0,\n FG = 1, // currently simply holds all known attrs\n BG = 2 // currently unused\n}\n\n\ninterface IExtendedAttrsExt extends IExtendedAttrs {\n _ext: number;\n _urlId: number;\n}\n\n\nexport const DEFAULT_ATTR_DATA = Object.freeze(new AttributeData());\n\n// Work variables to avoid garbage collection\nlet $startIndex = 0;\nconst $workCell = new CellData();\nconst $extended = DEFAULT_ATTR_DATA.extended.clone() as IExtendedAttrsExt;\n\n\n/**\n * Typed array based bufferline implementation.\n *\n * There are 2 ways to insert data into the cell buffer:\n * - `setCellFromCodepoint` + `addCodepointToCell`\n * Use these for data that is already UTF32.\n * Used during normal input in `InputHandler` for faster buffer access.\n * - `setCell`\n * This method takes a CellData object and stores the data in the buffer.\n * Use `CellData.fromCharData` to create the CellData object (e.g. from JS string).\n *\n * To retrieve data from the buffer use either one of the primitive methods\n * (if only one particular value is needed) or `loadCell`. For `loadCell` in a loop\n * memory allocs / GC pressure can be greatly reduced by reusing the CellData object.\n */\nexport class BufferLine implements IBufferLine {\n protected _data: Uint32Array;\n /** Sparse cache; only read when `IS_COMBINED_MASK` is set in `_data`. */\n protected _combined: {[index: number]: string} = {};\n /** Sparse cache; only read when `HAS_EXTENDED` is set in `_data`. */\n protected _extendedAttrs: {[index: number]: IExtendedAttrs | undefined} = {};\n public length: number;\n\n /** line text cache */\n protected _cacheValid = false;\n protected _cache: string = '';\n protected _cacheTrimmed = false;\n\n constructor(\n cols: number,\n fillCellData?: ICellData,\n public isWrapped: boolean = false\n ) {\n this._data = new Uint32Array(cols * Constants.CELL_INDICIES);\n const cell = fillCellData ?? CellData.fromCharData([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);\n for (let i = 0; i < cols; ++i) {\n this.setCell(i, cell);\n }\n this.length = cols;\n }\n\n /**\n * Get cell data CharData.\n * @deprecated\n */\n public get(index: number): CharData {\n const content = this._data[index * Constants.CELL_INDICIES + Cell.CONTENT];\n const cp = content & Content.CODEPOINT_MASK;\n return [\n this._data[index * Constants.CELL_INDICIES + Cell.FG],\n (content & Content.IS_COMBINED_MASK)\n ? this._combined[index]\n : (cp) ? stringFromCodePoint(cp) : '',\n content >> Content.WIDTH_SHIFT,\n (content & Content.IS_COMBINED_MASK)\n ? this._combined[index].charCodeAt(this._combined[index].length - 1)\n : cp\n ];\n }\n\n /**\n * Set cell data from CharData.\n * @deprecated\n */\n public set(index: number, value: CharData): void {\n this._cacheValid = false;\n this._data[index * Constants.CELL_INDICIES + Cell.FG] = value[CHAR_DATA_ATTR_INDEX];\n if (value[CHAR_DATA_CHAR_INDEX].length > 1) {\n this._combined[index] = value[1];\n this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = index | Content.IS_COMBINED_MASK | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);\n } else {\n this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);\n }\n }\n\n /**\n * primitive getters\n * use these when only one value is needed, otherwise use `loadCell`\n */\n public getWidth(index: number): number {\n return this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] >> Content.WIDTH_SHIFT;\n }\n\n /** Test whether content has width. */\n public hasWidth(index: number): number {\n return this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] & Content.WIDTH_MASK;\n }\n\n /** Get FG cell component. */\n public getFg(index: number): number {\n return this._data[index * Constants.CELL_INDICIES + Cell.FG];\n }\n\n /** Get BG cell component. */\n public getBg(index: number): number {\n return this._data[index * Constants.CELL_INDICIES + Cell.BG];\n }\n\n /**\n * Test whether contains any chars.\n * Basically an empty has no content, but other cells might differ in FG/BG\n * from real empty cells.\n */\n public hasContent(index: number): number {\n return this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] & Content.HAS_CONTENT_MASK;\n }\n\n /**\n * Get codepoint of the cell.\n * To be in line with `code` in CharData this either returns\n * a single UTF32 codepoint or the last codepoint of a combined string.\n */\n public getCodePoint(index: number): number {\n const content = this._data[index * Constants.CELL_INDICIES + Cell.CONTENT];\n if (content & Content.IS_COMBINED_MASK) {\n return this._combined[index].charCodeAt(this._combined[index].length - 1);\n }\n return content & Content.CODEPOINT_MASK;\n }\n\n /** Test whether the cell contains a combined string. */\n public isCombined(index: number): number {\n return this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] & Content.IS_COMBINED_MASK;\n }\n\n /** Returns the string content of the cell. */\n public getString(index: number): string {\n const content = this._data[index * Constants.CELL_INDICIES + Cell.CONTENT];\n if (content & Content.IS_COMBINED_MASK) {\n return this._combined[index];\n }\n if (content & Content.CODEPOINT_MASK) {\n return stringFromCodePoint(content & Content.CODEPOINT_MASK);\n }\n // return empty string for empty cells\n return '';\n }\n\n /** Get state of protected flag. */\n public isProtected(index: number): number {\n return this._data[index * Constants.CELL_INDICIES + Cell.BG] & BgFlags.PROTECTED;\n }\n\n /**\n * Load data at `index` into `cell`. This is used to access cells in a way that's more friendly\n * to GC as it significantly reduced the amount of new objects/references needed.\n */\n public loadCell(index: number, cell: ICellData): ICellData {\n $startIndex = index * Constants.CELL_INDICIES;\n cell.content = this._data[$startIndex + Cell.CONTENT];\n cell.fg = this._data[$startIndex + Cell.FG];\n cell.bg = this._data[$startIndex + Cell.BG];\n if (cell.content & Content.IS_COMBINED_MASK) {\n cell.combinedData = this._combined[index];\n } else {\n cell.combinedData = '';\n }\n if (cell.bg & BgFlags.HAS_EXTENDED) {\n cell.extended = this._extendedAttrs[index]!;\n } else {\n // Do not mutate cell.extended in place: it may still reference this line's map entry from a\n // prior loadCell into a reused CellData (e.g. $workCell during insert/delete).\n // We use $extended as blueprint and reset the internals\n // mimicking the ctor to avoid a new allocation.\n $extended._ext = 0;\n $extended._urlId = 0;\n cell.extended = $extended;\n }\n return cell;\n }\n\n /**\n * Set data at `index` to `cell`.\n */\n public setCell(index: number, cell: ICellData): void {\n this._cacheValid = false;\n if (cell.content & Content.IS_COMBINED_MASK) {\n this._combined[index] = cell.combinedData;\n }\n if (cell.bg & BgFlags.HAS_EXTENDED) {\n this._extendedAttrs[index] = cell.extended;\n }\n this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = cell.content;\n this._data[index * Constants.CELL_INDICIES + Cell.FG] = cell.fg;\n this._data[index * Constants.CELL_INDICIES + Cell.BG] = cell.bg;\n }\n\n /**\n * Set cell data from input handler.\n * Since the input handler see the incoming chars as UTF32 codepoints,\n * it gets an optimized access method.\n */\n public setCellFromCodepoint(index: number, codePoint: number, width: number, attrs: IAttributeData): void {\n this._cacheValid = false;\n if (attrs.bg & BgFlags.HAS_EXTENDED) {\n this._extendedAttrs[index] = attrs.extended;\n }\n const $idx = index * Constants.CELL_INDICIES;\n this._data[$idx + Cell.CONTENT] = codePoint | (width << Content.WIDTH_SHIFT);\n this._data[$idx + Cell.FG] = attrs.fg;\n this._data[$idx + Cell.BG] = attrs.bg;\n }\n\n /**\n * Add a codepoint to a cell from input handler.\n * During input stage combining chars with a width of 0 follow and stack\n * onto a leading char. Since we already set the attrs\n * by the previous `setDataFromCodePoint` call, we can omit it here.\n */\n public addCodepointToCell(index: number, codePoint: number, width: number): void {\n this._cacheValid = false;\n let content = this._data[index * Constants.CELL_INDICIES + Cell.CONTENT];\n if (content & Content.IS_COMBINED_MASK) {\n // we already have a combined string, simply add\n this._combined[index] += stringFromCodePoint(codePoint);\n } else {\n if (content & Content.CODEPOINT_MASK) {\n // normal case for combining chars:\n // - move current leading char + new one into combined string\n // - set combined flag\n this._combined[index] = stringFromCodePoint(content & Content.CODEPOINT_MASK) + stringFromCodePoint(codePoint);\n content &= ~Content.CODEPOINT_MASK; // set codepoint in buffer to 0\n content |= Content.IS_COMBINED_MASK;\n } else {\n // should not happen - we actually have no data in the cell yet\n // simply set the data in the cell buffer with a width of 1\n content = codePoint | (1 << Content.WIDTH_SHIFT);\n }\n }\n if (width) {\n content &= ~Content.WIDTH_MASK;\n content |= width << Content.WIDTH_SHIFT;\n }\n this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = content;\n }\n\n public insertCells(pos: number, n: number, fillCellData: ICellData): void {\n this._cacheValid = false;\n pos %= this.length;\n\n // handle fullwidth at pos: reset cell one to the left if pos is second cell of a wide char\n if (pos && this.getWidth(pos - 1) === 2) {\n this.setCellFromCodepoint(pos - 1, 0, 1, fillCellData);\n }\n\n if (n < this.length - pos) {\n for (let i = this.length - pos - n - 1; i >= 0; --i) {\n this.setCell(pos + n + i, this.loadCell(pos + i, $workCell));\n }\n for (let i = 0; i < n; ++i) {\n this.setCell(pos + i, fillCellData);\n }\n } else {\n for (let i = pos; i < this.length; ++i) {\n this.setCell(i, fillCellData);\n }\n }\n\n // handle fullwidth at line end: reset last cell if it is first cell of a wide char\n if (this.getWidth(this.length - 1) === 2) {\n this.setCellFromCodepoint(this.length - 1, 0, 1, fillCellData);\n }\n }\n\n public deleteCells(pos: number, n: number, fillCellData: ICellData): void {\n this._cacheValid = false;\n pos %= this.length;\n if (n < this.length - pos) {\n for (let i = 0; i < this.length - pos - n; ++i) {\n this.setCell(pos + i, this.loadCell(pos + n + i, $workCell));\n }\n for (let i = this.length - n; i < this.length; ++i) {\n this.setCell(i, fillCellData);\n }\n } else {\n for (let i = pos; i < this.length; ++i) {\n this.setCell(i, fillCellData);\n }\n }\n\n // handle fullwidth at pos:\n // - reset pos-1 if wide char\n // - reset pos if width==0 (previous second cell of a wide char)\n if (pos && this.getWidth(pos - 1) === 2) {\n this.setCellFromCodepoint(pos - 1, 0, 1, fillCellData);\n }\n if (this.getWidth(pos) === 0 && !this.hasContent(pos)) {\n this.setCellFromCodepoint(pos, 0, 1, fillCellData);\n }\n }\n\n public replaceCells(start: number, end: number, fillCellData: ICellData, respectProtect: boolean = false): void {\n this._cacheValid = false;\n // full branching on respectProtect==true, hopefully getting fast JIT for standard case\n if (respectProtect) {\n if (start && this.getWidth(start - 1) === 2 && !this.isProtected(start - 1)) {\n this.setCellFromCodepoint(start - 1, 0, 1, fillCellData);\n }\n if (end < this.length && this.getWidth(end - 1) === 2 && !this.isProtected(end)) {\n this.setCellFromCodepoint(end, 0, 1, fillCellData);\n }\n while (start < end && start < this.length) {\n if (!this.isProtected(start)) {\n this.setCell(start, fillCellData);\n }\n start++;\n }\n return;\n }\n\n // handle fullwidth at start: reset cell one to the left if start is second cell of a wide char\n if (start && this.getWidth(start - 1) === 2) {\n this.setCellFromCodepoint(start - 1, 0, 1, fillCellData);\n }\n // handle fullwidth at last cell + 1: reset to empty cell if it is second part of a wide char\n if (end < this.length && this.getWidth(end - 1) === 2) {\n this.setCellFromCodepoint(end, 0, 1, fillCellData);\n }\n\n while (start < end && start < this.length) {\n this.setCell(start++, fillCellData);\n }\n }\n\n /**\n * Resize BufferLine to `cols` filling excess cells with `fillCellData`.\n * The underlying array buffer will not change if there is still enough space\n * to hold the new buffer line data.\n * Returns a boolean indicating, whether a `cleanupMemory` call would free\n * excess memory (true after shrinking > Constants.CLEANUP_THRESHOLD).\n */\n public resize(cols: number, fillCellData: ICellData): boolean {\n this._cacheValid = false;\n if (cols === this.length) {\n return this._data.length * 4 * Constants.CLEANUP_THRESHOLD < this._data.buffer.byteLength;\n }\n const uint32Cells = cols * Constants.CELL_INDICIES;\n if (cols > this.length) {\n if (this._data.buffer.byteLength >= uint32Cells * 4) {\n // optimization: avoid alloc and data copy if buffer has enough room\n this._data = new Uint32Array(this._data.buffer, 0, uint32Cells);\n } else {\n // slow path: new alloc and full data copy\n const data = new Uint32Array(uint32Cells);\n data.set(this._data);\n this._data = data;\n }\n for (let i = this.length; i < cols; ++i) {\n this.setCell(i, fillCellData);\n }\n } else {\n // optimization: just shrink the view on existing buffer\n this._data = this._data.subarray(0, uint32Cells);\n // Remove any cut off combined data\n const keys = Object.keys(this._combined);\n for (let i = 0; i < keys.length; i++) {\n const key = parseInt(keys[i], 10);\n if (key >= cols) {\n delete this._combined[key];\n }\n }\n // remove any cut off extended attributes\n const extKeys = Object.keys(this._extendedAttrs);\n for (let i = 0; i < extKeys.length; i++) {\n const key = parseInt(extKeys[i], 10);\n if (key >= cols) {\n delete this._extendedAttrs[key];\n }\n }\n }\n this.length = cols;\n return uint32Cells * 4 * Constants.CLEANUP_THRESHOLD < this._data.buffer.byteLength;\n }\n\n /**\n * Cleanup underlying array buffer.\n * A cleanup will be triggered if the array buffer exceeds the actual used\n * memory by a factor of Constants.CLEANUP_THRESHOLD.\n * Returns 0 or 1 indicating whether a cleanup happened.\n */\n public cleanupMemory(): number {\n if (this._data.length * 4 * Constants.CLEANUP_THRESHOLD < this._data.buffer.byteLength) {\n const data = new Uint32Array(this._data.length);\n data.set(this._data);\n this._data = data;\n return 1;\n }\n return 0;\n }\n\n /** fill a line with fillCharData */\n public fill(fillCellData: ICellData, respectProtect: boolean = false): void {\n this._cacheValid = false;\n // full branching on respectProtect==true, hopefully getting fast JIT for standard case\n if (respectProtect) {\n for (let i = 0; i < this.length; ++i) {\n if (!this.isProtected(i)) {\n this.setCell(i, fillCellData);\n }\n }\n return;\n }\n this._combined = {};\n this._extendedAttrs = {};\n for (let i = 0; i < this.length; ++i) {\n this.setCell(i, fillCellData);\n }\n }\n\n /** alter to a full copy of line */\n public copyFrom(line: BufferLine, blank?: boolean): void {\n if (this.length !== line.length) {\n this._data = new Uint32Array(line._data);\n } else {\n // use high speed copy if lengths are equal\n this._data.set(line._data);\n }\n this.length = line.length;\n if (blank) {\n // a blank line may never hold combined or extended attrs,\n // thus we can skip handling them\n this._combined = {};\n this._extendedAttrs = {};\n } else {\n this._copySparseMapsFrom(line);\n }\n this._cache = '';\n this._cacheValid = false;\n this.isWrapped = line.isWrapped;\n }\n\n /** create a new clone */\n public clone(blank?: boolean): IBufferLine {\n const newLine = new BufferLine(0, undefined, false);\n newLine._data = new Uint32Array(this._data);\n newLine.length = this.length;\n if (!blank) {\n // a blank line may never hold combined or extended attrs,\n // thus we can skip handling them\n newLine._copySparseMapsFrom(this);\n }\n newLine.isWrapped = this.isWrapped;\n return newLine;\n }\n\n public getTrimmedLength(): number {\n for (let i = this.length - 1; i >= 0; --i) {\n if ((this._data[i * Constants.CELL_INDICIES + Cell.CONTENT] & Content.HAS_CONTENT_MASK)) {\n return i + (this._data[i * Constants.CELL_INDICIES + Cell.CONTENT] >> Content.WIDTH_SHIFT);\n }\n }\n return 0;\n }\n\n public getNoBgTrimmedLength(): number {\n for (let i = this.length - 1; i >= 0; --i) {\n if ((this._data[i * Constants.CELL_INDICIES + Cell.CONTENT] & Content.HAS_CONTENT_MASK) || (this._data[i * Constants.CELL_INDICIES + Cell.BG] & Attributes.CM_MASK)) {\n return i + (this._data[i * Constants.CELL_INDICIES + Cell.CONTENT] >> Content.WIDTH_SHIFT);\n }\n }\n return 0;\n }\n\n public copyCellsFrom(src: BufferLine, srcCol: number, destCol: number, length: number, applyInReverse: boolean): void {\n this._cacheValid = false;\n const srcData = src._data;\n if (applyInReverse) {\n for (let cell = length - 1; cell >= 0; cell--) {\n for (let i = 0; i < Constants.CELL_INDICIES; i++) {\n this._data[(destCol + cell) * Constants.CELL_INDICIES + i] = srcData[(srcCol + cell) * Constants.CELL_INDICIES + i];\n }\n this._copyCellMapsFrom(src, srcCol + cell, destCol + cell);\n }\n } else {\n for (let cell = 0; cell < length; cell++) {\n for (let i = 0; i < Constants.CELL_INDICIES; i++) {\n this._data[(destCol + cell) * Constants.CELL_INDICIES + i] = srcData[(srcCol + cell) * Constants.CELL_INDICIES + i];\n }\n this._copyCellMapsFrom(src, srcCol + cell, destCol + cell);\n }\n }\n }\n\n /**\n * Translates the buffer line to a string. Caching only applies to canonical full-line translation\n * requests (regardless of `trimRight` value).\n *\n * @param trimRight Whether to trim any empty cells on the right.\n * @param startCol The column to start the string (0-based inclusive).\n * @param endCol The column to end the string (0-based exclusive).\n * @param outColumns if specified, this array will be filled with column numbers such that\n * `returnedString[i]` is displayed at `outColumns[i]` column. `outColumns[returnedString.length]`\n * is where the character following `returnedString` will be displayed.\n *\n * When a single cell is translated to multiple UTF-16 code units (e.g. surrogate pair) in the\n * returned string, the corresponding entries in `outColumns` will have the same column number.\n */\n public translateToString(trimRight?: boolean, startCol?: number, endCol?: number, outColumns?: number[]): string {\n const isCanonical = (startCol === undefined || startCol === 0) && endCol === undefined && outColumns === undefined;\n if (isCanonical && this._cacheValid) {\n if (trimRight) {\n return this._cacheTrimmed ? this._cache : this._cache.trimEnd();\n }\n if (!this._cacheTrimmed) {\n return this._cache;\n }\n }\n startCol = startCol ?? 0;\n endCol = endCol ?? this.length;\n if (trimRight) {\n endCol = Math.min(endCol, this.getTrimmedLength());\n }\n if (outColumns) {\n outColumns.length = 0;\n }\n const cellContents: string[] = [];\n while (startCol < endCol) {\n const content = this._data[startCol * Constants.CELL_INDICIES + Cell.CONTENT];\n const cp = content & Content.CODEPOINT_MASK;\n const chars = (content & Content.IS_COMBINED_MASK) ? this._combined[startCol] : (cp) ? stringFromCodePoint(cp) : WHITESPACE_CELL_CHAR;\n cellContents.push(chars);\n if (outColumns) {\n for (let i = 0; i < chars.length; ++i) {\n outColumns.push(startCol);\n }\n }\n startCol += (content >> Content.WIDTH_SHIFT) || 1; // always advance by at least 1\n }\n if (outColumns) {\n outColumns.push(startCol);\n }\n const result = cellContents.join('');\n if (isCanonical) {\n this._cache = result;\n this._cacheValid = true;\n this._cacheTrimmed = !!trimRight;\n }\n return result;\n }\n\n /** Copy sparse map entries for a single cell when `_data` flags require them. */\n private _copyCellMapsFrom(src: BufferLine, srcCol: number, destCol: number): void {\n const srcStart = srcCol * Constants.CELL_INDICIES;\n if (src._data[srcStart + Cell.CONTENT] & Content.IS_COMBINED_MASK) {\n this._combined[destCol] = src._combined[srcCol];\n }\n if (src._data[srcStart + Cell.BG] & BgFlags.HAS_EXTENDED) {\n this._extendedAttrs[destCol] = src._extendedAttrs[srcCol];\n }\n }\n\n /** Rebuild sparse maps from another line, keyed only by `_data` flags. */\n private _copySparseMapsFrom(line: BufferLine): void {\n this._combined = {};\n this._extendedAttrs = {};\n for (let i = 0; i < line.length; i++) {\n this._copyCellMapsFrom(line, i, i);\n }\n }\n}\n","/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { BufferLine } from './BufferLine';\nimport { CircularList } from '../CircularList';\nimport { IBufferLine, ICellData } from './Types';\n\nexport interface INewLayoutResult {\n layout: number[];\n countRemoved: number;\n}\n\n/**\n * Evaluates and returns indexes to be removed after a reflow larger occurs. Lines will be removed\n * when a wrapped line unwraps.\n * @param lines The buffer lines.\n * @param oldCols The columns before resize\n * @param newCols The columns after resize.\n * @param bufferAbsoluteY The absolute y position of the cursor (baseY + cursorY).\n * @param nullCell The cell data to use when filling in empty cells.\n * @param reflowCursorLine Whether to reflow the line containing the cursor.\n */\nexport function reflowLargerGetLinesToRemove(lines: CircularList, oldCols: number, newCols: number, bufferAbsoluteY: number, nullCell: ICellData, reflowCursorLine: boolean): number[] {\n // Gather all BufferLines that need to be removed from the Buffer here so that they can be\n // batched up and only committed once\n const toRemove: number[] = [];\n\n for (let y = 0; y < lines.length - 1; y++) {\n // Check if this row is wrapped\n let i = y;\n let nextLine = lines.get(++i) as BufferLine;\n if (!nextLine.isWrapped) {\n continue;\n }\n\n // Check how many lines it's wrapped for\n const wrappedLines: BufferLine[] = [lines.get(y) as BufferLine];\n while (i < lines.length && nextLine.isWrapped) {\n wrappedLines.push(nextLine);\n nextLine = lines.get(++i) as BufferLine;\n }\n\n if (!reflowCursorLine) {\n // If these lines contain the cursor don't touch them, the program will handle fixing up\n // wrapped lines with the cursor\n if (bufferAbsoluteY >= y && bufferAbsoluteY < i) {\n y += wrappedLines.length - 1;\n continue;\n }\n }\n\n // Copy buffer data to new locations\n let destLineIndex = 0;\n let destCol = getWrappedLineTrimmedLength(wrappedLines, destLineIndex, oldCols);\n let srcLineIndex = 1;\n let srcCol = 0;\n while (srcLineIndex < wrappedLines.length) {\n const srcTrimmedTineLength = getWrappedLineTrimmedLength(wrappedLines, srcLineIndex, oldCols);\n const srcRemainingCells = srcTrimmedTineLength - srcCol;\n const destRemainingCells = newCols - destCol;\n const cellsToCopy = Math.min(srcRemainingCells, destRemainingCells);\n\n wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol, destCol, cellsToCopy, false);\n\n destCol += cellsToCopy;\n if (destCol === newCols) {\n destLineIndex++;\n destCol = 0;\n }\n srcCol += cellsToCopy;\n if (srcCol === srcTrimmedTineLength) {\n srcLineIndex++;\n srcCol = 0;\n }\n\n // Make sure the last cell isn't wide, if it is copy it to the current dest\n if (destCol === 0 && destLineIndex !== 0) {\n if (wrappedLines[destLineIndex - 1].getWidth(newCols - 1) === 2) {\n wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[destLineIndex - 1], newCols - 1, destCol++, 1, false);\n // Null out the end of the last row\n wrappedLines[destLineIndex - 1].setCell(newCols - 1, nullCell);\n }\n }\n }\n\n // Clear out remaining cells or fragments could remain;\n wrappedLines[destLineIndex].replaceCells(destCol, newCols, nullCell);\n\n // Work backwards and remove any rows at the end that only contain null cells\n let countToRemove = 0;\n for (let i = wrappedLines.length - 1; i > 0; i--) {\n if (i > destLineIndex || wrappedLines[i].getTrimmedLength() === 0) {\n countToRemove++;\n } else {\n break;\n }\n }\n\n if (countToRemove > 0) {\n toRemove.push(y + wrappedLines.length - countToRemove); // index\n toRemove.push(countToRemove);\n }\n\n y += wrappedLines.length - 1;\n }\n return toRemove;\n}\n\n/**\n * Creates and return the new layout for lines given an array of indexes to be removed.\n * @param lines The buffer lines.\n * @param toRemove The indexes to remove.\n */\nexport function reflowLargerCreateNewLayout(lines: CircularList, toRemove: number[]): INewLayoutResult {\n const layout: number[] = [];\n // First iterate through the list and get the actual indexes to use for rows\n let nextToRemoveIndex = 0;\n let nextToRemoveStart = toRemove[nextToRemoveIndex];\n let countRemovedSoFar = 0;\n for (let i = 0; i < lines.length; i++) {\n if (nextToRemoveStart === i) {\n const countToRemove = toRemove[++nextToRemoveIndex];\n\n // Tell markers that there was a deletion\n lines.onDeleteEmitter.fire({\n index: i - countRemovedSoFar,\n amount: countToRemove\n });\n\n i += countToRemove - 1;\n countRemovedSoFar += countToRemove;\n nextToRemoveStart = toRemove[++nextToRemoveIndex];\n } else {\n layout.push(i);\n }\n }\n return {\n layout,\n countRemoved: countRemovedSoFar\n };\n}\n\n/**\n * Applies a new layout to the buffer. This essentially does the same as many splice calls but it's\n * done all at once in a single iteration through the list since splice is very expensive.\n * @param lines The buffer lines.\n * @param newLayout The new layout to apply.\n */\nexport function reflowLargerApplyNewLayout(lines: CircularList, newLayout: number[]): void {\n // Record original lines so they don't get overridden when we rearrange the list\n const newLayoutLines: BufferLine[] = [];\n for (let i = 0; i < newLayout.length; i++) {\n newLayoutLines.push(lines.get(newLayout[i]) as BufferLine);\n }\n\n // Rearrange the list\n for (let i = 0; i < newLayoutLines.length; i++) {\n lines.set(i, newLayoutLines[i]);\n }\n lines.length = newLayout.length;\n}\n\n/**\n * Gets the new line lengths for a given wrapped line. The purpose of this function it to pre-\n * compute the wrapping points since wide characters may need to be wrapped onto the following line.\n * This function will return an array of numbers of where each line wraps to, the resulting array\n * will only contain the values `newCols` (when the line does not end with a wide character) and\n * `newCols - 1` (when the line does end with a wide character), except for the last value which\n * will contain the remaining items to fill the line.\n *\n * Calling this with a `newCols` value of `1` will lock up.\n *\n * @param wrappedLines The wrapped lines to evaluate.\n * @param oldCols The columns before resize.\n * @param newCols The columns after resize.\n */\nexport function reflowSmallerGetNewLineLengths(wrappedLines: BufferLine[], oldCols: number, newCols: number): number[] {\n const newLineLengths: number[] = [];\n let cellsNeeded = 0;\n for (let i = 0; i < wrappedLines.length; i++) {\n cellsNeeded += getWrappedLineTrimmedLength(wrappedLines, i, oldCols);\n }\n\n // Use srcCol and srcLine to find the new wrapping point, use that to get the cellsAvailable and\n // linesNeeded\n let srcCol = 0;\n let srcLine = 0;\n let cellsAvailable = 0;\n while (cellsAvailable < cellsNeeded) {\n if (cellsNeeded - cellsAvailable < newCols) {\n // Add the final line and exit the loop\n newLineLengths.push(cellsNeeded - cellsAvailable);\n break;\n }\n srcCol += newCols;\n const oldTrimmedLength = getWrappedLineTrimmedLength(wrappedLines, srcLine, oldCols);\n if (srcCol > oldTrimmedLength) {\n srcCol -= oldTrimmedLength;\n srcLine++;\n }\n const endsWithWide = wrappedLines[srcLine].getWidth(srcCol - 1) === 2;\n if (endsWithWide) {\n srcCol--;\n }\n const lineLength = endsWithWide ? newCols - 1 : newCols;\n newLineLengths.push(lineLength);\n cellsAvailable += lineLength;\n }\n\n return newLineLengths;\n}\n\nexport function getWrappedLineTrimmedLength(lines: BufferLine[], i: number, cols: number): number {\n // If this is the last row in the wrapped line, get the actual trimmed length\n if (i === lines.length - 1) {\n return lines[i].getTrimmedLength();\n }\n // Detect whether the following line starts with a wide character and the end of the current line\n // is null, if so then we can be pretty sure the null character should be excluded from the line\n // length]\n const endsInNull = !(lines[i].hasContent(cols - 1)) && lines[i].getWidth(cols - 1) === 1;\n const followingLineStartsWithWide = lines[i + 1].getWidth(0) === 2;\n if (endsInNull && followingLineStartsWithWide) {\n return cols - 1;\n }\n return cols;\n}\n","/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { Disposable, MutableDisposable } from '../Lifecycle';\nimport { Buffer } from './Buffer';\nimport { IAttributeData, IBuffer, IBufferSet } from './Types';\nimport { IBufferService, ILogService, IOptionsService } from '../services/Services';\nimport { Emitter } from '../Event';\n\n/**\n * The BufferSet represents the set of two buffers used by xterm terminals (normal and alt) and\n * provides also utilities for working with them.\n */\nexport class BufferSet extends Disposable implements IBufferSet {\n private _normal!: Buffer;\n private _alt!: Buffer;\n private _activeBuffer!: Buffer;\n private readonly _normalBuffer = this._register(new MutableDisposable());\n private readonly _altBuffer = this._register(new MutableDisposable());\n\n private readonly _onBufferActivate = this._register(new Emitter<{ activeBuffer: IBuffer, inactiveBuffer: IBuffer }>());\n public readonly onBufferActivate = this._onBufferActivate.event;\n\n /**\n * Create a new BufferSet for the given terminal.\n */\n constructor(\n private readonly _optionsService: IOptionsService,\n private readonly _bufferService: IBufferService,\n private readonly _logService: ILogService\n ) {\n super();\n this.reset();\n this._register(this._optionsService.onSpecificOptionChange('scrollback', () => this.resize(this._bufferService.cols, this._bufferService.rows)));\n this._register(this._optionsService.onSpecificOptionChange('tabStopWidth', () => this.setupTabStops()));\n }\n\n public reset(): void {\n this._normal = new Buffer(true, this._optionsService, this._bufferService, this._logService);\n this._normalBuffer.value = this._normal;\n this._normal.fillViewportRows();\n\n // The alt buffer should never have scrollback.\n // See http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-The-Alternate-Screen-Buffer\n this._alt = new Buffer(false, this._optionsService, this._bufferService, this._logService);\n this._altBuffer.value = this._alt;\n this._activeBuffer = this._normal;\n this._onBufferActivate.fire({\n activeBuffer: this._normal,\n inactiveBuffer: this._alt\n });\n\n this.setupTabStops();\n }\n\n /**\n * Returns the alt Buffer of the BufferSet\n */\n public get alt(): Buffer {\n return this._alt;\n }\n\n /**\n * Returns the currently active Buffer of the BufferSet\n */\n public get active(): Buffer {\n return this._activeBuffer;\n }\n\n /**\n * Returns the normal Buffer of the BufferSet\n */\n public get normal(): Buffer {\n return this._normal;\n }\n\n /**\n * Sets the normal Buffer of the BufferSet as its currently active Buffer\n */\n public activateNormalBuffer(): void {\n if (this._activeBuffer === this._normal) {\n return;\n }\n this._normal.x = this._alt.x;\n this._normal.y = this._alt.y;\n // The alt buffer should always be cleared when we switch to the normal\n // buffer. This frees up memory since the alt buffer should always be new\n // when activated.\n this._alt.clearAllMarkers();\n this._alt.clear();\n this._activeBuffer = this._normal;\n this._onBufferActivate.fire({\n activeBuffer: this._normal,\n inactiveBuffer: this._alt\n });\n }\n\n /**\n * Sets the alt Buffer of the BufferSet as its currently active Buffer\n */\n public activateAltBuffer(fillAttr?: IAttributeData): void {\n if (this._activeBuffer === this._alt) {\n return;\n }\n // Since the alt buffer is always cleared when the normal buffer is\n // activated, we want to fill it when switching to it.\n this._alt.fillViewportRows(fillAttr);\n this._alt.x = this._normal.x;\n this._alt.y = this._normal.y;\n this._activeBuffer = this._alt;\n this._onBufferActivate.fire({\n activeBuffer: this._alt,\n inactiveBuffer: this._normal\n });\n }\n\n /**\n * Resizes both normal and alt buffers, adjusting their data accordingly.\n * @param newCols The new number of columns.\n * @param newRows The new number of rows.\n */\n public resize(newCols: number, newRows: number): void {\n this._normal.resize(newCols, newRows);\n this._alt.resize(newCols, newRows);\n this.setupTabStops(newCols);\n }\n\n /**\n * Setup the tab stops.\n * @param i The index to start setting up tab stops from.\n */\n public setupTabStops(i?: number): void {\n this._normal.setupTabStops(i);\n this._alt.setupTabStops(i);\n }\n}\n","/**\n * Copyright (c) 2018 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { CharData, ICellData, IExtendedAttrs } from './Types';\nimport { stringFromCodePoint } from '../input/TextDecoder';\nimport { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_ATTR_INDEX, Content } from './Constants';\nimport { AttributeData, ExtendedAttrs } from './AttributeData';\nimport type { IBufferCell as IBufferCellApi } from '@xterm/xterm';\n\n/**\n * CellData - represents a single Cell in the terminal buffer.\n */\nexport class CellData extends AttributeData implements ICellData {\n /** Helper to create CellData from CharData. */\n public static fromCharData(value: CharData): CellData {\n const obj = new CellData();\n obj.setFromCharData(value);\n return obj;\n }\n /** Primitives from terminal buffer. */\n public content = 0;\n public fg = 0;\n public bg = 0;\n public extended: IExtendedAttrs = new ExtendedAttrs();\n public combinedData = '';\n /** Whether cell contains a combined string. */\n public isCombined(): number {\n return this.content & Content.IS_COMBINED_MASK;\n }\n /** Width of the cell. */\n public getWidth(): number {\n return this.content >> Content.WIDTH_SHIFT;\n }\n /** JS string of the content. */\n public getChars(): string {\n if (this.content & Content.IS_COMBINED_MASK) {\n return this.combinedData;\n }\n if (this.content & Content.CODEPOINT_MASK) {\n return stringFromCodePoint(this.content & Content.CODEPOINT_MASK);\n }\n return '';\n }\n /**\n * Codepoint of cell\n * Note this returns the UTF32 codepoint of single chars,\n * if content is a combined string it returns the codepoint\n * of the last char in string to be in line with code in CharData.\n */\n public getCode(): number {\n return (this.isCombined())\n ? this.combinedData.charCodeAt(this.combinedData.length - 1)\n : this.content & Content.CODEPOINT_MASK;\n }\n /** Set data from CharData */\n public setFromCharData(value: CharData): void {\n this.fg = value[CHAR_DATA_ATTR_INDEX];\n this.bg = 0;\n let combined = false;\n // surrogates and combined strings need special treatment\n if (value[CHAR_DATA_CHAR_INDEX].length > 2) {\n combined = true;\n }\n else if (value[CHAR_DATA_CHAR_INDEX].length === 2) {\n const code = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0);\n // if the 2-char string is a surrogate create single codepoint\n // everything else is combined\n if (0xD800 <= code && code <= 0xDBFF) {\n const second = value[CHAR_DATA_CHAR_INDEX].charCodeAt(1);\n if (0xDC00 <= second && second <= 0xDFFF) {\n this.content = ((code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);\n }\n else {\n combined = true;\n }\n }\n else {\n combined = true;\n }\n }\n else {\n this.content = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);\n }\n if (combined) {\n this.combinedData = value[CHAR_DATA_CHAR_INDEX];\n this.content = Content.IS_COMBINED_MASK | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);\n }\n }\n /** Get data as CharData. */\n public getAsCharData(): CharData {\n return [this.fg, this.getChars(), this.getWidth(), this.getCode()];\n }\n\n public attributesEquals(other: IBufferCellApi): boolean {\n if (this.getFgColorMode() !== other.getFgColorMode() || this.getFgColor() !== other.getFgColor()) {\n return false;\n }\n if (this.getBgColorMode() !== other.getBgColorMode() || this.getBgColor() !== other.getBgColor()) {\n return false;\n }\n if (this.isInverse() !== other.isInverse()) {\n return false;\n }\n if (this.isBold() !== other.isBold()) {\n return false;\n }\n if (this.isUnderline() !== other.isUnderline()) {\n return false;\n }\n if (this.isUnderline()) {\n if (this.getUnderlineStyle() !== other.getUnderlineStyle()) {\n return false;\n }\n const thisDefault = this.isUnderlineColorDefault();\n const otherDefault = other.isUnderlineColorDefault();\n if (!(thisDefault && otherDefault)) {\n if (thisDefault !== otherDefault) {\n return false;\n }\n if (this.getUnderlineColor() !== other.getUnderlineColor()) {\n return false;\n }\n if (this.getUnderlineColorMode() !== other.getUnderlineColorMode()) {\n return false;\n }\n }\n }\n if (this.isOverline() !== other.isOverline()) {\n return false;\n }\n if (this.isBlink() !== other.isBlink()) {\n return false;\n }\n if (this.isInvisible() !== other.isInvisible()) {\n return false;\n }\n if (this.isItalic() !== other.isItalic()) {\n return false;\n }\n if (this.isDim() !== other.isDim()) {\n return false;\n }\n if (this.isStrikethrough() !== other.isStrikethrough()) {\n return false;\n }\n return true;\n }\n\n}\n","/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nexport const DEFAULT_COLOR = 0;\nexport const DEFAULT_ATTR = (0 << 18) | (DEFAULT_COLOR << 9) | (256 << 0);\nexport const DEFAULT_EXT = 0;\n\nexport const CHAR_DATA_ATTR_INDEX = 0;\nexport const CHAR_DATA_CHAR_INDEX = 1;\nexport const CHAR_DATA_WIDTH_INDEX = 2;\nexport const CHAR_DATA_CODE_INDEX = 3;\n\n/**\n * Null cell - a real empty cell (containing nothing).\n * Note that code should always be 0 for a null cell as\n * several test condition of the buffer line rely on this.\n */\nexport const NULL_CELL_CHAR = '';\nexport const NULL_CELL_WIDTH = 1;\nexport const NULL_CELL_CODE = 0;\n\n/**\n * Whitespace cell.\n * This is meant as a replacement for empty cells when needed\n * during rendering lines to preserve correct alignment.\n */\nexport const WHITESPACE_CELL_CHAR = ' ';\nexport const WHITESPACE_CELL_WIDTH = 1;\nexport const WHITESPACE_CELL_CODE = 32;\n\n/**\n * Bitmasks for accessing data in `content`.\n */\nexport const enum Content {\n /**\n * bit 1..21 codepoint, max allowed in UTF32 is 0x10FFFF (21 bits taken)\n * read: `codepoint = content & Content.CODEPOINT_MASK;`\n * write: `content |= codepoint & Content.CODEPOINT_MASK;`\n * shortcut if precondition `codepoint <= 0x10FFFF` is met:\n * `content |= codepoint;`\n */\n CODEPOINT_MASK = 0x1FFFFF,\n\n /**\n * bit 22 flag indicating whether a cell contains combined content\n * read: `isCombined = content & Content.IS_COMBINED_MASK;`\n * set: `content |= Content.IS_COMBINED_MASK;`\n * clear: `content &= ~Content.IS_COMBINED_MASK;`\n */\n IS_COMBINED_MASK = 0x200000, // 1 << 21\n\n /**\n * bit 1..22 mask to check whether a cell contains any string data\n * we need to check for codepoint and isCombined bits to see\n * whether a cell contains anything\n * read: `isEmpty = !(content & Content.HAS_CONTENT_MASK)`\n */\n HAS_CONTENT_MASK = 0x3FFFFF,\n\n /**\n * bit 23..24 wcwidth value of cell, takes 2 bits (ranges from 0..2)\n * read: `width = (content & Content.WIDTH_MASK) >> Content.WIDTH_SHIFT;`\n * `hasWidth = content & Content.WIDTH_MASK;`\n * as long as wcwidth is highest value in `content`:\n * `width = content >> Content.WIDTH_SHIFT;`\n * write: `content |= (width << Content.WIDTH_SHIFT) & Content.WIDTH_MASK;`\n * shortcut if precondition `0 <= width <= 3` is met:\n * `content |= width << Content.WIDTH_SHIFT;`\n */\n WIDTH_MASK = 0xC00000, // 3 << 22\n WIDTH_SHIFT = 22\n}\n\nexport const enum Attributes {\n /**\n * bit 1..8 blue in RGB, color in P256 and P16\n */\n BLUE_MASK = 0xFF,\n BLUE_SHIFT = 0,\n PCOLOR_MASK = 0xFF,\n PCOLOR_SHIFT = 0,\n\n /**\n * bit 9..16 green in RGB\n */\n GREEN_MASK = 0xFF00,\n GREEN_SHIFT = 8,\n\n /**\n * bit 17..24 red in RGB\n */\n RED_MASK = 0xFF0000,\n RED_SHIFT = 16,\n\n /**\n * bit 25..26 color mode: DEFAULT (0) | P16 (1) | P256 (2) | RGB (3)\n */\n CM_MASK = 0x3000000,\n CM_DEFAULT = 0,\n CM_P16 = 0x1000000,\n CM_P256 = 0x2000000,\n CM_RGB = 0x3000000,\n\n /**\n * bit 1..24 RGB room\n */\n RGB_MASK = 0xFFFFFF\n}\n\nexport const enum FgFlags {\n /**\n * bit 27..32\n */\n INVERSE = 0x4000000,\n BOLD = 0x8000000,\n UNDERLINE = 0x10000000,\n BLINK = 0x20000000,\n INVISIBLE = 0x40000000,\n STRIKETHROUGH = 0x80000000,\n}\n\nexport const enum BgFlags {\n /**\n * bit 27..32 (upper 2 unused)\n */\n ITALIC = 0x4000000,\n DIM = 0x8000000,\n HAS_EXTENDED = 0x10000000,\n PROTECTED = 0x20000000,\n OVERLINE = 0x40000000\n}\n\nexport const enum ExtFlags {\n /**\n * bit 27..29\n */\n UNDERLINE_STYLE = 0x1C000000,\n\n /**\n * bit 30..32\n *\n * An optional variant for the glyph, this can be used for example to offset underlines by a\n * number of pixels to create a perfect pattern.\n */\n VARIANT_OFFSET = 0xE0000000\n}\n\nexport const enum UnderlineStyle {\n NONE = 0,\n SINGLE = 1,\n DOUBLE = 2,\n CURLY = 3,\n DOTTED = 4,\n DASHED = 5\n}\n","/**\n * Copyright (c) 2018 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { dispose, IDisposable } from '../Lifecycle';\nimport { IMarker } from './Types';\nimport { Emitter } from '../Event';\n\nexport class Marker implements IMarker {\n private static _nextId = 1;\n\n public isDisposed: boolean = false;\n private readonly _disposables: IDisposable[] = [];\n\n private readonly _id: number = Marker._nextId++;\n public get id(): number { return this._id; }\n\n private readonly _onDispose = this.register(new Emitter());\n public readonly onDispose = this._onDispose.event;\n\n constructor(\n public line: number\n ) {\n }\n\n public dispose(): void {\n if (this.isDisposed) {\n return;\n }\n this.isDisposed = true;\n this.line = -1;\n // Emit before super.dispose such that dispose listeners get a chance to react\n this._onDispose.fire();\n dispose(this._disposables);\n this._disposables.length = 0;\n }\n\n public register(disposable: T): T {\n this._disposables.push(disposable);\n return disposable;\n }\n}\n","/**\n * Copyright (c) 2016 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { ICharset } from '../Types';\n\n/**\n * The character sets supported by the terminal. These enable several languages\n * to be represented within the terminal with only 8-bit encoding. See ISO 2022\n * for a discussion on character sets. Only VT100 character sets are supported.\n */\nexport const CHARSETS: { [key: string]: ICharset | undefined } = {};\n\n/**\n * The default character set, US.\n */\nexport const DEFAULT_CHARSET: ICharset | undefined = CHARSETS['B'];\n\n/**\n * DEC Special Character and Line Drawing Set.\n * Reference: http://vt100.net/docs/vt102-ug/table5-13.html\n * A lot of curses apps use this if they see TERM=xterm.\n * testing: echo -e '\\e(0a\\e(B'\n * The xterm output sometimes seems to conflict with the\n * reference above. xterm seems in line with the reference\n * when running vttest however.\n * The table below now uses xterm's output from vttest.\n */\nCHARSETS['0'] = {\n '`': '\\u25c6', // '◆'\n 'a': '\\u2592', // '▒'\n 'b': '\\u2409', // '␉' (HT)\n 'c': '\\u240c', // '␌' (FF)\n 'd': '\\u240d', // '␍' (CR)\n 'e': '\\u240a', // '␊' (LF)\n 'f': '\\u00b0', // '°'\n 'g': '\\u00b1', // '±'\n 'h': '\\u2424', // '␤' (NL)\n 'i': '\\u240b', // '␋' (VT)\n 'j': '\\u2518', // '┘'\n 'k': '\\u2510', // '┐'\n 'l': '\\u250c', // '┌'\n 'm': '\\u2514', // '└'\n 'n': '\\u253c', // '┼'\n 'o': '\\u23ba', // '⎺'\n 'p': '\\u23bb', // '⎻'\n 'q': '\\u2500', // '─'\n 'r': '\\u23bc', // '⎼'\n 's': '\\u23bd', // '⎽'\n 't': '\\u251c', // '├'\n 'u': '\\u2524', // '┤'\n 'v': '\\u2534', // '┴'\n 'w': '\\u252c', // '┬'\n 'x': '\\u2502', // '│'\n 'y': '\\u2264', // '≤'\n 'z': '\\u2265', // '≥'\n '{': '\\u03c0', // 'π'\n '|': '\\u2260', // '≠'\n '}': '\\u00a3', // '£'\n '~': '\\u00b7' // '·'\n};\n\n/**\n * British character set\n * ESC (A\n * Reference: http://vt100.net/docs/vt220-rm/table2-5.html\n */\nCHARSETS['A'] = {\n '#': '£'\n};\n\n/**\n * United States character set\n * ESC (B\n */\nCHARSETS['B'] = undefined;\n\n/**\n * Dutch character set\n * ESC (4\n * Reference: http://vt100.net/docs/vt220-rm/table2-6.html\n */\nCHARSETS['4'] = {\n '#': '£',\n '@': '¾',\n '[': 'ij',\n '\\\\': '½',\n ']': '|',\n '{': '¨',\n '|': 'f',\n '}': '¼',\n '~': '´'\n};\n\n/**\n * Finnish character set\n * ESC (C or ESC (5\n * Reference: http://vt100.net/docs/vt220-rm/table2-7.html\n */\nCHARSETS['C'] = CHARSETS['5'] = {\n '[': 'Ä',\n '\\\\': 'Ö',\n ']': 'Å',\n '^': 'Ü',\n '`': 'é',\n '{': 'ä',\n '|': 'ö',\n '}': 'å',\n '~': 'ü'\n};\n\n/**\n * French character set\n * ESC (R\n * Reference: http://vt100.net/docs/vt220-rm/table2-8.html\n */\nCHARSETS['R'] = {\n '#': '£',\n '@': 'à',\n '[': '°',\n '\\\\': 'ç',\n ']': '§',\n '{': 'é',\n '|': 'ù',\n '}': 'è',\n '~': '¨'\n};\n\n/**\n * French Canadian character set\n * ESC (Q\n * Reference: http://vt100.net/docs/vt220-rm/table2-9.html\n */\nCHARSETS['Q'] = {\n '@': 'à',\n '[': 'â',\n '\\\\': 'ç',\n ']': 'ê',\n '^': 'î',\n '`': 'ô',\n '{': 'é',\n '|': 'ù',\n '}': 'è',\n '~': 'û'\n};\n\n/**\n * German character set\n * ESC (K\n * Reference: http://vt100.net/docs/vt220-rm/table2-10.html\n */\nCHARSETS['K'] = {\n '@': '§',\n '[': 'Ä',\n '\\\\': 'Ö',\n ']': 'Ü',\n '{': 'ä',\n '|': 'ö',\n '}': 'ü',\n '~': 'ß'\n};\n\n/**\n * Italian character set\n * ESC (Y\n * Reference: http://vt100.net/docs/vt220-rm/table2-11.html\n */\nCHARSETS['Y'] = {\n '#': '£',\n '@': '§',\n '[': '°',\n '\\\\': 'ç',\n ']': 'é',\n '`': 'ù',\n '{': 'à',\n '|': 'ò',\n '}': 'è',\n '~': 'ì'\n};\n\n/**\n * Norwegian/Danish character set\n * ESC (E or ESC (6\n * Reference: http://vt100.net/docs/vt220-rm/table2-12.html\n */\nCHARSETS['E'] = CHARSETS['6'] = {\n '@': 'Ä',\n '[': 'Æ',\n '\\\\': 'Ø',\n ']': 'Å',\n '^': 'Ü',\n '`': 'ä',\n '{': 'æ',\n '|': 'ø',\n '}': 'å',\n '~': 'ü'\n};\n\n/**\n * Spanish character set\n * ESC (Z\n * Reference: http://vt100.net/docs/vt220-rm/table2-13.html\n */\nCHARSETS['Z'] = {\n '#': '£',\n '@': '§',\n '[': '¡',\n '\\\\': 'Ñ',\n ']': '¿',\n '{': '°',\n '|': 'ñ',\n '}': 'ç'\n};\n\n/**\n * Swedish character set\n * ESC (H or ESC (7\n * Reference: http://vt100.net/docs/vt220-rm/table2-14.html\n */\nCHARSETS['H'] = CHARSETS['7'] = {\n '@': 'É',\n '[': 'Ä',\n '\\\\': 'Ö',\n ']': 'Å',\n '^': 'Ü',\n '`': 'é',\n '{': 'ä',\n '|': 'ö',\n '}': 'å',\n '~': 'ü'\n};\n\n/**\n * Swiss character set\n * ESC (=\n * Reference: http://vt100.net/docs/vt220-rm/table2-15.html\n */\nCHARSETS['='] = {\n '#': 'ù',\n '@': 'à',\n '[': 'é',\n '\\\\': 'ç',\n ']': 'ê',\n '^': 'î',\n\n '_': 'è',\n '`': 'ô',\n '{': 'ä',\n '|': 'ö',\n '}': 'ü',\n '~': 'û'\n};\n","/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\n/**\n * Polyfill - Convert UTF32 codepoint into JS string.\n * Note: The built-in String.fromCodePoint happens to be much slower\n * due to additional sanity checks. We can avoid them since\n * we always operate on legal UTF32 (granted by the input decoders)\n * and use this faster version instead.\n */\nexport function stringFromCodePoint(codePoint: number): string {\n if (codePoint > 0xFFFF) {\n codePoint -= 0x10000;\n return String.fromCharCode((codePoint >> 10) + 0xD800) + String.fromCharCode((codePoint % 0x400) + 0xDC00);\n }\n return String.fromCharCode(codePoint);\n}\n\n/**\n * Convert UTF32 char codes into JS string.\n * Basically the same as `stringFromCodePoint` but for multiple codepoints\n * in a loop (which is a lot faster).\n */\nexport function utf32ToString(data: Uint32Array, start: number = 0, end: number = data.length): string {\n let result = '';\n for (let i = start; i < end; ++i) {\n let codepoint = data[i];\n if (codepoint > 0xFFFF) {\n // JS strings are encoded as UTF16, thus a non BMP codepoint gets converted into a surrogate\n // pair conversion rules:\n // - subtract 0x10000 from code point, leaving a 20 bit number\n // - add high 10 bits to 0xD800 --> first surrogate\n // - add low 10 bits to 0xDC00 --> second surrogate\n codepoint -= 0x10000;\n result += String.fromCharCode((codepoint >> 10) + 0xD800) + String.fromCharCode((codepoint % 0x400) + 0xDC00);\n } else {\n result += String.fromCharCode(codepoint);\n }\n }\n return result;\n}\n\n/**\n * StringToUtf32 - decodes UTF16 sequences into UTF32 codepoints.\n * To keep the decoder in line with JS strings it handles single surrogates as UCS2.\n */\nexport class StringToUtf32 {\n private _interim: number = 0;\n\n /**\n * Clears interim and resets decoder to clean state.\n */\n public clear(): void {\n this._interim = 0;\n }\n\n /**\n * Decode JS string to UTF32 codepoints.\n * The methods assumes stream input and will store partly transmitted\n * surrogate pairs and decode them with the next data chunk.\n * Note: The method does no bound checks for target, therefore make sure\n * the provided input data does not exceed the size of `target`.\n * Returns the number of written codepoints in `target`.\n */\n public decode(input: string, target: Uint32Array): number {\n const length = input.length;\n\n if (!length) {\n return 0;\n }\n\n let size = 0;\n let startPos = 0;\n\n // handle leftover surrogate high\n if (this._interim) {\n const second = input.charCodeAt(startPos++);\n if (0xDC00 <= second && second <= 0xDFFF) {\n target[size++] = (this._interim - 0xD800) * 0x400 + second - 0xDC00 + 0x10000;\n } else {\n // illegal codepoint (USC2 handling)\n target[size++] = this._interim;\n target[size++] = second;\n }\n this._interim = 0;\n }\n\n for (let i = startPos; i < length; ++i) {\n const code = input.charCodeAt(i);\n // surrogate pair first\n if (0xD800 <= code && code <= 0xDBFF) {\n if (++i >= length) {\n this._interim = code;\n return size;\n }\n const second = input.charCodeAt(i);\n if (0xDC00 <= second && second <= 0xDFFF) {\n target[size++] = (code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000;\n } else {\n // illegal codepoint (USC2 handling)\n target[size++] = code;\n target[size++] = second;\n }\n continue;\n }\n if (code === 0xFEFF) {\n // BOM\n continue;\n }\n target[size++] = code;\n }\n return size;\n }\n}\n\n/**\n * Utf8Decoder - decodes UTF8 byte sequences into UTF32 codepoints.\n */\nexport class Utf8ToUtf32 {\n public interim: Uint8Array = new Uint8Array(3);\n\n /**\n * Clears interim bytes and resets decoder to clean state.\n */\n public clear(): void {\n this.interim.fill(0);\n }\n\n /**\n * Decodes UTF8 byte sequences in `input` to UTF32 codepoints in `target`.\n * The methods assumes stream input and will store partly transmitted bytes\n * and decode them with the next data chunk.\n * Note: The method does no bound checks for target, therefore make sure\n * the provided data chunk does not exceed the size of `target`.\n * Returns the number of written codepoints in `target`.\n */\n public decode(input: Uint8Array, target: Uint32Array): number {\n const length = input.length;\n\n if (!length) {\n return 0;\n }\n\n let size = 0;\n let byte1: number;\n let byte2: number;\n let byte3: number;\n let byte4: number;\n let codepoint;\n let startPos = 0;\n\n // handle leftover bytes\n if (this.interim[0]) {\n let discardInterim = false;\n let cp = this.interim[0];\n cp &= ((((cp & 0xE0) === 0xC0)) ? 0x1F : (((cp & 0xF0) === 0xE0)) ? 0x0F : 0x07);\n let pos = 0;\n let tmp: number;\n while ((tmp = this.interim[++pos]) && pos < 4) {\n cp <<= 6;\n cp |= tmp & 0x3F;\n }\n // missing bytes - read ahead from input\n const type = (((this.interim[0] & 0xE0) === 0xC0)) ? 2 : (((this.interim[0] & 0xF0) === 0xE0)) ? 3 : 4;\n const missing = type - pos;\n while (startPos < missing) {\n if (startPos >= length) {\n return 0;\n }\n tmp = input[startPos++];\n if ((tmp & 0xC0) !== 0x80) {\n // wrong continuation, discard interim bytes completely\n startPos--;\n discardInterim = true;\n break;\n } else {\n // need to save so we can continue short inputs in next call\n this.interim[pos++] = tmp;\n cp <<= 6;\n cp |= tmp & 0x3F;\n }\n }\n if (!discardInterim) {\n // final test is type dependent\n if (type === 2) {\n if (cp < 0x80) {\n // wrong starter byte\n startPos--;\n } else {\n target[size++] = cp;\n }\n } else if (type === 3) {\n if (cp < 0x0800 || (cp >= 0xD800 && cp <= 0xDFFF) || cp === 0xFEFF) {\n // illegal codepoint or BOM\n } else {\n target[size++] = cp;\n }\n } else {\n if (cp < 0x010000 || cp > 0x10FFFF) {\n // illegal codepoint\n } else {\n target[size++] = cp;\n }\n }\n }\n this.interim.fill(0);\n }\n\n // loop through input\n const fourStop = length - 4;\n let i = startPos;\n while (i < length) {\n /**\n * ASCII shortcut with loop unrolled to 4 consecutive ASCII chars.\n * This is a compromise between speed gain for ASCII\n * and penalty for non ASCII:\n * For best ASCII performance the char should be stored directly into target,\n * but even a single attempt to write to target and compare afterwards\n * penalizes non ASCII really bad (-50%), thus we load the char into byteX first,\n * which reduces ASCII performance by ~15%.\n * This trial for ASCII reduces non ASCII performance by ~10% which seems acceptible\n * compared to the gains.\n * Note that this optimization only takes place for 4 consecutive ASCII chars,\n * for any shorter it bails out. Worst case - all 4 bytes being read but\n * thrown away due to the last being a non ASCII char (-10% performance).\n */\n while (i < fourStop\n && !((byte1 = input[i]) & 0x80)\n && !((byte2 = input[i + 1]) & 0x80)\n && !((byte3 = input[i + 2]) & 0x80)\n && !((byte4 = input[i + 3]) & 0x80))\n {\n target[size++] = byte1;\n target[size++] = byte2;\n target[size++] = byte3;\n target[size++] = byte4;\n i += 4;\n }\n\n // reread byte1\n byte1 = input[i++];\n\n // 1 byte\n if (byte1 < 0x80) {\n target[size++] = byte1;\n\n // 2 bytes\n } else if ((byte1 & 0xE0) === 0xC0) {\n if (i >= length) {\n this.interim[0] = byte1;\n return size;\n }\n byte2 = input[i++];\n if ((byte2 & 0xC0) !== 0x80) {\n // wrong continuation\n i--;\n continue;\n }\n codepoint = (byte1 & 0x1F) << 6 | (byte2 & 0x3F);\n if (codepoint < 0x80) {\n // wrong starter byte\n i--;\n continue;\n }\n target[size++] = codepoint;\n\n // 3 bytes\n } else if ((byte1 & 0xF0) === 0xE0) {\n if (i >= length) {\n this.interim[0] = byte1;\n return size;\n }\n byte2 = input[i++];\n if ((byte2 & 0xC0) !== 0x80) {\n // wrong continuation\n i--;\n continue;\n }\n if (i >= length) {\n this.interim[0] = byte1;\n this.interim[1] = byte2;\n return size;\n }\n byte3 = input[i++];\n if ((byte3 & 0xC0) !== 0x80) {\n // wrong continuation\n i--;\n continue;\n }\n codepoint = (byte1 & 0x0F) << 12 | (byte2 & 0x3F) << 6 | (byte3 & 0x3F);\n if (codepoint < 0x0800 || (codepoint >= 0xD800 && codepoint <= 0xDFFF) || codepoint === 0xFEFF) {\n // illegal codepoint or BOM, no i-- here\n continue;\n }\n target[size++] = codepoint;\n\n // 4 bytes\n } else if ((byte1 & 0xF8) === 0xF0) {\n if (i >= length) {\n this.interim[0] = byte1;\n return size;\n }\n byte2 = input[i++];\n if ((byte2 & 0xC0) !== 0x80) {\n // wrong continuation\n i--;\n continue;\n }\n if (i >= length) {\n this.interim[0] = byte1;\n this.interim[1] = byte2;\n return size;\n }\n byte3 = input[i++];\n if ((byte3 & 0xC0) !== 0x80) {\n // wrong continuation\n i--;\n continue;\n }\n if (i >= length) {\n this.interim[0] = byte1;\n this.interim[1] = byte2;\n this.interim[2] = byte3;\n return size;\n }\n byte4 = input[i++];\n if ((byte4 & 0xC0) !== 0x80) {\n // wrong continuation\n i--;\n continue;\n }\n codepoint = (byte1 & 0x07) << 18 | (byte2 & 0x3F) << 12 | (byte3 & 0x3F) << 6 | (byte4 & 0x3F);\n if (codepoint < 0x010000 || codepoint > 0x10FFFF) {\n // illegal codepoint, no i-- here\n continue;\n }\n target[size++] = codepoint;\n } else {\n // illegal byte, just skip\n }\n }\n return size;\n }\n}\n","/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\nimport { IUnicodeVersionProvider, UnicodeCharProperties, UnicodeCharWidth } from '../services/Services';\nimport { UnicodeService } from '../services/UnicodeService';\n\nconst BMP_COMBINING = [\n [0x0300, 0x036F], [0x0483, 0x0486], [0x0488, 0x0489],\n [0x0591, 0x05BD], [0x05BF, 0x05BF], [0x05C1, 0x05C2],\n [0x05C4, 0x05C5], [0x05C7, 0x05C7], [0x0600, 0x0603],\n [0x0610, 0x0615], [0x064B, 0x065E], [0x0670, 0x0670],\n [0x06D6, 0x06E4], [0x06E7, 0x06E8], [0x06EA, 0x06ED],\n [0x070F, 0x070F], [0x0711, 0x0711], [0x0730, 0x074A],\n [0x07A6, 0x07B0], [0x07EB, 0x07F3], [0x0901, 0x0902],\n [0x093C, 0x093C], [0x0941, 0x0948], [0x094D, 0x094D],\n [0x0951, 0x0954], [0x0962, 0x0963], [0x0981, 0x0981],\n [0x09BC, 0x09BC], [0x09C1, 0x09C4], [0x09CD, 0x09CD],\n [0x09E2, 0x09E3], [0x0A01, 0x0A02], [0x0A3C, 0x0A3C],\n [0x0A41, 0x0A42], [0x0A47, 0x0A48], [0x0A4B, 0x0A4D],\n [0x0A70, 0x0A71], [0x0A81, 0x0A82], [0x0ABC, 0x0ABC],\n [0x0AC1, 0x0AC5], [0x0AC7, 0x0AC8], [0x0ACD, 0x0ACD],\n [0x0AE2, 0x0AE3], [0x0B01, 0x0B01], [0x0B3C, 0x0B3C],\n [0x0B3F, 0x0B3F], [0x0B41, 0x0B43], [0x0B4D, 0x0B4D],\n [0x0B56, 0x0B56], [0x0B82, 0x0B82], [0x0BC0, 0x0BC0],\n [0x0BCD, 0x0BCD], [0x0C3E, 0x0C40], [0x0C46, 0x0C48],\n [0x0C4A, 0x0C4D], [0x0C55, 0x0C56], [0x0CBC, 0x0CBC],\n [0x0CBF, 0x0CBF], [0x0CC6, 0x0CC6], [0x0CCC, 0x0CCD],\n [0x0CE2, 0x0CE3], [0x0D41, 0x0D43], [0x0D4D, 0x0D4D],\n [0x0DCA, 0x0DCA], [0x0DD2, 0x0DD4], [0x0DD6, 0x0DD6],\n [0x0E31, 0x0E31], [0x0E34, 0x0E3A], [0x0E47, 0x0E4E],\n [0x0EB1, 0x0EB1], [0x0EB4, 0x0EB9], [0x0EBB, 0x0EBC],\n [0x0EC8, 0x0ECD], [0x0F18, 0x0F19], [0x0F35, 0x0F35],\n [0x0F37, 0x0F37], [0x0F39, 0x0F39], [0x0F71, 0x0F7E],\n [0x0F80, 0x0F84], [0x0F86, 0x0F87], [0x0F90, 0x0F97],\n [0x0F99, 0x0FBC], [0x0FC6, 0x0FC6], [0x102D, 0x1030],\n [0x1032, 0x1032], [0x1036, 0x1037], [0x1039, 0x1039],\n [0x1058, 0x1059], [0x1160, 0x11FF], [0x135F, 0x135F],\n [0x1712, 0x1714], [0x1732, 0x1734], [0x1752, 0x1753],\n [0x1772, 0x1773], [0x17B4, 0x17B5], [0x17B7, 0x17BD],\n [0x17C6, 0x17C6], [0x17C9, 0x17D3], [0x17DD, 0x17DD],\n [0x180B, 0x180D], [0x18A9, 0x18A9], [0x1920, 0x1922],\n [0x1927, 0x1928], [0x1932, 0x1932], [0x1939, 0x193B],\n [0x1A17, 0x1A18], [0x1B00, 0x1B03], [0x1B34, 0x1B34],\n [0x1B36, 0x1B3A], [0x1B3C, 0x1B3C], [0x1B42, 0x1B42],\n [0x1B6B, 0x1B73], [0x1DC0, 0x1DCA], [0x1DFE, 0x1DFF],\n [0x200B, 0x200F], [0x202A, 0x202E], [0x2060, 0x2063],\n [0x206A, 0x206F], [0x20D0, 0x20EF], [0x302A, 0x302F],\n [0x3099, 0x309A], [0xA806, 0xA806], [0xA80B, 0xA80B],\n [0xA825, 0xA826], [0xFB1E, 0xFB1E], [0xFE00, 0xFE0F],\n [0xFE20, 0xFE23], [0xFEFF, 0xFEFF], [0xFFF9, 0xFFFB]\n];\nconst HIGH_COMBINING = [\n [0x10A01, 0x10A03], [0x10A05, 0x10A06], [0x10A0C, 0x10A0F],\n [0x10A38, 0x10A3A], [0x10A3F, 0x10A3F], [0x1D167, 0x1D169],\n [0x1D173, 0x1D182], [0x1D185, 0x1D18B], [0x1D1AA, 0x1D1AD],\n [0x1D242, 0x1D244], [0xE0001, 0xE0001], [0xE0020, 0xE007F],\n [0xE0100, 0xE01EF]\n];\n\n// BMP lookup table, lazy initialized during first addon loading\nlet table: Uint8Array;\n\nfunction bisearch(ucs: number, data: number[][]): boolean {\n let min = 0;\n let max = data.length - 1;\n let mid;\n if (ucs < data[0][0] || ucs > data[max][1]) {\n return false;\n }\n while (max >= min) {\n mid = (min + max) >> 1;\n if (ucs > data[mid][1]) {\n min = mid + 1;\n } else if (ucs < data[mid][0]) {\n max = mid - 1;\n } else {\n return true;\n }\n }\n return false;\n}\n\nexport class UnicodeV6 implements IUnicodeVersionProvider {\n public readonly version = '6';\n\n constructor() {\n // init lookup table once\n if (!table) {\n table = new Uint8Array(65536);\n table.fill(1);\n table[0] = 0;\n // control chars\n table.fill(0, 1, 32);\n table.fill(0, 0x7f, 0xa0);\n\n // apply wide char rules first\n // wide chars\n table.fill(2, 0x1100, 0x1160);\n table[0x2329] = 2;\n table[0x232a] = 2;\n table.fill(2, 0x2e80, 0xa4d0);\n table[0x303f] = 1; // wrongly in last line\n\n table.fill(2, 0xac00, 0xd7a4);\n table.fill(2, 0xf900, 0xfb00);\n table.fill(2, 0xfe10, 0xfe1a);\n table.fill(2, 0xfe30, 0xfe70);\n table.fill(2, 0xff00, 0xff61);\n table.fill(2, 0xffe0, 0xffe7);\n\n // apply combining last to ensure we overwrite\n // wrongly wide set chars:\n // the original algo evals combining first and falls\n // through to wide check so we simply do here the opposite\n // combining 0\n for (let r = 0; r < BMP_COMBINING.length; ++r) {\n table.fill(0, BMP_COMBINING[r][0], BMP_COMBINING[r][1] + 1);\n }\n }\n }\n\n public wcwidth(num: number): UnicodeCharWidth {\n if (num < 32) return 0;\n if (num < 127) return 1;\n if (num < 65536) return table[num] as UnicodeCharWidth;\n if (bisearch(num, HIGH_COMBINING)) return 0;\n if ((num >= 0x20000 && num <= 0x2fffd) || (num >= 0x30000 && num <= 0x3fffd)) return 2;\n return 1;\n }\n\n public charProperties(codepoint: number, preceding: UnicodeCharProperties): UnicodeCharProperties {\n let width = this.wcwidth(codepoint);\n let shouldJoin = width === 0 && preceding !== 0;\n // HACK: Ideally this file would not depend on the service which uses it\n if (shouldJoin) {\n const oldWidth = UnicodeService.extractWidth(preceding);\n if (oldWidth === 0) {\n shouldJoin = false;\n } else if (oldWidth > width) {\n width = oldWidth;\n }\n }\n return UnicodeService.createPropertyValue(0, width, shouldJoin);\n }\n}\n","\n/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { TimeoutTimer } from '../Async';\nimport { Disposable, toDisposable } from '../Lifecycle';\nimport { Emitter } from '../Event';\n\nconst enum Constants {\n /**\n * Safety watermark to avoid memory exhaustion and browser engine crash on fast data input.\n * Enable flow control to avoid this limit and make sure that your backend correctly\n * propagates this to the underlying pty. (see docs for further instructions)\n * Since this limit is meant as a safety parachute to prevent browser crashs,\n * it is set to a very high number. Typically xterm.js gets unresponsive with\n * a 100 times lower number (>500 kB).\n */\n DISCARD_WATERMARK = 50000000, // ~50 MB\n /**\n * The max number of ms to spend on writes before allowing the renderer to\n * catch up with a 0ms setTimeout. A value of < 33 to keep us close to\n * 30fps, and a value of < 16 to try to run at 60fps. Of course, the real FPS\n * depends on the time it takes for the renderer to draw the frame.\n */\n WRITE_TIMEOUT_MS = 12,\n /**\n * Threshold of max held chunks in the write buffer, that were already processed.\n * This is a tradeoff between extensive write buffer shifts (bad runtime) and high\n * memory consumption by data thats not used anymore.\n */\n WRITE_BUFFER_LENGTH_THRESHOLD = 50\n}\n\nexport class WriteBuffer extends Disposable {\n private _writeBuffer: (string | Uint8Array)[] = [];\n private _callbacks: ((() => void) | undefined)[] = [];\n private _pendingData = 0;\n private _bufferOffset = 0;\n private _isSyncWriting = false;\n private _syncCalls = 0;\n private _didUserInput = false;\n\n private readonly _innerWriteTimer = this._register(new TimeoutTimer());\n private readonly _onWriteParsed = this._register(new Emitter());\n public readonly onWriteParsed = this._onWriteParsed.event;\n\n constructor(private _action: (data: string | Uint8Array, promiseResult?: boolean) => void | Promise) {\n super();\n this._register(toDisposable(() => {\n this._writeBuffer.length = 0;\n this._callbacks.length = 0;\n this._pendingData = 0;\n this._bufferOffset = 0;\n }));\n }\n\n public handleUserInput(): void {\n this._didUserInput = true;\n }\n\n /**\n * Flushes all pending writes synchronously. This is useful when you need to\n * ensure all queued data is processed before performing an operation that\n * depends upon everything being parsed like resize.\n *\n * Note: This is unreliable with async parser handlers as it does not wait for\n * promises to resolve.\n */\n public flushSync(): void {\n if (this._store.isDisposed) {\n return;\n }\n // exit early if another sync write loop is active\n if (this._isSyncWriting) {\n return;\n }\n this._isSyncWriting = true;\n\n // Process all pending chunks synchronously\n let chunk: string | Uint8Array | undefined;\n let didProcess = false;\n while (chunk = this._writeBuffer.shift()) {\n didProcess = true;\n this._action(chunk);\n const cb = this._callbacks.shift();\n if (cb) cb();\n }\n\n // Reset buffer state\n this._pendingData = 0;\n this._bufferOffset = 0x7FFFFFFF;\n this._writeBuffer.length = 0;\n this._callbacks.length = 0;\n\n this._isSyncWriting = false;\n if (didProcess) {\n this._onWriteParsed.fire();\n }\n }\n\n /**\n * @deprecated Unreliable, to be removed soon.\n */\n public writeSync(data: string | Uint8Array, maxSubsequentCalls?: number): void {\n if (this._store.isDisposed) {\n return;\n }\n // stop writeSync recursions with maxSubsequentCalls argument\n // This is dangerous to use as it will lose the current data chunk\n // and return immediately.\n if (maxSubsequentCalls !== undefined && this._syncCalls > maxSubsequentCalls) {\n // comment next line if a whole loop block should only contain x `writeSync` calls\n // (total flat vs. deep nested limit)\n this._syncCalls = 0;\n return;\n }\n // append chunk to buffer\n this._pendingData += data.length;\n this._writeBuffer.push(data);\n this._callbacks.push(undefined);\n\n // increase recursion counter\n this._syncCalls++;\n // exit early if another writeSync loop is active\n if (this._isSyncWriting) {\n return;\n }\n this._isSyncWriting = true;\n\n // force sync processing on pending data chunks to avoid in-band data scrambling\n // does the same as innerWrite but without event loop\n // we have to do it here as single loop steps to not corrupt loop subject\n // by another writeSync call triggered from _action\n let chunk: string | Uint8Array | undefined;\n while (chunk = this._writeBuffer.shift()) {\n this._action(chunk);\n const cb = this._callbacks.shift();\n if (cb) cb();\n }\n // reset to avoid reprocessing of chunks with scheduled innerWrite call\n // stopping scheduled innerWrite by offset > length condition\n this._pendingData = 0;\n this._bufferOffset = 0x7FFFFFFF;\n\n // allow another writeSync to loop\n this._isSyncWriting = false;\n this._syncCalls = 0;\n }\n\n public write(data: string | Uint8Array, callback?: () => void): void {\n if (this._store.isDisposed) {\n return;\n }\n if (this._pendingData > Constants.DISCARD_WATERMARK) {\n throw new Error('write data discarded, use flow control to avoid losing data');\n }\n\n // schedule chunk processing for next event loop run\n if (!this._writeBuffer.length) {\n this._bufferOffset = 0;\n\n // If this is the first write call after the user has done some input,\n // parse it immediately to minimize input latency,\n // otherwise schedule for the next event\n if (this._didUserInput) {\n this._didUserInput = false;\n this._pendingData += data.length;\n this._writeBuffer.push(data);\n this._callbacks.push(callback);\n this._innerWrite();\n return;\n }\n\n this._scheduleInnerWrite();\n }\n\n this._pendingData += data.length;\n this._writeBuffer.push(data);\n this._callbacks.push(callback);\n }\n\n /**\n * Inner write call, that enters the sliced chunk processing by timing.\n *\n * `lastTime` indicates, when the last _innerWrite call had started.\n * It is used to aggregate async handler execution under a timeout constraint\n * effectively lowering the redrawing needs, schematically:\n *\n * macroTask _innerWrite:\n * if (performance.now() - (lastTime | 0) < Constants.WRITE_TIMEOUT_MS):\n * schedule microTask _innerWrite(lastTime)\n * else:\n * schedule macroTask _innerWrite(0)\n *\n * overall execution order on task queues:\n *\n * macrotasks: [...] --> _innerWrite(0) --> [...] --> screenUpdate --> [...]\n * m t: |\n * i a: [...]\n * c s: |\n * r k: while < timeout:\n * o s: _innerWrite(timeout)\n *\n * `promiseResult` depicts the promise resolve value of an async handler.\n * This value gets carried forward through all saved stack states of the\n * paused parser for proper continuation.\n *\n * Note, for pure sync code `lastTime` and `promiseResult` have no meaning.\n */\n private _scheduleInnerWrite(lastTime: number = 0, promiseResult: boolean = true): void {\n if (this._store.isDisposed) {\n return;\n }\n this._innerWriteTimer.cancelAndSet(() => this._innerWrite(lastTime, promiseResult), 0);\n }\n\n protected _innerWrite(lastTime: number = 0, promiseResult: boolean = true): void {\n if (this._store.isDisposed) {\n return;\n }\n const startTime = lastTime || performance.now();\n while (this._writeBuffer.length > this._bufferOffset) {\n const data = this._writeBuffer[this._bufferOffset];\n const result = this._action(data, promiseResult);\n if (result) {\n /**\n * If we get a promise as return value, we re-schedule the continuation\n * as thenable on the promise and exit right away.\n *\n * The exit here means, that we block input processing at the current active chunk,\n * the exact execution position within the chunk is preserved by the saved\n * stack content in InputHandler and EscapeSequenceParser.\n *\n * Resuming happens automatically from that saved stack state.\n * Also the resolved promise value is passed along the callstack to\n * `EscapeSequenceParser.parse` to correctly resume the stopped handler loop.\n *\n * Exceptions on async handlers will be logged to console async, but do not interrupt\n * the input processing (continues with next handler at the current input position).\n */\n\n /**\n * If a promise takes long to resolve, we should schedule continuation behind setTimeout.\n * This might already be too late, if our .then enters really late (executor + prev thens\n * took very long). This cannot be solved here for the handler itself (it is the handlers\n * responsibility to slice hard work), but we can at least schedule a screen update as we\n * gain control.\n */\n const continuation: (r: boolean) => void = (r: boolean) => {\n if (this._store.isDisposed) {\n return;\n }\n if (performance.now() - startTime >= Constants.WRITE_TIMEOUT_MS) {\n this._scheduleInnerWrite(0, r);\n } else {\n this._innerWrite(startTime, r);\n }\n };\n\n /**\n * Optimization considerations:\n * The continuation above favors FPS over throughput by eval'ing `startTime` on resolve.\n * This might schedule too many screen updates with bad throughput drops (in case a slow\n * resolving handler sliced its work properly behind setTimeout calls). We cannot spot\n * this condition here, also the renderer has no way to spot nonsense updates either.\n * FIXME: A proper fix for this would track the FPS at the renderer entry level separately.\n *\n * If favoring of FPS shows bad throughput impact, use the following instead. It favors\n * throughput by eval'ing `startTime` upfront pulling at least one more chunk into the\n * current microtask queue (executed before setTimeout).\n */\n // const continuation: (r: boolean) => void = performance.now() - startTime >=\n // Constants.WRITE_TIMEOUT_MS\n // ? r => setTimeout(() => this._innerWrite(0, r))\n // : r => this._innerWrite(startTime, r);\n\n // Handle exceptions synchronously to current band position, idea:\n // 1. spawn a single microtask which we allow to throw hard\n // 2. spawn a promise immediately resolving to `true`\n // (executed on the same queue, thus properly aligned before continuation happens)\n result.catch(err => {\n queueMicrotask(() => {throw err;});\n return Promise.resolve(false);\n }).then(continuation);\n return;\n }\n\n const cb = this._callbacks[this._bufferOffset];\n if (cb) cb();\n this._bufferOffset++;\n this._pendingData -= data.length;\n\n if (performance.now() - startTime >= Constants.WRITE_TIMEOUT_MS) {\n break;\n }\n }\n if (this._writeBuffer.length > this._bufferOffset) {\n // Allow renderer to catch up before processing the next batch\n // trim already processed chunks if we are above threshold\n if (this._bufferOffset > Constants.WRITE_BUFFER_LENGTH_THRESHOLD) {\n this._writeBuffer = this._writeBuffer.slice(this._bufferOffset);\n this._callbacks = this._callbacks.slice(this._bufferOffset);\n this._bufferOffset = 0;\n }\n this._scheduleInnerWrite();\n } else {\n this._writeBuffer.length = 0;\n this._callbacks.length = 0;\n this._pendingData = 0;\n this._bufferOffset = 0;\n }\n this._onWriteParsed.fire();\n }\n}\n","/**\n * Copyright (c) 2021 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\n\n// 'rgb:' rule - matching: r/g/b | rr/gg/bb | rrr/ggg/bbb | rrrr/gggg/bbbb (hex digits)\nconst RGB_REX = /^([\\da-f])\\/([\\da-f])\\/([\\da-f])$|^([\\da-f]{2})\\/([\\da-f]{2})\\/([\\da-f]{2})$|^([\\da-f]{3})\\/([\\da-f]{3})\\/([\\da-f]{3})$|^([\\da-f]{4})\\/([\\da-f]{4})\\/([\\da-f]{4})$/;\n// '#...' rule - matching any hex digits\nconst HASH_REX = /^[\\da-f]+$/;\n\n/**\n * Parse color spec to RGB values (8 bit per channel).\n * See `man xparsecolor` for details about certain format specifications.\n *\n * Supported formats:\n * - rgb:// with , , in h | hh | hhh | hhhh\n * - #RGB, #RRGGBB, #RRRGGGBBB, #RRRRGGGGBBBB\n *\n * All other formats like rgbi: or device-independent string specifications\n * with float numbering are not supported.\n */\nexport function parseColor(data: string): [number, number, number] | undefined {\n if (!data) return;\n // also handle uppercases\n let low = data.toLowerCase();\n if (low.startsWith('rgb:')) {\n // 'rgb:' specifier\n low = low.slice(4);\n const m = RGB_REX.exec(low);\n if (m) {\n const base = m[1] ? 15 : m[4] ? 255 : m[7] ? 4095 : 65535;\n return [\n Math.round(parseInt(m[1] || m[4] || m[7] || m[10], 16) / base * 255),\n Math.round(parseInt(m[2] || m[5] || m[8] || m[11], 16) / base * 255),\n Math.round(parseInt(m[3] || m[6] || m[9] || m[12], 16) / base * 255)\n ];\n }\n } else if (low.startsWith('#')) {\n // '#' specifier\n low = low.slice(1);\n if (HASH_REX.exec(low) && [3, 6, 9, 12].includes(low.length)) {\n const adv = low.length / 3;\n const result: [number, number, number] = [0, 0, 0];\n for (let i = 0; i < 3; ++i) {\n const c = parseInt(low.slice(adv * i, adv * i + adv), 16);\n result[i] = adv === 1 ? c << 4 : adv === 2 ? c : adv === 3 ? c >> 4 : c >> 8;\n }\n return result;\n }\n }\n\n // Named colors are currently not supported due to the large addition to the xterm.js bundle size\n // they would add. In order to support named colors, we would need some way of optionally loading\n // additional payloads so startup/download time is not bloated (see #3530).\n}\n\n// pad hex output to requested bit width\nfunction pad(n: number, bits: number): string {\n const s = n.toString(16);\n const s2 = s.length < 2 ? '0' + s : s;\n switch (bits) {\n case 4:\n return s[0];\n case 8:\n return s2;\n case 12:\n return (s2 + s2).slice(0, 3);\n default:\n return s2 + s2;\n }\n}\n\n/**\n * Convert a given color to rgb:../../.. string of `bits` depth.\n */\nexport function toRgbString(color: [number, number, number], bits: number = 16): string {\n const [r, g, b] = color;\n return `rgb:${pad(r, bits)}/${pad(g, bits)}/${pad(b, bits)}`;\n}\n","/**\n * Copyright (c) 2025 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { IApcHandler, IHandlerCollection, ApcFallbackHandlerType, IApcParser, ISubParserStackState } from './Types';\nimport { ParserConstants } from './Constants';\nimport { utf32ToString } from '../input/TextDecoder';\nimport { IDisposable } from '../Types';\nimport { LimitedStringBuilder } from '../StringBuilder';\n\nconst EMPTY_HANDLERS: IApcHandler[] = [];\n\n/**\n * APC Parser for handling Application Program Command sequences.\n * APC sequences use the format: ESC _ ESC \\\n *\n * Unlike OSC which uses numeric identifiers (e.g., OSC 1337),\n * APC uses the first character as the identifier (e.g., 'G' for Kitty graphics).\n * The identifier is the character code of the first byte after ESC _.\n */\nexport class ApcParser implements IApcParser {\n private _handlers: IHandlerCollection = Object.create(null);\n private _active = EMPTY_HANDLERS;\n private _ident: number = 0;\n private _handlerFb: ApcFallbackHandlerType = () => { };\n private _stack: ISubParserStackState = {\n paused: false,\n loopPosition: 0,\n fallThrough: false\n };\n\n /**\n * Register an APC handler for a specific identifier.\n * @param ident The character code of the first byte (e.g., 0x47 for 'G')\n * @param handler The handler to register\n */\n public registerHandler(ident: number, handler: IApcHandler): IDisposable {\n this._handlers[ident] ??= [];\n const handlerList = this._handlers[ident];\n handlerList.push(handler);\n return {\n dispose: () => {\n const handlerIndex = handlerList.indexOf(handler);\n if (handlerIndex !== -1) {\n handlerList.splice(handlerIndex, 1);\n }\n }\n };\n }\n\n public clearHandler(ident: number): void {\n if (this._handlers[ident]) delete this._handlers[ident];\n }\n\n public setHandlerFallback(handler: ApcFallbackHandlerType): void {\n this._handlerFb = handler;\n }\n\n public dispose(): void {\n this._handlers = Object.create(null);\n this._handlerFb = () => { };\n this._active = EMPTY_HANDLERS;\n }\n\n public reset(): void {\n // force cleanup handlers\n if (this._active.length) {\n for (let j = this._stack.paused ? this._stack.loopPosition - 1 : this._active.length - 1; j >= 0; --j) {\n this._active[j].end(false);\n }\n }\n this._stack.paused = false;\n this._active = EMPTY_HANDLERS;\n this._ident = 0;\n }\n\n public start(ident: number): void {\n // always reset leftover handlers\n this.reset();\n this._ident = ident;\n this._active = this._handlers[ident] || EMPTY_HANDLERS;\n if (!this._active.length) {\n this._handlerFb(this._ident, 'START');\n } else {\n for (let j = this._active.length - 1; j >= 0; j--) {\n this._active[j].start();\n }\n }\n }\n\n public put(data: Uint32Array, start: number, end: number): void {\n if (!this._active.length) {\n this._handlerFb(this._ident, 'PUT', utf32ToString(data, start, end));\n } else {\n for (let j = this._active.length - 1; j >= 0; j--) {\n this._active[j].put(data, start, end);\n }\n }\n }\n\n /**\n * Indicates end of an APC command.\n * Whether the APC got aborted or finished normally\n * is indicated by `success`.\n */\n public end(success: boolean, promiseResult: boolean = true): void | Promise {\n if (!this._active.length) {\n this._handlerFb(this._ident, 'END', success);\n } else {\n let handlerResult: boolean | Promise = false;\n let j = this._active.length - 1;\n let fallThrough = false;\n if (this._stack.paused) {\n j = this._stack.loopPosition - 1;\n handlerResult = promiseResult;\n fallThrough = this._stack.fallThrough;\n this._stack.paused = false;\n }\n if (!fallThrough && handlerResult === false) {\n for (; j >= 0; j--) {\n handlerResult = this._active[j].end(success);\n if (handlerResult === true) {\n break;\n } else if (handlerResult instanceof Promise) {\n this._stack.paused = true;\n this._stack.loopPosition = j;\n this._stack.fallThrough = false;\n return handlerResult;\n }\n }\n j--;\n }\n // cleanup left over handlers (fallThrough for async)\n for (; j >= 0; j--) {\n handlerResult = this._active[j].end(false);\n if (handlerResult instanceof Promise) {\n this._stack.paused = true;\n this._stack.loopPosition = j;\n this._stack.fallThrough = true;\n return handlerResult;\n }\n }\n }\n this._active = EMPTY_HANDLERS;\n this._ident = 0;\n }\n}\n\n/**\n * Convenient class to allow attaching string based handler functions\n * as APC handlers.\n */\nexport class ApcHandler implements IApcHandler {\n private static _payloadLimit = ParserConstants.PAYLOAD_LIMIT;\n\n private _data = new LimitedStringBuilder(ApcHandler._payloadLimit);\n private _hitLimit: boolean = false;\n\n constructor(private _handler: (data: string) => boolean | Promise) { }\n\n public start(): void {\n this._data.reset();\n this._hitLimit = false;\n }\n\n public put(data: Uint32Array, start: number, end: number): void {\n if (this._hitLimit) {\n return;\n }\n if (this._data.append(utf32ToString(data, start, end))) {\n this._hitLimit = true;\n }\n }\n\n public end(success: boolean): boolean | Promise {\n let ret: boolean | Promise = false;\n if (this._hitLimit) {\n ret = false;\n } else if (success) {\n ret = this._handler(this._data.toString());\n if (ret instanceof Promise) {\n // need to hold data until `ret` got resolved\n // dont care for errors, data will be freed anyway on next start\n return ret.then(res => {\n this._data.reset();\n this._hitLimit = false;\n return res;\n });\n }\n }\n this._data.reset();\n this._hitLimit = false;\n return ret;\n }\n}\n","/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { IDisposable } from '../Types';\nimport { IDcsHandler, IParams, IHandlerCollection, IDcsParser, DcsFallbackHandlerType, ISubParserStackState } from './Types';\nimport { utf32ToString } from '../input/TextDecoder';\nimport { Params } from './Params';\nimport { ParserConstants } from './Constants';\nimport { LimitedStringBuilder } from '../StringBuilder';\n\nconst EMPTY_HANDLERS: IDcsHandler[] = [];\n\nexport class DcsParser implements IDcsParser {\n private _handlers: IHandlerCollection = Object.create(null);\n private _active: IDcsHandler[] = EMPTY_HANDLERS;\n private _ident: number = 0;\n private _handlerFb: DcsFallbackHandlerType = () => { };\n private _stack: ISubParserStackState = {\n paused: false,\n loopPosition: 0,\n fallThrough: false\n };\n\n public dispose(): void {\n this._handlers = Object.create(null);\n this._handlerFb = () => { };\n this._active = EMPTY_HANDLERS;\n }\n\n public registerHandler(ident: number, handler: IDcsHandler): IDisposable {\n this._handlers[ident] ??= [];\n const handlerList = this._handlers[ident];\n handlerList.push(handler);\n return {\n dispose: () => {\n const handlerIndex = handlerList.indexOf(handler);\n if (handlerIndex !== -1) {\n handlerList.splice(handlerIndex, 1);\n }\n }\n };\n }\n\n public clearHandler(ident: number): void {\n if (this._handlers[ident]) delete this._handlers[ident];\n }\n\n public setHandlerFallback(handler: DcsFallbackHandlerType): void {\n this._handlerFb = handler;\n }\n\n public reset(): void {\n // force cleanup leftover handlers\n if (this._active.length) {\n for (let j = this._stack.paused ? this._stack.loopPosition - 1 : this._active.length - 1; j >= 0; --j) {\n this._active[j].unhook(false);\n }\n }\n this._stack.paused = false;\n this._active = EMPTY_HANDLERS;\n this._ident = 0;\n }\n\n public hook(ident: number, params: IParams): void {\n // always reset leftover handlers\n this.reset();\n this._ident = ident;\n this._active = this._handlers[ident] || EMPTY_HANDLERS;\n if (!this._active.length) {\n this._handlerFb(this._ident, 'HOOK', params);\n } else {\n for (let j = this._active.length - 1; j >= 0; j--) {\n this._active[j].hook(params);\n }\n }\n }\n\n public put(data: Uint32Array, start: number, end: number): void {\n if (!this._active.length) {\n this._handlerFb(this._ident, 'PUT', utf32ToString(data, start, end));\n } else {\n for (let j = this._active.length - 1; j >= 0; j--) {\n this._active[j].put(data, start, end);\n }\n }\n }\n\n public unhook(success: boolean, promiseResult: boolean = true): void | Promise {\n if (!this._active.length) {\n this._handlerFb(this._ident, 'UNHOOK', success);\n } else {\n let handlerResult: boolean | Promise = false;\n let j = this._active.length - 1;\n let fallThrough = false;\n if (this._stack.paused) {\n j = this._stack.loopPosition - 1;\n handlerResult = promiseResult;\n fallThrough = this._stack.fallThrough;\n this._stack.paused = false;\n }\n if (!fallThrough && handlerResult === false) {\n for (; j >= 0; j--) {\n handlerResult = this._active[j].unhook(success);\n if (handlerResult === true) {\n break;\n } else if (handlerResult instanceof Promise) {\n this._stack.paused = true;\n this._stack.loopPosition = j;\n this._stack.fallThrough = false;\n return handlerResult;\n }\n }\n j--;\n }\n // cleanup left over handlers (fallThrough for async)\n for (; j >= 0; j--) {\n handlerResult = this._active[j].unhook(false);\n if (handlerResult instanceof Promise) {\n this._stack.paused = true;\n this._stack.loopPosition = j;\n this._stack.fallThrough = true;\n return handlerResult;\n }\n }\n }\n this._active = EMPTY_HANDLERS;\n this._ident = 0;\n }\n}\n\n// predefine empty params as [0] (ZDM)\nconst EMPTY_PARAMS = new Params();\nEMPTY_PARAMS.addParam(0);\n\n/**\n * Convenient class to create a DCS handler from a single callback function.\n * Note: The payload is currently limited to 50 MB (hardcoded).\n */\nexport class DcsHandler implements IDcsHandler {\n private static _payloadLimit = ParserConstants.PAYLOAD_LIMIT;\n\n private _data = new LimitedStringBuilder(DcsHandler._payloadLimit);\n private _params: IParams = EMPTY_PARAMS;\n private _hitLimit: boolean = false;\n\n constructor(private _handler: (data: string, params: IParams) => boolean | Promise) { }\n\n public hook(params: IParams): void {\n // since we need to preserve params until `unhook`, we have to clone it\n // (only borrowed from parser and spans multiple parser states)\n // perf optimization:\n // clone only, if we have non empty params, otherwise stick with default\n this._params = (params.length > 1 || params.params[0]) ? params.clone() : EMPTY_PARAMS;\n this._data.reset();\n this._hitLimit = false;\n }\n\n public put(data: Uint32Array, start: number, end: number): void {\n if (this._hitLimit) {\n return;\n }\n if (this._data.append(utf32ToString(data, start, end))) {\n this._hitLimit = true;\n }\n }\n\n public unhook(success: boolean): boolean | Promise {\n let ret: boolean | Promise = false;\n if (this._hitLimit) {\n ret = false;\n } else if (success) {\n ret = this._handler(this._data.toString(), this._params);\n if (ret instanceof Promise) {\n // need to hold data and params until `ret` got resolved\n // dont care for errors, data will be freed anyway on next start\n return ret.then(res => {\n this._params = EMPTY_PARAMS;\n this._data.reset();\n this._hitLimit = false;\n return res;\n });\n }\n }\n this._params = EMPTY_PARAMS;\n this._data.reset();\n this._hitLimit = false;\n return ret;\n }\n}\n","/**\n * Copyright (c) 2018 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandlerType, OscFallbackHandlerType, IOscParser, EscHandlerType, IDcsParser, DcsFallbackHandlerType, IFunctionIdentifier, ExecuteFallbackHandlerType, CsiFallbackHandlerType, EscFallbackHandlerType, PrintHandlerType, PrintFallbackHandlerType, ExecuteHandlerType, IParserStackState, ParserStackType, ResumableHandlersType, IApcHandler, IApcParser, ApcFallbackHandlerType } from './Types';\nimport { ParserState, ParserAction } from './Constants';\nimport { Disposable, toDisposable } from '../Lifecycle';\nimport { IDisposable } from '../Types';\nimport { Params } from './Params';\nimport { OscParser } from './OscParser';\nimport { DcsParser } from './DcsParser';\nimport { ApcParser } from './ApcParser';\n\n/**\n * VT commands done by the parser\n */\n// @vt: #Y ESC CSI \"Control Sequence Introducer\" \"ESC [\" \"Start of a CSI sequence.\"\n// @vt: #Y ESC OSC \"Operating System Command\" \"ESC ]\" \"Start of an OSC sequence.\"\n// @vt: #Y ESC DCS \"Device Control String\" \"ESC P\" \"Start of a DCS sequence.\"\n// @vt: #Y ESC ST \"String Terminator\" \"ESC \\\\\" \"Terminator used for string type sequences.\"\n// @vt: #Y ESC PM \"Privacy Message\" \"ESC ^\" \"Start of a privacy message.\"\n// @vt: #Y ESC APC \"Application Program Command\" \"ESC _\" \"Start of an APC sequence.\"\n// @vt: #Y C1 CSI \"Control Sequence Introducer\" \"\\x9B\" \"Start of a CSI sequence.\"\n// @vt: #Y C1 OSC \"Operating System Command\" \"\\x9D\" \"Start of an OSC sequence.\"\n// @vt: #Y C1 DCS \"Device Control String\" \"\\x90\" \"Start of a DCS sequence.\"\n// @vt: #Y C1 ST \"String Terminator\" \"\\x9C\" \"Terminator used for string type sequences.\"\n// @vt: #Y C1 PM \"Privacy Message\" \"\\x9E\" \"Start of a privacy message.\"\n// @vt: #Y C1 APC \"Application Program Command\" \"\\x9F\" \"Start of an APC sequence.\"\n// @vt: #Y C0 NUL \"Null\" \"\\0, \\x00\" \"NUL is ignored.\"\n// @vt: #Y C0 ESC \"Escape\" \"\\e, \\x1B\" \"Start of a sequence. Cancels any other sequence.\"\n\n/**\n * Table values are generated like this:\n * index: currentState << TableValue.INDEX_STATE_SHIFT | charCode\n * value: action << TableValue.TRANSITION_ACTION_SHIFT | nextState\n */\nconst enum TableAccess {\n TRANSITION_ACTION_SHIFT = 8,\n TRANSITION_STATE_MASK = 255,\n INDEX_STATE_SHIFT = 8\n}\n\n/**\n * Transition table for EscapeSequenceParser.\n */\nexport class TransitionTable {\n public table: Uint16Array;\n\n constructor(length: number) {\n this.table = new Uint16Array(length);\n }\n\n /**\n * Set default transition.\n * @param action default action\n * @param next default next state\n */\n public setDefault(action: ParserAction, next: ParserState): void {\n this.table.fill(action << TableAccess.TRANSITION_ACTION_SHIFT | next);\n }\n\n /**\n * Add a transition to the transition table.\n * @param code input character code\n * @param state current parser state\n * @param action parser action to be done\n * @param next next parser state\n */\n public add(code: number, state: ParserState, action: ParserAction, next: ParserState): void {\n this.table[state << TableAccess.INDEX_STATE_SHIFT | code] = action << TableAccess.TRANSITION_ACTION_SHIFT | next;\n }\n\n /**\n * Add transitions for multiple input character codes.\n * @param codes input character code array\n * @param state current parser state\n * @param action parser action to be done\n * @param next next parser state\n */\n public addMany(codes: number[], state: ParserState, action: ParserAction, next: ParserState): void {\n for (let i = 0; i < codes.length; i++) {\n this.table[state << TableAccess.INDEX_STATE_SHIFT | codes[i]] = action << TableAccess.TRANSITION_ACTION_SHIFT | next;\n }\n }\n}\n\n\n// Pseudo-character placeholder for printable non-ascii characters (unicode).\nconst NON_ASCII_PRINTABLE = 0xA0;\n\n\n/**\n * VT500 compatible transition table.\n * Taken from https://vt100.net/emu/dec_ansi_parser.\n */\nexport const VT500_TRANSITION_TABLE = (function (): TransitionTable {\n // table size:\n // (ParserState.STATE_LENGTH - 1) << TableAccess.INDEX_STATE_SHIFT | NON_ASCII_PRINTABLE + 1\n const table: TransitionTable = new TransitionTable(4257);\n\n // range macro for byte\n const BYTE_VALUES = 256;\n const blueprint = Array.apply(null, Array(BYTE_VALUES)).map((unused: any, i: number) => i);\n const r = (start: number, end: number): number[] => blueprint.slice(start, end);\n\n // Default definitions.\n const PRINTABLES = r(0x20, 0x7f); // 0x20 (SP) included, 0x7F (DEL) excluded\n const EXECUTABLES = r(0x00, 0x18);\n EXECUTABLES.push(0x19);\n EXECUTABLES.push.apply(EXECUTABLES, r(0x1c, 0x20));\n\n const states: number[] = r(ParserState.GROUND, ParserState.STATE_LENGTH);\n\n // set default transition\n table.setDefault(ParserAction.ERROR, ParserState.GROUND);\n // printables\n table.addMany(PRINTABLES, ParserState.GROUND, ParserAction.PRINT, ParserState.GROUND);\n // global anywhere rules\n for (const state of states) {\n table.addMany([0x18, 0x1a, 0x99, 0x9a], state, ParserAction.EXECUTE, ParserState.GROUND);\n table.addMany(r(0x80, 0x90), state, ParserAction.EXECUTE, ParserState.GROUND);\n table.addMany(r(0x90, 0x98), state, ParserAction.EXECUTE, ParserState.GROUND);\n table.add(0x9c, state, ParserAction.IGNORE, ParserState.GROUND); // ST as terminator\n table.add(0x1b, state, ParserAction.CLEAR, ParserState.ESCAPE); // ESC\n table.add(0x9d, state, ParserAction.OSC_START, ParserState.OSC_STRING); // OSC\n table.addMany([0x98, 0x9e], state, ParserAction.IGNORE, ParserState.SOS_PM_STRING); // SOS, PM\n table.add(0x9f, state, ParserAction.CLEAR, ParserState.APC_ENTRY); // APC\n table.add(0x9b, state, ParserAction.CLEAR, ParserState.CSI_ENTRY); // CSI\n table.add(0x90, state, ParserAction.CLEAR, ParserState.DCS_ENTRY); // DCS\n }\n // rules for executables and 7f\n table.addMany(EXECUTABLES, ParserState.GROUND, ParserAction.EXECUTE, ParserState.GROUND);\n table.addMany(EXECUTABLES, ParserState.ESCAPE, ParserAction.EXECUTE, ParserState.ESCAPE);\n table.add(0x7f, ParserState.ESCAPE, ParserAction.IGNORE, ParserState.ESCAPE);\n table.addMany(EXECUTABLES, ParserState.OSC_STRING, ParserAction.IGNORE, ParserState.OSC_STRING);\n table.addMany(EXECUTABLES, ParserState.CSI_ENTRY, ParserAction.EXECUTE, ParserState.CSI_ENTRY);\n table.add(0x7f, ParserState.CSI_ENTRY, ParserAction.IGNORE, ParserState.CSI_ENTRY);\n table.addMany(EXECUTABLES, ParserState.CSI_PARAM, ParserAction.EXECUTE, ParserState.CSI_PARAM);\n table.add(0x7f, ParserState.CSI_PARAM, ParserAction.IGNORE, ParserState.CSI_PARAM);\n table.addMany(EXECUTABLES, ParserState.CSI_IGNORE, ParserAction.EXECUTE, ParserState.CSI_IGNORE);\n table.addMany(EXECUTABLES, ParserState.CSI_INTERMEDIATE, ParserAction.EXECUTE, ParserState.CSI_INTERMEDIATE);\n table.add(0x7f, ParserState.CSI_INTERMEDIATE, ParserAction.IGNORE, ParserState.CSI_INTERMEDIATE);\n table.addMany(EXECUTABLES, ParserState.ESCAPE_INTERMEDIATE, ParserAction.EXECUTE, ParserState.ESCAPE_INTERMEDIATE);\n table.add(0x7f, ParserState.ESCAPE_INTERMEDIATE, ParserAction.IGNORE, ParserState.ESCAPE_INTERMEDIATE);\n // osc\n table.add(0x5d, ParserState.ESCAPE, ParserAction.OSC_START, ParserState.OSC_STRING);\n table.addMany(PRINTABLES, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING);\n table.add(0x7f, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING);\n table.addMany([0x9c, 0x1b, 0x18, 0x1a, 0x07], ParserState.OSC_STRING, ParserAction.OSC_END, ParserState.GROUND);\n table.addMany(r(0x1c, 0x20), ParserState.OSC_STRING, ParserAction.IGNORE, ParserState.OSC_STRING);\n // sos/pm\n table.addMany([0x58, 0x5e], ParserState.ESCAPE, ParserAction.IGNORE, ParserState.SOS_PM_STRING);\n table.addMany(PRINTABLES, ParserState.SOS_PM_STRING, ParserAction.IGNORE, ParserState.SOS_PM_STRING);\n table.addMany(EXECUTABLES, ParserState.SOS_PM_STRING, ParserAction.IGNORE, ParserState.SOS_PM_STRING);\n table.add(0x9c, ParserState.SOS_PM_STRING, ParserAction.IGNORE, ParserState.GROUND);\n table.add(0x7f, ParserState.SOS_PM_STRING, ParserAction.IGNORE, ParserState.SOS_PM_STRING);\n // apc\n table.add(0x5f, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.APC_ENTRY);\n table.addMany(EXECUTABLES, ParserState.APC_ENTRY, ParserAction.IGNORE, ParserState.APC_ENTRY);\n table.add(0x7f, ParserState.APC_ENTRY, ParserAction.IGNORE, ParserState.APC_ENTRY);\n table.addMany(r(0x20, 0x30), ParserState.APC_ENTRY, ParserAction.COLLECT, ParserState.APC_INTERMEDIATE);\n table.addMany(r(0x30, 0x7f), ParserState.APC_ENTRY, ParserAction.APC_START, ParserState.APC_PASSTHROUGH);\n table.addMany(r(0x30, 0x7f), ParserState.APC_INTERMEDIATE, ParserAction.APC_START, ParserState.APC_PASSTHROUGH);\n table.addMany(EXECUTABLES, ParserState.APC_INTERMEDIATE, ParserAction.IGNORE, ParserState.APC_INTERMEDIATE);\n table.addMany(r(0x20, 0x30), ParserState.APC_INTERMEDIATE, ParserAction.COLLECT, ParserState.APC_INTERMEDIATE);\n table.add(0x7f, ParserState.APC_INTERMEDIATE, ParserAction.IGNORE, ParserState.APC_INTERMEDIATE);\n table.addMany(PRINTABLES, ParserState.APC_PASSTHROUGH, ParserAction.APC_PUT, ParserState.APC_PASSTHROUGH);\n table.addMany(EXECUTABLES, ParserState.APC_PASSTHROUGH, ParserAction.IGNORE, ParserState.APC_PASSTHROUGH);\n table.addMany(r(0x08, 0x0e), ParserState.APC_PASSTHROUGH, ParserAction.APC_PUT, ParserState.APC_PASSTHROUGH);\n table.add(0x7f, ParserState.APC_PASSTHROUGH, ParserAction.IGNORE, ParserState.APC_PASSTHROUGH);\n table.addMany([0x1b, 0x9c, 0x18, 0x1a], ParserState.APC_PASSTHROUGH, ParserAction.APC_END, ParserState.GROUND);\n // csi entries\n table.add(0x5b, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.CSI_ENTRY);\n table.addMany(r(0x40, 0x7f), ParserState.CSI_ENTRY, ParserAction.CSI_DISPATCH, ParserState.GROUND);\n table.addMany(r(0x30, 0x3c), ParserState.CSI_ENTRY, ParserAction.PARAM, ParserState.CSI_PARAM);\n table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.CSI_ENTRY, ParserAction.COLLECT, ParserState.CSI_PARAM);\n table.addMany(r(0x30, 0x3c), ParserState.CSI_PARAM, ParserAction.PARAM, ParserState.CSI_PARAM);\n table.addMany(r(0x40, 0x7f), ParserState.CSI_PARAM, ParserAction.CSI_DISPATCH, ParserState.GROUND);\n table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.CSI_PARAM, ParserAction.IGNORE, ParserState.CSI_IGNORE);\n table.addMany(r(0x20, 0x40), ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE);\n table.add(0x7f, ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE);\n table.addMany(r(0x40, 0x7f), ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.GROUND);\n table.addMany(r(0x20, 0x30), ParserState.CSI_ENTRY, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE);\n table.addMany(r(0x20, 0x30), ParserState.CSI_INTERMEDIATE, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE);\n table.addMany(r(0x30, 0x40), ParserState.CSI_INTERMEDIATE, ParserAction.IGNORE, ParserState.CSI_IGNORE);\n table.addMany(r(0x40, 0x7f), ParserState.CSI_INTERMEDIATE, ParserAction.CSI_DISPATCH, ParserState.GROUND);\n table.addMany(r(0x20, 0x30), ParserState.CSI_PARAM, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE);\n // esc_intermediate\n table.addMany(r(0x20, 0x30), ParserState.ESCAPE, ParserAction.COLLECT, ParserState.ESCAPE_INTERMEDIATE);\n table.addMany(r(0x20, 0x30), ParserState.ESCAPE_INTERMEDIATE, ParserAction.COLLECT, ParserState.ESCAPE_INTERMEDIATE);\n table.addMany(r(0x30, 0x7f), ParserState.ESCAPE_INTERMEDIATE, ParserAction.ESC_DISPATCH, ParserState.GROUND);\n table.addMany(r(0x30, 0x50), ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND);\n table.addMany(r(0x51, 0x58), ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND);\n table.addMany([0x59, 0x5a, 0x5c], ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND);\n table.addMany(r(0x60, 0x7f), ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND);\n // dcs entry\n table.add(0x50, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.DCS_ENTRY);\n table.addMany(EXECUTABLES, ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY);\n table.add(0x7f, ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY);\n table.addMany(r(0x20, 0x30), ParserState.DCS_ENTRY, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE);\n table.addMany(r(0x30, 0x3c), ParserState.DCS_ENTRY, ParserAction.PARAM, ParserState.DCS_PARAM);\n table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.DCS_ENTRY, ParserAction.COLLECT, ParserState.DCS_PARAM);\n table.addMany(EXECUTABLES, ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE);\n table.addMany(r(0x20, 0x80), ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE);\n table.addMany(EXECUTABLES, ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM);\n table.add(0x7f, ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM);\n table.addMany(r(0x30, 0x3c), ParserState.DCS_PARAM, ParserAction.PARAM, ParserState.DCS_PARAM);\n table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_IGNORE);\n table.addMany(r(0x20, 0x30), ParserState.DCS_PARAM, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE);\n table.addMany(EXECUTABLES, ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE);\n table.add(0x7f, ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE);\n table.addMany(r(0x20, 0x30), ParserState.DCS_INTERMEDIATE, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE);\n table.addMany(r(0x30, 0x40), ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_IGNORE);\n table.addMany(r(0x40, 0x7f), ParserState.DCS_INTERMEDIATE, ParserAction.DCS_HOOK, ParserState.DCS_PASSTHROUGH);\n table.addMany(r(0x40, 0x7f), ParserState.DCS_PARAM, ParserAction.DCS_HOOK, ParserState.DCS_PASSTHROUGH);\n table.addMany(r(0x40, 0x7f), ParserState.DCS_ENTRY, ParserAction.DCS_HOOK, ParserState.DCS_PASSTHROUGH);\n table.addMany(EXECUTABLES, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH);\n table.addMany(PRINTABLES, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH);\n table.add(0x7f, ParserState.DCS_PASSTHROUGH, ParserAction.IGNORE, ParserState.DCS_PASSTHROUGH);\n table.addMany([0x1b, 0x9c, 0x18, 0x1a], ParserState.DCS_PASSTHROUGH, ParserAction.DCS_UNHOOK, ParserState.GROUND);\n // special handling of unicode chars\n table.add(NON_ASCII_PRINTABLE, ParserState.GROUND, ParserAction.PRINT, ParserState.GROUND);\n table.add(NON_ASCII_PRINTABLE, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING);\n table.add(NON_ASCII_PRINTABLE, ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE);\n table.add(NON_ASCII_PRINTABLE, ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE);\n table.add(NON_ASCII_PRINTABLE, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH);\n table.add(NON_ASCII_PRINTABLE, ParserState.APC_PASSTHROUGH, ParserAction.APC_PUT, ParserState.APC_PASSTHROUGH);\n return table;\n})();\n\n\n/**\n * EscapeSequenceParser.\n * This class implements the ANSI/DEC compatible parser described by\n * Paul Williams (https://vt100.net/emu/dec_ansi_parser).\n *\n * To implement custom ANSI compliant escape sequences it is not needed to\n * alter this parser, instead consider registering a custom handler.\n * For non ANSI compliant sequences change the transition table with\n * the optional `transitions` constructor argument and\n * reimplement the `parse` method.\n *\n * This parser is currently hardcoded to operate in ZDM (Zero Default Mode)\n * as suggested by the original parser, thus empty parameters are set to 0.\n * This is not in line with the latest ECMA-48 specification\n * (ZDM was part of the early specs and got completely removed later on).\n *\n * Other than the original parser from vt100.net this parser supports\n * sub parameters in digital parameters separated by colons. Empty sub parameters\n * are set to -1 (no ZDM for sub parameters).\n *\n * About prefix and intermediate bytes:\n * This parser follows the assumptions of the vt100.net parser with these restrictions:\n * - only one prefix byte is allowed as first parameter byte, byte range 0x3c .. 0x3f\n * - max. two intermediates are respected, byte range 0x20 .. 0x2f\n * Note that this is not in line with ECMA-48 which does not limit either of those.\n * Furthermore ECMA-48 allows the prefix byte range at any param byte position. Currently\n * there are no known sequences that follow the broader definition of the specification.\n *\n * TODO: implement error recovery hook via error handler return values\n */\nexport class EscapeSequenceParser extends Disposable implements IEscapeSequenceParser {\n public initialState: number;\n public currentState: number;\n public precedingJoinState: number; // UnicodeJoinProperties\n\n // buffers over several parse calls\n protected _params: Params;\n protected _collect: number;\n\n // handler lookup containers\n protected _printHandler: PrintHandlerType;\n protected _executeHandlers: { [flag: number]: ExecuteHandlerType };\n // fast path for EXE bytes < 0x18\n protected _executeHandlersArr: (ExecuteHandlerType | undefined)[];\n protected _csiHandlers: IHandlerCollection;\n protected _escHandlers: IHandlerCollection;\n protected readonly _oscParser: IOscParser;\n protected readonly _dcsParser: IDcsParser;\n protected readonly _apcParser: IApcParser;\n protected _errorHandler: (state: IParsingState) => IParsingState;\n\n // fallback handlers\n protected _printHandlerFb: PrintFallbackHandlerType;\n protected _executeHandlerFb: ExecuteFallbackHandlerType;\n protected _csiHandlerFb: CsiFallbackHandlerType;\n protected _escHandlerFb: EscFallbackHandlerType;\n protected _errorHandlerFb: (state: IParsingState) => IParsingState;\n\n // parser stack save for async handler support\n protected _parseStack: IParserStackState = {\n state: ParserStackType.NONE,\n handlers: [],\n handlerPos: 0,\n transition: 0,\n chunkPos: 0\n };\n\n constructor(\n protected readonly _transitions: TransitionTable = VT500_TRANSITION_TABLE\n ) {\n super();\n\n this.initialState = ParserState.GROUND;\n this.currentState = this.initialState;\n this._params = new Params(); // defaults to 32 storable params/subparams\n this._params.addParam(0); // ZDM\n this._collect = 0;\n this.precedingJoinState = 0;\n\n // set default fallback handlers and handler lookup containers\n this._printHandlerFb = (data, start, end): void => { };\n this._executeHandlerFb = (code: number): void => { };\n this._csiHandlerFb = (ident: number, params: IParams): void => { };\n this._escHandlerFb = (ident: number): void => { };\n this._errorHandlerFb = (state: IParsingState): IParsingState => state;\n this._printHandler = this._printHandlerFb;\n this._executeHandlers = Object.create(null);\n this._executeHandlersArr = new Array(0x18).fill(undefined);\n this._csiHandlers = Object.create(null);\n this._escHandlers = Object.create(null);\n this._register(toDisposable(() => {\n this._csiHandlers = Object.create(null);\n this._executeHandlers = Object.create(null);\n this._executeHandlersArr = new Array(0x18).fill(undefined);\n this._escHandlers = Object.create(null);\n }));\n this._oscParser = this._register(new OscParser());\n this._dcsParser = this._register(new DcsParser());\n this._apcParser = this._register(new ApcParser());\n this._errorHandler = this._errorHandlerFb;\n\n // swallow 7bit ST (ESC+\\)\n this.registerEscHandler({ final: '\\\\' }, () => true);\n }\n\n protected _identifier(id: IFunctionIdentifier, finalRange: number[] = [0x40, 0x7e]): number {\n let res = 0;\n if (id.prefix) {\n if (id.prefix.length > 1) {\n throw new Error('only one byte as prefix supported');\n }\n res = id.prefix.charCodeAt(0);\n if (res < 0x3c || res > 0x3f) {\n throw new Error('prefix must be in range 0x3c .. 0x3f');\n }\n }\n if (id.intermediates) {\n if (id.intermediates.length > 2) {\n throw new Error('only two bytes as intermediates are supported');\n }\n for (let i = 0; i < id.intermediates.length; ++i) {\n const intermediate = id.intermediates.charCodeAt(i);\n if (0x20 > intermediate || intermediate > 0x2f) {\n throw new Error('intermediate must be in range 0x20 .. 0x2f');\n }\n res <<= 8;\n res |= intermediate;\n }\n }\n if (id.final.length !== 1) {\n throw new Error('final must be a single byte');\n }\n const finalCode = id.final.charCodeAt(0);\n if (finalRange[0] > finalCode || finalCode > finalRange[1]) {\n throw new Error(`final must be in range ${finalRange[0]} .. ${finalRange[1]}`);\n }\n res <<= 8;\n res |= finalCode;\n\n return res;\n }\n\n public identToString(ident: number): string {\n const res: string[] = [];\n while (ident) {\n res.push(String.fromCharCode(ident & 0xFF));\n ident >>= 8;\n }\n return res.reverse().join('');\n }\n\n public setPrintHandler(handler: PrintHandlerType): void {\n this._printHandler = handler;\n }\n public clearPrintHandler(): void {\n this._printHandler = this._printHandlerFb;\n }\n\n public registerEscHandler(id: IFunctionIdentifier, handler: EscHandlerType): IDisposable {\n const ident = this._identifier(id, [0x30, 0x7e]);\n this._escHandlers[ident] ??= [];\n const handlerList = this._escHandlers[ident];\n handlerList.push(handler);\n return {\n dispose: () => {\n const handlerIndex = handlerList.indexOf(handler);\n if (handlerIndex !== -1) {\n handlerList.splice(handlerIndex, 1);\n }\n }\n };\n }\n public clearEscHandler(id: IFunctionIdentifier): void {\n if (this._escHandlers[this._identifier(id, [0x30, 0x7e])]) delete this._escHandlers[this._identifier(id, [0x30, 0x7e])];\n }\n public setEscHandlerFallback(handler: EscFallbackHandlerType): void {\n this._escHandlerFb = handler;\n }\n\n public setExecuteHandler(flag: string, handler: ExecuteHandlerType): void {\n const code = flag.charCodeAt(0);\n this._executeHandlers[code] = handler;\n if (code < 0x18) this._executeHandlersArr[code] = handler;\n }\n public clearExecuteHandler(flag: string): void {\n const code = flag.charCodeAt(0);\n if (this._executeHandlers[code]) delete this._executeHandlers[code];\n if (code < 0x18) this._executeHandlersArr[code] = undefined;\n }\n public setExecuteHandlerFallback(handler: ExecuteFallbackHandlerType): void {\n this._executeHandlerFb = handler;\n }\n\n public registerCsiHandler(id: IFunctionIdentifier, handler: CsiHandlerType): IDisposable {\n const ident = this._identifier(id);\n this._csiHandlers[ident] ??= [];\n const handlerList = this._csiHandlers[ident];\n handlerList.push(handler);\n return {\n dispose: () => {\n const handlerIndex = handlerList.indexOf(handler);\n if (handlerIndex !== -1) {\n handlerList.splice(handlerIndex, 1);\n }\n }\n };\n }\n public clearCsiHandler(id: IFunctionIdentifier): void {\n if (this._csiHandlers[this._identifier(id)]) delete this._csiHandlers[this._identifier(id)];\n }\n public setCsiHandlerFallback(callback: (ident: number, params: IParams) => void): void {\n this._csiHandlerFb = callback;\n }\n\n public registerDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): IDisposable {\n return this._dcsParser.registerHandler(this._identifier(id), handler);\n }\n public clearDcsHandler(id: IFunctionIdentifier): void {\n this._dcsParser.clearHandler(this._identifier(id));\n }\n public setDcsHandlerFallback(handler: DcsFallbackHandlerType): void {\n this._dcsParser.setHandlerFallback(handler);\n }\n\n public registerOscHandler(ident: number, handler: IOscHandler): IDisposable {\n return this._oscParser.registerHandler(ident, handler);\n }\n public clearOscHandler(ident: number): void {\n this._oscParser.clearHandler(ident);\n }\n public setOscHandlerFallback(handler: OscFallbackHandlerType): void {\n this._oscParser.setHandlerFallback(handler);\n }\n\n public registerApcHandler(id: IFunctionIdentifier, handler: IApcHandler): IDisposable {\n id.prefix = undefined; // APC does not support prefix byte\n return this._apcParser.registerHandler(this._identifier(id, [0x30, 0x7e]), handler);\n }\n public clearApcHandler(id: IFunctionIdentifier): void {\n id.prefix = undefined; // APC does not support prefix byte\n this._apcParser.clearHandler(this._identifier(id, [0x30, 0x7e]));\n }\n public setApcHandlerFallback(handler: ApcFallbackHandlerType): void {\n this._apcParser.setHandlerFallback(handler);\n }\n\n public setErrorHandler(callback: (state: IParsingState) => IParsingState): void {\n this._errorHandler = callback;\n }\n public clearErrorHandler(): void {\n this._errorHandler = this._errorHandlerFb;\n }\n\n /**\n * Reset parser to initial values.\n *\n * This can also be used to lift the improper continuation error condition\n * when dealing with async handlers. Use this only as a last resort to silence\n * that error when the terminal has no pending data to be processed. Note that\n * the interrupted async handler might continue its work in the future messing\n * up the terminal state even further.\n */\n public reset(): void {\n this.currentState = this.initialState;\n this._oscParser.reset();\n this._dcsParser.reset();\n this._apcParser.reset();\n this._params.resetZdm();\n this._collect = 0;\n this.precedingJoinState = 0;\n // abort pending continuation from async handler\n // Here the RESET type indicates, that the next parse call will\n // ignore any saved stack, instead continues sync with next codepoint from GROUND\n if (this._parseStack.state !== ParserStackType.NONE) {\n this._parseStack.state = ParserStackType.RESET;\n this._parseStack.handlers = []; // also release handlers ref\n }\n }\n\n /**\n * Async parse support.\n */\n protected _preserveStack(\n state: ParserStackType,\n handlers: ResumableHandlersType,\n handlerPos: number,\n transition: number,\n chunkPos: number\n ): void {\n this._parseStack.state = state;\n this._parseStack.handlers = handlers;\n this._parseStack.handlerPos = handlerPos;\n this._parseStack.transition = transition;\n this._parseStack.chunkPos = chunkPos;\n }\n\n /**\n * Parse UTF32 codepoints in `data` up to `length`.\n *\n * Note: For several actions with high data load the parsing is optimized\n * by using local read ahead loops with hardcoded conditions to\n * avoid costly table lookups. Make sure that any change of table values\n * will be reflected in the loop conditions as well and vice versa.\n * Affected states/actions:\n * - GROUND:PRINT\n * - CSI_PARAM:PARAM\n * - DCS_PARAM:PARAM\n * - OSC_STRING:OSC_PUT\n * - DCS_PASSTHROUGH:DCS_PUT\n *\n * Additionally the following fast paths exist before the table lookup:\n * - EXE bytes < 0x18 in non-payload states (avoids table lookup entirely)\n * - 7-bit CSI sequences without intermediates (ESC [ params final)\n *\n * Note on asynchronous handler support:\n * Any handler returning a promise will be treated as asynchronous.\n * To keep the in-band blocking working for async handlers, `parse` pauses execution,\n * creates a stack save and returns the promise to the caller.\n * For proper continuation of the paused state it is important\n * to await the promise resolving. On resolve the parse must be repeated\n * with the same chunk of data and the resolved value in `promiseResult`\n * until no promise is returned.\n *\n * Important: With only sync handlers defined, parsing is completely synchronous as well.\n * As soon as an async handler is involved, synchronous parsing is not possible anymore.\n *\n * Boilerplate for proper parsing of multiple chunks with async handlers:\n *\n * ```typescript\n * async function parseMultipleChunks(chunks: Uint32Array[]): Promise {\n * for (const chunk of chunks) {\n * let result: void | Promise;\n * let prev: boolean | undefined;\n * while (result = parser.parse(chunk, chunk.length, prev)) {\n * prev = await result;\n * }\n * }\n * // finished parsing all chunks...\n * }\n * ```\n */\n public parse(data: Uint32Array, length: number, promiseResult?: boolean): void | Promise {\n let code: number;\n let transition: number;\n let start = 0;\n let handlerResult: void | boolean | Promise;\n\n // resume from async handler\n if (this._parseStack.state) {\n // allow sync parser reset even in continuation mode\n // Note: can be used to recover parser from improper continuation error below\n if (this._parseStack.state === ParserStackType.RESET) {\n this._parseStack.state = ParserStackType.NONE;\n start = this._parseStack.chunkPos + 1; // continue with next codepoint in GROUND\n } else {\n if (promiseResult === undefined || this._parseStack.state === ParserStackType.FAIL) {\n /**\n * Reject further parsing on improper continuation after pausing. This is a really bad\n * condition with screwed up execution order and prolly messed up terminal state,\n * therefore we exit hard with an exception and reject any further parsing.\n *\n * Note: With `Terminal.write` usage this exception should never occur, as the top level\n * calls are guaranteed to handle async conditions properly. If you ever encounter this\n * exception in your terminal integration it indicates, that you injected data chunks to\n * `InputHandler.parse` or `EscapeSequenceParser.parse` synchronously without waiting for\n * continuation of a running async handler.\n *\n * It is possible to get rid of this error by calling `reset`. But dont rely on that, as\n * the pending async handler still might mess up the terminal later. Instead fix the\n * faulty async handling, so this error will not be thrown anymore.\n */\n this._parseStack.state = ParserStackType.FAIL;\n throw new Error('improper continuation due to previous async handler, giving up parsing');\n }\n\n // we have to resume the old handler loop if:\n // - return value of the promise was `false`\n // - handlers are not exhausted yet\n const handlers = this._parseStack.handlers;\n let handlerPos = this._parseStack.handlerPos - 1;\n switch (this._parseStack.state) {\n case ParserStackType.CSI:\n if (promiseResult === false && handlerPos > -1) {\n for (; handlerPos >= 0; handlerPos--) {\n handlerResult = (handlers as CsiHandlerType[])[handlerPos](this._params);\n if (handlerResult === true) {\n break;\n } else if (handlerResult instanceof Promise) {\n this._parseStack.handlerPos = handlerPos;\n return handlerResult;\n }\n }\n }\n this._parseStack.handlers = [];\n break;\n case ParserStackType.ESC:\n if (promiseResult === false && handlerPos > -1) {\n for (; handlerPos >= 0; handlerPos--) {\n handlerResult = (handlers as EscHandlerType[])[handlerPos]();\n if (handlerResult === true) {\n break;\n } else if (handlerResult instanceof Promise) {\n this._parseStack.handlerPos = handlerPos;\n return handlerResult;\n }\n }\n }\n this._parseStack.handlers = [];\n break;\n case ParserStackType.DCS:\n code = data[this._parseStack.chunkPos];\n handlerResult = this._dcsParser.unhook(code !== 0x18 && code !== 0x1a, promiseResult);\n if (handlerResult) {\n return handlerResult;\n }\n if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE;\n this._params.resetZdm();\n this._collect = 0;\n break;\n case ParserStackType.OSC:\n code = data[this._parseStack.chunkPos];\n handlerResult = this._oscParser.end(code !== 0x18 && code !== 0x1a, promiseResult);\n if (handlerResult) {\n return handlerResult;\n }\n if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE;\n this._params.resetZdm();\n this._collect = 0;\n break;\n case ParserStackType.APC:\n code = data[this._parseStack.chunkPos];\n handlerResult = this._apcParser.end(code !== 0x18 && code !== 0x1a, promiseResult);\n if (handlerResult) {\n return handlerResult;\n }\n if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE;\n this._params.resetZdm();\n this._collect = 0;\n break;\n }\n // cleanup before continuing with the main sync loop\n this._parseStack.state = ParserStackType.NONE;\n start = this._parseStack.chunkPos + 1;\n this.precedingJoinState = 0;\n this.currentState = this._parseStack.transition & TableAccess.TRANSITION_STATE_MASK;\n }\n }\n\n // continue with main sync loop\n\n // process input string\n for (let i = start; i < length; ++i) {\n code = data[i];\n\n // EXE fast-path: common control bytes (0x00-0x17) in non-payload states\n if (code < 0x18 && this.currentState <= ParserState.CSI_IGNORE) {\n (this._executeHandlersArr[code] ?? this._executeHandlerFb)(code);\n this.precedingJoinState = 0;\n continue;\n }\n\n // CSI fast-path: collapse ESC [ into a single entry, parse params+final in a tight loop\n if (code === 0x1b\n && this.currentState < ParserState.OSC_STRING\n && i + 2 < length && data[i + 1] === 0x5b\n ) {\n this._params.resetZdm();\n this._collect = 0;\n let k = i + 2;\n let ch = data[k];\n if (ch >= 0x3c && ch <= 0x3f) {\n this._collect = ch;\n k++;\n }\n let csiDone = false;\n for (; k < length; k++) {\n ch = data[k];\n if (ch >= 0x30 && ch <= 0x39) {\n this._params.addDigit(ch - 48);\n } else if (ch === 0x3b) {\n this._params.addParam(0);\n } else if (ch === 0x3a) {\n this._params.addSubParam(-1);\n } else if (ch >= 0x40 && ch <= 0x7e) {\n const handlers = this._csiHandlers[this._collect << 8 | ch];\n let j = handlers ? handlers.length - 1 : -1;\n for (; j >= 0; j--) {\n handlerResult = handlers[j](this._params);\n if (handlerResult === true) {\n break;\n } else if (handlerResult instanceof Promise) {\n transition = ParserAction.CSI_DISPATCH << TableAccess.TRANSITION_ACTION_SHIFT | ParserState.GROUND;\n this._preserveStack(ParserStackType.CSI, handlers, j, transition, k);\n return handlerResult;\n }\n }\n if (j < 0) {\n this._csiHandlerFb(this._collect << 8 | ch, this._params);\n }\n this.precedingJoinState = 0;\n i = k;\n this.currentState = ParserState.GROUND;\n csiDone = true;\n break;\n } else {\n break;\n }\n }\n if (!csiDone) {\n i = k - 1;\n this.currentState = ParserState.CSI_PARAM;\n }\n continue;\n }\n\n // normal transition & action lookup\n transition = this._transitions.table[\n this.currentState << TableAccess.INDEX_STATE_SHIFT |\n (code < NON_ASCII_PRINTABLE ? code : NON_ASCII_PRINTABLE)\n ];\n switch (transition >> TableAccess.TRANSITION_ACTION_SHIFT) {\n case ParserAction.PRINT:\n // Note: 0x20 (SP) is included, 0x7F (DEL) is excluded\n let c = i;\n const l4 = length - 4;\n while (c < l4\n && data[++c] >= 0x20 && (data[c] <= 0x7e || data[c] >= NON_ASCII_PRINTABLE)\n && data[++c] >= 0x20 && (data[c] <= 0x7e || data[c] >= NON_ASCII_PRINTABLE)\n && data[++c] >= 0x20 && (data[c] <= 0x7e || data[c] >= NON_ASCII_PRINTABLE)\n && data[++c] >= 0x20 && (data[c] <= 0x7e || data[c] >= NON_ASCII_PRINTABLE)\n ) {}\n if (c >= l4) {\n while (c < length && data[c] >= 0x20 && (data[c] <= 0x7e || data[c] >= NON_ASCII_PRINTABLE)) {\n c++;\n }\n }\n this._printHandler(data, i, c);\n i = c - 1;\n break;\n case ParserAction.EXECUTE:\n if (this._executeHandlers[code]) this._executeHandlers[code]();\n else this._executeHandlerFb(code);\n this.precedingJoinState = 0;\n break;\n case ParserAction.IGNORE:\n break;\n case ParserAction.ERROR:\n const inject: IParsingState = this._errorHandler(\n {\n position: i,\n code,\n currentState: this.currentState,\n collect: this._collect,\n params: this._params,\n abort: false\n });\n if (inject.abort) return;\n // inject values: currently not implemented\n break;\n case ParserAction.CSI_DISPATCH:\n // Trigger CSI Handler\n const handlers = this._csiHandlers[this._collect << 8 | code];\n let j = handlers ? handlers.length - 1 : -1;\n for (; j >= 0; j--) {\n // true means success and to stop bubbling\n // a promise indicates an async handler that needs to finish before progressing\n handlerResult = handlers[j](this._params);\n if (handlerResult === true) {\n break;\n } else if (handlerResult instanceof Promise) {\n this._preserveStack(ParserStackType.CSI, handlers, j, transition, i);\n return handlerResult;\n }\n }\n if (j < 0) {\n this._csiHandlerFb(this._collect << 8 | code, this._params);\n }\n this.precedingJoinState = 0;\n break;\n case ParserAction.PARAM:\n // inner loop: digits (0x30 - 0x39) and ; (0x3b) and : (0x3a)\n do {\n switch (code) {\n case 0x3b:\n this._params.addParam(0); // ZDM\n break;\n case 0x3a:\n this._params.addSubParam(-1);\n break;\n default: // 0x30 - 0x39\n this._params.addDigit(code - 48);\n }\n } while (++i < length && (code = data[i]) > 0x2f && code < 0x3c);\n i--;\n break;\n case ParserAction.COLLECT:\n this._collect <<= 8;\n this._collect |= code;\n break;\n case ParserAction.ESC_DISPATCH:\n const handlersEsc = this._escHandlers[this._collect << 8 | code];\n let jj = handlersEsc ? handlersEsc.length - 1 : -1;\n for (; jj >= 0; jj--) {\n // true means success and to stop bubbling\n // a promise indicates an async handler that needs to finish before progressing\n handlerResult = handlersEsc[jj]();\n if (handlerResult === true) {\n break;\n } else if (handlerResult instanceof Promise) {\n this._preserveStack(ParserStackType.ESC, handlersEsc, jj, transition, i);\n return handlerResult;\n }\n }\n if (jj < 0) {\n this._escHandlerFb(this._collect << 8 | code);\n }\n this.precedingJoinState = 0;\n break;\n case ParserAction.CLEAR:\n this._params.resetZdm();\n this._collect = 0;\n break;\n case ParserAction.DCS_HOOK:\n this._dcsParser.hook(this._collect << 8 | code, this._params);\n break;\n case ParserAction.DCS_PUT:\n // inner loop - exit DCS_PUT: 0x18, 0x1a, 0x1b, 0x7f, 0x80 - 0x9f\n // unhook triggered by: 0x1b, 0x9c (success) and 0x18, 0x1a (abort)\n for (let j = i + 1; ; ++j) {\n if (j >= length || (code = data[j]) === 0x18 || code === 0x1a || code === 0x1b || (code > 0x7f && code < NON_ASCII_PRINTABLE)) {\n this._dcsParser.put(data, i, j);\n i = j - 1;\n break;\n }\n }\n break;\n case ParserAction.DCS_UNHOOK:\n handlerResult = this._dcsParser.unhook(code !== 0x18 && code !== 0x1a);\n if (handlerResult) {\n this._preserveStack(ParserStackType.DCS, [], 0, transition, i);\n return handlerResult;\n }\n if (code === 0x1b) transition |= ParserState.ESCAPE;\n this._params.resetZdm();\n this._collect = 0;\n this.precedingJoinState = 0;\n break;\n case ParserAction.OSC_START:\n this._oscParser.start();\n break;\n case ParserAction.OSC_PUT:\n // inner loop: 0x20 (SP) included, 0x7F (DEL) included\n for (let j = i + 1; ; j++) {\n if (j >= length || (code = data[j]) < 0x20 || (code > 0x7f && code < NON_ASCII_PRINTABLE)) {\n this._oscParser.put(data, i, j);\n i = j - 1;\n break;\n }\n }\n break;\n case ParserAction.OSC_END:\n handlerResult = this._oscParser.end(code !== 0x18 && code !== 0x1a);\n if (handlerResult) {\n this._preserveStack(ParserStackType.OSC, [], 0, transition, i);\n return handlerResult;\n }\n if (code === 0x1b) transition |= ParserState.ESCAPE;\n this._params.resetZdm();\n this._collect = 0;\n this.precedingJoinState = 0;\n break;\n case ParserAction.APC_START:\n this._apcParser.start(this._collect << 8 | code);\n break;\n case ParserAction.APC_PUT:\n // inner loop - exit APC_PUT: 0x18, 0x1a, 0x1b, 0x9c\n // allowed: 00/08 .. 00/13, 02/00 .. 07/14 + NON_ASCII_PRINTABLE\n for (let j = i + 1; ; ++j) {\n if (j < length && (\n (data[j] >= 0x20 && data[j] < 0x7f) || (data[j] >= 0x08 && data[j] < 0x0e) || data[j] >= NON_ASCII_PRINTABLE\n )) continue;\n this._apcParser.put(data, i, j);\n i = j - 1;\n break;\n }\n break;\n case ParserAction.APC_END:\n handlerResult = this._apcParser.end(code !== 0x18 && code !== 0x1a);\n if (handlerResult) {\n this._preserveStack(ParserStackType.APC, [], 0, transition, i);\n return handlerResult;\n }\n if (code === 0x1b) transition |= ParserState.ESCAPE;\n this._params.resetZdm();\n this._collect = 0;\n this.precedingJoinState = 0;\n break;\n }\n this.currentState = transition & TableAccess.TRANSITION_STATE_MASK;\n }\n }\n}\n","/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { IOscHandler, IHandlerCollection, OscFallbackHandlerType, IOscParser, ISubParserStackState } from './Types';\nimport { OscState, ParserConstants } from './Constants';\nimport { utf32ToString } from '../input/TextDecoder';\nimport { IDisposable } from '../Types';\nimport { LimitedStringBuilder } from '../StringBuilder';\n\nconst EMPTY_HANDLERS: IOscHandler[] = [];\n\nexport class OscParser implements IOscParser {\n private _state = OscState.START;\n private _active = EMPTY_HANDLERS;\n private _id = -1;\n private _handlers: IHandlerCollection = Object.create(null);\n private _handlerFb: OscFallbackHandlerType = () => { };\n private _stack: ISubParserStackState = {\n paused: false,\n loopPosition: 0,\n fallThrough: false\n };\n\n public registerHandler(ident: number, handler: IOscHandler): IDisposable {\n this._handlers[ident] ??= [];\n const handlerList = this._handlers[ident];\n handlerList.push(handler);\n return {\n dispose: () => {\n const handlerIndex = handlerList.indexOf(handler);\n if (handlerIndex !== -1) {\n handlerList.splice(handlerIndex, 1);\n }\n }\n };\n }\n public clearHandler(ident: number): void {\n if (this._handlers[ident]) delete this._handlers[ident];\n }\n public setHandlerFallback(handler: OscFallbackHandlerType): void {\n this._handlerFb = handler;\n }\n\n public dispose(): void {\n this._handlers = Object.create(null);\n this._handlerFb = () => { };\n this._active = EMPTY_HANDLERS;\n }\n\n public reset(): void {\n // force cleanup handlers if payload was already sent\n if (this._state === OscState.PAYLOAD) {\n for (let j = this._stack.paused ? this._stack.loopPosition - 1 : this._active.length - 1; j >= 0; --j) {\n this._active[j].end(false);\n }\n }\n this._stack.paused = false;\n this._active = EMPTY_HANDLERS;\n this._id = -1;\n this._state = OscState.START;\n }\n\n private _start(): void {\n this._active = this._handlers[this._id] || EMPTY_HANDLERS;\n if (!this._active.length) {\n this._handlerFb(this._id, 'START');\n } else {\n for (let j = this._active.length - 1; j >= 0; j--) {\n this._active[j].start();\n }\n }\n }\n\n private _put(data: Uint32Array, start: number, end: number): void {\n if (!this._active.length) {\n this._handlerFb(this._id, 'PUT', utf32ToString(data, start, end));\n } else {\n for (let j = this._active.length - 1; j >= 0; j--) {\n this._active[j].put(data, start, end);\n }\n }\n }\n\n public start(): void {\n // always reset leftover handlers\n this.reset();\n this._state = OscState.ID;\n }\n\n /**\n * Put data to current OSC command.\n * Expects the identifier of the OSC command in the form\n * OSC id ; payload ST/BEL\n * Payload chunks are not further processed and get\n * directly passed to the handlers.\n */\n public put(data: Uint32Array, start: number, end: number): void {\n if (this._state === OscState.ABORT) {\n return;\n }\n if (this._state === OscState.ID) {\n while (start < end) {\n const code = data[start++];\n if (code === 0x3b) {\n this._state = OscState.PAYLOAD;\n this._start();\n break;\n }\n if (code < 0x30 || 0x39 < code) {\n this._state = OscState.ABORT;\n return;\n }\n if (this._id === -1) {\n this._id = 0;\n }\n this._id = this._id * 10 + code - 48;\n }\n }\n if (this._state === OscState.PAYLOAD && end - start > 0) {\n this._put(data, start, end);\n }\n }\n\n /**\n * Indicates end of an OSC command.\n * Whether the OSC got aborted or finished normally\n * is indicated by `success`.\n */\n public end(success: boolean, promiseResult: boolean = true): void | Promise {\n if (this._state === OscState.START) {\n return;\n }\n // do nothing if command was faulty\n if (this._state !== OscState.ABORT) {\n // if we are still in ID state and get an early end\n // means that the command has no payload thus we still have\n // to announce START and send END right after\n if (this._state === OscState.ID) {\n this._start();\n }\n\n if (!this._active.length) {\n this._handlerFb(this._id, 'END', success);\n } else {\n let handlerResult: boolean | Promise = false;\n let j = this._active.length - 1;\n let fallThrough = false;\n if (this._stack.paused) {\n j = this._stack.loopPosition - 1;\n handlerResult = promiseResult;\n fallThrough = this._stack.fallThrough;\n this._stack.paused = false;\n }\n if (!fallThrough && handlerResult === false) {\n for (; j >= 0; j--) {\n handlerResult = this._active[j].end(success);\n if (handlerResult === true) {\n break;\n } else if (handlerResult instanceof Promise) {\n this._stack.paused = true;\n this._stack.loopPosition = j;\n this._stack.fallThrough = false;\n return handlerResult;\n }\n }\n j--;\n }\n // cleanup left over handlers\n // we always have to call .end for proper cleanup,\n // here we use `success` to indicate whether a handler should execute\n for (; j >= 0; j--) {\n handlerResult = this._active[j].end(false);\n if (handlerResult instanceof Promise) {\n this._stack.paused = true;\n this._stack.loopPosition = j;\n this._stack.fallThrough = true;\n return handlerResult;\n }\n }\n }\n\n }\n this._active = EMPTY_HANDLERS;\n this._id = -1;\n this._state = OscState.START;\n }\n}\n\n/**\n * Convenient class to allow attaching string based handler functions\n * as OSC handlers.\n */\nexport class OscHandler implements IOscHandler {\n private static _payloadLimit = ParserConstants.PAYLOAD_LIMIT;\n\n private _data = new LimitedStringBuilder(OscHandler._payloadLimit);\n private _hitLimit: boolean = false;\n\n constructor(private _handler: (data: string) => boolean | Promise) { }\n\n public start(): void {\n this._data.reset();\n this._hitLimit = false;\n }\n\n public put(data: Uint32Array, start: number, end: number): void {\n if (this._hitLimit) {\n return;\n }\n if (this._data.append(utf32ToString(data, start, end))) {\n this._hitLimit = true;\n }\n }\n\n public end(success: boolean): boolean | Promise {\n let ret: boolean | Promise = false;\n if (this._hitLimit) {\n ret = false;\n } else if (success) {\n ret = this._handler(this._data.toString());\n if (ret instanceof Promise) {\n // need to hold data until `ret` got resolved\n // dont care for errors, data will be freed anyway on next start\n return ret.then(res => {\n this._data.reset();\n this._hitLimit = false;\n return res;\n });\n }\n }\n this._data.reset();\n this._hitLimit = false;\n return ret;\n }\n}\n","/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\nimport { IParams, ParamsArray } from './Types';\n\nconst enum Constants {\n /**\n * Max value supported for a single param/subparam (clamped to positive int32 range)\n */\n MAX_VALUE = 0x7FFFFFFF,\n /**\n * Max allowed subparams for a single sequence (hardcoded limitation)\n */\n MAX_SUBPARAMS = 256\n}\n\n/**\n * Params storage class.\n * This type is used by the parser to accumulate sequence parameters and sub parameters\n * and transmit them to the input handler actions.\n *\n * NOTES:\n * - params object for action handlers is borrowed, use `.toArray` or `.clone` to get a copy\n * - never read beyond `params.length - 1` (likely to contain arbitrary data)\n * - `.getSubParams` returns a borrowed typed array, use `.getSubParamsAll` for cloned sub params\n * - hardcoded limitations:\n * - max. value for a single (sub) param is 2^31 - 1 (greater values are clamped to that)\n * - max. 256 sub params possible\n * - negative values are not allowed beside -1 (placeholder for default value)\n *\n * About ZDM (Zero Default Mode):\n * ZDM is not orchestrated by this class. If the parser is in ZDM,\n * it should add 0 for empty params, otherwise -1. This does not apply\n * to subparams, empty subparams should always be added with -1.\n */\nexport class Params implements IParams {\n // params store and length\n public params: Int32Array;\n public length: number;\n\n // sub params store and length\n protected _subParams: Int32Array;\n protected _subParamsLength: number;\n\n // sub params offsets from param: param idx --> [start, end] offset\n private _subParamsIdx: Uint16Array;\n private _rejectDigits: boolean;\n private _rejectSubDigits: boolean;\n private _digitIsSub: boolean;\n\n /**\n * Create a `Params` type from JS array representation.\n */\n public static fromArray(values: ParamsArray): Params {\n const params = new Params();\n if (!values.length) {\n return params;\n }\n // skip leading sub params\n for (let i = (Array.isArray(values[0])) ? 1 : 0; i < values.length; ++i) {\n const value = values[i];\n if (Array.isArray(value)) {\n for (let k = 0; k < value.length; ++k) {\n params.addSubParam(value[k]);\n }\n } else {\n params.addParam(value);\n }\n }\n return params;\n }\n\n /**\n * @param maxLength max length of storable parameters\n * @param maxSubParamsLength max length of storable sub parameters\n */\n constructor(public maxLength: number = 32, public maxSubParamsLength: number = 32) {\n if (maxSubParamsLength > Constants.MAX_SUBPARAMS) {\n throw new Error('maxSubParamsLength must not be greater than 256');\n }\n this.params = new Int32Array(maxLength);\n this.length = 0;\n this._subParams = new Int32Array(maxSubParamsLength);\n this._subParamsLength = 0;\n this._subParamsIdx = new Uint16Array(maxLength);\n this._rejectDigits = false;\n this._rejectSubDigits = false;\n this._digitIsSub = false;\n }\n\n /**\n * Clone object.\n */\n public clone(): Params {\n const newParams = new Params(this.maxLength, this.maxSubParamsLength);\n newParams.params.set(this.params);\n newParams.length = this.length;\n newParams._subParams.set(this._subParams);\n newParams._subParamsLength = this._subParamsLength;\n newParams._subParamsIdx.set(this._subParamsIdx);\n newParams._rejectDigits = this._rejectDigits;\n newParams._rejectSubDigits = this._rejectSubDigits;\n newParams._digitIsSub = this._digitIsSub;\n return newParams;\n }\n\n /**\n * Get a JS array representation of the current parameters and sub parameters.\n * The array is structured as follows:\n * sequence: \"1;2:3:4;5::6\"\n * array : [1, 2, [3, 4], 5, [-1, 6]]\n */\n public toArray(): ParamsArray {\n const res: ParamsArray = [];\n for (let i = 0; i < this.length; ++i) {\n res.push(this.params[i]);\n const start = this._subParamsIdx[i] >> 8;\n const end = this._subParamsIdx[i] & 0xFF;\n if (end - start > 0) {\n res.push(Array.prototype.slice.call(this._subParams, start, end));\n }\n }\n return res;\n }\n\n /**\n * Reset to initial empty state.\n */\n public reset(): void {\n this.length = 0;\n this._subParamsLength = 0;\n this._rejectDigits = false;\n this._rejectSubDigits = false;\n this._digitIsSub = false;\n }\n\n /**\n * Reset and add 0 as first param (ZDM).\n */\n public resetZdm(): void {\n this.length = 1;\n this._subParamsLength = 0;\n this._rejectDigits = false;\n this._rejectSubDigits = false;\n this._digitIsSub = false;\n this._subParamsIdx[0] = 0;\n this.params[0] = 0;\n }\n\n /**\n * Add a parameter value.\n * `Params` only stores up to `maxLength` parameters, any later\n * parameter will be ignored.\n * Note: VT devices only stored up to 16 values, xterm seems to\n * store up to 30.\n */\n public addParam(value: number): void {\n this._digitIsSub = false;\n if (this.length >= this.maxLength) {\n this._rejectDigits = true;\n return;\n }\n if (value < -1) {\n throw new Error('values less than -1 are not allowed');\n }\n this._subParamsIdx[this.length] = this._subParamsLength << 8 | this._subParamsLength;\n this.params[this.length++] = value > Constants.MAX_VALUE ? Constants.MAX_VALUE : value;\n }\n\n /**\n * Add a sub parameter value.\n * The sub parameter is automatically associated with the last parameter value.\n * Thus it is not possible to add a subparameter without any parameter added yet.\n * `Params` only stores up to `maxSubParamsLength` sub parameters, any later\n * sub parameter will be ignored.\n */\n public addSubParam(value: number): void {\n this._digitIsSub = true;\n if (!this.length) {\n return;\n }\n if (this._rejectDigits || this._subParamsLength >= this.maxSubParamsLength) {\n this._rejectSubDigits = true;\n return;\n }\n if (value < -1) {\n throw new Error('values less than -1 are not allowed');\n }\n this._subParams[this._subParamsLength++] = value > Constants.MAX_VALUE ? Constants.MAX_VALUE : value;\n this._subParamsIdx[this.length - 1]++;\n }\n\n /**\n * Whether parameter at index `idx` has sub parameters.\n */\n public hasSubParams(idx: number): boolean {\n return ((this._subParamsIdx[idx] & 0xFF) - (this._subParamsIdx[idx] >> 8) > 0);\n }\n\n /**\n * Return sub parameters for parameter at index `idx`.\n * Note: The values are borrowed, thus you need to copy\n * the values if you need to hold them in nonlocal scope.\n */\n public getSubParams(idx: number): Int32Array | null {\n const start = this._subParamsIdx[idx] >> 8;\n const end = this._subParamsIdx[idx] & 0xFF;\n if (end - start > 0) {\n return this._subParams.subarray(start, end);\n }\n return null;\n }\n\n /**\n * Return all sub parameters as {idx: subparams} mapping.\n * Note: The values are not borrowed.\n */\n public getSubParamsAll(): {[idx: number]: Int32Array} {\n const result: {[idx: number]: Int32Array} = {};\n for (let i = 0; i < this.length; ++i) {\n const start = this._subParamsIdx[i] >> 8;\n const end = this._subParamsIdx[i] & 0xFF;\n if (end - start > 0) {\n result[i] = this._subParams.slice(start, end);\n }\n }\n return result;\n }\n\n /**\n * Add a single digit value to current parameter.\n * This is used by the parser to account digits on a char by char basis.\n */\n public addDigit(value: number): void {\n let length;\n if (this._rejectDigits\n || !(length = this._digitIsSub ? this._subParamsLength : this.length)\n || (this._digitIsSub && this._rejectSubDigits)\n ) {\n return;\n }\n\n const store = this._digitIsSub ? this._subParams : this.params;\n const cur = store[length - 1];\n store[length - 1] = ~cur ? Math.min(cur * 10 + value, Constants.MAX_VALUE) : value;\n }\n}\n","/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { ITerminalAddon, IDisposable, Terminal } from '@xterm/xterm';\n\nexport interface ILoadedAddon {\n instance: ITerminalAddon;\n dispose: () => void;\n isDisposed: boolean;\n}\n\nexport class AddonManager implements IDisposable {\n protected _addons: ILoadedAddon[] = [];\n\n public dispose(): void {\n for (let i = this._addons.length - 1; i >= 0; i--) {\n this._addons[i].instance.dispose();\n }\n }\n\n public loadAddon(terminal: Terminal, instance: ITerminalAddon): void {\n const loadedAddon: ILoadedAddon = {\n instance,\n dispose: instance.dispose,\n isDisposed: false\n };\n this._addons.push(loadedAddon);\n instance.dispose = () => this._wrappedAddonDispose(loadedAddon);\n instance.activate(terminal as any);\n }\n\n private _wrappedAddonDispose(loadedAddon: ILoadedAddon): void {\n if (loadedAddon.isDisposed) {\n // Do nothing if already disposed\n return;\n }\n let index = -1;\n for (let i = 0; i < this._addons.length; i++) {\n if (this._addons[i] === loadedAddon) {\n index = i;\n break;\n }\n }\n if (index === -1) {\n throw new Error('Could not dispose an addon that has not been loaded');\n }\n loadedAddon.isDisposed = true;\n loadedAddon.dispose.apply(loadedAddon.instance);\n this._addons.splice(index, 1);\n }\n}\n","/**\n * Copyright (c) 2021 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { IBuffer as IBufferApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi } from '@xterm/xterm';\nimport { IBuffer } from '../buffer/Types';\nimport { BufferLineApiView } from './BufferLineApiView';\nimport { CellData } from '../buffer/CellData';\n\nexport class BufferApiView implements IBufferApi {\n constructor(\n private _buffer: IBuffer,\n public readonly type: 'normal' | 'alternate'\n ) { }\n\n public init(buffer: IBuffer): BufferApiView {\n this._buffer = buffer;\n return this;\n }\n\n public get cursorY(): number { return this._buffer.y; }\n public get cursorX(): number { return this._buffer.x; }\n public get viewportY(): number { return this._buffer.ydisp; }\n public get baseY(): number { return this._buffer.ybase; }\n public get length(): number { return this._buffer.lines.length; }\n public getLine(y: number): IBufferLineApi | undefined {\n const line = this._buffer.lines.get(y);\n if (!line) {\n return undefined;\n }\n return new BufferLineApiView(line);\n }\n public getNullCell(): IBufferCellApi { return new CellData(); }\n}\n","/**\n * Copyright (c) 2021 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { CellData } from '../buffer/CellData';\nimport { IBufferLine, ICellData } from '../buffer/Types';\nimport { IBufferCell as IBufferCellApi, IBufferLine as IBufferLineApi } from '@xterm/xterm';\n\nexport class BufferLineApiView implements IBufferLineApi {\n constructor(private _line: IBufferLine) { }\n\n public get isWrapped(): boolean { return this._line.isWrapped; }\n public get length(): number { return this._line.length; }\n public getCell(x: number, cell?: IBufferCellApi): IBufferCellApi | undefined {\n if (x < 0 || x >= this._line.length) {\n return undefined;\n }\n\n if (cell) {\n this._line.loadCell(x, cell as unknown as ICellData);\n return cell;\n }\n return this._line.loadCell(x, new CellData()) as unknown as IBufferCellApi;\n }\n public translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): string {\n return this._line.translateToString(trimRight, startColumn, endColumn);\n }\n}\n","/**\n * Copyright (c) 2021 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { IBuffer as IBufferApi, IBufferNamespace as IBufferNamespaceApi } from '@xterm/xterm';\nimport { BufferApiView } from './BufferApiView';\nimport { ICoreTerminal } from '../CoreTerminal';\nimport { Disposable } from '../Lifecycle';\nimport { Emitter } from '../Event';\n\nexport class BufferNamespaceApi extends Disposable implements IBufferNamespaceApi {\n private _normal: BufferApiView;\n private _alternate: BufferApiView;\n\n private readonly _onBufferChange = this._register(new Emitter());\n public readonly onBufferChange = this._onBufferChange.event;\n\n constructor(private _core: ICoreTerminal) {\n super();\n this._normal = new BufferApiView(this._core.buffers.normal, 'normal');\n this._alternate = new BufferApiView(this._core.buffers.alt, 'alternate');\n this._register(this._core.buffers.onBufferActivate(() => this._onBufferChange.fire(this.active)));\n }\n public get active(): IBufferApi {\n if (this._core.buffers.active === this._core.buffers.normal) { return this.normal; }\n if (this._core.buffers.active === this._core.buffers.alt) { return this.alternate; }\n throw new Error('Active buffer is neither normal nor alternate');\n }\n public get normal(): IBufferApi {\n return this._normal.init(this._core.buffers.normal);\n }\n public get alternate(): IBufferApi {\n return this._alternate.init(this._core.buffers.alt);\n }\n}\n","/**\n * Copyright (c) 2021 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { IParams } from '../parser/Types';\nimport { IDisposable, IFunctionIdentifier, IParser } from '@xterm/xterm';\nimport { ICoreTerminal } from '../CoreTerminal';\n\nexport class ParserApi implements IParser {\n constructor(private _core: ICoreTerminal) { }\n\n public registerCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean | Promise): IDisposable {\n return this._core.registerCsiHandler(id, (params: IParams) => callback(params.toArray()));\n }\n public addCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean | Promise): IDisposable {\n return this.registerCsiHandler(id, callback);\n }\n public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean | Promise): IDisposable {\n return this._core.registerDcsHandler(id, (data: string, params: IParams) => callback(data, params.toArray()));\n }\n public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean | Promise): IDisposable {\n return this.registerDcsHandler(id, callback);\n }\n public registerEscHandler(id: IFunctionIdentifier, handler: () => boolean | Promise): IDisposable {\n return this._core.registerEscHandler(id, handler);\n }\n public addEscHandler(id: IFunctionIdentifier, handler: () => boolean | Promise): IDisposable {\n return this.registerEscHandler(id, handler);\n }\n public registerOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable {\n return this._core.registerOscHandler(ident, callback);\n }\n public addOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable {\n return this.registerOscHandler(ident, callback);\n }\n public registerApcHandler(id: IFunctionIdentifier, callback: (data: string) => boolean | Promise): IDisposable {\n return this._core.registerApcHandler(id, callback);\n }\n}\n","/**\n * Copyright (c) 2021 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { ICoreTerminal } from '../CoreTerminal';\nimport { IUnicodeHandling, IUnicodeVersionProvider } from '@xterm/xterm';\n\nexport class UnicodeApi implements IUnicodeHandling {\n constructor(private _core: ICoreTerminal) { }\n\n public register(provider: IUnicodeVersionProvider): void {\n this._core.unicodeService.register(provider);\n }\n\n public get versions(): string[] {\n return this._core.unicodeService.versions;\n }\n\n public get activeVersion(): string {\n return this._core.unicodeService.activeVersion;\n }\n\n public set activeVersion(version: string) {\n this._core.unicodeService.activeVersion = version;\n }\n}\n","/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { Disposable } from '../Lifecycle';\nimport { IAttributeData, IBuffer, IBufferLine, IBufferSet } from '../buffer/Types';\nimport { BufferSet } from '../buffer/BufferSet';\nimport { IBufferService, ILogService, IOptionsService, type IBufferResizeEvent } from './Services';\nimport { Emitter } from '../Event';\n\nexport const enum BufferServiceConstants {\n MINIMUM_COLS = 2, // Less than 2 can mess with wide chars\n MINIMUM_ROWS = 1\n}\n\nexport class BufferService extends Disposable implements IBufferService {\n public serviceBrand: any;\n\n public cols: number;\n public rows: number;\n public buffers: IBufferSet;\n /** Whether the user is scrolling (locks the scroll position) */\n public isUserScrolling: boolean = false;\n\n private readonly _onResize = this._register(new Emitter());\n public readonly onResize = this._onResize.event;\n private readonly _onScroll = this._register(new Emitter());\n public readonly onScroll = this._onScroll.event;\n\n public get buffer(): IBuffer { return this.buffers.active; }\n\n /** An IBufferline to clone/copy from for new blank lines */\n private _cachedBlankLine: IBufferLine | undefined;\n\n constructor(\n @IOptionsService optionsService: IOptionsService,\n @ILogService logService: ILogService\n ) {\n super();\n this.cols = Math.max(optionsService.rawOptions.cols || 0, BufferServiceConstants.MINIMUM_COLS);\n this.rows = Math.max(optionsService.rawOptions.rows || 0, BufferServiceConstants.MINIMUM_ROWS);\n this.buffers = this._register(new BufferSet(optionsService, this, logService));\n this._register(this.buffers.onBufferActivate(e => {\n this._onScroll.fire(e.activeBuffer.ydisp);\n }));\n }\n\n public resize(cols: number, rows: number): void {\n const colsChanged = this.cols !== cols;\n const rowsChanged = this.rows !== rows;\n this.cols = cols;\n this.rows = rows;\n this.buffers.resize(cols, rows);\n this._onResize.fire({ cols, rows, colsChanged, rowsChanged });\n }\n\n public reset(): void {\n this.buffers.reset();\n this.isUserScrolling = false;\n }\n\n /**\n * Scroll the terminal down 1 row, creating a blank line.\n * @param eraseAttr The attribute data to use the for blank line.\n * @param isWrapped Whether the new line is wrapped from the previous line.\n */\n public scroll(eraseAttr: IAttributeData, isWrapped: boolean = false): void {\n const buffer = this.buffer;\n\n let newLine: IBufferLine | undefined;\n newLine = this._cachedBlankLine;\n if (!newLine || newLine.length !== this.cols || newLine.getFg(0) !== eraseAttr.fg || newLine.getBg(0) !== eraseAttr.bg) {\n newLine = buffer.getBlankLine(eraseAttr, isWrapped);\n this._cachedBlankLine = newLine;\n }\n newLine.isWrapped = isWrapped;\n\n const topRow = buffer.ybase + buffer.scrollTop;\n const bottomRow = buffer.ybase + buffer.scrollBottom;\n\n if (buffer.scrollTop === 0) {\n // Determine whether the buffer is going to be trimmed after insertion.\n const willBufferBeTrimmed = buffer.lines.isFull;\n\n // Insert the line using the fastest method\n if (bottomRow === buffer.lines.length - 1) {\n if (willBufferBeTrimmed) {\n buffer.lines.recycle().copyFrom(newLine, true);\n } else {\n buffer.lines.push(newLine.clone(true));\n }\n } else {\n buffer.lines.splice(bottomRow + 1, 0, newLine.clone(true));\n }\n\n // Only adjust ybase and ydisp when the buffer is not trimmed\n if (!willBufferBeTrimmed) {\n buffer.ybase++;\n // Only scroll the ydisp with ybase if the user has not scrolled up\n if (!this.isUserScrolling) {\n buffer.ydisp++;\n }\n } else {\n // When the buffer is full and the user has scrolled up, keep the text\n // stable unless ydisp is right at the top\n if (this.isUserScrolling) {\n buffer.ydisp = Math.max(buffer.ydisp - 1, 0);\n }\n }\n } else {\n // scrollTop is non-zero which means no line will be going to the\n // scrollback, instead we can just shift them in-place.\n const scrollRegionHeight = bottomRow - topRow + 1 /* as it's zero-based */;\n buffer.lines.shiftElements(topRow + 1, scrollRegionHeight - 1, -1);\n buffer.lines.set(bottomRow, newLine.clone(true));\n }\n\n // Move the viewport to the bottom of the buffer unless the user is\n // scrolling.\n if (!this.isUserScrolling) {\n buffer.ydisp = buffer.ybase;\n }\n\n this._onScroll.fire(buffer.ydisp);\n }\n\n /**\n * Scroll the display of the terminal\n * @param disp The number of lines to scroll down (negative scroll up).\n * @param suppressScrollEvent Don't emit the scroll event as scrollLines. This is used\n * to avoid unwanted events being handled by the viewport when the event was triggered from the\n * viewport originally.\n */\n public scrollLines(disp: number, suppressScrollEvent?: boolean): void {\n const buffer = this.buffer;\n if (disp < 0) {\n if (buffer.ydisp === 0) {\n return;\n }\n this.isUserScrolling = true;\n } else if (disp + buffer.ydisp >= buffer.ybase) {\n this.isUserScrolling = false;\n }\n\n const oldYdisp = buffer.ydisp;\n buffer.ydisp = Math.max(Math.min(buffer.ydisp + disp, buffer.ybase), 0);\n\n // No change occurred, don't trigger scroll/refresh\n if (oldYdisp === buffer.ydisp) {\n return;\n }\n\n if (!suppressScrollEvent) {\n this._onScroll.fire(buffer.ydisp);\n }\n }\n}\n","/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { ICharsetService } from './Services';\nimport { ICharset } from '../Types';\n\nexport class CharsetService implements ICharsetService {\n public serviceBrand: any;\n\n public charset: ICharset | undefined;\n public glevel: number = 0;\n\n private _charsets: (ICharset | undefined)[] = [];\n\n public get charsets(): (ICharset | undefined)[] {\n return this._charsets;\n }\n\n public reset(): void {\n this.charset = undefined;\n this._charsets = [];\n this.glevel = 0;\n }\n\n public setgLevel(g: number): void {\n this.glevel = g;\n this.charset = this._charsets[g];\n }\n\n public setgCharset(g: number, charset: ICharset | undefined): void {\n this._charsets[g] = charset;\n if (this.glevel === g) {\n this.charset = charset;\n }\n }\n}\n","/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { Disposable } from '../Lifecycle';\nimport { IDecPrivateModes, IKittyKeyboardState, IModes } from '../Types';\nimport { IBufferService, ICoreService, ILogService, IOptionsService } from './Services';\nimport { Emitter } from '../Event';\n\nconst DEFAULT_MODES: IModes = Object.freeze({\n insertMode: false\n});\n\nconst DEFAULT_DEC_PRIVATE_MODES: IDecPrivateModes = Object.freeze({\n applicationCursorKeys: false,\n applicationKeypad: false,\n bracketedPasteMode: false,\n colorSchemeUpdates: false,\n cursorBlink: undefined,\n cursorStyle: undefined,\n origin: false,\n reverseWraparound: false,\n sendFocus: false,\n synchronizedOutput: false,\n win32InputMode: false,\n wraparound: true // defaults: xterm - true, vt100 - false\n});\n\nconst DEFAULT_KITTY_KEYBOARD_STATE = (): IKittyKeyboardState => ({\n flags: 0,\n mainFlags: 0,\n altFlags: 0,\n mainStack: [],\n altStack: []\n});\n\nexport class CoreService extends Disposable implements ICoreService {\n public serviceBrand: any;\n\n public isCursorInitialized: boolean;\n public isCursorHidden: boolean = false;\n public modes: IModes;\n public decPrivateModes: IDecPrivateModes;\n public kittyKeyboard: IKittyKeyboardState;\n\n private readonly _onData = this._register(new Emitter());\n public readonly onData = this._onData.event;\n private readonly _onUserInput = this._register(new Emitter());\n public readonly onUserInput = this._onUserInput.event;\n private readonly _onBinary = this._register(new Emitter());\n public readonly onBinary = this._onBinary.event;\n private readonly _onRequestScrollToBottom = this._register(new Emitter());\n public readonly onRequestScrollToBottom = this._onRequestScrollToBottom.event;\n\n constructor(\n @IBufferService private readonly _bufferService: IBufferService,\n @ILogService private readonly _logService: ILogService,\n @IOptionsService private readonly _optionsService: IOptionsService\n ) {\n super();\n this.isCursorInitialized = _optionsService.rawOptions.showCursorImmediately ?? false;\n this.modes = structuredClone(DEFAULT_MODES);\n this.decPrivateModes = structuredClone(DEFAULT_DEC_PRIVATE_MODES);\n this.kittyKeyboard = DEFAULT_KITTY_KEYBOARD_STATE();\n }\n\n public reset(): void {\n this.modes = structuredClone(DEFAULT_MODES);\n this.decPrivateModes = structuredClone(DEFAULT_DEC_PRIVATE_MODES);\n this.kittyKeyboard = DEFAULT_KITTY_KEYBOARD_STATE();\n }\n\n public triggerDataEvent(data: string, wasUserInput: boolean = false): void {\n // Prevents all events to pty process if stdin is disabled\n if (this._optionsService.rawOptions.disableStdin) {\n return;\n }\n\n // Input is being sent to the terminal, the terminal should focus the prompt.\n const buffer = this._bufferService.buffer;\n if (wasUserInput && this._optionsService.rawOptions.scrollOnUserInput && buffer.ybase !== buffer.ydisp) {\n this._onRequestScrollToBottom.fire();\n }\n\n // Fire onUserInput so listeners can react as well (eg. clear selection)\n if (wasUserInput) {\n this._onUserInput.fire();\n }\n\n // Fire onData API\n this._logService.debug(`sending data \"${data}\"`);\n this._logService.trace(`sending data (codes)`, () => data.split('').map(e => e.charCodeAt(0)));\n this._onData.fire(data);\n }\n\n public triggerBinaryEvent(data: string): void {\n if (this._optionsService.rawOptions.disableStdin) {\n return;\n }\n this._logService.debug(`sending binary \"${data}\"`);\n this._logService.trace(`sending binary (codes)`, () => data.split('').map(e => e.charCodeAt(0)));\n this._onBinary.fire(data);\n }\n}\n","/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n *\n * This was heavily inspired from microsoft/vscode's dependency injection system (MIT).\n */\n/*---------------------------------------------------------------------------------------------\n * Copyright (c) Microsoft Corporation. All rights reserved.\n * Licensed under the MIT License. See License.txt in the project root for license information.\n *--------------------------------------------------------------------------------------------*/\n\nimport { IInstantiationService } from './Services';\nimport { IServiceIdentifier, getServiceDependencies } from './ServiceRegistry';\n\nexport class ServiceCollection {\n\n private _entries = new Map, any>();\n\n constructor(...entries: [IServiceIdentifier, any][]) {\n for (const [id, service] of entries) {\n this.set(id, service);\n }\n }\n\n public set(id: IServiceIdentifier, instance: T): T {\n const result = this._entries.get(id);\n this._entries.set(id, instance);\n return result;\n }\n\n public forEach(callback: (id: IServiceIdentifier, instance: any) => any): void {\n for (const [key, value] of this._entries.entries()) {\n callback(key, value);\n }\n }\n\n public has(id: IServiceIdentifier): boolean {\n return this._entries.has(id);\n }\n\n public get(id: IServiceIdentifier): T | undefined {\n return this._entries.get(id);\n }\n}\n\nexport class InstantiationService implements IInstantiationService {\n public serviceBrand: undefined;\n\n private readonly _services: ServiceCollection = new ServiceCollection();\n\n constructor() {\n this._services.set(IInstantiationService, this);\n }\n\n public setService(id: IServiceIdentifier, instance: T): void {\n this._services.set(id, instance);\n }\n\n public getService(id: IServiceIdentifier): T | undefined {\n return this._services.get(id);\n }\n\n public createInstance(ctor: any, ...args: any[]): T {\n const serviceDependencies = getServiceDependencies(ctor).sort((a, b) => a.index - b.index);\n\n const serviceArgs: any[] = [];\n for (const dependency of serviceDependencies) {\n const service = this._services.get(dependency.id);\n if (!service) {\n throw new Error(`[createInstance] ${ctor.name} depends on UNKNOWN service ${dependency.id._id}.`);\n }\n serviceArgs.push(service);\n }\n\n const firstServiceArgPos = serviceDependencies.length > 0 ? serviceDependencies[0].index : args.length;\n\n // check for argument mismatches, adjust static args if needed\n if (args.length !== firstServiceArgPos) {\n throw new Error(`[createInstance] First service dependency of ${ctor.name} at position ${firstServiceArgPos + 1} conflicts with ${args.length} static arguments`);\n }\n\n // now create the instance\n return new ctor(...[...args, ...serviceArgs]);\n }\n}\n","/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { Disposable } from '../Lifecycle';\nimport { ILogService, IOptionsService, LogLevelEnum } from './Services';\n\ntype LogType = (message?: any, ...optionalParams: any[]) => void;\n\ninterface IConsole {\n log: LogType;\n error: LogType;\n info: LogType;\n trace: LogType;\n warn: LogType;\n}\n\n// console is available on both node.js and browser contexts but the common\n// module doesn't depend on them so we need to explicitly declare it.\ndeclare const console: IConsole;\n\nconst optionsKeyToLogLevel: { [key: string]: LogLevelEnum } = {\n trace: LogLevelEnum.TRACE,\n debug: LogLevelEnum.DEBUG,\n info: LogLevelEnum.INFO,\n warn: LogLevelEnum.WARN,\n error: LogLevelEnum.ERROR,\n off: LogLevelEnum.OFF\n};\n\nconst LOG_PREFIX = 'xterm.js: ';\n\nexport class LogService extends Disposable implements ILogService {\n public serviceBrand: any;\n\n private _logLevel: LogLevelEnum = LogLevelEnum.OFF;\n public get logLevel(): LogLevelEnum { return this._logLevel; }\n\n constructor(\n @IOptionsService private readonly _optionsService: IOptionsService\n ) {\n super();\n this._updateLogLevel();\n this._register(this._optionsService.onSpecificOptionChange('logLevel', () => this._updateLogLevel()));\n }\n\n private _updateLogLevel(): void {\n this._logLevel = optionsKeyToLogLevel[this._optionsService.rawOptions.logLevel];\n }\n\n private _evalLazyOptionalParams(optionalParams: any[]): void {\n for (let i = 0; i < optionalParams.length; i++) {\n if (typeof optionalParams[i] === 'function') {\n optionalParams[i] = optionalParams[i]();\n }\n }\n }\n\n private _log(type: LogType, message: string, optionalParams: any[]): void {\n this._evalLazyOptionalParams(optionalParams);\n type.call(console, (this._optionsService.options.logger ? '' : LOG_PREFIX) + message, ...optionalParams);\n }\n\n public trace(message: string, ...optionalParams: any[]): void {\n if (this._logLevel <= LogLevelEnum.TRACE) {\n this._log(this._optionsService.options.logger?.trace.bind(this._optionsService.options.logger) ?? console.log, message, optionalParams);\n }\n }\n\n public debug(message: string, ...optionalParams: any[]): void {\n if (this._logLevel <= LogLevelEnum.DEBUG) {\n this._log(this._optionsService.options.logger?.debug.bind(this._optionsService.options.logger) ?? console.log, message, optionalParams);\n }\n }\n\n public info(message: string, ...optionalParams: any[]): void {\n if (this._logLevel <= LogLevelEnum.INFO) {\n this._log(this._optionsService.options.logger?.info.bind(this._optionsService.options.logger) ?? console.info, message, optionalParams);\n }\n }\n\n public warn(message: string, ...optionalParams: any[]): void {\n if (this._logLevel <= LogLevelEnum.WARN) {\n this._log(this._optionsService.options.logger?.warn.bind(this._optionsService.options.logger) ?? console.warn, message, optionalParams);\n }\n }\n\n public error(message: string, ...optionalParams: any[]): void {\n if (this._logLevel <= LogLevelEnum.ERROR) {\n this._log(this._optionsService.options.logger?.error.bind(this._optionsService.options.logger) ?? console.error, message, optionalParams);\n }\n }\n}\n","/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\nimport { IMouseStateService } from './Services';\nimport { ICoreMouseProtocol, ICoreMouseEvent, CoreMouseEncoding, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from '../Types';\nimport { Disposable } from '../Lifecycle';\nimport { Emitter } from '../Event';\n\n/**\n * Supported default protocols.\n */\nconst DEFAULT_PROTOCOLS: { [key: string]: ICoreMouseProtocol } = {\n /**\n * NONE\n * Events: none\n * Modifiers: none\n */\n NONE: {\n events: CoreMouseEventType.NONE,\n restrict: () => false\n },\n /**\n * X10\n * Events: mousedown\n * Modifiers: none\n */\n X10: {\n events: CoreMouseEventType.DOWN,\n restrict: (e: ICoreMouseEvent) => {\n // no wheel, no move, no up\n if (e.button === CoreMouseButton.WHEEL || e.action !== CoreMouseAction.DOWN) {\n return false;\n }\n // no modifiers\n e.ctrl = false;\n e.alt = false;\n e.shift = false;\n return true;\n }\n },\n /**\n * VT200\n * Events: mousedown / mouseup / wheel\n * Modifiers: all\n */\n VT200: {\n events: CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL,\n restrict: (e: ICoreMouseEvent) => {\n // no move\n if (e.action === CoreMouseAction.MOVE) {\n return false;\n }\n return true;\n }\n },\n /**\n * DRAG\n * Events: mousedown / mouseup / wheel / mousedrag\n * Modifiers: all\n */\n DRAG: {\n events: CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL | CoreMouseEventType.DRAG,\n restrict: (e: ICoreMouseEvent) => {\n // no move without button\n if (e.action === CoreMouseAction.MOVE && e.button === CoreMouseButton.NONE) {\n return false;\n }\n return true;\n }\n },\n /**\n * ANY\n * Events: all mouse related events\n * Modifiers: all\n */\n ANY: {\n events:\n CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL\n | CoreMouseEventType.DRAG | CoreMouseEventType.MOVE,\n restrict: (e: ICoreMouseEvent) => true\n }\n};\n\nconst enum Modifiers {\n SHIFT = 4,\n ALT = 8,\n CTRL = 16\n}\n\n// helper for default encoders to generate the event code.\nfunction eventCode(e: ICoreMouseEvent, isSGR: boolean): number {\n let code = (e.ctrl ? Modifiers.CTRL : 0) | (e.shift ? Modifiers.SHIFT : 0) | (e.alt ? Modifiers.ALT : 0);\n if (e.button === CoreMouseButton.WHEEL) {\n code |= 64;\n code |= e.action;\n } else {\n code |= e.button & 3;\n if (e.button & 4) {\n code |= 64;\n }\n if (e.button & 8) {\n code |= 128;\n }\n if (e.action === CoreMouseAction.MOVE) {\n code |= CoreMouseAction.MOVE;\n } else if (e.action === CoreMouseAction.UP && !isSGR) {\n // special case - only SGR can report button on release\n // all others have to go with NONE\n code |= CoreMouseButton.NONE;\n }\n }\n return code;\n}\n\nconst S = String.fromCharCode;\n\n/**\n * Supported default encodings.\n */\nconst DEFAULT_ENCODINGS: { [key: string]: CoreMouseEncoding } = {\n /**\n * DEFAULT - CSI M Pb Px Py\n * Single byte encoding for coords and event code.\n * Can encode values up to 223 (1-based).\n */\n DEFAULT: (e: ICoreMouseEvent) => {\n const params = [eventCode(e, false) + 32, e.col + 32, e.row + 32];\n // supress mouse report if we exceed addressible range\n // Note this is handled differently by emulators\n // - xterm: sends 0;0 coords instead\n // - vte, konsole: no report\n if (params[0] > 255 || params[1] > 255 || params[2] > 255) {\n return '';\n }\n return `\\x1b[M${S(params[0])}${S(params[1])}${S(params[2])}`;\n },\n /**\n * SGR - CSI < Pb ; Px ; Py M|m\n * No encoding limitation.\n * Can report button on release and works with a well formed sequence.\n */\n SGR: (e: ICoreMouseEvent) => {\n const final = (e.action === CoreMouseAction.UP && e.button !== CoreMouseButton.WHEEL) ? 'm' : 'M';\n return `\\x1b[<${eventCode(e, true)};${e.col};${e.row}${final}`;\n },\n SGR_PIXELS: (e: ICoreMouseEvent) => {\n const final = (e.action === CoreMouseAction.UP && e.button !== CoreMouseButton.WHEEL) ? 'm' : 'M';\n return `\\x1b[<${eventCode(e, true)};${e.x};${e.y}${final}`;\n }\n};\n\n/**\n * MouseStateService\n *\n * Provides mouse tracking reports with different protocols and encodings.\n * - protocols: NONE (default), X10, VT200, DRAG, ANY\n * - encodings: DEFAULT, SGR (UTF8, URXVT removed in #2507)\n *\n * Custom protocols/encodings can be added by `addProtocol` / `addEncoding`.\n * To activate a protocol/encoding, set `activeProtocol` / `activeEncoding`.\n * Switching a protocol will send a notification event `onProtocolChange`\n * with a list of needed events to track.\n *\n * The service handles the mouse tracking state and decides whether to send\n * a tracking report to the backend based on protocol and encoding limitations.\n * To send a mouse event call `triggerMouseEvent`.\n */\nexport class MouseStateService extends Disposable implements IMouseStateService {\n public serviceBrand: any;\n\n private _protocols: { [name: string]: ICoreMouseProtocol } = {};\n private _encodings: { [name: string]: CoreMouseEncoding } = {};\n private _activeProtocol: string = '';\n private _activeEncoding: string = '';\n private _customWheelEventHandler: ((event: WheelEvent) => boolean) | undefined;\n\n private readonly _onProtocolChange = this._register(new Emitter());\n public readonly onProtocolChange = this._onProtocolChange.event;\n\n constructor() {\n super();\n\n // register default protocols and encodings\n for (const name of Object.keys(DEFAULT_PROTOCOLS)) this.addProtocol(name, DEFAULT_PROTOCOLS[name]);\n for (const name of Object.keys(DEFAULT_ENCODINGS)) this.addEncoding(name, DEFAULT_ENCODINGS[name]);\n // call reset to set defaults\n this.reset();\n }\n\n public addProtocol(name: string, protocol: ICoreMouseProtocol): void {\n this._protocols[name] = protocol;\n }\n\n public addEncoding(name: string, encoding: CoreMouseEncoding): void {\n this._encodings[name] = encoding;\n }\n\n public get activeProtocol(): string {\n return this._activeProtocol;\n }\n\n public get areMouseEventsActive(): boolean {\n return this._protocols[this._activeProtocol].events !== 0;\n }\n\n public set activeProtocol(name: string) {\n if (!this._protocols[name]) {\n throw new Error(`unknown protocol \"${name}\"`);\n }\n this._activeProtocol = name;\n this._onProtocolChange.fire(this._protocols[name].events);\n }\n\n public get activeEncoding(): string {\n return this._activeEncoding;\n }\n\n public set activeEncoding(name: string) {\n if (!this._encodings[name]) {\n throw new Error(`unknown encoding \"${name}\"`);\n }\n this._activeEncoding = name;\n }\n\n public reset(): void {\n this.activeProtocol = 'NONE';\n this.activeEncoding = 'DEFAULT';\n }\n\n public setCustomWheelEventHandler(customWheelEventHandler: ((event: WheelEvent) => boolean) | undefined): void {\n this._customWheelEventHandler = customWheelEventHandler;\n }\n\n public allowCustomWheelEvent(ev: WheelEvent): boolean {\n return this._customWheelEventHandler ? this._customWheelEventHandler(ev) !== false : true;\n }\n\n public restrictMouseEvent(e: ICoreMouseEvent): boolean {\n return this._protocols[this._activeProtocol].restrict(e);\n }\n\n public encodeMouseEvent(e: ICoreMouseEvent): string {\n return this._encodings[this._activeEncoding](e);\n }\n\n public get isDefaultEncoding(): boolean {\n return this._activeEncoding === 'DEFAULT';\n }\n\n public get isPixelEncoding(): boolean {\n return this._activeEncoding === 'SGR_PIXELS';\n }\n}\n","/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { Disposable, toDisposable } from '../Lifecycle';\nimport { isMac } from '../Platform';\nimport { CursorStyle, IDisposable } from '../Types';\nimport { FontWeight, IOptionsService, ITerminalOptions } from './Services';\nimport { Emitter } from '../Event';\n\nexport const DEFAULT_OPTIONS: Readonly> = {\n cols: 80,\n rows: 24,\n showCursorImmediately: false,\n cursorBlink: false,\n blinkIntervalDuration: 0,\n cursorStyle: 'block',\n cursorWidth: 1,\n cursorInactiveStyle: 'outline',\n drawBoldTextInBrightColors: true,\n documentOverride: null,\n fastScrollSensitivity: 5,\n fontFamily: 'monospace',\n fontSize: 15,\n fontWeight: 'normal',\n fontWeightBold: 'bold',\n ignoreBracketedPasteMode: false,\n lineHeight: 1.0,\n letterSpacing: 0,\n linkHandler: null,\n logLevel: 'info',\n logger: null,\n scrollback: 1000,\n scrollbar: { showScrollbar: true },\n scrollOnEraseInDisplay: false,\n scrollOnUserInput: true,\n scrollSensitivity: 1,\n screenReaderMode: false,\n smoothScrollDuration: 0,\n macOptionIsMeta: false,\n macOptionClickForcesSelection: false,\n minimumContrastRatio: 1,\n mouseEventsRequireAlt: false,\n disableStdin: false,\n allowProposedApi: false,\n allowTransparency: false,\n tabStopWidth: 8,\n theme: {},\n reflowCursorLine: false,\n rescaleOverlappingGlyphs: false,\n rightClickSelectsWord: isMac,\n windowOptions: {},\n windowsPty: {},\n wordSeparator: ' ()[]{}\\',\"`',\n altClickMovesCursor: true,\n convertEol: false,\n termName: 'xterm',\n quirks: {},\n vtExtensions: {}\n};\n\nconst FONT_WEIGHT_OPTIONS: Extract[] = ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'];\n\nexport class OptionsService extends Disposable implements IOptionsService {\n public serviceBrand: any;\n\n public readonly rawOptions: Required;\n public options: Required;\n\n private readonly _onOptionChange = this._register(new Emitter());\n public readonly onOptionChange = this._onOptionChange.event;\n\n constructor(options: Partial) {\n super();\n // set the default value of each option\n const defaultOptions = { ...DEFAULT_OPTIONS };\n for (const key in options) {\n if (key in defaultOptions) {\n try {\n const newValue = options[key];\n defaultOptions[key] = this._sanitizeAndValidateOption(key, newValue);\n } catch (e) {\n console.error(e);\n }\n }\n }\n\n // set up getters and setters for each option\n this.rawOptions = defaultOptions;\n this.options = { ... defaultOptions };\n this._setupOptions();\n\n // Clear out options that could link outside xterm.js as they could easily cause an embedder\n // memory leak\n this._register(toDisposable(() => {\n this.rawOptions.linkHandler = null;\n this.rawOptions.documentOverride = null;\n }));\n }\n\n // eslint-disable-next-line @typescript-eslint/naming-convention\n public onSpecificOptionChange(key: T, listener: (value: ITerminalOptions[T]) => any): IDisposable {\n return this.onOptionChange(eventKey => {\n if (eventKey === key) {\n listener(this.rawOptions[key]);\n }\n });\n }\n\n // eslint-disable-next-line @typescript-eslint/naming-convention\n public onMultipleOptionChange(keys: (keyof ITerminalOptions)[], listener: () => any): IDisposable {\n return this.onOptionChange(eventKey => {\n if (keys.indexOf(eventKey) !== -1) {\n listener();\n }\n });\n }\n\n private _setupOptions(): void {\n const getter = (propName: string): any => {\n if (!(propName in DEFAULT_OPTIONS)) {\n throw new Error(`No option with key \"${propName}\"`);\n }\n return this.rawOptions[propName];\n };\n\n const setter = (propName: string, value: any): void => {\n if (!(propName in DEFAULT_OPTIONS)) {\n throw new Error(`No option with key \"${propName}\"`);\n }\n\n value = this._sanitizeAndValidateOption(propName, value);\n // Don't fire an option change event if they didn't change\n if (this.rawOptions[propName] !== value) {\n this.rawOptions[propName] = value;\n this._onOptionChange.fire(propName);\n }\n };\n\n for (const propName in this.rawOptions) {\n const desc = {\n get: getter.bind(this, propName),\n set: setter.bind(this, propName)\n };\n Object.defineProperty(this.options, propName, desc);\n }\n }\n\n private _sanitizeAndValidateOption(key: string, value: any): any {\n switch (key) {\n case 'cursorStyle':\n if (!value) {\n value = DEFAULT_OPTIONS[key];\n }\n if (!isCursorStyle(value)) {\n throw new Error(`\"${value}\" is not a valid value for ${key}`);\n }\n break;\n case 'wordSeparator':\n if (!value) {\n value = DEFAULT_OPTIONS[key];\n }\n break;\n case 'fontWeight':\n case 'fontWeightBold':\n if (typeof value === 'number' && 1 <= value && value <= 1000) {\n // already valid numeric value\n break;\n }\n value = FONT_WEIGHT_OPTIONS.includes(value) ? value : DEFAULT_OPTIONS[key];\n break;\n case 'blinkIntervalDuration':\n value = Math.floor(value);\n if (value < 0) {\n throw new Error(`${key} cannot be less than 0, value: ${value}`);\n }\n break;\n case 'cursorWidth':\n value = Math.floor(value);\n // Fall through for bounds check\n case 'lineHeight':\n case 'tabStopWidth':\n if (value < 1) {\n throw new Error(`${key} cannot be less than 1, value: ${value}`);\n }\n break;\n case 'minimumContrastRatio':\n value = Math.max(1, Math.min(21, Math.round(value * 10) / 10));\n break;\n case 'scrollback':\n value = Math.min(value, 4294967295);\n if (value < 0) {\n throw new Error(`${key} cannot be less than 0, value: ${value}`);\n }\n break;\n case 'fastScrollSensitivity':\n case 'scrollSensitivity':\n if (value <= 0) {\n throw new Error(`${key} cannot be less than or equal to 0, value: ${value}`);\n }\n break;\n case 'rows':\n case 'cols':\n if (!value && value !== 0) {\n throw new Error(`${key} must be numeric, value: ${value}`);\n }\n break;\n case 'windowsPty':\n value = value ?? {};\n break;\n }\n return value;\n }\n}\n\nfunction isCursorStyle(value: unknown): value is CursorStyle {\n return value === 'block' || value === 'underline' || value === 'bar';\n}\n","/**\n * Copyright (c) 2022 The xterm.js authors. All rights reserved.\n * @license MIT\n */\nimport { IBufferService, IOscLinkService } from './Services';\nimport { IOscLinkData } from '../Types';\nimport { IMarker } from '../buffer/Types';\n\nexport class OscLinkService implements IOscLinkService {\n public serviceBrand: any;\n\n private _nextId = 1;\n\n /**\n * A map of the link key to link entry. This is used to add additional lines to links with ids.\n */\n private _entriesWithId: Map = new Map();\n\n /**\n * A map of the link id to the link entry. The \"link id\" (number) which is the numberic\n * representation of a unique link should not be confused with \"id\" (string) which comes in with\n * `id=` in the OSC link's properties.\n */\n private _dataByLinkId: Map = new Map();\n\n constructor(\n @IBufferService private readonly _bufferService: IBufferService\n ) {\n }\n\n public registerLink(data: IOscLinkData): number {\n const buffer = this._bufferService.buffer;\n\n // Links with no id will only ever be registered a single time\n if (data.id === undefined) {\n const marker = buffer.addMarker(buffer.ybase + buffer.y);\n const entry: IOscLinkEntryNoId = {\n data,\n id: this._nextId++,\n lines: [marker]\n };\n marker.onDispose(() => this._removeMarkerFromLink(entry, marker));\n this._dataByLinkId.set(entry.id, entry);\n return entry.id;\n }\n\n // Add the line to the link if it already exists\n const castData = data as Required;\n const key = this._getEntryIdKey(castData);\n const match = this._entriesWithId.get(key);\n if (match) {\n this.addLineToLink(match.id, buffer.ybase + buffer.y);\n return match.id;\n }\n\n // Create the link\n const marker = buffer.addMarker(buffer.ybase + buffer.y);\n const entry: IOscLinkEntryWithId = {\n id: this._nextId++,\n key: this._getEntryIdKey(castData),\n data: castData,\n lines: [marker]\n };\n marker.onDispose(() => this._removeMarkerFromLink(entry, marker));\n this._entriesWithId.set(entry.key, entry);\n this._dataByLinkId.set(entry.id, entry);\n return entry.id;\n }\n\n public addLineToLink(linkId: number, y: number): void {\n const entry = this._dataByLinkId.get(linkId);\n if (!entry) {\n return;\n }\n if (entry.lines.every(e => e.line !== y)) {\n const marker = this._bufferService.buffer.addMarker(y);\n entry.lines.push(marker);\n marker.onDispose(() => this._removeMarkerFromLink(entry, marker));\n }\n }\n\n public getLinkData(linkId: number): IOscLinkData | undefined {\n return this._dataByLinkId.get(linkId)?.data;\n }\n\n private _getEntryIdKey(linkData: Required): string {\n return `${linkData.id};;${linkData.uri}`;\n }\n\n private _removeMarkerFromLink(entry: IOscLinkEntryNoId | IOscLinkEntryWithId, marker: IMarker): void {\n const index = entry.lines.indexOf(marker);\n if (index === -1) {\n return;\n }\n entry.lines.splice(index, 1);\n if (entry.lines.length === 0) {\n if (entry.data.id !== undefined) {\n this._entriesWithId.delete((entry as IOscLinkEntryWithId).key);\n }\n this._dataByLinkId.delete(entry.id);\n }\n }\n}\n\ninterface IOscLinkEntry {\n data: T;\n id: number;\n lines: IMarker[];\n}\n\ninterface IOscLinkEntryNoId extends IOscLinkEntry {\n}\n\ninterface IOscLinkEntryWithId extends IOscLinkEntry> {\n key: string;\n}\n","/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n *\n * This was heavily inspired from microsoft/vscode's dependency injection system (MIT).\n */\n/*---------------------------------------------------------------------------------------------\n * Copyright (c) Microsoft Corporation. All rights reserved.\n * Licensed under the MIT License. See License.txt in the project root for license information.\n *--------------------------------------------------------------------------------------------*/\n\nexport interface IServiceIdentifier {\n (...args: any[]): void;\n type: T;\n _id: string;\n}\n\nconst enum Constants {\n DI_TARGET = 'di$target',\n DI_DEPENDENCIES = 'di$dependencies'\n}\n\nexport const serviceRegistry: Map> = new Map();\n\nexport function getServiceDependencies(ctor: any): { id: IServiceIdentifier, index: number, optional: boolean }[] {\n return ctor[Constants.DI_DEPENDENCIES] || [];\n}\n\nexport function createDecorator(id: string): IServiceIdentifier {\n if (serviceRegistry.has(id)) {\n return serviceRegistry.get(id)!;\n }\n\n const decorator: any = function (target: Function, key: string, index: number): any {\n if (arguments.length !== 3) {\n throw new Error('@IServiceName-decorator can only be used to decorate a parameter');\n }\n\n storeServiceDependency(decorator, target, index);\n };\n\n decorator._id = id;\n\n serviceRegistry.set(id, decorator);\n return decorator;\n}\n\nfunction storeServiceDependency(id: Function, target: Function, index: number): void {\n if ((target as any)[Constants.DI_TARGET] === target) {\n (target as any)[Constants.DI_DEPENDENCIES].push({ id, index });\n } else {\n (target as any)[Constants.DI_DEPENDENCIES] = [{ id, index }];\n (target as any)[Constants.DI_TARGET] = target;\n }\n}\n","/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport type { IDecoration, IDecorationOptions, ILinkHandler, ILogger, IWindowsPty, IOverviewRulerOptions } from '@xterm/xterm';\nimport { CoreMouseEncoding, CoreMouseEventType, CursorInactiveStyle, CursorStyle, ICharset, IColor, ICoreMouseEvent, ICoreMouseProtocol, IDecPrivateModes, IDisposable, IKittyKeyboardState, IModes, IOscLinkData, IWindowOptions } from '../Types';\nimport { IAttributeData, IBuffer, IBufferSet } from '../buffer/Types';\nimport { createDecorator, IServiceIdentifier } from './ServiceRegistry';\nimport type { Emitter, IEvent } from '../Event';\n\nexport const IBufferService = createDecorator('BufferService');\nexport interface IBufferService {\n serviceBrand: undefined;\n\n readonly cols: number;\n readonly rows: number;\n readonly buffer: IBuffer;\n readonly buffers: IBufferSet;\n isUserScrolling: boolean;\n onResize: IEvent;\n onScroll: IEvent;\n scroll(eraseAttr: IAttributeData, isWrapped?: boolean): void;\n scrollLines(disp: number, suppressScrollEvent?: boolean): void;\n resize(cols: number, rows: number): void;\n reset(): void;\n}\n\nexport interface IBufferResizeEvent {\n cols: number;\n rows: number;\n colsChanged: boolean;\n rowsChanged: boolean;\n}\n\nexport const IMouseStateService = createDecorator('MouseStateService');\nexport interface IMouseStateService {\n serviceBrand: undefined;\n\n activeProtocol: string;\n activeEncoding: string;\n areMouseEventsActive: boolean;\n addProtocol(name: string, protocol: ICoreMouseProtocol): void;\n addEncoding(name: string, encoding: CoreMouseEncoding): void;\n reset(): void;\n setCustomWheelEventHandler(customWheelEventHandler: ((event: WheelEvent) => boolean) | undefined): void;\n allowCustomWheelEvent(ev: WheelEvent): boolean;\n\n /**\n * Event to announce changes in mouse tracking.\n */\n onProtocolChange: IEvent;\n restrictMouseEvent(event: ICoreMouseEvent): boolean;\n encodeMouseEvent(event: ICoreMouseEvent): string;\n readonly isDefaultEncoding: boolean;\n readonly isPixelEncoding: boolean;\n}\n\nexport const ICoreService = createDecorator('CoreService');\nexport interface ICoreService {\n serviceBrand: undefined;\n\n /**\n * Initially the cursor will not be visible until the first time the terminal\n * is focused.\n */\n isCursorInitialized: boolean;\n isCursorHidden: boolean;\n\n readonly modes: IModes;\n readonly decPrivateModes: IDecPrivateModes;\n readonly kittyKeyboard: IKittyKeyboardState;\n\n readonly onData: IEvent;\n readonly onUserInput: IEvent;\n readonly onBinary: IEvent;\n readonly onRequestScrollToBottom: IEvent;\n\n reset(): void;\n\n /**\n * Triggers the onData event in the public API.\n * @param data The data that is being emitted.\n * @param wasUserInput Whether the data originated from the user (as opposed to\n * resulting from parsing incoming data). When true this will also:\n * - Scroll to the bottom of the buffer if option scrollOnUserInput is true.\n * - Fire the `onUserInput` event (so selection can be cleared).\n */\n triggerDataEvent(data: string, wasUserInput?: boolean): void;\n\n /**\n * Triggers the onBinary event in the public API.\n * @param data The data that is being emitted.\n */\n triggerBinaryEvent(data: string): void;\n}\n\nexport const ICharsetService = createDecorator('CharsetService');\nexport interface ICharsetService {\n serviceBrand: undefined;\n\n charset: ICharset | undefined;\n readonly glevel: number;\n readonly charsets: (ICharset | undefined)[];\n\n reset(): void;\n\n /**\n * Set the G level of the terminal.\n * @param g\n */\n setgLevel(g: number): void;\n\n /**\n * Set the charset for the given G level of the terminal.\n * @param g\n * @param charset\n */\n setgCharset(g: number, charset: ICharset | undefined): void;\n}\n\nexport interface IBrandedService {\n serviceBrand: undefined;\n}\n\ntype GetLeadingNonServiceArgs = TArgs extends [] ? []\n : TArgs extends [...infer TFirst, infer TLast] ? TLast extends IBrandedService ? GetLeadingNonServiceArgs : TArgs\n : never;\n\nexport const IInstantiationService = createDecorator('InstantiationService');\nexport interface IInstantiationService {\n serviceBrand: undefined;\n\n setService(id: IServiceIdentifier, instance: T): void;\n getService(id: IServiceIdentifier): T | undefined;\n createInstance any, R extends InstanceType>(t: Ctor, ...args: GetLeadingNonServiceArgs>): R;\n}\n\nexport enum LogLevelEnum {\n TRACE = 0,\n DEBUG = 1,\n INFO = 2,\n WARN = 3,\n ERROR = 4,\n OFF = 5\n}\n\nexport const ILogService = createDecorator('LogService');\nexport interface ILogService {\n serviceBrand: undefined;\n\n readonly logLevel: LogLevelEnum;\n\n trace(message: any, ...optionalParams: any[]): void;\n debug(message: any, ...optionalParams: any[]): void;\n info(message: any, ...optionalParams: any[]): void;\n warn(message: any, ...optionalParams: any[]): void;\n error(message: any, ...optionalParams: any[]): void;\n}\n\nexport const IOptionsService = createDecorator('OptionsService');\nexport interface IOptionsService {\n serviceBrand: undefined;\n\n /**\n * Read only access to the raw options object, this is an internal-only fast path for accessing\n * single options without any validation as we trust TypeScript to enforce correct usage\n * internally.\n */\n readonly rawOptions: Required;\n\n /**\n * Options as exposed through the public API, this property uses getters and setters with\n * validation which makes it safer but slower. {@link rawOptions} should be used for pretty much\n * all internal usage for performance reasons.\n */\n readonly options: Required;\n\n /**\n * Adds an event listener for when any option changes.\n */\n readonly onOptionChange: IEvent;\n\n /**\n * Adds an event listener for when a specific option changes, this is a convenience method that is\n * preferred over {@link onOptionChange} when only a single option is being listened to.\n */\n // eslint-disable-next-line @typescript-eslint/naming-convention\n onSpecificOptionChange(key: T, listener: (arg1: Required[T]) => any): IDisposable;\n\n /**\n * Adds an event listener for when a set of specific options change, this is a convenience method\n * that is preferred over {@link onOptionChange} when multiple options are being listened to and\n * handled the same way.\n */\n // eslint-disable-next-line @typescript-eslint/naming-convention\n onMultipleOptionChange(keys: (keyof ITerminalOptions)[], listener: () => any): IDisposable;\n}\n\nexport type FontWeight = 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900' | number;\nexport type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'off';\n\nexport interface ITerminalOptions {\n allowProposedApi?: boolean;\n allowTransparency?: boolean;\n altClickMovesCursor?: boolean;\n cols?: number;\n convertEol?: boolean;\n cursorBlink?: boolean;\n blinkIntervalDuration?: number;\n cursorStyle?: CursorStyle;\n cursorWidth?: number;\n cursorInactiveStyle?: CursorInactiveStyle;\n disableStdin?: boolean;\n documentOverride?: any | null;\n drawBoldTextInBrightColors?: boolean;\n fastScrollSensitivity?: number;\n fontSize?: number;\n fontFamily?: string;\n fontWeight?: FontWeight;\n fontWeightBold?: FontWeight;\n ignoreBracketedPasteMode?: boolean;\n letterSpacing?: number;\n lineHeight?: number;\n linkHandler?: ILinkHandler | null;\n logLevel?: LogLevel;\n logger?: ILogger | null;\n macOptionIsMeta?: boolean;\n macOptionClickForcesSelection?: boolean;\n minimumContrastRatio?: number;\n mouseEventsRequireAlt?: boolean;\n reflowCursorLine?: boolean;\n rescaleOverlappingGlyphs?: boolean;\n rightClickSelectsWord?: boolean;\n rows?: number;\n showCursorImmediately?: boolean;\n screenReaderMode?: boolean;\n scrollback?: number;\n scrollOnUserInput?: boolean;\n scrollSensitivity?: number;\n smoothScrollDuration?: number;\n tabStopWidth?: number;\n theme?: ITheme;\n windowsPty?: IWindowsPty;\n windowOptions?: IWindowOptions;\n wordSeparator?: string;\n quirks?: ITerminalQuirks;\n scrollbar?: IScrollbarOptions;\n scrollOnEraseInDisplay?: boolean;\n vtExtensions?: IVtExtensions;\n\n [key: string]: any;\n termName: string;\n}\n\nexport interface ITheme {\n foreground?: string;\n background?: string;\n cursor?: string;\n cursorAccent?: string;\n selectionForeground?: string;\n selectionBackground?: string;\n selectionInactiveBackground?: string;\n scrollbarSliderBackground?: string;\n scrollbarSliderHoverBackground?: string;\n scrollbarSliderActiveBackground?: string;\n overviewRulerBorder?: string;\n black?: string;\n red?: string;\n green?: string;\n yellow?: string;\n blue?: string;\n magenta?: string;\n cyan?: string;\n white?: string;\n brightBlack?: string;\n brightRed?: string;\n brightGreen?: string;\n brightYellow?: string;\n brightBlue?: string;\n brightMagenta?: string;\n brightCyan?: string;\n brightWhite?: string;\n extendedAnsi?: string[];\n}\n\nexport interface ITerminalQuirks {\n allowSetCursorBlink?: boolean;\n}\n\nexport interface IScrollbarOptions {\n showScrollbar?: boolean;\n showArrows?: boolean;\n width?: number;\n overviewRuler?: IOverviewRulerOptions;\n}\n\nexport interface IVtExtensions {\n kittyKeyboard?: boolean;\n kittySgrBoldFaintControl?: boolean;\n win32InputMode?: boolean;\n colorSchemeQuery?: boolean;\n}\n\nexport const IOscLinkService = createDecorator('OscLinkService');\nexport interface IOscLinkService {\n serviceBrand: undefined;\n /**\n * Registers a link to the service, returning the link ID. The link data is managed by this\n * service and will be freed when this current cursor position is trimmed off the buffer.\n */\n registerLink(linkData: IOscLinkData): number;\n /**\n * Adds a line to a link if needed.\n */\n addLineToLink(linkId: number, y: number): void;\n /** Get the link data associated with a link ID. */\n getLinkData(linkId: number): IOscLinkData | undefined;\n}\n\n/*\n * Width and Grapheme_Cluster_Break properties of a character as a bit mask.\n *\n * bit 0: shouldJoin - should combine with preceding character.\n * bit 1..2: wcwidth - see UnicodeCharWidth.\n * bit 3..31: class of character (currently only 4 bits are used).\n * This is used to determined grapheme clustering - i.e. which codepoints\n * are to be combined into a single compound character.\n *\n * Use the UnicodeService static function createPropertyValue to create a\n * UnicodeCharProperties; use extractShouldJoin, extractWidth, and\n * extractCharKind to extract the components.\n */\nexport type UnicodeCharProperties = number;\n\n/**\n * Width in columns of a character.\n * In a CJK context, \"half-width\" characters (such as Latin) are width 1,\n * while \"full-width\" characters (such as Kanji) are 2 columns wide.\n * Combining characters (such as accents) are width 0.\n */\nexport type UnicodeCharWidth = 0 | 1 | 2;\n\nexport const IUnicodeService = createDecorator('UnicodeService');\nexport interface IUnicodeService {\n serviceBrand: undefined;\n /** Register a Unicode version provider. */\n register(provider: IUnicodeVersionProvider): void;\n /** Registered Unicode versions. */\n readonly versions: string[];\n /** Currently active version. */\n activeVersion: string;\n /** Event triggered when the active version changes. */\n readonly onChange: IEvent;\n\n /**\n * Unicode version dependent\n */\n wcwidth(codepoint: number): UnicodeCharWidth;\n getStringCellWidth(s: string): number;\n /**\n * Return character width and type for grapheme clustering.\n * If preceding != 0, it is the return code from the previous character;\n * in that case the result specifies if the characters should be joined.\n */\n charProperties(codepoint: number, preceding: UnicodeCharProperties): UnicodeCharProperties;\n}\n\nexport interface IUnicodeVersionProvider {\n readonly version: string;\n wcwidth(ucs: number): UnicodeCharWidth;\n charProperties(codepoint: number, preceding: UnicodeCharProperties): UnicodeCharProperties;\n}\n\nexport const IDecorationService = createDecorator('DecorationService');\nexport interface IDecorationService extends IDisposable {\n serviceBrand: undefined;\n readonly decorations: IterableIterator;\n readonly onDecorationRegistered: IEvent;\n readonly onDecorationRemoved: IEvent;\n registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined;\n reset(): void;\n /**\n * Trigger a callback over the decoration at a cell (in no particular order). This uses a callback\n * instead of an iterator as it's typically used in hot code paths.\n */\n forEachDecorationAtCell(x: number, line: number, layer: 'bottom' | 'top' | undefined, callback: (decoration: IInternalDecoration) => void): void;\n}\nexport interface IInternalDecoration extends IDecoration {\n readonly options: IDecorationOptions;\n readonly backgroundColorRGB: IColor | undefined;\n readonly foregroundColorRGB: IColor | undefined;\n readonly onRenderEmitter: Emitter;\n /** @internal Start line for line-index removal; kept in sync on buffer line shifts. */\n _indexedStartLine: number;\n}\n","/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { IUnicodeService, IUnicodeVersionProvider, UnicodeCharProperties, UnicodeCharWidth } from './Services';\nimport { Emitter } from '../Event';\n\nexport class UnicodeService implements IUnicodeService {\n public serviceBrand: any;\n\n private _providers: {[key: string]: IUnicodeVersionProvider} = Object.create(null);\n private _active: string = '';\n private _activeProvider!: IUnicodeVersionProvider;\n\n private readonly _onChange = new Emitter();\n public readonly onChange = this._onChange.event;\n\n public static extractShouldJoin(value: UnicodeCharProperties): boolean {\n return (value & 1) !== 0;\n }\n public static extractWidth(value: UnicodeCharProperties): UnicodeCharWidth {\n return ((value >> 1) & 0x3) as UnicodeCharWidth;\n }\n public static extractCharKind(value: UnicodeCharProperties): number {\n return value >> 3;\n }\n public static createPropertyValue(state: number, width: number, shouldJoin: boolean = false): UnicodeCharProperties {\n return ((state & 0xffffff) << 3) | ((width & 3) << 1) | (shouldJoin?1:0);\n }\n\n public dispose(): void {\n this._onChange.dispose();\n }\n\n public get versions(): string[] {\n return Object.keys(this._providers);\n }\n\n public get activeVersion(): string {\n return this._active;\n }\n\n public set activeVersion(version: string) {\n if (!this._providers[version]) {\n throw new Error(`unknown Unicode version \"${version}\"`);\n }\n this._active = version;\n this._activeProvider = this._providers[version];\n this._onChange.fire(version);\n }\n\n public register(provider: IUnicodeVersionProvider): void {\n this._providers[provider.version] = provider;\n if (!this._active) {\n this.activeVersion = provider.version;\n }\n }\n\n /**\n * Unicode version dependent interface.\n */\n public wcwidth(num: number): UnicodeCharWidth {\n return this._activeProvider.wcwidth(num);\n }\n\n public getStringCellWidth(s: string): number {\n let result = 0;\n let precedingInfo = 0;\n const length = s.length;\n for (let i = 0; i < length; ++i) {\n let code = s.charCodeAt(i);\n // surrogate pair first\n if (0xD800 <= code && code <= 0xDBFF) {\n if (++i >= length) {\n // this should not happen with strings retrieved from\n // Buffer.translateToString as it converts from UTF-32\n // and therefore always should contain the second part\n // for any other string we still have to handle it somehow:\n // simply treat the lonely surrogate first as a single char (UCS-2 behavior)\n return result + this.wcwidth(code);\n }\n const second = s.charCodeAt(i);\n // convert surrogate pair to high codepoint only for valid second part (UTF-16)\n // otherwise treat them independently (UCS-2 behavior)\n if (0xDC00 <= second && second <= 0xDFFF) {\n code = (code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000;\n } else {\n result += this.wcwidth(second);\n }\n }\n const currentInfo = this.charProperties(code, precedingInfo);\n let chWidth = UnicodeService.extractWidth(currentInfo);\n if (UnicodeService.extractShouldJoin(currentInfo)) {\n chWidth -= UnicodeService.extractWidth(precedingInfo);\n }\n result += chWidth;\n precedingInfo = currentInfo;\n }\n return result;\n }\n\n public charProperties(codepoint: number, preceding: UnicodeCharProperties): UnicodeCharProperties {\n return this._activeProvider.charProperties(codepoint, preceding);\n }\n}\n","/**\n * Copyright (c) 2014 The xterm.js authors. All rights reserved.\n * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)\n * @license MIT\n *\n * Originally forked from (with the author's permission):\n * Fabrice Bellard's javascript vt100 for jslinux:\n * http://bellard.org/jslinux/\n * Copyright (c) 2011 Fabrice Bellard\n * The original design remains. The terminal itself\n * has been extended to include xterm CSI codes, among\n * other features.\n *\n * Terminal Emulation References:\n * http://vt100.net/\n * http://invisible-island.net/xterm/ctlseqs/ctlseqs.txt\n * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html\n * http://invisible-island.net/vttest/\n * http://www.inwap.com/pdp10/ansicode.txt\n * http://linux.die.net/man/4/console_codes\n * http://linux.die.net/man/7/urxvt\n */\n\nimport { DEFAULT_ATTR_DATA } from '../common/buffer/BufferLine';\nimport { IBuffer, IMarker } from '../common/buffer/Types';\nimport { CoreTerminal } from '../common/CoreTerminal';\nimport { ITerminalOptions } from '../common/Types';\nimport { Emitter, EventUtils } from '../common/Event';\n\nexport class Terminal extends CoreTerminal {\n private readonly _onBell = this._register(new Emitter());\n public readonly onBell = this._onBell.event;\n private readonly _onCursorMove = this._register(new Emitter());\n public readonly onCursorMove = this._onCursorMove.event;\n private readonly _onTitleChange = this._register(new Emitter());\n public readonly onTitleChange = this._onTitleChange.event;\n private readonly _onA11yCharEmitter = this._register(new Emitter());\n public readonly onA11yChar = this._onA11yCharEmitter.event;\n private readonly _onA11yTabEmitter = this._register(new Emitter());\n public readonly onA11yTab = this._onA11yTabEmitter.event;\n\n constructor(\n options: ITerminalOptions = {}\n ) {\n super(options);\n\n this._setup();\n\n // Setup InputHandler listeners\n this._register(this._inputHandler.onRequestBell(() => this.bell()));\n this._register(this._inputHandler.onRequestReset(() => this.reset()));\n this._register(EventUtils.forward(this._inputHandler.onCursorMove, this._onCursorMove));\n this._register(EventUtils.forward(this._inputHandler.onTitleChange, this._onTitleChange));\n this._register(EventUtils.forward(this._inputHandler.onA11yChar, this._onA11yCharEmitter));\n this._register(EventUtils.forward(this._inputHandler.onA11yTab, this._onA11yTabEmitter));\n this._register(EventUtils.forward(EventUtils.map(this._inputHandler.onRequestRefreshRows, e => ({ start: e?.start ?? 0, end: e?.end ?? this.rows - 1 })), this._onRender));\n }\n\n /**\n * Convenience property to active buffer.\n */\n public get buffer(): IBuffer {\n return this.buffers.active;\n }\n\n // TODO: Support paste here?\n\n public get markers(): IMarker[] {\n return this.buffer.markers;\n }\n\n public registerMarker(cursorYOffset: number): IMarker {\n return this.buffer.addMarker(this.buffer.ybase + this.buffer.y + cursorYOffset);\n }\n\n public bell(): void {\n this._onBell.fire();\n }\n\n public input(data: string, wasUserInput: boolean = true): void {\n this.coreService.triggerDataEvent(data, wasUserInput);\n }\n\n /**\n * Resizes the terminal.\n *\n * @param x The number of columns to resize to.\n * @param y The number of rows to resize to.\n */\n public resize(x: number, y: number): void {\n if (x === this.cols && y === this.rows) {\n return;\n }\n\n super.resize(x, y);\n }\n\n /**\n * Clear the entire buffer, making the prompt line the new first line.\n */\n public clear(): void {\n this.buffer.clearAllMarkers();\n this.buffer.lines.set(0, this.buffer.lines.get(this.buffer.ybase + this.buffer.y)!);\n this.buffer.lines.length = 1;\n this.buffer.ydisp = 0;\n this.buffer.ybase = 0;\n this.buffer.y = 0;\n for (let i = 1; i < this.rows; i++) {\n this.buffer.lines.push(this.buffer.getBlankLine(DEFAULT_ATTR_DATA));\n }\n this._onScroll.fire({ position: this.buffer.ydisp });\n }\n\n /**\n * Reset terminal.\n * Note: Calling this directly from JS is synchronous but does not clear\n * input buffers and does not reset the parser, thus the terminal will\n * continue to apply pending input data.\n * If you need in band reset (synchronous with input data) consider\n * using DECSTR (soft reset, CSI ! p) or RIS instead (hard reset, ESC c).\n */\n public reset(): void {\n /**\n * Since _setup handles a full terminal creation, we have to carry forward\n * a few things that should not reset.\n */\n this.options.rows = this.rows;\n this.options.cols = this.cols;\n\n this._setup();\n super.reset();\n }\n}\n","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n","/**\n * Copyright (c) 2018 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { BufferNamespaceApi } from '../../common/public/BufferNamespaceApi';\nimport { ParserApi } from '../../common/public/ParserApi';\nimport { UnicodeApi } from '../../common/public/UnicodeApi';\nimport { IBufferNamespace as IBufferNamespaceApi, IMarker, IModes, IParser, ITerminalAddon, ITerminalInitOnlyOptions, IUnicodeHandling, Terminal as ITerminalApi } from '@xterm/headless';\nimport { Terminal as TerminalCore } from '../Terminal';\nimport { AddonManager } from '../../common/public/AddonManager';\nimport { ITerminalOptions } from '../../common/Types';\nimport { Disposable } from '../../common/Lifecycle';\nimport type { IEvent } from '../../common/Event';\n/**\n * The set of options that only have an effect when set in the Terminal constructor.\n */\nconst CONSTRUCTOR_ONLY_OPTIONS = ['cols', 'rows'];\n\nexport class Terminal extends Disposable implements ITerminalApi {\n private _core: TerminalCore;\n private _addonManager: AddonManager;\n private _parser: IParser | undefined;\n private _buffer: BufferNamespaceApi | undefined;\n private _publicOptions: Required;\n\n constructor(options?: ITerminalOptions & ITerminalInitOnlyOptions) {\n super();\n\n this._core = this._register(new TerminalCore(options));\n this._addonManager = this._register(new AddonManager());\n\n this._publicOptions = { ... this._core.options };\n const getter = (propName: string): any => {\n return this._core.options[propName];\n };\n const setter = (propName: string, value: any): void => {\n this._checkReadonlyOptions(propName);\n this._core.options[propName] = value;\n };\n\n for (const propName in this._core.options) {\n const desc = {\n get: getter.bind(this, propName),\n set: setter.bind(this, propName)\n };\n Object.defineProperty(this._publicOptions, propName, desc);\n }\n }\n\n private _checkReadonlyOptions(propName: string): void {\n // Throw an error if any constructor only option is modified\n // from terminal.options\n // Modifications from anywhere else are allowed\n if (CONSTRUCTOR_ONLY_OPTIONS.includes(propName)) {\n throw new Error(`Option \"${propName}\" can only be set in the constructor`);\n }\n }\n\n private _checkProposedApi(): void {\n if (!this._core.optionsService.options.allowProposedApi) {\n throw new Error('You must set the allowProposedApi option to true to use proposed API');\n }\n }\n\n public get onBell(): IEvent { return this._core.onBell; }\n public get onBinary(): IEvent { return this._core.onBinary; }\n public get onCursorMove(): IEvent { return this._core.onCursorMove; }\n public get onData(): IEvent { return this._core.onData; }\n public get onLineFeed(): IEvent { return this._core.onLineFeed; }\n public get onRender(): IEvent<{ start: number, end: number }> { return this._core.onRender; }\n public get onResize(): IEvent<{ cols: number, rows: number }> { return this._core.onResize; }\n public get onScroll(): IEvent { return this._core.onScroll; }\n public get onTitleChange(): IEvent { return this._core.onTitleChange; }\n public get onWriteParsed(): IEvent { return this._core.onWriteParsed; }\n\n public get parser(): IParser {\n this._parser ??= new ParserApi(this._core);\n return this._parser;\n }\n public get unicode(): IUnicodeHandling {\n this._checkProposedApi();\n return new UnicodeApi(this._core);\n }\n public get rows(): number { return this._core.rows; }\n public get cols(): number { return this._core.cols; }\n public get buffer(): IBufferNamespaceApi {\n this._buffer ??= this._register(new BufferNamespaceApi(this._core));\n return this._buffer;\n }\n public get markers(): ReadonlyArray {\n return this._core.markers;\n }\n public get modes(): IModes {\n const m = this._core.coreService.decPrivateModes;\n let mouseTrackingMode: 'none' | 'x10' | 'vt200' | 'drag' | 'any' = 'none';\n switch (this._core.mouseStateService.activeProtocol) {\n case 'X10': mouseTrackingMode = 'x10'; break;\n case 'VT200': mouseTrackingMode = 'vt200'; break;\n case 'DRAG': mouseTrackingMode = 'drag'; break;\n case 'ANY': mouseTrackingMode = 'any'; break;\n }\n return {\n applicationCursorKeysMode: m.applicationCursorKeys,\n applicationKeypadMode: m.applicationKeypad,\n bracketedPasteMode: m.bracketedPasteMode,\n insertMode: this._core.coreService.modes.insertMode,\n mouseTrackingMode: mouseTrackingMode,\n originMode: m.origin,\n reverseWraparoundMode: m.reverseWraparound,\n sendFocusMode: m.sendFocus,\n showCursor: !this._core.coreService.isCursorHidden,\n synchronizedOutputMode: m.synchronizedOutput,\n win32InputMode: m.win32InputMode,\n wraparoundMode: m.wraparound\n };\n }\n public get options(): Required {\n return this._publicOptions;\n }\n public set options(options: ITerminalOptions) {\n for (const propName in options) {\n this._publicOptions[propName] = options[propName];\n }\n }\n public input(data: string, wasUserInput: boolean = true): void {\n this._core.input(data, wasUserInput);\n }\n public resize(columns: number, rows: number): void {\n this._verifyIntegers(columns, rows);\n this._core.resize(columns, rows);\n }\n public registerMarker(cursorYOffset: number = 0): IMarker {\n this._verifyIntegers(cursorYOffset);\n return this._core.registerMarker(cursorYOffset);\n }\n public addMarker(cursorYOffset: number): IMarker {\n return this.registerMarker(cursorYOffset);\n }\n public dispose(): void {\n super.dispose();\n }\n public scrollLines(amount: number): void {\n this._verifyIntegers(amount);\n this._core.scrollLines(amount);\n }\n public scrollPages(pageCount: number): void {\n this._verifyIntegers(pageCount);\n this._core.scrollPages(pageCount);\n }\n public scrollToTop(): void {\n this._core.scrollToTop();\n }\n public scrollToBottom(): void {\n this._core.scrollToBottom();\n }\n public scrollToLine(line: number): void {\n this._verifyIntegers(line);\n this._core.scrollToLine(line);\n }\n public clear(): void {\n this._core.clear();\n }\n public write(data: string | Uint8Array, callback?: () => void): void {\n this._core.write(data, callback);\n }\n public writeln(data: string | Uint8Array, callback?: () => void): void {\n this._core.write(data);\n this._core.write('\\r\\n', callback);\n }\n public reset(): void {\n this._core.reset();\n }\n public loadAddon(addon: ITerminalAddon): void {\n // TODO: This could cause issues if the addon calls renderer apis\n this._addonManager.loadAddon(this as any, addon);\n }\n\n private _verifyIntegers(...values: number[]): void {\n for (const value of values) {\n if (value === Infinity || isNaN(value) || value % 1 !== 0) {\n throw new Error('This API only accepts integers');\n }\n }\n }\n}\n"],"names":["millis","Promise","resolve","setTimeout","handler","timeout","store","timer","disposable","dispose","Lifecycle_1","toDisposable","clearTimeout","add","__webpack_require__","constructor","this","_token","_isDisposed","cancel","cancelAndSet","runner","Error","setIfNotSet","_isScheduled","set","queueMicrotask","_disposable","undefined","interval","context","globalThis","handle","setInterval","clearInterval","Event_1","CircularList","Disposable","_maxLength","super","onDeleteEmitter","_register","Emitter","onDelete","event","onInsertEmitter","onInsert","onTrimEmitter","onTrim","_array","Array","_startIndex","_length","maxLength","newMaxLength","newArray","i","Math","min","length","_getCyclicIndex","newLength","get","index","value","push","fire","recycle","isFull","pop","splice","start","deleteCount","items","amount","countToTrim","trimStart","count","shiftElements","offset","expandListBy","Services_1","InstantiationService_1","LogService_1","BufferService_1","OptionsService_1","CoreService_1","MouseStateService_1","UnicodeV6_1","UnicodeService_1","CharsetService_1","WindowsMode_1","InputHandler_1","WriteBuffer_1","OscLinkService_1","hasWriteSyncWarnHappened","CoreTerminal","onScroll","_onScrollApi","_onScroll","ev","position","cols","_bufferService","rows","buffers","options","optionsService","key","_windowsWrappingHeuristics","MutableDisposable","_onBinary","onBinary","_onData","onData","_onLineFeed","onLineFeed","_onRender","onRender","_onResize","onResize","_onWriteParsed","onWriteParsed","_instantiationService","InstantiationService","OptionsService","setService","IOptionsService","_logService","createInstance","LogService","ILogService","BufferService","IBufferService","coreService","CoreService","ICoreService","mouseStateService","MouseStateService","IMouseStateService","unicodeService","UnicodeService","register","UnicodeV6","IUnicodeService","_charsetService","CharsetService","ICharsetService","_oscLinkService","OscLinkService","IOscLinkService","_inputHandler","InputHandler","EventUtils","forward","onRequestScrollToBottom","scrollToBottom","onUserInput","_writeBuffer","handleUserInput","onMultipleOptionChange","_handleWindowsPtyOptionChange","buffer","ydisp","markRangeDirty","scrollTop","scrollBottom","WriteBuffer","data","promiseResult","parse","write","callback","writeSync","maxSubsequentCalls","logLevel","LogLevelEnum","WARN","warn","input","wasUserInput","triggerDataEvent","resize","x","y","isNaN","max","flushSync","scroll","eraseAttr","isWrapped","scrollLines","disp","suppressScrollEvent","scrollPages","pageCount","scrollToTop","disableSmoothScroll","ybase","scrollToLine","line","scrollAmount","registerEscHandler","id","registerDcsHandler","registerCsiHandler","registerOscHandler","ident","registerApcHandler","_setup","reset","windowsPty","rawOptions","backend","buildNumber","_enableWindowsWrappingHeuristics","clear","disposables","updateWindowsModeWrappedState","bind","final","d","_listeners","_disposed","_event","listener","thisArgs","entry","fn","slice","result","idx","indexOf","isArray","call","listeners","len","from","to","e","map","any","events","DisposableStore","runAndSubscribe","initial","exports","Charsets_1","EscapeSequenceParser_1","TextDecoder_1","BufferLine_1","Constants_1","CellData_1","AttributeData_1","OscParser_1","DcsParser_1","ApcParser_1","XParseColor_1","Version_1","GLEVEL","paramToWindowOption","n","opts","setWinLines","restoreWin","minimizeWin","setWinPosition","setWinSizePixels","raiseWin","lowerWin","refreshWin","setWinSizeChars","maximizeWin","fullscreenWin","getWinState","getWinPosition","getWinSizePixels","getScreenSizePixels","getCellSizePixels","getWinSizeChars","getScreenSizeChars","getIconTitle","getWinTitle","pushTitle","popTitle","WindowsOptionsReportType","$temp","getAttrData","_curAttrData","_coreService","_optionsService","_mouseStateService","_unicodeService","_parser","EscapeSequenceParser","_parseBuffer","Uint32Array","_stringDecoder","StringToUtf32","_utf8Decoder","Utf8ToUtf32","_windowTitle","_iconName","_windowTitleStack","_iconNameStack","DEFAULT_ATTR_DATA","clone","_eraseAttrDataInternal","_onRequestBell","onRequestBell","_onRequestRefreshRows","onRequestRefreshRows","_onRequestReset","onRequestReset","_onRequestSendFocus","onRequestSendFocus","_onRequestSyncScrollBar","onRequestSyncScrollBar","_onRequestWindowsOptionsReport","onRequestWindowsOptionsReport","_onA11yChar","onA11yChar","_onA11yTab","onA11yTab","_onCursorMove","onCursorMove","_onTitleChange","onTitleChange","_onColor","onColor","_onRequestColorSchemeQuery","onRequestColorSchemeQuery","_parseStack","paused","cursorStartX","cursorStartY","decodedLength","_specialColors","_dirtyRowTracker","DirtyRowTracker","_activeBuffer","onBufferActivate","activeBuffer","setCsiHandlerFallback","params","debug","identifier","identToString","toArray","setEscHandlerFallback","setExecuteHandlerFallback","code","setOscHandlerFallback","action","setDcsHandlerFallback","payload","setApcHandlerFallback","setPrintHandler","end","print","insertChars","intermediates","scrollLeft","cursorUp","scrollRight","cursorDown","cursorForward","cursorBackward","cursorNextLine","cursorPrecedingLine","cursorCharAbsolute","cursorPosition","cursorForwardTab","eraseInDisplay","prefix","eraseInLine","insertLines","deleteLines","deleteChars","scrollUp","scrollDown","eraseChars","cursorBackwardTab","charPosAbsolute","hPositionRelative","repeatPrecedingCharacter","sendDeviceAttributesPrimary","sendDeviceAttributesSecondary","linePosAbsolute","vPositionRelative","hVPosition","tabClear","setMode","setModePrivate","resetMode","resetModePrivate","charAttributes","deviceStatus","deviceStatusPrivate","softReset","sendXtVersion","setCursorStyle","setScrollRegion","saveCursor","windowOptions","restoreCursor","insertColumns","deleteColumns","selectProtected","requestMode","kittyKeyboardSet","kittyKeyboardQuery","kittyKeyboardPush","kittyKeyboardPop","setExecuteHandler","bell","lineFeed","carriageReturn","backspace","tab","shiftOut","shiftIn","nextLine","tabSet","OscHandler","setTitle","setIconName","setOrReportIndexedColor","setHyperlink","setOrReportFgColor","setOrReportBgColor","setOrReportCursorColor","restoreIndexedColor","restoreFgColor","restoreBgColor","restoreCursorColor","reverseIndex","keypadApplicationMode","keypadNumericMode","fullReset","setgLevel","selectDefaultCharset","flag","CHARSETS","selectCharset","screenAlignmentPattern","setErrorHandler","state","error","DcsHandler","requestStatusString","_preserveStack","_logSlowResolvingAsync","p","slowTimeout","slowPromise","_res","rej","race","then","err","console","_getCurrentLinkId","extended","urlId","wasPaused","DEBUG","prototype","String","fromCharCode","join","TRACE","trace","split","charCodeAt","clearRange","decode","substring","subarray","viewportEnd","viewportStart","chWidth","charset","screenReaderMode","wraparoundMode","decPrivateModes","wraparound","insertMode","modes","curAttr","bufferRow","lines","markDirty","getWidth","setCellFromCodepoint","precedingJoinState","pos","ch","currentInfo","charProperties","extractWidth","shouldJoin","extractShouldJoin","oldWidth","stringFromCodePoint","linkId","addLineToLink","oldRow","oldCol","_eraseAttrData","BufferLine","copyCellsFrom","addCodepointToCell","delta","insertCells","getNullCell","NULL_CELL_CODE","NULL_CELL_WIDTH","hasContent","ApcHandler","convertEol","reverseWraparound","_restrictCursor","hasWidth","originalX","nextStop","maxCol","origin","_setCursor","_moveCursor","diffToTop","diffToBottom","param","tabs","prevStop","bg","_eraseInBufferLine","clearWrap","respectProtect","replaceCells","_resetBufferLine","fill","clearMarkers","j","scrollOnEraseInDisplay","currentLine","getTrimmedLength","scrollBackSize","normal","isUserScrolling","row","scrollBottomRowsOffset","scrollBottomAbsolute","getBlankLine","deleteCells","joinState","text","getString","idata","itext","codePointAt","tlength","copyWithin","_is","XTERM_VERSION","term","termName","startsWith","applicationCursorKeys","setgCharset","DEFAULT_CHARSET","quirks","allowSetCursorBlink","cursorBlink","applicationKeypad","activeProtocol","sendFocus","activeEncoding","isCursorHidden","vtExtensions","kittyKeyboard","mainFlags","flags","altFlags","activateAltBuffer","isCursorInitialized","bracketedPasteMode","synchronizedOutput","colorSchemeQuery","colorSchemeUpdates","win32InputMode","activateNormalBuffer","ansi","dm","mouseProtocol","mouseEncoding","cs","active","alt","f","m","v","b2v","_updateAttrColor","color","mode","c1","c2","c3","AttributeData","fromColorRGB","_extractColor","attr","accu","cSpace","advance","hasSubParams","subparams","getSubParams","fg","underlineColor","_processUnderline","style","underlineStyle","updateExtended","_processSGR0","l","kittySgrBoldFaintControl","savedX","savedY","savedCurAttrData","savedCharset","cursorStyle","isBlinking","top","bottom","second","GET_WIN_SIZE_PIXELS","GET_CELL_SIZE_PIXELS","shift","savedCharsets","charsets","savedGlevel","glevel","savedOriginMode","savedWraparoundMode","slots","spec","exec","parseInt","isValidColorIndex","type","parseColor","trim","uri","_createHyperlink","_finishHyperlink","parsedParams","idParamIndex","findIndex","registerLink","_setOrReportSpecialColor","collectAndFlag","scrollRegionHeight","level","cell","CellData","content","yOffset","markAllDirty","b","s","isProtected","block","underline","bar","y1","y2","stack","altStack","mainStack","__decorate","__param","arg","_disposables","Set","isDisposed","o","_store","None","Object","freeze","_value","_targetWindow","isSafari","majorVersion","userAgent","match","isNode","process","navigator","platform","isFirefox","includes","isChrome","isLegacyEdge","test","isMac","isWindows","isLinux","isChromeOS","StringBuilder","_chunks","append","chunk","toString","_limit","_builder","limit","TaskQueue","logService","_tasks","_i","enqueue","task","_start","flush","_idleCallback","_cancelCallback","_requestCallback","_process","deadline","taskDuration","deadlineRemaining","longestTask","lastDeadlineRemaining","timeRemaining","performance","now","abs","round","PriorityTaskQueue","_createDeadline","duration","IdleTaskQueue","requestIdleCallback","cancelIdleCallback","_queue","bufferService","lastChar","CHAR_DATA_CODE_INDEX","WHITESPACE_CELL_CODE","ExtendedAttrs","toColorRGB","newObj","isInverse","isBold","isUnderline","hasExtendedAttrs","isBlink","isInvisible","isItalic","isDim","isStrikethrough","isOverline","getFgColorMode","getBgColorMode","isFgRGB","isBgRGB","isFgPalette","isBgPalette","isFgDefault","isBgDefault","isAttributeDefault","getFgColor","getBgColor","isEmpty","getUnderlineColor","getUnderlineColorMode","isUnderlineColorRGB","isUnderlineColorPalette","isUnderlineColorDefault","getUnderlineStyle","getUnderlineVariantOffset","underlineVariantOffset","ext","_urlId","_ext","val","CircularList_1","TaskQueue_1","BufferReflow_1","Marker_1","MAX_BUFFER_SIZE","Buffer","_hasScrollback","markers","_nullCell","fromCharData","NULL_CELL_CHAR","_whitespaceCell","WHITESPACE_CELL_CHAR","WHITESPACE_CELL_WIDTH","_isClearing","_memoryCleanupPosition","_cols","_rows","_getCorrectBufferLength","setupTabStops","_memoryCleanupQueue","clearAllMarkers","getWhitespaceCell","hasScrollback","isCursorInViewport","relativeY","correctBufferLength","scrollback","fillViewportRows","fillAttr","newCols","newRows","nullCell","dirtyMemoryLines","addToY","amountToTrim","_isReflowEnabled","_reflow","maxY","_batchedMemoryCleanup","normalRun","counted","cleanupMemory","_reflowLarger","_reflowSmaller","reflowCursorLine","toRemove","reflowLargerGetLinesToRemove","newLayoutResult","reflowLargerCreateNewLayout","reflowLargerApplyNewLayout","layout","_reflowLargerAdjustViewport","countRemoved","viewportAdjustments","toInsert","countToInsert","wrappedLines","unshift","absoluteY","lastLineLength","destLineLengths","reflowSmallerGetNewLineLengths","linesToAdd","trimmedLines","newLines","newLine","destLineIndex","destCol","srcLineIndex","srcCol","cellsToCopy","wrappedLinesIndex","getWrappedLineTrimmedLength","setCell","insertEvents","originalLines","originalLinesLength","originalLineIndex","nextToInsertIndex","nextToInsert","countInsertedSoFar","nextI","insertCountEmitted","translateBufferLineToString","lineIndex","trimRight","startCol","endCol","translateToString","getWrappedRangeForLine","first","last","tabStopWidth","addMarker","marker","Marker","onDispose","_removeMarker","$startIndex","$workCell","$extended","fillCellData","_combined","_extendedAttrs","_cacheValid","_cache","_cacheTrimmed","_data","cp","CHAR_DATA_ATTR_INDEX","CHAR_DATA_CHAR_INDEX","CHAR_DATA_WIDTH_INDEX","getFg","getBg","getCodePoint","isCombined","loadCell","combinedData","codePoint","width","attrs","$idx","byteLength","uint32Cells","keys","extKeys","copyFrom","blank","_copySparseMapsFrom","getNoBgTrimmedLength","src","applyInReverse","srcData","_copyCellMapsFrom","outColumns","isCanonical","trimEnd","cellContents","chars","srcStart","endsInNull","followingLineStartsWithWide","oldCols","bufferAbsoluteY","srcTrimmedTineLength","srcRemainingCells","destRemainingCells","countToRemove","nextToRemoveIndex","nextToRemoveStart","countRemovedSoFar","newLayout","newLayoutLines","newLineLengths","cellsNeeded","srcLine","cellsAvailable","oldTrimmedLength","endsWithWide","lineLength","Buffer_1","BufferSet","_normalBuffer","_altBuffer","_onBufferActivate","onSpecificOptionChange","_normal","_alt","inactiveBuffer","obj","setFromCharData","getChars","getCode","combined","getAsCharData","attributesEquals","other","thisDefault","otherDefault","DEFAULT_COLOR","DEFAULT_ATTR","DEFAULT_EXT","_id","_nextId","_onDispose","a","c","g","h","k","q","r","t","u","w","z","A","B","C","R","Q","K","Y","E","Z","H","_","codepoint","_interim","target","size","startPos","interim","Uint8Array","byte1","byte2","byte3","byte4","discardInterim","tmp","missing","fourStop","BMP_COMBINING","HIGH_COMBINING","table","version","wcwidth","num","ucs","mid","bisearch","preceding","createPropertyValue","Async_1","_action","_callbacks","_pendingData","_bufferOffset","_isSyncWriting","_syncCalls","_didUserInput","_innerWriteTimer","TimeoutTimer","didProcess","cb","_innerWrite","_scheduleInnerWrite","lastTime","startTime","continuation","catch","low","toLowerCase","RGB_REX","base","HASH_REX","adv","bits","pad","s2","StringBuilder_1","EMPTY_HANDLERS","_handlers","create","_active","_ident","_handlerFb","_stack","loopPosition","fallThrough","registerHandler","handlerList","handlerIndex","clearHandler","setHandlerFallback","put","utf32ToString","success","handlerResult","_handler","LimitedStringBuilder","_payloadLimit","_hitLimit","ret","res","Params_1","unhook","hook","EMPTY_PARAMS","Params","addParam","_params","TransitionTable","Uint16Array","setDefault","next","addMany","codes","NON_ASCII_PRINTABLE","VT500_TRANSITION_TABLE","blueprint","apply","unused","PRINTABLES","EXECUTABLES","states","_transitions","handlers","handlerPos","transition","chunkPos","initialState","currentState","_collect","_printHandlerFb","_executeHandlerFb","_csiHandlerFb","_escHandlerFb","_errorHandlerFb","_printHandler","_executeHandlers","_executeHandlersArr","_csiHandlers","_escHandlers","_oscParser","OscParser","_dcsParser","DcsParser","_apcParser","ApcParser","_errorHandler","_identifier","finalRange","intermediate","finalCode","reverse","clearPrintHandler","clearEscHandler","clearExecuteHandler","clearCsiHandler","clearDcsHandler","clearOscHandler","clearApcHandler","clearErrorHandler","resetZdm","csiDone","addDigit","addSubParam","l4","collect","abort","handlersEsc","jj","_state","_put","fromArray","values","maxSubParamsLength","Int32Array","_subParams","_subParamsLength","_subParamsIdx","_rejectDigits","_rejectSubDigits","_digitIsSub","newParams","getSubParamsAll","cur","_addons","instance","loadAddon","terminal","loadedAddon","_wrappedAddonDispose","activate","BufferLineApiView_1","_buffer","init","cursorY","cursorX","viewportY","baseY","getLine","BufferLineApiView","_line","getCell","startColumn","endColumn","BufferApiView_1","BufferNamespaceApi","_core","_onBufferChange","onBufferChange","BufferApiView","_alternate","alternate","addCsiHandler","addDcsHandler","addEscHandler","addOscHandler","provider","versions","activeVersion","BufferSet_1","colsChanged","rowsChanged","_cachedBlankLine","topRow","bottomRow","willBufferBeTrimmed","oldYdisp","_charsets","DEFAULT_MODES","DEFAULT_DEC_PRIVATE_MODES","_onUserInput","_onRequestScrollToBottom","showCursorImmediately","structuredClone","disableStdin","scrollOnUserInput","triggerBinaryEvent","ServiceRegistry_1","ServiceCollection","entries","_entries","Map","service","forEach","has","_services","IInstantiationService","getService","ctor","args","serviceDependencies","getServiceDependencies","sort","serviceArgs","dependency","name","firstServiceArgPos","optionsKeyToLogLevel","info","INFO","ERROR","off","OFF","_logLevel","_updateLogLevel","_evalLazyOptionalParams","optionalParams","_log","message","logger","log","DEFAULT_PROTOCOLS","NONE","restrict","X10","button","ctrl","VT200","DRAG","ANY","eventCode","isSGR","S","DEFAULT_ENCODINGS","DEFAULT","col","SGR","SGR_PIXELS","_protocols","_encodings","_activeProtocol","_activeEncoding","_onProtocolChange","onProtocolChange","addProtocol","addEncoding","protocol","encoding","areMouseEventsActive","setCustomWheelEventHandler","customWheelEventHandler","_customWheelEventHandler","allowCustomWheelEvent","restrictMouseEvent","encodeMouseEvent","isDefaultEncoding","isPixelEncoding","Platform_1","DEFAULT_OPTIONS","blinkIntervalDuration","cursorWidth","cursorInactiveStyle","drawBoldTextInBrightColors","documentOverride","fastScrollSensitivity","fontFamily","fontSize","fontWeight","fontWeightBold","ignoreBracketedPasteMode","lineHeight","letterSpacing","linkHandler","scrollbar","showScrollbar","scrollSensitivity","smoothScrollDuration","macOptionIsMeta","macOptionClickForcesSelection","minimumContrastRatio","mouseEventsRequireAlt","allowProposedApi","allowTransparency","theme","rescaleOverlappingGlyphs","rightClickSelectsWord","wordSeparator","altClickMovesCursor","FONT_WEIGHT_OPTIONS","_onOptionChange","onOptionChange","defaultOptions","newValue","_sanitizeAndValidateOption","_setupOptions","eventKey","getter","propName","setter","desc","defineProperty","isCursorStyle","floor","_entriesWithId","_dataByLinkId","_removeMarkerFromLink","castData","_getEntryIdKey","every","getLinkData","linkData","delete","serviceRegistry","decorator","arguments","storeServiceDependency","createDecorator","IDecorationService","_providers","_onChange","onChange","extractCharKind","_activeProvider","getStringCellWidth","precedingInfo","CoreTerminal_1","Terminal","_onBell","onBell","_onA11yCharEmitter","_onA11yTabEmitter","registerMarker","cursorYOffset","__webpack_module_cache__","moduleId","cachedModule","module","__webpack_modules__","BufferNamespaceApi_1","ParserApi_1","UnicodeApi_1","Terminal_1","AddonManager_1","CONSTRUCTOR_ONLY_OPTIONS","_addonManager","AddonManager","_publicOptions","_checkReadonlyOptions","_checkProposedApi","parser","ParserApi","unicode","UnicodeApi","mouseTrackingMode","applicationCursorKeysMode","applicationKeypadMode","originMode","reverseWraparoundMode","sendFocusMode","showCursor","synchronizedOutputMode","columns","_verifyIntegers","writeln","addon","Infinity"],"sourceRoot":""} +\ No newline at end of file ++{"version":3,"file":"xterm-headless.js","mappings":"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S,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAC7C,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAC7C,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAC7C,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAC7C,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAC7C,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAC7C,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAC7C,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAC7C,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAC7C,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAC7C,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAC7C,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAC7C,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAC7C,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAC7C,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAC7C,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAC7C,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAC7C,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAC7C,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAC7C,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAC7C,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAC7C,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAC7C,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAC7C,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAC7C,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAC7C,CAAC,KAAQ,MAAS,CAAC,KAAQ,MAAS,CAAC,MAAQ,OAC7C,CAAC,MAAQ,OAAS,CA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","sources":["webpack://@xterm/xterm/./src/common/Async.ts","webpack://@xterm/xterm/./src/common/CircularList.ts","webpack://@xterm/xterm/./src/common/CoreTerminal.ts","webpack://@xterm/xterm/./src/common/Event.ts","webpack://@xterm/xterm/./src/common/InputHandler.ts","webpack://@xterm/xterm/./src/common/Lifecycle.ts","webpack://@xterm/xterm/./src/common/Platform.ts","webpack://@xterm/xterm/./src/common/StringBuilder.ts","webpack://@xterm/xterm/./src/common/TaskQueue.ts","webpack://@xterm/xterm/./src/common/Version.ts","webpack://@xterm/xterm/./src/common/WindowsMode.ts","webpack://@xterm/xterm/./src/common/buffer/AttributeData.ts","webpack://@xterm/xterm/./src/common/buffer/Buffer.ts","webpack://@xterm/xterm/./src/common/buffer/BufferLine.ts","webpack://@xterm/xterm/./src/common/buffer/BufferReflow.ts","webpack://@xterm/xterm/./src/common/buffer/BufferSet.ts","webpack://@xterm/xterm/./src/common/buffer/CellData.ts","webpack://@xterm/xterm/./src/common/buffer/Constants.ts","webpack://@xterm/xterm/./src/common/buffer/Marker.ts","webpack://@xterm/xterm/./src/common/data/Charsets.ts","webpack://@xterm/xterm/./src/common/input/TextDecoder.ts","webpack://@xterm/xterm/./src/common/input/UnicodeV6.ts","webpack://@xterm/xterm/./src/common/input/WriteBuffer.ts","webpack://@xterm/xterm/./src/common/input/XParseColor.ts","webpack://@xterm/xterm/./src/common/parser/ApcParser.ts","webpack://@xterm/xterm/./src/common/parser/DcsParser.ts","webpack://@xterm/xterm/./src/common/parser/EscapeSequenceParser.ts","webpack://@xterm/xterm/./src/common/parser/OscParser.ts","webpack://@xterm/xterm/./src/common/parser/Params.ts","webpack://@xterm/xterm/./src/common/public/AddonManager.ts","webpack://@xterm/xterm/./src/common/public/BufferApiView.ts","webpack://@xterm/xterm/./src/common/public/BufferLineApiView.ts","webpack://@xterm/xterm/./src/common/public/BufferNamespaceApi.ts","webpack://@xterm/xterm/./src/common/public/ParserApi.ts","webpack://@xterm/xterm/./src/common/public/UnicodeApi.ts","webpack://@xterm/xterm/./src/common/services/BufferService.ts","webpack://@xterm/xterm/./src/common/services/CharsetService.ts","webpack://@xterm/xterm/./src/common/services/CoreService.ts","webpack://@xterm/xterm/./src/common/services/InstantiationService.ts","webpack://@xterm/xterm/./src/common/services/LogService.ts","webpack://@xterm/xterm/./src/common/services/MouseStateService.ts","webpack://@xterm/xterm/./src/common/services/OptionsService.ts","webpack://@xterm/xterm/./src/common/services/OscLinkService.ts","webpack://@xterm/xterm/./src/common/services/ServiceRegistry.ts","webpack://@xterm/xterm/./src/common/services/Services.ts","webpack://@xterm/xterm/./src/common/services/UnicodeService.ts","webpack://@xterm/xterm/./src/headless/Terminal.ts","webpack://@xterm/xterm/webpack/bootstrap","webpack://@xterm/xterm/./src/headless/public/Terminal.ts"],"sourcesContent":["/**\n * Copyright (c) 2026 The xterm.js authors. All rights reserved.\n * @license MIT\n *\n * Minimal async helpers for xterm.js core.\n */\n\nimport { DisposableStore, IDisposable, toDisposable } from './Lifecycle';\n\nexport function timeout(millis: number): Promise {\n return new Promise(resolve => setTimeout(resolve, millis));\n}\n\n/**\n * Creates a timeout that can be disposed using its returned value.\n * @param handler The timeout handler.\n * @param timeout An optional timeout in milliseconds.\n * @param store An optional {@link DisposableStore} that will have the timeout disposable managed\n * automatically.\n */\nexport function disposableTimeout(handler: () => void, timeout = 0, store?: DisposableStore): IDisposable {\n const timer = setTimeout(() => {\n handler();\n if (store) {\n disposable.dispose();\n }\n }, timeout);\n const disposable = toDisposable(() => {\n clearTimeout(timer);\n });\n store?.add(disposable);\n return disposable;\n}\n\nexport class TimeoutTimer implements IDisposable {\n private _token: any = -1;\n private _isDisposed = false;\n\n public dispose(): void {\n this.cancel();\n this._isDisposed = true;\n }\n\n public cancel(): void {\n if (this._token !== -1) {\n clearTimeout(this._token);\n this._token = -1;\n }\n }\n\n public cancelAndSet(runner: () => void, timeout: number): void {\n if (this._isDisposed) {\n throw new Error('Calling cancelAndSet on a disposed TimeoutTimer');\n }\n this.cancel();\n this._token = setTimeout(() => {\n this._token = -1;\n runner();\n }, timeout);\n }\n\n public setIfNotSet(runner: () => void, timeout: number): void {\n if (this._isDisposed) {\n throw new Error('Calling setIfNotSet on a disposed TimeoutTimer');\n }\n if (this._token !== -1) {\n return;\n }\n this._token = setTimeout(() => {\n this._token = -1;\n runner();\n }, timeout);\n }\n}\n\n/**\n * Schedules a single runner on the microtask queue. Unlike {@link TimeoutTimer}, a scheduled\n * microtask cannot be unqueued; {@link cancel} prevents the runner from executing if it has not\n * run yet.\n */\nexport class MicrotaskTimer implements IDisposable {\n private _isScheduled = false;\n private _isDisposed = false;\n\n public dispose(): void {\n this.cancel();\n this._isDisposed = true;\n }\n\n public cancel(): void {\n this._isScheduled = false;\n }\n\n public set(runner: () => void): void {\n if (this._isDisposed) {\n throw new Error('Calling set on a disposed MicrotaskTimer');\n }\n if (this._isScheduled) {\n return;\n }\n this._isScheduled = true;\n queueMicrotask(() => {\n if (!this._isScheduled) {\n return;\n }\n this._isScheduled = false;\n runner();\n });\n }\n}\n\nexport class IntervalTimer implements IDisposable {\n private _disposable: IDisposable | undefined;\n private _isDisposed = false;\n\n public cancel(): void {\n this._disposable?.dispose();\n this._disposable = undefined;\n }\n\n public cancelAndSet(runner: () => void, interval: number, context: Window | typeof globalThis = globalThis): void {\n if (this._isDisposed) {\n throw new Error('Calling cancelAndSet on a disposed IntervalTimer');\n }\n this.cancel();\n const handle = context.setInterval(() => {\n runner();\n }, interval);\n this._disposable = {\n dispose: () => {\n context.clearInterval(handle as any);\n this._disposable = undefined;\n }\n };\n }\n\n public dispose(): void {\n this.cancel();\n this._isDisposed = true;\n }\n}\n","/**\n * Copyright (c) 2016 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { Disposable } from './Lifecycle';\nimport { Emitter, type IEvent } from './Event';\n\nexport interface IInsertEvent {\n index: number;\n amount: number;\n}\n\nexport interface IDeleteEvent {\n index: number;\n amount: number;\n}\n\nexport interface ICircularList {\n length: number;\n maxLength: number;\n isFull: boolean;\n\n onDeleteEmitter: Emitter;\n onDelete: IEvent;\n onInsertEmitter: Emitter;\n onInsert: IEvent;\n onTrimEmitter: Emitter;\n onTrim: IEvent;\n\n get(index: number): T | undefined;\n set(index: number, value: T): void;\n push(value: T): void;\n recycle(): T;\n pop(): T | undefined;\n splice(start: number, deleteCount: number, ...items: T[]): void;\n trimStart(count: number): void;\n shiftElements(start: number, count: number, offset: number): void;\n}\n\n/**\n * Represents a circular list; a list with a maximum size that wraps around when push is called,\n * overriding values at the start of the list.\n */\nexport class CircularList extends Disposable implements ICircularList {\n protected _array: (T | undefined)[];\n private _startIndex: number;\n private _length: number;\n\n public readonly onDeleteEmitter = this._register(new Emitter());\n public readonly onDelete = this.onDeleteEmitter.event;\n public readonly onInsertEmitter = this._register(new Emitter());\n public readonly onInsert = this.onInsertEmitter.event;\n public readonly onTrimEmitter = this._register(new Emitter());\n public readonly onTrim = this.onTrimEmitter.event;\n\n constructor(\n private _maxLength: number\n ) {\n super();\n this._array = new Array(this._maxLength);\n this._startIndex = 0;\n this._length = 0;\n }\n\n public get maxLength(): number {\n return this._maxLength;\n }\n\n public set maxLength(newMaxLength: number) {\n // There was no change in maxLength, return early.\n if (this._maxLength === newMaxLength) {\n return;\n }\n\n // Reconstruct array, starting at index 0. Only transfer values from the\n // indexes 0 to length.\n const newArray = new Array(newMaxLength);\n for (let i = 0; i < Math.min(newMaxLength, this.length); i++) {\n newArray[i] = this._array[this._getCyclicIndex(i)];\n }\n this._array = newArray;\n this._maxLength = newMaxLength;\n this._startIndex = 0;\n }\n\n public get length(): number {\n return this._length;\n }\n\n public set length(newLength: number) {\n if (newLength > this._length) {\n for (let i = this._length; i < newLength; i++) {\n this._array[i] = undefined;\n }\n }\n this._length = newLength;\n }\n\n /**\n * Gets the value at an index.\n *\n * Note that for performance reasons there is no bounds checking here, the index reference is\n * circular so this should always return a value and never throw.\n * @param index The index of the value to get.\n * @returns The value corresponding to the index.\n */\n public get(index: number): T | undefined {\n return this._array[this._getCyclicIndex(index)];\n }\n\n /**\n * Sets the value at an index.\n *\n * Note that for performance reasons there is no bounds checking here, the index reference is\n * circular so this should always return a value and never throw.\n * @param index The index to set.\n * @param value The value to set.\n */\n public set(index: number, value: T | undefined): void {\n this._array[this._getCyclicIndex(index)] = value;\n }\n\n /**\n * Pushes a new value onto the list, wrapping around to the start of the array, overriding index 0\n * if the maximum length is reached.\n * @param value The value to push onto the list.\n */\n public push(value: T): void {\n this._array[this._getCyclicIndex(this._length)] = value;\n if (this._length === this._maxLength) {\n this._startIndex = ++this._startIndex % this._maxLength;\n this.onTrimEmitter.fire(1);\n } else {\n this._length++;\n }\n }\n\n /**\n * Advance ringbuffer index and return current element for recycling.\n * Note: The buffer must be full for this method to work.\n * @throws When the buffer is not full.\n */\n public recycle(): T {\n if (this._length !== this._maxLength) {\n throw new Error('Can only recycle when the buffer is full');\n }\n this._startIndex = ++this._startIndex % this._maxLength;\n this.onTrimEmitter.fire(1);\n return this._array[this._getCyclicIndex(this._length - 1)]!;\n }\n\n /**\n * Ringbuffer is at max length.\n */\n public get isFull(): boolean {\n return this._length === this._maxLength;\n }\n\n /**\n * Removes and returns the last value on the list.\n * @returns The popped value.\n */\n public pop(): T | undefined {\n return this._array[this._getCyclicIndex(this._length-- - 1)];\n }\n\n /**\n * Deletes and/or inserts items at a particular index (in that order). Unlike\n * Array.prototype.splice, this operation does not return the deleted items as a new array in\n * order to save creating a new array. Note that this operation may shift all values in the list\n * in the worst case.\n * @param start The index to delete and/or insert.\n * @param deleteCount The number of elements to delete.\n * @param items The items to insert.\n */\n public splice(start: number, deleteCount: number, ...items: T[]): void {\n // Delete items\n if (deleteCount) {\n for (let i = start; i < this._length - deleteCount; i++) {\n this._array[this._getCyclicIndex(i)] = this._array[this._getCyclicIndex(i + deleteCount)];\n }\n this._length -= deleteCount;\n this.onDeleteEmitter.fire({ index: start, amount: deleteCount });\n }\n\n // Add items\n for (let i = this._length - 1; i >= start; i--) {\n this._array[this._getCyclicIndex(i + items.length)] = this._array[this._getCyclicIndex(i)];\n }\n for (let i = 0; i < items.length; i++) {\n this._array[this._getCyclicIndex(start + i)] = items[i];\n }\n if (items.length) {\n this.onInsertEmitter.fire({ index: start, amount: items.length });\n }\n\n // Adjust length as needed\n if (this._length + items.length > this._maxLength) {\n const countToTrim = (this._length + items.length) - this._maxLength;\n this._startIndex += countToTrim;\n this._length = this._maxLength;\n this.onTrimEmitter.fire(countToTrim);\n } else {\n this._length += items.length;\n }\n }\n\n /**\n * Trims a number of items from the start of the list.\n * @param count The number of items to remove.\n */\n public trimStart(count: number): void {\n if (count > this._length) {\n count = this._length;\n }\n this._startIndex += count;\n this._length -= count;\n this.onTrimEmitter.fire(count);\n }\n\n public shiftElements(start: number, count: number, offset: number): void {\n if (count <= 0) {\n return;\n }\n if (start < 0 || start >= this._length) {\n throw new Error('start argument out of range');\n }\n if (start + offset < 0) {\n throw new Error('Cannot shift elements in list beyond index 0');\n }\n\n if (offset > 0) {\n for (let i = count - 1; i >= 0; i--) {\n this.set(start + i + offset, this.get(start + i));\n }\n const expandListBy = (start + count + offset) - this._length;\n if (expandListBy > 0) {\n this._length += expandListBy;\n while (this._length > this._maxLength) {\n this._length--;\n this._startIndex++;\n this.onTrimEmitter.fire(1);\n }\n }\n } else {\n for (let i = 0; i < count; i++) {\n this.set(start + i + offset, this.get(start + i));\n }\n }\n }\n\n /**\n * Gets the cyclic index for the specified regular index. The cyclic index can then be used on the\n * backing array to get the element associated with the regular index.\n * @param index The regular index.\n * @returns The cyclic index.\n */\n private _getCyclicIndex(index: number): number {\n return (this._startIndex + index) % this._maxLength;\n }\n}\n","/**\n * Copyright (c) 2014-2020 The xterm.js authors. All rights reserved.\n * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)\n * @license MIT\n *\n * Originally forked from (with the author's permission):\n * Fabrice Bellard's javascript vt100 for jslinux:\n * http://bellard.org/jslinux/\n * Copyright (c) 2011 Fabrice Bellard\n * The original design remains. The terminal itself\n * has been extended to include xterm CSI codes, among\n * other features.\n *\n * Terminal Emulation References:\n * http://vt100.net/\n * http://invisible-island.net/xterm/ctlseqs/ctlseqs.txt\n * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html\n * http://invisible-island.net/vttest/\n * http://www.inwap.com/pdp10/ansicode.txt\n * http://linux.die.net/man/4/console_codes\n * http://linux.die.net/man/7/urxvt\n */\n\nimport { IInstantiationService, IOptionsService, IBufferService, ILogService, ICharsetService, ICoreService, IMouseStateService, IUnicodeService, LogLevelEnum, IOscLinkService } from './services/Services';\nimport { InstantiationService } from './services/InstantiationService';\nimport { LogService } from './services/LogService';\nimport { BufferService, BufferServiceConstants } from './services/BufferService';\nimport { OptionsService } from './services/OptionsService';\nimport { IDisposable, IScrollEvent, ITerminalOptions, IParams } from './Types';\nimport { IAttributeData, IBufferSet } from './buffer/Types';\nimport { CoreService } from './services/CoreService';\nimport { MouseStateService } from './services/MouseStateService';\nimport { UnicodeV6 } from './input/UnicodeV6';\nimport { UnicodeService } from './services/UnicodeService';\nimport { CharsetService } from './services/CharsetService';\nimport { updateWindowsModeWrappedState } from './WindowsMode';\nimport { IFunctionIdentifier } from './parser/Types';\nimport { InputHandler } from './InputHandler';\nimport { WriteBuffer } from './input/WriteBuffer';\nimport { OscLinkService } from './services/OscLinkService';\nimport { Emitter, EventUtils, type IEvent } from './Event';\nimport { Disposable, MutableDisposable, toDisposable } from './Lifecycle';\n\n// Only trigger this warning a single time per session\nlet hasWriteSyncWarnHappened = false;\n\nexport interface ICoreTerminal {\n mouseStateService: IMouseStateService;\n coreService: ICoreService;\n optionsService: IOptionsService;\n unicodeService: IUnicodeService;\n buffers: IBufferSet;\n options: Required;\n registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise): IDisposable;\n registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise): IDisposable;\n registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise): IDisposable;\n registerOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable;\n registerApcHandler(id: IFunctionIdentifier, callback: (data: string) => boolean | Promise): IDisposable;\n}\n\nexport abstract class CoreTerminal extends Disposable implements ICoreTerminal {\n protected readonly _instantiationService: IInstantiationService;\n protected readonly _bufferService: IBufferService;\n protected readonly _logService: ILogService;\n protected readonly _charsetService: ICharsetService;\n protected readonly _oscLinkService: IOscLinkService;\n\n public readonly mouseStateService: IMouseStateService;\n public readonly coreService: ICoreService;\n public readonly unicodeService: IUnicodeService;\n public readonly optionsService: IOptionsService;\n\n protected _inputHandler: InputHandler;\n private _writeBuffer: WriteBuffer;\n private _windowsWrappingHeuristics = this._register(new MutableDisposable());\n\n private readonly _onBinary = this._register(new Emitter());\n public readonly onBinary = this._onBinary.event;\n private readonly _onData = this._register(new Emitter());\n public readonly onData = this._onData.event;\n protected _onLineFeed = this._register(new Emitter());\n public readonly onLineFeed = this._onLineFeed.event;\n protected readonly _onRender = this._register(new Emitter<{ start: number, end: number }>());\n public readonly onRender = this._onRender.event;\n private readonly _onResize = this._register(new Emitter<{ cols: number, rows: number }>());\n public readonly onResize = this._onResize.event;\n protected readonly _onWriteParsed = this._register(new Emitter());\n public readonly onWriteParsed = this._onWriteParsed.event;\n\n /**\n * Internally we track the source of the scroll but this is meaningless outside the library so\n * it's filtered out.\n */\n protected _onScrollApi?: Emitter;\n protected _onScroll = this._register(new Emitter());\n public get onScroll(): IEvent {\n if (!this._onScrollApi) {\n this._onScrollApi = this._register(new Emitter());\n this._onScroll.event(ev => {\n this._onScrollApi?.fire(ev.position);\n });\n }\n return this._onScrollApi.event;\n }\n\n public get cols(): number { return this._bufferService.cols; }\n public get rows(): number { return this._bufferService.rows; }\n public get buffers(): IBufferSet { return this._bufferService.buffers; }\n public get options(): Required { return this.optionsService.options; }\n public set options(options: ITerminalOptions) {\n for (const key in options) {\n this.optionsService.options[key] = options[key];\n }\n }\n\n constructor(\n options: Partial\n ) {\n super();\n\n // Setup and initialize services\n this._instantiationService = new InstantiationService();\n this.optionsService = this._register(new OptionsService(options));\n this._instantiationService.setService(IOptionsService, this.optionsService);\n this._logService = this._register(this._instantiationService.createInstance(LogService));\n this._instantiationService.setService(ILogService, this._logService);\n this._bufferService = this._register(this._instantiationService.createInstance(BufferService));\n this._instantiationService.setService(IBufferService, this._bufferService);\n this.coreService = this._register(this._instantiationService.createInstance(CoreService));\n this._instantiationService.setService(ICoreService, this.coreService);\n this.mouseStateService = this._register(this._instantiationService.createInstance(MouseStateService));\n this._instantiationService.setService(IMouseStateService, this.mouseStateService);\n this.unicodeService = this._register(this._instantiationService.createInstance(UnicodeService));\n this.unicodeService.register(new UnicodeV6());\n this._instantiationService.setService(IUnicodeService, this.unicodeService);\n this._charsetService = this._instantiationService.createInstance(CharsetService);\n this._instantiationService.setService(ICharsetService, this._charsetService);\n this._oscLinkService = this._instantiationService.createInstance(OscLinkService);\n this._instantiationService.setService(IOscLinkService, this._oscLinkService);\n\n\n // Register input handler and handle/forward events\n this._inputHandler = this._register(new InputHandler(this._bufferService, this._charsetService, this.coreService, this._logService, this.optionsService, this._oscLinkService, this.mouseStateService, this.unicodeService));\n this._register(EventUtils.forward(this._inputHandler.onLineFeed, this._onLineFeed));\n\n // Setup listeners\n this._register(EventUtils.forward(this._bufferService.onResize, this._onResize));\n this._register(EventUtils.forward(this.coreService.onData, this._onData));\n this._register(EventUtils.forward(this.coreService.onBinary, this._onBinary));\n this._register(this.coreService.onRequestScrollToBottom(() => this.scrollToBottom(true)));\n this._register(this.coreService.onUserInput(() => this._writeBuffer.handleUserInput()));\n this._register(this.optionsService.onMultipleOptionChange(['windowsPty'], () => this._handleWindowsPtyOptionChange()));\n this._register(this._bufferService.onScroll(() => {\n this._onScroll.fire({ position: this._bufferService.buffer.ydisp });\n this._inputHandler.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom);\n }));\n // Setup WriteBuffer\n this._writeBuffer = this._register(new WriteBuffer((data, promiseResult) => this._inputHandler.parse(data, promiseResult)));\n this._register(EventUtils.forward(this._writeBuffer.onWriteParsed, this._onWriteParsed));\n }\n\n public write(data: string | Uint8Array, callback?: () => void): void {\n this._writeBuffer.write(data, callback);\n }\n\n /**\n * Write data to terminal synchonously.\n *\n * This method is unreliable with async parser handlers, thus should not\n * be used anymore. If you need blocking semantics on data input consider\n * `write` with a callback instead.\n *\n * @deprecated Unreliable, will be removed soon.\n */\n public writeSync(data: string | Uint8Array, maxSubsequentCalls?: number): void {\n if (this._logService.logLevel <= LogLevelEnum.WARN && !hasWriteSyncWarnHappened) {\n this._logService.warn('writeSync is unreliable and will be removed soon.');\n hasWriteSyncWarnHappened = true;\n }\n this._writeBuffer.writeSync(data, maxSubsequentCalls);\n }\n\n public input(data: string, wasUserInput: boolean = true): void {\n this.coreService.triggerDataEvent(data, wasUserInput);\n }\n\n public resize(x: number, y: number): void {\n if (isNaN(x) || isNaN(y)) {\n return;\n }\n\n x = Math.max(x, BufferServiceConstants.MINIMUM_COLS);\n y = Math.max(y, BufferServiceConstants.MINIMUM_ROWS);\n\n // Flush pending writes before resize to avoid race conditions where async\n // writes are processed with incorrect dimensions\n this._writeBuffer.flushSync();\n\n this._bufferService.resize(x, y);\n }\n\n /**\n * Scroll the terminal down 1 row, creating a blank line.\n * @param eraseAttr The attribute data to use the for blank line.\n * @param isWrapped Whether the new line is wrapped from the previous line.\n */\n public scroll(eraseAttr: IAttributeData, isWrapped: boolean = false): void {\n this._bufferService.scroll(eraseAttr, isWrapped);\n }\n\n /**\n * Scroll the display of the terminal\n * @param disp The number of lines to scroll down (negative scroll up).\n * @param suppressScrollEvent Don't emit the scroll event as scrollLines. This is used to avoid\n * unwanted events being handled by the viewport when the event was triggered from the viewport\n * originally.\n */\n public scrollLines(disp: number, suppressScrollEvent?: boolean): void {\n this._bufferService.scrollLines(disp, suppressScrollEvent);\n }\n\n public scrollPages(pageCount: number): void {\n this.scrollLines(pageCount * (this.rows - 1));\n }\n\n public scrollToTop(): void {\n this.scrollLines(-this._bufferService.buffer.ydisp);\n }\n\n public scrollToBottom(disableSmoothScroll?: boolean): void {\n this.scrollLines(this._bufferService.buffer.ybase - this._bufferService.buffer.ydisp);\n }\n\n public scrollToLine(line: number): void {\n const scrollAmount = line - this._bufferService.buffer.ydisp;\n if (scrollAmount !== 0) {\n this.scrollLines(scrollAmount);\n }\n }\n\n /** Add handler for ESC escape sequence. See xterm.d.ts for details. */\n public registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise): IDisposable {\n return this._inputHandler.registerEscHandler(id, callback);\n }\n\n /** Add handler for DCS escape sequence. See xterm.d.ts for details. */\n public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise): IDisposable {\n return this._inputHandler.registerDcsHandler(id, callback);\n }\n\n /** Add handler for CSI escape sequence. See xterm.d.ts for details. */\n public registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise): IDisposable {\n return this._inputHandler.registerCsiHandler(id, callback);\n }\n\n /** Add handler for OSC escape sequence. See xterm.d.ts for details. */\n public registerOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable {\n return this._inputHandler.registerOscHandler(ident, callback);\n }\n\n /** Add handler for APC escape sequence. See xterm.d.ts for details. */\n public registerApcHandler(id: IFunctionIdentifier, callback: (data: string) => boolean | Promise): IDisposable {\n return this._inputHandler.registerApcHandler(id, callback);\n }\n\n protected _setup(): void {\n this._handleWindowsPtyOptionChange();\n }\n\n public reset(): void {\n this._inputHandler.reset();\n this._bufferService.reset();\n this._charsetService.reset();\n this.coreService.reset();\n this.mouseStateService.reset();\n }\n\n\n private _handleWindowsPtyOptionChange(): void {\n let value = false;\n const windowsPty = this.optionsService.rawOptions.windowsPty;\n if (windowsPty && windowsPty.backend !== undefined && windowsPty.buildNumber !== undefined) {\n value = !!(windowsPty.backend === 'conpty' && windowsPty.buildNumber < 21376);\n }\n if (value) {\n this._enableWindowsWrappingHeuristics();\n } else {\n this._windowsWrappingHeuristics.clear();\n }\n }\n\n protected _enableWindowsWrappingHeuristics(): void {\n if (!this._windowsWrappingHeuristics.value) {\n const disposables: IDisposable[] = [];\n disposables.push(this.onLineFeed(updateWindowsModeWrappedState.bind(null, this._bufferService)));\n disposables.push(this.registerCsiHandler({ final: 'H' }, () => {\n updateWindowsModeWrappedState(this._bufferService);\n return false;\n }));\n this._windowsWrappingHeuristics.value = toDisposable(() => {\n for (const d of disposables) {\n d.dispose();\n }\n });\n }\n }\n}\n","/**\n * Copyright (c) 2024-2026 The xterm.js authors. All rights reserved.\n * @license MIT\n *\n * Minimal event utilities for xterm.js core.\n * Simplified from VS Code's event.ts - no leak detection/profiling.\n */\n\nimport { IDisposable, DisposableStore, toDisposable } from './Lifecycle';\n\nexport interface IEvent {\n (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore): IDisposable;\n}\n\nexport class Emitter {\n private _listeners: { fn: (e: T) => any, thisArgs: any }[] = [];\n private _disposed = false;\n private _event: IEvent | undefined;\n\n public get event(): IEvent {\n if (this._event) {\n return this._event;\n }\n this._event = (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => {\n if (this._disposed) {\n return toDisposable(() => {});\n }\n\n const entry = { fn: listener, thisArgs };\n this._listeners = this._listeners.slice();\n this._listeners.push(entry);\n\n const result = toDisposable(() => {\n const idx = this._listeners.indexOf(entry);\n if (idx !== -1) {\n this._listeners = this._listeners.slice();\n this._listeners.splice(idx, 1);\n }\n });\n\n if (disposables) {\n if (Array.isArray(disposables)) {\n disposables.push(result);\n } else {\n disposables.add(result);\n }\n }\n\n return result;\n };\n return this._event;\n }\n\n public fire(event: T): void {\n if (this._disposed || !this._listeners.length) {\n return;\n }\n if (this._listeners.length === 1) {\n this._listeners[0].fn.call(this._listeners[0].thisArgs, event);\n return;\n }\n const listeners = this._listeners;\n for (let i = 0, len = listeners.length; i < len; ++i) {\n listeners[i].fn.call(listeners[i].thisArgs, event);\n }\n }\n\n public dispose(): void {\n if (this._disposed) {\n return;\n }\n this._disposed = true;\n this._listeners.length = 0;\n }\n}\n\nexport namespace EventUtils {\n export function forward(from: IEvent, to: Emitter): IDisposable {\n return from(e => to.fire(e));\n }\n\n export function map(event: IEvent, map: (i: I) => O): IEvent {\n return (listener: (e: O) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => {\n return event(i => listener.call(thisArgs, map(i)), undefined, disposables);\n };\n }\n\n export function any(...events: IEvent[]): IEvent;\n export function any(...events: IEvent[]): IEvent;\n export function any(...events: IEvent[]): IEvent {\n return (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => {\n const store = new DisposableStore();\n for (const event of events) {\n store.add(event(e => listener.call(thisArgs, e)));\n }\n if (disposables) {\n if (Array.isArray(disposables)) {\n disposables.push(store);\n } else {\n disposables.add(store);\n }\n }\n return store;\n };\n }\n\n export function runAndSubscribe(event: IEvent, handler: (e: T) => void, initial: T): IDisposable;\n export function runAndSubscribe(event: IEvent, handler: (e: T | undefined) => void): IDisposable;\n export function runAndSubscribe(event: IEvent, handler: (e: T | undefined) => void, initial?: T): IDisposable {\n handler(initial);\n return event(e => handler(e));\n }\n}\n","/**\n * Copyright (c) 2014 The xterm.js authors. All rights reserved.\n * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)\n * @license MIT\n */\n\nimport { IInputHandler, IDisposable, IWindowOptions, IColorEvent, IParseStack, ColorIndex, ColorRequestType, SpecialColorIndex } from './Types';\nimport { IAttributeData, IBuffer } from './buffer/Types';\nimport { C0, C1 } from './data/EscapeSequences';\nimport { CHARSETS, DEFAULT_CHARSET } from './data/Charsets';\nimport { EscapeSequenceParser } from './parser/EscapeSequenceParser';\nimport { Disposable } from './Lifecycle';\nimport { StringToUtf32, stringFromCodePoint, Utf8ToUtf32 } from './input/TextDecoder';\nimport { BufferLine, DEFAULT_ATTR_DATA } from './buffer/BufferLine';\nimport { IParsingState, IEscapeSequenceParser, IParams, IFunctionIdentifier } from './parser/Types';\nimport { NULL_CELL_CODE, NULL_CELL_WIDTH, Attributes, FgFlags, BgFlags, Content, UnderlineStyle } from './buffer/Constants';\nimport { CellData } from './buffer/CellData';\nimport { AttributeData } from './buffer/AttributeData';\nimport { ICoreService, IBufferService, IOptionsService, ILogService, IMouseStateService, ICharsetService, IUnicodeService, LogLevelEnum, IOscLinkService } from './services/Services';\nimport { UnicodeService } from './services/UnicodeService';\nimport { OscHandler } from './parser/OscParser';\nimport { DcsHandler } from './parser/DcsParser';\nimport { ApcHandler } from './parser/ApcParser';\nimport { parseColor } from './input/XParseColor';\nimport { Emitter } from './Event';\nimport { XTERM_VERSION } from './Version';\n\n/**\n * Map collect to glevel. Used in `selectCharset`.\n */\nconst GLEVEL: { [key: string]: number } = { '(': 0, ')': 1, '*': 2, '+': 3, '-': 1, '.': 2 };\n\n/**\n * Document xterm VT features here that are currently unsupported\n */\n// @vt: #N DCS DECUDK \"User Defined Keys\" \"DCS Ps ; Ps \\| Pt ST\" \"Definitions for user-defined keys.\"\n// @vt: #N DCS XTGETTCAP \"Request Terminfo String\" \"DCS + q Pt ST\" \"Request Terminfo String.\"\n// @vt: #N DCS XTSETTCAP \"Set Terminfo Data\" \"DCS + p Pt ST\" \"Set Terminfo Data.\"\n// @vt: #N OSC 1 \"Set Icon Name\" \"OSC 1 ; Pt BEL\" \"Set icon name.\"\n\n/**\n * Max length of the UTF32 input buffer. Real memory consumption is 4 times higher.\n */\nconst enum Constants {\n MAX_PARSEBUFFER_LENGTH = 131072,\n /** Limit length of title and icon name stacks. */\n STACK_LIMIT = 10,\n // create a warning log if an async handler takes longer than the limit (in ms)\n SLOW_ASYNC_LIMIT = 5000\n}\n\n// map params to window option\nfunction paramToWindowOption(n: number, opts: IWindowOptions): boolean {\n if (n > 24) {\n return opts.setWinLines || false;\n }\n switch (n) {\n case 1: return !!opts.restoreWin;\n case 2: return !!opts.minimizeWin;\n case 3: return !!opts.setWinPosition;\n case 4: return !!opts.setWinSizePixels;\n case 5: return !!opts.raiseWin;\n case 6: return !!opts.lowerWin;\n case 7: return !!opts.refreshWin;\n case 8: return !!opts.setWinSizeChars;\n case 9: return !!opts.maximizeWin;\n case 10: return !!opts.fullscreenWin;\n case 11: return !!opts.getWinState;\n case 13: return !!opts.getWinPosition;\n case 14: return !!opts.getWinSizePixels;\n case 15: return !!opts.getScreenSizePixels;\n case 16: return !!opts.getCellSizePixels;\n case 18: return !!opts.getWinSizeChars;\n case 19: return !!opts.getScreenSizeChars;\n case 20: return !!opts.getIconTitle;\n case 21: return !!opts.getWinTitle;\n case 22: return !!opts.pushTitle;\n case 23: return !!opts.popTitle;\n case 24: return !!opts.setWinLines;\n }\n return false;\n}\n\nexport enum WindowsOptionsReportType {\n GET_WIN_SIZE_PIXELS = 0,\n GET_CELL_SIZE_PIXELS = 1\n}\n\n// Work variables to avoid garbage collection\nlet $temp = 0;\n\n/**\n * The terminal's standard implementation of IInputHandler, this handles all\n * input from the Parser.\n *\n * Refer to http://invisible-island.net/xterm/ctlseqs/ctlseqs.html to understand\n * each function's header comment.\n */\nexport class InputHandler extends Disposable implements IInputHandler {\n private _parseBuffer: Uint32Array = new Uint32Array(4096);\n private _stringDecoder: StringToUtf32 = new StringToUtf32();\n private _utf8Decoder: Utf8ToUtf32 = new Utf8ToUtf32();\n private _windowTitle = '';\n private _iconName = '';\n private _dirtyRowTracker: IDirtyRowTracker;\n protected _windowTitleStack: string[] = [];\n protected _iconNameStack: string[] = [];\n\n private _curAttrData: IAttributeData = DEFAULT_ATTR_DATA.clone();\n public getAttrData(): IAttributeData { return this._curAttrData; }\n private _eraseAttrDataInternal: IAttributeData = DEFAULT_ATTR_DATA.clone();\n\n private _activeBuffer: IBuffer;\n\n private readonly _onRequestBell = this._register(new Emitter());\n public readonly onRequestBell = this._onRequestBell.event;\n private readonly _onRequestRefreshRows = this._register(new Emitter<{ start: number, end: number } | undefined>());\n public readonly onRequestRefreshRows = this._onRequestRefreshRows.event;\n private readonly _onRequestReset = this._register(new Emitter());\n public readonly onRequestReset = this._onRequestReset.event;\n private readonly _onRequestSendFocus = this._register(new Emitter());\n public readonly onRequestSendFocus = this._onRequestSendFocus.event;\n private readonly _onRequestSyncScrollBar = this._register(new Emitter());\n public readonly onRequestSyncScrollBar = this._onRequestSyncScrollBar.event;\n private readonly _onRequestWindowsOptionsReport = this._register(new Emitter());\n public readonly onRequestWindowsOptionsReport = this._onRequestWindowsOptionsReport.event;\n\n private readonly _onA11yChar = this._register(new Emitter());\n public readonly onA11yChar = this._onA11yChar.event;\n private readonly _onA11yTab = this._register(new Emitter());\n public readonly onA11yTab = this._onA11yTab.event;\n private readonly _onCursorMove = this._register(new Emitter());\n public readonly onCursorMove = this._onCursorMove.event;\n private readonly _onLineFeed = this._register(new Emitter());\n public readonly onLineFeed = this._onLineFeed.event;\n private readonly _onScroll = this._register(new Emitter());\n public readonly onScroll = this._onScroll.event;\n private readonly _onTitleChange = this._register(new Emitter());\n public readonly onTitleChange = this._onTitleChange.event;\n private readonly _onColor = this._register(new Emitter());\n public readonly onColor = this._onColor.event;\n private readonly _onRequestColorSchemeQuery = this._register(new Emitter());\n public readonly onRequestColorSchemeQuery = this._onRequestColorSchemeQuery.event;\n\n private _parseStack: IParseStack = {\n paused: false,\n cursorStartX: 0,\n cursorStartY: 0,\n decodedLength: 0,\n position: 0\n };\n\n constructor(\n private readonly _bufferService: IBufferService,\n private readonly _charsetService: ICharsetService,\n private readonly _coreService: ICoreService,\n private readonly _logService: ILogService,\n private readonly _optionsService: IOptionsService,\n private readonly _oscLinkService: IOscLinkService,\n private readonly _mouseStateService: IMouseStateService,\n private readonly _unicodeService: IUnicodeService,\n private readonly _parser: IEscapeSequenceParser = new EscapeSequenceParser()\n ) {\n super();\n this._register(this._parser);\n this._dirtyRowTracker = new DirtyRowTracker(this._bufferService);\n\n // Track properties used in performance critical code manually to avoid using slow getters\n this._activeBuffer = this._bufferService.buffer;\n this._register(this._bufferService.buffers.onBufferActivate(e => this._activeBuffer = e.activeBuffer));\n\n /**\n * custom fallback handlers\n */\n this._parser.setCsiHandlerFallback((ident, params) => {\n this._logService.debug('Unknown CSI code: ', { identifier: this._parser.identToString(ident), params: params.toArray() });\n });\n this._parser.setEscHandlerFallback(ident => {\n this._logService.debug('Unknown ESC code: ', { identifier: this._parser.identToString(ident) });\n });\n this._parser.setExecuteHandlerFallback(code => {\n this._logService.debug('Unknown EXECUTE code: ', { code });\n });\n this._parser.setOscHandlerFallback((identifier, action, data) => {\n this._logService.debug('Unknown OSC code: ', { identifier, action, data });\n });\n this._parser.setDcsHandlerFallback((ident, action, payload) => {\n if (action === 'HOOK') {\n payload = payload.toArray();\n }\n this._logService.debug('Unknown DCS code: ', { identifier: this._parser.identToString(ident), action, payload });\n });\n this._parser.setApcHandlerFallback((ident, action, payload) => {\n this._logService.debug('Unknown APC code: ', { identifier: this._parser.identToString(ident), action, payload });\n });\n\n /**\n * print handler\n */\n this._parser.setPrintHandler((data, start, end) => this.print(data, start, end));\n\n /**\n * CSI handler\n */\n this._parser.registerCsiHandler({ final: '@' }, params => this.insertChars(params));\n this._parser.registerCsiHandler({ intermediates: ' ', final: '@' }, params => this.scrollLeft(params));\n this._parser.registerCsiHandler({ final: 'A' }, params => this.cursorUp(params));\n this._parser.registerCsiHandler({ intermediates: ' ', final: 'A' }, params => this.scrollRight(params));\n this._parser.registerCsiHandler({ final: 'B' }, params => this.cursorDown(params));\n this._parser.registerCsiHandler({ final: 'C' }, params => this.cursorForward(params));\n this._parser.registerCsiHandler({ final: 'D' }, params => this.cursorBackward(params));\n this._parser.registerCsiHandler({ final: 'E' }, params => this.cursorNextLine(params));\n this._parser.registerCsiHandler({ final: 'F' }, params => this.cursorPrecedingLine(params));\n this._parser.registerCsiHandler({ final: 'G' }, params => this.cursorCharAbsolute(params));\n this._parser.registerCsiHandler({ final: 'H' }, params => this.cursorPosition(params));\n this._parser.registerCsiHandler({ final: 'I' }, params => this.cursorForwardTab(params));\n this._parser.registerCsiHandler({ final: 'J' }, params => this.eraseInDisplay(params, false));\n this._parser.registerCsiHandler({ prefix: '?', final: 'J' }, params => this.eraseInDisplay(params, true));\n this._parser.registerCsiHandler({ final: 'K' }, params => this.eraseInLine(params, false));\n this._parser.registerCsiHandler({ prefix: '?', final: 'K' }, params => this.eraseInLine(params, true));\n this._parser.registerCsiHandler({ final: 'L' }, params => this.insertLines(params));\n this._parser.registerCsiHandler({ final: 'M' }, params => this.deleteLines(params));\n this._parser.registerCsiHandler({ final: 'P' }, params => this.deleteChars(params));\n this._parser.registerCsiHandler({ final: 'S' }, params => this.scrollUp(params));\n this._parser.registerCsiHandler({ final: 'T' }, params => this.scrollDown(params));\n this._parser.registerCsiHandler({ final: 'X' }, params => this.eraseChars(params));\n this._parser.registerCsiHandler({ final: 'Z' }, params => this.cursorBackwardTab(params));\n this._parser.registerCsiHandler({ final: '^' }, params => this.scrollDown(params));\n this._parser.registerCsiHandler({ final: '`' }, params => this.charPosAbsolute(params));\n this._parser.registerCsiHandler({ final: 'a' }, params => this.hPositionRelative(params));\n this._parser.registerCsiHandler({ final: 'b' }, params => this.repeatPrecedingCharacter(params));\n this._parser.registerCsiHandler({ final: 'c' }, params => this.sendDeviceAttributesPrimary(params));\n this._parser.registerCsiHandler({ prefix: '>', final: 'c' }, params => this.sendDeviceAttributesSecondary(params));\n this._parser.registerCsiHandler({ final: 'd' }, params => this.linePosAbsolute(params));\n this._parser.registerCsiHandler({ final: 'e' }, params => this.vPositionRelative(params));\n this._parser.registerCsiHandler({ final: 'f' }, params => this.hVPosition(params));\n this._parser.registerCsiHandler({ final: 'g' }, params => this.tabClear(params));\n this._parser.registerCsiHandler({ final: 'h' }, params => this.setMode(params));\n this._parser.registerCsiHandler({ prefix: '?', final: 'h' }, params => this.setModePrivate(params));\n this._parser.registerCsiHandler({ final: 'l' }, params => this.resetMode(params));\n this._parser.registerCsiHandler({ prefix: '?', final: 'l' }, params => this.resetModePrivate(params));\n this._parser.registerCsiHandler({ final: 'm' }, params => this.charAttributes(params));\n this._parser.registerCsiHandler({ final: 'n' }, params => this.deviceStatus(params));\n this._parser.registerCsiHandler({ prefix: '?', final: 'n' }, params => this.deviceStatusPrivate(params));\n this._parser.registerCsiHandler({ intermediates: '!', final: 'p' }, params => this.softReset(params));\n this._parser.registerCsiHandler({ prefix: '>', final: 'q' }, params => this.sendXtVersion(params));\n this._parser.registerCsiHandler({ intermediates: ' ', final: 'q' }, params => this.setCursorStyle(params));\n this._parser.registerCsiHandler({ final: 'r' }, params => this.setScrollRegion(params));\n this._parser.registerCsiHandler({ final: 's' }, params => this.saveCursor(params));\n this._parser.registerCsiHandler({ final: 't' }, params => this.windowOptions(params));\n this._parser.registerCsiHandler({ final: 'u' }, params => this.restoreCursor(params));\n this._parser.registerCsiHandler({ intermediates: '\\'', final: '}' }, params => this.insertColumns(params));\n this._parser.registerCsiHandler({ intermediates: '\\'', final: '~' }, params => this.deleteColumns(params));\n this._parser.registerCsiHandler({ intermediates: '\"', final: 'q' }, params => this.selectProtected(params));\n this._parser.registerCsiHandler({ intermediates: '$', final: 'p' }, params => this.requestMode(params, true));\n this._parser.registerCsiHandler({ prefix: '?', intermediates: '$', final: 'p' }, params => this.requestMode(params, false));\n\n // Kitty keyboard protocol handlers\n this._parser.registerCsiHandler({ prefix: '=', final: 'u' }, params => this.kittyKeyboardSet(params));\n this._parser.registerCsiHandler({ prefix: '?', final: 'u' }, params => this.kittyKeyboardQuery(params));\n this._parser.registerCsiHandler({ prefix: '>', final: 'u' }, params => this.kittyKeyboardPush(params));\n this._parser.registerCsiHandler({ prefix: '<', final: 'u' }, params => this.kittyKeyboardPop(params));\n\n /**\n * execute handler\n */\n this._parser.setExecuteHandler(C0.BEL, () => this.bell());\n this._parser.setExecuteHandler(C0.LF, () => this.lineFeed());\n this._parser.setExecuteHandler(C0.VT, () => this.lineFeed());\n this._parser.setExecuteHandler(C0.FF, () => this.lineFeed());\n this._parser.setExecuteHandler(C0.CR, () => this.carriageReturn());\n this._parser.setExecuteHandler(C0.BS, () => this.backspace());\n this._parser.setExecuteHandler(C0.HT, () => this.tab());\n this._parser.setExecuteHandler(C0.SO, () => this.shiftOut());\n this._parser.setExecuteHandler(C0.SI, () => this.shiftIn());\n // FIXME: What do to with missing? Old code just added those to print.\n\n this._parser.setExecuteHandler(C1.IND, () => this.index());\n this._parser.setExecuteHandler(C1.NEL, () => this.nextLine());\n this._parser.setExecuteHandler(C1.HTS, () => this.tabSet());\n\n /**\n * OSC handler\n */\n // 0 - icon name + title\n this._parser.registerOscHandler(0, new OscHandler(data => { this.setTitle(data); this.setIconName(data); return true; }));\n // 1 - icon name\n this._parser.registerOscHandler(1, new OscHandler(data => this.setIconName(data)));\n // 2 - title\n this._parser.registerOscHandler(2, new OscHandler(data => this.setTitle(data)));\n // 3 - set property X in the form \"prop=value\"\n // 4 - Change Color Number\n this._parser.registerOscHandler(4, new OscHandler(data => this.setOrReportIndexedColor(data)));\n // 5 - Change Special Color Number\n // 6 - Enable/disable Special Color Number c\n // 7 - current directory? (not in xterm spec, see https://gitlab.com/gnachman/iterm2/issues/3939)\n // 8 - create hyperlink (not in xterm spec, see https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda)\n this._parser.registerOscHandler(8, new OscHandler(data => this.setHyperlink(data)));\n // 10 - Change VT100 text foreground color to Pt.\n this._parser.registerOscHandler(10, new OscHandler(data => this.setOrReportFgColor(data)));\n // 11 - Change VT100 text background color to Pt.\n this._parser.registerOscHandler(11, new OscHandler(data => this.setOrReportBgColor(data)));\n // 12 - Change text cursor color to Pt.\n this._parser.registerOscHandler(12, new OscHandler(data => this.setOrReportCursorColor(data)));\n // 13 - Change mouse foreground color to Pt.\n // 14 - Change mouse background color to Pt.\n // 15 - Change Tektronix foreground color to Pt.\n // 16 - Change Tektronix background color to Pt.\n // 17 - Change highlight background color to Pt.\n // 18 - Change Tektronix cursor color to Pt.\n // 19 - Change highlight foreground color to Pt.\n // 46 - Change Log File to Pt.\n // 50 - Set Font to Pt.\n // 51 - reserved for Emacs shell.\n // 52 - Manipulate Selection Data.\n // 104 ; c - Reset Color Number c.\n this._parser.registerOscHandler(104, new OscHandler(data => this.restoreIndexedColor(data)));\n // 105 ; c - Reset Special Color Number c.\n // 106 ; c; f - Enable/disable Special Color Number c.\n // 110 - Reset VT100 text foreground color.\n this._parser.registerOscHandler(110, new OscHandler(data => this.restoreFgColor(data)));\n // 111 - Reset VT100 text background color.\n this._parser.registerOscHandler(111, new OscHandler(data => this.restoreBgColor(data)));\n // 112 - Reset text cursor color.\n this._parser.registerOscHandler(112, new OscHandler(data => this.restoreCursorColor(data)));\n // 113 - Reset mouse foreground color.\n // 114 - Reset mouse background color.\n // 115 - Reset Tektronix foreground color.\n // 116 - Reset Tektronix background color.\n // 117 - Reset highlight color.\n // 118 - Reset Tektronix cursor color.\n // 119 - Reset highlight foreground color.\n\n /**\n * ESC handlers\n */\n this._parser.registerEscHandler({ final: '7' }, () => this.saveCursor());\n this._parser.registerEscHandler({ final: '8' }, () => this.restoreCursor());\n this._parser.registerEscHandler({ final: 'D' }, () => this.index());\n this._parser.registerEscHandler({ final: 'E' }, () => this.nextLine());\n this._parser.registerEscHandler({ final: 'H' }, () => this.tabSet());\n this._parser.registerEscHandler({ final: 'M' }, () => this.reverseIndex());\n this._parser.registerEscHandler({ final: '=' }, () => this.keypadApplicationMode());\n this._parser.registerEscHandler({ final: '>' }, () => this.keypadNumericMode());\n this._parser.registerEscHandler({ final: 'c' }, () => this.fullReset());\n this._parser.registerEscHandler({ final: 'n' }, () => this.setgLevel(2));\n this._parser.registerEscHandler({ final: 'o' }, () => this.setgLevel(3));\n this._parser.registerEscHandler({ final: '|' }, () => this.setgLevel(3));\n this._parser.registerEscHandler({ final: '}' }, () => this.setgLevel(2));\n this._parser.registerEscHandler({ final: '~' }, () => this.setgLevel(1));\n this._parser.registerEscHandler({ intermediates: '%', final: '@' }, () => this.selectDefaultCharset());\n this._parser.registerEscHandler({ intermediates: '%', final: 'G' }, () => this.selectDefaultCharset());\n for (const flag in CHARSETS) {\n this._parser.registerEscHandler({ intermediates: '(', final: flag }, () => this.selectCharset('(' + flag));\n this._parser.registerEscHandler({ intermediates: ')', final: flag }, () => this.selectCharset(')' + flag));\n this._parser.registerEscHandler({ intermediates: '*', final: flag }, () => this.selectCharset('*' + flag));\n this._parser.registerEscHandler({ intermediates: '+', final: flag }, () => this.selectCharset('+' + flag));\n this._parser.registerEscHandler({ intermediates: '-', final: flag }, () => this.selectCharset('-' + flag));\n this._parser.registerEscHandler({ intermediates: '.', final: flag }, () => this.selectCharset('.' + flag));\n this._parser.registerEscHandler({ intermediates: '/', final: flag }, () => this.selectCharset('/' + flag)); // TODO: supported?\n }\n this._parser.registerEscHandler({ intermediates: '#', final: '8' }, () => this.screenAlignmentPattern());\n\n /**\n * error handler\n */\n this._parser.setErrorHandler((state: IParsingState) => {\n this._logService.error('Parsing error: ', state);\n return state;\n });\n\n /**\n * DCS handler\n */\n this._parser.registerDcsHandler({ intermediates: '$', final: 'q' }, new DcsHandler((data, params) => this.requestStatusString(data, params)));\n }\n\n /**\n * Async parse support.\n */\n private _preserveStack(cursorStartX: number, cursorStartY: number, decodedLength: number, position: number): void {\n this._parseStack.paused = true;\n this._parseStack.cursorStartX = cursorStartX;\n this._parseStack.cursorStartY = cursorStartY;\n this._parseStack.decodedLength = decodedLength;\n this._parseStack.position = position;\n }\n\n private _logSlowResolvingAsync(p: Promise): void {\n // log a limited warning about an async handler taking too long\n if (this._logService.logLevel <= LogLevelEnum.WARN) {\n let slowTimeout: ReturnType | undefined;\n const slowPromise = new Promise((_res, rej) => {\n slowTimeout = setTimeout(() => rej('#SLOW_TIMEOUT'), Constants.SLOW_ASYNC_LIMIT);\n });\n Promise.race([p, slowPromise])\n .then(() => {\n if (slowTimeout !== undefined) {\n clearTimeout(slowTimeout);\n }\n }, err => {\n if (slowTimeout !== undefined) {\n clearTimeout(slowTimeout);\n }\n if (err !== '#SLOW_TIMEOUT') {\n throw err;\n }\n console.warn(`async parser handler taking longer than ${Constants.SLOW_ASYNC_LIMIT} ms`);\n });\n }\n }\n\n private _getCurrentLinkId(): number {\n return this._curAttrData.extended.urlId;\n }\n\n /**\n * Parse call with async handler support.\n *\n * Whether the stack state got preserved for the next call, is indicated by the return value:\n * - undefined (void):\n * all handlers were sync, no stack save, continue normally with next chunk\n * - Promise\\:\n * execution stopped at async handler, stack saved, continue with same chunk and the promise\n * resolve value as `promiseResult` until the method returns `undefined`\n *\n * Note: This method should only be called by `Terminal.write` to ensure correct execution order\n * and proper continuation of async parser handlers.\n */\n public parse(data: string | Uint8Array, promiseResult?: boolean): void | Promise {\n let result: void | Promise;\n let cursorStartX = this._activeBuffer.x;\n let cursorStartY = this._activeBuffer.y;\n let start = 0;\n const wasPaused = this._parseStack.paused;\n\n if (wasPaused) {\n // assumption: _parseBuffer never mutates between async calls\n if (result = this._parser.parse(this._parseBuffer, this._parseStack.decodedLength, promiseResult)) {\n this._logSlowResolvingAsync(result);\n return result;\n }\n cursorStartX = this._parseStack.cursorStartX;\n cursorStartY = this._parseStack.cursorStartY;\n this._parseStack.paused = false;\n if (data.length > Constants.MAX_PARSEBUFFER_LENGTH) {\n start = this._parseStack.position + Constants.MAX_PARSEBUFFER_LENGTH;\n }\n }\n\n // Log debug data, the log level gate is to prevent extra work in this hot path\n if (this._logService.logLevel <= LogLevelEnum.DEBUG) {\n this._logService.debug(`parsing data ${typeof data === 'string' ? ` \"${data}\"` : ` \"${Array.prototype.map.call(data, e => String.fromCharCode(e)).join('')}\"`}`);\n }\n if (this._logService.logLevel === LogLevelEnum.TRACE) {\n this._logService.trace(`parsing data (codes)`, typeof data === 'string'\n ? data.split('').map(e => e.charCodeAt(0))\n : data\n );\n }\n\n // resize input buffer if needed\n if (this._parseBuffer.length < data.length) {\n if (this._parseBuffer.length < Constants.MAX_PARSEBUFFER_LENGTH) {\n this._parseBuffer = new Uint32Array(Math.min(data.length, Constants.MAX_PARSEBUFFER_LENGTH));\n }\n }\n\n // Clear the dirty row service so we know which lines changed as a result of parsing\n // Important: do not clear between async calls, otherwise we lost pending update information.\n if (!wasPaused) {\n this._dirtyRowTracker.clearRange();\n }\n\n // process big data in smaller chunks\n if (data.length > Constants.MAX_PARSEBUFFER_LENGTH) {\n for (let i = start; i < data.length; i += Constants.MAX_PARSEBUFFER_LENGTH) {\n const end = i + Constants.MAX_PARSEBUFFER_LENGTH < data.length ? i + Constants.MAX_PARSEBUFFER_LENGTH : data.length;\n const len = (typeof data === 'string')\n ? this._stringDecoder.decode(data.substring(i, end), this._parseBuffer)\n : this._utf8Decoder.decode(data.subarray(i, end), this._parseBuffer);\n if (result = this._parser.parse(this._parseBuffer, len)) {\n this._preserveStack(cursorStartX, cursorStartY, len, i);\n this._logSlowResolvingAsync(result);\n return result;\n }\n }\n } else {\n if (!wasPaused) {\n const len = (typeof data === 'string')\n ? this._stringDecoder.decode(data, this._parseBuffer)\n : this._utf8Decoder.decode(data, this._parseBuffer);\n if (result = this._parser.parse(this._parseBuffer, len)) {\n this._preserveStack(cursorStartX, cursorStartY, len, 0);\n this._logSlowResolvingAsync(result);\n return result;\n }\n }\n }\n\n if (this._activeBuffer.x !== cursorStartX || this._activeBuffer.y !== cursorStartY) {\n this._onCursorMove.fire();\n }\n\n // Refresh any dirty rows accumulated as part of parsing, fire only for rows within the\n // _viewport_ which is relative to ydisp, not relative to ybase.\n const viewportEnd = this._dirtyRowTracker.end + (this._bufferService.buffer.ybase - this._bufferService.buffer.ydisp);\n const viewportStart = this._dirtyRowTracker.start + (this._bufferService.buffer.ybase - this._bufferService.buffer.ydisp);\n if (viewportStart < this._bufferService.rows) {\n this._onRequestRefreshRows.fire({\n start: Math.min(viewportStart, this._bufferService.rows - 1),\n end: Math.min(viewportEnd, this._bufferService.rows - 1)\n });\n }\n }\n\n public print(data: Uint32Array, start: number, end: number): void {\n let code: number;\n let chWidth: number;\n const charset = this._charsetService.charset;\n const screenReaderMode = this._optionsService.rawOptions.screenReaderMode;\n const cols = this._bufferService.cols;\n const wraparoundMode = this._coreService.decPrivateModes.wraparound;\n const insertMode = this._coreService.modes.insertMode;\n const curAttr = this._curAttrData;\n let bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y);\n\n // Defensive check: bufferRow can be undefined if a resize occurred mid-write due to async\n // scheduling gaps in WriteBuffer. See https://github.com/xtermjs/xterm.js/issues/5597\n if (!bufferRow) {\n return;\n }\n\n this._dirtyRowTracker.markDirty(this._activeBuffer.y);\n\n // handle wide chars: reset start_cell-1 if we would overwrite the second cell of a wide char\n if (this._activeBuffer.x && end - start > 0 && bufferRow.getWidth(this._activeBuffer.x - 1) === 2) {\n bufferRow.setCellFromCodepoint(this._activeBuffer.x - 1, 0, 1, curAttr);\n }\n\n let precedingJoinState = this._parser.precedingJoinState;\n for (let pos = start; pos < end; ++pos) {\n // Orca's width-one ASCII invariant excludes ZWJ joins; keep wrapping on the general path.\n if (precedingJoinState === 2 && !charset && !screenReaderMode && !insertMode\n && this._unicodeService.activeVersion === 'orca-11-zwj' && !this._getCurrentLinkId()\n && this._activeBuffer.x < cols && data[pos] >= 0x20 && data[pos] < 0x7f) {\n do {\n bufferRow.setCellFromCodepoint(this._activeBuffer.x++, data[pos], 1, curAttr);\n } while (++pos < end && this._activeBuffer.x < cols && data[pos] >= 0x20 && data[pos] < 0x7f);\n pos--;\n continue;\n }\n code = data[pos];\n\n // Soft hyphen's (U+00AD) behavior is ambiguous and differs across terminals. We opt to treat\n // it as a zero-width hint to text layout engines and simply ignore it.\n if (code === 0xAD) {\n continue;\n }\n\n // get charset replacement character\n // charset is only defined for ASCII, therefore we only\n // search for an replacement char if code < 127\n if (code < 127 && charset) {\n const ch = charset[String.fromCharCode(code)];\n if (ch) {\n code = ch.charCodeAt(0);\n }\n }\n\n const currentInfo = this._unicodeService.charProperties(code, precedingJoinState);\n chWidth = UnicodeService.extractWidth(currentInfo);\n const shouldJoin = UnicodeService.extractShouldJoin(currentInfo);\n const oldWidth = shouldJoin ? UnicodeService.extractWidth(precedingJoinState) : 0;\n precedingJoinState = currentInfo;\n\n if (screenReaderMode) {\n this._onA11yChar.fire(stringFromCodePoint(code));\n }\n const linkId = this._getCurrentLinkId();\n if (linkId) {\n this._oscLinkService.addLineToLink(linkId, this._activeBuffer.ybase + this._activeBuffer.y);\n }\n\n // goto next line if ch would overflow\n // NOTE: To avoid costly width checks here,\n // the terminal does not allow a cols < 2.\n if (this._activeBuffer.x + chWidth - oldWidth > cols) {\n // autowrap - DECAWM\n // automatically wraps to the beginning of the next line\n if (wraparoundMode) {\n const oldRow = bufferRow;\n let oldCol = this._activeBuffer.x - oldWidth;\n this._activeBuffer.x = oldWidth;\n this._activeBuffer.y++;\n if (this._activeBuffer.y === this._activeBuffer.scrollBottom + 1) {\n this._activeBuffer.y--;\n this._bufferService.scroll(this._eraseAttrData(), true);\n } else {\n if (this._activeBuffer.y >= this._bufferService.rows) {\n this._activeBuffer.y = this._bufferService.rows - 1;\n }\n // The line already exists (eg. the initial viewport), mark it as a\n // wrapped line\n this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!.isWrapped = true;\n }\n // row changed, get it again\n bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y);\n if (!bufferRow) {\n return;\n }\n if (oldWidth > 0 && bufferRow instanceof BufferLine) {\n // Combining character widens 1 column to 2.\n // Move old character to next line.\n bufferRow.copyCellsFrom(oldRow as BufferLine,\n oldCol, 0, oldWidth, false);\n }\n // clear left over cells to the right\n while (oldCol < cols) {\n oldRow.setCellFromCodepoint(oldCol++, 0, 1, curAttr);\n }\n } else {\n this._activeBuffer.x = cols - 1;\n if (chWidth === 2) {\n // FIXME: check for xterm behavior\n // What to do here? We got a wide char that does not fit into last cell\n continue;\n }\n }\n }\n\n // insert combining char at last cursor position\n // this._activeBuffer.x should never be 0 for a combining char\n // since they always follow a cell consuming char\n // therefore we can test for this._activeBuffer.x to avoid overflow left\n if (shouldJoin && this._activeBuffer.x) {\n const offset = bufferRow.getWidth(this._activeBuffer.x - 1) ? 1 : 2;\n // if empty cell after fullwidth, need to go 2 cells back\n // it is save to step 2 cells back here\n // since an empty cell is only set by fullwidth chars\n bufferRow.addCodepointToCell(this._activeBuffer.x - offset,\n code, chWidth);\n for (let delta = chWidth - oldWidth; --delta >= 0;) {\n bufferRow.setCellFromCodepoint(this._activeBuffer.x++, 0, 0, curAttr);\n }\n continue;\n }\n\n // insert mode: move characters to right\n if (insertMode) {\n // right shift cells according to the width\n bufferRow.insertCells(this._activeBuffer.x, chWidth - oldWidth, this._activeBuffer.getNullCell(curAttr));\n // test last cell - since the last cell has only room for\n // a halfwidth char any fullwidth shifted there is lost\n // and will be set to empty cell\n if (bufferRow.getWidth(cols - 1) === 2) {\n bufferRow.setCellFromCodepoint(cols - 1, NULL_CELL_CODE, NULL_CELL_WIDTH, curAttr);\n }\n }\n\n // write current char to buffer and advance cursor\n bufferRow.setCellFromCodepoint(this._activeBuffer.x++, code, chWidth, curAttr);\n\n // fullwidth char - also set next cell to placeholder stub and advance cursor\n // for graphemes bigger than fullwidth we can simply loop to zero\n // we already made sure above, that this._activeBuffer.x + chWidth will not overflow right\n if (chWidth > 0) {\n while (--chWidth) {\n // other than a regular empty cell a cell following a wide char has no width\n bufferRow.setCellFromCodepoint(this._activeBuffer.x++, 0, 0, curAttr);\n }\n }\n }\n\n this._parser.precedingJoinState = precedingJoinState;\n\n // handle wide chars: reset cell to the right if it is second cell of a wide char\n if (this._activeBuffer.x < cols && end - start > 0 && bufferRow.getWidth(this._activeBuffer.x) === 0 && !bufferRow.hasContent(this._activeBuffer.x)) {\n bufferRow.setCellFromCodepoint(this._activeBuffer.x, 0, 1, curAttr);\n }\n\n this._dirtyRowTracker.markDirty(this._activeBuffer.y);\n }\n\n /**\n * Forward registerCsiHandler from parser.\n */\n public registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise): IDisposable {\n if (id.final === 't' && !id.prefix && !id.intermediates) {\n // security: always check whether window option is allowed\n return this._parser.registerCsiHandler(id, params => {\n if (!paramToWindowOption(params.params[0], this._optionsService.rawOptions.windowOptions)) {\n return true;\n }\n return callback(params);\n });\n }\n return this._parser.registerCsiHandler(id, callback);\n }\n\n /**\n * Forward registerDcsHandler from parser.\n */\n public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise): IDisposable {\n return this._parser.registerDcsHandler(id, new DcsHandler(callback));\n }\n\n /**\n * Forward registerEscHandler from parser.\n */\n public registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise): IDisposable {\n return this._parser.registerEscHandler(id, callback);\n }\n\n /**\n * Forward registerOscHandler from parser.\n */\n public registerOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable {\n return this._parser.registerOscHandler(ident, new OscHandler(callback));\n }\n\n /**\n * Forward registerApcHandler from parser.\n */\n public registerApcHandler(id: IFunctionIdentifier, callback: (data: string) => boolean | Promise): IDisposable {\n return this._parser.registerApcHandler(id, new ApcHandler(callback));\n }\n\n /**\n * BEL\n * Bell (Ctrl-G).\n *\n * @vt: #Y C0 BEL \"Bell\" \"\\a, \\x07\" \"Ring the bell.\"\n * The behavior of the bell is further customizable with `ITerminalOptions.bellStyle`\n * and `ITerminalOptions.bellSound`.\n */\n public bell(): boolean {\n this._onRequestBell.fire();\n return true;\n }\n\n /**\n * LF\n * Line Feed or New Line (NL). (LF is Ctrl-J).\n *\n * @vt: #Y C0 LF \"Line Feed\" \"\\n, \\x0A\" \"Move the cursor one row down, scrolling if needed.\"\n * Scrolling is restricted to scroll margins and will only happen on the bottom line.\n *\n * @vt: #Y C0 VT \"Vertical Tabulation\" \"\\v, \\x0B\" \"Treated as LF.\"\n * @vt: #Y C0 FF \"Form Feed\" \"\\f, \\x0C\" \"Treated as LF.\"\n */\n public lineFeed(): boolean {\n this._dirtyRowTracker.markDirty(this._activeBuffer.y);\n if (this._optionsService.rawOptions.convertEol) {\n this._activeBuffer.x = 0;\n }\n this._activeBuffer.y++;\n if (this._activeBuffer.y === this._activeBuffer.scrollBottom + 1) {\n this._activeBuffer.y--;\n this._bufferService.scroll(this._eraseAttrData());\n } else if (this._activeBuffer.y >= this._bufferService.rows) {\n this._activeBuffer.y = this._bufferService.rows - 1;\n } else {\n // There was an explicit line feed (not just a carriage return), so clear the wrapped state of\n // the line. This is particularly important on conpty/Windows where revisiting lines to\n // reprint is common, especially on resize. Note that the windowsMode wrapped line heuristics\n // can mess with this so windowsMode should be disabled, which is recommended on Windows build\n // 21376 and above.\n this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!.isWrapped = false;\n }\n // If the end of the line is hit, prevent this action from wrapping around to the next line.\n if (this._activeBuffer.x >= this._bufferService.cols) {\n this._activeBuffer.x--;\n }\n this._dirtyRowTracker.markDirty(this._activeBuffer.y);\n\n this._onLineFeed.fire();\n return true;\n }\n\n /**\n * CR\n * Carriage Return (Ctrl-M).\n *\n * @vt: #Y C0 CR \"Carriage Return\" \"\\r, \\x0D\" \"Move the cursor to the beginning of the row.\"\n */\n public carriageReturn(): boolean {\n this._activeBuffer.x = 0;\n return true;\n }\n\n /**\n * BS\n * Backspace (Ctrl-H).\n *\n * @vt: #Y C0 BS \"Backspace\" \"\\b, \\x08\" \"Move the cursor one position to the left.\"\n * By default it is not possible to move the cursor past the leftmost position.\n * If `reverse wrap-around` (`CSI ? 45 h`) is set, a previous soft line wrap (DECAWM)\n * can be undone with BS within the scroll margins. In that case the cursor will wrap back\n * to the end of the previous row. Note that it is not possible to peek back into the scrollbuffer\n * with the cursor, thus at the home position (top-leftmost cell) this has no effect.\n */\n public backspace(): boolean {\n // reverse wrap-around is disabled\n if (!this._coreService.decPrivateModes.reverseWraparound) {\n this._restrictCursor();\n if (this._activeBuffer.x > 0) {\n this._activeBuffer.x--;\n }\n return true;\n }\n\n // reverse wrap-around is enabled\n // other than for normal operation mode, reverse wrap-around allows the cursor\n // to be at x=cols to be able to address the last cell of a row by BS\n this._restrictCursor(this._bufferService.cols);\n\n if (this._activeBuffer.x > 0) {\n this._activeBuffer.x--;\n } else {\n /**\n * reverse wrap-around handling:\n * Our implementation deviates from xterm on purpose. Details:\n * - only previous soft NLs can be reversed (isWrapped=true)\n * - only works within scrollborders (top/bottom, left/right not yet supported)\n * - cannot peek into scrollbuffer\n * - any cursor movement sequence keeps working as expected\n */\n if (this._activeBuffer.x === 0\n && this._activeBuffer.y > this._activeBuffer.scrollTop\n && this._activeBuffer.y <= this._activeBuffer.scrollBottom\n && this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)?.isWrapped) {\n this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!.isWrapped = false;\n this._activeBuffer.y--;\n this._activeBuffer.x = this._bufferService.cols - 1;\n // find last taken cell - last cell can have 3 different states:\n // - hasContent(true) + hasWidth(1): narrow char - we are done\n // - hasWidth(0): second part of wide char - we are done\n // - hasContent(false) + hasWidth(1): empty cell due to early wrapping wide char, go one\n // cell further back\n const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!;\n if (line.hasWidth(this._activeBuffer.x) && !line.hasContent(this._activeBuffer.x)) {\n this._activeBuffer.x--;\n // We do this only once, since width=1 + hasContent=false currently happens only once\n // before early wrapping of a wide char.\n // This needs to be fixed once we support graphemes taking more than 2 cells.\n }\n }\n }\n this._restrictCursor();\n return true;\n }\n\n /**\n * TAB\n * Horizontal Tab (HT) (Ctrl-I).\n *\n * @vt: #Y C0 HT \"Horizontal Tabulation\" \"\\t, \\x09\" \"Move the cursor to the next character tab stop.\"\n */\n public tab(): boolean {\n if (this._activeBuffer.x >= this._bufferService.cols) {\n return true;\n }\n const originalX = this._activeBuffer.x;\n this._activeBuffer.x = this._activeBuffer.nextStop();\n if (this._optionsService.rawOptions.screenReaderMode) {\n this._onA11yTab.fire(this._activeBuffer.x - originalX);\n }\n return true;\n }\n\n /**\n * SO\n * Shift Out (Ctrl-N) -> Switch to Alternate Character Set. This invokes the\n * G1 character set.\n *\n * @vt: #P[Only limited ISO-2022 charset support.] C0 SO \"Shift Out\" \"\\x0E\" \"Switch to an alternative character set.\"\n */\n public shiftOut(): boolean {\n this._charsetService.setgLevel(1);\n return true;\n }\n\n /**\n * SI\n * Shift In (Ctrl-O) -> Switch to Standard Character Set. This invokes the G0\n * character set (the default).\n *\n * @vt: #Y C0 SI \"Shift In\" \"\\x0F\" \"Return to regular character set after Shift Out.\"\n */\n public shiftIn(): boolean {\n this._charsetService.setgLevel(0);\n return true;\n }\n\n /**\n * Restrict cursor to viewport size / scroll margin (origin mode).\n */\n private _restrictCursor(maxCol: number = this._bufferService.cols - 1): void {\n this._activeBuffer.x = Math.min(maxCol, Math.max(0, this._activeBuffer.x));\n this._activeBuffer.y = this._coreService.decPrivateModes.origin\n ? Math.min(this._activeBuffer.scrollBottom, Math.max(this._activeBuffer.scrollTop, this._activeBuffer.y))\n : Math.min(this._bufferService.rows - 1, Math.max(0, this._activeBuffer.y));\n this._dirtyRowTracker.markDirty(this._activeBuffer.y);\n }\n\n /**\n * Set absolute cursor position.\n */\n private _setCursor(x: number, y: number): void {\n this._dirtyRowTracker.markDirty(this._activeBuffer.y);\n if (this._coreService.decPrivateModes.origin) {\n this._activeBuffer.x = x;\n this._activeBuffer.y = this._activeBuffer.scrollTop + y;\n } else {\n this._activeBuffer.x = x;\n this._activeBuffer.y = y;\n }\n this._restrictCursor();\n this._dirtyRowTracker.markDirty(this._activeBuffer.y);\n }\n\n /**\n * Set relative cursor position.\n */\n private _moveCursor(x: number, y: number): void {\n // for relative changes we have to make sure we are within 0 .. cols/rows - 1\n // before calculating the new position\n this._restrictCursor();\n this._setCursor(this._activeBuffer.x + x, this._activeBuffer.y + y);\n }\n\n /**\n * CSI Ps A\n * Cursor Up Ps Times (default = 1) (CUU).\n *\n * @vt: #Y CSI CUU \"Cursor Up\" \"CSI Ps A\" \"Move cursor `Ps` times up (default=1).\"\n * If the cursor would pass the top scroll margin, it will stop there.\n */\n public cursorUp(params: IParams): boolean {\n // stop at scrollTop\n const diffToTop = this._activeBuffer.y - this._activeBuffer.scrollTop;\n if (diffToTop >= 0) {\n this._moveCursor(0, -Math.min(diffToTop, params.params[0] || 1));\n } else {\n this._moveCursor(0, -(params.params[0] || 1));\n }\n return true;\n }\n\n /**\n * CSI Ps B\n * Cursor Down Ps Times (default = 1) (CUD).\n *\n * @vt: #Y CSI CUD \"Cursor Down\" \"CSI Ps B\" \"Move cursor `Ps` times down (default=1).\"\n * If the cursor would pass the bottom scroll margin, it will stop there.\n */\n public cursorDown(params: IParams): boolean {\n // stop at scrollBottom\n const diffToBottom = this._activeBuffer.scrollBottom - this._activeBuffer.y;\n if (diffToBottom >= 0) {\n this._moveCursor(0, Math.min(diffToBottom, params.params[0] || 1));\n } else {\n this._moveCursor(0, params.params[0] || 1);\n }\n return true;\n }\n\n /**\n * CSI Ps C\n * Cursor Forward Ps Times (default = 1) (CUF).\n *\n * @vt: #Y CSI CUF \"Cursor Forward\" \"CSI Ps C\" \"Move cursor `Ps` times forward (default=1).\"\n */\n public cursorForward(params: IParams): boolean {\n this._moveCursor(params.params[0] || 1, 0);\n return true;\n }\n\n /**\n * CSI Ps D\n * Cursor Backward Ps Times (default = 1) (CUB).\n *\n * @vt: #Y CSI CUB \"Cursor Backward\" \"CSI Ps D\" \"Move cursor `Ps` times backward (default=1).\"\n */\n public cursorBackward(params: IParams): boolean {\n this._moveCursor(-(params.params[0] || 1), 0);\n return true;\n }\n\n /**\n * CSI Ps E\n * Cursor Next Line Ps Times (default = 1) (CNL).\n * Other than cursorDown (CUD) also set the cursor to first column.\n *\n * @vt: #Y CSI CNL \"Cursor Next Line\" \"CSI Ps E\" \"Move cursor `Ps` times down (default=1) and to the first column.\"\n * Same as CUD, additionally places the cursor at the first column.\n */\n public cursorNextLine(params: IParams): boolean {\n this.cursorDown(params);\n this._activeBuffer.x = 0;\n return true;\n }\n\n /**\n * CSI Ps F\n * Cursor Previous Line Ps Times (default = 1) (CPL).\n * Other than cursorUp (CUU) also set the cursor to first column.\n *\n * @vt: #Y CSI CPL \"Cursor Backward\" \"CSI Ps F\" \"Move cursor `Ps` times up (default=1) and to the first column.\"\n * Same as CUU, additionally places the cursor at the first column.\n */\n public cursorPrecedingLine(params: IParams): boolean {\n this.cursorUp(params);\n this._activeBuffer.x = 0;\n return true;\n }\n\n /**\n * CSI Ps G\n * Cursor Character Absolute [column] (default = [row,1]) (CHA).\n *\n * @vt: #Y CSI CHA \"Cursor Horizontal Absolute\" \"CSI Ps G\" \"Move cursor to `Ps`-th column of the active row (default=1).\"\n */\n public cursorCharAbsolute(params: IParams): boolean {\n this._setCursor((params.params[0] || 1) - 1, this._activeBuffer.y);\n return true;\n }\n\n /**\n * CSI Ps ; Ps H\n * Cursor Position [row;column] (default = [1,1]) (CUP).\n *\n * @vt: #Y CSI CUP \"Cursor Position\" \"CSI Ps ; Ps H\" \"Set cursor to position [`Ps`, `Ps`] (default = [1, 1]).\"\n * If ORIGIN mode is set, places the cursor to the absolute position within the scroll margins.\n * If ORIGIN mode is not set, places the cursor to the absolute position within the viewport.\n * Note that the coordinates are 1-based, thus the top left position starts at `1 ; 1`.\n */\n public cursorPosition(params: IParams): boolean {\n this._setCursor(\n // col\n (params.length >= 2) ? (params.params[1] || 1) - 1 : 0,\n // row\n (params.params[0] || 1) - 1\n );\n return true;\n }\n\n /**\n * CSI Pm ` Character Position Absolute\n * [column] (default = [row,1]) (HPA).\n * Currently same functionality as CHA.\n *\n * @vt: #Y CSI HPA \"Horizontal Position Absolute\" \"CSI Ps ` \" \"Same as CHA.\"\n */\n public charPosAbsolute(params: IParams): boolean {\n this._setCursor((params.params[0] || 1) - 1, this._activeBuffer.y);\n return true;\n }\n\n /**\n * CSI Pm a Character Position Relative\n * [columns] (default = [row,col+1]) (HPR)\n *\n * @vt: #Y CSI HPR \"Horizontal Position Relative\" \"CSI Ps a\" \"Same as CUF.\"\n */\n public hPositionRelative(params: IParams): boolean {\n this._moveCursor(params.params[0] || 1, 0);\n return true;\n }\n\n /**\n * CSI Pm d Vertical Position Absolute (VPA)\n * [row] (default = [1,column])\n *\n * @vt: #Y CSI VPA \"Vertical Position Absolute\" \"CSI Ps d\" \"Move cursor to `Ps`-th row (default=1).\"\n */\n public linePosAbsolute(params: IParams): boolean {\n this._setCursor(this._activeBuffer.x, (params.params[0] || 1) - 1);\n return true;\n }\n\n /**\n * CSI Pm e Vertical Position Relative (VPR)\n * [rows] (default = [row+1,column])\n * reuse CSI Ps B ?\n *\n * @vt: #Y CSI VPR \"Vertical Position Relative\" \"CSI Ps e\" \"Move cursor `Ps` times down (default=1).\"\n */\n public vPositionRelative(params: IParams): boolean {\n this._moveCursor(0, params.params[0] || 1);\n return true;\n }\n\n /**\n * CSI Ps ; Ps f\n * Horizontal and Vertical Position [row;column] (default =\n * [1,1]) (HVP).\n * Same as CUP.\n *\n * @vt: #Y CSI HVP \"Horizontal and Vertical Position\" \"CSI Ps ; Ps f\" \"Same as CUP.\"\n */\n public hVPosition(params: IParams): boolean {\n this.cursorPosition(params);\n return true;\n }\n\n /**\n * CSI Ps g Tab Clear (TBC).\n * Ps = 0 -> Clear Current Column (default).\n * Ps = 3 -> Clear All.\n * Potentially:\n * Ps = 2 -> Clear Stops on Line.\n * http://vt100.net/annarbor/aaa-ug/section6.html\n *\n * @vt: #Y CSI TBC \"Tab Clear\" \"CSI Ps g\" \"Clear tab stops at current position (0) or all (3) (default=0).\"\n * Clearing tabstops off the active row (Ps = 2, VT100) is currently not supported.\n */\n public tabClear(params: IParams): boolean {\n const param = params.params[0];\n if (param === 0) {\n delete this._activeBuffer.tabs[this._activeBuffer.x];\n } else if (param === 3) {\n this._activeBuffer.tabs = {};\n }\n return true;\n }\n\n /**\n * CSI Ps I\n * Cursor Forward Tabulation Ps tab stops (default = 1) (CHT).\n *\n * @vt: #Y CSI CHT \"Cursor Horizontal Tabulation\" \"CSI Ps I\" \"Move cursor `Ps` times tabs forward (default=1).\"\n */\n public cursorForwardTab(params: IParams): boolean {\n if (this._activeBuffer.x >= this._bufferService.cols) {\n return true;\n }\n let param = params.params[0] || 1;\n while (param--) {\n this._activeBuffer.x = this._activeBuffer.nextStop();\n }\n return true;\n }\n\n /**\n * CSI Ps Z Cursor Backward Tabulation Ps tab stops (default = 1) (CBT).\n *\n * @vt: #Y CSI CBT \"Cursor Backward Tabulation\" \"CSI Ps Z\" \"Move cursor `Ps` tabs backward (default=1).\"\n */\n public cursorBackwardTab(params: IParams): boolean {\n if (this._activeBuffer.x >= this._bufferService.cols) {\n return true;\n }\n let param = params.params[0] || 1;\n\n while (param--) {\n this._activeBuffer.x = this._activeBuffer.prevStop();\n }\n return true;\n }\n\n /**\n * CSI Ps \" q Select Character Protection Attribute (DECSCA).\n *\n * @vt: #Y CSI DECSCA \"Select Character Protection Attribute\" \"CSI Ps \" q\" \"Whether DECSED and DECSEL can erase (0=default, 2) or not (1).\"\n */\n public selectProtected(params: IParams): boolean {\n const p = params.params[0];\n if (p === 1) this._curAttrData.bg |= BgFlags.PROTECTED;\n if (p === 2 || p === 0) this._curAttrData.bg &= ~BgFlags.PROTECTED;\n return true;\n }\n\n\n /**\n * Helper method to erase cells in a terminal row.\n * The cell gets replaced with the eraseChar of the terminal.\n * @param y The row index relative to the viewport.\n * @param start The start x index of the range to be erased.\n * @param end The end x index of the range to be erased (exclusive).\n * @param clearWrap clear the isWrapped flag\n * @param respectProtect Whether to respect the protection attribute (DECSCA).\n */\n private _eraseInBufferLine(y: number, start: number, end: number, clearWrap: boolean = false, respectProtect: boolean = false): void {\n const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y);\n if (!line) {\n return;\n }\n line.replaceCells(\n start,\n end,\n this._activeBuffer.getNullCell(this._eraseAttrData()),\n respectProtect\n );\n if (clearWrap) {\n line.isWrapped = false;\n }\n }\n\n /**\n * Helper method to reset cells in a terminal row. The cell gets replaced with the eraseChar of\n * the terminal and the isWrapped property is set to false.\n * @param y row index\n */\n private _resetBufferLine(y: number, respectProtect: boolean = false): void {\n const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y);\n if (line) {\n line.fill(this._activeBuffer.getNullCell(this._eraseAttrData()), respectProtect);\n this._bufferService.buffer.clearMarkers(this._activeBuffer.ybase + y);\n line.isWrapped = false;\n }\n }\n\n /**\n * CSI Ps J Erase in Display (ED).\n * Ps = 0 -> Erase Below (default).\n * Ps = 1 -> Erase Above.\n * Ps = 2 -> Erase All.\n * Ps = 3 -> Erase Saved Lines (xterm).\n * CSI ? Ps J\n * Erase in Display (DECSED).\n * Ps = 0 -> Selective Erase Below (default).\n * Ps = 1 -> Selective Erase Above.\n * Ps = 2 -> Selective Erase All.\n *\n * @vt: #Y CSI ED \"Erase In Display\" \"CSI Ps J\" \"Erase various parts of the viewport.\"\n * Supported param values:\n *\n * | Ps | Effect |\n * | -- | ------------------------------------------------------------ |\n * | 0 | Erase from the cursor through the end of the viewport. |\n * | 1 | Erase from the beginning of the viewport through the cursor. |\n * | 2 | Erase complete viewport. |\n * | 3 | Erase scrollback. |\n *\n * @vt: #Y CSI DECSED \"Selective Erase In Display\" \"CSI ? Ps J\" \"Same as ED with respecting protection flag.\"\n */\n public eraseInDisplay(params: IParams, respectProtect: boolean = false): boolean {\n this._restrictCursor(this._bufferService.cols);\n let j;\n switch (params.params[0]) {\n case 0:\n j = this._activeBuffer.y;\n this._dirtyRowTracker.markDirty(j);\n this._eraseInBufferLine(j++, this._activeBuffer.x, this._bufferService.cols, this._activeBuffer.x === 0, respectProtect);\n for (; j < this._bufferService.rows; j++) {\n this._resetBufferLine(j, respectProtect);\n }\n this._dirtyRowTracker.markDirty(j);\n break;\n case 1:\n j = this._activeBuffer.y;\n this._dirtyRowTracker.markDirty(j);\n // Deleted front part of line and everything before. This line will no longer be wrapped.\n this._eraseInBufferLine(j, 0, this._activeBuffer.x + 1, true, respectProtect);\n if (this._activeBuffer.x + 1 >= this._bufferService.cols) {\n // Deleted entire previous line. This next line can no longer be wrapped.\n const nextLine = this._activeBuffer.lines.get(j + 1);\n if (nextLine) {\n nextLine.isWrapped = false;\n }\n }\n while (j--) {\n this._resetBufferLine(j, respectProtect);\n }\n this._dirtyRowTracker.markDirty(0);\n break;\n case 2:\n if (this._optionsService.rawOptions.scrollOnEraseInDisplay) {\n j = this._bufferService.rows;\n this._dirtyRowTracker.markRangeDirty(0, j - 1);\n while (j--) {\n const currentLine = this._activeBuffer.lines.get(this._activeBuffer.ybase + j);\n if (currentLine?.getTrimmedLength()) {\n break;\n }\n }\n for (; j >= 0; j--) {\n this._bufferService.scroll(this._eraseAttrData());\n }\n }\n else {\n j = this._bufferService.rows;\n this._dirtyRowTracker.markDirty(j - 1);\n while (j--) {\n this._resetBufferLine(j, respectProtect);\n }\n this._dirtyRowTracker.markDirty(0);\n }\n break;\n case 3:\n // Clear scrollback (everything not in viewport)\n const scrollBackSize = this._activeBuffer.lines.length - this._bufferService.rows;\n if (scrollBackSize > 0) {\n this._activeBuffer.lines.trimStart(scrollBackSize);\n this._activeBuffer.ybase = Math.max(this._activeBuffer.ybase - scrollBackSize, 0);\n this._activeBuffer.ydisp = Math.max(this._activeBuffer.ydisp - scrollBackSize, 0);\n // isUserScrolling tracks the normal buffer's viewport, so ED3 on the alt\n // screen must not touch it\n if (this._activeBuffer === this._bufferService.buffers.normal) {\n this._bufferService.isUserScrolling = false;\n }\n // Force a scroll event to refresh viewport\n this._onScroll.fire(0);\n }\n break;\n }\n return true;\n }\n\n /**\n * CSI Ps K Erase in Line (EL).\n * Ps = 0 -> Erase to Right (default).\n * Ps = 1 -> Erase to Left.\n * Ps = 2 -> Erase All.\n * CSI ? Ps K\n * Erase in Line (DECSEL).\n * Ps = 0 -> Selective Erase to Right (default).\n * Ps = 1 -> Selective Erase to Left.\n * Ps = 2 -> Selective Erase All.\n *\n * @vt: #Y CSI EL \"Erase In Line\" \"CSI Ps K\" \"Erase various parts of the active row.\"\n * Supported param values:\n *\n * | Ps | Effect |\n * | -- | -------------------------------------------------------- |\n * | 0 | Erase from the cursor through the end of the row. |\n * | 1 | Erase from the beginning of the line through the cursor. |\n * | 2 | Erase complete line. |\n *\n * @vt: #Y CSI DECSEL \"Selective Erase In Line\" \"CSI ? Ps K\" \"Same as EL with respecting protecting flag.\"\n */\n public eraseInLine(params: IParams, respectProtect: boolean = false): boolean {\n this._restrictCursor(this._bufferService.cols);\n switch (params.params[0]) {\n case 0:\n this._eraseInBufferLine(this._activeBuffer.y, this._activeBuffer.x, this._bufferService.cols, this._activeBuffer.x === 0, respectProtect);\n break;\n case 1:\n this._eraseInBufferLine(this._activeBuffer.y, 0, this._activeBuffer.x + 1, false, respectProtect);\n break;\n case 2:\n this._eraseInBufferLine(this._activeBuffer.y, 0, this._bufferService.cols, true, respectProtect);\n break;\n }\n this._dirtyRowTracker.markDirty(this._activeBuffer.y);\n return true;\n }\n\n /**\n * CSI Ps L\n * Insert Ps Line(s) (default = 1) (IL).\n *\n * @vt: #Y CSI IL \"Insert Line\" \"CSI Ps L\" \"Insert `Ps` blank lines at active row (default=1).\"\n * For every inserted line at the scroll top one line at the scroll bottom gets removed.\n * The cursor is set to the first column.\n * IL has no effect if the cursor is outside the scroll margins.\n */\n public insertLines(params: IParams): boolean {\n this._restrictCursor();\n let param = params.params[0] || 1;\n\n if (this._activeBuffer.y > this._activeBuffer.scrollBottom || this._activeBuffer.y < this._activeBuffer.scrollTop) {\n return true;\n }\n\n const row: number = this._activeBuffer.ybase + this._activeBuffer.y;\n\n const scrollBottomRowsOffset = this._bufferService.rows - 1 - this._activeBuffer.scrollBottom;\n const scrollBottomAbsolute = this._bufferService.rows - 1 + this._activeBuffer.ybase - scrollBottomRowsOffset + 1;\n while (param--) {\n // test: echo -e '\\e[44m\\e[1L\\e[0m'\n // blankLine(true) - xterm/linux behavior\n this._activeBuffer.lines.splice(scrollBottomAbsolute - 1, 1);\n this._activeBuffer.lines.splice(row, 0, this._activeBuffer.getBlankLine(this._eraseAttrData()));\n }\n\n this._dirtyRowTracker.markRangeDirty(this._activeBuffer.y, this._activeBuffer.scrollBottom);\n this._activeBuffer.x = 0; // see https://vt100.net/docs/vt220-rm/chapter4.html - vt220 only?\n return true;\n }\n\n /**\n * CSI Ps M\n * Delete Ps Line(s) (default = 1) (DL).\n *\n * @vt: #Y CSI DL \"Delete Line\" \"CSI Ps M\" \"Delete `Ps` lines at active row (default=1).\"\n * For every deleted line at the scroll top one blank line at the scroll bottom gets appended.\n * The cursor is set to the first column.\n * DL has no effect if the cursor is outside the scroll margins.\n */\n public deleteLines(params: IParams): boolean {\n this._restrictCursor();\n let param = params.params[0] || 1;\n\n if (this._activeBuffer.y > this._activeBuffer.scrollBottom || this._activeBuffer.y < this._activeBuffer.scrollTop) {\n return true;\n }\n\n const row: number = this._activeBuffer.ybase + this._activeBuffer.y;\n\n let j: number;\n j = this._bufferService.rows - 1 - this._activeBuffer.scrollBottom;\n j = this._bufferService.rows - 1 + this._activeBuffer.ybase - j;\n while (param--) {\n // test: echo -e '\\e[44m\\e[1M\\e[0m'\n // blankLine(true) - xterm/linux behavior\n this._activeBuffer.lines.splice(row, 1);\n this._activeBuffer.lines.splice(j, 0, this._activeBuffer.getBlankLine(this._eraseAttrData()));\n }\n\n this._dirtyRowTracker.markRangeDirty(this._activeBuffer.y, this._activeBuffer.scrollBottom);\n this._activeBuffer.x = 0; // see https://vt100.net/docs/vt220-rm/chapter4.html - vt220 only?\n return true;\n }\n\n /**\n * CSI Ps @\n * Insert Ps (Blank) Character(s) (default = 1) (ICH).\n *\n * @vt: #Y CSI ICH \"Insert Characters\" \"CSI Ps @\" \"Insert `Ps` (blank) characters (default = 1).\"\n * The ICH sequence inserts `Ps` blank characters. The cursor remains at the beginning of the\n * blank characters. Text between the cursor and right margin moves to the right. Characters moved\n * past the right margin are lost.\n *\n *\n * FIXME: check against xterm - should not work outside of scroll margins (see VT520 manual)\n */\n public insertChars(params: IParams): boolean {\n this._restrictCursor();\n const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y);\n if (line) {\n line.insertCells(\n this._activeBuffer.x,\n params.params[0] || 1,\n this._activeBuffer.getNullCell(this._eraseAttrData())\n );\n this._dirtyRowTracker.markDirty(this._activeBuffer.y);\n }\n return true;\n }\n\n /**\n * CSI Ps P\n * Delete Ps Character(s) (default = 1) (DCH).\n *\n * @vt: #Y CSI DCH \"Delete Character\" \"CSI Ps P\" \"Delete `Ps` characters (default=1).\"\n * As characters are deleted, the remaining characters between the cursor and right margin move to\n * the left. Character attributes move with the characters. The terminal adds blank characters at\n * the right margin.\n *\n *\n * FIXME: check against xterm - should not work outside of scroll margins (see VT520 manual)\n */\n public deleteChars(params: IParams): boolean {\n this._restrictCursor();\n const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y);\n if (line) {\n line.deleteCells(\n this._activeBuffer.x,\n params.params[0] || 1,\n this._activeBuffer.getNullCell(this._eraseAttrData())\n );\n this._dirtyRowTracker.markDirty(this._activeBuffer.y);\n }\n return true;\n }\n\n /**\n * CSI Ps S Scroll up Ps lines (default = 1) (SU).\n *\n * @vt: #Y CSI SU \"Scroll Up\" \"CSI Ps S\" \"Scroll `Ps` lines up (default=1).\"\n *\n *\n * FIXME: scrolled out lines at top = 1 should add to scrollback (xterm)\n */\n public scrollUp(params: IParams): boolean {\n let param = params.params[0] || 1;\n\n while (param--) {\n this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollTop, 1);\n this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollBottom, 0, this._activeBuffer.getBlankLine(this._eraseAttrData()));\n }\n this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);\n return true;\n }\n\n /**\n * CSI Ps T Scroll down Ps lines (default = 1) (SD).\n *\n * @vt: #Y CSI SD \"Scroll Down\" \"CSI Ps T\" \"Scroll `Ps` lines down (default=1).\"\n */\n public scrollDown(params: IParams): boolean {\n let param = params.params[0] || 1;\n\n while (param--) {\n this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollBottom, 1);\n this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollTop, 0, this._activeBuffer.getBlankLine(DEFAULT_ATTR_DATA));\n }\n this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);\n return true;\n }\n\n /**\n * CSI Ps SP @ Scroll left Ps columns (default = 1) (SL) ECMA-48\n *\n * Notation: (Pn)\n * Representation: CSI Pn 02/00 04/00\n * Parameter default value: Pn = 1\n * SL causes the data in the presentation component to be moved by n character positions\n * if the line orientation is horizontal, or by n line positions if the line orientation\n * is vertical, such that the data appear to move to the left; where n equals the value of Pn.\n * The active presentation position is not affected by this control function.\n *\n * Supported:\n * - always left shift (no line orientation setting respected)\n *\n * @vt: #Y CSI SL \"Scroll Left\" \"CSI Ps SP @\" \"Scroll viewport `Ps` times to the left.\"\n * SL moves the content of all lines within the scroll margins `Ps` times to the left.\n * SL has no effect outside of the scroll margins.\n */\n public scrollLeft(params: IParams): boolean {\n if (this._activeBuffer.y > this._activeBuffer.scrollBottom || this._activeBuffer.y < this._activeBuffer.scrollTop) {\n return true;\n }\n const param = params.params[0] || 1;\n for (let y = this._activeBuffer.scrollTop; y <= this._activeBuffer.scrollBottom; ++y) {\n const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!;\n line.deleteCells(0, param, this._activeBuffer.getNullCell(this._eraseAttrData()));\n line.isWrapped = false;\n }\n this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);\n return true;\n }\n\n /**\n * CSI Ps SP A Scroll right Ps columns (default = 1) (SR) ECMA-48\n *\n * Notation: (Pn)\n * Representation: CSI Pn 02/00 04/01\n * Parameter default value: Pn = 1\n * SR causes the data in the presentation component to be moved by n character positions\n * if the line orientation is horizontal, or by n line positions if the line orientation\n * is vertical, such that the data appear to move to the right; where n equals the value of Pn.\n * The active presentation position is not affected by this control function.\n *\n * Supported:\n * - always right shift (no line orientation setting respected)\n *\n * @vt: #Y CSI SR \"Scroll Right\" \"CSI Ps SP A\" \"Scroll viewport `Ps` times to the right.\"\n * SL moves the content of all lines within the scroll margins `Ps` times to the right.\n * Content at the right margin is lost.\n * SL has no effect outside of the scroll margins.\n */\n public scrollRight(params: IParams): boolean {\n if (this._activeBuffer.y > this._activeBuffer.scrollBottom || this._activeBuffer.y < this._activeBuffer.scrollTop) {\n return true;\n }\n const param = params.params[0] || 1;\n for (let y = this._activeBuffer.scrollTop; y <= this._activeBuffer.scrollBottom; ++y) {\n const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!;\n line.insertCells(0, param, this._activeBuffer.getNullCell(this._eraseAttrData()));\n line.isWrapped = false;\n }\n this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);\n return true;\n }\n\n /**\n * CSI Pm ' }\n * Insert Ps Column(s) (default = 1) (DECIC), VT420 and up.\n *\n * @vt: #Y CSI DECIC \"Insert Columns\" \"CSI Ps ' }\" \"Insert `Ps` columns at cursor position.\"\n * DECIC inserts `Ps` times blank columns at the cursor position for all lines with the scroll\n * margins, moving content to the right. Content at the right margin is lost. DECIC has no effect\n * outside the scrolling margins.\n */\n public insertColumns(params: IParams): boolean {\n if (this._activeBuffer.y > this._activeBuffer.scrollBottom || this._activeBuffer.y < this._activeBuffer.scrollTop) {\n return true;\n }\n const param = params.params[0] || 1;\n for (let y = this._activeBuffer.scrollTop; y <= this._activeBuffer.scrollBottom; ++y) {\n const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!;\n line.insertCells(this._activeBuffer.x, param, this._activeBuffer.getNullCell(this._eraseAttrData()));\n line.isWrapped = false;\n }\n this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);\n return true;\n }\n\n /**\n * CSI Pm ' ~\n * Delete Ps Column(s) (default = 1) (DECDC), VT420 and up.\n *\n * @vt: #Y CSI DECDC \"Delete Columns\" \"CSI Ps ' ~\" \"Delete `Ps` columns at cursor position.\"\n * DECDC deletes `Ps` times columns at the cursor position for all lines with the scroll margins,\n * moving content to the left. Blank columns are added at the right margin.\n * DECDC has no effect outside the scrolling margins.\n */\n public deleteColumns(params: IParams): boolean {\n if (this._activeBuffer.y > this._activeBuffer.scrollBottom || this._activeBuffer.y < this._activeBuffer.scrollTop) {\n return true;\n }\n const param = params.params[0] || 1;\n for (let y = this._activeBuffer.scrollTop; y <= this._activeBuffer.scrollBottom; ++y) {\n const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!;\n line.deleteCells(this._activeBuffer.x, param, this._activeBuffer.getNullCell(this._eraseAttrData()));\n line.isWrapped = false;\n }\n this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);\n return true;\n }\n\n /**\n * CSI Ps X\n * Erase Ps Character(s) (default = 1) (ECH).\n *\n * @vt: #Y CSI ECH \"Erase Character\" \"CSI Ps X\" \"Erase `Ps` characters from current cursor position to the right (default=1).\"\n * ED erases `Ps` characters from current cursor position to the right.\n * ED works inside or outside the scrolling margins.\n */\n public eraseChars(params: IParams): boolean {\n this._restrictCursor();\n const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y);\n if (line) {\n line.replaceCells(\n this._activeBuffer.x,\n this._activeBuffer.x + (params.params[0] || 1),\n this._activeBuffer.getNullCell(this._eraseAttrData())\n );\n this._dirtyRowTracker.markDirty(this._activeBuffer.y);\n }\n return true;\n }\n\n /**\n * CSI Ps b Repeat the preceding graphic character Ps times (REP).\n * From ECMA 48 (@see http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-048.pdf)\n * Notation: (Pn)\n * Representation: CSI Pn 06/02\n * Parameter default value: Pn = 1\n * REP is used to indicate that the preceding character in the data stream,\n * if it is a graphic character (represented by one or more bit combinations) including SPACE,\n * is to be repeated n times, where n equals the value of Pn.\n * If the character preceding REP is a control function or part of a control function,\n * the effect of REP is not defined by this Standard.\n *\n * We extend xterm's behavior to allow repeating entire grapheme clusters.\n * This isn't 100% xterm-compatible, but it seems saner and more useful.\n * - text attrs are applied normally\n * - wrap around is respected\n * - any valid sequence resets the carried forward char\n *\n * Note: To get reset on a valid sequence working correctly without much runtime penalty, the\n * preceding codepoint is stored on the parser in `this.print` and reset during `parser.parse`.\n *\n * @vt: #Y CSI REP \"Repeat Preceding Character\" \"CSI Ps b\" \"Repeat preceding character `Ps` times (default=1).\"\n * REP repeats the previous character `Ps` times advancing the cursor, also wrapping if DECAWM is\n * set. REP has no effect if the sequence does not follow a printable ASCII character\n * (NOOP for any other sequence in between or NON ASCII characters).\n */\n public repeatPrecedingCharacter(params: IParams): boolean {\n const joinState = this._parser.precedingJoinState;\n if (!joinState) {\n return true;\n }\n // call print to insert the chars and handle correct wrapping\n const length = params.params[0] || 1;\n const chWidth = UnicodeService.extractWidth(joinState);\n const x = this._activeBuffer.x - chWidth;\n const bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!;\n const text = bufferRow.getString(x);\n const data = new Uint32Array(text.length * length);\n let idata = 0;\n for (let itext = 0; itext < text.length;) {\n const ch = text.codePointAt(itext) || 0;\n data[idata++] = ch;\n itext += ch > 0xffff ? 2 : 1;\n }\n let tlength = idata;\n for (let i = 1; i < length; ++i) {\n data.copyWithin(tlength, 0, idata);\n tlength += idata;\n }\n this.print(data, 0, tlength);\n return true;\n }\n\n /**\n * CSI Ps c Send Device Attributes (Primary DA).\n * Ps = 0 or omitted -> request attributes from terminal. The\n * response depends on the decTerminalID resource setting.\n * -> CSI ? 1 ; 2 c (``VT100 with Advanced Video Option'')\n * -> CSI ? 1 ; 0 c (``VT101 with No Options'')\n * -> CSI ? 6 c (``VT102'')\n * -> CSI ? 6 0 ; 1 ; 2 ; 6 ; 8 ; 9 ; 1 5 ; c (``VT220'')\n * The VT100-style response parameters do not mean anything by\n * themselves. VT220 parameters do, telling the host what fea-\n * tures the terminal supports:\n * Ps = 1 -> 132-columns.\n * Ps = 2 -> Printer.\n * Ps = 6 -> Selective erase.\n * Ps = 8 -> User-defined keys.\n * Ps = 9 -> National replacement character sets.\n * Ps = 1 5 -> Technical characters.\n * Ps = 2 2 -> ANSI color, e.g., VT525.\n * Ps = 2 9 -> ANSI text locator (i.e., DEC Locator mode).\n *\n * @vt: #Y CSI DA1 \"Primary Device Attributes\" \"CSI c\" \"Send primary device attributes.\"\n *\n *\n * TODO: fix and cleanup response\n */\n public sendDeviceAttributesPrimary(params: IParams): boolean {\n if (params.params[0] > 0) {\n return true;\n }\n if (this._is('xterm') || this._is('rxvt-unicode') || this._is('screen')) {\n this._coreService.triggerDataEvent(C0.ESC + '[?1;2c');\n } else if (this._is('linux')) {\n this._coreService.triggerDataEvent(C0.ESC + '[?6c');\n }\n return true;\n }\n\n /**\n * CSI > Ps c\n * Send Device Attributes (Secondary DA).\n * Ps = 0 or omitted -> request the terminal's identification\n * code. The response depends on the decTerminalID resource set-\n * ting. It should apply only to VT220 and up, but xterm extends\n * this to VT100.\n * -> CSI > Pp ; Pv ; Pc c\n * where Pp denotes the terminal type\n * Pp = 0 -> ``VT100''.\n * Pp = 1 -> ``VT220''.\n * and Pv is the firmware version (for xterm, this was originally\n * the XFree86 patch number, starting with 95). In a DEC termi-\n * nal, Pc indicates the ROM cartridge registration number and is\n * always zero.\n * More information:\n * xterm/charproc.c - line 2012, for more information.\n * vim responds with ^[[?0c or ^[[?1c after the terminal's response (?)\n *\n * @vt: #Y CSI DA2 \"Secondary Device Attributes\" \"CSI > c\" \"Send primary device attributes.\"\n *\n *\n * TODO: fix and cleanup response\n */\n public sendDeviceAttributesSecondary(params: IParams): boolean {\n if (params.params[0] > 0) {\n return true;\n }\n // xterm and urxvt\n // seem to spit this\n // out around ~370 times (?).\n if (this._is('xterm')) {\n this._coreService.triggerDataEvent(C0.ESC + '[>0;276;0c');\n } else if (this._is('rxvt-unicode')) {\n this._coreService.triggerDataEvent(C0.ESC + '[>85;95;0c');\n } else if (this._is('linux')) {\n // not supported by linux console.\n // linux console echoes parameters.\n this._coreService.triggerDataEvent(params.params[0] + 'c');\n } else if (this._is('screen')) {\n this._coreService.triggerDataEvent(C0.ESC + '[>83;40003;0c');\n }\n return true;\n }\n\n /**\n * CSI > Ps q\n * Ps = 0 => Report xterm name and version (XTVERSION).\n *\n * The response is a DCS sequence identifying the version: DCS > | text ST\n *\n * @vt: #Y CSI XTVERSION \"Report Xterm Version\" \"CSI > q\" \"Report the terminal name and version.\"\n */\n public sendXtVersion(params: IParams): boolean {\n if (params.params[0] > 0) {\n return true;\n }\n this._coreService.triggerDataEvent(`${C0.ESC}P>|xterm.js(${XTERM_VERSION})${C0.ESC}\\\\`);\n return true;\n }\n\n /**\n * Evaluate if the current terminal is the given argument.\n * @param term The terminal name to evaluate\n */\n private _is(term: string): boolean {\n return (this._optionsService.rawOptions.termName + '').startsWith(term);\n }\n\n /**\n * CSI Pm h Set Mode (SM).\n * Ps = 2 -> Keyboard Action Mode (AM).\n * Ps = 4 -> Insert Mode (IRM).\n * Ps = 1 2 -> Send/receive (SRM).\n * Ps = 2 0 -> Automatic Newline (LNM).\n *\n * @vt: #P[Only IRM is supported.] CSI SM \"Set Mode\" \"CSI Pm h\" \"Set various terminal modes.\"\n * Supported param values by SM:\n *\n * | Param | Action | Support |\n * | ----- | -------------------------------------- | ------- |\n * | 2 | Keyboard Action Mode (KAM). Always on. | #N |\n * | 4 | Insert Mode (IRM). | #Y |\n * | 12 | Send/receive (SRM). Always off. | #N |\n * | 20 | Automatic Newline (LNM). | #Y |\n */\n public setMode(params: IParams): boolean {\n for (let i = 0; i < params.length; i++) {\n switch (params.params[i]) {\n case 4:\n this._coreService.modes.insertMode = true;\n break;\n case 20:\n this._optionsService.options.convertEol = true;\n break;\n }\n }\n return true;\n }\n\n /**\n * CSI ? Pm h\n * DEC Private Mode Set (DECSET).\n * Ps = 1 -> Application Cursor Keys (DECCKM).\n * Ps = 2 -> Designate USASCII for character sets G0-G3\n * (DECANM), and set VT100 mode.\n * Ps = 3 -> 132 Column Mode (DECCOLM).\n * Ps = 4 -> Smooth (Slow) Scroll (DECSCLM).\n * Ps = 5 -> Reverse Video (DECSCNM).\n * Ps = 6 -> Origin Mode (DECOM).\n * Ps = 7 -> Wraparound Mode (DECAWM).\n * Ps = 8 -> Auto-repeat Keys (DECARM).\n * Ps = 9 -> Send Mouse X & Y on button press. See the sec-\n * tion Mouse Tracking.\n * Ps = 1 0 -> Show toolbar (rxvt).\n * Ps = 1 2 -> Start Blinking Cursor (att610).\n * Ps = 1 8 -> Print form feed (DECPFF).\n * Ps = 1 9 -> Set print extent to full screen (DECPEX).\n * Ps = 2 5 -> Show Cursor (DECTCEM).\n * Ps = 3 0 -> Show scrollbar (rxvt).\n * Ps = 3 5 -> Enable font-shifting functions (rxvt).\n * Ps = 3 8 -> Enter Tektronix Mode (DECTEK).\n * Ps = 4 0 -> Allow 80 -> 132 Mode.\n * Ps = 4 1 -> more(1) fix (see curses resource).\n * Ps = 4 2 -> Enable Nation Replacement Character sets (DECN-\n * RCM).\n * Ps = 4 4 -> Turn On Margin Bell.\n * Ps = 4 5 -> Reverse-wraparound Mode.\n * Ps = 4 6 -> Start Logging. This is normally disabled by a\n * compile-time option.\n * Ps = 4 7 -> Use Alternate Screen Buffer. (This may be dis-\n * abled by the titeInhibit resource).\n * Ps = 6 6 -> Application keypad (DECNKM).\n * Ps = 6 7 -> Backarrow key sends backspace (DECBKM).\n * Ps = 1 0 0 0 -> Send Mouse X & Y on button press and\n * release. See the section Mouse Tracking.\n * Ps = 1 0 0 1 -> Use Hilite Mouse Tracking.\n * Ps = 1 0 0 2 -> Use Cell Motion Mouse Tracking.\n * Ps = 1 0 0 3 -> Use All Motion Mouse Tracking.\n * Ps = 1 0 0 4 -> Send FocusIn/FocusOut events.\n * Ps = 1 0 0 5 -> Enable Extended Mouse Mode.\n * Ps = 1 0 1 0 -> Scroll to bottom on tty output (rxvt).\n * Ps = 1 0 1 1 -> Scroll to bottom on key press (rxvt).\n * Ps = 1 0 3 4 -> Interpret \"meta\" key, sets eighth bit.\n * (enables the eightBitInput resource).\n * Ps = 1 0 3 5 -> Enable special modifiers for Alt and Num-\n * Lock keys. (This enables the numLock resource).\n * Ps = 1 0 3 6 -> Send ESC when Meta modifies a key. (This\n * enables the metaSendsEscape resource).\n * Ps = 1 0 3 7 -> Send DEL from the editing-keypad Delete\n * key.\n * Ps = 1 0 3 9 -> Send ESC when Alt modifies a key. (This\n * enables the altSendsEscape resource).\n * Ps = 1 0 4 0 -> Keep selection even if not highlighted.\n * (This enables the keepSelection resource).\n * Ps = 1 0 4 1 -> Use the CLIPBOARD selection. (This enables\n * the selectToClipboard resource).\n * Ps = 1 0 4 2 -> Enable Urgency window manager hint when\n * Control-G is received. (This enables the bellIsUrgent\n * resource).\n * Ps = 1 0 4 3 -> Enable raising of the window when Control-G\n * is received. (enables the popOnBell resource).\n * Ps = 1 0 4 7 -> Use Alternate Screen Buffer. (This may be\n * disabled by the titeInhibit resource).\n * Ps = 1 0 4 8 -> Save cursor as in DECSC. (This may be dis-\n * abled by the titeInhibit resource).\n * Ps = 1 0 4 9 -> Save cursor as in DECSC and use Alternate\n * Screen Buffer, clearing it first. (This may be disabled by\n * the titeInhibit resource). This combines the effects of the 1\n * 0 4 7 and 1 0 4 8 modes. Use this with terminfo-based\n * applications rather than the 4 7 mode.\n * Ps = 1 0 5 0 -> Set terminfo/termcap function-key mode.\n * Ps = 1 0 5 1 -> Set Sun function-key mode.\n * Ps = 1 0 5 2 -> Set HP function-key mode.\n * Ps = 1 0 5 3 -> Set SCO function-key mode.\n * Ps = 1 0 6 0 -> Set legacy keyboard emulation (X11R6).\n * Ps = 1 0 6 1 -> Set VT220 keyboard emulation.\n * Ps = 2 0 0 4 -> Set bracketed paste mode.\n * Modes:\n * http: *vt100.net/docs/vt220-rm/chapter4.html\n *\n * @vt: #P[See below for supported modes.] CSI DECSET \"DEC Private Set Mode\" \"CSI ? Pm h\" \"Set various terminal attributes.\"\n * Supported param values by DECSET:\n *\n * | param | Action | Support |\n * | ----- | ------------------------------------------------------- | --------|\n * | 1 | Application Cursor Keys (DECCKM). | #Y |\n * | 2 | Designate US-ASCII for character sets G0-G3 (DECANM). | #Y |\n * | 3 | 132 Column Mode (DECCOLM). | #Y |\n * | 6 | Origin Mode (DECOM). | #Y |\n * | 7 | Auto-wrap Mode (DECAWM). | #Y |\n * | 8 | Auto-repeat Keys (DECARM). Always on. | #N |\n * | 9 | X10 xterm mouse protocol. | #Y |\n * | 12 | Start Blinking Cursor. | #P[Requires the allowSetCursorBlink quirk option enabled.] |\n * | 25 | Show Cursor (DECTCEM). | #Y |\n * | 45 | Reverse wrap-around. | #Y |\n * | 47 | Use Alternate Screen Buffer. | #Y |\n * | 66 | Application keypad (DECNKM). | #Y |\n * | 1000 | X11 xterm mouse protocol. | #Y |\n * | 1002 | Use Cell Motion Mouse Tracking. | #Y |\n * | 1003 | Use All Motion Mouse Tracking. | #Y |\n * | 1004 | Send FocusIn/FocusOut events | #Y |\n * | 1005 | Enable UTF-8 Mouse Mode. | #N |\n * | 1006 | Enable SGR Mouse Mode. | #Y |\n * | 1015 | Enable urxvt Mouse Mode. | #N |\n * | 1016 | Enable SGR-Pixels Mouse Mode. | #Y |\n * | 1047 | Use Alternate Screen Buffer. | #Y |\n * | 1048 | Save cursor as in DECSC. | #Y |\n * | 1049 | Save cursor and switch to alternate buffer clearing it. | #P[Does not clear the alternate buffer.] |\n * | 2004 | Set bracketed paste mode. | #Y |\n *\n *\n * FIXME: implement DECSCNM, 1049 should clear altbuffer\n */\n public setModePrivate(params: IParams): boolean {\n for (let i = 0; i < params.length; i++) {\n switch (params.params[i]) {\n case 1:\n this._coreService.decPrivateModes.applicationCursorKeys = true;\n break;\n case 2:\n this._charsetService.setgCharset(0, DEFAULT_CHARSET);\n this._charsetService.setgCharset(1, DEFAULT_CHARSET);\n this._charsetService.setgCharset(2, DEFAULT_CHARSET);\n this._charsetService.setgCharset(3, DEFAULT_CHARSET);\n // set VT100 mode here\n break;\n case 3:\n /**\n * DECCOLM - 132 column mode.\n * This is only active if 'SetWinLines' (24) is enabled\n * through `options.windowsOptions`.\n */\n if (this._optionsService.rawOptions.windowOptions.setWinLines) {\n this._bufferService.resize(132, this._bufferService.rows);\n this._onRequestReset.fire();\n }\n break;\n case 6:\n this._coreService.decPrivateModes.origin = true;\n this._setCursor(0, 0);\n break;\n case 7:\n this._coreService.decPrivateModes.wraparound = true;\n break;\n case 12:\n if (this._optionsService.rawOptions.quirks?.allowSetCursorBlink) {\n this._optionsService.options.cursorBlink = true;\n }\n break;\n case 45:\n this._coreService.decPrivateModes.reverseWraparound = true;\n break;\n case 66:\n this._logService.debug('Serial port requested application keypad.');\n this._coreService.decPrivateModes.applicationKeypad = true;\n this._onRequestSyncScrollBar.fire();\n break;\n case 9: // X10 Mouse\n // no release, no motion, no wheel, no modifiers.\n this._mouseStateService.activeProtocol = 'X10';\n break;\n case 1000: // vt200 mouse\n // no motion.\n this._mouseStateService.activeProtocol = 'VT200';\n break;\n case 1002: // button event mouse\n this._mouseStateService.activeProtocol = 'DRAG';\n break;\n case 1003: // any event mouse\n // any event - sends motion events,\n // even if there is no button held down.\n this._mouseStateService.activeProtocol = 'ANY';\n break;\n case 1004: // send focusin/focusout events\n // focusin: ^[[I\n // focusout: ^[[O\n this._coreService.decPrivateModes.sendFocus = true;\n this._onRequestSendFocus.fire();\n break;\n case 1005: // utf8 ext mode mouse - removed in #2507\n this._logService.debug('DECSET 1005 not supported (see #2507)');\n break;\n case 1006: // sgr ext mode mouse\n this._mouseStateService.activeEncoding = 'SGR';\n break;\n case 1015: // urxvt ext mode mouse - removed in #2507\n this._logService.debug('DECSET 1015 not supported (see #2507)');\n break;\n case 1016: // sgr pixels mode mouse\n this._mouseStateService.activeEncoding = 'SGR_PIXELS';\n break;\n case 25: // show cursor\n this._coreService.isCursorHidden = false;\n break;\n case 1048: // alt screen cursor\n this.saveCursor();\n break;\n case 1049: // alt screen buffer cursor\n this.saveCursor();\n // FALL-THROUGH\n case 47: // alt screen buffer\n case 1047: // alt screen buffer\n // Swap kitty keyboard flags: save main, restore alt\n if (this._optionsService.rawOptions.vtExtensions?.kittyKeyboard) {\n const state = this._coreService.kittyKeyboard;\n state.mainFlags = state.flags;\n state.flags = state.altFlags;\n }\n this._bufferService.buffers.activateAltBuffer(this._eraseAttrData());\n this._coreService.isCursorInitialized = true;\n this._onRequestRefreshRows.fire(undefined);\n this._onRequestSyncScrollBar.fire();\n break;\n case 2004: // bracketed paste mode (https://cirw.in/blog/bracketed-paste)\n this._coreService.decPrivateModes.bracketedPasteMode = true;\n break;\n case 2026: // synchronized output (https://github.com/contour-terminal/vt-extensions/blob/master/synchronized-output.md)\n this._coreService.decPrivateModes.synchronizedOutput = true;\n break;\n case 2031: // color scheme updates (https://contour-terminal.org/vt-extensions/color-palette-update-notifications/)\n if (this._optionsService.rawOptions.vtExtensions?.colorSchemeQuery ?? true) {\n this._coreService.decPrivateModes.colorSchemeUpdates = true;\n }\n break;\n case 9001: // win32-input-mode (https://github.com/microsoft/terminal/blob/main/doc/specs/%234999%20-%20Improved%20keyboard%20handling%20in%20Conpty.md)\n if (this._optionsService.rawOptions.vtExtensions?.win32InputMode) {\n this._coreService.decPrivateModes.win32InputMode = true;\n }\n break;\n }\n }\n return true;\n }\n\n\n /**\n * CSI Pm l Reset Mode (RM).\n * Ps = 2 -> Keyboard Action Mode (AM).\n * Ps = 4 -> Replace Mode (IRM).\n * Ps = 1 2 -> Send/receive (SRM).\n * Ps = 2 0 -> Normal Linefeed (LNM).\n *\n * @vt: #P[Only IRM is supported.] CSI RM \"Reset Mode\" \"CSI Pm l\" \"Set various terminal attributes.\"\n * Supported param values by RM:\n *\n * | Param | Action | Support |\n * | ----- | -------------------------------------- | ------- |\n * | 2 | Keyboard Action Mode (KAM). Always on. | #N |\n * | 4 | Replace Mode (IRM). (default) | #Y |\n * | 12 | Send/receive (SRM). Always off. | #N |\n * | 20 | Normal Linefeed (LNM). | #Y |\n *\n *\n * FIXME: why is LNM commented out?\n */\n public resetMode(params: IParams): boolean {\n for (let i = 0; i < params.length; i++) {\n switch (params.params[i]) {\n case 4:\n this._coreService.modes.insertMode = false;\n break;\n case 20:\n this._optionsService.options.convertEol = false;\n break;\n }\n }\n return true;\n }\n\n /**\n * CSI ? Pm l\n * DEC Private Mode Reset (DECRST).\n * Ps = 1 -> Normal Cursor Keys (DECCKM).\n * Ps = 2 -> Designate VT52 mode (DECANM).\n * Ps = 3 -> 80 Column Mode (DECCOLM).\n * Ps = 4 -> Jump (Fast) Scroll (DECSCLM).\n * Ps = 5 -> Normal Video (DECSCNM).\n * Ps = 6 -> Normal Cursor Mode (DECOM).\n * Ps = 7 -> No Wraparound Mode (DECAWM).\n * Ps = 8 -> No Auto-repeat Keys (DECARM).\n * Ps = 9 -> Don't send Mouse X & Y on button press.\n * Ps = 1 0 -> Hide toolbar (rxvt).\n * Ps = 1 2 -> Stop Blinking Cursor (att610).\n * Ps = 1 8 -> Don't print form feed (DECPFF).\n * Ps = 1 9 -> Limit print to scrolling region (DECPEX).\n * Ps = 2 5 -> Hide Cursor (DECTCEM).\n * Ps = 3 0 -> Don't show scrollbar (rxvt).\n * Ps = 3 5 -> Disable font-shifting functions (rxvt).\n * Ps = 4 0 -> Disallow 80 -> 132 Mode.\n * Ps = 4 1 -> No more(1) fix (see curses resource).\n * Ps = 4 2 -> Disable Nation Replacement Character sets (DEC-\n * NRCM).\n * Ps = 4 4 -> Turn Off Margin Bell.\n * Ps = 4 5 -> No Reverse-wraparound Mode.\n * Ps = 4 6 -> Stop Logging. (This is normally disabled by a\n * compile-time option).\n * Ps = 4 7 -> Use Normal Screen Buffer.\n * Ps = 6 6 -> Numeric keypad (DECNKM).\n * Ps = 6 7 -> Backarrow key sends delete (DECBKM).\n * Ps = 1 0 0 0 -> Don't send Mouse X & Y on button press and\n * release. See the section Mouse Tracking.\n * Ps = 1 0 0 1 -> Don't use Hilite Mouse Tracking.\n * Ps = 1 0 0 2 -> Don't use Cell Motion Mouse Tracking.\n * Ps = 1 0 0 3 -> Don't use All Motion Mouse Tracking.\n * Ps = 1 0 0 4 -> Don't send FocusIn/FocusOut events.\n * Ps = 1 0 0 5 -> Disable Extended Mouse Mode.\n * Ps = 1 0 1 0 -> Don't scroll to bottom on tty output\n * (rxvt).\n * Ps = 1 0 1 1 -> Don't scroll to bottom on key press (rxvt).\n * Ps = 1 0 3 4 -> Don't interpret \"meta\" key. (This disables\n * the eightBitInput resource).\n * Ps = 1 0 3 5 -> Disable special modifiers for Alt and Num-\n * Lock keys. (This disables the numLock resource).\n * Ps = 1 0 3 6 -> Don't send ESC when Meta modifies a key.\n * (This disables the metaSendsEscape resource).\n * Ps = 1 0 3 7 -> Send VT220 Remove from the editing-keypad\n * Delete key.\n * Ps = 1 0 3 9 -> Don't send ESC when Alt modifies a key.\n * (This disables the altSendsEscape resource).\n * Ps = 1 0 4 0 -> Do not keep selection when not highlighted.\n * (This disables the keepSelection resource).\n * Ps = 1 0 4 1 -> Use the PRIMARY selection. (This disables\n * the selectToClipboard resource).\n * Ps = 1 0 4 2 -> Disable Urgency window manager hint when\n * Control-G is received. (This disables the bellIsUrgent\n * resource).\n * Ps = 1 0 4 3 -> Disable raising of the window when Control-\n * G is received. (This disables the popOnBell resource).\n * Ps = 1 0 4 7 -> Use Normal Screen Buffer, clearing screen\n * first if in the Alternate Screen. (This may be disabled by\n * the titeInhibit resource).\n * Ps = 1 0 4 8 -> Restore cursor as in DECRC. (This may be\n * disabled by the titeInhibit resource).\n * Ps = 1 0 4 9 -> Use Normal Screen Buffer and restore cursor\n * as in DECRC. (This may be disabled by the titeInhibit\n * resource). This combines the effects of the 1 0 4 7 and 1 0\n * 4 8 modes. Use this with terminfo-based applications rather\n * than the 4 7 mode.\n * Ps = 1 0 5 0 -> Reset terminfo/termcap function-key mode.\n * Ps = 1 0 5 1 -> Reset Sun function-key mode.\n * Ps = 1 0 5 2 -> Reset HP function-key mode.\n * Ps = 1 0 5 3 -> Reset SCO function-key mode.\n * Ps = 1 0 6 0 -> Reset legacy keyboard emulation (X11R6).\n * Ps = 1 0 6 1 -> Reset keyboard emulation to Sun/PC style.\n * Ps = 2 0 0 4 -> Reset bracketed paste mode.\n *\n * @vt: #P[See below for supported modes.] CSI DECRST \"DEC Private Reset Mode\" \"CSI ? Pm l\" \"Reset various terminal attributes.\"\n * Supported param values by DECRST:\n *\n * | param | Action | Support |\n * | ----- | ------------------------------------------------------- | ------- |\n * | 1 | Normal Cursor Keys (DECCKM). | #Y |\n * | 2 | Designate VT52 mode (DECANM). | #N |\n * | 3 | 80 Column Mode (DECCOLM). | #B[Switches to old column width instead of 80.] |\n * | 6 | Normal Cursor Mode (DECOM). | #Y |\n * | 7 | No Wraparound Mode (DECAWM). | #Y |\n * | 8 | No Auto-repeat Keys (DECARM). | #N |\n * | 9 | Don't send Mouse X & Y on button press. | #Y |\n * | 12 | Stop Blinking Cursor. | #P[Requires the allowSetCursorBlink quirk option enabled.] |\n * | 25 | Hide Cursor (DECTCEM). | #Y |\n * | 45 | No reverse wrap-around. | #Y |\n * | 47 | Use Normal Screen Buffer. | #Y |\n * | 66 | Numeric keypad (DECNKM). | #Y |\n * | 1000 | Don't send Mouse reports. | #Y |\n * | 1002 | Don't use Cell Motion Mouse Tracking. | #Y |\n * | 1003 | Don't use All Motion Mouse Tracking. | #Y |\n * | 1004 | Don't send FocusIn/FocusOut events. | #Y |\n * | 1005 | Disable UTF-8 Mouse Mode. | #N |\n * | 1006 | Disable SGR Mouse Mode. | #Y |\n * | 1015 | Disable urxvt Mouse Mode. | #N |\n * | 1016 | Disable SGR-Pixels Mouse Mode. | #Y |\n * | 1047 | Use Normal Screen Buffer (clearing screen if in alt). | #Y |\n * | 1048 | Restore cursor as in DECRC. | #Y |\n * | 1049 | Use Normal Screen Buffer and restore cursor. | #Y |\n * | 2004 | Reset bracketed paste mode. | #Y |\n *\n *\n * FIXME: DECCOLM is currently broken (already fixed in window options PR)\n */\n public resetModePrivate(params: IParams): boolean {\n for (let i = 0; i < params.length; i++) {\n switch (params.params[i]) {\n case 1:\n this._coreService.decPrivateModes.applicationCursorKeys = false;\n break;\n case 3:\n /**\n * DECCOLM - 80 column mode.\n * This is only active if 'SetWinLines' (24) is enabled\n * through `options.windowsOptions`.\n */\n if (this._optionsService.rawOptions.windowOptions.setWinLines) {\n this._bufferService.resize(80, this._bufferService.rows);\n this._onRequestReset.fire();\n }\n break;\n case 6:\n this._coreService.decPrivateModes.origin = false;\n this._setCursor(0, 0);\n break;\n case 7:\n this._coreService.decPrivateModes.wraparound = false;\n break;\n case 12:\n if (this._optionsService.rawOptions.quirks?.allowSetCursorBlink) {\n this._optionsService.options.cursorBlink = false;\n }\n break;\n case 45:\n this._coreService.decPrivateModes.reverseWraparound = false;\n break;\n case 66:\n this._logService.debug('Switching back to normal keypad.');\n this._coreService.decPrivateModes.applicationKeypad = false;\n this._onRequestSyncScrollBar.fire();\n break;\n case 9: // X10 Mouse\n case 1000: // vt200 mouse\n case 1002: // button event mouse\n case 1003: // any event mouse\n this._mouseStateService.activeProtocol = 'NONE';\n break;\n case 1004: // send focusin/focusout events\n this._coreService.decPrivateModes.sendFocus = false;\n break;\n case 1005: // utf8 ext mode mouse - removed in #2507\n this._logService.debug('DECRST 1005 not supported (see #2507)');\n break;\n case 1006: // sgr ext mode mouse\n this._mouseStateService.activeEncoding = 'DEFAULT';\n break;\n case 1015: // urxvt ext mode mouse - removed in #2507\n this._logService.debug('DECRST 1015 not supported (see #2507)');\n break;\n case 1016: // sgr pixels mode mouse\n this._mouseStateService.activeEncoding = 'DEFAULT';\n break;\n case 25: // hide cursor\n this._coreService.isCursorHidden = true;\n break;\n case 1048: // alt screen cursor\n this.restoreCursor();\n break;\n case 1049: // alt screen buffer cursor\n // FALL-THROUGH\n case 47: // normal screen buffer\n case 1047: // normal screen buffer - clearing it first\n // Swap kitty keyboard flags: save alt, restore main\n if (this._optionsService.rawOptions.vtExtensions?.kittyKeyboard) {\n const state = this._coreService.kittyKeyboard;\n state.altFlags = state.flags;\n state.flags = state.mainFlags;\n }\n // Ensure the selection manager has the correct buffer\n this._bufferService.buffers.activateNormalBuffer();\n if (params.params[i] === 1049) {\n this.restoreCursor();\n }\n this._coreService.isCursorInitialized = true;\n this._onRequestRefreshRows.fire(undefined);\n this._onRequestSyncScrollBar.fire();\n break;\n case 2004: // bracketed paste mode (https://cirw.in/blog/bracketed-paste)\n this._coreService.decPrivateModes.bracketedPasteMode = false;\n break;\n case 2026: // synchronized output (https://github.com/contour-terminal/vt-extensions/blob/master/synchronized-output.md)\n this._coreService.decPrivateModes.synchronizedOutput = false;\n this._onRequestRefreshRows.fire(undefined);\n break;\n case 2031: // color scheme updates (https://contour-terminal.org/vt-extensions/color-palette-update-notifications/)\n if (this._optionsService.rawOptions.vtExtensions?.colorSchemeQuery ?? true) {\n this._coreService.decPrivateModes.colorSchemeUpdates = false;\n }\n break;\n case 9001: // win32-input-mode\n if (this._optionsService.rawOptions.vtExtensions?.win32InputMode) {\n this._coreService.decPrivateModes.win32InputMode = false;\n }\n break;\n }\n }\n return true;\n }\n\n /**\n * CSI Ps $ p Request ANSI Mode (DECRQM).\n *\n * Reports CSI Ps; Pm $ y (DECRPM), where Ps is the mode number as in SM/RM,\n * and Pm is the mode value:\n * 0 - not recognized\n * 1 - set\n * 2 - reset\n * 3 - permanently set\n * 4 - permanently reset\n *\n * @vt: #Y CSI DECRQM \"Request Mode\" \"CSI Ps $p\" \"Request mode state.\"\n * Returns a report as `CSI Ps; Pm $ y` (DECRPM), where `Ps` is the mode number as in SM/RM\n * or DECSET/DECRST, and `Pm` is the mode value:\n * - 0: not recognized\n * - 1: set\n * - 2: reset\n * - 3: permanently set\n * - 4: permanently reset\n *\n * For modes not understood xterm.js always returns `notRecognized`. In general this means,\n * that a certain operation mode is not implemented and cannot be used.\n *\n * Modes changing the active terminal buffer (47, 1047, 1049) are not subqueried\n * and only report, whether the alternate buffer is set.\n *\n * Mouse encodings and mouse protocols are handled mutual exclusive,\n * thus only one of each of those can be set at a given time.\n *\n * There is a chance, that some mode reports are not fully in line with xterm.js' behavior,\n * e.g. if the default implementation already exposes a certain behavior. If you find\n * discrepancies in the mode reports, please file a bug.\n */\n public requestMode(params: IParams, ansi: boolean): boolean {\n // return value as in DECRPM\n const enum V {\n NOT_RECOGNIZED = 0,\n SET = 1,\n RESET = 2,\n PERMANENTLY_SET = 3,\n PERMANENTLY_RESET = 4\n }\n\n // access helpers\n const dm = this._coreService.decPrivateModes;\n const { activeProtocol: mouseProtocol, activeEncoding: mouseEncoding } = this._mouseStateService;\n const cs = this._coreService;\n const { buffers, cols } = this._bufferService;\n const { active, alt } = buffers;\n const opts = this._optionsService.rawOptions;\n\n const f = (m: number, v: V): boolean => {\n cs.triggerDataEvent(`${C0.ESC}[${ansi ? '' : '?'}${m};${v}$y`);\n return true;\n };\n const b2v = (value: boolean): V => value ? V.SET : V.RESET;\n\n const p = params.params[0];\n\n if (ansi) {\n if (p === 2) return f(p, V.PERMANENTLY_RESET);\n if (p === 4) return f(p, b2v(cs.modes.insertMode));\n if (p === 12) return f(p, V.PERMANENTLY_SET);\n if (p === 20) return f(p, b2v(opts.convertEol));\n return f(p, V.NOT_RECOGNIZED);\n }\n\n if (p === 1) return f(p, b2v(dm.applicationCursorKeys));\n if (p === 3) return f(p, opts.windowOptions.setWinLines ? (cols === 80 ? V.RESET : cols === 132 ? V.SET : V.NOT_RECOGNIZED) : V.NOT_RECOGNIZED);\n if (p === 6) return f(p, b2v(dm.origin));\n if (p === 7) return f(p, b2v(dm.wraparound));\n if (p === 8) return f(p, V.PERMANENTLY_SET);\n if (p === 9) return f(p, b2v(mouseProtocol === 'X10'));\n if (p === 12) return f(p, b2v(opts.cursorBlink));\n if (p === 25) return f(p, b2v(!cs.isCursorHidden));\n if (p === 45) return f(p, b2v(dm.reverseWraparound));\n if (p === 66) return f(p, b2v(dm.applicationKeypad));\n if (p === 67) return f(p, V.PERMANENTLY_RESET);\n if (p === 1000) return f(p, b2v(mouseProtocol === 'VT200'));\n if (p === 1002) return f(p, b2v(mouseProtocol === 'DRAG'));\n if (p === 1003) return f(p, b2v(mouseProtocol === 'ANY'));\n if (p === 1004) return f(p, b2v(dm.sendFocus));\n if (p === 1005) return f(p, V.PERMANENTLY_RESET);\n if (p === 1006) return f(p, b2v(mouseEncoding === 'SGR'));\n if (p === 1015) return f(p, V.PERMANENTLY_RESET);\n if (p === 1016) return f(p, b2v(mouseEncoding === 'SGR_PIXELS'));\n if (p === 1048) return f(p, V.SET); // xterm always returns SET here\n if (p === 47 || p === 1047 || p === 1049) return f(p, b2v(active === alt));\n if (p === 2004) return f(p, b2v(dm.bracketedPasteMode));\n if (p === 2026) return f(p, b2v(dm.synchronizedOutput));\n if (p === 9001) return this._optionsService.rawOptions.vtExtensions?.win32InputMode ? f(p, b2v(dm.win32InputMode)) : f(p, V.NOT_RECOGNIZED);\n return f(p, V.NOT_RECOGNIZED);\n }\n\n /**\n * Helper to write color information packed with color mode.\n */\n private _updateAttrColor(color: number, mode: number, c1: number, c2: number, c3: number): number {\n if (mode === 2) {\n color |= Attributes.CM_RGB;\n color &= ~Attributes.RGB_MASK;\n color |= AttributeData.fromColorRGB([c1, c2, c3]);\n } else if (mode === 5) {\n color &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);\n color |= Attributes.CM_P256 | (c1 & 0xff);\n }\n return color;\n }\n\n /**\n * Helper to extract and apply color params/subparams.\n * Returns advance for params index.\n */\n private _extractColor(params: IParams, pos: number, attr: IAttributeData): number {\n // normalize params\n // meaning: [target, CM, ign, val, val, val]\n // RGB : [ 38/48, 2, ign, r, g, b]\n // P256 : [ 38/48, 5, ign, v, ign, ign]\n const accu = [0, 0, -1, 0, 0, 0];\n\n // alignment placeholder for non color space sequences\n let cSpace = 0;\n\n // return advance we took in params\n let advance = 0;\n\n do {\n accu[advance + cSpace] = params.params[pos + advance];\n if (params.hasSubParams(pos + advance)) {\n const subparams = params.getSubParams(pos + advance)!;\n let i = 0;\n do {\n if (accu[1] === 5) {\n cSpace = 1;\n }\n accu[advance + i + 1 + cSpace] = subparams[i];\n } while (++i < subparams.length && i + advance + 1 + cSpace < accu.length);\n break;\n }\n // exit early if can decide color mode with semicolons\n if ((accu[1] === 5 && advance + cSpace >= 2)\n || (accu[1] === 2 && advance + cSpace >= 5)) {\n break;\n }\n // offset colorSpace slot for semicolon mode\n if (accu[1]) {\n cSpace = 1;\n }\n } while (++advance + pos < params.length && advance + cSpace < accu.length);\n\n // set default values to 0\n for (let i = 2; i < accu.length; ++i) {\n if (accu[i] === -1) {\n accu[i] = 0;\n }\n }\n\n // apply colors\n switch (accu[0]) {\n case 38:\n attr.fg = this._updateAttrColor(attr.fg, accu[1], accu[3], accu[4], accu[5]);\n break;\n case 48:\n attr.bg = this._updateAttrColor(attr.bg, accu[1], accu[3], accu[4], accu[5]);\n break;\n case 58:\n attr.extended = attr.extended.clone();\n attr.extended.underlineColor = this._updateAttrColor(attr.extended.underlineColor, accu[1], accu[3], accu[4], accu[5]);\n }\n\n return advance;\n }\n\n /**\n * SGR 4 subparams:\n * 4:0 - equal to SGR 24 (turn off all underline)\n * 4:1 - equal to SGR 4 (single underline)\n * 4:2 - equal to SGR 21 (double underline)\n * 4:3 - curly underline\n * 4:4 - dotted underline\n * 4:5 - dashed underline\n */\n private _processUnderline(style: number, attr: IAttributeData): void {\n // treat extended attrs as immutable, thus always clone from old one\n // this is needed since the buffer only holds references to it\n attr.extended = attr.extended.clone();\n\n // default to 1 == single underline\n if (!~style || style > 5) {\n style = 1;\n }\n attr.extended.underlineStyle = style;\n attr.fg |= FgFlags.UNDERLINE;\n\n // 0 deactivates underline\n if (style === 0) {\n attr.fg &= ~FgFlags.UNDERLINE;\n }\n\n // update HAS_EXTENDED in BG\n attr.updateExtended();\n }\n\n private _processSGR0(attr: IAttributeData): void {\n attr.fg = DEFAULT_ATTR_DATA.fg;\n attr.bg = DEFAULT_ATTR_DATA.bg;\n attr.extended = attr.extended.clone();\n // Reset underline style and color. Note that we don't want to reset other\n // fields such as the url id.\n attr.extended.underlineStyle = UnderlineStyle.NONE;\n attr.extended.underlineColor &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);\n attr.updateExtended();\n }\n\n /**\n * CSI Pm m Character Attributes (SGR).\n *\n * @vt: #P[See below for supported attributes.] CSI SGR \"Select Graphic Rendition\" \"CSI Pm m\" \"Set/Reset various text attributes.\"\n * SGR selects one or more character attributes at the same time. Multiple params (up to 32)\n * are applied in order from left to right. The changed attributes are applied to all new\n * characters received. If you move characters in the viewport by scrolling or any other means,\n * then the attributes move with the characters.\n *\n * Supported param values by SGR:\n *\n * | Param | Meaning | Support |\n * | --------- | -------------------------------------------------------- | ------- |\n * | 0 | Normal (default). Resets any other preceding SGR. | #Y |\n * | 1 | Bold. (also see `options.drawBoldTextInBrightColors`) | #Y |\n * | 2 | Faint, decreased intensity. | #Y |\n * | 3 | Italic. | #Y |\n * | 4 | Underlined (see below for style support). | #Y |\n * | 5 | Slowly blinking. | #N |\n * | 6 | Rapidly blinking. | #N |\n * | 7 | Inverse. Flips foreground and background color. | #Y |\n * | 8 | Invisible (hidden). | #Y |\n * | 9 | Crossed-out characters (strikethrough). | #Y |\n * | 21 | Doubly underlined. | #Y |\n * | 22 | Normal (neither bold nor faint). | #Y |\n * | 23 | No italic. | #Y |\n * | 24 | Not underlined. | #Y |\n * | 25 | Steady (not blinking). | #Y |\n * | 27 | Positive (not inverse). | #Y |\n * | 28 | Visible (not hidden). | #Y |\n * | 29 | Not Crossed-out (strikethrough). | #Y |\n * | 30 | Foreground color: Black. | #Y |\n * | 31 | Foreground color: Red. | #Y |\n * | 32 | Foreground color: Green. | #Y |\n * | 33 | Foreground color: Yellow. | #Y |\n * | 34 | Foreground color: Blue. | #Y |\n * | 35 | Foreground color: Magenta. | #Y |\n * | 36 | Foreground color: Cyan. | #Y |\n * | 37 | Foreground color: White. | #Y |\n * | 38 | Foreground color: Extended color. | #P[Support for RGB and indexed colors, see below.] |\n * | 39 | Foreground color: Default (original). | #Y |\n * | 40 | Background color: Black. | #Y |\n * | 41 | Background color: Red. | #Y |\n * | 42 | Background color: Green. | #Y |\n * | 43 | Background color: Yellow. | #Y |\n * | 44 | Background color: Blue. | #Y |\n * | 45 | Background color: Magenta. | #Y |\n * | 46 | Background color: Cyan. | #Y |\n * | 47 | Background color: White. | #Y |\n * | 48 | Background color: Extended color. | #P[Support for RGB and indexed colors, see below.] |\n * | 49 | Background color: Default (original). | #Y |\n * | 53 | Overlined. | #Y |\n * | 55 | Not Overlined. | #Y |\n * | 58 | Underline color: Extended color. | #P[Support for RGB and indexed colors, see below.] |\n * | 221 | Not bold (kitty extension). | #Y |\n * | 222 | Not faint (kitty extension). | #Y |\n * | 90 - 97 | Bright foreground color (analogous to 30 - 37). | #Y |\n * | 100 - 107 | Bright background color (analogous to 40 - 47). | #Y |\n *\n * Underline supports subparams to denote the style in the form `4 : x`:\n *\n * | x | Meaning | Support |\n * | ------ | ------------------------------------------------------------- | ------- |\n * | 0 | No underline. Same as `SGR 24 m`. | #Y |\n * | 1 | Single underline. Same as `SGR 4 m`. | #Y |\n * | 2 | Double underline. | #Y |\n * | 3 | Curly underline. | #Y |\n * | 4 | Dotted underline. | #Y |\n * | 5 | Dashed underline. | #Y |\n * | other | Single underline. Same as `SGR 4 m`. | #Y |\n *\n * Extended colors are supported for foreground (Ps=38), background (Ps=48) and underline (Ps=58)\n * as follows:\n *\n * | Ps + 1 | Meaning | Support |\n * | ------ | ------------------------------------------------------------- | ------- |\n * | 0 | Implementation defined. | #N |\n * | 1 | Transparent. | #N |\n * | 2 | RGB color as `Ps ; 2 ; R ; G ; B` or `Ps : 2 : : R : G : B`. | #Y |\n * | 3 | CMY color. | #N |\n * | 4 | CMYK color. | #N |\n * | 5 | Indexed (256 colors) as `Ps ; 5 ; INDEX` or `Ps : 5 : INDEX`. | #Y |\n */\n public charAttributes(params: IParams): boolean {\n // Optimize a single SGR0.\n if (params.length === 1 && params.params[0] === 0) {\n this._processSGR0(this._curAttrData);\n return true;\n }\n\n const l = params.length;\n let p;\n const attr = this._curAttrData;\n\n for (let i = 0; i < l; i++) {\n p = params.params[i];\n if (p >= 30 && p <= 37) {\n // fg color 8\n attr.fg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);\n attr.fg |= Attributes.CM_P16 | (p - 30);\n } else if (p >= 40 && p <= 47) {\n // bg color 8\n attr.bg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);\n attr.bg |= Attributes.CM_P16 | (p - 40);\n } else if (p >= 90 && p <= 97) {\n // fg color 16\n attr.fg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);\n attr.fg |= Attributes.CM_P16 | (p - 90) | 8;\n } else if (p >= 100 && p <= 107) {\n // bg color 16\n attr.bg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);\n attr.bg |= Attributes.CM_P16 | (p - 100) | 8;\n } else if (p === 0) {\n // default\n this._processSGR0(attr);\n } else if (p === 1) {\n // bold text\n attr.fg |= FgFlags.BOLD;\n } else if (p === 3) {\n // italic text\n attr.bg |= BgFlags.ITALIC;\n } else if (p === 4) {\n // underlined text\n attr.fg |= FgFlags.UNDERLINE;\n this._processUnderline(params.hasSubParams(i) ? params.getSubParams(i)![0] : UnderlineStyle.SINGLE, attr);\n } else if (p === 5) {\n // blink\n attr.fg |= FgFlags.BLINK;\n } else if (p === 7) {\n // inverse and positive\n // test with: echo -e '\\e[31m\\e[42mhello\\e[7mworld\\e[27mhi\\e[m'\n attr.fg |= FgFlags.INVERSE;\n } else if (p === 8) {\n // invisible\n attr.fg |= FgFlags.INVISIBLE;\n } else if (p === 9) {\n // strikethrough\n attr.fg |= FgFlags.STRIKETHROUGH;\n } else if (p === 2) {\n // dimmed text\n attr.bg |= BgFlags.DIM;\n } else if (p === 21) {\n // double underline\n this._processUnderline(UnderlineStyle.DOUBLE, attr);\n } else if (p === 22) {\n // not bold nor faint\n attr.fg &= ~FgFlags.BOLD;\n attr.bg &= ~BgFlags.DIM;\n } else if (p === 23) {\n // not italic\n attr.bg &= ~BgFlags.ITALIC;\n } else if (p === 24) {\n // not underlined\n attr.fg &= ~FgFlags.UNDERLINE;\n this._processUnderline(UnderlineStyle.NONE, attr);\n } else if (p === 25) {\n // not blink\n attr.fg &= ~FgFlags.BLINK;\n } else if (p === 27) {\n // not inverse\n attr.fg &= ~FgFlags.INVERSE;\n } else if (p === 28) {\n // not invisible\n attr.fg &= ~FgFlags.INVISIBLE;\n } else if (p === 29) {\n // not strikethrough\n attr.fg &= ~FgFlags.STRIKETHROUGH;\n } else if (p === 39) {\n // reset fg\n attr.fg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);\n attr.fg |= DEFAULT_ATTR_DATA.fg & Attributes.RGB_MASK;\n } else if (p === 49) {\n // reset bg\n attr.bg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);\n attr.bg |= DEFAULT_ATTR_DATA.bg & Attributes.RGB_MASK;\n } else if (p === 38 || p === 48 || p === 58) {\n // fg color 256 and RGB\n i += this._extractColor(params, i, attr);\n } else if (p === 53) {\n // overline\n attr.bg |= BgFlags.OVERLINE;\n } else if (p === 55) {\n // not overline\n attr.bg &= ~BgFlags.OVERLINE;\n } else if (p === 221 && (this._optionsService.rawOptions.vtExtensions?.kittySgrBoldFaintControl ?? true)) {\n // not bold (kitty extension)\n attr.fg &= ~FgFlags.BOLD;\n } else if (p === 222 && (this._optionsService.rawOptions.vtExtensions?.kittySgrBoldFaintControl ?? true)) {\n // not faint (kitty extension)\n attr.bg &= ~BgFlags.DIM;\n } else if (p === 59) {\n attr.extended = attr.extended.clone();\n attr.extended.underlineColor = -1;\n attr.updateExtended();\n } else {\n this._logService.debug('Unknown SGR attribute: %d.', p);\n }\n }\n return true;\n }\n\n /**\n * CSI Ps n Device Status Report (DSR).\n * Ps = 5 -> Status Report. Result (``OK'') is\n * CSI 0 n\n * Ps = 6 -> Report Cursor Position (CPR) [row;column].\n * Result is\n * CSI r ; c R\n * CSI ? Ps n\n * Device Status Report (DSR, DEC-specific).\n * Ps = 6 -> Report Cursor Position (CPR) [row;column] as CSI\n * ? r ; c R (assumes page is zero).\n * Ps = 1 5 -> Report Printer status as CSI ? 1 0 n (ready).\n * or CSI ? 1 1 n (not ready).\n * Ps = 2 5 -> Report UDK status as CSI ? 2 0 n (unlocked)\n * or CSI ? 2 1 n (locked).\n * Ps = 2 6 -> Report Keyboard status as\n * CSI ? 2 7 ; 1 ; 0 ; 0 n (North American).\n * The last two parameters apply to VT400 & up, and denote key-\n * board ready and LK01 respectively.\n * Ps = 5 3 -> Report Locator status as\n * CSI ? 5 3 n Locator available, if compiled-in, or\n * CSI ? 5 0 n No Locator, if not.\n *\n * @vt: #Y CSI DSR \"Device Status Report\" \"CSI Ps n\" \"Request cursor position (CPR) with `Ps` = 6.\"\n */\n public deviceStatus(params: IParams): boolean {\n switch (params.params[0]) {\n case 5:\n // status report\n this._coreService.triggerDataEvent(`${C0.ESC}[0n`);\n break;\n case 6:\n // cursor position\n const y = this._activeBuffer.y + 1;\n const x = this._activeBuffer.x + 1;\n this._coreService.triggerDataEvent(`${C0.ESC}[${y};${x}R`);\n break;\n }\n return true;\n }\n\n // @vt: #P[Only CPR is supported.] CSI DECDSR \"DEC Device Status Report\" \"CSI ? Ps n\" \"Only CPR is supported (same as DSR).\"\n public deviceStatusPrivate(params: IParams): boolean {\n // modern xterm doesnt seem to\n // respond to any of these except ?6, 6, and 5\n switch (params.params[0]) {\n case 6:\n // cursor position\n const y = this._activeBuffer.y + 1;\n const x = this._activeBuffer.x + 1;\n this._coreService.triggerDataEvent(`${C0.ESC}[?${y};${x}R`);\n break;\n case 15:\n // no printer\n // this.handler(C0.ESC + '[?11n');\n break;\n case 25:\n // dont support user defined keys\n // this.handler(C0.ESC + '[?21n');\n break;\n case 26:\n // north american keyboard\n // this.handler(C0.ESC + '[?27;1;0;0n');\n break;\n case 53:\n // no dec locator/mouse\n // this.handler(C0.ESC + '[?50n');\n break;\n case 996:\n // color scheme query (https://contour-terminal.org/vt-extensions/color-palette-update-notifications/)\n if (this._optionsService.rawOptions.vtExtensions?.colorSchemeQuery ?? true) {\n this._onRequestColorSchemeQuery.fire();\n }\n break;\n }\n return true;\n }\n\n /**\n * CSI ! p Soft terminal reset (DECSTR).\n * http://vt100.net/docs/vt220-rm/table4-10.html\n *\n * @vt: #Y CSI DECSTR \"Soft Terminal Reset\" \"CSI ! p\" \"Reset several terminal attributes to initial state.\"\n * There are two terminal reset sequences - RIS and DECSTR. While RIS performs almost a full\n * terminal bootstrap, DECSTR only resets certain attributes. For most needs DECSTR should be\n * sufficient.\n *\n * The following terminal attributes are reset to default values:\n * - IRM is reset (dafault = false)\n * - scroll margins are reset (default = viewport size)\n * - erase attributes are reset to default\n * - charsets are reset\n * - DECSC data is reset to initial values\n * - DECOM is reset to absolute mode\n *\n *\n * FIXME: there are several more attributes missing (see VT520 manual)\n */\n public softReset(params: IParams): boolean {\n this._coreService.isCursorHidden = false;\n this._onRequestSyncScrollBar.fire();\n this._activeBuffer.scrollTop = 0;\n this._activeBuffer.scrollBottom = this._bufferService.rows - 1;\n this._curAttrData = DEFAULT_ATTR_DATA.clone();\n this._coreService.reset();\n this._charsetService.reset();\n\n // reset DECSC data\n this._activeBuffer.savedX = 0;\n this._activeBuffer.savedY = this._activeBuffer.ybase;\n this._activeBuffer.savedCurAttrData.fg = this._curAttrData.fg;\n this._activeBuffer.savedCurAttrData.bg = this._curAttrData.bg;\n this._activeBuffer.savedCharset = this._charsetService.charset;\n\n // reset DECOM\n this._coreService.decPrivateModes.origin = false;\n return true;\n }\n\n /**\n * CSI Ps SP q Set cursor style (DECSCUSR, VT520).\n * Ps = 0 -> reset to option.\n * Ps = 1 -> blinking block (default).\n * Ps = 2 -> steady block.\n * Ps = 3 -> blinking underline.\n * Ps = 4 -> steady underline.\n * Ps = 5 -> blinking bar (xterm).\n * Ps = 6 -> steady bar (xterm).\n *\n * @vt: #Y CSI DECSCUSR \"Set Cursor Style\" \"CSI Ps SP q\" \"Set cursor style.\"\n * Supported cursor styles:\n * - 0: reset to option\n * - empty, 1: blinking block\n * - 2: steady block\n * - 3: blinking underline\n * - 4: steady underline\n * - 5: blinking bar\n * - 6: steady bar\n */\n public setCursorStyle(params: IParams): boolean {\n const param = params.length === 0 ? 1 : params.params[0];\n if (param === 0) {\n this._coreService.decPrivateModes.cursorStyle = undefined;\n this._coreService.decPrivateModes.cursorBlink = undefined;\n } else {\n switch (param) {\n case 1:\n case 2:\n this._coreService.decPrivateModes.cursorStyle = 'block';\n break;\n case 3:\n case 4:\n this._coreService.decPrivateModes.cursorStyle = 'underline';\n break;\n case 5:\n case 6:\n this._coreService.decPrivateModes.cursorStyle = 'bar';\n break;\n }\n const isBlinking = param % 2 === 1;\n this._coreService.decPrivateModes.cursorBlink = isBlinking;\n }\n return true;\n }\n\n /**\n * CSI Ps ; Ps r\n * Set Scrolling Region [top;bottom] (default = full size of win-\n * dow) (DECSTBM).\n *\n * @vt: #Y CSI DECSTBM \"Set Top and Bottom Margin\" \"CSI Ps ; Ps r\" \"Set top and bottom margins of the viewport [top;bottom] (default = viewport size).\"\n */\n public setScrollRegion(params: IParams): boolean {\n const top = params.params[0] || 1;\n let bottom: number;\n\n if (params.length < 2 || (bottom = params.params[1]) > this._bufferService.rows || bottom === 0) {\n bottom = this._bufferService.rows;\n }\n\n if (bottom > top) {\n this._activeBuffer.scrollTop = top - 1;\n this._activeBuffer.scrollBottom = bottom - 1;\n this._setCursor(0, 0);\n }\n return true;\n }\n\n /**\n * CSI Ps ; Ps ; Ps t - Various window manipulations and reports (xterm)\n *\n * Note: Only those listed below are supported. All others are left to integrators and\n * need special treatment based on the embedding environment.\n *\n * Ps = 1 4 supported\n * Report xterm text area size in pixels.\n * Result is CSI 4 ; height ; width t\n * Ps = 14 ; 2 not implemented\n * Ps = 16 supported\n * Report xterm character cell size in pixels.\n * Result is CSI 6 ; height ; width t\n * Ps = 18 supported\n * Report the size of the text area in characters.\n * Result is CSI 8 ; height ; width t\n * Ps = 20 supported\n * Report xterm window's icon label.\n * Result is OSC L label ST\n * Ps = 21 supported\n * Report xterm window's title.\n * Result is OSC l label ST\n * Ps = 22 ; 0 -> Save xterm icon and window title on stack. supported\n * Ps = 22 ; 1 -> Save xterm icon title on stack. supported\n * Ps = 22 ; 2 -> Save xterm window title on stack. supported\n * Ps = 23 ; 0 -> Restore xterm icon and window title from stack. supported\n * Ps = 23 ; 1 -> Restore xterm icon title from stack. supported\n * Ps = 23 ; 2 -> Restore xterm window title from stack. supported\n * Ps >= 24 not implemented\n */\n public windowOptions(params: IParams): boolean {\n if (!paramToWindowOption(params.params[0], this._optionsService.rawOptions.windowOptions)) {\n return true;\n }\n const second = (params.length > 1) ? params.params[1] : 0;\n switch (params.params[0]) {\n case 14: // GetWinSizePixels, returns CSI 4 ; height ; width t\n if (second !== 2) {\n this._onRequestWindowsOptionsReport.fire(WindowsOptionsReportType.GET_WIN_SIZE_PIXELS);\n }\n break;\n case 16: // GetCellSizePixels, returns CSI 6 ; height ; width t\n this._onRequestWindowsOptionsReport.fire(WindowsOptionsReportType.GET_CELL_SIZE_PIXELS);\n break;\n case 18: // GetWinSizeChars, returns CSI 8 ; height ; width t\n if (this._bufferService) {\n this._coreService.triggerDataEvent(`${C0.ESC}[8;${this._bufferService.rows};${this._bufferService.cols}t`);\n }\n break;\n case 22: // PushTitle\n if (second === 0 || second === 2) {\n this._windowTitleStack.push(this._windowTitle);\n if (this._windowTitleStack.length > Constants.STACK_LIMIT) {\n this._windowTitleStack.shift();\n }\n }\n if (second === 0 || second === 1) {\n this._iconNameStack.push(this._iconName);\n if (this._iconNameStack.length > Constants.STACK_LIMIT) {\n this._iconNameStack.shift();\n }\n }\n break;\n case 23: // PopTitle\n if (second === 0 || second === 2) {\n if (this._windowTitleStack.length) {\n this.setTitle(this._windowTitleStack.pop()!);\n }\n }\n if (second === 0 || second === 1) {\n if (this._iconNameStack.length) {\n this.setIconName(this._iconNameStack.pop()!);\n }\n }\n break;\n }\n return true;\n }\n\n\n /**\n * CSI s\n * ESC 7\n * Save cursor (ANSI.SYS).\n *\n * @vt: #P[TODO...] CSI SCOSC \"Save Cursor\" \"CSI s\" \"Save cursor position, charmap and text attributes.\"\n * @vt: #Y ESC SC \"Save Cursor\" \"ESC 7\" \"Save cursor position, charmap and text attributes.\"\n */\n public saveCursor(params?: IParams): boolean {\n this._activeBuffer.savedX = this._activeBuffer.x;\n this._activeBuffer.savedY = this._activeBuffer.ybase + this._activeBuffer.y;\n this._activeBuffer.savedCurAttrData.fg = this._curAttrData.fg;\n this._activeBuffer.savedCurAttrData.bg = this._curAttrData.bg;\n this._activeBuffer.savedCharset = this._charsetService.charset;\n this._activeBuffer.savedCharsets = this._charsetService.charsets.slice();\n this._activeBuffer.savedGlevel = this._charsetService.glevel;\n this._activeBuffer.savedOriginMode = this._coreService.decPrivateModes.origin;\n this._activeBuffer.savedWraparoundMode = this._coreService.decPrivateModes.wraparound;\n return true;\n }\n\n\n /**\n * CSI u\n * ESC 8\n * Restore cursor (ANSI.SYS).\n *\n * @vt: #P[TODO...] CSI SCORC \"Restore Cursor\" \"CSI u\" \"Restore cursor position, charmap and text attributes.\"\n * @vt: #Y ESC RC \"Restore Cursor\" \"ESC 8\" \"Restore cursor position, charmap and text attributes.\"\n */\n public restoreCursor(params?: IParams): boolean {\n this._activeBuffer.x = this._activeBuffer.savedX || 0;\n this._activeBuffer.y = Math.max(this._activeBuffer.savedY - this._activeBuffer.ybase, 0);\n this._curAttrData.fg = this._activeBuffer.savedCurAttrData.fg;\n this._curAttrData.bg = this._activeBuffer.savedCurAttrData.bg;\n for (let i = 0; i < this._activeBuffer.savedCharsets.length; i++) {\n this._charsetService.setgCharset(i, this._activeBuffer.savedCharsets[i]);\n }\n this._charsetService.setgLevel(this._activeBuffer.savedGlevel);\n this._coreService.decPrivateModes.origin = this._activeBuffer.savedOriginMode;\n this._coreService.decPrivateModes.wraparound = this._activeBuffer.savedWraparoundMode;\n this._restrictCursor();\n return true;\n }\n\n /**\n * OSC 2; ST (set window title)\n * Proxy to set window title.\n *\n * @vt: #P[Icon name is not exposed.] OSC 0 \"Set Windows Title and Icon Name\" \"OSC 0 ; Pt BEL\" \"Set window title and icon name.\"\n * Icon name is not supported. For Window Title see below.\n *\n * @vt: #Y OSC 2 \"Set Windows Title\" \"OSC 2 ; Pt BEL\" \"Set window title.\"\n * xterm.js does not manipulate the title directly, instead exposes changes via the event\n * `Terminal.onTitleChange`.\n */\n public setTitle(data: string): boolean {\n this._windowTitle = data;\n this._onTitleChange.fire(data);\n return true;\n }\n\n /**\n * OSC 1; ST\n * Note: Icon name is not exposed.\n */\n public setIconName(data: string): boolean {\n this._iconName = data;\n return true;\n }\n\n /**\n * OSC 4; ; ST (set ANSI color to )\n *\n * @vt: #Y OSC 4 \"Set ANSI color\" \"OSC 4 ; c ; spec BEL\" \"Change color number `c` to the color specified by `spec`.\"\n * `c` is the color index between 0 and 255. The color format of `spec` is derived from\n * `XParseColor` (see OSC 10 for supported formats). There may be multipe `c ; spec` pairs present\n * in the same instruction. If `spec` contains `?` the terminal returns a sequence with the\n * currently set color.\n */\n public setOrReportIndexedColor(data: string): boolean {\n const event: IColorEvent = [];\n const slots = data.split(';');\n while (slots.length > 1) {\n const idx = slots.shift() as string;\n const spec = slots.shift() as string;\n if (/^\\d+$/.exec(idx)) {\n const index = parseInt(idx, 10);\n if (isValidColorIndex(index)) {\n if (spec === '?') {\n event.push({ type: ColorRequestType.REPORT, index });\n } else {\n const color = parseColor(spec);\n if (color) {\n event.push({ type: ColorRequestType.SET, index, color });\n }\n }\n }\n }\n }\n if (event.length) {\n this._onColor.fire(event);\n }\n return true;\n }\n\n /**\n * OSC 8 ; ; ST - create hyperlink\n * OSC 8 ; ; ST - finish hyperlink\n *\n * Test case:\n *\n * ```sh\n * printf '\\e]8;;http://example.com\\e\\\\This is a link\\e]8;;\\e\\\\\\n'\n * ```\n *\n * @vt: #Y OSC 8 \"Create hyperlink\" \"OSC 8 ; params ; uri BEL\" \"Create a hyperlink to `uri` using `params`.\"\n * `uri` is a hyperlink starting with `http://`, `https://`, `ftp://`, `file://` or `mailto://`. `params` is an\n * optional list of key=value assignments, separated by the : character.\n * Example: `id=xyz123:foo=bar:baz=quux`.\n * Currently only the id key is defined. Cells that share the same ID and URI share hover\n * feedback. Use `OSC 8 ; ; BEL` to finish the current hyperlink.\n */\n public setHyperlink(data: string): boolean {\n // Arg parsing is special cases to support unencoded semi-colons in the URIs (#4944)\n const idx = data.indexOf(';');\n if (idx === -1) {\n // malformed sequence, just return as handled\n return true;\n }\n const id = data.slice(0, idx).trim();\n const uri = data.slice(idx + 1);\n if (uri) {\n return this._createHyperlink(id, uri);\n }\n if (id.trim()) {\n return false;\n }\n return this._finishHyperlink();\n }\n\n private _createHyperlink(params: string, uri: string): boolean {\n // It's legal to open a new hyperlink without explicitly finishing the previous one\n if (this._getCurrentLinkId()) {\n this._finishHyperlink();\n }\n const parsedParams = params.split(':');\n let id: string | undefined;\n const idParamIndex = parsedParams.findIndex(e => e.startsWith('id='));\n if (idParamIndex !== -1) {\n id = parsedParams[idParamIndex].slice(3) || undefined;\n }\n this._curAttrData.extended = this._curAttrData.extended.clone();\n this._curAttrData.extended.urlId = this._oscLinkService.registerLink({ id, uri });\n this._curAttrData.updateExtended();\n return true;\n }\n\n private _finishHyperlink(): boolean {\n this._curAttrData.extended = this._curAttrData.extended.clone();\n this._curAttrData.extended.urlId = 0;\n this._curAttrData.updateExtended();\n return true;\n }\n\n // special colors - OSC 10 | 11 | 12\n private _specialColors = [SpecialColorIndex.FOREGROUND, SpecialColorIndex.BACKGROUND, SpecialColorIndex.CURSOR];\n\n /**\n * Apply colors requests for special colors in OSC 10 | 11 | 12.\n * Since these commands are stacking from multiple parameters,\n * we handle them in a loop with an entry offset to `_specialColors`.\n */\n private _setOrReportSpecialColor(data: string, offset: number): boolean {\n const slots = data.split(';');\n for (let i = 0; i < slots.length; ++i, ++offset) {\n if (offset >= this._specialColors.length) break;\n if (slots[i] === '?') {\n this._onColor.fire([{ type: ColorRequestType.REPORT, index: this._specialColors[offset] }]);\n } else {\n const color = parseColor(slots[i]);\n if (color) {\n this._onColor.fire([{ type: ColorRequestType.SET, index: this._specialColors[offset], color }]);\n }\n }\n }\n return true;\n }\n\n /**\n * OSC 10 ; | ST - set or query default foreground color\n *\n * @vt: #Y OSC 10 \"Set or query default foreground color\" \"OSC 10 ; Pt BEL\" \"Set or query default foreground color.\"\n * To set the color, the following color specification formats are supported:\n * - `rgb://` for `, , ` in `h | hh | hhh | hhhh`, where\n * `h` is a single hexadecimal digit (case insignificant). The different widths scale\n * from 4 bit (`h`) to 16 bit (`hhhh`) and get converted to 8 bit (`hh`).\n * - `#RGB` - 4 bits per channel, expanded to `#R0G0B0`\n * - `#RRGGBB` - 8 bits per channel\n * - `#RRRGGGBBB` - 12 bits per channel, truncated to `#RRGGBB`\n * - `#RRRRGGGGBBBB` - 16 bits per channel, truncated to `#RRGGBB`\n *\n * **Note:** X11 named colors are currently unsupported.\n *\n * If `Pt` contains `?` instead of a color specification, the terminal\n * returns a sequence with the current default foreground color\n * (use that sequence to restore the color after changes).\n *\n * **Note:** Other than xterm, xterm.js does not support OSC 12 - 19.\n * Therefore stacking multiple `Pt` separated by `;` only works for the first two entries.\n */\n public setOrReportFgColor(data: string): boolean {\n return this._setOrReportSpecialColor(data, 0);\n }\n\n /**\n * OSC 11 ; | ST - set or query default background color\n *\n * @vt: #Y OSC 11 \"Set or query default background color\" \"OSC 11 ; Pt BEL\" \"Same as OSC 10, but for default background.\"\n */\n public setOrReportBgColor(data: string): boolean {\n return this._setOrReportSpecialColor(data, 1);\n }\n\n /**\n * OSC 12 ; | ST - set or query default cursor color\n *\n * @vt: #Y OSC 12 \"Set or query default cursor color\" \"OSC 12 ; Pt BEL\" \"Same as OSC 10, but for default cursor color.\"\n */\n public setOrReportCursorColor(data: string): boolean {\n return this._setOrReportSpecialColor(data, 2);\n }\n\n /**\n * OSC 104 ; ST - restore ANSI color \n *\n * @vt: #Y OSC 104 \"Reset ANSI color\" \"OSC 104 ; c BEL\" \"Reset color number `c` to themed color.\"\n * `c` is the color index between 0 and 255. This function restores the default color for `c` as\n * specified by the loaded theme. Any number of `c` parameters may be given.\n * If no parameters are given, the entire indexed color table will be reset.\n */\n public restoreIndexedColor(data: string): boolean {\n if (!data) {\n this._onColor.fire([{ type: ColorRequestType.RESTORE }]);\n return true;\n }\n const event: IColorEvent = [];\n const slots = data.split(';');\n for (let i = 0; i < slots.length; ++i) {\n if (/^\\d+$/.exec(slots[i])) {\n const index = parseInt(slots[i], 10);\n if (isValidColorIndex(index)) {\n event.push({ type: ColorRequestType.RESTORE, index });\n }\n }\n }\n if (event.length) {\n this._onColor.fire(event);\n }\n return true;\n }\n\n /**\n * OSC 110 ST - restore default foreground color\n *\n * @vt: #Y OSC 110 \"Restore default foreground color\" \"OSC 110 BEL\" \"Restore default foreground to themed color.\"\n */\n public restoreFgColor(data: string): boolean {\n this._onColor.fire([{ type: ColorRequestType.RESTORE, index: SpecialColorIndex.FOREGROUND }]);\n return true;\n }\n\n /**\n * OSC 111 ST - restore default background color\n *\n * @vt: #Y OSC 111 \"Restore default background color\" \"OSC 111 BEL\" \"Restore default background to themed color.\"\n */\n public restoreBgColor(data: string): boolean {\n this._onColor.fire([{ type: ColorRequestType.RESTORE, index: SpecialColorIndex.BACKGROUND }]);\n return true;\n }\n\n /**\n * OSC 112 ST - restore default cursor color\n *\n * @vt: #Y OSC 112 \"Restore default cursor color\" \"OSC 112 BEL\" \"Restore default cursor to themed color.\"\n */\n public restoreCursorColor(data: string): boolean {\n this._onColor.fire([{ type: ColorRequestType.RESTORE, index: SpecialColorIndex.CURSOR }]);\n return true;\n }\n\n /**\n * ESC E\n * C1.NEL\n * DEC mnemonic: NEL (https://vt100.net/docs/vt510-rm/NEL)\n * Moves cursor to first position on next line.\n *\n * @vt: #Y C1 NEL \"Next Line\" \"\\x85\" \"Move the cursor to the beginning of the next row.\"\n * @vt: #Y ESC NEL \"Next Line\" \"ESC E\" \"Move the cursor to the beginning of the next row.\"\n */\n public nextLine(): boolean {\n this._activeBuffer.x = 0;\n this.index();\n return true;\n }\n\n /**\n * ESC =\n * DEC mnemonic: DECKPAM (https://vt100.net/docs/vt510-rm/DECKPAM.html)\n * Enables the numeric keypad to send application sequences to the host.\n */\n public keypadApplicationMode(): boolean {\n this._logService.debug('Serial port requested application keypad.');\n this._coreService.decPrivateModes.applicationKeypad = true;\n this._onRequestSyncScrollBar.fire();\n return true;\n }\n\n /**\n * ESC >\n * DEC mnemonic: DECKPNM (https://vt100.net/docs/vt510-rm/DECKPNM.html)\n * Enables the keypad to send numeric characters to the host.\n */\n public keypadNumericMode(): boolean {\n this._logService.debug('Switching back to normal keypad.');\n this._coreService.decPrivateModes.applicationKeypad = false;\n this._onRequestSyncScrollBar.fire();\n return true;\n }\n\n /**\n * ESC % @\n * ESC % G\n * Select default character set. UTF-8 is not supported (string are unicode anyways)\n * therefore ESC % G does the same.\n */\n public selectDefaultCharset(): boolean {\n this._charsetService.setgLevel(0);\n this._charsetService.setgCharset(0, DEFAULT_CHARSET); // US (default)\n return true;\n }\n\n /**\n * ESC ( C\n * Designate G0 Character Set, VT100, ISO 2022.\n * ESC ) C\n * Designate G1 Character Set (ISO 2022, VT100).\n * ESC * C\n * Designate G2 Character Set (ISO 2022, VT220).\n * ESC + C\n * Designate G3 Character Set (ISO 2022, VT220).\n * ESC - C\n * Designate G1 Character Set (VT300).\n * ESC . C\n * Designate G2 Character Set (VT300).\n * ESC / C\n * Designate G3 Character Set (VT300). C = A -> ISO Latin-1 Supplemental. - Supported?\n */\n public selectCharset(collectAndFlag: string): boolean {\n if (collectAndFlag.length !== 2) {\n this.selectDefaultCharset();\n return true;\n }\n if (collectAndFlag[0] === '/') {\n return true; // TODO: Is this supported?\n }\n this._charsetService.setgCharset(GLEVEL[collectAndFlag[0]], CHARSETS[collectAndFlag[1]] ?? DEFAULT_CHARSET);\n return true;\n }\n\n /**\n * ESC D\n * C1.IND\n * DEC mnemonic: IND (https://vt100.net/docs/vt510-rm/IND.html)\n * Moves the cursor down one line in the same column.\n *\n * @vt: #Y C1 IND \"Index\" \"\\x84\" \"Move the cursor one line down scrolling if needed.\"\n * @vt: #Y ESC IND \"Index\" \"ESC D\" \"Move the cursor one line down scrolling if needed.\"\n */\n public index(): boolean {\n this._restrictCursor();\n this._activeBuffer.y++;\n if (this._activeBuffer.y === this._activeBuffer.scrollBottom + 1) {\n this._activeBuffer.y--;\n this._bufferService.scroll(this._eraseAttrData());\n } else if (this._activeBuffer.y >= this._bufferService.rows) {\n this._activeBuffer.y = this._bufferService.rows - 1;\n }\n this._restrictCursor();\n return true;\n }\n\n /**\n * ESC H\n * C1.HTS\n * DEC mnemonic: HTS (https://vt100.net/docs/vt510-rm/HTS.html)\n * Sets a horizontal tab stop at the column position indicated by\n * the value of the active column when the terminal receives an HTS.\n *\n * @vt: #Y C1 HTS \"Horizontal Tabulation Set\" \"\\x88\" \"Places a tab stop at the current cursor position.\"\n * @vt: #Y ESC HTS \"Horizontal Tabulation Set\" \"ESC H\" \"Places a tab stop at the current cursor position.\"\n */\n public tabSet(): boolean {\n this._activeBuffer.tabs[this._activeBuffer.x] = true;\n return true;\n }\n\n /**\n * ESC M\n * C1.RI\n * DEC mnemonic: HTS\n * Moves the cursor up one line in the same column. If the cursor is at the top margin,\n * the page scrolls down.\n *\n * @vt: #Y ESC IR \"Reverse Index\" \"ESC M\" \"Move the cursor one line up scrolling if needed.\"\n */\n public reverseIndex(): boolean {\n this._restrictCursor();\n if (this._activeBuffer.y === this._activeBuffer.scrollTop) {\n // possibly move the code below to term.reverseScroll();\n // test: echo -ne '\\e[1;1H\\e[44m\\eM\\e[0m'\n // blankLine(true) is xterm/linux behavior\n const scrollRegionHeight = this._activeBuffer.scrollBottom - this._activeBuffer.scrollTop;\n this._activeBuffer.lines.shiftElements(this._activeBuffer.ybase + this._activeBuffer.y, scrollRegionHeight, 1);\n this._activeBuffer.lines.set(this._activeBuffer.ybase + this._activeBuffer.y, this._activeBuffer.getBlankLine(this._eraseAttrData()));\n this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);\n } else {\n this._activeBuffer.y--;\n this._restrictCursor(); // quickfix to not run out of bounds\n }\n return true;\n }\n\n /**\n * ESC c\n * DEC mnemonic: RIS (https://vt100.net/docs/vt510-rm/RIS.html)\n * Reset to initial state.\n *\n * @vt: #Y ESC RIS \"Full Reset\" \"ESC c\" \"Reset to initial state.\"\n */\n public fullReset(): boolean {\n this._parser.reset();\n this._onRequestReset.fire();\n return true;\n }\n\n public reset(): void {\n this._curAttrData = DEFAULT_ATTR_DATA.clone();\n this._eraseAttrDataInternal = DEFAULT_ATTR_DATA.clone();\n }\n\n /**\n * back_color_erase feature for xterm.\n */\n private _eraseAttrData(): IAttributeData {\n this._eraseAttrDataInternal.bg &= ~(Attributes.CM_MASK | 0xFFFFFF);\n this._eraseAttrDataInternal.bg |= this._curAttrData.bg & ~0xFC000000;\n return this._eraseAttrDataInternal;\n }\n\n /**\n * ESC n\n * ESC o\n * ESC |\n * ESC }\n * ESC ~\n * DEC mnemonic: LS (https://vt100.net/docs/vt510-rm/LS.html)\n * When you use a locking shift, the character set remains in GL or GR until\n * you use another locking shift. (partly supported)\n */\n public setgLevel(level: number): boolean {\n this._charsetService.setgLevel(level);\n return true;\n }\n\n /**\n * ESC # 8\n * DEC mnemonic: DECALN (https://vt100.net/docs/vt510-rm/DECALN.html)\n * This control function fills the complete screen area with\n * a test pattern (E) used for adjusting screen alignment.\n *\n * @vt: #Y ESC DECALN \"Screen Alignment Pattern\" \"ESC # 8\" \"Fill viewport with a test pattern (E).\"\n */\n public screenAlignmentPattern(): boolean {\n // prepare cell data\n const cell = new CellData();\n cell.content = 1 << Content.WIDTH_SHIFT | 'E'.charCodeAt(0);\n cell.fg = this._curAttrData.fg;\n cell.bg = this._curAttrData.bg;\n\n\n this._setCursor(0, 0);\n for (let yOffset = 0; yOffset < this._bufferService.rows; ++yOffset) {\n const row = this._activeBuffer.ybase + this._activeBuffer.y + yOffset;\n const line = this._activeBuffer.lines.get(row);\n if (line) {\n line.fill(cell);\n line.isWrapped = false;\n }\n }\n this._dirtyRowTracker.markAllDirty();\n this._setCursor(0, 0);\n return true;\n }\n\n\n /**\n * DCS $ q Pt ST\n * DECRQSS (https://vt100.net/docs/vt510-rm/DECRQSS.html)\n * Request Status String (DECRQSS), VT420 and up.\n * Response: DECRPSS (https://vt100.net/docs/vt510-rm/DECRPSS.html)\n *\n * @vt: #P[Limited support, see below.] DCS DECRQSS \"Request Selection or Setting\" \"DCS $ q Pt ST\" \"Request several terminal settings.\"\n * Response is in the form `ESC P 1 $ r Pt ST` for valid requests, where `Pt` contains the\n * corresponding CSI string, `ESC P 0 ST` for invalid requests.\n *\n * Supported requests and responses:\n *\n * | Type | Request | Response (`Pt`) |\n * | -------------------------------- | ----------------- | ----------------------------------------------------- |\n * | Graphic Rendition (SGR) | `DCS $ q m ST` | always reporting `0m` (currently broken) |\n * | Top and Bottom Margins (DECSTBM) | `DCS $ q r ST` | `Ps ; Ps r` |\n * | Cursor Style (DECSCUSR) | `DCS $ q SP q ST` | `Ps SP q` |\n * | Protection Attribute (DECSCA) | `DCS $ q \" q ST` | `Ps \" q` (DECSCA 2 is reported as Ps = 0) |\n * | Conformance Level (DECSCL) | `DCS $ q \" p ST` | always reporting `61 ; 1 \" p` (DECSCL is unsupported) |\n *\n *\n * TODO:\n * - fix SGR report\n * - either check which conformance is better suited or remove the report completely\n * --> we are currently a mixture of all up to VT400 but dont follow anyone strictly\n */\n public requestStatusString(data: string, params: IParams): boolean {\n const f = (s: string): boolean => {\n this._coreService.triggerDataEvent(`${C0.ESC}${s}${C0.ESC}\\\\`);\n return true;\n };\n\n // access helpers\n const b = this._bufferService.buffer;\n const opts = this._optionsService.rawOptions;\n const STYLES: { [key: string]: number } = { 'block': 2, 'underline': 4, 'bar': 6 };\n\n if (data === '\"q') return f(`P1$r${this._curAttrData.isProtected() ? 1 : 0}\"q`);\n if (data === '\"p') return f(`P1$r61;1\"p`);\n if (data === 'r') return f(`P1$r${b.scrollTop + 1};${b.scrollBottom + 1}r`);\n // FIXME: report real SGR settings instead of 0m\n if (data === 'm') return f(`P1$r0m`);\n if (data === ' q') return f(`P1$r${STYLES[opts.cursorStyle] - (opts.cursorBlink ? 1 : 0)} q`);\n return f(`P0$r`);\n }\n\n public markRangeDirty(y1: number, y2: number): void {\n this._dirtyRowTracker.markRangeDirty(y1, y2);\n }\n\n // #region Kitty keyboard\n\n /**\n * CSI = flags ; mode u\n * Set Kitty keyboard protocol flags.\n * mode: 1=set, 2=set-only-specified, 3=reset-only-specified\n *\n * @vt: #Y CSI KKBDSET \"Kitty Keyboard Set\" \"CSI = Ps ; Pm u\" \"Set Kitty keyboard protocol flags.\"\n */\n public kittyKeyboardSet(params: IParams): boolean {\n if (!this._optionsService.rawOptions.vtExtensions?.kittyKeyboard) {\n return true;\n }\n const flags = params.params[0] || 0;\n const mode = params.length > 1 ? (params.params[1] || 1) : 1;\n const state = this._coreService.kittyKeyboard;\n\n switch (mode) {\n case 1: // Set all flags\n state.flags = flags;\n break;\n case 2: // Set only specified flags (OR)\n state.flags |= flags;\n break;\n case 3: // Reset only specified flags (AND NOT)\n state.flags &= ~flags;\n break;\n }\n return true;\n }\n\n /**\n * CSI ? u\n * Query Kitty keyboard protocol flags.\n * Terminal responds with CSI ? flags u\n *\n * @vt: #Y CSI KKBDQUERY \"Kitty Keyboard Query\" \"CSI ? u\" \"Query Kitty keyboard protocol flags.\"\n */\n public kittyKeyboardQuery(params: IParams): boolean {\n if (!this._optionsService.rawOptions.vtExtensions?.kittyKeyboard) {\n return true;\n }\n const flags = this._coreService.kittyKeyboard.flags;\n this._coreService.triggerDataEvent(`${C0.ESC}[?${flags}u`);\n return true;\n }\n\n /**\n * CSI > flags u\n * Push Kitty keyboard flags onto stack and set new flags.\n *\n * @vt: #Y CSI KKBDPUSH \"Kitty Keyboard Push\" \"CSI > Ps u\" \"Push keyboard flags to stack and set new flags.\"\n */\n public kittyKeyboardPush(params: IParams): boolean {\n if (!this._optionsService.rawOptions.vtExtensions?.kittyKeyboard) {\n return true;\n }\n const flags = params.params[0] || 0;\n const state = this._coreService.kittyKeyboard;\n const isAlt = this._bufferService.buffer === this._bufferService.buffers.alt;\n const stack = isAlt ? state.altStack : state.mainStack;\n\n // Evict oldest entry if stack is full (DoS protection, limit of 16)\n if (stack.length >= 16) {\n stack.shift();\n }\n\n // Push current flags onto stack and set new flags\n stack.push(state.flags);\n state.flags = flags;\n return true;\n }\n\n /**\n * CSI < count u\n * Pop Kitty keyboard flags from stack.\n *\n * @vt: #Y CSI KKBDPOP \"Kitty Keyboard Pop\" \"CSI < Ps u\" \"Pop keyboard flags from stack.\"\n */\n public kittyKeyboardPop(params: IParams): boolean {\n if (!this._optionsService.rawOptions.vtExtensions?.kittyKeyboard) {\n return true;\n }\n const count = Math.max(1, params.params[0] || 1);\n const state = this._coreService.kittyKeyboard;\n const isAlt = this._bufferService.buffer === this._bufferService.buffers.alt;\n const stack = isAlt ? state.altStack : state.mainStack;\n\n // Pop specified number of entries from stack\n for (let i = 0; i < count && stack.length > 0; i++) {\n state.flags = stack.pop()!;\n }\n // If stack is empty after popping, reset to 0\n if (stack.length === 0 && count > 0) {\n state.flags = 0;\n }\n return true;\n }\n\n // #endregion\n}\n\nexport interface IDirtyRowTracker {\n readonly start: number;\n readonly end: number;\n\n clearRange(): void;\n markDirty(y: number): void;\n markRangeDirty(y1: number, y2: number): void;\n markAllDirty(): void;\n}\n\nclass DirtyRowTracker implements IDirtyRowTracker {\n public start!: number;\n public end!: number;\n\n constructor(\n @IBufferService private readonly _bufferService: IBufferService\n ) {\n this.clearRange();\n }\n\n public clearRange(): void {\n this.start = this._bufferService.buffer.y;\n this.end = this._bufferService.buffer.y;\n }\n\n public markDirty(y: number): void {\n if (y < this.start) {\n this.start = y;\n } else if (y > this.end) {\n this.end = y;\n }\n }\n\n public markRangeDirty(y1: number, y2: number): void {\n if (y1 > y2) {\n $temp = y1;\n y1 = y2;\n y2 = $temp;\n }\n if (y1 < this.start) {\n this.start = y1;\n }\n if (y2 > this.end) {\n this.end = y2;\n }\n }\n\n public markAllDirty(): void {\n this.markRangeDirty(0, this._bufferService.rows - 1);\n }\n}\n\nexport function isValidColorIndex(value: number): value is ColorIndex {\n return 0 <= value && value < 256;\n}\n","/**\n * Copyright (c) 2024-2026 The xterm.js authors. All rights reserved.\n * @license MIT\n *\n * Minimal lifecycle utilities for xterm.js core.\n * Simplified from VS Code's lifecycle.ts - no tracking/leak detection.\n */\n\nexport interface IDisposable {\n dispose(): void;\n}\n\nexport function toDisposable(fn: () => void): IDisposable {\n return { dispose: fn };\n}\n\nexport function dispose(disposable: T): T;\nexport function dispose(disposable: T | undefined): T | undefined;\nexport function dispose(disposables: T[]): T[];\nexport function dispose(arg: T | T[] | undefined): T | T[] | undefined {\n if (!arg) {\n return arg;\n }\n if (Array.isArray(arg)) {\n for (const d of arg) {\n d.dispose();\n }\n return [];\n }\n arg.dispose();\n return arg;\n}\n\nexport function combinedDisposable(...disposables: IDisposable[]): IDisposable {\n return toDisposable(() => dispose(disposables));\n}\n\nexport class DisposableStore implements IDisposable {\n private readonly _disposables = new Set();\n private _isDisposed = false;\n\n public get isDisposed(): boolean {\n return this._isDisposed;\n }\n\n public add(o: T): T {\n if (this._isDisposed) {\n o.dispose();\n } else {\n this._disposables.add(o);\n }\n return o;\n }\n\n public dispose(): void {\n if (this._isDisposed) {\n return;\n }\n this._isDisposed = true;\n for (const d of this._disposables) {\n d.dispose();\n }\n this._disposables.clear();\n }\n\n public clear(): void {\n for (const d of this._disposables) {\n d.dispose();\n }\n this._disposables.clear();\n }\n}\n\nexport abstract class Disposable implements IDisposable {\n public static readonly None: IDisposable = Object.freeze({ dispose() { } });\n\n protected readonly _store = new DisposableStore();\n\n public dispose(): void {\n this._store.dispose();\n }\n\n protected _register(o: T): T {\n return this._store.add(o);\n }\n}\n\nexport class MutableDisposable implements IDisposable {\n private _value: T | undefined;\n private _isDisposed = false;\n\n public get value(): T | undefined {\n return this._isDisposed ? undefined : this._value;\n }\n\n public set value(value: T | undefined) {\n if (this._isDisposed || value === this._value) {\n return;\n }\n this._value?.dispose();\n this._value = value;\n }\n\n public clear(): void {\n this.value = undefined;\n }\n\n public dispose(): void {\n this._isDisposed = true;\n this._value?.dispose();\n this._value = undefined;\n }\n}\n","/**\n * Copyright (c) 2016 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\ninterface INavigator {\n userAgent: string;\n language: string;\n platform: string;\n}\n\n// We're declaring a navigator global here as we expect it in all runtimes (node and browser), but\n// we want this module to live in common.\ndeclare const navigator: INavigator;\ndeclare const process: unknown;\n\n// navigator.userAgent is also checked here because bundling with the process module can cause\n// issues otherwise. Note that navigator exists in Node.js 21+ but the userAgent is\n// \"Node.js/\".\nexport const isNode = (typeof process !== 'undefined' && 'title' in (process as any) && (typeof navigator === 'undefined' || navigator.userAgent.startsWith('Node.js/'))) ? true : false;\nconst userAgent = (isNode) ? 'node' : navigator.userAgent;\nconst platform = (isNode) ? 'node' : navigator.platform;\n\nexport const isFirefox = userAgent.includes('Firefox');\nexport const isChrome = userAgent.includes('Chrome');\nexport const isLegacyEdge = userAgent.includes('Edge');\nexport const isSafari = /^((?!chrome|android).)*safari/i.test(userAgent);\n\ninterface IZoomWindow {\n devicePixelRatio?: number;\n}\n\nexport function getZoomFactor(_targetWindow: IZoomWindow): number {\n return 1;\n}\nexport function getSafariVersion(): number {\n if (!isSafari) {\n return 0;\n }\n const majorVersion = userAgent.match(/Version\\/(\\d+)/);\n if (majorVersion === null || majorVersion.length < 2) {\n return 0;\n }\n return parseInt(majorVersion[1], 10);\n}\n\n// Find the user's platform. We use this to interpret the meta key\n// and ISO third level shifts.\n// http://stackoverflow.com/q/19877924/577598\nexport const isMac = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'].includes(platform);\nexport const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].includes(platform);\nexport const isLinux = platform.indexOf('Linux') >= 0;\n// Note that when this is true, isLinux will also be true.\nexport const isChromeOS = /\\bCrOS\\b/.test(userAgent);\n","/**\n * Copyright (c) 2026 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\n/**\n * Accumulates string data from multiple chunks without O(n²) string concatenation.\n */\nexport class StringBuilder {\n private _chunks: string[] = [];\n private _length = 0;\n\n public get length(): number {\n return this._length;\n }\n\n public reset(): void {\n this._chunks.length = 0;\n this._length = 0;\n }\n\n public append(chunk: string): void {\n this._chunks.push(chunk);\n this._length += chunk.length;\n }\n\n public toString(): string {\n return this._chunks.join('');\n }\n}\n\n/**\n * String builder that rejects payloads larger than a fixed limit.\n */\nexport class LimitedStringBuilder {\n private readonly _builder = new StringBuilder();\n\n constructor(private readonly _limit: number) { }\n\n public get length(): number {\n return this._builder.length;\n }\n\n public get limit(): number {\n return this._limit;\n }\n\n public reset(): void {\n this._builder.reset();\n }\n\n /**\n * @returns true if the limit was exceeded (buffer is cleared in that case)\n */\n public append(chunk: string): boolean {\n this._builder.append(chunk);\n if (this._builder.length > this._limit) {\n this._builder.reset();\n return true;\n }\n return false;\n }\n\n public toString(): string {\n return this._builder.toString();\n }\n}\n","/**\n * Copyright (c) 2022 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport type { ILogService } from './services/Services';\n\ninterface ITaskQueue {\n /**\n * Adds a task to the queue which will run in a future idle callback.\n * To avoid perceivable stalls on the main thread, tasks with heavy workload\n * should split their work into smaller pieces and return `true` to get\n * called again until the work is done (on falsy return value).\n */\n enqueue(task: () => boolean | void): void;\n\n /**\n * Flushes the queue, running all remaining tasks synchronously.\n */\n flush(): void;\n\n /**\n * Clears any remaining tasks from the queue, these will not be run.\n */\n clear(): void;\n}\n\ninterface ITaskDeadline {\n timeRemaining(): number;\n}\ntype CallbackWithDeadline = (deadline: ITaskDeadline) => void;\n\nabstract class TaskQueue implements ITaskQueue {\n private _tasks: (() => boolean | void)[] = [];\n private _idleCallback?: number;\n private _i = 0;\n protected readonly _logService: ILogService;\n\n constructor(logService: ILogService) {\n this._logService = logService;\n }\n\n protected abstract _requestCallback(callback: CallbackWithDeadline): number;\n protected abstract _cancelCallback(identifier: number): void;\n\n public enqueue(task: () => boolean | void): void {\n this._tasks.push(task);\n this._start();\n }\n\n public flush(): void {\n while (this._i < this._tasks.length) {\n if (!this._tasks[this._i]()) {\n this._i++;\n }\n }\n this.clear();\n }\n\n public clear(): void {\n if (this._idleCallback) {\n this._cancelCallback(this._idleCallback);\n this._idleCallback = undefined;\n }\n this._i = 0;\n this._tasks.length = 0;\n }\n\n private _start(): void {\n if (!this._idleCallback) {\n this._idleCallback = this._requestCallback(this._process.bind(this));\n }\n }\n\n private _process(deadline: ITaskDeadline): void {\n this._idleCallback = undefined;\n let taskDuration: number;\n let longestTask = 0;\n let lastDeadlineRemaining = deadline.timeRemaining();\n let deadlineRemaining: number;\n while (this._i < this._tasks.length) {\n taskDuration = performance.now();\n if (!this._tasks[this._i]()) {\n this._i++;\n }\n // other than performance.now, performance.now might not be stable (changes on wall clock\n // changes), this is not an issue here as a clock change during a short running task is very\n // unlikely in case it still happened and leads to negative duration, simply assume 1 msec\n taskDuration = Math.max(1, performance.now() - taskDuration);\n longestTask = Math.max(taskDuration, longestTask);\n // Guess the following task will take a similar time to the longest task in this batch, allow\n // additional room to try avoid exceeding the deadline\n deadlineRemaining = deadline.timeRemaining();\n if (longestTask * 1.5 > deadlineRemaining) {\n // Warn when the time exceeding the deadline is over 20ms, if this happens in practice the\n // task should be split into sub-tasks to ensure the UI remains responsive.\n if (lastDeadlineRemaining - taskDuration < -20) {\n this._logService.warn(`task queue exceeded allotted deadline by ${Math.abs(Math.round(lastDeadlineRemaining - taskDuration))}ms`);\n }\n this._start();\n return;\n }\n lastDeadlineRemaining = deadlineRemaining;\n }\n this.clear();\n }\n}\n\n/**\n * A queue of that runs tasks over several tasks via setTimeout, trying to maintain above 60 frames\n * per second. The tasks will run in the order they are enqueued, but they will run some time later,\n * and care should be taken to ensure they're non-urgent and will not introduce race conditions.\n */\nexport class PriorityTaskQueue extends TaskQueue {\n protected _requestCallback(callback: CallbackWithDeadline): number {\n return setTimeout(() => callback(this._createDeadline(16)));\n }\n\n protected _cancelCallback(identifier: number): void {\n clearTimeout(identifier);\n }\n\n private _createDeadline(duration: number): ITaskDeadline {\n const end = performance.now() + duration;\n return {\n timeRemaining: () => Math.max(0, end - performance.now())\n };\n }\n}\n\nclass IdleTaskQueueInternal extends TaskQueue {\n protected _requestCallback(callback: IdleRequestCallback): number {\n return requestIdleCallback(callback);\n }\n\n protected _cancelCallback(identifier: number): void {\n cancelIdleCallback(identifier);\n }\n}\n\n/**\n * A queue of that runs tasks over several idle callbacks, trying to respect the idle callback's\n * deadline given by the environment. The tasks will run in the order they are enqueued, but they\n * will run some time later, and care should be taken to ensure they're non-urgent and will not\n * introduce race conditions.\n *\n * This reverts to a {@link PriorityTaskQueue} if the environment does not support idle callbacks.\n */\n// eslint-disable-next-line @typescript-eslint/naming-convention\nexport const IdleTaskQueue = ('requestIdleCallback' in globalThis) ? IdleTaskQueueInternal : PriorityTaskQueue;\n\n/**\n * An object that tracks a single debounced task that will run on the next idle frame. When called\n * multiple times, only the last set task will run.\n */\nexport class DebouncedIdleTask {\n private _queue: ITaskQueue;\n\n constructor(logService: ILogService) {\n this._queue = new IdleTaskQueue(logService);\n }\n\n public set(task: () => boolean | void): void {\n this._queue.clear();\n this._queue.enqueue(task);\n }\n\n public flush(): void {\n this._queue.flush();\n }\n\n public dispose(): void {\n this._queue.clear();\n }\n}\n","/**\n * Copyright (c) 2025 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\n/**\n * The xterm.js version. This is updated by the publish script from package.json.\n */\nexport const XTERM_VERSION = '6.0.0';\n","/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { CHAR_DATA_CODE_INDEX, NULL_CELL_CODE, WHITESPACE_CELL_CODE } from './buffer/Constants';\nimport { IBufferService } from './services/Services';\n\nexport function updateWindowsModeWrappedState(bufferService: IBufferService): void {\n // Winpty does not support wraparound mode which means that lines will never\n // be marked as wrapped. This causes issues for things like copying a line\n // retaining the wrapped new line characters or if consumers are listening\n // in on the data stream.\n //\n // The workaround for this is to listen to every incoming line feed and mark\n // the line as wrapped if the last character in the previous line is not a\n // space. This is certainly not without its problems, but generally on\n // Windows when text reaches the end of the terminal it's likely going to be\n // wrapped.\n const line = bufferService.buffer.lines.get(bufferService.buffer.ybase + bufferService.buffer.y - 1);\n const lastChar = line?.get(bufferService.cols - 1);\n\n const nextLine = bufferService.buffer.lines.get(bufferService.buffer.ybase + bufferService.buffer.y);\n if (nextLine && lastChar) {\n nextLine.isWrapped = (lastChar[CHAR_DATA_CODE_INDEX] !== NULL_CELL_CODE && lastChar[CHAR_DATA_CODE_INDEX] !== WHITESPACE_CELL_CODE);\n }\n}\n","/**\n * Copyright (c) 2018 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { IColorRGB } from '../Types';\nimport { IAttributeData, IExtendedAttrs } from './Types';\nimport { Attributes, FgFlags, BgFlags, UnderlineStyle, ExtFlags } from './Constants';\n\nexport class AttributeData implements IAttributeData {\n public static toColorRGB(value: number): IColorRGB {\n return [\n value >>> Attributes.RED_SHIFT & 255,\n value >>> Attributes.GREEN_SHIFT & 255,\n value & 255\n ];\n }\n\n public static fromColorRGB(value: IColorRGB): number {\n return (value[0] & 255) << Attributes.RED_SHIFT | (value[1] & 255) << Attributes.GREEN_SHIFT | value[2] & 255;\n }\n\n public clone(): IAttributeData {\n const newObj = new AttributeData();\n newObj.fg = this.fg;\n newObj.bg = this.bg;\n newObj.extended = this.extended.clone();\n return newObj;\n }\n\n // data\n public fg = 0;\n public bg = 0;\n public extended: IExtendedAttrs = new ExtendedAttrs();\n\n // flags\n public isInverse(): number { return this.fg & FgFlags.INVERSE; }\n public isBold(): number { return this.fg & FgFlags.BOLD; }\n public isUnderline(): number {\n if (this.hasExtendedAttrs() && this.extended.underlineStyle !== UnderlineStyle.NONE) {\n return 1;\n }\n return this.fg & FgFlags.UNDERLINE;\n }\n public isBlink(): number { return this.fg & FgFlags.BLINK; }\n public isInvisible(): number { return this.fg & FgFlags.INVISIBLE; }\n public isItalic(): number { return this.bg & BgFlags.ITALIC; }\n public isDim(): number { return this.bg & BgFlags.DIM; }\n public isStrikethrough(): number { return this.fg & FgFlags.STRIKETHROUGH; }\n public isProtected(): number { return this.bg & BgFlags.PROTECTED; }\n public isOverline(): number { return this.bg & BgFlags.OVERLINE; }\n\n // color modes\n public getFgColorMode(): number { return this.fg & Attributes.CM_MASK; }\n public getBgColorMode(): number { return this.bg & Attributes.CM_MASK; }\n public isFgRGB(): boolean { return (this.fg & Attributes.CM_MASK) === Attributes.CM_RGB; }\n public isBgRGB(): boolean { return (this.bg & Attributes.CM_MASK) === Attributes.CM_RGB; }\n public isFgPalette(): boolean { return (this.fg & Attributes.CM_MASK) === Attributes.CM_P16 || (this.fg & Attributes.CM_MASK) === Attributes.CM_P256; }\n public isBgPalette(): boolean { return (this.bg & Attributes.CM_MASK) === Attributes.CM_P16 || (this.bg & Attributes.CM_MASK) === Attributes.CM_P256; }\n public isFgDefault(): boolean { return (this.fg & Attributes.CM_MASK) === 0; }\n public isBgDefault(): boolean { return (this.bg & Attributes.CM_MASK) === 0; }\n public isAttributeDefault(): boolean { return this.fg === 0 && this.bg === 0; }\n\n // colors\n public getFgColor(): number {\n switch (this.fg & Attributes.CM_MASK) {\n case Attributes.CM_P16:\n case Attributes.CM_P256: return this.fg & Attributes.PCOLOR_MASK;\n case Attributes.CM_RGB: return this.fg & Attributes.RGB_MASK;\n default: return -1; // CM_DEFAULT defaults to -1\n }\n }\n public getBgColor(): number {\n switch (this.bg & Attributes.CM_MASK) {\n case Attributes.CM_P16:\n case Attributes.CM_P256: return this.bg & Attributes.PCOLOR_MASK;\n case Attributes.CM_RGB: return this.bg & Attributes.RGB_MASK;\n default: return -1; // CM_DEFAULT defaults to -1\n }\n }\n\n // extended attrs\n public hasExtendedAttrs(): number {\n return this.bg & BgFlags.HAS_EXTENDED;\n }\n public updateExtended(): void {\n if (this.extended.isEmpty()) {\n this.bg &= ~BgFlags.HAS_EXTENDED;\n } else {\n this.bg |= BgFlags.HAS_EXTENDED;\n }\n }\n public getUnderlineColor(): number {\n if ((this.bg & BgFlags.HAS_EXTENDED) && ~this.extended.underlineColor) {\n switch (this.extended.underlineColor & Attributes.CM_MASK) {\n case Attributes.CM_P16:\n case Attributes.CM_P256: return this.extended.underlineColor & Attributes.PCOLOR_MASK;\n case Attributes.CM_RGB: return this.extended.underlineColor & Attributes.RGB_MASK;\n default: return this.getFgColor();\n }\n }\n return this.getFgColor();\n }\n public getUnderlineColorMode(): number {\n return (this.bg & BgFlags.HAS_EXTENDED) && ~this.extended.underlineColor\n ? this.extended.underlineColor & Attributes.CM_MASK\n : this.getFgColorMode();\n }\n public isUnderlineColorRGB(): boolean {\n return (this.bg & BgFlags.HAS_EXTENDED) && ~this.extended.underlineColor\n ? (this.extended.underlineColor & Attributes.CM_MASK) === Attributes.CM_RGB\n : this.isFgRGB();\n }\n public isUnderlineColorPalette(): boolean {\n return (this.bg & BgFlags.HAS_EXTENDED) && ~this.extended.underlineColor\n ? (this.extended.underlineColor & Attributes.CM_MASK) === Attributes.CM_P16\n || (this.extended.underlineColor & Attributes.CM_MASK) === Attributes.CM_P256\n : this.isFgPalette();\n }\n public isUnderlineColorDefault(): boolean {\n return (this.bg & BgFlags.HAS_EXTENDED) && ~this.extended.underlineColor\n ? (this.extended.underlineColor & Attributes.CM_MASK) === 0\n : this.isFgDefault();\n }\n public getUnderlineStyle(): UnderlineStyle {\n return this.fg & FgFlags.UNDERLINE\n ? (this.bg & BgFlags.HAS_EXTENDED ? this.extended.underlineStyle : UnderlineStyle.SINGLE)\n : UnderlineStyle.NONE;\n }\n public getUnderlineVariantOffset(): number {\n return this.extended.underlineVariantOffset;\n }\n}\n\n\n/**\n * Extended attributes for a cell.\n * Holds information about different underline styles and color.\n */\nexport class ExtendedAttrs implements IExtendedAttrs {\n private _ext: number = 0;\n public get ext(): number {\n if (this._urlId) {\n return (\n (this._ext & ~ExtFlags.UNDERLINE_STYLE) |\n (this.underlineStyle << 26)\n );\n }\n return this._ext;\n }\n public set ext(value: number) { this._ext = value; }\n\n public get underlineStyle(): UnderlineStyle {\n // Always return the URL style if it has one\n if (this._urlId) {\n return UnderlineStyle.DASHED;\n }\n return (this._ext & ExtFlags.UNDERLINE_STYLE) >> 26;\n }\n public set underlineStyle(value: UnderlineStyle) {\n this._ext &= ~ExtFlags.UNDERLINE_STYLE;\n this._ext |= (value << 26) & ExtFlags.UNDERLINE_STYLE;\n }\n\n public get underlineColor(): number {\n return this._ext & (Attributes.CM_MASK | Attributes.RGB_MASK);\n }\n public set underlineColor(value: number) {\n this._ext &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);\n this._ext |= value & (Attributes.CM_MASK | Attributes.RGB_MASK);\n }\n\n private _urlId: number = 0;\n public get urlId(): number {\n return this._urlId;\n }\n public set urlId(value: number) {\n this._urlId = value;\n }\n\n public get underlineVariantOffset(): number {\n const val = (this._ext & ExtFlags.VARIANT_OFFSET) >> 29;\n if (val < 0) {\n return val ^ 0xFFFFFFF8;\n }\n return val;\n }\n public set underlineVariantOffset(value: number) {\n this._ext &= ~ExtFlags.VARIANT_OFFSET;\n this._ext |= (value << 29) & ExtFlags.VARIANT_OFFSET;\n }\n\n constructor(\n ext: number = 0,\n urlId: number = 0\n ) {\n this._ext = ext;\n this._urlId = urlId;\n }\n\n public clone(): IExtendedAttrs {\n return new ExtendedAttrs(this._ext, this._urlId);\n }\n\n /**\n * Convenient method to indicate whether the object holds no additional information,\n * that needs to be persistant in the buffer.\n */\n public isEmpty(): boolean {\n return this.underlineStyle === UnderlineStyle.NONE && this._urlId === 0;\n }\n}\n","/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { CircularList, IInsertEvent } from '../CircularList';\nimport { Disposable, toDisposable } from '../Lifecycle';\nimport { IdleTaskQueue } from '../TaskQueue';\nimport { ICharset } from '../Types';\nimport { IAttributeData, IBuffer, IBufferLine, ICellData } from './Types';\nimport { ExtendedAttrs } from './AttributeData';\nimport { BufferLine, DEFAULT_ATTR_DATA } from './BufferLine';\nimport { getWrappedLineTrimmedLength, reflowLargerApplyNewLayout, reflowLargerCreateNewLayout, reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths } from './BufferReflow';\nimport { CellData } from './CellData';\nimport { NULL_CELL_CHAR, NULL_CELL_CODE, NULL_CELL_WIDTH, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE, WHITESPACE_CELL_WIDTH } from './Constants';\nimport { Marker } from './Marker';\nimport { DEFAULT_CHARSET } from '../data/Charsets';\nimport { IBufferService, ILogService, IOptionsService } from '../services/Services';\n\nexport const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1\n\n/**\n * This class represents a terminal buffer (an internal state of the terminal), where the\n * following information is stored (in high-level):\n * - text content of this particular buffer\n * - cursor position\n * - scroll position\n */\nexport class Buffer extends Disposable implements IBuffer {\n public lines: CircularList;\n public ydisp: number = 0;\n public ybase: number = 0;\n public y: number = 0;\n public x: number = 0;\n public scrollBottom: number;\n public scrollTop: number;\n public tabs: { [column: number]: boolean | undefined } = {};\n public savedY: number = 0;\n public savedX: number = 0;\n public savedCurAttrData = DEFAULT_ATTR_DATA.clone();\n public savedCharset: ICharset | undefined = DEFAULT_CHARSET;\n public savedCharsets: (ICharset | undefined)[] = [];\n public savedGlevel: number = 0;\n public savedOriginMode: boolean = false;\n public savedWraparoundMode: boolean = true;\n public markers: Marker[] = [];\n private _nullCell: ICellData = CellData.fromCharData([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);\n private _whitespaceCell: ICellData = CellData.fromCharData([0, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_WIDTH, WHITESPACE_CELL_CODE]);\n private _cols: number;\n private _rows: number;\n private _isClearing: boolean = false;\n private _memoryCleanupQueue: InstanceType;\n private _memoryCleanupPosition = 0;\n\n constructor(\n private _hasScrollback: boolean,\n private _optionsService: IOptionsService,\n private _bufferService: IBufferService,\n private readonly _logService: ILogService\n ) {\n super();\n this._cols = this._bufferService.cols;\n this._rows = this._bufferService.rows;\n this.lines = new CircularList(this._getCorrectBufferLength(this._rows));\n this.scrollTop = 0;\n this.scrollBottom = this._rows - 1;\n this.setupTabStops();\n this._memoryCleanupQueue = new IdleTaskQueue(this._logService);\n this._register(toDisposable(() => this._memoryCleanupQueue.clear()));\n this._register(toDisposable(() => this.clearAllMarkers()));\n }\n\n public getNullCell(attr?: IAttributeData): ICellData {\n if (attr) {\n this._nullCell.fg = attr.fg;\n this._nullCell.bg = attr.bg;\n this._nullCell.extended = attr.extended;\n } else {\n this._nullCell.fg = 0;\n this._nullCell.bg = 0;\n this._nullCell.extended = new ExtendedAttrs();\n }\n return this._nullCell;\n }\n\n public getWhitespaceCell(attr?: IAttributeData): ICellData {\n if (attr) {\n this._whitespaceCell.fg = attr.fg;\n this._whitespaceCell.bg = attr.bg;\n this._whitespaceCell.extended = attr.extended;\n } else {\n this._whitespaceCell.fg = 0;\n this._whitespaceCell.bg = 0;\n this._whitespaceCell.extended = new ExtendedAttrs();\n }\n return this._whitespaceCell;\n }\n\n public getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine {\n return new BufferLine(this._bufferService.cols, this.getNullCell(attr), isWrapped);\n }\n\n public get hasScrollback(): boolean {\n return this._hasScrollback && this.lines.maxLength > this._rows;\n }\n\n public get isCursorInViewport(): boolean {\n const absoluteY = this.ybase + this.y;\n const relativeY = absoluteY - this.ydisp;\n return (relativeY >= 0 && relativeY < this._rows);\n }\n\n /**\n * Gets the correct buffer length based on the rows provided, the terminal's\n * scrollback and whether this buffer is flagged to have scrollback or not.\n * @param rows The terminal rows to use in the calculation.\n */\n private _getCorrectBufferLength(rows: number): number {\n if (!this._hasScrollback) {\n return rows;\n }\n\n const correctBufferLength = rows + this._optionsService.rawOptions.scrollback;\n\n return correctBufferLength > MAX_BUFFER_SIZE ? MAX_BUFFER_SIZE : correctBufferLength;\n }\n\n /**\n * Fills the buffer's viewport with blank lines.\n */\n public fillViewportRows(fillAttr?: IAttributeData): void {\n if (this.lines.length === 0) {\n fillAttr ??= DEFAULT_ATTR_DATA;\n let i = this._rows;\n while (i--) {\n this.lines.push(this.getBlankLine(fillAttr));\n }\n }\n }\n\n /**\n * Clears the buffer to its initial state, discarding all previous data.\n */\n public clear(): void {\n this.ydisp = 0;\n this.ybase = 0;\n this.y = 0;\n this.x = 0;\n this.lines = new CircularList(this._getCorrectBufferLength(this._rows));\n this.scrollTop = 0;\n this.scrollBottom = this._rows - 1;\n this.setupTabStops();\n }\n\n /**\n * Resizes the buffer, adjusting its data accordingly.\n * @param newCols The new number of columns.\n * @param newRows The new number of rows.\n */\n public resize(newCols: number, newRows: number): void {\n // store reference to null cell with default attrs\n const nullCell = this.getNullCell(DEFAULT_ATTR_DATA);\n\n // count bufferlines with overly big memory to be cleaned afterwards\n let dirtyMemoryLines = 0;\n\n // Increase max length if needed before adjustments to allow space to fill\n // as required.\n const newMaxLength = this._getCorrectBufferLength(newRows);\n if (newMaxLength > this.lines.maxLength) {\n this.lines.maxLength = newMaxLength;\n }\n\n // if (this._cols > newCols) {\n // console.log('increase!');\n // }\n\n // The following adjustments should only happen if the buffer has been\n // initialized/filled.\n if (this.lines.length > 0) {\n // Deal with columns increasing (reducing needs to happen after reflow)\n if (this._cols < newCols) {\n for (let i = 0; i < this.lines.length; i++) {\n // +boolean for fast 0 or 1 conversion\n dirtyMemoryLines += +this.lines.get(i)!.resize(newCols, nullCell);\n }\n }\n\n // Resize rows in both directions as needed\n let addToY = 0;\n if (this._rows < newRows) {\n for (let y = this._rows; y < newRows; y++) {\n if (this.lines.length < newRows + this.ybase) {\n if (this._optionsService.rawOptions.windowsPty.backend !== undefined || this._optionsService.rawOptions.windowsPty.buildNumber !== undefined) {\n // Just add the new missing rows on Windows as conpty reprints the screen with its\n // view of the world. Once a line enters scrollback for conpty it remains there\n this.lines.push(new BufferLine(newCols, nullCell, false));\n } else {\n if (this.ybase > 0 && this.lines.length <= this.ybase + this.y + addToY + 1) {\n // There is room above the buffer and there are no empty elements below the line,\n // scroll up\n this.ybase--;\n addToY++;\n if (this.ydisp > 0) {\n // Viewport is at the top of the buffer, must increase downwards\n this.ydisp--;\n }\n } else {\n // Add a blank line if there is no buffer left at the top to scroll to, or if there\n // are blank lines after the cursor\n this.lines.push(new BufferLine(newCols, nullCell, false));\n }\n }\n }\n }\n } else { // (this._rows >= newRows)\n for (let y = this._rows; y > newRows; y--) {\n if (this.lines.length > newRows + this.ybase) {\n if (this.lines.length > this.ybase + this.y + 1) {\n // The line is a blank line below the cursor, remove it\n this.lines.pop();\n } else {\n // The line is the cursor, scroll down\n this.ybase++;\n this.ydisp++;\n }\n }\n }\n }\n\n // Reduce max length if needed after adjustments, this is done after as it\n // would otherwise cut data from the bottom of the buffer.\n if (newMaxLength < this.lines.maxLength) {\n // Trim from the top of the buffer and adjust ybase and ydisp.\n const amountToTrim = this.lines.length - newMaxLength;\n if (amountToTrim > 0) {\n this.lines.trimStart(amountToTrim);\n this.ybase = Math.max(this.ybase - amountToTrim, 0);\n this.ydisp = Math.max(this.ydisp - amountToTrim, 0);\n this.savedY = Math.max(this.savedY - amountToTrim, 0);\n }\n this.lines.maxLength = newMaxLength;\n }\n\n // Make sure that the cursor stays on screen\n this.x = Math.min(this.x, newCols - 1);\n this.y = Math.min(this.y, newRows - 1);\n if (addToY) {\n this.y += addToY;\n }\n this.savedX = Math.min(this.savedX, newCols - 1);\n\n this.scrollTop = 0;\n }\n\n this.scrollBottom = newRows - 1;\n\n if (this._isReflowEnabled) {\n this._reflow(newCols, newRows);\n\n // Trim the end of the line off if cols shrunk\n if (this._cols > newCols) {\n for (let i = 0; i < this.lines.length; i++) {\n // +boolean for fast 0 or 1 conversion\n dirtyMemoryLines += +this.lines.get(i)!.resize(newCols, nullCell);\n }\n }\n }\n\n this._cols = newCols;\n this._rows = newRows;\n\n // Ensure the cursor position invariant: ybase + y must be within buffer bounds\n // This can be violated during reflow or when shrinking rows\n if (this.lines.length > 0) {\n const maxY = Math.max(0, this.lines.length - this.ybase - 1);\n this.y = Math.min(this.y, maxY);\n }\n\n this._memoryCleanupQueue.clear();\n // schedule memory cleanup only, if more than 10% of the lines are affected\n if (dirtyMemoryLines > 0.1 * this.lines.length) {\n this._memoryCleanupPosition = 0;\n this._memoryCleanupQueue.enqueue(() => this._batchedMemoryCleanup());\n }\n }\n\n private _batchedMemoryCleanup(): boolean {\n let normalRun = true;\n if (this._memoryCleanupPosition >= this.lines.length) {\n // cleanup made it once through all lines, thus rescan in loop below to also catch shifted\n // lines, which should finish rather quick if there are no more cleanups pending\n this._memoryCleanupPosition = 0;\n normalRun = false;\n }\n let counted = 0;\n while (this._memoryCleanupPosition < this.lines.length) {\n counted += this.lines.get(this._memoryCleanupPosition++)!.cleanupMemory();\n // cleanup max 100 lines per batch\n if (counted > 100) {\n return true;\n }\n }\n // normal runs always need another rescan afterwards\n // if we made it here with normalRun=false, we are in a final run\n // and can end the cleanup task for sure\n return normalRun;\n }\n\n private get _isReflowEnabled(): boolean {\n const windowsPty = this._optionsService.rawOptions.windowsPty;\n if (windowsPty && windowsPty.buildNumber) {\n return this._hasScrollback && windowsPty.backend === 'conpty' && windowsPty.buildNumber >= 21376;\n }\n return this._hasScrollback;\n }\n\n private _reflow(newCols: number, newRows: number): void {\n if (this._cols === newCols) {\n return;\n }\n\n // Iterate through rows, ignore the last one as it cannot be wrapped\n if (newCols > this._cols) {\n this._reflowLarger(newCols, newRows);\n } else {\n this._reflowSmaller(newCols, newRows);\n }\n }\n\n private _reflowLarger(newCols: number, newRows: number): void {\n const reflowCursorLine = this._optionsService.rawOptions.reflowCursorLine;\n const toRemove: number[] = reflowLargerGetLinesToRemove(this.lines, this._cols, newCols, this.ybase + this.y, this.getNullCell(DEFAULT_ATTR_DATA), reflowCursorLine);\n if (toRemove.length > 0) {\n const newLayoutResult = reflowLargerCreateNewLayout(this.lines, toRemove);\n reflowLargerApplyNewLayout(this.lines, newLayoutResult.layout);\n this._reflowLargerAdjustViewport(newCols, newRows, newLayoutResult.countRemoved);\n }\n }\n\n private _reflowLargerAdjustViewport(newCols: number, newRows: number, countRemoved: number): void {\n const nullCell = this.getNullCell(DEFAULT_ATTR_DATA);\n // Adjust viewport based on number of items removed\n let viewportAdjustments = countRemoved;\n while (viewportAdjustments-- > 0) {\n if (this.ybase === 0) {\n if (this.y > 0) {\n this.y--;\n }\n if (this.lines.length < newRows) {\n // Add an extra row at the bottom of the viewport\n this.lines.push(new BufferLine(newCols, nullCell, false));\n }\n } else {\n if (this.ydisp === this.ybase) {\n this.ydisp--;\n }\n this.ybase--;\n }\n }\n this.savedY = Math.max(this.savedY - countRemoved, 0);\n }\n\n private _reflowSmaller(newCols: number, newRows: number): void {\n const reflowCursorLine = this._optionsService.rawOptions.reflowCursorLine;\n const nullCell = this.getNullCell(DEFAULT_ATTR_DATA);\n // Gather all BufferLines that need to be inserted into the Buffer here so that they can be\n // batched up and only committed once\n const toInsert = [];\n let countToInsert = 0;\n // Go backwards as many lines may be trimmed and this will avoid considering them\n for (let y = this.lines.length - 1; y >= 0; y--) {\n // Check whether this line is a problem\n let nextLine = this.lines.get(y) as BufferLine;\n if (!nextLine || !nextLine.isWrapped && nextLine.getTrimmedLength() <= newCols) {\n continue;\n }\n\n // Gather wrapped lines and adjust y to be the starting line\n const wrappedLines: BufferLine[] = [nextLine];\n while (nextLine.isWrapped && y > 0) {\n nextLine = this.lines.get(--y) as BufferLine;\n wrappedLines.unshift(nextLine);\n }\n\n if (!reflowCursorLine) {\n // If these lines contain the cursor don't touch them, the program will handle fixing up\n // wrapped lines with the cursor\n const absoluteY = this.ybase + this.y;\n if (absoluteY >= y && absoluteY < y + wrappedLines.length) {\n continue;\n }\n }\n\n const lastLineLength = wrappedLines[wrappedLines.length - 1].getTrimmedLength();\n const destLineLengths = reflowSmallerGetNewLineLengths(wrappedLines, this._cols, newCols);\n const linesToAdd = destLineLengths.length - wrappedLines.length;\n let trimmedLines: number;\n if (this.ybase === 0 && this.y !== this.lines.length - 1) {\n // If the top section of the buffer is not yet filled\n trimmedLines = Math.max(0, this.y - this.lines.maxLength + linesToAdd);\n } else {\n trimmedLines = Math.max(0, this.lines.length - this.lines.maxLength + linesToAdd);\n }\n\n // Add the new lines\n const newLines: BufferLine[] = [];\n for (let i = 0; i < linesToAdd; i++) {\n const newLine = this.getBlankLine(DEFAULT_ATTR_DATA, true) as BufferLine;\n newLines.push(newLine);\n }\n if (newLines.length > 0) {\n toInsert.push({\n // countToInsert here gets the actual index, taking into account other inserted items.\n // using this we can iterate through the list forwards\n start: y + wrappedLines.length + countToInsert,\n newLines\n });\n countToInsert += newLines.length;\n }\n wrappedLines.push(...newLines);\n\n // Copy buffer data to new locations, this needs to happen backwards to do in-place\n let destLineIndex = destLineLengths.length - 1; // Math.floor(cellsNeeded / newCols);\n let destCol = destLineLengths[destLineIndex]; // cellsNeeded % newCols;\n if (destCol === 0) {\n destLineIndex--;\n destCol = destLineLengths[destLineIndex];\n }\n let srcLineIndex = wrappedLines.length - linesToAdd - 1;\n let srcCol = lastLineLength;\n while (srcLineIndex >= 0) {\n const cellsToCopy = Math.min(srcCol, destCol);\n if (wrappedLines[destLineIndex] === undefined) {\n // Sanity check that the line exists, this has been known to fail for an unknown reason\n // which would stop the reflow from happening if an exception would throw.\n break;\n }\n wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol - cellsToCopy, destCol - cellsToCopy, cellsToCopy, true);\n destCol -= cellsToCopy;\n if (destCol === 0) {\n destLineIndex--;\n destCol = destLineLengths[destLineIndex];\n }\n srcCol -= cellsToCopy;\n if (srcCol === 0) {\n srcLineIndex--;\n const wrappedLinesIndex = Math.max(srcLineIndex, 0);\n srcCol = getWrappedLineTrimmedLength(wrappedLines, wrappedLinesIndex, this._cols);\n }\n }\n\n // Null out the end of the line ends if a wide character wrapped to the following line\n for (let i = 0; i < wrappedLines.length; i++) {\n if (destLineLengths[i] < newCols) {\n wrappedLines[i].setCell(destLineLengths[i], nullCell);\n }\n }\n\n // Adjust viewport as needed\n let viewportAdjustments = linesToAdd - trimmedLines;\n while (viewportAdjustments-- > 0) {\n if (this.ybase === 0) {\n if (this.y < newRows - 1) {\n this.y++;\n this.lines.pop();\n } else {\n this.ybase++;\n this.ydisp++;\n }\n } else {\n // Ensure ybase does not exceed its maximum value\n if (this.ybase < Math.min(this.lines.maxLength, this.lines.length + countToInsert) - newRows) {\n if (this.ybase === this.ydisp) {\n this.ydisp++;\n }\n this.ybase++;\n }\n }\n }\n this.savedY = Math.min(this.savedY + linesToAdd, this.ybase + newRows - 1);\n }\n\n // Rearrange lines in the buffer if there are any insertions, this is done at the end rather\n // than earlier so that it's a single O(n) pass through the buffer, instead of O(n^2) from many\n // costly calls to CircularList.splice.\n if (toInsert.length > 0) {\n // Record buffer insert events and then play them back backwards so that the indexes are\n // correct\n const insertEvents: IInsertEvent[] = [];\n\n // Record original lines so they don't get overridden when we rearrange the list\n const originalLines: BufferLine[] = [];\n for (let i = 0; i < this.lines.length; i++) {\n originalLines.push(this.lines.get(i) as BufferLine);\n }\n const originalLinesLength = this.lines.length;\n\n let originalLineIndex = originalLinesLength - 1;\n let nextToInsertIndex = 0;\n let nextToInsert = toInsert[nextToInsertIndex];\n this.lines.length = Math.min(this.lines.maxLength, this.lines.length + countToInsert);\n let countInsertedSoFar = 0;\n for (let i = Math.min(this.lines.maxLength - 1, originalLinesLength + countToInsert - 1); i >= 0; i--) {\n if (nextToInsert && nextToInsert.start > originalLineIndex + countInsertedSoFar) {\n // Insert extra lines here, adjusting i as needed\n for (let nextI = nextToInsert.newLines.length - 1; nextI >= 0; nextI--) {\n this.lines.set(i--, nextToInsert.newLines[nextI]);\n }\n i++;\n\n // Create insert events for later\n insertEvents.push({\n index: originalLineIndex + 1,\n amount: nextToInsert.newLines.length\n });\n\n countInsertedSoFar += nextToInsert.newLines.length;\n nextToInsert = toInsert[++nextToInsertIndex];\n } else {\n this.lines.set(i, originalLines[originalLineIndex--]);\n }\n }\n\n // Update markers\n let insertCountEmitted = 0;\n for (let i = insertEvents.length - 1; i >= 0; i--) {\n insertEvents[i].index += insertCountEmitted;\n this.lines.onInsertEmitter.fire(insertEvents[i]);\n insertCountEmitted += insertEvents[i].amount;\n }\n const amountToTrim = Math.max(0, originalLinesLength + countToInsert - this.lines.maxLength);\n if (amountToTrim > 0) {\n this.lines.onTrimEmitter.fire(amountToTrim);\n }\n }\n }\n\n /**\n * Translates a buffer line to a string, with optional start and end columns.\n * Wide characters will count as two columns in the resulting string. This\n * function is useful for getting the actual text underneath the raw selection\n * position.\n * @param lineIndex The absolute index of the line being translated.\n * @param trimRight Whether to trim whitespace to the right.\n * @param startCol The column to start at.\n * @param endCol The column to end at.\n */\n public translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol: number = 0, endCol?: number): string {\n const line = this.lines.get(lineIndex);\n if (!line) {\n return '';\n }\n return line.translateToString(trimRight, startCol, endCol);\n }\n\n public getWrappedRangeForLine(y: number): { first: number, last: number } {\n let first = y;\n let last = y;\n // Scan upwards for wrapped lines\n while (first > 0 && this.lines.get(first)!.isWrapped) {\n first--;\n }\n // Scan downwards for wrapped lines\n while (last + 1 < this.lines.length && this.lines.get(last + 1)!.isWrapped) {\n last++;\n }\n return { first, last };\n }\n\n /**\n * Setup the tab stops.\n * @param i The index to start setting up tab stops from.\n */\n public setupTabStops(i?: number): void {\n if (i !== null && i !== undefined) {\n if (!this.tabs[i]) {\n i = this.prevStop(i);\n }\n } else {\n this.tabs = {};\n i = 0;\n }\n\n for (; i < this._cols; i += this._optionsService.rawOptions.tabStopWidth) {\n this.tabs[i] = true;\n }\n }\n\n /**\n * Move the cursor to the previous tab stop from the given position (default is current).\n * @param x The position to move the cursor to the previous tab stop.\n */\n public prevStop(x?: number): number {\n x ??= this.x;\n while (!this.tabs[--x] && x > 0);\n return x >= this._cols ? this._cols - 1 : x < 0 ? 0 : x;\n }\n\n /**\n * Move the cursor one tab stop forward from the given position (default is current).\n * @param x The position to move the cursor one tab stop forward.\n */\n public nextStop(x?: number): number {\n x ??= this.x;\n while (!this.tabs[++x] && x < this._cols);\n return x >= this._cols ? this._cols - 1 : x < 0 ? 0 : x;\n }\n\n /**\n * Clears markers on single line.\n * @param y The line to clear.\n */\n public clearMarkers(y: number): void {\n this._isClearing = true;\n for (let i = 0; i < this.markers.length; i++) {\n if (this.markers[i].line === y) {\n this.markers[i].dispose();\n this.markers.splice(i--, 1);\n }\n }\n this._isClearing = false;\n }\n\n /**\n * Clears markers on all lines\n */\n public clearAllMarkers(): void {\n this._isClearing = true;\n for (let i = 0; i < this.markers.length; i++) {\n this.markers[i].dispose();\n }\n this.markers.length = 0;\n this._isClearing = false;\n }\n\n public addMarker(y: number): Marker {\n const marker = new Marker(y);\n this.markers.push(marker);\n marker.register(this.lines.onTrim(amount => {\n marker.line -= amount;\n // The marker should be disposed when the line is trimmed from the buffer\n if (marker.line < 0) {\n marker.dispose();\n }\n }));\n marker.register(this.lines.onInsert(event => {\n if (marker.line >= event.index) {\n marker.line += event.amount;\n }\n }));\n marker.register(this.lines.onDelete(event => {\n // Delete the marker if it's within the range\n if (marker.line >= event.index && marker.line < event.index + event.amount) {\n marker.dispose();\n }\n\n // Shift the marker if it's after the deleted range\n if (marker.line > event.index) {\n marker.line -= event.amount;\n }\n }));\n marker.register(marker.onDispose(() => this._removeMarker(marker)));\n return marker;\n }\n\n private _removeMarker(marker: Marker): void {\n if (!this._isClearing) {\n this.markers.splice(this.markers.indexOf(marker), 1);\n }\n }\n}\n","/**\n * Copyright (c) 2018 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { CharData, IAttributeData, IBufferLine, ICellData, IExtendedAttrs } from './Types';\nimport { AttributeData } from './AttributeData';\nimport { CellData } from './CellData';\nimport { Attributes, BgFlags, CHAR_DATA_ATTR_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, Content, NULL_CELL_CHAR, NULL_CELL_CODE, NULL_CELL_WIDTH, WHITESPACE_CELL_CHAR } from './Constants';\nimport { stringFromCodePoint } from '../input/TextDecoder';\n\n// Buffer memory layout:\n//\n// [0]: content `uint32_t` - wcwidth(2) comb(1) codepoint(21)\n// [1]: fg `uint32_t` - flags(8) r(8) g(8) b(8)\n// [2]: bg `uint32_t` - flags(8) r(8) g(8) b(8)\n\nconst enum Constants {\n /** The number of 32 bit array indices taken by one cell. */\n CELL_INDICIES = 3,\n /** Factor when to cleanup underlying array buffer after shrinking. */\n CLEANUP_THRESHOLD = 2\n}\n\n/**\n * Cell member indices.\n *\n * Direct access:\n * `content = data[column * Constants.CELL_INDICIES + Cell.CONTENT];`\n * `fg = data[column * Constants.CELL_INDICIES + Cell.FG];`\n * `bg = data[column * Constants.CELL_INDICIES + Cell.BG];`\n */\nconst enum Cell {\n CONTENT = 0,\n FG = 1, // currently simply holds all known attrs\n BG = 2 // currently unused\n}\n\n\ninterface IExtendedAttrsExt extends IExtendedAttrs {\n _ext: number;\n _urlId: number;\n}\n\n\nexport const DEFAULT_ATTR_DATA = Object.freeze(new AttributeData());\n\n// Work variables to avoid garbage collection\nlet $startIndex = 0;\nconst $workCell = new CellData();\nconst $extended = DEFAULT_ATTR_DATA.extended.clone() as IExtendedAttrsExt;\n\n\n/**\n * Typed array based bufferline implementation.\n *\n * There are 2 ways to insert data into the cell buffer:\n * - `setCellFromCodepoint` + `addCodepointToCell`\n * Use these for data that is already UTF32.\n * Used during normal input in `InputHandler` for faster buffer access.\n * - `setCell`\n * This method takes a CellData object and stores the data in the buffer.\n * Use `CellData.fromCharData` to create the CellData object (e.g. from JS string).\n *\n * To retrieve data from the buffer use either one of the primitive methods\n * (if only one particular value is needed) or `loadCell`. For `loadCell` in a loop\n * memory allocs / GC pressure can be greatly reduced by reusing the CellData object.\n */\nexport class BufferLine implements IBufferLine {\n protected _data: Uint32Array;\n /** Sparse cache; only read when `IS_COMBINED_MASK` is set in `_data`. */\n protected _combined: {[index: number]: string} = {};\n /** Sparse cache; only read when `HAS_EXTENDED` is set in `_data`. */\n protected _extendedAttrs: {[index: number]: IExtendedAttrs | undefined} = {};\n public length: number;\n\n /** line text cache */\n protected _cacheValid = false;\n protected _cache: string = '';\n protected _cacheTrimmed = false;\n\n constructor(\n cols: number,\n fillCellData?: ICellData,\n public isWrapped: boolean = false\n ) {\n this._data = new Uint32Array(cols * Constants.CELL_INDICIES);\n const cell = fillCellData ?? CellData.fromCharData([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);\n for (let i = 0; i < cols; ++i) {\n this.setCell(i, cell);\n }\n this.length = cols;\n }\n\n /**\n * Get cell data CharData.\n * @deprecated\n */\n public get(index: number): CharData {\n const content = this._data[index * Constants.CELL_INDICIES + Cell.CONTENT];\n const cp = content & Content.CODEPOINT_MASK;\n return [\n this._data[index * Constants.CELL_INDICIES + Cell.FG],\n (content & Content.IS_COMBINED_MASK)\n ? this._combined[index]\n : (cp) ? stringFromCodePoint(cp) : '',\n content >> Content.WIDTH_SHIFT,\n (content & Content.IS_COMBINED_MASK)\n ? this._combined[index].charCodeAt(this._combined[index].length - 1)\n : cp\n ];\n }\n\n /**\n * Set cell data from CharData.\n * @deprecated\n */\n public set(index: number, value: CharData): void {\n this._cacheValid = false;\n this._data[index * Constants.CELL_INDICIES + Cell.FG] = value[CHAR_DATA_ATTR_INDEX];\n if (value[CHAR_DATA_CHAR_INDEX].length > 1) {\n this._combined[index] = value[1];\n this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = index | Content.IS_COMBINED_MASK | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);\n } else {\n this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);\n }\n }\n\n /**\n * primitive getters\n * use these when only one value is needed, otherwise use `loadCell`\n */\n public getWidth(index: number): number {\n return this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] >> Content.WIDTH_SHIFT;\n }\n\n /** Test whether content has width. */\n public hasWidth(index: number): number {\n return this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] & Content.WIDTH_MASK;\n }\n\n /** Get FG cell component. */\n public getFg(index: number): number {\n return this._data[index * Constants.CELL_INDICIES + Cell.FG];\n }\n\n /** Get BG cell component. */\n public getBg(index: number): number {\n return this._data[index * Constants.CELL_INDICIES + Cell.BG];\n }\n\n /**\n * Test whether contains any chars.\n * Basically an empty has no content, but other cells might differ in FG/BG\n * from real empty cells.\n */\n public hasContent(index: number): number {\n return this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] & Content.HAS_CONTENT_MASK;\n }\n\n /**\n * Get codepoint of the cell.\n * To be in line with `code` in CharData this either returns\n * a single UTF32 codepoint or the last codepoint of a combined string.\n */\n public getCodePoint(index: number): number {\n const content = this._data[index * Constants.CELL_INDICIES + Cell.CONTENT];\n if (content & Content.IS_COMBINED_MASK) {\n return this._combined[index].charCodeAt(this._combined[index].length - 1);\n }\n return content & Content.CODEPOINT_MASK;\n }\n\n /** Test whether the cell contains a combined string. */\n public isCombined(index: number): number {\n return this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] & Content.IS_COMBINED_MASK;\n }\n\n /** Returns the string content of the cell. */\n public getString(index: number): string {\n const content = this._data[index * Constants.CELL_INDICIES + Cell.CONTENT];\n if (content & Content.IS_COMBINED_MASK) {\n return this._combined[index];\n }\n if (content & Content.CODEPOINT_MASK) {\n return stringFromCodePoint(content & Content.CODEPOINT_MASK);\n }\n // return empty string for empty cells\n return '';\n }\n\n /** Get state of protected flag. */\n public isProtected(index: number): number {\n return this._data[index * Constants.CELL_INDICIES + Cell.BG] & BgFlags.PROTECTED;\n }\n\n /**\n * Load data at `index` into `cell`. This is used to access cells in a way that's more friendly\n * to GC as it significantly reduced the amount of new objects/references needed.\n */\n public loadCell(index: number, cell: ICellData): ICellData {\n $startIndex = index * Constants.CELL_INDICIES;\n cell.content = this._data[$startIndex + Cell.CONTENT];\n cell.fg = this._data[$startIndex + Cell.FG];\n cell.bg = this._data[$startIndex + Cell.BG];\n if (cell.content & Content.IS_COMBINED_MASK) {\n cell.combinedData = this._combined[index];\n } else {\n cell.combinedData = '';\n }\n if (cell.bg & BgFlags.HAS_EXTENDED) {\n cell.extended = this._extendedAttrs[index]!;\n } else {\n // Do not mutate cell.extended in place: it may still reference this line's map entry from a\n // prior loadCell into a reused CellData (e.g. $workCell during insert/delete).\n // We use $extended as blueprint and reset the internals\n // mimicking the ctor to avoid a new allocation.\n $extended._ext = 0;\n $extended._urlId = 0;\n cell.extended = $extended;\n }\n return cell;\n }\n\n /**\n * Set data at `index` to `cell`.\n */\n public setCell(index: number, cell: ICellData): void {\n this._cacheValid = false;\n if (cell.content & Content.IS_COMBINED_MASK) {\n this._combined[index] = cell.combinedData;\n }\n if (cell.bg & BgFlags.HAS_EXTENDED) {\n this._extendedAttrs[index] = cell.extended;\n }\n this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = cell.content;\n this._data[index * Constants.CELL_INDICIES + Cell.FG] = cell.fg;\n this._data[index * Constants.CELL_INDICIES + Cell.BG] = cell.bg;\n }\n\n /**\n * Set cell data from input handler.\n * Since the input handler see the incoming chars as UTF32 codepoints,\n * it gets an optimized access method.\n */\n public setCellFromCodepoint(index: number, codePoint: number, width: number, attrs: IAttributeData): void {\n this._cacheValid = false;\n if (attrs.bg & BgFlags.HAS_EXTENDED) {\n this._extendedAttrs[index] = attrs.extended;\n }\n const $idx = index * Constants.CELL_INDICIES;\n this._data[$idx + Cell.CONTENT] = codePoint | (width << Content.WIDTH_SHIFT);\n this._data[$idx + Cell.FG] = attrs.fg;\n this._data[$idx + Cell.BG] = attrs.bg;\n }\n\n /**\n * Add a codepoint to a cell from input handler.\n * During input stage combining chars with a width of 0 follow and stack\n * onto a leading char. Since we already set the attrs\n * by the previous `setDataFromCodePoint` call, we can omit it here.\n */\n public addCodepointToCell(index: number, codePoint: number, width: number): void {\n this._cacheValid = false;\n let content = this._data[index * Constants.CELL_INDICIES + Cell.CONTENT];\n if (content & Content.IS_COMBINED_MASK) {\n // we already have a combined string, simply add\n this._combined[index] += stringFromCodePoint(codePoint);\n } else {\n if (content & Content.CODEPOINT_MASK) {\n // normal case for combining chars:\n // - move current leading char + new one into combined string\n // - set combined flag\n this._combined[index] = stringFromCodePoint(content & Content.CODEPOINT_MASK) + stringFromCodePoint(codePoint);\n content &= ~Content.CODEPOINT_MASK; // set codepoint in buffer to 0\n content |= Content.IS_COMBINED_MASK;\n } else {\n // should not happen - we actually have no data in the cell yet\n // simply set the data in the cell buffer with a width of 1\n content = codePoint | (1 << Content.WIDTH_SHIFT);\n }\n }\n if (width) {\n content &= ~Content.WIDTH_MASK;\n content |= width << Content.WIDTH_SHIFT;\n }\n this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = content;\n }\n\n public insertCells(pos: number, n: number, fillCellData: ICellData): void {\n this._cacheValid = false;\n pos %= this.length;\n\n // handle fullwidth at pos: reset cell one to the left if pos is second cell of a wide char\n if (pos && this.getWidth(pos - 1) === 2) {\n this.setCellFromCodepoint(pos - 1, 0, 1, fillCellData);\n }\n\n if (n < this.length - pos) {\n for (let i = this.length - pos - n - 1; i >= 0; --i) {\n this.setCell(pos + n + i, this.loadCell(pos + i, $workCell));\n }\n for (let i = 0; i < n; ++i) {\n this.setCell(pos + i, fillCellData);\n }\n } else {\n for (let i = pos; i < this.length; ++i) {\n this.setCell(i, fillCellData);\n }\n }\n\n // handle fullwidth at line end: reset last cell if it is first cell of a wide char\n if (this.getWidth(this.length - 1) === 2) {\n this.setCellFromCodepoint(this.length - 1, 0, 1, fillCellData);\n }\n }\n\n public deleteCells(pos: number, n: number, fillCellData: ICellData): void {\n this._cacheValid = false;\n pos %= this.length;\n if (n < this.length - pos) {\n for (let i = 0; i < this.length - pos - n; ++i) {\n this.setCell(pos + i, this.loadCell(pos + n + i, $workCell));\n }\n for (let i = this.length - n; i < this.length; ++i) {\n this.setCell(i, fillCellData);\n }\n } else {\n for (let i = pos; i < this.length; ++i) {\n this.setCell(i, fillCellData);\n }\n }\n\n // handle fullwidth at pos:\n // - reset pos-1 if wide char\n // - reset pos if width==0 (previous second cell of a wide char)\n if (pos && this.getWidth(pos - 1) === 2) {\n this.setCellFromCodepoint(pos - 1, 0, 1, fillCellData);\n }\n if (this.getWidth(pos) === 0 && !this.hasContent(pos)) {\n this.setCellFromCodepoint(pos, 0, 1, fillCellData);\n }\n }\n\n public replaceCells(start: number, end: number, fillCellData: ICellData, respectProtect: boolean = false): void {\n this._cacheValid = false;\n // full branching on respectProtect==true, hopefully getting fast JIT for standard case\n if (respectProtect) {\n if (start && this.getWidth(start - 1) === 2 && !this.isProtected(start - 1)) {\n this.setCellFromCodepoint(start - 1, 0, 1, fillCellData);\n }\n if (end < this.length && this.getWidth(end - 1) === 2 && !this.isProtected(end)) {\n this.setCellFromCodepoint(end, 0, 1, fillCellData);\n }\n while (start < end && start < this.length) {\n if (!this.isProtected(start)) {\n this.setCell(start, fillCellData);\n }\n start++;\n }\n return;\n }\n\n // handle fullwidth at start: reset cell one to the left if start is second cell of a wide char\n if (start && this.getWidth(start - 1) === 2) {\n this.setCellFromCodepoint(start - 1, 0, 1, fillCellData);\n }\n // handle fullwidth at last cell + 1: reset to empty cell if it is second part of a wide char\n if (end < this.length && this.getWidth(end - 1) === 2) {\n this.setCellFromCodepoint(end, 0, 1, fillCellData);\n }\n\n while (start < end && start < this.length) {\n this.setCell(start++, fillCellData);\n }\n }\n\n /**\n * Resize BufferLine to `cols` filling excess cells with `fillCellData`.\n * The underlying array buffer will not change if there is still enough space\n * to hold the new buffer line data.\n * Returns a boolean indicating, whether a `cleanupMemory` call would free\n * excess memory (true after shrinking > Constants.CLEANUP_THRESHOLD).\n */\n public resize(cols: number, fillCellData: ICellData): boolean {\n this._cacheValid = false;\n if (cols === this.length) {\n return this._data.length * 4 * Constants.CLEANUP_THRESHOLD < this._data.buffer.byteLength;\n }\n const uint32Cells = cols * Constants.CELL_INDICIES;\n if (cols > this.length) {\n if (this._data.buffer.byteLength >= uint32Cells * 4) {\n // optimization: avoid alloc and data copy if buffer has enough room\n this._data = new Uint32Array(this._data.buffer, 0, uint32Cells);\n } else {\n // slow path: new alloc and full data copy\n const data = new Uint32Array(uint32Cells);\n data.set(this._data);\n this._data = data;\n }\n for (let i = this.length; i < cols; ++i) {\n this.setCell(i, fillCellData);\n }\n } else {\n // optimization: just shrink the view on existing buffer\n this._data = this._data.subarray(0, uint32Cells);\n // Remove any cut off combined data\n const keys = Object.keys(this._combined);\n for (let i = 0; i < keys.length; i++) {\n const key = parseInt(keys[i], 10);\n if (key >= cols) {\n delete this._combined[key];\n }\n }\n // remove any cut off extended attributes\n const extKeys = Object.keys(this._extendedAttrs);\n for (let i = 0; i < extKeys.length; i++) {\n const key = parseInt(extKeys[i], 10);\n if (key >= cols) {\n delete this._extendedAttrs[key];\n }\n }\n }\n this.length = cols;\n return uint32Cells * 4 * Constants.CLEANUP_THRESHOLD < this._data.buffer.byteLength;\n }\n\n /**\n * Cleanup underlying array buffer.\n * A cleanup will be triggered if the array buffer exceeds the actual used\n * memory by a factor of Constants.CLEANUP_THRESHOLD.\n * Returns 0 or 1 indicating whether a cleanup happened.\n */\n public cleanupMemory(): number {\n if (this._data.length * 4 * Constants.CLEANUP_THRESHOLD < this._data.buffer.byteLength) {\n const data = new Uint32Array(this._data.length);\n data.set(this._data);\n this._data = data;\n return 1;\n }\n return 0;\n }\n\n /** fill a line with fillCharData */\n public fill(fillCellData: ICellData, respectProtect: boolean = false): void {\n this._cacheValid = false;\n // full branching on respectProtect==true, hopefully getting fast JIT for standard case\n if (respectProtect) {\n for (let i = 0; i < this.length; ++i) {\n if (!this.isProtected(i)) {\n this.setCell(i, fillCellData);\n }\n }\n return;\n }\n this._combined = {};\n this._extendedAttrs = {};\n for (let i = 0; i < this.length; ++i) {\n this.setCell(i, fillCellData);\n }\n }\n\n /** alter to a full copy of line */\n public copyFrom(line: BufferLine, blank?: boolean): void {\n if (this.length !== line.length) {\n this._data = new Uint32Array(line._data);\n } else {\n // use high speed copy if lengths are equal\n this._data.set(line._data);\n }\n this.length = line.length;\n if (blank) {\n // a blank line may never hold combined or extended attrs,\n // thus we can skip handling them\n this._combined = {};\n this._extendedAttrs = {};\n } else {\n this._copySparseMapsFrom(line);\n }\n this._cache = '';\n this._cacheValid = false;\n this.isWrapped = line.isWrapped;\n }\n\n /** create a new clone */\n public clone(blank?: boolean): IBufferLine {\n const newLine = new BufferLine(0, undefined, false);\n newLine._data = new Uint32Array(this._data);\n newLine.length = this.length;\n if (!blank) {\n // a blank line may never hold combined or extended attrs,\n // thus we can skip handling them\n newLine._copySparseMapsFrom(this);\n }\n newLine.isWrapped = this.isWrapped;\n return newLine;\n }\n\n public getTrimmedLength(): number {\n for (let i = this.length - 1; i >= 0; --i) {\n if ((this._data[i * Constants.CELL_INDICIES + Cell.CONTENT] & Content.HAS_CONTENT_MASK)) {\n return i + (this._data[i * Constants.CELL_INDICIES + Cell.CONTENT] >> Content.WIDTH_SHIFT);\n }\n }\n return 0;\n }\n\n public getNoBgTrimmedLength(): number {\n for (let i = this.length - 1; i >= 0; --i) {\n if ((this._data[i * Constants.CELL_INDICIES + Cell.CONTENT] & Content.HAS_CONTENT_MASK) || (this._data[i * Constants.CELL_INDICIES + Cell.BG] & Attributes.CM_MASK)) {\n return i + (this._data[i * Constants.CELL_INDICIES + Cell.CONTENT] >> Content.WIDTH_SHIFT);\n }\n }\n return 0;\n }\n\n public copyCellsFrom(src: BufferLine, srcCol: number, destCol: number, length: number, applyInReverse: boolean): void {\n this._cacheValid = false;\n const srcData = src._data;\n if (applyInReverse) {\n for (let cell = length - 1; cell >= 0; cell--) {\n for (let i = 0; i < Constants.CELL_INDICIES; i++) {\n this._data[(destCol + cell) * Constants.CELL_INDICIES + i] = srcData[(srcCol + cell) * Constants.CELL_INDICIES + i];\n }\n this._copyCellMapsFrom(src, srcCol + cell, destCol + cell);\n }\n } else {\n for (let cell = 0; cell < length; cell++) {\n for (let i = 0; i < Constants.CELL_INDICIES; i++) {\n this._data[(destCol + cell) * Constants.CELL_INDICIES + i] = srcData[(srcCol + cell) * Constants.CELL_INDICIES + i];\n }\n this._copyCellMapsFrom(src, srcCol + cell, destCol + cell);\n }\n }\n }\n\n /**\n * Translates the buffer line to a string. Caching only applies to canonical full-line translation\n * requests (regardless of `trimRight` value).\n *\n * @param trimRight Whether to trim any empty cells on the right.\n * @param startCol The column to start the string (0-based inclusive).\n * @param endCol The column to end the string (0-based exclusive).\n * @param outColumns if specified, this array will be filled with column numbers such that\n * `returnedString[i]` is displayed at `outColumns[i]` column. `outColumns[returnedString.length]`\n * is where the character following `returnedString` will be displayed.\n *\n * When a single cell is translated to multiple UTF-16 code units (e.g. surrogate pair) in the\n * returned string, the corresponding entries in `outColumns` will have the same column number.\n */\n public translateToString(trimRight?: boolean, startCol?: number, endCol?: number, outColumns?: number[]): string {\n const isCanonical = (startCol === undefined || startCol === 0) && endCol === undefined && outColumns === undefined;\n if (isCanonical && this._cacheValid) {\n if (trimRight) {\n return this._cacheTrimmed ? this._cache : this._cache.trimEnd();\n }\n if (!this._cacheTrimmed) {\n return this._cache;\n }\n }\n startCol = startCol ?? 0;\n endCol = endCol ?? this.length;\n if (trimRight) {\n endCol = Math.min(endCol, this.getTrimmedLength());\n }\n if (outColumns) {\n outColumns.length = 0;\n }\n const cellContents: string[] = [];\n while (startCol < endCol) {\n const content = this._data[startCol * Constants.CELL_INDICIES + Cell.CONTENT];\n const cp = content & Content.CODEPOINT_MASK;\n const chars = (content & Content.IS_COMBINED_MASK) ? this._combined[startCol] : (cp) ? stringFromCodePoint(cp) : WHITESPACE_CELL_CHAR;\n cellContents.push(chars);\n if (outColumns) {\n for (let i = 0; i < chars.length; ++i) {\n outColumns.push(startCol);\n }\n }\n startCol += (content >> Content.WIDTH_SHIFT) || 1; // always advance by at least 1\n }\n if (outColumns) {\n outColumns.push(startCol);\n }\n const result = cellContents.join('');\n if (isCanonical) {\n this._cache = result;\n this._cacheValid = true;\n this._cacheTrimmed = !!trimRight;\n }\n return result;\n }\n\n /** Copy sparse map entries for a single cell when `_data` flags require them. */\n private _copyCellMapsFrom(src: BufferLine, srcCol: number, destCol: number): void {\n const srcStart = srcCol * Constants.CELL_INDICIES;\n if (src._data[srcStart + Cell.CONTENT] & Content.IS_COMBINED_MASK) {\n this._combined[destCol] = src._combined[srcCol];\n }\n if (src._data[srcStart + Cell.BG] & BgFlags.HAS_EXTENDED) {\n this._extendedAttrs[destCol] = src._extendedAttrs[srcCol];\n }\n }\n\n /** Rebuild sparse maps from another line, keyed only by `_data` flags. */\n private _copySparseMapsFrom(line: BufferLine): void {\n this._combined = {};\n this._extendedAttrs = {};\n for (let i = 0; i < line.length; i++) {\n this._copyCellMapsFrom(line, i, i);\n }\n }\n}\n","/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { BufferLine } from './BufferLine';\nimport { CircularList } from '../CircularList';\nimport { IBufferLine, ICellData } from './Types';\n\nexport interface INewLayoutResult {\n layout: number[];\n countRemoved: number;\n}\n\n/**\n * Evaluates and returns indexes to be removed after a reflow larger occurs. Lines will be removed\n * when a wrapped line unwraps.\n * @param lines The buffer lines.\n * @param oldCols The columns before resize\n * @param newCols The columns after resize.\n * @param bufferAbsoluteY The absolute y position of the cursor (baseY + cursorY).\n * @param nullCell The cell data to use when filling in empty cells.\n * @param reflowCursorLine Whether to reflow the line containing the cursor.\n */\nexport function reflowLargerGetLinesToRemove(lines: CircularList, oldCols: number, newCols: number, bufferAbsoluteY: number, nullCell: ICellData, reflowCursorLine: boolean): number[] {\n // Gather all BufferLines that need to be removed from the Buffer here so that they can be\n // batched up and only committed once\n const toRemove: number[] = [];\n\n for (let y = 0; y < lines.length - 1; y++) {\n // Check if this row is wrapped\n let i = y;\n let nextLine = lines.get(++i) as BufferLine;\n if (!nextLine.isWrapped) {\n continue;\n }\n\n // Check how many lines it's wrapped for\n const wrappedLines: BufferLine[] = [lines.get(y) as BufferLine];\n while (i < lines.length && nextLine.isWrapped) {\n wrappedLines.push(nextLine);\n nextLine = lines.get(++i) as BufferLine;\n }\n\n if (!reflowCursorLine) {\n // If these lines contain the cursor don't touch them, the program will handle fixing up\n // wrapped lines with the cursor\n if (bufferAbsoluteY >= y && bufferAbsoluteY < i) {\n y += wrappedLines.length - 1;\n continue;\n }\n }\n\n // Copy buffer data to new locations\n let destLineIndex = 0;\n let destCol = getWrappedLineTrimmedLength(wrappedLines, destLineIndex, oldCols);\n let srcLineIndex = 1;\n let srcCol = 0;\n while (srcLineIndex < wrappedLines.length) {\n const srcTrimmedTineLength = getWrappedLineTrimmedLength(wrappedLines, srcLineIndex, oldCols);\n const srcRemainingCells = srcTrimmedTineLength - srcCol;\n const destRemainingCells = newCols - destCol;\n const cellsToCopy = Math.min(srcRemainingCells, destRemainingCells);\n\n wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol, destCol, cellsToCopy, false);\n\n destCol += cellsToCopy;\n if (destCol === newCols) {\n destLineIndex++;\n destCol = 0;\n }\n srcCol += cellsToCopy;\n if (srcCol === srcTrimmedTineLength) {\n srcLineIndex++;\n srcCol = 0;\n }\n\n // Make sure the last cell isn't wide, if it is copy it to the current dest\n if (destCol === 0 && destLineIndex !== 0) {\n if (wrappedLines[destLineIndex - 1].getWidth(newCols - 1) === 2) {\n wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[destLineIndex - 1], newCols - 1, destCol++, 1, false);\n // Null out the end of the last row\n wrappedLines[destLineIndex - 1].setCell(newCols - 1, nullCell);\n }\n }\n }\n\n // Clear out remaining cells or fragments could remain;\n wrappedLines[destLineIndex].replaceCells(destCol, newCols, nullCell);\n\n // Work backwards and remove any rows at the end that only contain null cells\n let countToRemove = 0;\n for (let i = wrappedLines.length - 1; i > 0; i--) {\n if (i > destLineIndex || wrappedLines[i].getTrimmedLength() === 0) {\n countToRemove++;\n } else {\n break;\n }\n }\n\n if (countToRemove > 0) {\n toRemove.push(y + wrappedLines.length - countToRemove); // index\n toRemove.push(countToRemove);\n }\n\n y += wrappedLines.length - 1;\n }\n return toRemove;\n}\n\n/**\n * Creates and return the new layout for lines given an array of indexes to be removed.\n * @param lines The buffer lines.\n * @param toRemove The indexes to remove.\n */\nexport function reflowLargerCreateNewLayout(lines: CircularList, toRemove: number[]): INewLayoutResult {\n const layout: number[] = [];\n // First iterate through the list and get the actual indexes to use for rows\n let nextToRemoveIndex = 0;\n let nextToRemoveStart = toRemove[nextToRemoveIndex];\n let countRemovedSoFar = 0;\n for (let i = 0; i < lines.length; i++) {\n if (nextToRemoveStart === i) {\n const countToRemove = toRemove[++nextToRemoveIndex];\n\n // Tell markers that there was a deletion\n lines.onDeleteEmitter.fire({\n index: i - countRemovedSoFar,\n amount: countToRemove\n });\n\n i += countToRemove - 1;\n countRemovedSoFar += countToRemove;\n nextToRemoveStart = toRemove[++nextToRemoveIndex];\n } else {\n layout.push(i);\n }\n }\n return {\n layout,\n countRemoved: countRemovedSoFar\n };\n}\n\n/**\n * Applies a new layout to the buffer. This essentially does the same as many splice calls but it's\n * done all at once in a single iteration through the list since splice is very expensive.\n * @param lines The buffer lines.\n * @param newLayout The new layout to apply.\n */\nexport function reflowLargerApplyNewLayout(lines: CircularList, newLayout: number[]): void {\n // Record original lines so they don't get overridden when we rearrange the list\n const newLayoutLines: BufferLine[] = [];\n for (let i = 0; i < newLayout.length; i++) {\n newLayoutLines.push(lines.get(newLayout[i]) as BufferLine);\n }\n\n // Rearrange the list\n for (let i = 0; i < newLayoutLines.length; i++) {\n lines.set(i, newLayoutLines[i]);\n }\n lines.length = newLayout.length;\n}\n\n/**\n * Gets the new line lengths for a given wrapped line. The purpose of this function it to pre-\n * compute the wrapping points since wide characters may need to be wrapped onto the following line.\n * This function will return an array of numbers of where each line wraps to, the resulting array\n * will only contain the values `newCols` (when the line does not end with a wide character) and\n * `newCols - 1` (when the line does end with a wide character), except for the last value which\n * will contain the remaining items to fill the line.\n *\n * Calling this with a `newCols` value of `1` will lock up.\n *\n * @param wrappedLines The wrapped lines to evaluate.\n * @param oldCols The columns before resize.\n * @param newCols The columns after resize.\n */\nexport function reflowSmallerGetNewLineLengths(wrappedLines: BufferLine[], oldCols: number, newCols: number): number[] {\n const newLineLengths: number[] = [];\n let cellsNeeded = 0;\n for (let i = 0; i < wrappedLines.length; i++) {\n cellsNeeded += getWrappedLineTrimmedLength(wrappedLines, i, oldCols);\n }\n\n // Use srcCol and srcLine to find the new wrapping point, use that to get the cellsAvailable and\n // linesNeeded\n let srcCol = 0;\n let srcLine = 0;\n let cellsAvailable = 0;\n while (cellsAvailable < cellsNeeded) {\n if (cellsNeeded - cellsAvailable < newCols) {\n // Add the final line and exit the loop\n newLineLengths.push(cellsNeeded - cellsAvailable);\n break;\n }\n srcCol += newCols;\n const oldTrimmedLength = getWrappedLineTrimmedLength(wrappedLines, srcLine, oldCols);\n if (srcCol > oldTrimmedLength) {\n srcCol -= oldTrimmedLength;\n srcLine++;\n }\n const endsWithWide = wrappedLines[srcLine].getWidth(srcCol - 1) === 2;\n if (endsWithWide) {\n srcCol--;\n }\n const lineLength = endsWithWide ? newCols - 1 : newCols;\n newLineLengths.push(lineLength);\n cellsAvailable += lineLength;\n }\n\n return newLineLengths;\n}\n\nexport function getWrappedLineTrimmedLength(lines: BufferLine[], i: number, cols: number): number {\n // If this is the last row in the wrapped line, get the actual trimmed length\n if (i === lines.length - 1) {\n return lines[i].getTrimmedLength();\n }\n // Detect whether the following line starts with a wide character and the end of the current line\n // is null, if so then we can be pretty sure the null character should be excluded from the line\n // length]\n const endsInNull = !(lines[i].hasContent(cols - 1)) && lines[i].getWidth(cols - 1) === 1;\n const followingLineStartsWithWide = lines[i + 1].getWidth(0) === 2;\n if (endsInNull && followingLineStartsWithWide) {\n return cols - 1;\n }\n return cols;\n}\n","/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { Disposable, MutableDisposable } from '../Lifecycle';\nimport { Buffer } from './Buffer';\nimport { IAttributeData, IBuffer, IBufferSet } from './Types';\nimport { IBufferService, ILogService, IOptionsService } from '../services/Services';\nimport { Emitter } from '../Event';\n\n/**\n * The BufferSet represents the set of two buffers used by xterm terminals (normal and alt) and\n * provides also utilities for working with them.\n */\nexport class BufferSet extends Disposable implements IBufferSet {\n private _normal!: Buffer;\n private _alt!: Buffer;\n private _activeBuffer!: Buffer;\n private readonly _normalBuffer = this._register(new MutableDisposable());\n private readonly _altBuffer = this._register(new MutableDisposable());\n\n private readonly _onBufferActivate = this._register(new Emitter<{ activeBuffer: IBuffer, inactiveBuffer: IBuffer }>());\n public readonly onBufferActivate = this._onBufferActivate.event;\n\n /**\n * Create a new BufferSet for the given terminal.\n */\n constructor(\n private readonly _optionsService: IOptionsService,\n private readonly _bufferService: IBufferService,\n private readonly _logService: ILogService\n ) {\n super();\n this.reset();\n this._register(this._optionsService.onSpecificOptionChange('scrollback', () => this.resize(this._bufferService.cols, this._bufferService.rows)));\n this._register(this._optionsService.onSpecificOptionChange('tabStopWidth', () => this.setupTabStops()));\n }\n\n public reset(): void {\n this._normal = new Buffer(true, this._optionsService, this._bufferService, this._logService);\n this._normalBuffer.value = this._normal;\n this._normal.fillViewportRows();\n\n // The alt buffer should never have scrollback.\n // See http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-The-Alternate-Screen-Buffer\n this._alt = new Buffer(false, this._optionsService, this._bufferService, this._logService);\n this._altBuffer.value = this._alt;\n this._activeBuffer = this._normal;\n this._onBufferActivate.fire({\n activeBuffer: this._normal,\n inactiveBuffer: this._alt\n });\n\n this.setupTabStops();\n }\n\n /**\n * Returns the alt Buffer of the BufferSet\n */\n public get alt(): Buffer {\n return this._alt;\n }\n\n /**\n * Returns the currently active Buffer of the BufferSet\n */\n public get active(): Buffer {\n return this._activeBuffer;\n }\n\n /**\n * Returns the normal Buffer of the BufferSet\n */\n public get normal(): Buffer {\n return this._normal;\n }\n\n /**\n * Sets the normal Buffer of the BufferSet as its currently active Buffer\n */\n public activateNormalBuffer(): void {\n if (this._activeBuffer === this._normal) {\n return;\n }\n this._normal.x = this._alt.x;\n this._normal.y = this._alt.y;\n // The alt buffer should always be cleared when we switch to the normal\n // buffer. This frees up memory since the alt buffer should always be new\n // when activated.\n this._alt.clearAllMarkers();\n this._alt.clear();\n this._activeBuffer = this._normal;\n this._onBufferActivate.fire({\n activeBuffer: this._normal,\n inactiveBuffer: this._alt\n });\n }\n\n /**\n * Sets the alt Buffer of the BufferSet as its currently active Buffer\n */\n public activateAltBuffer(fillAttr?: IAttributeData): void {\n if (this._activeBuffer === this._alt) {\n return;\n }\n // Since the alt buffer is always cleared when the normal buffer is\n // activated, we want to fill it when switching to it.\n this._alt.fillViewportRows(fillAttr);\n this._alt.x = this._normal.x;\n this._alt.y = this._normal.y;\n this._activeBuffer = this._alt;\n this._onBufferActivate.fire({\n activeBuffer: this._alt,\n inactiveBuffer: this._normal\n });\n }\n\n /**\n * Resizes both normal and alt buffers, adjusting their data accordingly.\n * @param newCols The new number of columns.\n * @param newRows The new number of rows.\n */\n public resize(newCols: number, newRows: number): void {\n this._normal.resize(newCols, newRows);\n this._alt.resize(newCols, newRows);\n this.setupTabStops(newCols);\n }\n\n /**\n * Setup the tab stops.\n * @param i The index to start setting up tab stops from.\n */\n public setupTabStops(i?: number): void {\n this._normal.setupTabStops(i);\n this._alt.setupTabStops(i);\n }\n}\n","/**\n * Copyright (c) 2018 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { CharData, ICellData, IExtendedAttrs } from './Types';\nimport { stringFromCodePoint } from '../input/TextDecoder';\nimport { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_ATTR_INDEX, Content } from './Constants';\nimport { AttributeData, ExtendedAttrs } from './AttributeData';\nimport type { IBufferCell as IBufferCellApi } from '@xterm/xterm';\n\n/**\n * CellData - represents a single Cell in the terminal buffer.\n */\nexport class CellData extends AttributeData implements ICellData {\n /** Helper to create CellData from CharData. */\n public static fromCharData(value: CharData): CellData {\n const obj = new CellData();\n obj.setFromCharData(value);\n return obj;\n }\n /** Primitives from terminal buffer. */\n public content = 0;\n public fg = 0;\n public bg = 0;\n public extended: IExtendedAttrs = new ExtendedAttrs();\n public combinedData = '';\n /** Whether cell contains a combined string. */\n public isCombined(): number {\n return this.content & Content.IS_COMBINED_MASK;\n }\n /** Width of the cell. */\n public getWidth(): number {\n return this.content >> Content.WIDTH_SHIFT;\n }\n /** JS string of the content. */\n public getChars(): string {\n if (this.content & Content.IS_COMBINED_MASK) {\n return this.combinedData;\n }\n if (this.content & Content.CODEPOINT_MASK) {\n return stringFromCodePoint(this.content & Content.CODEPOINT_MASK);\n }\n return '';\n }\n /**\n * Codepoint of cell\n * Note this returns the UTF32 codepoint of single chars,\n * if content is a combined string it returns the codepoint\n * of the last char in string to be in line with code in CharData.\n */\n public getCode(): number {\n return (this.isCombined())\n ? this.combinedData.charCodeAt(this.combinedData.length - 1)\n : this.content & Content.CODEPOINT_MASK;\n }\n /** Set data from CharData */\n public setFromCharData(value: CharData): void {\n this.fg = value[CHAR_DATA_ATTR_INDEX];\n this.bg = 0;\n let combined = false;\n // surrogates and combined strings need special treatment\n if (value[CHAR_DATA_CHAR_INDEX].length > 2) {\n combined = true;\n }\n else if (value[CHAR_DATA_CHAR_INDEX].length === 2) {\n const code = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0);\n // if the 2-char string is a surrogate create single codepoint\n // everything else is combined\n if (0xD800 <= code && code <= 0xDBFF) {\n const second = value[CHAR_DATA_CHAR_INDEX].charCodeAt(1);\n if (0xDC00 <= second && second <= 0xDFFF) {\n this.content = ((code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);\n }\n else {\n combined = true;\n }\n }\n else {\n combined = true;\n }\n }\n else {\n this.content = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);\n }\n if (combined) {\n this.combinedData = value[CHAR_DATA_CHAR_INDEX];\n this.content = Content.IS_COMBINED_MASK | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);\n }\n }\n /** Get data as CharData. */\n public getAsCharData(): CharData {\n return [this.fg, this.getChars(), this.getWidth(), this.getCode()];\n }\n\n public attributesEquals(other: IBufferCellApi): boolean {\n if (this.getFgColorMode() !== other.getFgColorMode() || this.getFgColor() !== other.getFgColor()) {\n return false;\n }\n if (this.getBgColorMode() !== other.getBgColorMode() || this.getBgColor() !== other.getBgColor()) {\n return false;\n }\n if (this.isInverse() !== other.isInverse()) {\n return false;\n }\n if (this.isBold() !== other.isBold()) {\n return false;\n }\n if (this.isUnderline() !== other.isUnderline()) {\n return false;\n }\n if (this.isUnderline()) {\n if (this.getUnderlineStyle() !== other.getUnderlineStyle()) {\n return false;\n }\n const thisDefault = this.isUnderlineColorDefault();\n const otherDefault = other.isUnderlineColorDefault();\n if (!(thisDefault && otherDefault)) {\n if (thisDefault !== otherDefault) {\n return false;\n }\n if (this.getUnderlineColor() !== other.getUnderlineColor()) {\n return false;\n }\n if (this.getUnderlineColorMode() !== other.getUnderlineColorMode()) {\n return false;\n }\n }\n }\n if (this.isOverline() !== other.isOverline()) {\n return false;\n }\n if (this.isBlink() !== other.isBlink()) {\n return false;\n }\n if (this.isInvisible() !== other.isInvisible()) {\n return false;\n }\n if (this.isItalic() !== other.isItalic()) {\n return false;\n }\n if (this.isDim() !== other.isDim()) {\n return false;\n }\n if (this.isStrikethrough() !== other.isStrikethrough()) {\n return false;\n }\n return true;\n }\n\n}\n","/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nexport const DEFAULT_COLOR = 0;\nexport const DEFAULT_ATTR = (0 << 18) | (DEFAULT_COLOR << 9) | (256 << 0);\nexport const DEFAULT_EXT = 0;\n\nexport const CHAR_DATA_ATTR_INDEX = 0;\nexport const CHAR_DATA_CHAR_INDEX = 1;\nexport const CHAR_DATA_WIDTH_INDEX = 2;\nexport const CHAR_DATA_CODE_INDEX = 3;\n\n/**\n * Null cell - a real empty cell (containing nothing).\n * Note that code should always be 0 for a null cell as\n * several test condition of the buffer line rely on this.\n */\nexport const NULL_CELL_CHAR = '';\nexport const NULL_CELL_WIDTH = 1;\nexport const NULL_CELL_CODE = 0;\n\n/**\n * Whitespace cell.\n * This is meant as a replacement for empty cells when needed\n * during rendering lines to preserve correct alignment.\n */\nexport const WHITESPACE_CELL_CHAR = ' ';\nexport const WHITESPACE_CELL_WIDTH = 1;\nexport const WHITESPACE_CELL_CODE = 32;\n\n/**\n * Bitmasks for accessing data in `content`.\n */\nexport const enum Content {\n /**\n * bit 1..21 codepoint, max allowed in UTF32 is 0x10FFFF (21 bits taken)\n * read: `codepoint = content & Content.CODEPOINT_MASK;`\n * write: `content |= codepoint & Content.CODEPOINT_MASK;`\n * shortcut if precondition `codepoint <= 0x10FFFF` is met:\n * `content |= codepoint;`\n */\n CODEPOINT_MASK = 0x1FFFFF,\n\n /**\n * bit 22 flag indicating whether a cell contains combined content\n * read: `isCombined = content & Content.IS_COMBINED_MASK;`\n * set: `content |= Content.IS_COMBINED_MASK;`\n * clear: `content &= ~Content.IS_COMBINED_MASK;`\n */\n IS_COMBINED_MASK = 0x200000, // 1 << 21\n\n /**\n * bit 1..22 mask to check whether a cell contains any string data\n * we need to check for codepoint and isCombined bits to see\n * whether a cell contains anything\n * read: `isEmpty = !(content & Content.HAS_CONTENT_MASK)`\n */\n HAS_CONTENT_MASK = 0x3FFFFF,\n\n /**\n * bit 23..24 wcwidth value of cell, takes 2 bits (ranges from 0..2)\n * read: `width = (content & Content.WIDTH_MASK) >> Content.WIDTH_SHIFT;`\n * `hasWidth = content & Content.WIDTH_MASK;`\n * as long as wcwidth is highest value in `content`:\n * `width = content >> Content.WIDTH_SHIFT;`\n * write: `content |= (width << Content.WIDTH_SHIFT) & Content.WIDTH_MASK;`\n * shortcut if precondition `0 <= width <= 3` is met:\n * `content |= width << Content.WIDTH_SHIFT;`\n */\n WIDTH_MASK = 0xC00000, // 3 << 22\n WIDTH_SHIFT = 22\n}\n\nexport const enum Attributes {\n /**\n * bit 1..8 blue in RGB, color in P256 and P16\n */\n BLUE_MASK = 0xFF,\n BLUE_SHIFT = 0,\n PCOLOR_MASK = 0xFF,\n PCOLOR_SHIFT = 0,\n\n /**\n * bit 9..16 green in RGB\n */\n GREEN_MASK = 0xFF00,\n GREEN_SHIFT = 8,\n\n /**\n * bit 17..24 red in RGB\n */\n RED_MASK = 0xFF0000,\n RED_SHIFT = 16,\n\n /**\n * bit 25..26 color mode: DEFAULT (0) | P16 (1) | P256 (2) | RGB (3)\n */\n CM_MASK = 0x3000000,\n CM_DEFAULT = 0,\n CM_P16 = 0x1000000,\n CM_P256 = 0x2000000,\n CM_RGB = 0x3000000,\n\n /**\n * bit 1..24 RGB room\n */\n RGB_MASK = 0xFFFFFF\n}\n\nexport const enum FgFlags {\n /**\n * bit 27..32\n */\n INVERSE = 0x4000000,\n BOLD = 0x8000000,\n UNDERLINE = 0x10000000,\n BLINK = 0x20000000,\n INVISIBLE = 0x40000000,\n STRIKETHROUGH = 0x80000000,\n}\n\nexport const enum BgFlags {\n /**\n * bit 27..32 (upper 2 unused)\n */\n ITALIC = 0x4000000,\n DIM = 0x8000000,\n HAS_EXTENDED = 0x10000000,\n PROTECTED = 0x20000000,\n OVERLINE = 0x40000000\n}\n\nexport const enum ExtFlags {\n /**\n * bit 27..29\n */\n UNDERLINE_STYLE = 0x1C000000,\n\n /**\n * bit 30..32\n *\n * An optional variant for the glyph, this can be used for example to offset underlines by a\n * number of pixels to create a perfect pattern.\n */\n VARIANT_OFFSET = 0xE0000000\n}\n\nexport const enum UnderlineStyle {\n NONE = 0,\n SINGLE = 1,\n DOUBLE = 2,\n CURLY = 3,\n DOTTED = 4,\n DASHED = 5\n}\n","/**\n * Copyright (c) 2018 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { dispose, IDisposable } from '../Lifecycle';\nimport { IMarker } from './Types';\nimport { Emitter } from '../Event';\n\nexport class Marker implements IMarker {\n private static _nextId = 1;\n\n public isDisposed: boolean = false;\n private readonly _disposables: IDisposable[] = [];\n\n private readonly _id: number = Marker._nextId++;\n public get id(): number { return this._id; }\n\n private readonly _onDispose = this.register(new Emitter());\n public readonly onDispose = this._onDispose.event;\n\n constructor(\n public line: number\n ) {\n }\n\n public dispose(): void {\n if (this.isDisposed) {\n return;\n }\n this.isDisposed = true;\n this.line = -1;\n // Emit before super.dispose such that dispose listeners get a chance to react\n this._onDispose.fire();\n dispose(this._disposables);\n this._disposables.length = 0;\n }\n\n public register(disposable: T): T {\n this._disposables.push(disposable);\n return disposable;\n }\n}\n","/**\n * Copyright (c) 2016 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { ICharset } from '../Types';\n\n/**\n * The character sets supported by the terminal. These enable several languages\n * to be represented within the terminal with only 8-bit encoding. See ISO 2022\n * for a discussion on character sets. Only VT100 character sets are supported.\n */\nexport const CHARSETS: { [key: string]: ICharset | undefined } = {};\n\n/**\n * The default character set, US.\n */\nexport const DEFAULT_CHARSET: ICharset | undefined = CHARSETS['B'];\n\n/**\n * DEC Special Character and Line Drawing Set.\n * Reference: http://vt100.net/docs/vt102-ug/table5-13.html\n * A lot of curses apps use this if they see TERM=xterm.\n * testing: echo -e '\\e(0a\\e(B'\n * The xterm output sometimes seems to conflict with the\n * reference above. xterm seems in line with the reference\n * when running vttest however.\n * The table below now uses xterm's output from vttest.\n */\nCHARSETS['0'] = {\n '`': '\\u25c6', // '◆'\n 'a': '\\u2592', // '▒'\n 'b': '\\u2409', // '␉' (HT)\n 'c': '\\u240c', // '␌' (FF)\n 'd': '\\u240d', // '␍' (CR)\n 'e': '\\u240a', // '␊' (LF)\n 'f': '\\u00b0', // '°'\n 'g': '\\u00b1', // '±'\n 'h': '\\u2424', // '␤' (NL)\n 'i': '\\u240b', // '␋' (VT)\n 'j': '\\u2518', // '┘'\n 'k': '\\u2510', // '┐'\n 'l': '\\u250c', // '┌'\n 'm': '\\u2514', // '└'\n 'n': '\\u253c', // '┼'\n 'o': '\\u23ba', // '⎺'\n 'p': '\\u23bb', // '⎻'\n 'q': '\\u2500', // '─'\n 'r': '\\u23bc', // '⎼'\n 's': '\\u23bd', // '⎽'\n 't': '\\u251c', // '├'\n 'u': '\\u2524', // '┤'\n 'v': '\\u2534', // '┴'\n 'w': '\\u252c', // '┬'\n 'x': '\\u2502', // '│'\n 'y': '\\u2264', // '≤'\n 'z': '\\u2265', // '≥'\n '{': '\\u03c0', // 'π'\n '|': '\\u2260', // '≠'\n '}': '\\u00a3', // '£'\n '~': '\\u00b7' // '·'\n};\n\n/**\n * British character set\n * ESC (A\n * Reference: http://vt100.net/docs/vt220-rm/table2-5.html\n */\nCHARSETS['A'] = {\n '#': '£'\n};\n\n/**\n * United States character set\n * ESC (B\n */\nCHARSETS['B'] = undefined;\n\n/**\n * Dutch character set\n * ESC (4\n * Reference: http://vt100.net/docs/vt220-rm/table2-6.html\n */\nCHARSETS['4'] = {\n '#': '£',\n '@': '¾',\n '[': 'ij',\n '\\\\': '½',\n ']': '|',\n '{': '¨',\n '|': 'f',\n '}': '¼',\n '~': '´'\n};\n\n/**\n * Finnish character set\n * ESC (C or ESC (5\n * Reference: http://vt100.net/docs/vt220-rm/table2-7.html\n */\nCHARSETS['C'] = CHARSETS['5'] = {\n '[': 'Ä',\n '\\\\': 'Ö',\n ']': 'Å',\n '^': 'Ü',\n '`': 'é',\n '{': 'ä',\n '|': 'ö',\n '}': 'å',\n '~': 'ü'\n};\n\n/**\n * French character set\n * ESC (R\n * Reference: http://vt100.net/docs/vt220-rm/table2-8.html\n */\nCHARSETS['R'] = {\n '#': '£',\n '@': 'à',\n '[': '°',\n '\\\\': 'ç',\n ']': '§',\n '{': 'é',\n '|': 'ù',\n '}': 'è',\n '~': '¨'\n};\n\n/**\n * French Canadian character set\n * ESC (Q\n * Reference: http://vt100.net/docs/vt220-rm/table2-9.html\n */\nCHARSETS['Q'] = {\n '@': 'à',\n '[': 'â',\n '\\\\': 'ç',\n ']': 'ê',\n '^': 'î',\n '`': 'ô',\n '{': 'é',\n '|': 'ù',\n '}': 'è',\n '~': 'û'\n};\n\n/**\n * German character set\n * ESC (K\n * Reference: http://vt100.net/docs/vt220-rm/table2-10.html\n */\nCHARSETS['K'] = {\n '@': '§',\n '[': 'Ä',\n '\\\\': 'Ö',\n ']': 'Ü',\n '{': 'ä',\n '|': 'ö',\n '}': 'ü',\n '~': 'ß'\n};\n\n/**\n * Italian character set\n * ESC (Y\n * Reference: http://vt100.net/docs/vt220-rm/table2-11.html\n */\nCHARSETS['Y'] = {\n '#': '£',\n '@': '§',\n '[': '°',\n '\\\\': 'ç',\n ']': 'é',\n '`': 'ù',\n '{': 'à',\n '|': 'ò',\n '}': 'è',\n '~': 'ì'\n};\n\n/**\n * Norwegian/Danish character set\n * ESC (E or ESC (6\n * Reference: http://vt100.net/docs/vt220-rm/table2-12.html\n */\nCHARSETS['E'] = CHARSETS['6'] = {\n '@': 'Ä',\n '[': 'Æ',\n '\\\\': 'Ø',\n ']': 'Å',\n '^': 'Ü',\n '`': 'ä',\n '{': 'æ',\n '|': 'ø',\n '}': 'å',\n '~': 'ü'\n};\n\n/**\n * Spanish character set\n * ESC (Z\n * Reference: http://vt100.net/docs/vt220-rm/table2-13.html\n */\nCHARSETS['Z'] = {\n '#': '£',\n '@': '§',\n '[': '¡',\n '\\\\': 'Ñ',\n ']': '¿',\n '{': '°',\n '|': 'ñ',\n '}': 'ç'\n};\n\n/**\n * Swedish character set\n * ESC (H or ESC (7\n * Reference: http://vt100.net/docs/vt220-rm/table2-14.html\n */\nCHARSETS['H'] = CHARSETS['7'] = {\n '@': 'É',\n '[': 'Ä',\n '\\\\': 'Ö',\n ']': 'Å',\n '^': 'Ü',\n '`': 'é',\n '{': 'ä',\n '|': 'ö',\n '}': 'å',\n '~': 'ü'\n};\n\n/**\n * Swiss character set\n * ESC (=\n * Reference: http://vt100.net/docs/vt220-rm/table2-15.html\n */\nCHARSETS['='] = {\n '#': 'ù',\n '@': 'à',\n '[': 'é',\n '\\\\': 'ç',\n ']': 'ê',\n '^': 'î',\n\n '_': 'è',\n '`': 'ô',\n '{': 'ä',\n '|': 'ö',\n '}': 'ü',\n '~': 'û'\n};\n","/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\n/**\n * Polyfill - Convert UTF32 codepoint into JS string.\n * Note: The built-in String.fromCodePoint happens to be much slower\n * due to additional sanity checks. We can avoid them since\n * we always operate on legal UTF32 (granted by the input decoders)\n * and use this faster version instead.\n */\nexport function stringFromCodePoint(codePoint: number): string {\n if (codePoint > 0xFFFF) {\n codePoint -= 0x10000;\n return String.fromCharCode((codePoint >> 10) + 0xD800) + String.fromCharCode((codePoint % 0x400) + 0xDC00);\n }\n return String.fromCharCode(codePoint);\n}\n\n/**\n * Convert UTF32 char codes into JS string.\n * Basically the same as `stringFromCodePoint` but for multiple codepoints\n * in a loop (which is a lot faster).\n */\nexport function utf32ToString(data: Uint32Array, start: number = 0, end: number = data.length): string {\n let result = '';\n for (let i = start; i < end; ++i) {\n let codepoint = data[i];\n if (codepoint > 0xFFFF) {\n // JS strings are encoded as UTF16, thus a non BMP codepoint gets converted into a surrogate\n // pair conversion rules:\n // - subtract 0x10000 from code point, leaving a 20 bit number\n // - add high 10 bits to 0xD800 --> first surrogate\n // - add low 10 bits to 0xDC00 --> second surrogate\n codepoint -= 0x10000;\n result += String.fromCharCode((codepoint >> 10) + 0xD800) + String.fromCharCode((codepoint % 0x400) + 0xDC00);\n } else {\n result += String.fromCharCode(codepoint);\n }\n }\n return result;\n}\n\n/**\n * StringToUtf32 - decodes UTF16 sequences into UTF32 codepoints.\n * To keep the decoder in line with JS strings it handles single surrogates as UCS2.\n */\nexport class StringToUtf32 {\n private _interim: number = 0;\n\n /**\n * Clears interim and resets decoder to clean state.\n */\n public clear(): void {\n this._interim = 0;\n }\n\n /**\n * Decode JS string to UTF32 codepoints.\n * The methods assumes stream input and will store partly transmitted\n * surrogate pairs and decode them with the next data chunk.\n * Note: The method does no bound checks for target, therefore make sure\n * the provided input data does not exceed the size of `target`.\n * Returns the number of written codepoints in `target`.\n */\n public decode(input: string, target: Uint32Array): number {\n const length = input.length;\n\n if (!length) {\n return 0;\n }\n\n let size = 0;\n let startPos = 0;\n\n // handle leftover surrogate high\n if (this._interim) {\n const second = input.charCodeAt(startPos++);\n if (0xDC00 <= second && second <= 0xDFFF) {\n target[size++] = (this._interim - 0xD800) * 0x400 + second - 0xDC00 + 0x10000;\n } else {\n // illegal codepoint (USC2 handling)\n target[size++] = this._interim;\n target[size++] = second;\n }\n this._interim = 0;\n }\n\n for (let i = startPos; i < length; ++i) {\n const code = input.charCodeAt(i);\n // surrogate pair first\n if (0xD800 <= code && code <= 0xDBFF) {\n if (++i >= length) {\n this._interim = code;\n return size;\n }\n const second = input.charCodeAt(i);\n if (0xDC00 <= second && second <= 0xDFFF) {\n target[size++] = (code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000;\n } else {\n // illegal codepoint (USC2 handling)\n target[size++] = code;\n target[size++] = second;\n }\n continue;\n }\n if (code === 0xFEFF) {\n // BOM\n continue;\n }\n target[size++] = code;\n }\n return size;\n }\n}\n\n/**\n * Utf8Decoder - decodes UTF8 byte sequences into UTF32 codepoints.\n */\nexport class Utf8ToUtf32 {\n public interim: Uint8Array = new Uint8Array(3);\n\n /**\n * Clears interim bytes and resets decoder to clean state.\n */\n public clear(): void {\n this.interim.fill(0);\n }\n\n /**\n * Decodes UTF8 byte sequences in `input` to UTF32 codepoints in `target`.\n * The methods assumes stream input and will store partly transmitted bytes\n * and decode them with the next data chunk.\n * Note: The method does no bound checks for target, therefore make sure\n * the provided data chunk does not exceed the size of `target`.\n * Returns the number of written codepoints in `target`.\n */\n public decode(input: Uint8Array, target: Uint32Array): number {\n const length = input.length;\n\n if (!length) {\n return 0;\n }\n\n let size = 0;\n let byte1: number;\n let byte2: number;\n let byte3: number;\n let byte4: number;\n let codepoint;\n let startPos = 0;\n\n // handle leftover bytes\n if (this.interim[0]) {\n let discardInterim = false;\n let cp = this.interim[0];\n cp &= ((((cp & 0xE0) === 0xC0)) ? 0x1F : (((cp & 0xF0) === 0xE0)) ? 0x0F : 0x07);\n let pos = 0;\n let tmp: number;\n while ((tmp = this.interim[++pos]) && pos < 4) {\n cp <<= 6;\n cp |= tmp & 0x3F;\n }\n // missing bytes - read ahead from input\n const type = (((this.interim[0] & 0xE0) === 0xC0)) ? 2 : (((this.interim[0] & 0xF0) === 0xE0)) ? 3 : 4;\n const missing = type - pos;\n while (startPos < missing) {\n if (startPos >= length) {\n return 0;\n }\n tmp = input[startPos++];\n if ((tmp & 0xC0) !== 0x80) {\n // wrong continuation, discard interim bytes completely\n startPos--;\n discardInterim = true;\n break;\n } else {\n // need to save so we can continue short inputs in next call\n this.interim[pos++] = tmp;\n cp <<= 6;\n cp |= tmp & 0x3F;\n }\n }\n if (!discardInterim) {\n // final test is type dependent\n if (type === 2) {\n if (cp < 0x80) {\n // wrong starter byte\n startPos--;\n } else {\n target[size++] = cp;\n }\n } else if (type === 3) {\n if (cp < 0x0800 || (cp >= 0xD800 && cp <= 0xDFFF) || cp === 0xFEFF) {\n // illegal codepoint or BOM\n } else {\n target[size++] = cp;\n }\n } else {\n if (cp < 0x010000 || cp > 0x10FFFF) {\n // illegal codepoint\n } else {\n target[size++] = cp;\n }\n }\n }\n this.interim.fill(0);\n }\n\n // loop through input\n const fourStop = length - 4;\n let i = startPos;\n while (i < length) {\n /**\n * ASCII shortcut with loop unrolled to 4 consecutive ASCII chars.\n * This is a compromise between speed gain for ASCII\n * and penalty for non ASCII:\n * For best ASCII performance the char should be stored directly into target,\n * but even a single attempt to write to target and compare afterwards\n * penalizes non ASCII really bad (-50%), thus we load the char into byteX first,\n * which reduces ASCII performance by ~15%.\n * This trial for ASCII reduces non ASCII performance by ~10% which seems acceptible\n * compared to the gains.\n * Note that this optimization only takes place for 4 consecutive ASCII chars,\n * for any shorter it bails out. Worst case - all 4 bytes being read but\n * thrown away due to the last being a non ASCII char (-10% performance).\n */\n while (i < fourStop\n && !((byte1 = input[i]) & 0x80)\n && !((byte2 = input[i + 1]) & 0x80)\n && !((byte3 = input[i + 2]) & 0x80)\n && !((byte4 = input[i + 3]) & 0x80))\n {\n target[size++] = byte1;\n target[size++] = byte2;\n target[size++] = byte3;\n target[size++] = byte4;\n i += 4;\n }\n\n // reread byte1\n byte1 = input[i++];\n\n // 1 byte\n if (byte1 < 0x80) {\n target[size++] = byte1;\n\n // 2 bytes\n } else if ((byte1 & 0xE0) === 0xC0) {\n if (i >= length) {\n this.interim[0] = byte1;\n return size;\n }\n byte2 = input[i++];\n if ((byte2 & 0xC0) !== 0x80) {\n // wrong continuation\n i--;\n continue;\n }\n codepoint = (byte1 & 0x1F) << 6 | (byte2 & 0x3F);\n if (codepoint < 0x80) {\n // wrong starter byte\n i--;\n continue;\n }\n target[size++] = codepoint;\n\n // 3 bytes\n } else if ((byte1 & 0xF0) === 0xE0) {\n if (i >= length) {\n this.interim[0] = byte1;\n return size;\n }\n byte2 = input[i++];\n if ((byte2 & 0xC0) !== 0x80) {\n // wrong continuation\n i--;\n continue;\n }\n if (i >= length) {\n this.interim[0] = byte1;\n this.interim[1] = byte2;\n return size;\n }\n byte3 = input[i++];\n if ((byte3 & 0xC0) !== 0x80) {\n // wrong continuation\n i--;\n continue;\n }\n codepoint = (byte1 & 0x0F) << 12 | (byte2 & 0x3F) << 6 | (byte3 & 0x3F);\n if (codepoint < 0x0800 || (codepoint >= 0xD800 && codepoint <= 0xDFFF) || codepoint === 0xFEFF) {\n // illegal codepoint or BOM, no i-- here\n continue;\n }\n target[size++] = codepoint;\n\n // 4 bytes\n } else if ((byte1 & 0xF8) === 0xF0) {\n if (i >= length) {\n this.interim[0] = byte1;\n return size;\n }\n byte2 = input[i++];\n if ((byte2 & 0xC0) !== 0x80) {\n // wrong continuation\n i--;\n continue;\n }\n if (i >= length) {\n this.interim[0] = byte1;\n this.interim[1] = byte2;\n return size;\n }\n byte3 = input[i++];\n if ((byte3 & 0xC0) !== 0x80) {\n // wrong continuation\n i--;\n continue;\n }\n if (i >= length) {\n this.interim[0] = byte1;\n this.interim[1] = byte2;\n this.interim[2] = byte3;\n return size;\n }\n byte4 = input[i++];\n if ((byte4 & 0xC0) !== 0x80) {\n // wrong continuation\n i--;\n continue;\n }\n codepoint = (byte1 & 0x07) << 18 | (byte2 & 0x3F) << 12 | (byte3 & 0x3F) << 6 | (byte4 & 0x3F);\n if (codepoint < 0x010000 || codepoint > 0x10FFFF) {\n // illegal codepoint, no i-- here\n continue;\n }\n target[size++] = codepoint;\n } else {\n // illegal byte, just skip\n }\n }\n return size;\n }\n}\n","/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\nimport { IUnicodeVersionProvider, UnicodeCharProperties, UnicodeCharWidth } from '../services/Services';\nimport { UnicodeService } from '../services/UnicodeService';\n\nconst BMP_COMBINING = [\n [0x0300, 0x036F], [0x0483, 0x0486], [0x0488, 0x0489],\n [0x0591, 0x05BD], [0x05BF, 0x05BF], [0x05C1, 0x05C2],\n [0x05C4, 0x05C5], [0x05C7, 0x05C7], [0x0600, 0x0603],\n [0x0610, 0x0615], [0x064B, 0x065E], [0x0670, 0x0670],\n [0x06D6, 0x06E4], [0x06E7, 0x06E8], [0x06EA, 0x06ED],\n [0x070F, 0x070F], [0x0711, 0x0711], [0x0730, 0x074A],\n [0x07A6, 0x07B0], [0x07EB, 0x07F3], [0x0901, 0x0902],\n [0x093C, 0x093C], [0x0941, 0x0948], [0x094D, 0x094D],\n [0x0951, 0x0954], [0x0962, 0x0963], [0x0981, 0x0981],\n [0x09BC, 0x09BC], [0x09C1, 0x09C4], [0x09CD, 0x09CD],\n [0x09E2, 0x09E3], [0x0A01, 0x0A02], [0x0A3C, 0x0A3C],\n [0x0A41, 0x0A42], [0x0A47, 0x0A48], [0x0A4B, 0x0A4D],\n [0x0A70, 0x0A71], [0x0A81, 0x0A82], [0x0ABC, 0x0ABC],\n [0x0AC1, 0x0AC5], [0x0AC7, 0x0AC8], [0x0ACD, 0x0ACD],\n [0x0AE2, 0x0AE3], [0x0B01, 0x0B01], [0x0B3C, 0x0B3C],\n [0x0B3F, 0x0B3F], [0x0B41, 0x0B43], [0x0B4D, 0x0B4D],\n [0x0B56, 0x0B56], [0x0B82, 0x0B82], [0x0BC0, 0x0BC0],\n [0x0BCD, 0x0BCD], [0x0C3E, 0x0C40], [0x0C46, 0x0C48],\n [0x0C4A, 0x0C4D], [0x0C55, 0x0C56], [0x0CBC, 0x0CBC],\n [0x0CBF, 0x0CBF], [0x0CC6, 0x0CC6], [0x0CCC, 0x0CCD],\n [0x0CE2, 0x0CE3], [0x0D41, 0x0D43], [0x0D4D, 0x0D4D],\n [0x0DCA, 0x0DCA], [0x0DD2, 0x0DD4], [0x0DD6, 0x0DD6],\n [0x0E31, 0x0E31], [0x0E34, 0x0E3A], [0x0E47, 0x0E4E],\n [0x0EB1, 0x0EB1], [0x0EB4, 0x0EB9], [0x0EBB, 0x0EBC],\n [0x0EC8, 0x0ECD], [0x0F18, 0x0F19], [0x0F35, 0x0F35],\n [0x0F37, 0x0F37], [0x0F39, 0x0F39], [0x0F71, 0x0F7E],\n [0x0F80, 0x0F84], [0x0F86, 0x0F87], [0x0F90, 0x0F97],\n [0x0F99, 0x0FBC], [0x0FC6, 0x0FC6], [0x102D, 0x1030],\n [0x1032, 0x1032], [0x1036, 0x1037], [0x1039, 0x1039],\n [0x1058, 0x1059], [0x1160, 0x11FF], [0x135F, 0x135F],\n [0x1712, 0x1714], [0x1732, 0x1734], [0x1752, 0x1753],\n [0x1772, 0x1773], [0x17B4, 0x17B5], [0x17B7, 0x17BD],\n [0x17C6, 0x17C6], [0x17C9, 0x17D3], [0x17DD, 0x17DD],\n [0x180B, 0x180D], [0x18A9, 0x18A9], [0x1920, 0x1922],\n [0x1927, 0x1928], [0x1932, 0x1932], [0x1939, 0x193B],\n [0x1A17, 0x1A18], [0x1B00, 0x1B03], [0x1B34, 0x1B34],\n [0x1B36, 0x1B3A], [0x1B3C, 0x1B3C], [0x1B42, 0x1B42],\n [0x1B6B, 0x1B73], [0x1DC0, 0x1DCA], [0x1DFE, 0x1DFF],\n [0x200B, 0x200F], [0x202A, 0x202E], [0x2060, 0x2063],\n [0x206A, 0x206F], [0x20D0, 0x20EF], [0x302A, 0x302F],\n [0x3099, 0x309A], [0xA806, 0xA806], [0xA80B, 0xA80B],\n [0xA825, 0xA826], [0xFB1E, 0xFB1E], [0xFE00, 0xFE0F],\n [0xFE20, 0xFE23], [0xFEFF, 0xFEFF], [0xFFF9, 0xFFFB]\n];\nconst HIGH_COMBINING = [\n [0x10A01, 0x10A03], [0x10A05, 0x10A06], [0x10A0C, 0x10A0F],\n [0x10A38, 0x10A3A], [0x10A3F, 0x10A3F], [0x1D167, 0x1D169],\n [0x1D173, 0x1D182], [0x1D185, 0x1D18B], [0x1D1AA, 0x1D1AD],\n [0x1D242, 0x1D244], [0xE0001, 0xE0001], [0xE0020, 0xE007F],\n [0xE0100, 0xE01EF]\n];\n\n// BMP lookup table, lazy initialized during first addon loading\nlet table: Uint8Array;\n\nfunction bisearch(ucs: number, data: number[][]): boolean {\n let min = 0;\n let max = data.length - 1;\n let mid;\n if (ucs < data[0][0] || ucs > data[max][1]) {\n return false;\n }\n while (max >= min) {\n mid = (min + max) >> 1;\n if (ucs > data[mid][1]) {\n min = mid + 1;\n } else if (ucs < data[mid][0]) {\n max = mid - 1;\n } else {\n return true;\n }\n }\n return false;\n}\n\nexport class UnicodeV6 implements IUnicodeVersionProvider {\n public readonly version = '6';\n\n constructor() {\n // init lookup table once\n if (!table) {\n table = new Uint8Array(65536);\n table.fill(1);\n table[0] = 0;\n // control chars\n table.fill(0, 1, 32);\n table.fill(0, 0x7f, 0xa0);\n\n // apply wide char rules first\n // wide chars\n table.fill(2, 0x1100, 0x1160);\n table[0x2329] = 2;\n table[0x232a] = 2;\n table.fill(2, 0x2e80, 0xa4d0);\n table[0x303f] = 1; // wrongly in last line\n\n table.fill(2, 0xac00, 0xd7a4);\n table.fill(2, 0xf900, 0xfb00);\n table.fill(2, 0xfe10, 0xfe1a);\n table.fill(2, 0xfe30, 0xfe70);\n table.fill(2, 0xff00, 0xff61);\n table.fill(2, 0xffe0, 0xffe7);\n\n // apply combining last to ensure we overwrite\n // wrongly wide set chars:\n // the original algo evals combining first and falls\n // through to wide check so we simply do here the opposite\n // combining 0\n for (let r = 0; r < BMP_COMBINING.length; ++r) {\n table.fill(0, BMP_COMBINING[r][0], BMP_COMBINING[r][1] + 1);\n }\n }\n }\n\n public wcwidth(num: number): UnicodeCharWidth {\n if (num < 32) return 0;\n if (num < 127) return 1;\n if (num < 65536) return table[num] as UnicodeCharWidth;\n if (bisearch(num, HIGH_COMBINING)) return 0;\n if ((num >= 0x20000 && num <= 0x2fffd) || (num >= 0x30000 && num <= 0x3fffd)) return 2;\n return 1;\n }\n\n public charProperties(codepoint: number, preceding: UnicodeCharProperties): UnicodeCharProperties {\n let width = this.wcwidth(codepoint);\n let shouldJoin = width === 0 && preceding !== 0;\n // HACK: Ideally this file would not depend on the service which uses it\n if (shouldJoin) {\n const oldWidth = UnicodeService.extractWidth(preceding);\n if (oldWidth === 0) {\n shouldJoin = false;\n } else if (oldWidth > width) {\n width = oldWidth;\n }\n }\n return UnicodeService.createPropertyValue(0, width, shouldJoin);\n }\n}\n","\n/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { TimeoutTimer } from '../Async';\nimport { Disposable, toDisposable } from '../Lifecycle';\nimport { Emitter } from '../Event';\n\nconst enum Constants {\n /**\n * Safety watermark to avoid memory exhaustion and browser engine crash on fast data input.\n * Enable flow control to avoid this limit and make sure that your backend correctly\n * propagates this to the underlying pty. (see docs for further instructions)\n * Since this limit is meant as a safety parachute to prevent browser crashs,\n * it is set to a very high number. Typically xterm.js gets unresponsive with\n * a 100 times lower number (>500 kB).\n */\n DISCARD_WATERMARK = 50000000, // ~50 MB\n /**\n * The max number of ms to spend on writes before allowing the renderer to\n * catch up with a 0ms setTimeout. A value of < 33 to keep us close to\n * 30fps, and a value of < 16 to try to run at 60fps. Of course, the real FPS\n * depends on the time it takes for the renderer to draw the frame.\n */\n WRITE_TIMEOUT_MS = 12,\n /**\n * Threshold of max held chunks in the write buffer, that were already processed.\n * This is a tradeoff between extensive write buffer shifts (bad runtime) and high\n * memory consumption by data thats not used anymore.\n */\n WRITE_BUFFER_LENGTH_THRESHOLD = 50\n}\n\nexport class WriteBuffer extends Disposable {\n private _writeBuffer: (string | Uint8Array)[] = [];\n private _callbacks: ((() => void) | undefined)[] = [];\n private _pendingData = 0;\n private _bufferOffset = 0;\n private _isSyncWriting = false;\n private _syncCalls = 0;\n private _didUserInput = false;\n\n private readonly _innerWriteTimer = this._register(new TimeoutTimer());\n private readonly _onWriteParsed = this._register(new Emitter());\n public readonly onWriteParsed = this._onWriteParsed.event;\n\n constructor(private _action: (data: string | Uint8Array, promiseResult?: boolean) => void | Promise) {\n super();\n this._register(toDisposable(() => {\n this._writeBuffer.length = 0;\n this._callbacks.length = 0;\n this._pendingData = 0;\n this._bufferOffset = 0;\n }));\n }\n\n public handleUserInput(): void {\n this._didUserInput = true;\n }\n\n /**\n * Flushes all pending writes synchronously. This is useful when you need to\n * ensure all queued data is processed before performing an operation that\n * depends upon everything being parsed like resize.\n *\n * Note: This is unreliable with async parser handlers as it does not wait for\n * promises to resolve.\n */\n public flushSync(): void {\n if (this._store.isDisposed) {\n return;\n }\n // exit early if another sync write loop is active\n if (this._isSyncWriting) {\n return;\n }\n this._isSyncWriting = true;\n\n // Process all pending chunks synchronously\n let chunk: string | Uint8Array | undefined;\n let didProcess = false;\n while (chunk = this._writeBuffer.shift()) {\n didProcess = true;\n this._action(chunk);\n const cb = this._callbacks.shift();\n if (cb) cb();\n }\n\n // Reset buffer state\n this._pendingData = 0;\n this._bufferOffset = 0x7FFFFFFF;\n this._writeBuffer.length = 0;\n this._callbacks.length = 0;\n\n this._isSyncWriting = false;\n if (didProcess) {\n this._onWriteParsed.fire();\n }\n }\n\n /**\n * @deprecated Unreliable, to be removed soon.\n */\n public writeSync(data: string | Uint8Array, maxSubsequentCalls?: number): void {\n if (this._store.isDisposed) {\n return;\n }\n // stop writeSync recursions with maxSubsequentCalls argument\n // This is dangerous to use as it will lose the current data chunk\n // and return immediately.\n if (maxSubsequentCalls !== undefined && this._syncCalls > maxSubsequentCalls) {\n // comment next line if a whole loop block should only contain x `writeSync` calls\n // (total flat vs. deep nested limit)\n this._syncCalls = 0;\n return;\n }\n // append chunk to buffer\n this._pendingData += data.length;\n this._writeBuffer.push(data);\n this._callbacks.push(undefined);\n\n // increase recursion counter\n this._syncCalls++;\n // exit early if another writeSync loop is active\n if (this._isSyncWriting) {\n return;\n }\n this._isSyncWriting = true;\n\n // force sync processing on pending data chunks to avoid in-band data scrambling\n // does the same as innerWrite but without event loop\n // we have to do it here as single loop steps to not corrupt loop subject\n // by another writeSync call triggered from _action\n let chunk: string | Uint8Array | undefined;\n while (chunk = this._writeBuffer.shift()) {\n this._action(chunk);\n const cb = this._callbacks.shift();\n if (cb) cb();\n }\n // reset to avoid reprocessing of chunks with scheduled innerWrite call\n // stopping scheduled innerWrite by offset > length condition\n this._pendingData = 0;\n this._bufferOffset = 0x7FFFFFFF;\n\n // allow another writeSync to loop\n this._isSyncWriting = false;\n this._syncCalls = 0;\n }\n\n public write(data: string | Uint8Array, callback?: () => void): void {\n if (this._store.isDisposed) {\n return;\n }\n if (this._pendingData > Constants.DISCARD_WATERMARK) {\n throw new Error('write data discarded, use flow control to avoid losing data');\n }\n\n // schedule chunk processing for next event loop run\n if (!this._writeBuffer.length) {\n this._bufferOffset = 0;\n\n // If this is the first write call after the user has done some input,\n // parse it immediately to minimize input latency,\n // otherwise schedule for the next event\n if (this._didUserInput) {\n this._didUserInput = false;\n this._pendingData += data.length;\n this._writeBuffer.push(data);\n this._callbacks.push(callback);\n this._innerWrite();\n return;\n }\n\n this._scheduleInnerWrite();\n }\n\n this._pendingData += data.length;\n this._writeBuffer.push(data);\n this._callbacks.push(callback);\n }\n\n /**\n * Inner write call, that enters the sliced chunk processing by timing.\n *\n * `lastTime` indicates, when the last _innerWrite call had started.\n * It is used to aggregate async handler execution under a timeout constraint\n * effectively lowering the redrawing needs, schematically:\n *\n * macroTask _innerWrite:\n * if (performance.now() - (lastTime | 0) < Constants.WRITE_TIMEOUT_MS):\n * schedule microTask _innerWrite(lastTime)\n * else:\n * schedule macroTask _innerWrite(0)\n *\n * overall execution order on task queues:\n *\n * macrotasks: [...] --> _innerWrite(0) --> [...] --> screenUpdate --> [...]\n * m t: |\n * i a: [...]\n * c s: |\n * r k: while < timeout:\n * o s: _innerWrite(timeout)\n *\n * `promiseResult` depicts the promise resolve value of an async handler.\n * This value gets carried forward through all saved stack states of the\n * paused parser for proper continuation.\n *\n * Note, for pure sync code `lastTime` and `promiseResult` have no meaning.\n */\n private _scheduleInnerWrite(lastTime: number = 0, promiseResult: boolean = true): void {\n if (this._store.isDisposed) {\n return;\n }\n this._innerWriteTimer.cancelAndSet(() => this._innerWrite(lastTime, promiseResult), 0);\n }\n\n protected _innerWrite(lastTime: number = 0, promiseResult: boolean = true): void {\n if (this._store.isDisposed) {\n return;\n }\n const startTime = lastTime || performance.now();\n while (this._writeBuffer.length > this._bufferOffset) {\n const data = this._writeBuffer[this._bufferOffset];\n const result = this._action(data, promiseResult);\n if (result) {\n /**\n * If we get a promise as return value, we re-schedule the continuation\n * as thenable on the promise and exit right away.\n *\n * The exit here means, that we block input processing at the current active chunk,\n * the exact execution position within the chunk is preserved by the saved\n * stack content in InputHandler and EscapeSequenceParser.\n *\n * Resuming happens automatically from that saved stack state.\n * Also the resolved promise value is passed along the callstack to\n * `EscapeSequenceParser.parse` to correctly resume the stopped handler loop.\n *\n * Exceptions on async handlers will be logged to console async, but do not interrupt\n * the input processing (continues with next handler at the current input position).\n */\n\n /**\n * If a promise takes long to resolve, we should schedule continuation behind setTimeout.\n * This might already be too late, if our .then enters really late (executor + prev thens\n * took very long). This cannot be solved here for the handler itself (it is the handlers\n * responsibility to slice hard work), but we can at least schedule a screen update as we\n * gain control.\n */\n const continuation: (r: boolean) => void = (r: boolean) => {\n if (this._store.isDisposed) {\n return;\n }\n if (performance.now() - startTime >= Constants.WRITE_TIMEOUT_MS) {\n this._scheduleInnerWrite(0, r);\n } else {\n this._innerWrite(startTime, r);\n }\n };\n\n /**\n * Optimization considerations:\n * The continuation above favors FPS over throughput by eval'ing `startTime` on resolve.\n * This might schedule too many screen updates with bad throughput drops (in case a slow\n * resolving handler sliced its work properly behind setTimeout calls). We cannot spot\n * this condition here, also the renderer has no way to spot nonsense updates either.\n * FIXME: A proper fix for this would track the FPS at the renderer entry level separately.\n *\n * If favoring of FPS shows bad throughput impact, use the following instead. It favors\n * throughput by eval'ing `startTime` upfront pulling at least one more chunk into the\n * current microtask queue (executed before setTimeout).\n */\n // const continuation: (r: boolean) => void = performance.now() - startTime >=\n // Constants.WRITE_TIMEOUT_MS\n // ? r => setTimeout(() => this._innerWrite(0, r))\n // : r => this._innerWrite(startTime, r);\n\n // Handle exceptions synchronously to current band position, idea:\n // 1. spawn a single microtask which we allow to throw hard\n // 2. spawn a promise immediately resolving to `true`\n // (executed on the same queue, thus properly aligned before continuation happens)\n result.catch(err => {\n queueMicrotask(() => {throw err;});\n return Promise.resolve(false);\n }).then(continuation);\n return;\n }\n\n const cb = this._callbacks[this._bufferOffset];\n if (cb) cb();\n this._bufferOffset++;\n this._pendingData -= data.length;\n\n if (performance.now() - startTime >= Constants.WRITE_TIMEOUT_MS) {\n break;\n }\n }\n if (this._writeBuffer.length > this._bufferOffset) {\n // Allow renderer to catch up before processing the next batch\n // trim already processed chunks if we are above threshold\n if (this._bufferOffset > Constants.WRITE_BUFFER_LENGTH_THRESHOLD) {\n this._writeBuffer = this._writeBuffer.slice(this._bufferOffset);\n this._callbacks = this._callbacks.slice(this._bufferOffset);\n this._bufferOffset = 0;\n }\n this._scheduleInnerWrite();\n } else {\n this._writeBuffer.length = 0;\n this._callbacks.length = 0;\n this._pendingData = 0;\n this._bufferOffset = 0;\n }\n this._onWriteParsed.fire();\n }\n}\n","/**\n * Copyright (c) 2021 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\n\n// 'rgb:' rule - matching: r/g/b | rr/gg/bb | rrr/ggg/bbb | rrrr/gggg/bbbb (hex digits)\nconst RGB_REX = /^([\\da-f])\\/([\\da-f])\\/([\\da-f])$|^([\\da-f]{2})\\/([\\da-f]{2})\\/([\\da-f]{2})$|^([\\da-f]{3})\\/([\\da-f]{3})\\/([\\da-f]{3})$|^([\\da-f]{4})\\/([\\da-f]{4})\\/([\\da-f]{4})$/;\n// '#...' rule - matching any hex digits\nconst HASH_REX = /^[\\da-f]+$/;\n\n/**\n * Parse color spec to RGB values (8 bit per channel).\n * See `man xparsecolor` for details about certain format specifications.\n *\n * Supported formats:\n * - rgb:// with , , in h | hh | hhh | hhhh\n * - #RGB, #RRGGBB, #RRRGGGBBB, #RRRRGGGGBBBB\n *\n * All other formats like rgbi: or device-independent string specifications\n * with float numbering are not supported.\n */\nexport function parseColor(data: string): [number, number, number] | undefined {\n if (!data) return;\n // also handle uppercases\n let low = data.toLowerCase();\n if (low.startsWith('rgb:')) {\n // 'rgb:' specifier\n low = low.slice(4);\n const m = RGB_REX.exec(low);\n if (m) {\n const base = m[1] ? 15 : m[4] ? 255 : m[7] ? 4095 : 65535;\n return [\n Math.round(parseInt(m[1] || m[4] || m[7] || m[10], 16) / base * 255),\n Math.round(parseInt(m[2] || m[5] || m[8] || m[11], 16) / base * 255),\n Math.round(parseInt(m[3] || m[6] || m[9] || m[12], 16) / base * 255)\n ];\n }\n } else if (low.startsWith('#')) {\n // '#' specifier\n low = low.slice(1);\n if (HASH_REX.exec(low) && [3, 6, 9, 12].includes(low.length)) {\n const adv = low.length / 3;\n const result: [number, number, number] = [0, 0, 0];\n for (let i = 0; i < 3; ++i) {\n const c = parseInt(low.slice(adv * i, adv * i + adv), 16);\n result[i] = adv === 1 ? c << 4 : adv === 2 ? c : adv === 3 ? c >> 4 : c >> 8;\n }\n return result;\n }\n }\n\n // Named colors are currently not supported due to the large addition to the xterm.js bundle size\n // they would add. In order to support named colors, we would need some way of optionally loading\n // additional payloads so startup/download time is not bloated (see #3530).\n}\n\n// pad hex output to requested bit width\nfunction pad(n: number, bits: number): string {\n const s = n.toString(16);\n const s2 = s.length < 2 ? '0' + s : s;\n switch (bits) {\n case 4:\n return s[0];\n case 8:\n return s2;\n case 12:\n return (s2 + s2).slice(0, 3);\n default:\n return s2 + s2;\n }\n}\n\n/**\n * Convert a given color to rgb:../../.. string of `bits` depth.\n */\nexport function toRgbString(color: [number, number, number], bits: number = 16): string {\n const [r, g, b] = color;\n return `rgb:${pad(r, bits)}/${pad(g, bits)}/${pad(b, bits)}`;\n}\n","/**\n * Copyright (c) 2025 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { IApcHandler, IHandlerCollection, ApcFallbackHandlerType, IApcParser, ISubParserStackState } from './Types';\nimport { ParserConstants } from './Constants';\nimport { utf32ToString } from '../input/TextDecoder';\nimport { IDisposable } from '../Types';\nimport { LimitedStringBuilder } from '../StringBuilder';\n\nconst EMPTY_HANDLERS: IApcHandler[] = [];\n\n/**\n * APC Parser for handling Application Program Command sequences.\n * APC sequences use the format: ESC _ ESC \\\n *\n * Unlike OSC which uses numeric identifiers (e.g., OSC 1337),\n * APC uses the first character as the identifier (e.g., 'G' for Kitty graphics).\n * The identifier is the character code of the first byte after ESC _.\n */\nexport class ApcParser implements IApcParser {\n private _handlers: IHandlerCollection = Object.create(null);\n private _active = EMPTY_HANDLERS;\n private _ident: number = 0;\n private _handlerFb: ApcFallbackHandlerType = () => { };\n private _stack: ISubParserStackState = {\n paused: false,\n loopPosition: 0,\n fallThrough: false\n };\n\n /**\n * Register an APC handler for a specific identifier.\n * @param ident The character code of the first byte (e.g., 0x47 for 'G')\n * @param handler The handler to register\n */\n public registerHandler(ident: number, handler: IApcHandler): IDisposable {\n this._handlers[ident] ??= [];\n const handlerList = this._handlers[ident];\n handlerList.push(handler);\n return {\n dispose: () => {\n const handlerIndex = handlerList.indexOf(handler);\n if (handlerIndex !== -1) {\n handlerList.splice(handlerIndex, 1);\n }\n }\n };\n }\n\n public clearHandler(ident: number): void {\n if (this._handlers[ident]) delete this._handlers[ident];\n }\n\n public setHandlerFallback(handler: ApcFallbackHandlerType): void {\n this._handlerFb = handler;\n }\n\n public dispose(): void {\n this._handlers = Object.create(null);\n this._handlerFb = () => { };\n this._active = EMPTY_HANDLERS;\n }\n\n public reset(): void {\n // force cleanup handlers\n if (this._active.length) {\n for (let j = this._stack.paused ? this._stack.loopPosition - 1 : this._active.length - 1; j >= 0; --j) {\n this._active[j].end(false);\n }\n }\n this._stack.paused = false;\n this._active = EMPTY_HANDLERS;\n this._ident = 0;\n }\n\n public start(ident: number): void {\n // always reset leftover handlers\n this.reset();\n this._ident = ident;\n this._active = this._handlers[ident] || EMPTY_HANDLERS;\n if (!this._active.length) {\n this._handlerFb(this._ident, 'START');\n } else {\n for (let j = this._active.length - 1; j >= 0; j--) {\n this._active[j].start();\n }\n }\n }\n\n public put(data: Uint32Array, start: number, end: number): void {\n if (!this._active.length) {\n this._handlerFb(this._ident, 'PUT', utf32ToString(data, start, end));\n } else {\n for (let j = this._active.length - 1; j >= 0; j--) {\n this._active[j].put(data, start, end);\n }\n }\n }\n\n /**\n * Indicates end of an APC command.\n * Whether the APC got aborted or finished normally\n * is indicated by `success`.\n */\n public end(success: boolean, promiseResult: boolean = true): void | Promise {\n if (!this._active.length) {\n this._handlerFb(this._ident, 'END', success);\n } else {\n let handlerResult: boolean | Promise = false;\n let j = this._active.length - 1;\n let fallThrough = false;\n if (this._stack.paused) {\n j = this._stack.loopPosition - 1;\n handlerResult = promiseResult;\n fallThrough = this._stack.fallThrough;\n this._stack.paused = false;\n }\n if (!fallThrough && handlerResult === false) {\n for (; j >= 0; j--) {\n handlerResult = this._active[j].end(success);\n if (handlerResult === true) {\n break;\n } else if (handlerResult instanceof Promise) {\n this._stack.paused = true;\n this._stack.loopPosition = j;\n this._stack.fallThrough = false;\n return handlerResult;\n }\n }\n j--;\n }\n // cleanup left over handlers (fallThrough for async)\n for (; j >= 0; j--) {\n handlerResult = this._active[j].end(false);\n if (handlerResult instanceof Promise) {\n this._stack.paused = true;\n this._stack.loopPosition = j;\n this._stack.fallThrough = true;\n return handlerResult;\n }\n }\n }\n this._active = EMPTY_HANDLERS;\n this._ident = 0;\n }\n}\n\n/**\n * Convenient class to allow attaching string based handler functions\n * as APC handlers.\n */\nexport class ApcHandler implements IApcHandler {\n private static _payloadLimit = ParserConstants.PAYLOAD_LIMIT;\n\n private _data = new LimitedStringBuilder(ApcHandler._payloadLimit);\n private _hitLimit: boolean = false;\n\n constructor(private _handler: (data: string) => boolean | Promise) { }\n\n public start(): void {\n this._data.reset();\n this._hitLimit = false;\n }\n\n public put(data: Uint32Array, start: number, end: number): void {\n if (this._hitLimit) {\n return;\n }\n if (this._data.append(utf32ToString(data, start, end))) {\n this._hitLimit = true;\n }\n }\n\n public end(success: boolean): boolean | Promise {\n let ret: boolean | Promise = false;\n if (this._hitLimit) {\n ret = false;\n } else if (success) {\n ret = this._handler(this._data.toString());\n if (ret instanceof Promise) {\n // need to hold data until `ret` got resolved\n // dont care for errors, data will be freed anyway on next start\n return ret.then(res => {\n this._data.reset();\n this._hitLimit = false;\n return res;\n });\n }\n }\n this._data.reset();\n this._hitLimit = false;\n return ret;\n }\n}\n","/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { IDisposable } from '../Types';\nimport { IDcsHandler, IParams, IHandlerCollection, IDcsParser, DcsFallbackHandlerType, ISubParserStackState } from './Types';\nimport { utf32ToString } from '../input/TextDecoder';\nimport { Params } from './Params';\nimport { ParserConstants } from './Constants';\nimport { LimitedStringBuilder } from '../StringBuilder';\n\nconst EMPTY_HANDLERS: IDcsHandler[] = [];\n\nexport class DcsParser implements IDcsParser {\n private _handlers: IHandlerCollection = Object.create(null);\n private _active: IDcsHandler[] = EMPTY_HANDLERS;\n private _ident: number = 0;\n private _handlerFb: DcsFallbackHandlerType = () => { };\n private _stack: ISubParserStackState = {\n paused: false,\n loopPosition: 0,\n fallThrough: false\n };\n\n public dispose(): void {\n this._handlers = Object.create(null);\n this._handlerFb = () => { };\n this._active = EMPTY_HANDLERS;\n }\n\n public registerHandler(ident: number, handler: IDcsHandler): IDisposable {\n this._handlers[ident] ??= [];\n const handlerList = this._handlers[ident];\n handlerList.push(handler);\n return {\n dispose: () => {\n const handlerIndex = handlerList.indexOf(handler);\n if (handlerIndex !== -1) {\n handlerList.splice(handlerIndex, 1);\n }\n }\n };\n }\n\n public clearHandler(ident: number): void {\n if (this._handlers[ident]) delete this._handlers[ident];\n }\n\n public setHandlerFallback(handler: DcsFallbackHandlerType): void {\n this._handlerFb = handler;\n }\n\n public reset(): void {\n // force cleanup leftover handlers\n if (this._active.length) {\n for (let j = this._stack.paused ? this._stack.loopPosition - 1 : this._active.length - 1; j >= 0; --j) {\n this._active[j].unhook(false);\n }\n }\n this._stack.paused = false;\n this._active = EMPTY_HANDLERS;\n this._ident = 0;\n }\n\n public hook(ident: number, params: IParams): void {\n // always reset leftover handlers\n this.reset();\n this._ident = ident;\n this._active = this._handlers[ident] || EMPTY_HANDLERS;\n if (!this._active.length) {\n this._handlerFb(this._ident, 'HOOK', params);\n } else {\n for (let j = this._active.length - 1; j >= 0; j--) {\n this._active[j].hook(params);\n }\n }\n }\n\n public put(data: Uint32Array, start: number, end: number): void {\n if (!this._active.length) {\n this._handlerFb(this._ident, 'PUT', utf32ToString(data, start, end));\n } else {\n for (let j = this._active.length - 1; j >= 0; j--) {\n this._active[j].put(data, start, end);\n }\n }\n }\n\n public unhook(success: boolean, promiseResult: boolean = true): void | Promise {\n if (!this._active.length) {\n this._handlerFb(this._ident, 'UNHOOK', success);\n } else {\n let handlerResult: boolean | Promise = false;\n let j = this._active.length - 1;\n let fallThrough = false;\n if (this._stack.paused) {\n j = this._stack.loopPosition - 1;\n handlerResult = promiseResult;\n fallThrough = this._stack.fallThrough;\n this._stack.paused = false;\n }\n if (!fallThrough && handlerResult === false) {\n for (; j >= 0; j--) {\n handlerResult = this._active[j].unhook(success);\n if (handlerResult === true) {\n break;\n } else if (handlerResult instanceof Promise) {\n this._stack.paused = true;\n this._stack.loopPosition = j;\n this._stack.fallThrough = false;\n return handlerResult;\n }\n }\n j--;\n }\n // cleanup left over handlers (fallThrough for async)\n for (; j >= 0; j--) {\n handlerResult = this._active[j].unhook(false);\n if (handlerResult instanceof Promise) {\n this._stack.paused = true;\n this._stack.loopPosition = j;\n this._stack.fallThrough = true;\n return handlerResult;\n }\n }\n }\n this._active = EMPTY_HANDLERS;\n this._ident = 0;\n }\n}\n\n// predefine empty params as [0] (ZDM)\nconst EMPTY_PARAMS = new Params();\nEMPTY_PARAMS.addParam(0);\n\n/**\n * Convenient class to create a DCS handler from a single callback function.\n * Note: The payload is currently limited to 50 MB (hardcoded).\n */\nexport class DcsHandler implements IDcsHandler {\n private static _payloadLimit = ParserConstants.PAYLOAD_LIMIT;\n\n private _data = new LimitedStringBuilder(DcsHandler._payloadLimit);\n private _params: IParams = EMPTY_PARAMS;\n private _hitLimit: boolean = false;\n\n constructor(private _handler: (data: string, params: IParams) => boolean | Promise) { }\n\n public hook(params: IParams): void {\n // since we need to preserve params until `unhook`, we have to clone it\n // (only borrowed from parser and spans multiple parser states)\n // perf optimization:\n // clone only, if we have non empty params, otherwise stick with default\n this._params = (params.length > 1 || params.params[0]) ? params.clone() : EMPTY_PARAMS;\n this._data.reset();\n this._hitLimit = false;\n }\n\n public put(data: Uint32Array, start: number, end: number): void {\n if (this._hitLimit) {\n return;\n }\n if (this._data.append(utf32ToString(data, start, end))) {\n this._hitLimit = true;\n }\n }\n\n public unhook(success: boolean): boolean | Promise {\n let ret: boolean | Promise = false;\n if (this._hitLimit) {\n ret = false;\n } else if (success) {\n ret = this._handler(this._data.toString(), this._params);\n if (ret instanceof Promise) {\n // need to hold data and params until `ret` got resolved\n // dont care for errors, data will be freed anyway on next start\n return ret.then(res => {\n this._params = EMPTY_PARAMS;\n this._data.reset();\n this._hitLimit = false;\n return res;\n });\n }\n }\n this._params = EMPTY_PARAMS;\n this._data.reset();\n this._hitLimit = false;\n return ret;\n }\n}\n","/**\n * Copyright (c) 2018 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandlerType, OscFallbackHandlerType, IOscParser, EscHandlerType, IDcsParser, DcsFallbackHandlerType, IFunctionIdentifier, ExecuteFallbackHandlerType, CsiFallbackHandlerType, EscFallbackHandlerType, PrintHandlerType, PrintFallbackHandlerType, ExecuteHandlerType, IParserStackState, ParserStackType, ResumableHandlersType, IApcHandler, IApcParser, ApcFallbackHandlerType } from './Types';\nimport { ParserState, ParserAction } from './Constants';\nimport { Disposable, toDisposable } from '../Lifecycle';\nimport { IDisposable } from '../Types';\nimport { Params } from './Params';\nimport { OscParser } from './OscParser';\nimport { DcsParser } from './DcsParser';\nimport { ApcParser } from './ApcParser';\n\n/**\n * VT commands done by the parser\n */\n// @vt: #Y ESC CSI \"Control Sequence Introducer\" \"ESC [\" \"Start of a CSI sequence.\"\n// @vt: #Y ESC OSC \"Operating System Command\" \"ESC ]\" \"Start of an OSC sequence.\"\n// @vt: #Y ESC DCS \"Device Control String\" \"ESC P\" \"Start of a DCS sequence.\"\n// @vt: #Y ESC ST \"String Terminator\" \"ESC \\\\\" \"Terminator used for string type sequences.\"\n// @vt: #Y ESC PM \"Privacy Message\" \"ESC ^\" \"Start of a privacy message.\"\n// @vt: #Y ESC APC \"Application Program Command\" \"ESC _\" \"Start of an APC sequence.\"\n// @vt: #Y C1 CSI \"Control Sequence Introducer\" \"\\x9B\" \"Start of a CSI sequence.\"\n// @vt: #Y C1 OSC \"Operating System Command\" \"\\x9D\" \"Start of an OSC sequence.\"\n// @vt: #Y C1 DCS \"Device Control String\" \"\\x90\" \"Start of a DCS sequence.\"\n// @vt: #Y C1 ST \"String Terminator\" \"\\x9C\" \"Terminator used for string type sequences.\"\n// @vt: #Y C1 PM \"Privacy Message\" \"\\x9E\" \"Start of a privacy message.\"\n// @vt: #Y C1 APC \"Application Program Command\" \"\\x9F\" \"Start of an APC sequence.\"\n// @vt: #Y C0 NUL \"Null\" \"\\0, \\x00\" \"NUL is ignored.\"\n// @vt: #Y C0 ESC \"Escape\" \"\\e, \\x1B\" \"Start of a sequence. Cancels any other sequence.\"\n\n/**\n * Table values are generated like this:\n * index: currentState << TableValue.INDEX_STATE_SHIFT | charCode\n * value: action << TableValue.TRANSITION_ACTION_SHIFT | nextState\n */\nconst enum TableAccess {\n TRANSITION_ACTION_SHIFT = 8,\n TRANSITION_STATE_MASK = 255,\n INDEX_STATE_SHIFT = 8\n}\n\n/**\n * Transition table for EscapeSequenceParser.\n */\nexport class TransitionTable {\n public table: Uint16Array;\n\n constructor(length: number) {\n this.table = new Uint16Array(length);\n }\n\n /**\n * Set default transition.\n * @param action default action\n * @param next default next state\n */\n public setDefault(action: ParserAction, next: ParserState): void {\n this.table.fill(action << TableAccess.TRANSITION_ACTION_SHIFT | next);\n }\n\n /**\n * Add a transition to the transition table.\n * @param code input character code\n * @param state current parser state\n * @param action parser action to be done\n * @param next next parser state\n */\n public add(code: number, state: ParserState, action: ParserAction, next: ParserState): void {\n this.table[state << TableAccess.INDEX_STATE_SHIFT | code] = action << TableAccess.TRANSITION_ACTION_SHIFT | next;\n }\n\n /**\n * Add transitions for multiple input character codes.\n * @param codes input character code array\n * @param state current parser state\n * @param action parser action to be done\n * @param next next parser state\n */\n public addMany(codes: number[], state: ParserState, action: ParserAction, next: ParserState): void {\n for (let i = 0; i < codes.length; i++) {\n this.table[state << TableAccess.INDEX_STATE_SHIFT | codes[i]] = action << TableAccess.TRANSITION_ACTION_SHIFT | next;\n }\n }\n}\n\n\n// Pseudo-character placeholder for printable non-ascii characters (unicode).\nconst NON_ASCII_PRINTABLE = 0xA0;\n\n\n/**\n * VT500 compatible transition table.\n * Taken from https://vt100.net/emu/dec_ansi_parser.\n */\nexport const VT500_TRANSITION_TABLE = (function (): TransitionTable {\n // table size:\n // (ParserState.STATE_LENGTH - 1) << TableAccess.INDEX_STATE_SHIFT | NON_ASCII_PRINTABLE + 1\n const table: TransitionTable = new TransitionTable(4257);\n\n // range macro for byte\n const BYTE_VALUES = 256;\n const blueprint = Array.apply(null, Array(BYTE_VALUES)).map((unused: any, i: number) => i);\n const r = (start: number, end: number): number[] => blueprint.slice(start, end);\n\n // Default definitions.\n const PRINTABLES = r(0x20, 0x7f); // 0x20 (SP) included, 0x7F (DEL) excluded\n const EXECUTABLES = r(0x00, 0x18);\n EXECUTABLES.push(0x19);\n EXECUTABLES.push.apply(EXECUTABLES, r(0x1c, 0x20));\n\n const states: number[] = r(ParserState.GROUND, ParserState.STATE_LENGTH);\n\n // set default transition\n table.setDefault(ParserAction.ERROR, ParserState.GROUND);\n // printables\n table.addMany(PRINTABLES, ParserState.GROUND, ParserAction.PRINT, ParserState.GROUND);\n // global anywhere rules\n for (const state of states) {\n table.addMany([0x18, 0x1a, 0x99, 0x9a], state, ParserAction.EXECUTE, ParserState.GROUND);\n table.addMany(r(0x80, 0x90), state, ParserAction.EXECUTE, ParserState.GROUND);\n table.addMany(r(0x90, 0x98), state, ParserAction.EXECUTE, ParserState.GROUND);\n table.add(0x9c, state, ParserAction.IGNORE, ParserState.GROUND); // ST as terminator\n table.add(0x1b, state, ParserAction.CLEAR, ParserState.ESCAPE); // ESC\n table.add(0x9d, state, ParserAction.OSC_START, ParserState.OSC_STRING); // OSC\n table.addMany([0x98, 0x9e], state, ParserAction.IGNORE, ParserState.SOS_PM_STRING); // SOS, PM\n table.add(0x9f, state, ParserAction.CLEAR, ParserState.APC_ENTRY); // APC\n table.add(0x9b, state, ParserAction.CLEAR, ParserState.CSI_ENTRY); // CSI\n table.add(0x90, state, ParserAction.CLEAR, ParserState.DCS_ENTRY); // DCS\n }\n // rules for executables and 7f\n table.addMany(EXECUTABLES, ParserState.GROUND, ParserAction.EXECUTE, ParserState.GROUND);\n table.addMany(EXECUTABLES, ParserState.ESCAPE, ParserAction.EXECUTE, ParserState.ESCAPE);\n table.add(0x7f, ParserState.ESCAPE, ParserAction.IGNORE, ParserState.ESCAPE);\n table.addMany(EXECUTABLES, ParserState.OSC_STRING, ParserAction.IGNORE, ParserState.OSC_STRING);\n table.addMany(EXECUTABLES, ParserState.CSI_ENTRY, ParserAction.EXECUTE, ParserState.CSI_ENTRY);\n table.add(0x7f, ParserState.CSI_ENTRY, ParserAction.IGNORE, ParserState.CSI_ENTRY);\n table.addMany(EXECUTABLES, ParserState.CSI_PARAM, ParserAction.EXECUTE, ParserState.CSI_PARAM);\n table.add(0x7f, ParserState.CSI_PARAM, ParserAction.IGNORE, ParserState.CSI_PARAM);\n table.addMany(EXECUTABLES, ParserState.CSI_IGNORE, ParserAction.EXECUTE, ParserState.CSI_IGNORE);\n table.addMany(EXECUTABLES, ParserState.CSI_INTERMEDIATE, ParserAction.EXECUTE, ParserState.CSI_INTERMEDIATE);\n table.add(0x7f, ParserState.CSI_INTERMEDIATE, ParserAction.IGNORE, ParserState.CSI_INTERMEDIATE);\n table.addMany(EXECUTABLES, ParserState.ESCAPE_INTERMEDIATE, ParserAction.EXECUTE, ParserState.ESCAPE_INTERMEDIATE);\n table.add(0x7f, ParserState.ESCAPE_INTERMEDIATE, ParserAction.IGNORE, ParserState.ESCAPE_INTERMEDIATE);\n // osc\n table.add(0x5d, ParserState.ESCAPE, ParserAction.OSC_START, ParserState.OSC_STRING);\n table.addMany(PRINTABLES, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING);\n table.add(0x7f, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING);\n table.addMany([0x9c, 0x1b, 0x18, 0x1a, 0x07], ParserState.OSC_STRING, ParserAction.OSC_END, ParserState.GROUND);\n table.addMany(r(0x1c, 0x20), ParserState.OSC_STRING, ParserAction.IGNORE, ParserState.OSC_STRING);\n // sos/pm\n table.addMany([0x58, 0x5e], ParserState.ESCAPE, ParserAction.IGNORE, ParserState.SOS_PM_STRING);\n table.addMany(PRINTABLES, ParserState.SOS_PM_STRING, ParserAction.IGNORE, ParserState.SOS_PM_STRING);\n table.addMany(EXECUTABLES, ParserState.SOS_PM_STRING, ParserAction.IGNORE, ParserState.SOS_PM_STRING);\n table.add(0x9c, ParserState.SOS_PM_STRING, ParserAction.IGNORE, ParserState.GROUND);\n table.add(0x7f, ParserState.SOS_PM_STRING, ParserAction.IGNORE, ParserState.SOS_PM_STRING);\n // apc\n table.add(0x5f, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.APC_ENTRY);\n table.addMany(EXECUTABLES, ParserState.APC_ENTRY, ParserAction.IGNORE, ParserState.APC_ENTRY);\n table.add(0x7f, ParserState.APC_ENTRY, ParserAction.IGNORE, ParserState.APC_ENTRY);\n table.addMany(r(0x20, 0x30), ParserState.APC_ENTRY, ParserAction.COLLECT, ParserState.APC_INTERMEDIATE);\n table.addMany(r(0x30, 0x7f), ParserState.APC_ENTRY, ParserAction.APC_START, ParserState.APC_PASSTHROUGH);\n table.addMany(r(0x30, 0x7f), ParserState.APC_INTERMEDIATE, ParserAction.APC_START, ParserState.APC_PASSTHROUGH);\n table.addMany(EXECUTABLES, ParserState.APC_INTERMEDIATE, ParserAction.IGNORE, ParserState.APC_INTERMEDIATE);\n table.addMany(r(0x20, 0x30), ParserState.APC_INTERMEDIATE, ParserAction.COLLECT, ParserState.APC_INTERMEDIATE);\n table.add(0x7f, ParserState.APC_INTERMEDIATE, ParserAction.IGNORE, ParserState.APC_INTERMEDIATE);\n table.addMany(PRINTABLES, ParserState.APC_PASSTHROUGH, ParserAction.APC_PUT, ParserState.APC_PASSTHROUGH);\n table.addMany(EXECUTABLES, ParserState.APC_PASSTHROUGH, ParserAction.IGNORE, ParserState.APC_PASSTHROUGH);\n table.addMany(r(0x08, 0x0e), ParserState.APC_PASSTHROUGH, ParserAction.APC_PUT, ParserState.APC_PASSTHROUGH);\n table.add(0x7f, ParserState.APC_PASSTHROUGH, ParserAction.IGNORE, ParserState.APC_PASSTHROUGH);\n table.addMany([0x1b, 0x9c, 0x18, 0x1a], ParserState.APC_PASSTHROUGH, ParserAction.APC_END, ParserState.GROUND);\n // csi entries\n table.add(0x5b, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.CSI_ENTRY);\n table.addMany(r(0x40, 0x7f), ParserState.CSI_ENTRY, ParserAction.CSI_DISPATCH, ParserState.GROUND);\n table.addMany(r(0x30, 0x3c), ParserState.CSI_ENTRY, ParserAction.PARAM, ParserState.CSI_PARAM);\n table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.CSI_ENTRY, ParserAction.COLLECT, ParserState.CSI_PARAM);\n table.addMany(r(0x30, 0x3c), ParserState.CSI_PARAM, ParserAction.PARAM, ParserState.CSI_PARAM);\n table.addMany(r(0x40, 0x7f), ParserState.CSI_PARAM, ParserAction.CSI_DISPATCH, ParserState.GROUND);\n table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.CSI_PARAM, ParserAction.IGNORE, ParserState.CSI_IGNORE);\n table.addMany(r(0x20, 0x40), ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE);\n table.add(0x7f, ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE);\n table.addMany(r(0x40, 0x7f), ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.GROUND);\n table.addMany(r(0x20, 0x30), ParserState.CSI_ENTRY, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE);\n table.addMany(r(0x20, 0x30), ParserState.CSI_INTERMEDIATE, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE);\n table.addMany(r(0x30, 0x40), ParserState.CSI_INTERMEDIATE, ParserAction.IGNORE, ParserState.CSI_IGNORE);\n table.addMany(r(0x40, 0x7f), ParserState.CSI_INTERMEDIATE, ParserAction.CSI_DISPATCH, ParserState.GROUND);\n table.addMany(r(0x20, 0x30), ParserState.CSI_PARAM, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE);\n // esc_intermediate\n table.addMany(r(0x20, 0x30), ParserState.ESCAPE, ParserAction.COLLECT, ParserState.ESCAPE_INTERMEDIATE);\n table.addMany(r(0x20, 0x30), ParserState.ESCAPE_INTERMEDIATE, ParserAction.COLLECT, ParserState.ESCAPE_INTERMEDIATE);\n table.addMany(r(0x30, 0x7f), ParserState.ESCAPE_INTERMEDIATE, ParserAction.ESC_DISPATCH, ParserState.GROUND);\n table.addMany(r(0x30, 0x50), ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND);\n table.addMany(r(0x51, 0x58), ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND);\n table.addMany([0x59, 0x5a, 0x5c], ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND);\n table.addMany(r(0x60, 0x7f), ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND);\n // dcs entry\n table.add(0x50, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.DCS_ENTRY);\n table.addMany(EXECUTABLES, ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY);\n table.add(0x7f, ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY);\n table.addMany(r(0x20, 0x30), ParserState.DCS_ENTRY, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE);\n table.addMany(r(0x30, 0x3c), ParserState.DCS_ENTRY, ParserAction.PARAM, ParserState.DCS_PARAM);\n table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.DCS_ENTRY, ParserAction.COLLECT, ParserState.DCS_PARAM);\n table.addMany(EXECUTABLES, ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE);\n table.addMany(r(0x20, 0x80), ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE);\n table.addMany(EXECUTABLES, ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM);\n table.add(0x7f, ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM);\n table.addMany(r(0x30, 0x3c), ParserState.DCS_PARAM, ParserAction.PARAM, ParserState.DCS_PARAM);\n table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_IGNORE);\n table.addMany(r(0x20, 0x30), ParserState.DCS_PARAM, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE);\n table.addMany(EXECUTABLES, ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE);\n table.add(0x7f, ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE);\n table.addMany(r(0x20, 0x30), ParserState.DCS_INTERMEDIATE, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE);\n table.addMany(r(0x30, 0x40), ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_IGNORE);\n table.addMany(r(0x40, 0x7f), ParserState.DCS_INTERMEDIATE, ParserAction.DCS_HOOK, ParserState.DCS_PASSTHROUGH);\n table.addMany(r(0x40, 0x7f), ParserState.DCS_PARAM, ParserAction.DCS_HOOK, ParserState.DCS_PASSTHROUGH);\n table.addMany(r(0x40, 0x7f), ParserState.DCS_ENTRY, ParserAction.DCS_HOOK, ParserState.DCS_PASSTHROUGH);\n table.addMany(EXECUTABLES, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH);\n table.addMany(PRINTABLES, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH);\n table.add(0x7f, ParserState.DCS_PASSTHROUGH, ParserAction.IGNORE, ParserState.DCS_PASSTHROUGH);\n table.addMany([0x1b, 0x9c, 0x18, 0x1a], ParserState.DCS_PASSTHROUGH, ParserAction.DCS_UNHOOK, ParserState.GROUND);\n // special handling of unicode chars\n table.add(NON_ASCII_PRINTABLE, ParserState.GROUND, ParserAction.PRINT, ParserState.GROUND);\n table.add(NON_ASCII_PRINTABLE, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING);\n table.add(NON_ASCII_PRINTABLE, ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE);\n table.add(NON_ASCII_PRINTABLE, ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE);\n table.add(NON_ASCII_PRINTABLE, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH);\n table.add(NON_ASCII_PRINTABLE, ParserState.APC_PASSTHROUGH, ParserAction.APC_PUT, ParserState.APC_PASSTHROUGH);\n return table;\n})();\n\n\n/**\n * EscapeSequenceParser.\n * This class implements the ANSI/DEC compatible parser described by\n * Paul Williams (https://vt100.net/emu/dec_ansi_parser).\n *\n * To implement custom ANSI compliant escape sequences it is not needed to\n * alter this parser, instead consider registering a custom handler.\n * For non ANSI compliant sequences change the transition table with\n * the optional `transitions` constructor argument and\n * reimplement the `parse` method.\n *\n * This parser is currently hardcoded to operate in ZDM (Zero Default Mode)\n * as suggested by the original parser, thus empty parameters are set to 0.\n * This is not in line with the latest ECMA-48 specification\n * (ZDM was part of the early specs and got completely removed later on).\n *\n * Other than the original parser from vt100.net this parser supports\n * sub parameters in digital parameters separated by colons. Empty sub parameters\n * are set to -1 (no ZDM for sub parameters).\n *\n * About prefix and intermediate bytes:\n * This parser follows the assumptions of the vt100.net parser with these restrictions:\n * - only one prefix byte is allowed as first parameter byte, byte range 0x3c .. 0x3f\n * - max. two intermediates are respected, byte range 0x20 .. 0x2f\n * Note that this is not in line with ECMA-48 which does not limit either of those.\n * Furthermore ECMA-48 allows the prefix byte range at any param byte position. Currently\n * there are no known sequences that follow the broader definition of the specification.\n *\n * TODO: implement error recovery hook via error handler return values\n */\nexport class EscapeSequenceParser extends Disposable implements IEscapeSequenceParser {\n public initialState: number;\n public currentState: number;\n public precedingJoinState: number; // UnicodeJoinProperties\n\n // buffers over several parse calls\n protected _params: Params;\n protected _collect: number;\n\n // handler lookup containers\n protected _printHandler: PrintHandlerType;\n protected _executeHandlers: { [flag: number]: ExecuteHandlerType };\n // fast path for EXE bytes < 0x18\n protected _executeHandlersArr: (ExecuteHandlerType | undefined)[];\n protected _csiHandlers: IHandlerCollection;\n protected _escHandlers: IHandlerCollection;\n protected readonly _oscParser: IOscParser;\n protected readonly _dcsParser: IDcsParser;\n protected readonly _apcParser: IApcParser;\n protected _errorHandler: (state: IParsingState) => IParsingState;\n\n // fallback handlers\n protected _printHandlerFb: PrintFallbackHandlerType;\n protected _executeHandlerFb: ExecuteFallbackHandlerType;\n protected _csiHandlerFb: CsiFallbackHandlerType;\n protected _escHandlerFb: EscFallbackHandlerType;\n protected _errorHandlerFb: (state: IParsingState) => IParsingState;\n\n // parser stack save for async handler support\n protected _parseStack: IParserStackState = {\n state: ParserStackType.NONE,\n handlers: [],\n handlerPos: 0,\n transition: 0,\n chunkPos: 0\n };\n\n constructor(\n protected readonly _transitions: TransitionTable = VT500_TRANSITION_TABLE\n ) {\n super();\n\n this.initialState = ParserState.GROUND;\n this.currentState = this.initialState;\n this._params = new Params(); // defaults to 32 storable params/subparams\n this._params.addParam(0); // ZDM\n this._collect = 0;\n this.precedingJoinState = 0;\n\n // set default fallback handlers and handler lookup containers\n this._printHandlerFb = (data, start, end): void => { };\n this._executeHandlerFb = (code: number): void => { };\n this._csiHandlerFb = (ident: number, params: IParams): void => { };\n this._escHandlerFb = (ident: number): void => { };\n this._errorHandlerFb = (state: IParsingState): IParsingState => state;\n this._printHandler = this._printHandlerFb;\n this._executeHandlers = Object.create(null);\n this._executeHandlersArr = new Array(0x18).fill(undefined);\n this._csiHandlers = Object.create(null);\n this._escHandlers = Object.create(null);\n this._register(toDisposable(() => {\n this._csiHandlers = Object.create(null);\n this._executeHandlers = Object.create(null);\n this._executeHandlersArr = new Array(0x18).fill(undefined);\n this._escHandlers = Object.create(null);\n }));\n this._oscParser = this._register(new OscParser());\n this._dcsParser = this._register(new DcsParser());\n this._apcParser = this._register(new ApcParser());\n this._errorHandler = this._errorHandlerFb;\n\n // swallow 7bit ST (ESC+\\)\n this.registerEscHandler({ final: '\\\\' }, () => true);\n }\n\n protected _identifier(id: IFunctionIdentifier, finalRange: number[] = [0x40, 0x7e]): number {\n let res = 0;\n if (id.prefix) {\n if (id.prefix.length > 1) {\n throw new Error('only one byte as prefix supported');\n }\n res = id.prefix.charCodeAt(0);\n if (res < 0x3c || res > 0x3f) {\n throw new Error('prefix must be in range 0x3c .. 0x3f');\n }\n }\n if (id.intermediates) {\n if (id.intermediates.length > 2) {\n throw new Error('only two bytes as intermediates are supported');\n }\n for (let i = 0; i < id.intermediates.length; ++i) {\n const intermediate = id.intermediates.charCodeAt(i);\n if (0x20 > intermediate || intermediate > 0x2f) {\n throw new Error('intermediate must be in range 0x20 .. 0x2f');\n }\n res <<= 8;\n res |= intermediate;\n }\n }\n if (id.final.length !== 1) {\n throw new Error('final must be a single byte');\n }\n const finalCode = id.final.charCodeAt(0);\n if (finalRange[0] > finalCode || finalCode > finalRange[1]) {\n throw new Error(`final must be in range ${finalRange[0]} .. ${finalRange[1]}`);\n }\n res <<= 8;\n res |= finalCode;\n\n return res;\n }\n\n public identToString(ident: number): string {\n const res: string[] = [];\n while (ident) {\n res.push(String.fromCharCode(ident & 0xFF));\n ident >>= 8;\n }\n return res.reverse().join('');\n }\n\n public setPrintHandler(handler: PrintHandlerType): void {\n this._printHandler = handler;\n }\n public clearPrintHandler(): void {\n this._printHandler = this._printHandlerFb;\n }\n\n public registerEscHandler(id: IFunctionIdentifier, handler: EscHandlerType): IDisposable {\n const ident = this._identifier(id, [0x30, 0x7e]);\n this._escHandlers[ident] ??= [];\n const handlerList = this._escHandlers[ident];\n handlerList.push(handler);\n return {\n dispose: () => {\n const handlerIndex = handlerList.indexOf(handler);\n if (handlerIndex !== -1) {\n handlerList.splice(handlerIndex, 1);\n }\n }\n };\n }\n public clearEscHandler(id: IFunctionIdentifier): void {\n if (this._escHandlers[this._identifier(id, [0x30, 0x7e])]) delete this._escHandlers[this._identifier(id, [0x30, 0x7e])];\n }\n public setEscHandlerFallback(handler: EscFallbackHandlerType): void {\n this._escHandlerFb = handler;\n }\n\n public setExecuteHandler(flag: string, handler: ExecuteHandlerType): void {\n const code = flag.charCodeAt(0);\n this._executeHandlers[code] = handler;\n if (code < 0x18) this._executeHandlersArr[code] = handler;\n }\n public clearExecuteHandler(flag: string): void {\n const code = flag.charCodeAt(0);\n if (this._executeHandlers[code]) delete this._executeHandlers[code];\n if (code < 0x18) this._executeHandlersArr[code] = undefined;\n }\n public setExecuteHandlerFallback(handler: ExecuteFallbackHandlerType): void {\n this._executeHandlerFb = handler;\n }\n\n public registerCsiHandler(id: IFunctionIdentifier, handler: CsiHandlerType): IDisposable {\n const ident = this._identifier(id);\n this._csiHandlers[ident] ??= [];\n const handlerList = this._csiHandlers[ident];\n handlerList.push(handler);\n return {\n dispose: () => {\n const handlerIndex = handlerList.indexOf(handler);\n if (handlerIndex !== -1) {\n handlerList.splice(handlerIndex, 1);\n }\n }\n };\n }\n public clearCsiHandler(id: IFunctionIdentifier): void {\n if (this._csiHandlers[this._identifier(id)]) delete this._csiHandlers[this._identifier(id)];\n }\n public setCsiHandlerFallback(callback: (ident: number, params: IParams) => void): void {\n this._csiHandlerFb = callback;\n }\n\n public registerDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): IDisposable {\n return this._dcsParser.registerHandler(this._identifier(id), handler);\n }\n public clearDcsHandler(id: IFunctionIdentifier): void {\n this._dcsParser.clearHandler(this._identifier(id));\n }\n public setDcsHandlerFallback(handler: DcsFallbackHandlerType): void {\n this._dcsParser.setHandlerFallback(handler);\n }\n\n public registerOscHandler(ident: number, handler: IOscHandler): IDisposable {\n return this._oscParser.registerHandler(ident, handler);\n }\n public clearOscHandler(ident: number): void {\n this._oscParser.clearHandler(ident);\n }\n public setOscHandlerFallback(handler: OscFallbackHandlerType): void {\n this._oscParser.setHandlerFallback(handler);\n }\n\n public registerApcHandler(id: IFunctionIdentifier, handler: IApcHandler): IDisposable {\n id.prefix = undefined; // APC does not support prefix byte\n return this._apcParser.registerHandler(this._identifier(id, [0x30, 0x7e]), handler);\n }\n public clearApcHandler(id: IFunctionIdentifier): void {\n id.prefix = undefined; // APC does not support prefix byte\n this._apcParser.clearHandler(this._identifier(id, [0x30, 0x7e]));\n }\n public setApcHandlerFallback(handler: ApcFallbackHandlerType): void {\n this._apcParser.setHandlerFallback(handler);\n }\n\n public setErrorHandler(callback: (state: IParsingState) => IParsingState): void {\n this._errorHandler = callback;\n }\n public clearErrorHandler(): void {\n this._errorHandler = this._errorHandlerFb;\n }\n\n /**\n * Reset parser to initial values.\n *\n * This can also be used to lift the improper continuation error condition\n * when dealing with async handlers. Use this only as a last resort to silence\n * that error when the terminal has no pending data to be processed. Note that\n * the interrupted async handler might continue its work in the future messing\n * up the terminal state even further.\n */\n public reset(): void {\n this.currentState = this.initialState;\n this._oscParser.reset();\n this._dcsParser.reset();\n this._apcParser.reset();\n this._params.resetZdm();\n this._collect = 0;\n this.precedingJoinState = 0;\n // abort pending continuation from async handler\n // Here the RESET type indicates, that the next parse call will\n // ignore any saved stack, instead continues sync with next codepoint from GROUND\n if (this._parseStack.state !== ParserStackType.NONE) {\n this._parseStack.state = ParserStackType.RESET;\n this._parseStack.handlers = []; // also release handlers ref\n }\n }\n\n /**\n * Async parse support.\n */\n protected _preserveStack(\n state: ParserStackType,\n handlers: ResumableHandlersType,\n handlerPos: number,\n transition: number,\n chunkPos: number\n ): void {\n this._parseStack.state = state;\n this._parseStack.handlers = handlers;\n this._parseStack.handlerPos = handlerPos;\n this._parseStack.transition = transition;\n this._parseStack.chunkPos = chunkPos;\n }\n\n /**\n * Parse UTF32 codepoints in `data` up to `length`.\n *\n * Note: For several actions with high data load the parsing is optimized\n * by using local read ahead loops with hardcoded conditions to\n * avoid costly table lookups. Make sure that any change of table values\n * will be reflected in the loop conditions as well and vice versa.\n * Affected states/actions:\n * - GROUND:PRINT\n * - CSI_PARAM:PARAM\n * - DCS_PARAM:PARAM\n * - OSC_STRING:OSC_PUT\n * - DCS_PASSTHROUGH:DCS_PUT\n *\n * Additionally the following fast paths exist before the table lookup:\n * - EXE bytes < 0x18 in non-payload states (avoids table lookup entirely)\n * - 7-bit CSI sequences without intermediates (ESC [ params final)\n *\n * Note on asynchronous handler support:\n * Any handler returning a promise will be treated as asynchronous.\n * To keep the in-band blocking working for async handlers, `parse` pauses execution,\n * creates a stack save and returns the promise to the caller.\n * For proper continuation of the paused state it is important\n * to await the promise resolving. On resolve the parse must be repeated\n * with the same chunk of data and the resolved value in `promiseResult`\n * until no promise is returned.\n *\n * Important: With only sync handlers defined, parsing is completely synchronous as well.\n * As soon as an async handler is involved, synchronous parsing is not possible anymore.\n *\n * Boilerplate for proper parsing of multiple chunks with async handlers:\n *\n * ```typescript\n * async function parseMultipleChunks(chunks: Uint32Array[]): Promise {\n * for (const chunk of chunks) {\n * let result: void | Promise;\n * let prev: boolean | undefined;\n * while (result = parser.parse(chunk, chunk.length, prev)) {\n * prev = await result;\n * }\n * }\n * // finished parsing all chunks...\n * }\n * ```\n */\n public parse(data: Uint32Array, length: number, promiseResult?: boolean): void | Promise {\n let code: number;\n let transition: number;\n let start = 0;\n let handlerResult: void | boolean | Promise;\n\n // resume from async handler\n if (this._parseStack.state) {\n // allow sync parser reset even in continuation mode\n // Note: can be used to recover parser from improper continuation error below\n if (this._parseStack.state === ParserStackType.RESET) {\n this._parseStack.state = ParserStackType.NONE;\n start = this._parseStack.chunkPos + 1; // continue with next codepoint in GROUND\n } else {\n if (promiseResult === undefined || this._parseStack.state === ParserStackType.FAIL) {\n /**\n * Reject further parsing on improper continuation after pausing. This is a really bad\n * condition with screwed up execution order and prolly messed up terminal state,\n * therefore we exit hard with an exception and reject any further parsing.\n *\n * Note: With `Terminal.write` usage this exception should never occur, as the top level\n * calls are guaranteed to handle async conditions properly. If you ever encounter this\n * exception in your terminal integration it indicates, that you injected data chunks to\n * `InputHandler.parse` or `EscapeSequenceParser.parse` synchronously without waiting for\n * continuation of a running async handler.\n *\n * It is possible to get rid of this error by calling `reset`. But dont rely on that, as\n * the pending async handler still might mess up the terminal later. Instead fix the\n * faulty async handling, so this error will not be thrown anymore.\n */\n this._parseStack.state = ParserStackType.FAIL;\n throw new Error('improper continuation due to previous async handler, giving up parsing');\n }\n\n // we have to resume the old handler loop if:\n // - return value of the promise was `false`\n // - handlers are not exhausted yet\n const handlers = this._parseStack.handlers;\n let handlerPos = this._parseStack.handlerPos - 1;\n switch (this._parseStack.state) {\n case ParserStackType.CSI:\n if (promiseResult === false && handlerPos > -1) {\n for (; handlerPos >= 0; handlerPos--) {\n handlerResult = (handlers as CsiHandlerType[])[handlerPos](this._params);\n if (handlerResult === true) {\n break;\n } else if (handlerResult instanceof Promise) {\n this._parseStack.handlerPos = handlerPos;\n return handlerResult;\n }\n }\n }\n this._parseStack.handlers = [];\n break;\n case ParserStackType.ESC:\n if (promiseResult === false && handlerPos > -1) {\n for (; handlerPos >= 0; handlerPos--) {\n handlerResult = (handlers as EscHandlerType[])[handlerPos]();\n if (handlerResult === true) {\n break;\n } else if (handlerResult instanceof Promise) {\n this._parseStack.handlerPos = handlerPos;\n return handlerResult;\n }\n }\n }\n this._parseStack.handlers = [];\n break;\n case ParserStackType.DCS:\n code = data[this._parseStack.chunkPos];\n handlerResult = this._dcsParser.unhook(code !== 0x18 && code !== 0x1a, promiseResult);\n if (handlerResult) {\n return handlerResult;\n }\n if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE;\n this._params.resetZdm();\n this._collect = 0;\n break;\n case ParserStackType.OSC:\n code = data[this._parseStack.chunkPos];\n handlerResult = this._oscParser.end(code !== 0x18 && code !== 0x1a, promiseResult);\n if (handlerResult) {\n return handlerResult;\n }\n if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE;\n this._params.resetZdm();\n this._collect = 0;\n break;\n case ParserStackType.APC:\n code = data[this._parseStack.chunkPos];\n handlerResult = this._apcParser.end(code !== 0x18 && code !== 0x1a, promiseResult);\n if (handlerResult) {\n return handlerResult;\n }\n if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE;\n this._params.resetZdm();\n this._collect = 0;\n break;\n }\n // cleanup before continuing with the main sync loop\n this._parseStack.state = ParserStackType.NONE;\n start = this._parseStack.chunkPos + 1;\n this.precedingJoinState = 0;\n this.currentState = this._parseStack.transition & TableAccess.TRANSITION_STATE_MASK;\n }\n }\n\n // continue with main sync loop\n\n // process input string\n for (let i = start; i < length; ++i) {\n code = data[i];\n\n // EXE fast-path: common control bytes (0x00-0x17) in non-payload states\n if (code < 0x18 && this.currentState <= ParserState.CSI_IGNORE) {\n (this._executeHandlersArr[code] ?? this._executeHandlerFb)(code);\n this.precedingJoinState = 0;\n continue;\n }\n\n // CSI fast-path: collapse ESC [ into a single entry, parse params+final in a tight loop\n if (code === 0x1b\n && this.currentState < ParserState.OSC_STRING\n && i + 2 < length && data[i + 1] === 0x5b\n ) {\n this._params.resetZdm();\n this._collect = 0;\n let k = i + 2;\n let ch = data[k];\n if (ch >= 0x3c && ch <= 0x3f) {\n this._collect = ch;\n k++;\n }\n let csiDone = false;\n for (; k < length; k++) {\n ch = data[k];\n if (ch >= 0x30 && ch <= 0x39) {\n this._params.addDigit(ch - 48);\n } else if (ch === 0x3b) {\n this._params.addParam(0);\n } else if (ch === 0x3a) {\n this._params.addSubParam(-1);\n } else if (ch >= 0x40 && ch <= 0x7e) {\n const handlers = this._csiHandlers[this._collect << 8 | ch];\n let j = handlers ? handlers.length - 1 : -1;\n for (; j >= 0; j--) {\n handlerResult = handlers[j](this._params);\n if (handlerResult === true) {\n break;\n } else if (handlerResult instanceof Promise) {\n transition = ParserAction.CSI_DISPATCH << TableAccess.TRANSITION_ACTION_SHIFT | ParserState.GROUND;\n this._preserveStack(ParserStackType.CSI, handlers, j, transition, k);\n return handlerResult;\n }\n }\n if (j < 0) {\n this._csiHandlerFb(this._collect << 8 | ch, this._params);\n }\n this.precedingJoinState = 0;\n i = k;\n this.currentState = ParserState.GROUND;\n csiDone = true;\n break;\n } else {\n break;\n }\n }\n if (!csiDone) {\n i = k - 1;\n this.currentState = ParserState.CSI_PARAM;\n }\n continue;\n }\n\n // normal transition & action lookup\n transition = this._transitions.table[\n this.currentState << TableAccess.INDEX_STATE_SHIFT |\n (code < NON_ASCII_PRINTABLE ? code : NON_ASCII_PRINTABLE)\n ];\n switch (transition >> TableAccess.TRANSITION_ACTION_SHIFT) {\n case ParserAction.PRINT:\n // Note: 0x20 (SP) is included, 0x7F (DEL) is excluded\n let c = i;\n const l4 = length - 4;\n while (c < l4\n && data[++c] >= 0x20 && (data[c] <= 0x7e || data[c] >= NON_ASCII_PRINTABLE)\n && data[++c] >= 0x20 && (data[c] <= 0x7e || data[c] >= NON_ASCII_PRINTABLE)\n && data[++c] >= 0x20 && (data[c] <= 0x7e || data[c] >= NON_ASCII_PRINTABLE)\n && data[++c] >= 0x20 && (data[c] <= 0x7e || data[c] >= NON_ASCII_PRINTABLE)\n ) {}\n if (c >= l4) {\n while (c < length && data[c] >= 0x20 && (data[c] <= 0x7e || data[c] >= NON_ASCII_PRINTABLE)) {\n c++;\n }\n }\n this._printHandler(data, i, c);\n i = c - 1;\n break;\n case ParserAction.EXECUTE:\n if (this._executeHandlers[code]) this._executeHandlers[code]();\n else this._executeHandlerFb(code);\n this.precedingJoinState = 0;\n break;\n case ParserAction.IGNORE:\n break;\n case ParserAction.ERROR:\n const inject: IParsingState = this._errorHandler(\n {\n position: i,\n code,\n currentState: this.currentState,\n collect: this._collect,\n params: this._params,\n abort: false\n });\n if (inject.abort) return;\n // inject values: currently not implemented\n break;\n case ParserAction.CSI_DISPATCH:\n // Trigger CSI Handler\n const handlers = this._csiHandlers[this._collect << 8 | code];\n let j = handlers ? handlers.length - 1 : -1;\n for (; j >= 0; j--) {\n // true means success and to stop bubbling\n // a promise indicates an async handler that needs to finish before progressing\n handlerResult = handlers[j](this._params);\n if (handlerResult === true) {\n break;\n } else if (handlerResult instanceof Promise) {\n this._preserveStack(ParserStackType.CSI, handlers, j, transition, i);\n return handlerResult;\n }\n }\n if (j < 0) {\n this._csiHandlerFb(this._collect << 8 | code, this._params);\n }\n this.precedingJoinState = 0;\n break;\n case ParserAction.PARAM:\n // inner loop: digits (0x30 - 0x39) and ; (0x3b) and : (0x3a)\n do {\n switch (code) {\n case 0x3b:\n this._params.addParam(0); // ZDM\n break;\n case 0x3a:\n this._params.addSubParam(-1);\n break;\n default: // 0x30 - 0x39\n this._params.addDigit(code - 48);\n }\n } while (++i < length && (code = data[i]) > 0x2f && code < 0x3c);\n i--;\n break;\n case ParserAction.COLLECT:\n this._collect <<= 8;\n this._collect |= code;\n break;\n case ParserAction.ESC_DISPATCH:\n const handlersEsc = this._escHandlers[this._collect << 8 | code];\n let jj = handlersEsc ? handlersEsc.length - 1 : -1;\n for (; jj >= 0; jj--) {\n // true means success and to stop bubbling\n // a promise indicates an async handler that needs to finish before progressing\n handlerResult = handlersEsc[jj]();\n if (handlerResult === true) {\n break;\n } else if (handlerResult instanceof Promise) {\n this._preserveStack(ParserStackType.ESC, handlersEsc, jj, transition, i);\n return handlerResult;\n }\n }\n if (jj < 0) {\n this._escHandlerFb(this._collect << 8 | code);\n }\n this.precedingJoinState = 0;\n break;\n case ParserAction.CLEAR:\n this._params.resetZdm();\n this._collect = 0;\n break;\n case ParserAction.DCS_HOOK:\n this._dcsParser.hook(this._collect << 8 | code, this._params);\n break;\n case ParserAction.DCS_PUT:\n // inner loop - exit DCS_PUT: 0x18, 0x1a, 0x1b, 0x7f, 0x80 - 0x9f\n // unhook triggered by: 0x1b, 0x9c (success) and 0x18, 0x1a (abort)\n for (let j = i + 1; ; ++j) {\n if (j >= length || (code = data[j]) === 0x18 || code === 0x1a || code === 0x1b || (code > 0x7f && code < NON_ASCII_PRINTABLE)) {\n this._dcsParser.put(data, i, j);\n i = j - 1;\n break;\n }\n }\n break;\n case ParserAction.DCS_UNHOOK:\n handlerResult = this._dcsParser.unhook(code !== 0x18 && code !== 0x1a);\n if (handlerResult) {\n this._preserveStack(ParserStackType.DCS, [], 0, transition, i);\n return handlerResult;\n }\n if (code === 0x1b) transition |= ParserState.ESCAPE;\n this._params.resetZdm();\n this._collect = 0;\n this.precedingJoinState = 0;\n break;\n case ParserAction.OSC_START:\n this._oscParser.start();\n break;\n case ParserAction.OSC_PUT:\n // inner loop: 0x20 (SP) included, 0x7F (DEL) included\n for (let j = i + 1; ; j++) {\n if (j >= length || (code = data[j]) < 0x20 || (code > 0x7f && code < NON_ASCII_PRINTABLE)) {\n this._oscParser.put(data, i, j);\n i = j - 1;\n break;\n }\n }\n break;\n case ParserAction.OSC_END:\n handlerResult = this._oscParser.end(code !== 0x18 && code !== 0x1a);\n if (handlerResult) {\n this._preserveStack(ParserStackType.OSC, [], 0, transition, i);\n return handlerResult;\n }\n if (code === 0x1b) transition |= ParserState.ESCAPE;\n this._params.resetZdm();\n this._collect = 0;\n this.precedingJoinState = 0;\n break;\n case ParserAction.APC_START:\n this._apcParser.start(this._collect << 8 | code);\n break;\n case ParserAction.APC_PUT:\n // inner loop - exit APC_PUT: 0x18, 0x1a, 0x1b, 0x9c\n // allowed: 00/08 .. 00/13, 02/00 .. 07/14 + NON_ASCII_PRINTABLE\n for (let j = i + 1; ; ++j) {\n if (j < length && (\n (data[j] >= 0x20 && data[j] < 0x7f) || (data[j] >= 0x08 && data[j] < 0x0e) || data[j] >= NON_ASCII_PRINTABLE\n )) continue;\n this._apcParser.put(data, i, j);\n i = j - 1;\n break;\n }\n break;\n case ParserAction.APC_END:\n handlerResult = this._apcParser.end(code !== 0x18 && code !== 0x1a);\n if (handlerResult) {\n this._preserveStack(ParserStackType.APC, [], 0, transition, i);\n return handlerResult;\n }\n if (code === 0x1b) transition |= ParserState.ESCAPE;\n this._params.resetZdm();\n this._collect = 0;\n this.precedingJoinState = 0;\n break;\n }\n this.currentState = transition & TableAccess.TRANSITION_STATE_MASK;\n }\n }\n}\n","/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { IOscHandler, IHandlerCollection, OscFallbackHandlerType, IOscParser, ISubParserStackState } from './Types';\nimport { OscState, ParserConstants } from './Constants';\nimport { utf32ToString } from '../input/TextDecoder';\nimport { IDisposable } from '../Types';\nimport { LimitedStringBuilder } from '../StringBuilder';\n\nconst EMPTY_HANDLERS: IOscHandler[] = [];\n\nexport class OscParser implements IOscParser {\n private _state = OscState.START;\n private _active = EMPTY_HANDLERS;\n private _id = -1;\n private _handlers: IHandlerCollection = Object.create(null);\n private _handlerFb: OscFallbackHandlerType = () => { };\n private _stack: ISubParserStackState = {\n paused: false,\n loopPosition: 0,\n fallThrough: false\n };\n\n public registerHandler(ident: number, handler: IOscHandler): IDisposable {\n this._handlers[ident] ??= [];\n const handlerList = this._handlers[ident];\n handlerList.push(handler);\n return {\n dispose: () => {\n const handlerIndex = handlerList.indexOf(handler);\n if (handlerIndex !== -1) {\n handlerList.splice(handlerIndex, 1);\n }\n }\n };\n }\n public clearHandler(ident: number): void {\n if (this._handlers[ident]) delete this._handlers[ident];\n }\n public setHandlerFallback(handler: OscFallbackHandlerType): void {\n this._handlerFb = handler;\n }\n\n public dispose(): void {\n this._handlers = Object.create(null);\n this._handlerFb = () => { };\n this._active = EMPTY_HANDLERS;\n }\n\n public reset(): void {\n // force cleanup handlers if payload was already sent\n if (this._state === OscState.PAYLOAD) {\n for (let j = this._stack.paused ? this._stack.loopPosition - 1 : this._active.length - 1; j >= 0; --j) {\n this._active[j].end(false);\n }\n }\n this._stack.paused = false;\n this._active = EMPTY_HANDLERS;\n this._id = -1;\n this._state = OscState.START;\n }\n\n private _start(): void {\n this._active = this._handlers[this._id] || EMPTY_HANDLERS;\n if (!this._active.length) {\n this._handlerFb(this._id, 'START');\n } else {\n for (let j = this._active.length - 1; j >= 0; j--) {\n this._active[j].start();\n }\n }\n }\n\n private _put(data: Uint32Array, start: number, end: number): void {\n if (!this._active.length) {\n this._handlerFb(this._id, 'PUT', utf32ToString(data, start, end));\n } else {\n for (let j = this._active.length - 1; j >= 0; j--) {\n this._active[j].put(data, start, end);\n }\n }\n }\n\n public start(): void {\n // always reset leftover handlers\n this.reset();\n this._state = OscState.ID;\n }\n\n /**\n * Put data to current OSC command.\n * Expects the identifier of the OSC command in the form\n * OSC id ; payload ST/BEL\n * Payload chunks are not further processed and get\n * directly passed to the handlers.\n */\n public put(data: Uint32Array, start: number, end: number): void {\n if (this._state === OscState.ABORT) {\n return;\n }\n if (this._state === OscState.ID) {\n while (start < end) {\n const code = data[start++];\n if (code === 0x3b) {\n this._state = OscState.PAYLOAD;\n this._start();\n break;\n }\n if (code < 0x30 || 0x39 < code) {\n this._state = OscState.ABORT;\n return;\n }\n if (this._id === -1) {\n this._id = 0;\n }\n this._id = this._id * 10 + code - 48;\n }\n }\n if (this._state === OscState.PAYLOAD && end - start > 0) {\n this._put(data, start, end);\n }\n }\n\n /**\n * Indicates end of an OSC command.\n * Whether the OSC got aborted or finished normally\n * is indicated by `success`.\n */\n public end(success: boolean, promiseResult: boolean = true): void | Promise {\n if (this._state === OscState.START) {\n return;\n }\n // do nothing if command was faulty\n if (this._state !== OscState.ABORT) {\n // if we are still in ID state and get an early end\n // means that the command has no payload thus we still have\n // to announce START and send END right after\n if (this._state === OscState.ID) {\n this._start();\n }\n\n if (!this._active.length) {\n this._handlerFb(this._id, 'END', success);\n } else {\n let handlerResult: boolean | Promise = false;\n let j = this._active.length - 1;\n let fallThrough = false;\n if (this._stack.paused) {\n j = this._stack.loopPosition - 1;\n handlerResult = promiseResult;\n fallThrough = this._stack.fallThrough;\n this._stack.paused = false;\n }\n if (!fallThrough && handlerResult === false) {\n for (; j >= 0; j--) {\n handlerResult = this._active[j].end(success);\n if (handlerResult === true) {\n break;\n } else if (handlerResult instanceof Promise) {\n this._stack.paused = true;\n this._stack.loopPosition = j;\n this._stack.fallThrough = false;\n return handlerResult;\n }\n }\n j--;\n }\n // cleanup left over handlers\n // we always have to call .end for proper cleanup,\n // here we use `success` to indicate whether a handler should execute\n for (; j >= 0; j--) {\n handlerResult = this._active[j].end(false);\n if (handlerResult instanceof Promise) {\n this._stack.paused = true;\n this._stack.loopPosition = j;\n this._stack.fallThrough = true;\n return handlerResult;\n }\n }\n }\n\n }\n this._active = EMPTY_HANDLERS;\n this._id = -1;\n this._state = OscState.START;\n }\n}\n\n/**\n * Convenient class to allow attaching string based handler functions\n * as OSC handlers.\n */\nexport class OscHandler implements IOscHandler {\n private static _payloadLimit = ParserConstants.PAYLOAD_LIMIT;\n\n private _data = new LimitedStringBuilder(OscHandler._payloadLimit);\n private _hitLimit: boolean = false;\n\n constructor(private _handler: (data: string) => boolean | Promise) { }\n\n public start(): void {\n this._data.reset();\n this._hitLimit = false;\n }\n\n public put(data: Uint32Array, start: number, end: number): void {\n if (this._hitLimit) {\n return;\n }\n if (this._data.append(utf32ToString(data, start, end))) {\n this._hitLimit = true;\n }\n }\n\n public end(success: boolean): boolean | Promise {\n let ret: boolean | Promise = false;\n if (this._hitLimit) {\n ret = false;\n } else if (success) {\n ret = this._handler(this._data.toString());\n if (ret instanceof Promise) {\n // need to hold data until `ret` got resolved\n // dont care for errors, data will be freed anyway on next start\n return ret.then(res => {\n this._data.reset();\n this._hitLimit = false;\n return res;\n });\n }\n }\n this._data.reset();\n this._hitLimit = false;\n return ret;\n }\n}\n","/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\nimport { IParams, ParamsArray } from './Types';\n\nconst enum Constants {\n /**\n * Max value supported for a single param/subparam (clamped to positive int32 range)\n */\n MAX_VALUE = 0x7FFFFFFF,\n /**\n * Max allowed subparams for a single sequence (hardcoded limitation)\n */\n MAX_SUBPARAMS = 256\n}\n\n/**\n * Params storage class.\n * This type is used by the parser to accumulate sequence parameters and sub parameters\n * and transmit them to the input handler actions.\n *\n * NOTES:\n * - params object for action handlers is borrowed, use `.toArray` or `.clone` to get a copy\n * - never read beyond `params.length - 1` (likely to contain arbitrary data)\n * - `.getSubParams` returns a borrowed typed array, use `.getSubParamsAll` for cloned sub params\n * - hardcoded limitations:\n * - max. value for a single (sub) param is 2^31 - 1 (greater values are clamped to that)\n * - max. 256 sub params possible\n * - negative values are not allowed beside -1 (placeholder for default value)\n *\n * About ZDM (Zero Default Mode):\n * ZDM is not orchestrated by this class. If the parser is in ZDM,\n * it should add 0 for empty params, otherwise -1. This does not apply\n * to subparams, empty subparams should always be added with -1.\n */\nexport class Params implements IParams {\n // params store and length\n public params: Int32Array;\n public length: number;\n\n // sub params store and length\n protected _subParams: Int32Array;\n protected _subParamsLength: number;\n\n // sub params offsets from param: param idx --> [start, end] offset\n private _subParamsIdx: Uint16Array;\n private _rejectDigits: boolean;\n private _rejectSubDigits: boolean;\n private _digitIsSub: boolean;\n\n /**\n * Create a `Params` type from JS array representation.\n */\n public static fromArray(values: ParamsArray): Params {\n const params = new Params();\n if (!values.length) {\n return params;\n }\n // skip leading sub params\n for (let i = (Array.isArray(values[0])) ? 1 : 0; i < values.length; ++i) {\n const value = values[i];\n if (Array.isArray(value)) {\n for (let k = 0; k < value.length; ++k) {\n params.addSubParam(value[k]);\n }\n } else {\n params.addParam(value);\n }\n }\n return params;\n }\n\n /**\n * @param maxLength max length of storable parameters\n * @param maxSubParamsLength max length of storable sub parameters\n */\n constructor(public maxLength: number = 32, public maxSubParamsLength: number = 32) {\n if (maxSubParamsLength > Constants.MAX_SUBPARAMS) {\n throw new Error('maxSubParamsLength must not be greater than 256');\n }\n this.params = new Int32Array(maxLength);\n this.length = 0;\n this._subParams = new Int32Array(maxSubParamsLength);\n this._subParamsLength = 0;\n this._subParamsIdx = new Uint16Array(maxLength);\n this._rejectDigits = false;\n this._rejectSubDigits = false;\n this._digitIsSub = false;\n }\n\n /**\n * Clone object.\n */\n public clone(): Params {\n const newParams = new Params(this.maxLength, this.maxSubParamsLength);\n newParams.params.set(this.params);\n newParams.length = this.length;\n newParams._subParams.set(this._subParams);\n newParams._subParamsLength = this._subParamsLength;\n newParams._subParamsIdx.set(this._subParamsIdx);\n newParams._rejectDigits = this._rejectDigits;\n newParams._rejectSubDigits = this._rejectSubDigits;\n newParams._digitIsSub = this._digitIsSub;\n return newParams;\n }\n\n /**\n * Get a JS array representation of the current parameters and sub parameters.\n * The array is structured as follows:\n * sequence: \"1;2:3:4;5::6\"\n * array : [1, 2, [3, 4], 5, [-1, 6]]\n */\n public toArray(): ParamsArray {\n const res: ParamsArray = [];\n for (let i = 0; i < this.length; ++i) {\n res.push(this.params[i]);\n const start = this._subParamsIdx[i] >> 8;\n const end = this._subParamsIdx[i] & 0xFF;\n if (end - start > 0) {\n res.push(Array.prototype.slice.call(this._subParams, start, end));\n }\n }\n return res;\n }\n\n /**\n * Reset to initial empty state.\n */\n public reset(): void {\n this.length = 0;\n this._subParamsLength = 0;\n this._rejectDigits = false;\n this._rejectSubDigits = false;\n this._digitIsSub = false;\n }\n\n /**\n * Reset and add 0 as first param (ZDM).\n */\n public resetZdm(): void {\n this.length = 1;\n this._subParamsLength = 0;\n this._rejectDigits = false;\n this._rejectSubDigits = false;\n this._digitIsSub = false;\n this._subParamsIdx[0] = 0;\n this.params[0] = 0;\n }\n\n /**\n * Add a parameter value.\n * `Params` only stores up to `maxLength` parameters, any later\n * parameter will be ignored.\n * Note: VT devices only stored up to 16 values, xterm seems to\n * store up to 30.\n */\n public addParam(value: number): void {\n this._digitIsSub = false;\n if (this.length >= this.maxLength) {\n this._rejectDigits = true;\n return;\n }\n if (value < -1) {\n throw new Error('values less than -1 are not allowed');\n }\n this._subParamsIdx[this.length] = this._subParamsLength << 8 | this._subParamsLength;\n this.params[this.length++] = value > Constants.MAX_VALUE ? Constants.MAX_VALUE : value;\n }\n\n /**\n * Add a sub parameter value.\n * The sub parameter is automatically associated with the last parameter value.\n * Thus it is not possible to add a subparameter without any parameter added yet.\n * `Params` only stores up to `maxSubParamsLength` sub parameters, any later\n * sub parameter will be ignored.\n */\n public addSubParam(value: number): void {\n this._digitIsSub = true;\n if (!this.length) {\n return;\n }\n if (this._rejectDigits || this._subParamsLength >= this.maxSubParamsLength) {\n this._rejectSubDigits = true;\n return;\n }\n if (value < -1) {\n throw new Error('values less than -1 are not allowed');\n }\n this._subParams[this._subParamsLength++] = value > Constants.MAX_VALUE ? Constants.MAX_VALUE : value;\n this._subParamsIdx[this.length - 1]++;\n }\n\n /**\n * Whether parameter at index `idx` has sub parameters.\n */\n public hasSubParams(idx: number): boolean {\n return ((this._subParamsIdx[idx] & 0xFF) - (this._subParamsIdx[idx] >> 8) > 0);\n }\n\n /**\n * Return sub parameters for parameter at index `idx`.\n * Note: The values are borrowed, thus you need to copy\n * the values if you need to hold them in nonlocal scope.\n */\n public getSubParams(idx: number): Int32Array | null {\n const start = this._subParamsIdx[idx] >> 8;\n const end = this._subParamsIdx[idx] & 0xFF;\n if (end - start > 0) {\n return this._subParams.subarray(start, end);\n }\n return null;\n }\n\n /**\n * Return all sub parameters as {idx: subparams} mapping.\n * Note: The values are not borrowed.\n */\n public getSubParamsAll(): {[idx: number]: Int32Array} {\n const result: {[idx: number]: Int32Array} = {};\n for (let i = 0; i < this.length; ++i) {\n const start = this._subParamsIdx[i] >> 8;\n const end = this._subParamsIdx[i] & 0xFF;\n if (end - start > 0) {\n result[i] = this._subParams.slice(start, end);\n }\n }\n return result;\n }\n\n /**\n * Add a single digit value to current parameter.\n * This is used by the parser to account digits on a char by char basis.\n */\n public addDigit(value: number): void {\n let length;\n if (this._rejectDigits\n || !(length = this._digitIsSub ? this._subParamsLength : this.length)\n || (this._digitIsSub && this._rejectSubDigits)\n ) {\n return;\n }\n\n const store = this._digitIsSub ? this._subParams : this.params;\n const cur = store[length - 1];\n store[length - 1] = ~cur ? Math.min(cur * 10 + value, Constants.MAX_VALUE) : value;\n }\n}\n","/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { ITerminalAddon, IDisposable, Terminal } from '@xterm/xterm';\n\nexport interface ILoadedAddon {\n instance: ITerminalAddon;\n dispose: () => void;\n isDisposed: boolean;\n}\n\nexport class AddonManager implements IDisposable {\n protected _addons: ILoadedAddon[] = [];\n\n public dispose(): void {\n for (let i = this._addons.length - 1; i >= 0; i--) {\n this._addons[i].instance.dispose();\n }\n }\n\n public loadAddon(terminal: Terminal, instance: ITerminalAddon): void {\n const loadedAddon: ILoadedAddon = {\n instance,\n dispose: instance.dispose,\n isDisposed: false\n };\n this._addons.push(loadedAddon);\n instance.dispose = () => this._wrappedAddonDispose(loadedAddon);\n instance.activate(terminal as any);\n }\n\n private _wrappedAddonDispose(loadedAddon: ILoadedAddon): void {\n if (loadedAddon.isDisposed) {\n // Do nothing if already disposed\n return;\n }\n let index = -1;\n for (let i = 0; i < this._addons.length; i++) {\n if (this._addons[i] === loadedAddon) {\n index = i;\n break;\n }\n }\n if (index === -1) {\n throw new Error('Could not dispose an addon that has not been loaded');\n }\n loadedAddon.isDisposed = true;\n loadedAddon.dispose.apply(loadedAddon.instance);\n this._addons.splice(index, 1);\n }\n}\n","/**\n * Copyright (c) 2021 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { IBuffer as IBufferApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi } from '@xterm/xterm';\nimport { IBuffer } from '../buffer/Types';\nimport { BufferLineApiView } from './BufferLineApiView';\nimport { CellData } from '../buffer/CellData';\n\nexport class BufferApiView implements IBufferApi {\n constructor(\n private _buffer: IBuffer,\n public readonly type: 'normal' | 'alternate'\n ) { }\n\n public init(buffer: IBuffer): BufferApiView {\n this._buffer = buffer;\n return this;\n }\n\n public get cursorY(): number { return this._buffer.y; }\n public get cursorX(): number { return this._buffer.x; }\n public get viewportY(): number { return this._buffer.ydisp; }\n public get baseY(): number { return this._buffer.ybase; }\n public get length(): number { return this._buffer.lines.length; }\n public getLine(y: number): IBufferLineApi | undefined {\n const line = this._buffer.lines.get(y);\n if (!line) {\n return undefined;\n }\n return new BufferLineApiView(line);\n }\n public getNullCell(): IBufferCellApi { return new CellData(); }\n}\n","/**\n * Copyright (c) 2021 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { CellData } from '../buffer/CellData';\nimport { IBufferLine, ICellData } from '../buffer/Types';\nimport { IBufferCell as IBufferCellApi, IBufferLine as IBufferLineApi } from '@xterm/xterm';\n\nexport class BufferLineApiView implements IBufferLineApi {\n constructor(private _line: IBufferLine) { }\n\n public get isWrapped(): boolean { return this._line.isWrapped; }\n public get length(): number { return this._line.length; }\n public getCell(x: number, cell?: IBufferCellApi): IBufferCellApi | undefined {\n if (x < 0 || x >= this._line.length) {\n return undefined;\n }\n\n if (cell) {\n this._line.loadCell(x, cell as unknown as ICellData);\n return cell;\n }\n return this._line.loadCell(x, new CellData()) as unknown as IBufferCellApi;\n }\n public translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): string {\n return this._line.translateToString(trimRight, startColumn, endColumn);\n }\n}\n","/**\n * Copyright (c) 2021 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { IBuffer as IBufferApi, IBufferNamespace as IBufferNamespaceApi } from '@xterm/xterm';\nimport { BufferApiView } from './BufferApiView';\nimport { ICoreTerminal } from '../CoreTerminal';\nimport { Disposable } from '../Lifecycle';\nimport { Emitter } from '../Event';\n\nexport class BufferNamespaceApi extends Disposable implements IBufferNamespaceApi {\n private _normal: BufferApiView;\n private _alternate: BufferApiView;\n\n private readonly _onBufferChange = this._register(new Emitter());\n public readonly onBufferChange = this._onBufferChange.event;\n\n constructor(private _core: ICoreTerminal) {\n super();\n this._normal = new BufferApiView(this._core.buffers.normal, 'normal');\n this._alternate = new BufferApiView(this._core.buffers.alt, 'alternate');\n this._register(this._core.buffers.onBufferActivate(() => this._onBufferChange.fire(this.active)));\n }\n public get active(): IBufferApi {\n if (this._core.buffers.active === this._core.buffers.normal) { return this.normal; }\n if (this._core.buffers.active === this._core.buffers.alt) { return this.alternate; }\n throw new Error('Active buffer is neither normal nor alternate');\n }\n public get normal(): IBufferApi {\n return this._normal.init(this._core.buffers.normal);\n }\n public get alternate(): IBufferApi {\n return this._alternate.init(this._core.buffers.alt);\n }\n}\n","/**\n * Copyright (c) 2021 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { IParams } from '../parser/Types';\nimport { IDisposable, IFunctionIdentifier, IParser } from '@xterm/xterm';\nimport { ICoreTerminal } from '../CoreTerminal';\n\nexport class ParserApi implements IParser {\n constructor(private _core: ICoreTerminal) { }\n\n public registerCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean | Promise): IDisposable {\n return this._core.registerCsiHandler(id, (params: IParams) => callback(params.toArray()));\n }\n public addCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean | Promise): IDisposable {\n return this.registerCsiHandler(id, callback);\n }\n public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean | Promise): IDisposable {\n return this._core.registerDcsHandler(id, (data: string, params: IParams) => callback(data, params.toArray()));\n }\n public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean | Promise): IDisposable {\n return this.registerDcsHandler(id, callback);\n }\n public registerEscHandler(id: IFunctionIdentifier, handler: () => boolean | Promise): IDisposable {\n return this._core.registerEscHandler(id, handler);\n }\n public addEscHandler(id: IFunctionIdentifier, handler: () => boolean | Promise): IDisposable {\n return this.registerEscHandler(id, handler);\n }\n public registerOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable {\n return this._core.registerOscHandler(ident, callback);\n }\n public addOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable {\n return this.registerOscHandler(ident, callback);\n }\n public registerApcHandler(id: IFunctionIdentifier, callback: (data: string) => boolean | Promise): IDisposable {\n return this._core.registerApcHandler(id, callback);\n }\n}\n","/**\n * Copyright (c) 2021 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { ICoreTerminal } from '../CoreTerminal';\nimport { IUnicodeHandling, IUnicodeVersionProvider } from '@xterm/xterm';\n\nexport class UnicodeApi implements IUnicodeHandling {\n constructor(private _core: ICoreTerminal) { }\n\n public register(provider: IUnicodeVersionProvider): void {\n this._core.unicodeService.register(provider);\n }\n\n public get versions(): string[] {\n return this._core.unicodeService.versions;\n }\n\n public get activeVersion(): string {\n return this._core.unicodeService.activeVersion;\n }\n\n public set activeVersion(version: string) {\n this._core.unicodeService.activeVersion = version;\n }\n}\n","/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { Disposable } from '../Lifecycle';\nimport { IAttributeData, IBuffer, IBufferLine, IBufferSet } from '../buffer/Types';\nimport { BufferSet } from '../buffer/BufferSet';\nimport { IBufferService, ILogService, IOptionsService, type IBufferResizeEvent } from './Services';\nimport { Emitter } from '../Event';\n\nexport const enum BufferServiceConstants {\n MINIMUM_COLS = 2, // Less than 2 can mess with wide chars\n MINIMUM_ROWS = 1\n}\n\nexport class BufferService extends Disposable implements IBufferService {\n public serviceBrand: any;\n\n public cols: number;\n public rows: number;\n public buffers: IBufferSet;\n /** Whether the user is scrolling (locks the scroll position) */\n public isUserScrolling: boolean = false;\n\n private readonly _onResize = this._register(new Emitter());\n public readonly onResize = this._onResize.event;\n private readonly _onScroll = this._register(new Emitter());\n public readonly onScroll = this._onScroll.event;\n\n public get buffer(): IBuffer { return this.buffers.active; }\n\n /** An IBufferline to clone/copy from for new blank lines */\n private _cachedBlankLine: IBufferLine | undefined;\n\n constructor(\n @IOptionsService optionsService: IOptionsService,\n @ILogService logService: ILogService\n ) {\n super();\n this.cols = Math.max(optionsService.rawOptions.cols || 0, BufferServiceConstants.MINIMUM_COLS);\n this.rows = Math.max(optionsService.rawOptions.rows || 0, BufferServiceConstants.MINIMUM_ROWS);\n this.buffers = this._register(new BufferSet(optionsService, this, logService));\n this._register(this.buffers.onBufferActivate(e => {\n this._onScroll.fire(e.activeBuffer.ydisp);\n }));\n }\n\n public resize(cols: number, rows: number): void {\n const colsChanged = this.cols !== cols;\n const rowsChanged = this.rows !== rows;\n this.cols = cols;\n this.rows = rows;\n this.buffers.resize(cols, rows);\n this._onResize.fire({ cols, rows, colsChanged, rowsChanged });\n }\n\n public reset(): void {\n this.buffers.reset();\n this.isUserScrolling = false;\n }\n\n /**\n * Scroll the terminal down 1 row, creating a blank line.\n * @param eraseAttr The attribute data to use the for blank line.\n * @param isWrapped Whether the new line is wrapped from the previous line.\n */\n public scroll(eraseAttr: IAttributeData, isWrapped: boolean = false): void {\n const buffer = this.buffer;\n\n let newLine: IBufferLine | undefined;\n newLine = this._cachedBlankLine;\n if (!newLine || newLine.length !== this.cols || newLine.getFg(0) !== eraseAttr.fg || newLine.getBg(0) !== eraseAttr.bg) {\n newLine = buffer.getBlankLine(eraseAttr, isWrapped);\n this._cachedBlankLine = newLine;\n }\n newLine.isWrapped = isWrapped;\n\n const topRow = buffer.ybase + buffer.scrollTop;\n const bottomRow = buffer.ybase + buffer.scrollBottom;\n\n if (buffer.scrollTop === 0) {\n // Determine whether the buffer is going to be trimmed after insertion.\n const willBufferBeTrimmed = buffer.lines.isFull;\n\n // Insert the line using the fastest method\n if (bottomRow === buffer.lines.length - 1) {\n if (willBufferBeTrimmed) {\n buffer.lines.recycle().copyFrom(newLine, true);\n } else {\n buffer.lines.push(newLine.clone(true));\n }\n } else {\n buffer.lines.splice(bottomRow + 1, 0, newLine.clone(true));\n }\n\n // Only adjust ybase and ydisp when the buffer is not trimmed\n if (!willBufferBeTrimmed) {\n buffer.ybase++;\n // Only scroll the ydisp with ybase if the user has not scrolled up\n if (!this.isUserScrolling) {\n buffer.ydisp++;\n }\n } else {\n // When the buffer is full and the user has scrolled up, keep the text\n // stable unless ydisp is right at the top\n if (this.isUserScrolling) {\n buffer.ydisp = Math.max(buffer.ydisp - 1, 0);\n }\n }\n } else {\n // scrollTop is non-zero which means no line will be going to the\n // scrollback, instead we can just shift them in-place.\n const scrollRegionHeight = bottomRow - topRow + 1 /* as it's zero-based */;\n buffer.lines.shiftElements(topRow + 1, scrollRegionHeight - 1, -1);\n buffer.lines.set(bottomRow, newLine.clone(true));\n }\n\n // Move the viewport to the bottom of the buffer unless the user is\n // scrolling.\n if (!this.isUserScrolling) {\n buffer.ydisp = buffer.ybase;\n }\n\n this._onScroll.fire(buffer.ydisp);\n }\n\n /**\n * Scroll the display of the terminal\n * @param disp The number of lines to scroll down (negative scroll up).\n * @param suppressScrollEvent Don't emit the scroll event as scrollLines. This is used\n * to avoid unwanted events being handled by the viewport when the event was triggered from the\n * viewport originally.\n */\n public scrollLines(disp: number, suppressScrollEvent?: boolean): void {\n const buffer = this.buffer;\n if (disp < 0) {\n if (buffer.ydisp === 0) {\n return;\n }\n this.isUserScrolling = true;\n } else if (disp + buffer.ydisp >= buffer.ybase) {\n this.isUserScrolling = false;\n }\n\n const oldYdisp = buffer.ydisp;\n buffer.ydisp = Math.max(Math.min(buffer.ydisp + disp, buffer.ybase), 0);\n\n // No change occurred, don't trigger scroll/refresh\n if (oldYdisp === buffer.ydisp) {\n return;\n }\n\n if (!suppressScrollEvent) {\n this._onScroll.fire(buffer.ydisp);\n }\n }\n}\n","/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { ICharsetService } from './Services';\nimport { ICharset } from '../Types';\n\nexport class CharsetService implements ICharsetService {\n public serviceBrand: any;\n\n public charset: ICharset | undefined;\n public glevel: number = 0;\n\n private _charsets: (ICharset | undefined)[] = [];\n\n public get charsets(): (ICharset | undefined)[] {\n return this._charsets;\n }\n\n public reset(): void {\n this.charset = undefined;\n this._charsets = [];\n this.glevel = 0;\n }\n\n public setgLevel(g: number): void {\n this.glevel = g;\n this.charset = this._charsets[g];\n }\n\n public setgCharset(g: number, charset: ICharset | undefined): void {\n this._charsets[g] = charset;\n if (this.glevel === g) {\n this.charset = charset;\n }\n }\n}\n","/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { Disposable } from '../Lifecycle';\nimport { IDecPrivateModes, IKittyKeyboardState, IModes } from '../Types';\nimport { IBufferService, ICoreService, ILogService, IOptionsService } from './Services';\nimport { Emitter } from '../Event';\n\nconst DEFAULT_MODES: IModes = Object.freeze({\n insertMode: false\n});\n\nconst DEFAULT_DEC_PRIVATE_MODES: IDecPrivateModes = Object.freeze({\n applicationCursorKeys: false,\n applicationKeypad: false,\n bracketedPasteMode: false,\n colorSchemeUpdates: false,\n cursorBlink: undefined,\n cursorStyle: undefined,\n origin: false,\n reverseWraparound: false,\n sendFocus: false,\n synchronizedOutput: false,\n win32InputMode: false,\n wraparound: true // defaults: xterm - true, vt100 - false\n});\n\nconst DEFAULT_KITTY_KEYBOARD_STATE = (): IKittyKeyboardState => ({\n flags: 0,\n mainFlags: 0,\n altFlags: 0,\n mainStack: [],\n altStack: []\n});\n\nexport class CoreService extends Disposable implements ICoreService {\n public serviceBrand: any;\n\n public isCursorInitialized: boolean;\n public isCursorHidden: boolean = false;\n public modes: IModes;\n public decPrivateModes: IDecPrivateModes;\n public kittyKeyboard: IKittyKeyboardState;\n\n private readonly _onData = this._register(new Emitter());\n public readonly onData = this._onData.event;\n private readonly _onUserInput = this._register(new Emitter());\n public readonly onUserInput = this._onUserInput.event;\n private readonly _onBinary = this._register(new Emitter());\n public readonly onBinary = this._onBinary.event;\n private readonly _onRequestScrollToBottom = this._register(new Emitter());\n public readonly onRequestScrollToBottom = this._onRequestScrollToBottom.event;\n\n constructor(\n @IBufferService private readonly _bufferService: IBufferService,\n @ILogService private readonly _logService: ILogService,\n @IOptionsService private readonly _optionsService: IOptionsService\n ) {\n super();\n this.isCursorInitialized = _optionsService.rawOptions.showCursorImmediately ?? false;\n this.modes = structuredClone(DEFAULT_MODES);\n this.decPrivateModes = structuredClone(DEFAULT_DEC_PRIVATE_MODES);\n this.kittyKeyboard = DEFAULT_KITTY_KEYBOARD_STATE();\n }\n\n public reset(): void {\n this.modes = structuredClone(DEFAULT_MODES);\n this.decPrivateModes = structuredClone(DEFAULT_DEC_PRIVATE_MODES);\n this.kittyKeyboard = DEFAULT_KITTY_KEYBOARD_STATE();\n }\n\n public triggerDataEvent(data: string, wasUserInput: boolean = false): void {\n // Prevents all events to pty process if stdin is disabled\n if (this._optionsService.rawOptions.disableStdin) {\n return;\n }\n\n // Input is being sent to the terminal, the terminal should focus the prompt.\n const buffer = this._bufferService.buffer;\n if (wasUserInput && this._optionsService.rawOptions.scrollOnUserInput && buffer.ybase !== buffer.ydisp) {\n this._onRequestScrollToBottom.fire();\n }\n\n // Fire onUserInput so listeners can react as well (eg. clear selection)\n if (wasUserInput) {\n this._onUserInput.fire();\n }\n\n // Fire onData API\n this._logService.debug(`sending data \"${data}\"`);\n this._logService.trace(`sending data (codes)`, () => data.split('').map(e => e.charCodeAt(0)));\n this._onData.fire(data);\n }\n\n public triggerBinaryEvent(data: string): void {\n if (this._optionsService.rawOptions.disableStdin) {\n return;\n }\n this._logService.debug(`sending binary \"${data}\"`);\n this._logService.trace(`sending binary (codes)`, () => data.split('').map(e => e.charCodeAt(0)));\n this._onBinary.fire(data);\n }\n}\n","/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n *\n * This was heavily inspired from microsoft/vscode's dependency injection system (MIT).\n */\n/*---------------------------------------------------------------------------------------------\n * Copyright (c) Microsoft Corporation. All rights reserved.\n * Licensed under the MIT License. See License.txt in the project root for license information.\n *--------------------------------------------------------------------------------------------*/\n\nimport { IInstantiationService } from './Services';\nimport { IServiceIdentifier, getServiceDependencies } from './ServiceRegistry';\n\nexport class ServiceCollection {\n\n private _entries = new Map, any>();\n\n constructor(...entries: [IServiceIdentifier, any][]) {\n for (const [id, service] of entries) {\n this.set(id, service);\n }\n }\n\n public set(id: IServiceIdentifier, instance: T): T {\n const result = this._entries.get(id);\n this._entries.set(id, instance);\n return result;\n }\n\n public forEach(callback: (id: IServiceIdentifier, instance: any) => any): void {\n for (const [key, value] of this._entries.entries()) {\n callback(key, value);\n }\n }\n\n public has(id: IServiceIdentifier): boolean {\n return this._entries.has(id);\n }\n\n public get(id: IServiceIdentifier): T | undefined {\n return this._entries.get(id);\n }\n}\n\nexport class InstantiationService implements IInstantiationService {\n public serviceBrand: undefined;\n\n private readonly _services: ServiceCollection = new ServiceCollection();\n\n constructor() {\n this._services.set(IInstantiationService, this);\n }\n\n public setService(id: IServiceIdentifier, instance: T): void {\n this._services.set(id, instance);\n }\n\n public getService(id: IServiceIdentifier): T | undefined {\n return this._services.get(id);\n }\n\n public createInstance(ctor: any, ...args: any[]): T {\n const serviceDependencies = getServiceDependencies(ctor).sort((a, b) => a.index - b.index);\n\n const serviceArgs: any[] = [];\n for (const dependency of serviceDependencies) {\n const service = this._services.get(dependency.id);\n if (!service) {\n throw new Error(`[createInstance] ${ctor.name} depends on UNKNOWN service ${dependency.id._id}.`);\n }\n serviceArgs.push(service);\n }\n\n const firstServiceArgPos = serviceDependencies.length > 0 ? serviceDependencies[0].index : args.length;\n\n // check for argument mismatches, adjust static args if needed\n if (args.length !== firstServiceArgPos) {\n throw new Error(`[createInstance] First service dependency of ${ctor.name} at position ${firstServiceArgPos + 1} conflicts with ${args.length} static arguments`);\n }\n\n // now create the instance\n return new ctor(...[...args, ...serviceArgs]);\n }\n}\n","/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { Disposable } from '../Lifecycle';\nimport { ILogService, IOptionsService, LogLevelEnum } from './Services';\n\ntype LogType = (message?: any, ...optionalParams: any[]) => void;\n\ninterface IConsole {\n log: LogType;\n error: LogType;\n info: LogType;\n trace: LogType;\n warn: LogType;\n}\n\n// console is available on both node.js and browser contexts but the common\n// module doesn't depend on them so we need to explicitly declare it.\ndeclare const console: IConsole;\n\nconst optionsKeyToLogLevel: { [key: string]: LogLevelEnum } = {\n trace: LogLevelEnum.TRACE,\n debug: LogLevelEnum.DEBUG,\n info: LogLevelEnum.INFO,\n warn: LogLevelEnum.WARN,\n error: LogLevelEnum.ERROR,\n off: LogLevelEnum.OFF\n};\n\nconst LOG_PREFIX = 'xterm.js: ';\n\nexport class LogService extends Disposable implements ILogService {\n public serviceBrand: any;\n\n private _logLevel: LogLevelEnum = LogLevelEnum.OFF;\n public get logLevel(): LogLevelEnum { return this._logLevel; }\n\n constructor(\n @IOptionsService private readonly _optionsService: IOptionsService\n ) {\n super();\n this._updateLogLevel();\n this._register(this._optionsService.onSpecificOptionChange('logLevel', () => this._updateLogLevel()));\n }\n\n private _updateLogLevel(): void {\n this._logLevel = optionsKeyToLogLevel[this._optionsService.rawOptions.logLevel];\n }\n\n private _evalLazyOptionalParams(optionalParams: any[]): void {\n for (let i = 0; i < optionalParams.length; i++) {\n if (typeof optionalParams[i] === 'function') {\n optionalParams[i] = optionalParams[i]();\n }\n }\n }\n\n private _log(type: LogType, message: string, optionalParams: any[]): void {\n this._evalLazyOptionalParams(optionalParams);\n type.call(console, (this._optionsService.options.logger ? '' : LOG_PREFIX) + message, ...optionalParams);\n }\n\n public trace(message: string, ...optionalParams: any[]): void {\n if (this._logLevel <= LogLevelEnum.TRACE) {\n this._log(this._optionsService.options.logger?.trace.bind(this._optionsService.options.logger) ?? console.log, message, optionalParams);\n }\n }\n\n public debug(message: string, ...optionalParams: any[]): void {\n if (this._logLevel <= LogLevelEnum.DEBUG) {\n this._log(this._optionsService.options.logger?.debug.bind(this._optionsService.options.logger) ?? console.log, message, optionalParams);\n }\n }\n\n public info(message: string, ...optionalParams: any[]): void {\n if (this._logLevel <= LogLevelEnum.INFO) {\n this._log(this._optionsService.options.logger?.info.bind(this._optionsService.options.logger) ?? console.info, message, optionalParams);\n }\n }\n\n public warn(message: string, ...optionalParams: any[]): void {\n if (this._logLevel <= LogLevelEnum.WARN) {\n this._log(this._optionsService.options.logger?.warn.bind(this._optionsService.options.logger) ?? console.warn, message, optionalParams);\n }\n }\n\n public error(message: string, ...optionalParams: any[]): void {\n if (this._logLevel <= LogLevelEnum.ERROR) {\n this._log(this._optionsService.options.logger?.error.bind(this._optionsService.options.logger) ?? console.error, message, optionalParams);\n }\n }\n}\n","/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\nimport { IMouseStateService } from './Services';\nimport { ICoreMouseProtocol, ICoreMouseEvent, CoreMouseEncoding, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from '../Types';\nimport { Disposable } from '../Lifecycle';\nimport { Emitter } from '../Event';\n\n/**\n * Supported default protocols.\n */\nconst DEFAULT_PROTOCOLS: { [key: string]: ICoreMouseProtocol } = {\n /**\n * NONE\n * Events: none\n * Modifiers: none\n */\n NONE: {\n events: CoreMouseEventType.NONE,\n restrict: () => false\n },\n /**\n * X10\n * Events: mousedown\n * Modifiers: none\n */\n X10: {\n events: CoreMouseEventType.DOWN,\n restrict: (e: ICoreMouseEvent) => {\n // no wheel, no move, no up\n if (e.button === CoreMouseButton.WHEEL || e.action !== CoreMouseAction.DOWN) {\n return false;\n }\n // no modifiers\n e.ctrl = false;\n e.alt = false;\n e.shift = false;\n return true;\n }\n },\n /**\n * VT200\n * Events: mousedown / mouseup / wheel\n * Modifiers: all\n */\n VT200: {\n events: CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL,\n restrict: (e: ICoreMouseEvent) => {\n // no move\n if (e.action === CoreMouseAction.MOVE) {\n return false;\n }\n return true;\n }\n },\n /**\n * DRAG\n * Events: mousedown / mouseup / wheel / mousedrag\n * Modifiers: all\n */\n DRAG: {\n events: CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL | CoreMouseEventType.DRAG,\n restrict: (e: ICoreMouseEvent) => {\n // no move without button\n if (e.action === CoreMouseAction.MOVE && e.button === CoreMouseButton.NONE) {\n return false;\n }\n return true;\n }\n },\n /**\n * ANY\n * Events: all mouse related events\n * Modifiers: all\n */\n ANY: {\n events:\n CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL\n | CoreMouseEventType.DRAG | CoreMouseEventType.MOVE,\n restrict: (e: ICoreMouseEvent) => true\n }\n};\n\nconst enum Modifiers {\n SHIFT = 4,\n ALT = 8,\n CTRL = 16\n}\n\n// helper for default encoders to generate the event code.\nfunction eventCode(e: ICoreMouseEvent, isSGR: boolean): number {\n let code = (e.ctrl ? Modifiers.CTRL : 0) | (e.shift ? Modifiers.SHIFT : 0) | (e.alt ? Modifiers.ALT : 0);\n if (e.button === CoreMouseButton.WHEEL) {\n code |= 64;\n code |= e.action;\n } else {\n code |= e.button & 3;\n if (e.button & 4) {\n code |= 64;\n }\n if (e.button & 8) {\n code |= 128;\n }\n if (e.action === CoreMouseAction.MOVE) {\n code |= CoreMouseAction.MOVE;\n } else if (e.action === CoreMouseAction.UP && !isSGR) {\n // special case - only SGR can report button on release\n // all others have to go with NONE\n code |= CoreMouseButton.NONE;\n }\n }\n return code;\n}\n\nconst S = String.fromCharCode;\n\n/**\n * Supported default encodings.\n */\nconst DEFAULT_ENCODINGS: { [key: string]: CoreMouseEncoding } = {\n /**\n * DEFAULT - CSI M Pb Px Py\n * Single byte encoding for coords and event code.\n * Can encode values up to 223 (1-based).\n */\n DEFAULT: (e: ICoreMouseEvent) => {\n const params = [eventCode(e, false) + 32, e.col + 32, e.row + 32];\n // supress mouse report if we exceed addressible range\n // Note this is handled differently by emulators\n // - xterm: sends 0;0 coords instead\n // - vte, konsole: no report\n if (params[0] > 255 || params[1] > 255 || params[2] > 255) {\n return '';\n }\n return `\\x1b[M${S(params[0])}${S(params[1])}${S(params[2])}`;\n },\n /**\n * SGR - CSI < Pb ; Px ; Py M|m\n * No encoding limitation.\n * Can report button on release and works with a well formed sequence.\n */\n SGR: (e: ICoreMouseEvent) => {\n const final = (e.action === CoreMouseAction.UP && e.button !== CoreMouseButton.WHEEL) ? 'm' : 'M';\n return `\\x1b[<${eventCode(e, true)};${e.col};${e.row}${final}`;\n },\n SGR_PIXELS: (e: ICoreMouseEvent) => {\n const final = (e.action === CoreMouseAction.UP && e.button !== CoreMouseButton.WHEEL) ? 'm' : 'M';\n return `\\x1b[<${eventCode(e, true)};${e.x};${e.y}${final}`;\n }\n};\n\n/**\n * MouseStateService\n *\n * Provides mouse tracking reports with different protocols and encodings.\n * - protocols: NONE (default), X10, VT200, DRAG, ANY\n * - encodings: DEFAULT, SGR (UTF8, URXVT removed in #2507)\n *\n * Custom protocols/encodings can be added by `addProtocol` / `addEncoding`.\n * To activate a protocol/encoding, set `activeProtocol` / `activeEncoding`.\n * Switching a protocol will send a notification event `onProtocolChange`\n * with a list of needed events to track.\n *\n * The service handles the mouse tracking state and decides whether to send\n * a tracking report to the backend based on protocol and encoding limitations.\n * To send a mouse event call `triggerMouseEvent`.\n */\nexport class MouseStateService extends Disposable implements IMouseStateService {\n public serviceBrand: any;\n\n private _protocols: { [name: string]: ICoreMouseProtocol } = {};\n private _encodings: { [name: string]: CoreMouseEncoding } = {};\n private _activeProtocol: string = '';\n private _activeEncoding: string = '';\n private _customWheelEventHandler: ((event: WheelEvent) => boolean) | undefined;\n\n private readonly _onProtocolChange = this._register(new Emitter());\n public readonly onProtocolChange = this._onProtocolChange.event;\n\n constructor() {\n super();\n\n // register default protocols and encodings\n for (const name of Object.keys(DEFAULT_PROTOCOLS)) this.addProtocol(name, DEFAULT_PROTOCOLS[name]);\n for (const name of Object.keys(DEFAULT_ENCODINGS)) this.addEncoding(name, DEFAULT_ENCODINGS[name]);\n // call reset to set defaults\n this.reset();\n }\n\n public addProtocol(name: string, protocol: ICoreMouseProtocol): void {\n this._protocols[name] = protocol;\n }\n\n public addEncoding(name: string, encoding: CoreMouseEncoding): void {\n this._encodings[name] = encoding;\n }\n\n public get activeProtocol(): string {\n return this._activeProtocol;\n }\n\n public get areMouseEventsActive(): boolean {\n return this._protocols[this._activeProtocol].events !== 0;\n }\n\n public set activeProtocol(name: string) {\n if (!this._protocols[name]) {\n throw new Error(`unknown protocol \"${name}\"`);\n }\n this._activeProtocol = name;\n this._onProtocolChange.fire(this._protocols[name].events);\n }\n\n public get activeEncoding(): string {\n return this._activeEncoding;\n }\n\n public set activeEncoding(name: string) {\n if (!this._encodings[name]) {\n throw new Error(`unknown encoding \"${name}\"`);\n }\n this._activeEncoding = name;\n }\n\n public reset(): void {\n this.activeProtocol = 'NONE';\n this.activeEncoding = 'DEFAULT';\n }\n\n public setCustomWheelEventHandler(customWheelEventHandler: ((event: WheelEvent) => boolean) | undefined): void {\n this._customWheelEventHandler = customWheelEventHandler;\n }\n\n public allowCustomWheelEvent(ev: WheelEvent): boolean {\n return this._customWheelEventHandler ? this._customWheelEventHandler(ev) !== false : true;\n }\n\n public restrictMouseEvent(e: ICoreMouseEvent): boolean {\n return this._protocols[this._activeProtocol].restrict(e);\n }\n\n public encodeMouseEvent(e: ICoreMouseEvent): string {\n return this._encodings[this._activeEncoding](e);\n }\n\n public get isDefaultEncoding(): boolean {\n return this._activeEncoding === 'DEFAULT';\n }\n\n public get isPixelEncoding(): boolean {\n return this._activeEncoding === 'SGR_PIXELS';\n }\n}\n","/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { Disposable, toDisposable } from '../Lifecycle';\nimport { isMac } from '../Platform';\nimport { CursorStyle, IDisposable } from '../Types';\nimport { FontWeight, IOptionsService, ITerminalOptions } from './Services';\nimport { Emitter } from '../Event';\n\nexport const DEFAULT_OPTIONS: Readonly> = {\n cols: 80,\n rows: 24,\n showCursorImmediately: false,\n cursorBlink: false,\n blinkIntervalDuration: 0,\n cursorStyle: 'block',\n cursorWidth: 1,\n cursorInactiveStyle: 'outline',\n drawBoldTextInBrightColors: true,\n documentOverride: null,\n fastScrollSensitivity: 5,\n fontFamily: 'monospace',\n fontSize: 15,\n fontWeight: 'normal',\n fontWeightBold: 'bold',\n ignoreBracketedPasteMode: false,\n lineHeight: 1.0,\n letterSpacing: 0,\n linkHandler: null,\n logLevel: 'info',\n logger: null,\n scrollback: 1000,\n scrollbar: { showScrollbar: true },\n scrollOnEraseInDisplay: false,\n scrollOnUserInput: true,\n scrollSensitivity: 1,\n screenReaderMode: false,\n smoothScrollDuration: 0,\n macOptionIsMeta: false,\n macOptionClickForcesSelection: false,\n minimumContrastRatio: 1,\n mouseEventsRequireAlt: false,\n disableStdin: false,\n allowProposedApi: false,\n allowTransparency: false,\n tabStopWidth: 8,\n theme: {},\n reflowCursorLine: false,\n rescaleOverlappingGlyphs: false,\n rightClickSelectsWord: isMac,\n windowOptions: {},\n windowsPty: {},\n wordSeparator: ' ()[]{}\\',\"`',\n altClickMovesCursor: true,\n convertEol: false,\n termName: 'xterm',\n quirks: {},\n vtExtensions: {}\n};\n\nconst FONT_WEIGHT_OPTIONS: Extract[] = ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'];\n\nexport class OptionsService extends Disposable implements IOptionsService {\n public serviceBrand: any;\n\n public readonly rawOptions: Required;\n public options: Required;\n\n private readonly _onOptionChange = this._register(new Emitter());\n public readonly onOptionChange = this._onOptionChange.event;\n\n constructor(options: Partial) {\n super();\n // set the default value of each option\n const defaultOptions = { ...DEFAULT_OPTIONS };\n for (const key in options) {\n if (key in defaultOptions) {\n try {\n const newValue = options[key];\n defaultOptions[key] = this._sanitizeAndValidateOption(key, newValue);\n } catch (e) {\n console.error(e);\n }\n }\n }\n\n // set up getters and setters for each option\n this.rawOptions = defaultOptions;\n this.options = { ... defaultOptions };\n this._setupOptions();\n\n // Clear out options that could link outside xterm.js as they could easily cause an embedder\n // memory leak\n this._register(toDisposable(() => {\n this.rawOptions.linkHandler = null;\n this.rawOptions.documentOverride = null;\n }));\n }\n\n // eslint-disable-next-line @typescript-eslint/naming-convention\n public onSpecificOptionChange(key: T, listener: (value: ITerminalOptions[T]) => any): IDisposable {\n return this.onOptionChange(eventKey => {\n if (eventKey === key) {\n listener(this.rawOptions[key]);\n }\n });\n }\n\n // eslint-disable-next-line @typescript-eslint/naming-convention\n public onMultipleOptionChange(keys: (keyof ITerminalOptions)[], listener: () => any): IDisposable {\n return this.onOptionChange(eventKey => {\n if (keys.indexOf(eventKey) !== -1) {\n listener();\n }\n });\n }\n\n private _setupOptions(): void {\n const getter = (propName: string): any => {\n if (!(propName in DEFAULT_OPTIONS)) {\n throw new Error(`No option with key \"${propName}\"`);\n }\n return this.rawOptions[propName];\n };\n\n const setter = (propName: string, value: any): void => {\n if (!(propName in DEFAULT_OPTIONS)) {\n throw new Error(`No option with key \"${propName}\"`);\n }\n\n value = this._sanitizeAndValidateOption(propName, value);\n // Don't fire an option change event if they didn't change\n if (this.rawOptions[propName] !== value) {\n this.rawOptions[propName] = value;\n this._onOptionChange.fire(propName);\n }\n };\n\n for (const propName in this.rawOptions) {\n const desc = {\n get: getter.bind(this, propName),\n set: setter.bind(this, propName)\n };\n Object.defineProperty(this.options, propName, desc);\n }\n }\n\n private _sanitizeAndValidateOption(key: string, value: any): any {\n switch (key) {\n case 'cursorStyle':\n if (!value) {\n value = DEFAULT_OPTIONS[key];\n }\n if (!isCursorStyle(value)) {\n throw new Error(`\"${value}\" is not a valid value for ${key}`);\n }\n break;\n case 'wordSeparator':\n if (!value) {\n value = DEFAULT_OPTIONS[key];\n }\n break;\n case 'fontWeight':\n case 'fontWeightBold':\n if (typeof value === 'number' && 1 <= value && value <= 1000) {\n // already valid numeric value\n break;\n }\n value = FONT_WEIGHT_OPTIONS.includes(value) ? value : DEFAULT_OPTIONS[key];\n break;\n case 'blinkIntervalDuration':\n value = Math.floor(value);\n if (value < 0) {\n throw new Error(`${key} cannot be less than 0, value: ${value}`);\n }\n break;\n case 'cursorWidth':\n value = Math.floor(value);\n // Fall through for bounds check\n case 'lineHeight':\n case 'tabStopWidth':\n if (value < 1) {\n throw new Error(`${key} cannot be less than 1, value: ${value}`);\n }\n break;\n case 'minimumContrastRatio':\n value = Math.max(1, Math.min(21, Math.round(value * 10) / 10));\n break;\n case 'scrollback':\n value = Math.min(value, 4294967295);\n if (value < 0) {\n throw new Error(`${key} cannot be less than 0, value: ${value}`);\n }\n break;\n case 'fastScrollSensitivity':\n case 'scrollSensitivity':\n if (value <= 0) {\n throw new Error(`${key} cannot be less than or equal to 0, value: ${value}`);\n }\n break;\n case 'rows':\n case 'cols':\n if (!value && value !== 0) {\n throw new Error(`${key} must be numeric, value: ${value}`);\n }\n break;\n case 'windowsPty':\n value = value ?? {};\n break;\n }\n return value;\n }\n}\n\nfunction isCursorStyle(value: unknown): value is CursorStyle {\n return value === 'block' || value === 'underline' || value === 'bar';\n}\n","/**\n * Copyright (c) 2022 The xterm.js authors. All rights reserved.\n * @license MIT\n */\nimport { IBufferService, IOscLinkService } from './Services';\nimport { IOscLinkData } from '../Types';\nimport { IMarker } from '../buffer/Types';\n\nexport class OscLinkService implements IOscLinkService {\n public serviceBrand: any;\n\n private _nextId = 1;\n\n /**\n * A map of the link key to link entry. This is used to add additional lines to links with ids.\n */\n private _entriesWithId: Map = new Map();\n\n /**\n * A map of the link id to the link entry. The \"link id\" (number) which is the numberic\n * representation of a unique link should not be confused with \"id\" (string) which comes in with\n * `id=` in the OSC link's properties.\n */\n private _dataByLinkId: Map = new Map();\n\n constructor(\n @IBufferService private readonly _bufferService: IBufferService\n ) {\n }\n\n public registerLink(data: IOscLinkData): number {\n const buffer = this._bufferService.buffer;\n\n // Links with no id will only ever be registered a single time\n if (data.id === undefined) {\n const marker = buffer.addMarker(buffer.ybase + buffer.y);\n const entry: IOscLinkEntryNoId = {\n data,\n id: this._nextId++,\n lines: [marker]\n };\n marker.onDispose(() => this._removeMarkerFromLink(entry, marker));\n this._dataByLinkId.set(entry.id, entry);\n return entry.id;\n }\n\n // Add the line to the link if it already exists\n const castData = data as Required;\n const key = this._getEntryIdKey(castData);\n const match = this._entriesWithId.get(key);\n if (match) {\n this.addLineToLink(match.id, buffer.ybase + buffer.y);\n return match.id;\n }\n\n // Create the link\n const marker = buffer.addMarker(buffer.ybase + buffer.y);\n const entry: IOscLinkEntryWithId = {\n id: this._nextId++,\n key: this._getEntryIdKey(castData),\n data: castData,\n lines: [marker]\n };\n marker.onDispose(() => this._removeMarkerFromLink(entry, marker));\n this._entriesWithId.set(entry.key, entry);\n this._dataByLinkId.set(entry.id, entry);\n return entry.id;\n }\n\n public addLineToLink(linkId: number, y: number): void {\n const entry = this._dataByLinkId.get(linkId);\n if (!entry) {\n return;\n }\n if (entry.lines.every(e => e.line !== y)) {\n const marker = this._bufferService.buffer.addMarker(y);\n entry.lines.push(marker);\n marker.onDispose(() => this._removeMarkerFromLink(entry, marker));\n }\n }\n\n public getLinkData(linkId: number): IOscLinkData | undefined {\n return this._dataByLinkId.get(linkId)?.data;\n }\n\n private _getEntryIdKey(linkData: Required): string {\n return `${linkData.id};;${linkData.uri}`;\n }\n\n private _removeMarkerFromLink(entry: IOscLinkEntryNoId | IOscLinkEntryWithId, marker: IMarker): void {\n const index = entry.lines.indexOf(marker);\n if (index === -1) {\n return;\n }\n entry.lines.splice(index, 1);\n if (entry.lines.length === 0) {\n if (entry.data.id !== undefined) {\n this._entriesWithId.delete((entry as IOscLinkEntryWithId).key);\n }\n this._dataByLinkId.delete(entry.id);\n }\n }\n}\n\ninterface IOscLinkEntry {\n data: T;\n id: number;\n lines: IMarker[];\n}\n\ninterface IOscLinkEntryNoId extends IOscLinkEntry {\n}\n\ninterface IOscLinkEntryWithId extends IOscLinkEntry> {\n key: string;\n}\n","/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n *\n * This was heavily inspired from microsoft/vscode's dependency injection system (MIT).\n */\n/*---------------------------------------------------------------------------------------------\n * Copyright (c) Microsoft Corporation. All rights reserved.\n * Licensed under the MIT License. See License.txt in the project root for license information.\n *--------------------------------------------------------------------------------------------*/\n\nexport interface IServiceIdentifier {\n (...args: any[]): void;\n type: T;\n _id: string;\n}\n\nconst enum Constants {\n DI_TARGET = 'di$target',\n DI_DEPENDENCIES = 'di$dependencies'\n}\n\nexport const serviceRegistry: Map> = new Map();\n\nexport function getServiceDependencies(ctor: any): { id: IServiceIdentifier, index: number, optional: boolean }[] {\n return ctor[Constants.DI_DEPENDENCIES] || [];\n}\n\nexport function createDecorator(id: string): IServiceIdentifier {\n if (serviceRegistry.has(id)) {\n return serviceRegistry.get(id)!;\n }\n\n const decorator: any = function (target: Function, key: string, index: number): any {\n if (arguments.length !== 3) {\n throw new Error('@IServiceName-decorator can only be used to decorate a parameter');\n }\n\n storeServiceDependency(decorator, target, index);\n };\n\n decorator._id = id;\n\n serviceRegistry.set(id, decorator);\n return decorator;\n}\n\nfunction storeServiceDependency(id: Function, target: Function, index: number): void {\n if ((target as any)[Constants.DI_TARGET] === target) {\n (target as any)[Constants.DI_DEPENDENCIES].push({ id, index });\n } else {\n (target as any)[Constants.DI_DEPENDENCIES] = [{ id, index }];\n (target as any)[Constants.DI_TARGET] = target;\n }\n}\n","/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport type { IDecoration, IDecorationOptions, ILinkHandler, ILogger, IWindowsPty, IOverviewRulerOptions } from '@xterm/xterm';\nimport { CoreMouseEncoding, CoreMouseEventType, CursorInactiveStyle, CursorStyle, ICharset, IColor, ICoreMouseEvent, ICoreMouseProtocol, IDecPrivateModes, IDisposable, IKittyKeyboardState, IModes, IOscLinkData, IWindowOptions } from '../Types';\nimport { IAttributeData, IBuffer, IBufferSet } from '../buffer/Types';\nimport { createDecorator, IServiceIdentifier } from './ServiceRegistry';\nimport type { Emitter, IEvent } from '../Event';\n\nexport const IBufferService = createDecorator('BufferService');\nexport interface IBufferService {\n serviceBrand: undefined;\n\n readonly cols: number;\n readonly rows: number;\n readonly buffer: IBuffer;\n readonly buffers: IBufferSet;\n isUserScrolling: boolean;\n onResize: IEvent;\n onScroll: IEvent;\n scroll(eraseAttr: IAttributeData, isWrapped?: boolean): void;\n scrollLines(disp: number, suppressScrollEvent?: boolean): void;\n resize(cols: number, rows: number): void;\n reset(): void;\n}\n\nexport interface IBufferResizeEvent {\n cols: number;\n rows: number;\n colsChanged: boolean;\n rowsChanged: boolean;\n}\n\nexport const IMouseStateService = createDecorator('MouseStateService');\nexport interface IMouseStateService {\n serviceBrand: undefined;\n\n activeProtocol: string;\n activeEncoding: string;\n areMouseEventsActive: boolean;\n addProtocol(name: string, protocol: ICoreMouseProtocol): void;\n addEncoding(name: string, encoding: CoreMouseEncoding): void;\n reset(): void;\n setCustomWheelEventHandler(customWheelEventHandler: ((event: WheelEvent) => boolean) | undefined): void;\n allowCustomWheelEvent(ev: WheelEvent): boolean;\n\n /**\n * Event to announce changes in mouse tracking.\n */\n onProtocolChange: IEvent;\n restrictMouseEvent(event: ICoreMouseEvent): boolean;\n encodeMouseEvent(event: ICoreMouseEvent): string;\n readonly isDefaultEncoding: boolean;\n readonly isPixelEncoding: boolean;\n}\n\nexport const ICoreService = createDecorator('CoreService');\nexport interface ICoreService {\n serviceBrand: undefined;\n\n /**\n * Initially the cursor will not be visible until the first time the terminal\n * is focused.\n */\n isCursorInitialized: boolean;\n isCursorHidden: boolean;\n\n readonly modes: IModes;\n readonly decPrivateModes: IDecPrivateModes;\n readonly kittyKeyboard: IKittyKeyboardState;\n\n readonly onData: IEvent;\n readonly onUserInput: IEvent;\n readonly onBinary: IEvent;\n readonly onRequestScrollToBottom: IEvent;\n\n reset(): void;\n\n /**\n * Triggers the onData event in the public API.\n * @param data The data that is being emitted.\n * @param wasUserInput Whether the data originated from the user (as opposed to\n * resulting from parsing incoming data). When true this will also:\n * - Scroll to the bottom of the buffer if option scrollOnUserInput is true.\n * - Fire the `onUserInput` event (so selection can be cleared).\n */\n triggerDataEvent(data: string, wasUserInput?: boolean): void;\n\n /**\n * Triggers the onBinary event in the public API.\n * @param data The data that is being emitted.\n */\n triggerBinaryEvent(data: string): void;\n}\n\nexport const ICharsetService = createDecorator('CharsetService');\nexport interface ICharsetService {\n serviceBrand: undefined;\n\n charset: ICharset | undefined;\n readonly glevel: number;\n readonly charsets: (ICharset | undefined)[];\n\n reset(): void;\n\n /**\n * Set the G level of the terminal.\n * @param g\n */\n setgLevel(g: number): void;\n\n /**\n * Set the charset for the given G level of the terminal.\n * @param g\n * @param charset\n */\n setgCharset(g: number, charset: ICharset | undefined): void;\n}\n\nexport interface IBrandedService {\n serviceBrand: undefined;\n}\n\ntype GetLeadingNonServiceArgs = TArgs extends [] ? []\n : TArgs extends [...infer TFirst, infer TLast] ? TLast extends IBrandedService ? GetLeadingNonServiceArgs : TArgs\n : never;\n\nexport const IInstantiationService = createDecorator('InstantiationService');\nexport interface IInstantiationService {\n serviceBrand: undefined;\n\n setService(id: IServiceIdentifier, instance: T): void;\n getService(id: IServiceIdentifier): T | undefined;\n createInstance any, R extends InstanceType>(t: Ctor, ...args: GetLeadingNonServiceArgs>): R;\n}\n\nexport enum LogLevelEnum {\n TRACE = 0,\n DEBUG = 1,\n INFO = 2,\n WARN = 3,\n ERROR = 4,\n OFF = 5\n}\n\nexport const ILogService = createDecorator('LogService');\nexport interface ILogService {\n serviceBrand: undefined;\n\n readonly logLevel: LogLevelEnum;\n\n trace(message: any, ...optionalParams: any[]): void;\n debug(message: any, ...optionalParams: any[]): void;\n info(message: any, ...optionalParams: any[]): void;\n warn(message: any, ...optionalParams: any[]): void;\n error(message: any, ...optionalParams: any[]): void;\n}\n\nexport const IOptionsService = createDecorator('OptionsService');\nexport interface IOptionsService {\n serviceBrand: undefined;\n\n /**\n * Read only access to the raw options object, this is an internal-only fast path for accessing\n * single options without any validation as we trust TypeScript to enforce correct usage\n * internally.\n */\n readonly rawOptions: Required;\n\n /**\n * Options as exposed through the public API, this property uses getters and setters with\n * validation which makes it safer but slower. {@link rawOptions} should be used for pretty much\n * all internal usage for performance reasons.\n */\n readonly options: Required;\n\n /**\n * Adds an event listener for when any option changes.\n */\n readonly onOptionChange: IEvent;\n\n /**\n * Adds an event listener for when a specific option changes, this is a convenience method that is\n * preferred over {@link onOptionChange} when only a single option is being listened to.\n */\n // eslint-disable-next-line @typescript-eslint/naming-convention\n onSpecificOptionChange(key: T, listener: (arg1: Required[T]) => any): IDisposable;\n\n /**\n * Adds an event listener for when a set of specific options change, this is a convenience method\n * that is preferred over {@link onOptionChange} when multiple options are being listened to and\n * handled the same way.\n */\n // eslint-disable-next-line @typescript-eslint/naming-convention\n onMultipleOptionChange(keys: (keyof ITerminalOptions)[], listener: () => any): IDisposable;\n}\n\nexport type FontWeight = 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900' | number;\nexport type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'off';\n\nexport interface ITerminalOptions {\n allowProposedApi?: boolean;\n allowTransparency?: boolean;\n altClickMovesCursor?: boolean;\n cols?: number;\n convertEol?: boolean;\n cursorBlink?: boolean;\n blinkIntervalDuration?: number;\n cursorStyle?: CursorStyle;\n cursorWidth?: number;\n cursorInactiveStyle?: CursorInactiveStyle;\n disableStdin?: boolean;\n documentOverride?: any | null;\n drawBoldTextInBrightColors?: boolean;\n fastScrollSensitivity?: number;\n fontSize?: number;\n fontFamily?: string;\n fontWeight?: FontWeight;\n fontWeightBold?: FontWeight;\n ignoreBracketedPasteMode?: boolean;\n letterSpacing?: number;\n lineHeight?: number;\n linkHandler?: ILinkHandler | null;\n logLevel?: LogLevel;\n logger?: ILogger | null;\n macOptionIsMeta?: boolean;\n macOptionClickForcesSelection?: boolean;\n minimumContrastRatio?: number;\n mouseEventsRequireAlt?: boolean;\n reflowCursorLine?: boolean;\n rescaleOverlappingGlyphs?: boolean;\n rightClickSelectsWord?: boolean;\n rows?: number;\n showCursorImmediately?: boolean;\n screenReaderMode?: boolean;\n scrollback?: number;\n scrollOnUserInput?: boolean;\n scrollSensitivity?: number;\n smoothScrollDuration?: number;\n tabStopWidth?: number;\n theme?: ITheme;\n windowsPty?: IWindowsPty;\n windowOptions?: IWindowOptions;\n wordSeparator?: string;\n quirks?: ITerminalQuirks;\n scrollbar?: IScrollbarOptions;\n scrollOnEraseInDisplay?: boolean;\n vtExtensions?: IVtExtensions;\n\n [key: string]: any;\n termName: string;\n}\n\nexport interface ITheme {\n foreground?: string;\n background?: string;\n cursor?: string;\n cursorAccent?: string;\n selectionForeground?: string;\n selectionBackground?: string;\n selectionInactiveBackground?: string;\n scrollbarSliderBackground?: string;\n scrollbarSliderHoverBackground?: string;\n scrollbarSliderActiveBackground?: string;\n overviewRulerBorder?: string;\n black?: string;\n red?: string;\n green?: string;\n yellow?: string;\n blue?: string;\n magenta?: string;\n cyan?: string;\n white?: string;\n brightBlack?: string;\n brightRed?: string;\n brightGreen?: string;\n brightYellow?: string;\n brightBlue?: string;\n brightMagenta?: string;\n brightCyan?: string;\n brightWhite?: string;\n extendedAnsi?: string[];\n}\n\nexport interface ITerminalQuirks {\n allowSetCursorBlink?: boolean;\n}\n\nexport interface IScrollbarOptions {\n showScrollbar?: boolean;\n showArrows?: boolean;\n width?: number;\n overviewRuler?: IOverviewRulerOptions;\n}\n\nexport interface IVtExtensions {\n kittyKeyboard?: boolean;\n kittySgrBoldFaintControl?: boolean;\n win32InputMode?: boolean;\n colorSchemeQuery?: boolean;\n}\n\nexport const IOscLinkService = createDecorator('OscLinkService');\nexport interface IOscLinkService {\n serviceBrand: undefined;\n /**\n * Registers a link to the service, returning the link ID. The link data is managed by this\n * service and will be freed when this current cursor position is trimmed off the buffer.\n */\n registerLink(linkData: IOscLinkData): number;\n /**\n * Adds a line to a link if needed.\n */\n addLineToLink(linkId: number, y: number): void;\n /** Get the link data associated with a link ID. */\n getLinkData(linkId: number): IOscLinkData | undefined;\n}\n\n/*\n * Width and Grapheme_Cluster_Break properties of a character as a bit mask.\n *\n * bit 0: shouldJoin - should combine with preceding character.\n * bit 1..2: wcwidth - see UnicodeCharWidth.\n * bit 3..31: class of character (currently only 4 bits are used).\n * This is used to determined grapheme clustering - i.e. which codepoints\n * are to be combined into a single compound character.\n *\n * Use the UnicodeService static function createPropertyValue to create a\n * UnicodeCharProperties; use extractShouldJoin, extractWidth, and\n * extractCharKind to extract the components.\n */\nexport type UnicodeCharProperties = number;\n\n/**\n * Width in columns of a character.\n * In a CJK context, \"half-width\" characters (such as Latin) are width 1,\n * while \"full-width\" characters (such as Kanji) are 2 columns wide.\n * Combining characters (such as accents) are width 0.\n */\nexport type UnicodeCharWidth = 0 | 1 | 2;\n\nexport const IUnicodeService = createDecorator('UnicodeService');\nexport interface IUnicodeService {\n serviceBrand: undefined;\n /** Register a Unicode version provider. */\n register(provider: IUnicodeVersionProvider): void;\n /** Registered Unicode versions. */\n readonly versions: string[];\n /** Currently active version. */\n activeVersion: string;\n /** Event triggered when the active version changes. */\n readonly onChange: IEvent;\n\n /**\n * Unicode version dependent\n */\n wcwidth(codepoint: number): UnicodeCharWidth;\n getStringCellWidth(s: string): number;\n /**\n * Return character width and type for grapheme clustering.\n * If preceding != 0, it is the return code from the previous character;\n * in that case the result specifies if the characters should be joined.\n */\n charProperties(codepoint: number, preceding: UnicodeCharProperties): UnicodeCharProperties;\n}\n\nexport interface IUnicodeVersionProvider {\n readonly version: string;\n wcwidth(ucs: number): UnicodeCharWidth;\n charProperties(codepoint: number, preceding: UnicodeCharProperties): UnicodeCharProperties;\n}\n\nexport const IDecorationService = createDecorator('DecorationService');\nexport interface IDecorationService extends IDisposable {\n serviceBrand: undefined;\n readonly decorations: IterableIterator;\n readonly onDecorationRegistered: IEvent;\n readonly onDecorationRemoved: IEvent;\n registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined;\n reset(): void;\n /**\n * Trigger a callback over the decoration at a cell (in no particular order). This uses a callback\n * instead of an iterator as it's typically used in hot code paths.\n */\n forEachDecorationAtCell(x: number, line: number, layer: 'bottom' | 'top' | undefined, callback: (decoration: IInternalDecoration) => void): void;\n}\nexport interface IInternalDecoration extends IDecoration {\n readonly options: IDecorationOptions;\n readonly backgroundColorRGB: IColor | undefined;\n readonly foregroundColorRGB: IColor | undefined;\n readonly onRenderEmitter: Emitter;\n /** @internal Start line for line-index removal; kept in sync on buffer line shifts. */\n _indexedStartLine: number;\n}\n","/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { IUnicodeService, IUnicodeVersionProvider, UnicodeCharProperties, UnicodeCharWidth } from './Services';\nimport { Emitter } from '../Event';\n\nexport class UnicodeService implements IUnicodeService {\n public serviceBrand: any;\n\n private _providers: {[key: string]: IUnicodeVersionProvider} = Object.create(null);\n private _active: string = '';\n private _activeProvider!: IUnicodeVersionProvider;\n\n private readonly _onChange = new Emitter();\n public readonly onChange = this._onChange.event;\n\n public static extractShouldJoin(value: UnicodeCharProperties): boolean {\n return (value & 1) !== 0;\n }\n public static extractWidth(value: UnicodeCharProperties): UnicodeCharWidth {\n return ((value >> 1) & 0x3) as UnicodeCharWidth;\n }\n public static extractCharKind(value: UnicodeCharProperties): number {\n return value >> 3;\n }\n public static createPropertyValue(state: number, width: number, shouldJoin: boolean = false): UnicodeCharProperties {\n return ((state & 0xffffff) << 3) | ((width & 3) << 1) | (shouldJoin?1:0);\n }\n\n public dispose(): void {\n this._onChange.dispose();\n }\n\n public get versions(): string[] {\n return Object.keys(this._providers);\n }\n\n public get activeVersion(): string {\n return this._active;\n }\n\n public set activeVersion(version: string) {\n if (!this._providers[version]) {\n throw new Error(`unknown Unicode version \"${version}\"`);\n }\n this._active = version;\n this._activeProvider = this._providers[version];\n this._onChange.fire(version);\n }\n\n public register(provider: IUnicodeVersionProvider): void {\n this._providers[provider.version] = provider;\n if (!this._active) {\n this.activeVersion = provider.version;\n }\n }\n\n /**\n * Unicode version dependent interface.\n */\n public wcwidth(num: number): UnicodeCharWidth {\n return this._activeProvider.wcwidth(num);\n }\n\n public getStringCellWidth(s: string): number {\n let result = 0;\n let precedingInfo = 0;\n const length = s.length;\n for (let i = 0; i < length; ++i) {\n let code = s.charCodeAt(i);\n // surrogate pair first\n if (0xD800 <= code && code <= 0xDBFF) {\n if (++i >= length) {\n // this should not happen with strings retrieved from\n // Buffer.translateToString as it converts from UTF-32\n // and therefore always should contain the second part\n // for any other string we still have to handle it somehow:\n // simply treat the lonely surrogate first as a single char (UCS-2 behavior)\n return result + this.wcwidth(code);\n }\n const second = s.charCodeAt(i);\n // convert surrogate pair to high codepoint only for valid second part (UTF-16)\n // otherwise treat them independently (UCS-2 behavior)\n if (0xDC00 <= second && second <= 0xDFFF) {\n code = (code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000;\n } else {\n result += this.wcwidth(second);\n }\n }\n const currentInfo = this.charProperties(code, precedingInfo);\n let chWidth = UnicodeService.extractWidth(currentInfo);\n if (UnicodeService.extractShouldJoin(currentInfo)) {\n chWidth -= UnicodeService.extractWidth(precedingInfo);\n }\n result += chWidth;\n precedingInfo = currentInfo;\n }\n return result;\n }\n\n public charProperties(codepoint: number, preceding: UnicodeCharProperties): UnicodeCharProperties {\n return this._activeProvider.charProperties(codepoint, preceding);\n }\n}\n","/**\n * Copyright (c) 2014 The xterm.js authors. All rights reserved.\n * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)\n * @license MIT\n *\n * Originally forked from (with the author's permission):\n * Fabrice Bellard's javascript vt100 for jslinux:\n * http://bellard.org/jslinux/\n * Copyright (c) 2011 Fabrice Bellard\n * The original design remains. The terminal itself\n * has been extended to include xterm CSI codes, among\n * other features.\n *\n * Terminal Emulation References:\n * http://vt100.net/\n * http://invisible-island.net/xterm/ctlseqs/ctlseqs.txt\n * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html\n * http://invisible-island.net/vttest/\n * http://www.inwap.com/pdp10/ansicode.txt\n * http://linux.die.net/man/4/console_codes\n * http://linux.die.net/man/7/urxvt\n */\n\nimport { DEFAULT_ATTR_DATA } from '../common/buffer/BufferLine';\nimport { IBuffer, IMarker } from '../common/buffer/Types';\nimport { CoreTerminal } from '../common/CoreTerminal';\nimport { ITerminalOptions } from '../common/Types';\nimport { Emitter, EventUtils } from '../common/Event';\n\nexport class Terminal extends CoreTerminal {\n private readonly _onBell = this._register(new Emitter());\n public readonly onBell = this._onBell.event;\n private readonly _onCursorMove = this._register(new Emitter());\n public readonly onCursorMove = this._onCursorMove.event;\n private readonly _onTitleChange = this._register(new Emitter());\n public readonly onTitleChange = this._onTitleChange.event;\n private readonly _onA11yCharEmitter = this._register(new Emitter());\n public readonly onA11yChar = this._onA11yCharEmitter.event;\n private readonly _onA11yTabEmitter = this._register(new Emitter());\n public readonly onA11yTab = this._onA11yTabEmitter.event;\n\n constructor(\n options: ITerminalOptions = {}\n ) {\n super(options);\n\n this._setup();\n\n // Setup InputHandler listeners\n this._register(this._inputHandler.onRequestBell(() => this.bell()));\n this._register(this._inputHandler.onRequestReset(() => this.reset()));\n this._register(EventUtils.forward(this._inputHandler.onCursorMove, this._onCursorMove));\n this._register(EventUtils.forward(this._inputHandler.onTitleChange, this._onTitleChange));\n this._register(EventUtils.forward(this._inputHandler.onA11yChar, this._onA11yCharEmitter));\n this._register(EventUtils.forward(this._inputHandler.onA11yTab, this._onA11yTabEmitter));\n this._register(EventUtils.forward(EventUtils.map(this._inputHandler.onRequestRefreshRows, e => ({ start: e?.start ?? 0, end: e?.end ?? this.rows - 1 })), this._onRender));\n }\n\n /**\n * Convenience property to active buffer.\n */\n public get buffer(): IBuffer {\n return this.buffers.active;\n }\n\n // TODO: Support paste here?\n\n public get markers(): IMarker[] {\n return this.buffer.markers;\n }\n\n public registerMarker(cursorYOffset: number): IMarker {\n return this.buffer.addMarker(this.buffer.ybase + this.buffer.y + cursorYOffset);\n }\n\n public bell(): void {\n this._onBell.fire();\n }\n\n public input(data: string, wasUserInput: boolean = true): void {\n this.coreService.triggerDataEvent(data, wasUserInput);\n }\n\n /**\n * Resizes the terminal.\n *\n * @param x The number of columns to resize to.\n * @param y The number of rows to resize to.\n */\n public resize(x: number, y: number): void {\n if (x === this.cols && y === this.rows) {\n return;\n }\n\n super.resize(x, y);\n }\n\n /**\n * Clear the entire buffer, making the prompt line the new first line.\n */\n public clear(): void {\n this.buffer.clearAllMarkers();\n this.buffer.lines.set(0, this.buffer.lines.get(this.buffer.ybase + this.buffer.y)!);\n this.buffer.lines.length = 1;\n this.buffer.ydisp = 0;\n this.buffer.ybase = 0;\n this.buffer.y = 0;\n for (let i = 1; i < this.rows; i++) {\n this.buffer.lines.push(this.buffer.getBlankLine(DEFAULT_ATTR_DATA));\n }\n this._onScroll.fire({ position: this.buffer.ydisp });\n }\n\n /**\n * Reset terminal.\n * Note: Calling this directly from JS is synchronous but does not clear\n * input buffers and does not reset the parser, thus the terminal will\n * continue to apply pending input data.\n * If you need in band reset (synchronous with input data) consider\n * using DECSTR (soft reset, CSI ! p) or RIS instead (hard reset, ESC c).\n */\n public reset(): void {\n /**\n * Since _setup handles a full terminal creation, we have to carry forward\n * a few things that should not reset.\n */\n this.options.rows = this.rows;\n this.options.cols = this.cols;\n\n this._setup();\n super.reset();\n }\n}\n","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n","/**\n * Copyright (c) 2018 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { BufferNamespaceApi } from '../../common/public/BufferNamespaceApi';\nimport { ParserApi } from '../../common/public/ParserApi';\nimport { UnicodeApi } from '../../common/public/UnicodeApi';\nimport { IBufferNamespace as IBufferNamespaceApi, IMarker, IModes, IParser, ITerminalAddon, ITerminalInitOnlyOptions, IUnicodeHandling, Terminal as ITerminalApi } from '@xterm/headless';\nimport { Terminal as TerminalCore } from '../Terminal';\nimport { AddonManager } from '../../common/public/AddonManager';\nimport { ITerminalOptions } from '../../common/Types';\nimport { Disposable } from '../../common/Lifecycle';\nimport type { IEvent } from '../../common/Event';\n/**\n * The set of options that only have an effect when set in the Terminal constructor.\n */\nconst CONSTRUCTOR_ONLY_OPTIONS = ['cols', 'rows'];\n\nexport class Terminal extends Disposable implements ITerminalApi {\n private _core: TerminalCore;\n private _addonManager: AddonManager;\n private _parser: IParser | undefined;\n private _buffer: BufferNamespaceApi | undefined;\n private _publicOptions: Required;\n\n constructor(options?: ITerminalOptions & ITerminalInitOnlyOptions) {\n super();\n\n this._core = this._register(new TerminalCore(options));\n this._addonManager = this._register(new AddonManager());\n\n this._publicOptions = { ... this._core.options };\n const getter = (propName: string): any => {\n return this._core.options[propName];\n };\n const setter = (propName: string, value: any): void => {\n this._checkReadonlyOptions(propName);\n this._core.options[propName] = value;\n };\n\n for (const propName in this._core.options) {\n const desc = {\n get: getter.bind(this, propName),\n set: setter.bind(this, propName)\n };\n Object.defineProperty(this._publicOptions, propName, desc);\n }\n }\n\n private _checkReadonlyOptions(propName: string): void {\n // Throw an error if any constructor only option is modified\n // from terminal.options\n // Modifications from anywhere else are allowed\n if (CONSTRUCTOR_ONLY_OPTIONS.includes(propName)) {\n throw new Error(`Option \"${propName}\" can only be set in the constructor`);\n }\n }\n\n private _checkProposedApi(): void {\n if (!this._core.optionsService.options.allowProposedApi) {\n throw new Error('You must set the allowProposedApi option to true to use proposed API');\n }\n }\n\n public get onBell(): IEvent { return this._core.onBell; }\n public get onBinary(): IEvent { return this._core.onBinary; }\n public get onCursorMove(): IEvent { return this._core.onCursorMove; }\n public get onData(): IEvent { return this._core.onData; }\n public get onLineFeed(): IEvent { return this._core.onLineFeed; }\n public get onRender(): IEvent<{ start: number, end: number }> { return this._core.onRender; }\n public get onResize(): IEvent<{ cols: number, rows: number }> { return this._core.onResize; }\n public get onScroll(): IEvent { return this._core.onScroll; }\n public get onTitleChange(): IEvent { return this._core.onTitleChange; }\n public get onWriteParsed(): IEvent { return this._core.onWriteParsed; }\n\n public get parser(): IParser {\n this._parser ??= new ParserApi(this._core);\n return this._parser;\n }\n public get unicode(): IUnicodeHandling {\n this._checkProposedApi();\n return new UnicodeApi(this._core);\n }\n public get rows(): number { return this._core.rows; }\n public get cols(): number { return this._core.cols; }\n public get buffer(): IBufferNamespaceApi {\n this._buffer ??= this._register(new BufferNamespaceApi(this._core));\n return this._buffer;\n }\n public get markers(): ReadonlyArray {\n return this._core.markers;\n }\n public get modes(): IModes {\n const m = this._core.coreService.decPrivateModes;\n let mouseTrackingMode: 'none' | 'x10' | 'vt200' | 'drag' | 'any' = 'none';\n switch (this._core.mouseStateService.activeProtocol) {\n case 'X10': mouseTrackingMode = 'x10'; break;\n case 'VT200': mouseTrackingMode = 'vt200'; break;\n case 'DRAG': mouseTrackingMode = 'drag'; break;\n case 'ANY': mouseTrackingMode = 'any'; break;\n }\n return {\n applicationCursorKeysMode: m.applicationCursorKeys,\n applicationKeypadMode: m.applicationKeypad,\n bracketedPasteMode: m.bracketedPasteMode,\n insertMode: this._core.coreService.modes.insertMode,\n mouseTrackingMode: mouseTrackingMode,\n originMode: m.origin,\n reverseWraparoundMode: m.reverseWraparound,\n sendFocusMode: m.sendFocus,\n showCursor: !this._core.coreService.isCursorHidden,\n synchronizedOutputMode: m.synchronizedOutput,\n win32InputMode: m.win32InputMode,\n wraparoundMode: m.wraparound\n };\n }\n public get options(): Required {\n return this._publicOptions;\n }\n public set options(options: ITerminalOptions) {\n for (const propName in options) {\n this._publicOptions[propName] = options[propName];\n }\n }\n public input(data: string, wasUserInput: boolean = true): void {\n this._core.input(data, wasUserInput);\n }\n public resize(columns: number, rows: number): void {\n this._verifyIntegers(columns, rows);\n this._core.resize(columns, rows);\n }\n public registerMarker(cursorYOffset: number = 0): IMarker {\n this._verifyIntegers(cursorYOffset);\n return this._core.registerMarker(cursorYOffset);\n }\n public addMarker(cursorYOffset: number): IMarker {\n return this.registerMarker(cursorYOffset);\n }\n public dispose(): void {\n super.dispose();\n }\n public scrollLines(amount: number): void {\n this._verifyIntegers(amount);\n this._core.scrollLines(amount);\n }\n public scrollPages(pageCount: number): void {\n this._verifyIntegers(pageCount);\n this._core.scrollPages(pageCount);\n }\n public scrollToTop(): void {\n this._core.scrollToTop();\n }\n public scrollToBottom(): void {\n this._core.scrollToBottom();\n }\n public scrollToLine(line: number): void {\n this._verifyIntegers(line);\n this._core.scrollToLine(line);\n }\n public clear(): void {\n this._core.clear();\n }\n public write(data: string | Uint8Array, callback?: () => void): void {\n this._core.write(data, callback);\n }\n public writeln(data: string | Uint8Array, callback?: () => void): void {\n this._core.write(data);\n this._core.write('\\r\\n', callback);\n }\n public reset(): void {\n this._core.reset();\n }\n public loadAddon(addon: ITerminalAddon): void {\n // TODO: This could cause issues if the addon calls renderer apis\n this._addonManager.loadAddon(this as any, addon);\n }\n\n private _verifyIntegers(...values: number[]): void {\n for (const value of values) {\n if (value === Infinity || isNaN(value) || value % 1 !== 0) {\n throw new Error('This API only accepts integers');\n }\n }\n }\n}\n"],"names":["millis","Promise","resolve","setTimeout","handler","timeout","store","timer","disposable","dispose","Lifecycle_1","toDisposable","clearTimeout","add","__webpack_require__","constructor","this","_token","_isDisposed","cancel","cancelAndSet","runner","Error","setIfNotSet","_isScheduled","set","queueMicrotask","_disposable","undefined","interval","context","globalThis","handle","setInterval","clearInterval","Event_1","CircularList","Disposable","_maxLength","super","onDeleteEmitter","_register","Emitter","onDelete","event","onInsertEmitter","onInsert","onTrimEmitter","onTrim","_array","Array","_startIndex","_length","maxLength","newMaxLength","newArray","i","Math","min","length","_getCyclicIndex","newLength","get","index","value","push","fire","recycle","isFull","pop","splice","start","deleteCount","items","amount","countToTrim","trimStart","count","shiftElements","offset","expandListBy","Services_1","InstantiationService_1","LogService_1","BufferService_1","OptionsService_1","CoreService_1","MouseStateService_1","UnicodeV6_1","UnicodeService_1","CharsetService_1","WindowsMode_1","InputHandler_1","WriteBuffer_1","OscLinkService_1","hasWriteSyncWarnHappened","CoreTerminal","onScroll","_onScrollApi","_onScroll","ev","position","cols","_bufferService","rows","buffers","options","optionsService","key","_windowsWrappingHeuristics","MutableDisposable","_onBinary","onBinary","_onData","onData","_onLineFeed","onLineFeed","_onRender","onRender","_onResize","onResize","_onWriteParsed","onWriteParsed","_instantiationService","InstantiationService","OptionsService","setService","IOptionsService","_logService","createInstance","LogService","ILogService","BufferService","IBufferService","coreService","CoreService","ICoreService","mouseStateService","MouseStateService","IMouseStateService","unicodeService","UnicodeService","register","UnicodeV6","IUnicodeService","_charsetService","CharsetService","ICharsetService","_oscLinkService","OscLinkService","IOscLinkService","_inputHandler","InputHandler","EventUtils","forward","onRequestScrollToBottom","scrollToBottom","onUserInput","_writeBuffer","handleUserInput","onMultipleOptionChange","_handleWindowsPtyOptionChange","buffer","ydisp","markRangeDirty","scrollTop","scrollBottom","WriteBuffer","data","promiseResult","parse","write","callback","writeSync","maxSubsequentCalls","logLevel","LogLevelEnum","WARN","warn","input","wasUserInput","triggerDataEvent","resize","x","y","isNaN","max","flushSync","scroll","eraseAttr","isWrapped","scrollLines","disp","suppressScrollEvent","scrollPages","pageCount","scrollToTop","disableSmoothScroll","ybase","scrollToLine","line","scrollAmount","registerEscHandler","id","registerDcsHandler","registerCsiHandler","registerOscHandler","ident","registerApcHandler","_setup","reset","windowsPty","rawOptions","backend","buildNumber","_enableWindowsWrappingHeuristics","clear","disposables","updateWindowsModeWrappedState","bind","final","d","_listeners","_disposed","_event","listener","thisArgs","entry","fn","slice","result","idx","indexOf","isArray","call","listeners","len","from","to","e","map","any","events","DisposableStore","runAndSubscribe","initial","exports","Charsets_1","EscapeSequenceParser_1","TextDecoder_1","BufferLine_1","Constants_1","CellData_1","AttributeData_1","OscParser_1","DcsParser_1","ApcParser_1","XParseColor_1","Version_1","GLEVEL","paramToWindowOption","n","opts","setWinLines","restoreWin","minimizeWin","setWinPosition","setWinSizePixels","raiseWin","lowerWin","refreshWin","setWinSizeChars","maximizeWin","fullscreenWin","getWinState","getWinPosition","getWinSizePixels","getScreenSizePixels","getCellSizePixels","getWinSizeChars","getScreenSizeChars","getIconTitle","getWinTitle","pushTitle","popTitle","WindowsOptionsReportType","$temp","getAttrData","_curAttrData","_coreService","_optionsService","_mouseStateService","_unicodeService","_parser","EscapeSequenceParser","_parseBuffer","Uint32Array","_stringDecoder","StringToUtf32","_utf8Decoder","Utf8ToUtf32","_windowTitle","_iconName","_windowTitleStack","_iconNameStack","DEFAULT_ATTR_DATA","clone","_eraseAttrDataInternal","_onRequestBell","onRequestBell","_onRequestRefreshRows","onRequestRefreshRows","_onRequestReset","onRequestReset","_onRequestSendFocus","onRequestSendFocus","_onRequestSyncScrollBar","onRequestSyncScrollBar","_onRequestWindowsOptionsReport","onRequestWindowsOptionsReport","_onA11yChar","onA11yChar","_onA11yTab","onA11yTab","_onCursorMove","onCursorMove","_onTitleChange","onTitleChange","_onColor","onColor","_onRequestColorSchemeQuery","onRequestColorSchemeQuery","_parseStack","paused","cursorStartX","cursorStartY","decodedLength","_specialColors","_dirtyRowTracker","DirtyRowTracker","_activeBuffer","onBufferActivate","activeBuffer","setCsiHandlerFallback","params","debug","identifier","identToString","toArray","setEscHandlerFallback","setExecuteHandlerFallback","code","setOscHandlerFallback","action","setDcsHandlerFallback","payload","setApcHandlerFallback","setPrintHandler","end","print","insertChars","intermediates","scrollLeft","cursorUp","scrollRight","cursorDown","cursorForward","cursorBackward","cursorNextLine","cursorPrecedingLine","cursorCharAbsolute","cursorPosition","cursorForwardTab","eraseInDisplay","prefix","eraseInLine","insertLines","deleteLines","deleteChars","scrollUp","scrollDown","eraseChars","cursorBackwardTab","charPosAbsolute","hPositionRelative","repeatPrecedingCharacter","sendDeviceAttributesPrimary","sendDeviceAttributesSecondary","linePosAbsolute","vPositionRelative","hVPosition","tabClear","setMode","setModePrivate","resetMode","resetModePrivate","charAttributes","deviceStatus","deviceStatusPrivate","softReset","sendXtVersion","setCursorStyle","setScrollRegion","saveCursor","windowOptions","restoreCursor","insertColumns","deleteColumns","selectProtected","requestMode","kittyKeyboardSet","kittyKeyboardQuery","kittyKeyboardPush","kittyKeyboardPop","setExecuteHandler","bell","lineFeed","carriageReturn","backspace","tab","shiftOut","shiftIn","nextLine","tabSet","OscHandler","setTitle","setIconName","setOrReportIndexedColor","setHyperlink","setOrReportFgColor","setOrReportBgColor","setOrReportCursorColor","restoreIndexedColor","restoreFgColor","restoreBgColor","restoreCursorColor","reverseIndex","keypadApplicationMode","keypadNumericMode","fullReset","setgLevel","selectDefaultCharset","flag","CHARSETS","selectCharset","screenAlignmentPattern","setErrorHandler","state","error","DcsHandler","requestStatusString","_preserveStack","_logSlowResolvingAsync","p","slowTimeout","slowPromise","_res","rej","race","then","err","console","_getCurrentLinkId","extended","urlId","wasPaused","DEBUG","prototype","String","fromCharCode","join","TRACE","trace","split","charCodeAt","clearRange","decode","substring","subarray","viewportEnd","viewportStart","chWidth","charset","screenReaderMode","wraparoundMode","decPrivateModes","wraparound","insertMode","modes","curAttr","bufferRow","lines","markDirty","getWidth","setCellFromCodepoint","precedingJoinState","pos","activeVersion","ch","currentInfo","charProperties","extractWidth","shouldJoin","extractShouldJoin","oldWidth","stringFromCodePoint","linkId","addLineToLink","oldRow","oldCol","_eraseAttrData","BufferLine","copyCellsFrom","addCodepointToCell","delta","insertCells","getNullCell","NULL_CELL_CODE","NULL_CELL_WIDTH","hasContent","ApcHandler","convertEol","reverseWraparound","_restrictCursor","hasWidth","originalX","nextStop","maxCol","origin","_setCursor","_moveCursor","diffToTop","diffToBottom","param","tabs","prevStop","bg","_eraseInBufferLine","clearWrap","respectProtect","replaceCells","_resetBufferLine","fill","clearMarkers","j","scrollOnEraseInDisplay","currentLine","getTrimmedLength","scrollBackSize","normal","isUserScrolling","row","scrollBottomRowsOffset","scrollBottomAbsolute","getBlankLine","deleteCells","joinState","text","getString","idata","itext","codePointAt","tlength","copyWithin","_is","XTERM_VERSION","term","termName","startsWith","applicationCursorKeys","setgCharset","DEFAULT_CHARSET","quirks","allowSetCursorBlink","cursorBlink","applicationKeypad","activeProtocol","sendFocus","activeEncoding","isCursorHidden","vtExtensions","kittyKeyboard","mainFlags","flags","altFlags","activateAltBuffer","isCursorInitialized","bracketedPasteMode","synchronizedOutput","colorSchemeQuery","colorSchemeUpdates","win32InputMode","activateNormalBuffer","ansi","dm","mouseProtocol","mouseEncoding","cs","active","alt","f","m","v","b2v","_updateAttrColor","color","mode","c1","c2","c3","AttributeData","fromColorRGB","_extractColor","attr","accu","cSpace","advance","hasSubParams","subparams","getSubParams","fg","underlineColor","_processUnderline","style","underlineStyle","updateExtended","_processSGR0","l","kittySgrBoldFaintControl","savedX","savedY","savedCurAttrData","savedCharset","cursorStyle","isBlinking","top","bottom","second","GET_WIN_SIZE_PIXELS","GET_CELL_SIZE_PIXELS","shift","savedCharsets","charsets","savedGlevel","glevel","savedOriginMode","savedWraparoundMode","slots","spec","exec","parseInt","isValidColorIndex","type","parseColor","trim","uri","_createHyperlink","_finishHyperlink","parsedParams","idParamIndex","findIndex","registerLink","_setOrReportSpecialColor","collectAndFlag","scrollRegionHeight","level","cell","CellData","content","yOffset","markAllDirty","b","s","isProtected","block","underline","bar","y1","y2","stack","altStack","mainStack","__decorate","__param","arg","_disposables","Set","isDisposed","o","_store","None","Object","freeze","_value","_targetWindow","isSafari","majorVersion","userAgent","match","isNode","process","navigator","platform","isFirefox","includes","isChrome","isLegacyEdge","test","isMac","isWindows","isLinux","isChromeOS","StringBuilder","_chunks","append","chunk","toString","_limit","_builder","limit","TaskQueue","logService","_tasks","_i","enqueue","task","_start","flush","_idleCallback","_cancelCallback","_requestCallback","_process","deadline","taskDuration","deadlineRemaining","longestTask","lastDeadlineRemaining","timeRemaining","performance","now","abs","round","PriorityTaskQueue","_createDeadline","duration","IdleTaskQueue","requestIdleCallback","cancelIdleCallback","_queue","bufferService","lastChar","CHAR_DATA_CODE_INDEX","WHITESPACE_CELL_CODE","ExtendedAttrs","toColorRGB","newObj","isInverse","isBold","isUnderline","hasExtendedAttrs","isBlink","isInvisible","isItalic","isDim","isStrikethrough","isOverline","getFgColorMode","getBgColorMode","isFgRGB","isBgRGB","isFgPalette","isBgPalette","isFgDefault","isBgDefault","isAttributeDefault","getFgColor","getBgColor","isEmpty","getUnderlineColor","getUnderlineColorMode","isUnderlineColorRGB","isUnderlineColorPalette","isUnderlineColorDefault","getUnderlineStyle","getUnderlineVariantOffset","underlineVariantOffset","ext","_urlId","_ext","val","CircularList_1","TaskQueue_1","BufferReflow_1","Marker_1","MAX_BUFFER_SIZE","Buffer","_hasScrollback","markers","_nullCell","fromCharData","NULL_CELL_CHAR","_whitespaceCell","WHITESPACE_CELL_CHAR","WHITESPACE_CELL_WIDTH","_isClearing","_memoryCleanupPosition","_cols","_rows","_getCorrectBufferLength","setupTabStops","_memoryCleanupQueue","clearAllMarkers","getWhitespaceCell","hasScrollback","isCursorInViewport","relativeY","correctBufferLength","scrollback","fillViewportRows","fillAttr","newCols","newRows","nullCell","dirtyMemoryLines","addToY","amountToTrim","_isReflowEnabled","_reflow","maxY","_batchedMemoryCleanup","normalRun","counted","cleanupMemory","_reflowLarger","_reflowSmaller","reflowCursorLine","toRemove","reflowLargerGetLinesToRemove","newLayoutResult","reflowLargerCreateNewLayout","reflowLargerApplyNewLayout","layout","_reflowLargerAdjustViewport","countRemoved","viewportAdjustments","toInsert","countToInsert","wrappedLines","unshift","absoluteY","lastLineLength","destLineLengths","reflowSmallerGetNewLineLengths","linesToAdd","trimmedLines","newLines","newLine","destLineIndex","destCol","srcLineIndex","srcCol","cellsToCopy","wrappedLinesIndex","getWrappedLineTrimmedLength","setCell","insertEvents","originalLines","originalLinesLength","originalLineIndex","nextToInsertIndex","nextToInsert","countInsertedSoFar","nextI","insertCountEmitted","translateBufferLineToString","lineIndex","trimRight","startCol","endCol","translateToString","getWrappedRangeForLine","first","last","tabStopWidth","addMarker","marker","Marker","onDispose","_removeMarker","$startIndex","$workCell","$extended","fillCellData","_combined","_extendedAttrs","_cacheValid","_cache","_cacheTrimmed","_data","cp","CHAR_DATA_ATTR_INDEX","CHAR_DATA_CHAR_INDEX","CHAR_DATA_WIDTH_INDEX","getFg","getBg","getCodePoint","isCombined","loadCell","combinedData","codePoint","width","attrs","$idx","byteLength","uint32Cells","keys","extKeys","copyFrom","blank","_copySparseMapsFrom","getNoBgTrimmedLength","src","applyInReverse","srcData","_copyCellMapsFrom","outColumns","isCanonical","trimEnd","cellContents","chars","srcStart","endsInNull","followingLineStartsWithWide","oldCols","bufferAbsoluteY","srcTrimmedTineLength","srcRemainingCells","destRemainingCells","countToRemove","nextToRemoveIndex","nextToRemoveStart","countRemovedSoFar","newLayout","newLayoutLines","newLineLengths","cellsNeeded","srcLine","cellsAvailable","oldTrimmedLength","endsWithWide","lineLength","Buffer_1","BufferSet","_normalBuffer","_altBuffer","_onBufferActivate","onSpecificOptionChange","_normal","_alt","inactiveBuffer","obj","setFromCharData","getChars","getCode","combined","getAsCharData","attributesEquals","other","thisDefault","otherDefault","DEFAULT_COLOR","DEFAULT_ATTR","DEFAULT_EXT","_id","_nextId","_onDispose","a","c","g","h","k","q","r","t","u","w","z","A","B","C","R","Q","K","Y","E","Z","H","_","codepoint","_interim","target","size","startPos","interim","Uint8Array","byte1","byte2","byte3","byte4","discardInterim","tmp","missing","fourStop","BMP_COMBINING","HIGH_COMBINING","table","version","wcwidth","num","ucs","mid","bisearch","preceding","createPropertyValue","Async_1","_action","_callbacks","_pendingData","_bufferOffset","_isSyncWriting","_syncCalls","_didUserInput","_innerWriteTimer","TimeoutTimer","didProcess","cb","_innerWrite","_scheduleInnerWrite","lastTime","startTime","continuation","catch","low","toLowerCase","RGB_REX","base","HASH_REX","adv","bits","pad","s2","StringBuilder_1","EMPTY_HANDLERS","_handlers","create","_active","_ident","_handlerFb","_stack","loopPosition","fallThrough","registerHandler","handlerList","handlerIndex","clearHandler","setHandlerFallback","put","utf32ToString","success","handlerResult","_handler","LimitedStringBuilder","_payloadLimit","_hitLimit","ret","res","Params_1","unhook","hook","EMPTY_PARAMS","Params","addParam","_params","TransitionTable","Uint16Array","setDefault","next","addMany","codes","NON_ASCII_PRINTABLE","VT500_TRANSITION_TABLE","blueprint","apply","unused","PRINTABLES","EXECUTABLES","states","_transitions","handlers","handlerPos","transition","chunkPos","initialState","currentState","_collect","_printHandlerFb","_executeHandlerFb","_csiHandlerFb","_escHandlerFb","_errorHandlerFb","_printHandler","_executeHandlers","_executeHandlersArr","_csiHandlers","_escHandlers","_oscParser","OscParser","_dcsParser","DcsParser","_apcParser","ApcParser","_errorHandler","_identifier","finalRange","intermediate","finalCode","reverse","clearPrintHandler","clearEscHandler","clearExecuteHandler","clearCsiHandler","clearDcsHandler","clearOscHandler","clearApcHandler","clearErrorHandler","resetZdm","csiDone","addDigit","addSubParam","l4","collect","abort","handlersEsc","jj","_state","_put","fromArray","values","maxSubParamsLength","Int32Array","_subParams","_subParamsLength","_subParamsIdx","_rejectDigits","_rejectSubDigits","_digitIsSub","newParams","getSubParamsAll","cur","_addons","instance","loadAddon","terminal","loadedAddon","_wrappedAddonDispose","activate","BufferLineApiView_1","_buffer","init","cursorY","cursorX","viewportY","baseY","getLine","BufferLineApiView","_line","getCell","startColumn","endColumn","BufferApiView_1","BufferNamespaceApi","_core","_onBufferChange","onBufferChange","BufferApiView","_alternate","alternate","addCsiHandler","addDcsHandler","addEscHandler","addOscHandler","provider","versions","BufferSet_1","colsChanged","rowsChanged","_cachedBlankLine","topRow","bottomRow","willBufferBeTrimmed","oldYdisp","_charsets","DEFAULT_MODES","DEFAULT_DEC_PRIVATE_MODES","_onUserInput","_onRequestScrollToBottom","showCursorImmediately","structuredClone","disableStdin","scrollOnUserInput","triggerBinaryEvent","ServiceRegistry_1","ServiceCollection","entries","_entries","Map","service","forEach","has","_services","IInstantiationService","getService","ctor","args","serviceDependencies","getServiceDependencies","sort","serviceArgs","dependency","name","firstServiceArgPos","optionsKeyToLogLevel","info","INFO","ERROR","off","OFF","_logLevel","_updateLogLevel","_evalLazyOptionalParams","optionalParams","_log","message","logger","log","DEFAULT_PROTOCOLS","NONE","restrict","X10","button","ctrl","VT200","DRAG","ANY","eventCode","isSGR","S","DEFAULT_ENCODINGS","DEFAULT","col","SGR","SGR_PIXELS","_protocols","_encodings","_activeProtocol","_activeEncoding","_onProtocolChange","onProtocolChange","addProtocol","addEncoding","protocol","encoding","areMouseEventsActive","setCustomWheelEventHandler","customWheelEventHandler","_customWheelEventHandler","allowCustomWheelEvent","restrictMouseEvent","encodeMouseEvent","isDefaultEncoding","isPixelEncoding","Platform_1","DEFAULT_OPTIONS","blinkIntervalDuration","cursorWidth","cursorInactiveStyle","drawBoldTextInBrightColors","documentOverride","fastScrollSensitivity","fontFamily","fontSize","fontWeight","fontWeightBold","ignoreBracketedPasteMode","lineHeight","letterSpacing","linkHandler","scrollbar","showScrollbar","scrollSensitivity","smoothScrollDuration","macOptionIsMeta","macOptionClickForcesSelection","minimumContrastRatio","mouseEventsRequireAlt","allowProposedApi","allowTransparency","theme","rescaleOverlappingGlyphs","rightClickSelectsWord","wordSeparator","altClickMovesCursor","FONT_WEIGHT_OPTIONS","_onOptionChange","onOptionChange","defaultOptions","newValue","_sanitizeAndValidateOption","_setupOptions","eventKey","getter","propName","setter","desc","defineProperty","isCursorStyle","floor","_entriesWithId","_dataByLinkId","_removeMarkerFromLink","castData","_getEntryIdKey","every","getLinkData","linkData","delete","serviceRegistry","decorator","arguments","storeServiceDependency","createDecorator","IDecorationService","_providers","_onChange","onChange","extractCharKind","_activeProvider","getStringCellWidth","precedingInfo","CoreTerminal_1","Terminal","_onBell","onBell","_onA11yCharEmitter","_onA11yTabEmitter","registerMarker","cursorYOffset","__webpack_module_cache__","moduleId","cachedModule","module","__webpack_modules__","BufferNamespaceApi_1","ParserApi_1","UnicodeApi_1","Terminal_1","AddonManager_1","CONSTRUCTOR_ONLY_OPTIONS","_addonManager","AddonManager","_publicOptions","_checkReadonlyOptions","_checkProposedApi","parser","ParserApi","unicode","UnicodeApi","mouseTrackingMode","applicationCursorKeysMode","applicationKeypadMode","originMode","reverseWraparoundMode","sendFocusMode","showCursor","synchronizedOutputMode","columns","_verifyIntegers","writeln","addon","Infinity"],"sourceRoot":""} +\ No newline at end of file +diff --git a/lib-headless/xterm-headless.mjs b/lib-headless/xterm-headless.mjs +index badc70c2eabdb28bc2ab839bb69823fbc9f04ecd..70944f20a386a7add78f4105ed0515b44459f19b 100644 +--- a/lib-headless/xterm-headless.mjs ++++ b/lib-headless/xterm-headless.mjs +@@ -15,6 +15,6 @@ + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + var Kt=Object.defineProperty;var qt=Object.getOwnPropertyDescriptor;var H=(n,i,e,t)=>{for(var r=t>1?void 0:t?qt(i,e):i,s=n.length-1,o;s>=0;s--)(o=n[s])&&(r=(t?o(i,e,r):o(r))||r);return t&&r&&Kt(i,e,r),r},R=(n,i)=>(e,t)=>i(e,t,n);function k(n){return n>65535?(n-=65536,String.fromCharCode((n>>10)+55296)+String.fromCharCode(n%1024+56320)):String.fromCharCode(n)}function N(n,i=0,e=n.length){let t="";for(let r=i;r65535?(s-=65536,t+=String.fromCharCode((s>>10)+55296)+String.fromCharCode(s%1024+56320)):t+=String.fromCharCode(s)}return t}var be=class{constructor(){this._interim=0}clear(){this._interim=0}decode(i,e){let t=i.length;if(!t)return 0;let r=0,s=0;if(this._interim){let o=i.charCodeAt(s++);56320<=o&&o<=57343?e[r++]=(this._interim-55296)*1024+o-56320+65536:(e[r++]=this._interim,e[r++]=o),this._interim=0}for(let o=s;o=t)return this._interim=l,r;let c=i.charCodeAt(o);56320<=c&&c<=57343?e[r++]=(l-55296)*1024+c-56320+65536:(e[r++]=l,e[r++]=c);continue}l!==65279&&(e[r++]=l)}return r}},me=class{constructor(){this.interim=new Uint8Array(3)}clear(){this.interim.fill(0)}decode(i,e){let t=i.length;if(!t)return 0;let r=0,s,o,l,c,h,u=0;if(this.interim[0]){let _=!1,p=this.interim[0];p&=(p&224)===192?31:(p&240)===224?15:7;let v=0,d;for(;(d=this.interim[++v])&&v<4;)p<<=6,p|=d&63;let b=(this.interim[0]&224)===192?2:(this.interim[0]&240)===224?3:4,S=b-v;for(;u=t)return 0;if(d=i[u++],(d&192)!==128){u--,_=!0;break}else this.interim[v++]=d,p<<=6,p|=d&63}_||(b===2?p<128?u--:e[r++]=p:b===3?p<2048||p>=55296&&p<=57343||p===65279||(e[r++]=p):p<65536||p>1114111||(e[r++]=p)),this.interim.fill(0)}let a=t-4,f=u;for(;f=t)return this.interim[0]=s,r;if(o=i[f++],(o&192)!==128){f--;continue}if(h=(s&31)<<6|o&63,h<128){f--;continue}e[r++]=h}else if((s&240)===224){if(f>=t)return this.interim[0]=s,r;if(o=i[f++],(o&192)!==128){f--;continue}if(f>=t)return this.interim[0]=s,this.interim[1]=o,r;if(l=i[f++],(l&192)!==128){f--;continue}if(h=(s&15)<<12|(o&63)<<6|l&63,h<2048||h>=55296&&h<=57343||h===65279)continue;e[r++]=h}else if((s&248)===240){if(f>=t)return this.interim[0]=s,r;if(o=i[f++],(o&192)!==128){f--;continue}if(f>=t)return this.interim[0]=s,this.interim[1]=o,r;if(l=i[f++],(l&192)!==128){f--;continue}if(f>=t)return this.interim[0]=s,this.interim[1]=o,this.interim[2]=l,r;if(c=i[f++],(c&192)!==128){f--;continue}if(h=(s&7)<<18|(o&63)<<12|(l&63)<<6|c&63,h<65536||h>1114111)continue;e[r++]=h}}return r}};var G=class n{constructor(){this.fg=0;this.bg=0;this.extended=new K}static toColorRGB(i){return[i>>>16&255,i>>>8&255,i&255]}static fromColorRGB(i){return(i[0]&255)<<16|(i[1]&255)<<8|i[2]&255}clone(){let i=new n;return i.fg=this.fg,i.bg=this.bg,i.extended=this.extended.clone(),i}isInverse(){return this.fg&67108864}isBold(){return this.fg&134217728}isUnderline(){return this.hasExtendedAttrs()&&this.extended.underlineStyle!==0?1:this.fg&268435456}isBlink(){return this.fg&536870912}isInvisible(){return this.fg&1073741824}isItalic(){return this.bg&67108864}isDim(){return this.bg&134217728}isStrikethrough(){return this.fg&2147483648}isProtected(){return this.bg&536870912}isOverline(){return this.bg&1073741824}getFgColorMode(){return this.fg&50331648}getBgColorMode(){return this.bg&50331648}isFgRGB(){return(this.fg&50331648)===50331648}isBgRGB(){return(this.bg&50331648)===50331648}isFgPalette(){return(this.fg&50331648)===16777216||(this.fg&50331648)===33554432}isBgPalette(){return(this.bg&50331648)===16777216||(this.bg&50331648)===33554432}isFgDefault(){return(this.fg&50331648)===0}isBgDefault(){return(this.bg&50331648)===0}isAttributeDefault(){return this.fg===0&&this.bg===0}getFgColor(){switch(this.fg&50331648){case 16777216:case 33554432:return this.fg&255;case 50331648:return this.fg&16777215;default:return-1}}getBgColor(){switch(this.bg&50331648){case 16777216:case 33554432:return this.bg&255;case 50331648:return this.bg&16777215;default:return-1}}hasExtendedAttrs(){return this.bg&268435456}updateExtended(){this.extended.isEmpty()?this.bg&=-268435457:this.bg|=268435456}getUnderlineColor(){if(this.bg&268435456&&~this.extended.underlineColor)switch(this.extended.underlineColor&50331648){case 16777216:case 33554432:return this.extended.underlineColor&255;case 50331648:return this.extended.underlineColor&16777215;default:return this.getFgColor()}return this.getFgColor()}getUnderlineColorMode(){return this.bg&268435456&&~this.extended.underlineColor?this.extended.underlineColor&50331648:this.getFgColorMode()}isUnderlineColorRGB(){return this.bg&268435456&&~this.extended.underlineColor?(this.extended.underlineColor&50331648)===50331648:this.isFgRGB()}isUnderlineColorPalette(){return this.bg&268435456&&~this.extended.underlineColor?(this.extended.underlineColor&50331648)===16777216||(this.extended.underlineColor&50331648)===33554432:this.isFgPalette()}isUnderlineColorDefault(){return this.bg&268435456&&~this.extended.underlineColor?(this.extended.underlineColor&50331648)===0:this.isFgDefault()}getUnderlineStyle(){return this.fg&268435456?this.bg&268435456?this.extended.underlineStyle:1:0}getUnderlineVariantOffset(){return this.extended.underlineVariantOffset}},K=class n{constructor(i=0,e=0){this._ext=0;this._urlId=0;this._ext=i,this._urlId=e}get ext(){return this._urlId?this._ext&-469762049|this.underlineStyle<<26:this._ext}set ext(i){this._ext=i}get underlineStyle(){return this._urlId?5:(this._ext&469762048)>>26}set underlineStyle(i){this._ext&=-469762049,this._ext|=i<<26&469762048}get underlineColor(){return this._ext&67108863}set underlineColor(i){this._ext&=-67108864,this._ext|=i&67108863}get urlId(){return this._urlId}set urlId(i){this._urlId=i}get underlineVariantOffset(){let i=(this._ext&3758096384)>>29;return i<0?i^4294967288:i}set underlineVariantOffset(i){this._ext&=536870911,this._ext|=i<<29&3758096384}clone(){return new n(this._ext,this._urlId)}isEmpty(){return this.underlineStyle===0&&this._urlId===0}};var P=class n extends G{constructor(){super(...arguments);this.content=0;this.fg=0;this.bg=0;this.extended=new K;this.combinedData=""}static fromCharData(e){let t=new n;return t.setFromCharData(e),t}isCombined(){return this.content&2097152}getWidth(){return this.content>>22}getChars(){return this.content&2097152?this.combinedData:this.content&2097151?k(this.content&2097151):""}getCode(){return this.isCombined()?this.combinedData.charCodeAt(this.combinedData.length-1):this.content&2097151}setFromCharData(e){this.fg=e[0],this.bg=0;let t=!1;if(e[1].length>2)t=!0;else if(e[1].length===2){let r=e[1].charCodeAt(0);if(55296<=r&&r<=56319){let s=e[1].charCodeAt(1);56320<=s&&s<=57343?this.content=(r-55296)*1024+s-56320+65536|e[2]<<22:t=!0}else t=!0}else this.content=e[1].charCodeAt(0)|e[2]<<22;t&&(this.combinedData=e[1],this.content=2097152|e[2]<<22)}getAsCharData(){return[this.fg,this.getChars(),this.getWidth(),this.getCode()]}attributesEquals(e){if(this.getFgColorMode()!==e.getFgColorMode()||this.getFgColor()!==e.getFgColor()||this.getBgColorMode()!==e.getBgColorMode()||this.getBgColor()!==e.getBgColor()||this.isInverse()!==e.isInverse()||this.isBold()!==e.isBold()||this.isUnderline()!==e.isUnderline())return!1;if(this.isUnderline()){if(this.getUnderlineStyle()!==e.getUnderlineStyle())return!1;let t=this.isUnderlineColorDefault(),r=e.isUnderlineColorDefault();if(!(t&&r)&&(t!==r||this.getUnderlineColor()!==e.getUnderlineColor()||this.getUnderlineColorMode()!==e.getUnderlineColorMode()))return!1}return!(this.isOverline()!==e.isOverline()||this.isBlink()!==e.isBlink()||this.isInvisible()!==e.isInvisible()||this.isItalic()!==e.isItalic()||this.isDim()!==e.isDim()||this.isStrikethrough()!==e.isStrikethrough())}};var ve=class{constructor(i){this._line=i}get isWrapped(){return this._line.isWrapped}get length(){return this._line.length}getCell(i,e){if(!(i<0||i>=this._line.length))return e?(this._line.loadCell(i,e),e):this._line.loadCell(i,new P)}translateToString(i,e,t){return this._line.translateToString(i,e,t)}};var se=class{constructor(i,e){this._buffer=i;this.type=e}init(i){return this._buffer=i,this}get cursorY(){return this._buffer.y}get cursorX(){return this._buffer.x}get viewportY(){return this._buffer.ydisp}get baseY(){return this._buffer.ybase}get length(){return this._buffer.lines.length}getLine(i){let e=this._buffer.lines.get(i);if(e)return new ve(e)}getNullCell(){return new P}};function D(n){return{dispose:n}}function dt(n){if(!n)return n;if(Array.isArray(n)){for(let i of n)i.dispose();return[]}return n.dispose(),n}var ne=class{constructor(){this._disposables=new Set;this._isDisposed=!1}get isDisposed(){return this._isDisposed}add(i){return this._isDisposed?i.dispose():this._disposables.add(i),i}dispose(){if(!this._isDisposed){this._isDisposed=!0;for(let i of this._disposables)i.dispose();this._disposables.clear()}}clear(){for(let i of this._disposables)i.dispose();this._disposables.clear()}},I=class{constructor(){this._store=new ne}dispose(){this._store.dispose()}_register(i){return this._store.add(i)}};I.None=Object.freeze({dispose(){}});var Y=class{constructor(){this._isDisposed=!1}get value(){return this._isDisposed?void 0:this._value}set value(i){this._isDisposed||i===this._value||(this._value?.dispose(),this._value=i)}clear(){this.value=void 0}dispose(){this._isDisposed=!0,this._value?.dispose(),this._value=void 0}};var m=class{constructor(){this._listeners=[];this._disposed=!1}get event(){return this._event?this._event:(this._event=(i,e,t)=>{if(this._disposed)return D(()=>{});let r={fn:i,thisArgs:e};this._listeners=this._listeners.slice(),this._listeners.push(r);let s=D(()=>{let o=this._listeners.indexOf(r);o!==-1&&(this._listeners=this._listeners.slice(),this._listeners.splice(o,1))});return t&&(Array.isArray(t)?t.push(s):t.add(s)),s},this._event)}fire(i){if(this._disposed||!this._listeners.length)return;if(this._listeners.length===1){this._listeners[0].fn.call(this._listeners[0].thisArgs,i);return}let e=this._listeners;for(let t=0,r=e.length;t{function n(s,o){return s(l=>o.fire(l))}r.forward=n;function i(s,o){return(l,c,h)=>s(u=>l.call(c,o(u)),void 0,h)}r.map=i;function e(...s){return(o,l,c)=>{let h=new ne;for(let u of s)h.add(u(a=>o.call(l,a)));return c&&(Array.isArray(c)?c.push(h):c.add(h)),h}}r.any=e;function t(s,o,l){return o(l),s(c=>o(c))}r.runAndSubscribe=t})(B||={});var Ie=class extends I{constructor(e){super();this._core=e;this._onBufferChange=this._register(new m);this.onBufferChange=this._onBufferChange.event;this._normal=new se(this._core.buffers.normal,"normal"),this._alternate=new se(this._core.buffers.alt,"alternate"),this._register(this._core.buffers.onBufferActivate(()=>this._onBufferChange.fire(this.active)))}get active(){if(this._core.buffers.active===this._core.buffers.normal)return this.normal;if(this._core.buffers.active===this._core.buffers.alt)return this.alternate;throw new Error("Active buffer is neither normal nor alternate")}get normal(){return this._normal.init(this._core.buffers.normal)}get alternate(){return this._alternate.init(this._core.buffers.alt)}};var ge=class{constructor(i){this._core=i}registerCsiHandler(i,e){return this._core.registerCsiHandler(i,t=>e(t.toArray()))}addCsiHandler(i,e){return this.registerCsiHandler(i,e)}registerDcsHandler(i,e){return this._core.registerDcsHandler(i,(t,r)=>e(t,r.toArray()))}addDcsHandler(i,e){return this.registerDcsHandler(i,e)}registerEscHandler(i,e){return this._core.registerEscHandler(i,e)}addEscHandler(i,e){return this.registerEscHandler(i,e)}registerOscHandler(i,e){return this._core.registerOscHandler(i,e)}addOscHandler(i,e){return this.registerOscHandler(i,e)}registerApcHandler(i,e){return this._core.registerApcHandler(i,e)}};var Se=class{constructor(i){this._core=i}register(i){this._core.unicodeService.register(i)}get versions(){return this._core.unicodeService.versions}get activeVersion(){return this._core.unicodeService.activeVersion}set activeVersion(i){this._core.unicodeService.activeVersion=i}};var C=Object.freeze(new G),Ce=0,ft=new P,Ye=C.extended.clone(),U=class n{constructor(i,e,t=!1){this.isWrapped=t;this._combined={};this._extendedAttrs={};this._cacheValid=!1;this._cache="";this._cacheTrimmed=!1;this._data=new Uint32Array(i*3);let r=e??P.fromCharData([0,"",1,0]);for(let s=0;s>22,e&2097152?this._combined[i].charCodeAt(this._combined[i].length-1):t]}set(i,e){this._cacheValid=!1,this._data[i*3+1]=e[0],e[1].length>1?(this._combined[i]=e[1],this._data[i*3+0]=i|2097152|e[2]<<22):this._data[i*3+0]=e[1].charCodeAt(0)|e[2]<<22}getWidth(i){return this._data[i*3+0]>>22}hasWidth(i){return this._data[i*3+0]&12582912}getFg(i){return this._data[i*3+1]}getBg(i){return this._data[i*3+2]}hasContent(i){return this._data[i*3+0]&4194303}getCodePoint(i){let e=this._data[i*3+0];return e&2097152?this._combined[i].charCodeAt(this._combined[i].length-1):e&2097151}isCombined(i){return this._data[i*3+0]&2097152}getString(i){let e=this._data[i*3+0];return e&2097152?this._combined[i]:e&2097151?k(e&2097151):""}isProtected(i){return this._data[i*3+2]&536870912}loadCell(i,e){return Ce=i*3,e.content=this._data[Ce+0],e.fg=this._data[Ce+1],e.bg=this._data[Ce+2],e.content&2097152?e.combinedData=this._combined[i]:e.combinedData="",e.bg&268435456?e.extended=this._extendedAttrs[i]:(Ye._ext=0,Ye._urlId=0,e.extended=Ye),e}setCell(i,e){this._cacheValid=!1,e.content&2097152&&(this._combined[i]=e.combinedData),e.bg&268435456&&(this._extendedAttrs[i]=e.extended),this._data[i*3+0]=e.content,this._data[i*3+1]=e.fg,this._data[i*3+2]=e.bg}setCellFromCodepoint(i,e,t,r){this._cacheValid=!1,r.bg&268435456&&(this._extendedAttrs[i]=r.extended);let s=i*3;this._data[s+0]=e|t<<22,this._data[s+1]=r.fg,this._data[s+2]=r.bg}addCodepointToCell(i,e,t){this._cacheValid=!1;let r=this._data[i*3+0];r&2097152?this._combined[i]+=k(e):r&2097151?(this._combined[i]=k(r&2097151)+k(e),r&=-2097152,r|=2097152):r=e|1<<22,t&&(r&=-12582913,r|=t<<22),this._data[i*3+0]=r}insertCells(i,e,t){if(this._cacheValid=!1,i%=this.length,i&&this.getWidth(i-1)===2&&this.setCellFromCodepoint(i-1,0,1,t),e=0;--r)this.setCell(i+e+r,this.loadCell(i+r,ft));for(let r=0;rthis.length){if(this._data.buffer.byteLength>=t*4)this._data=new Uint32Array(this._data.buffer,0,t);else{let r=new Uint32Array(t);r.set(this._data),this._data=r}for(let r=this.length;r=i&&delete this._combined[l]}let s=Object.keys(this._extendedAttrs);for(let o=0;o=i&&delete this._extendedAttrs[l]}}return this.length=i,t*4*2=0;--i)if(this._data[i*3+0]&4194303)return i+(this._data[i*3+0]>>22);return 0}getNoBgTrimmedLength(){for(let i=this.length-1;i>=0;--i)if(this._data[i*3+0]&4194303||this._data[i*3+2]&50331648)return i+(this._data[i*3+0]>>22);return 0}copyCellsFrom(i,e,t,r,s){this._cacheValid=!1;let o=i._data;if(s)for(let l=r-1;l>=0;l--){for(let c=0;c<3;c++)this._data[(t+l)*3+c]=o[(e+l)*3+c];this._copyCellMapsFrom(i,e+l,t+l)}else for(let l=0;l>22||1}r&&r.push(e);let l=o.join("");return s&&(this._cache=l,this._cacheValid=!0,this._cacheTrimmed=!!i),l}_copyCellMapsFrom(i,e,t){let r=e*3;i._data[r+0]&2097152&&(this._combined[t]=i._combined[e]),i._data[r+2]&268435456&&(this._extendedAttrs[t]=i._extendedAttrs[e])}_copySparseMapsFrom(i){this._combined={},this._extendedAttrs={};for(let e=0;eo.index-l.index),r=[];for(let o of t){let l=this._services.get(o.id);if(!l)throw new Error(`[createInstance] ${i.name} depends on UNKNOWN service ${o.id._id}.`);r.push(l)}let s=t.length>0?t[0].index:e.length;if(e.length!==s)throw new Error(`[createInstance] First service dependency of ${i.name} at position ${s+1} conflicts with ${e.length} static arguments`);return new i(...e,...r)}};var $t={trace:0,debug:1,info:2,warn:3,error:4,off:5},jt="xterm.js: ",Z=class extends I{constructor(e){super();this._optionsService=e;this._logLevel=5;this._updateLogLevel(),this._register(this._optionsService.onSpecificOptionChange("logLevel",()=>this._updateLogLevel()))}get logLevel(){return this._logLevel}_updateLogLevel(){this._logLevel=$t[this._optionsService.rawOptions.logLevel]}_evalLazyOptionalParams(e){for(let t=0;tthis._length)for(let t=this._length;t=e;s--)this._array[this._getCyclicIndex(s+r.length)]=this._array[this._getCyclicIndex(s)];for(let s=0;sthis._maxLength){let s=this._length+r.length-this._maxLength;this._startIndex+=s,this._length=this._maxLength,this.onTrimEmitter.fire(s)}else this._length+=r.length}trimStart(e){e>this._length&&(e=this._length),this._startIndex+=e,this._length-=e,this.onTrimEmitter.fire(e)}shiftElements(e,t,r){if(!(t<=0)){if(e<0||e>=this._length)throw new Error("start argument out of range");if(e+r<0)throw new Error("Cannot shift elements in list beyond index 0");if(r>0){for(let o=t-1;o>=0;o--)this.set(e+o+r,this.get(e+o));let s=e+t+r-this._length;if(s>0)for(this._length+=s;this._length>this._maxLength;)this._length--,this._startIndex++,this.onTrimEmitter.fire(1)}else for(let s=0;ss){r-e<-20&&this._logService.warn(`task queue exceeded allotted deadline by ${Math.abs(Math.round(r-e))}ms`),this._start();return}r=s}this.clear()}},Je=class extends Ee{_requestCallback(i){return setTimeout(()=>i(this._createDeadline(16)))}_cancelCallback(i){clearTimeout(i)}_createDeadline(i){let e=performance.now()+i;return{timeRemaining:()=>Math.max(0,e-performance.now())}}},Qe=class extends Ee{_requestCallback(i){return requestIdleCallback(i)}_cancelCallback(i){cancelIdleCallback(i)}},Ct="requestIdleCallback"in globalThis?Qe:Je;function xt(n,i,e,t,r,s){let o=[];for(let l=0;l=l&&t0&&(d>a||u[d].getTrimmedLength()===0);d--)v++;v>0&&(o.push(l+u.length-v),o.push(v)),l+=u.length-1}return o}function Et(n,i){let e=[],t=0,r=i[t],s=0;for(let o=0;oc&&(s-=c,o++);let h=n[o].getWidth(s-1)===2;h&&s--;let u=h?e-1:e;t.push(u),l+=u}return t}function J(n,i,e){if(i===n.length-1)return n[i].getTrimmedLength();let t=!n[i].hasContent(e-1)&&n[i].getWidth(e-1)===1,r=n[i+1].getWidth(0)===2;return t&&r?e-1:e}var Te=class Te{constructor(i){this.line=i;this.isDisposed=!1;this._disposables=[];this._id=Te._nextId++;this._onDispose=this.register(new m);this.onDispose=this._onDispose.event}get id(){return this._id}dispose(){this.isDisposed||(this.isDisposed=!0,this.line=-1,this._onDispose.fire(),dt(this._disposables),this._disposables.length=0)}register(i){return this._disposables.push(i),i}};Te._nextId=1;var ye=Te;var x={},W=x.B;x[0]={"`":"\u25C6",a:"\u2592",b:"\u2409",c:"\u240C",d:"\u240D",e:"\u240A",f:"\xB0",g:"\xB1",h:"\u2424",i:"\u240B",j:"\u2518",k:"\u2510",l:"\u250C",m:"\u2514",n:"\u253C",o:"\u23BA",p:"\u23BB",q:"\u2500",r:"\u23BC",s:"\u23BD",t:"\u251C",u:"\u2524",v:"\u2534",w:"\u252C",x:"\u2502",y:"\u2264",z:"\u2265","{":"\u03C0","|":"\u2260","}":"\xA3","~":"\xB7"};x.A={"#":"\xA3"};x.B=void 0;x[4]={"#":"\xA3","@":"\xBE","[":"ij","\\":"\xBD","]":"|","{":"\xA8","|":"f","}":"\xBC","~":"\xB4"};x.C=x[5]={"[":"\xC4","\\":"\xD6","]":"\xC5","^":"\xDC","`":"\xE9","{":"\xE4","|":"\xF6","}":"\xE5","~":"\xFC"};x.R={"#":"\xA3","@":"\xE0","[":"\xB0","\\":"\xE7","]":"\xA7","{":"\xE9","|":"\xF9","}":"\xE8","~":"\xA8"};x.Q={"@":"\xE0","[":"\xE2","\\":"\xE7","]":"\xEA","^":"\xEE","`":"\xF4","{":"\xE9","|":"\xF9","}":"\xE8","~":"\xFB"};x.K={"@":"\xA7","[":"\xC4","\\":"\xD6","]":"\xDC","{":"\xE4","|":"\xF6","}":"\xFC","~":"\xDF"};x.Y={"#":"\xA3","@":"\xA7","[":"\xB0","\\":"\xE7","]":"\xE9","`":"\xF9","{":"\xE0","|":"\xF2","}":"\xE8","~":"\xEC"};x.E=x[6]={"@":"\xC4","[":"\xC6","\\":"\xD8","]":"\xC5","^":"\xDC","`":"\xE4","{":"\xE6","|":"\xF8","}":"\xE5","~":"\xFC"};x.Z={"#":"\xA3","@":"\xA7","[":"\xA1","\\":"\xD1","]":"\xBF","{":"\xB0","|":"\xF1","}":"\xE7"};x.H=x[7]={"@":"\xC9","[":"\xC4","\\":"\xD6","]":"\xC5","^":"\xDC","`":"\xE9","{":"\xE4","|":"\xF6","}":"\xE5","~":"\xFC"};x["="]={"#":"\xF9","@":"\xE0","[":"\xE9","\\":"\xE7","]":"\xEA","^":"\xEE",_:"\xE8","`":"\xF4","{":"\xE4","|":"\xF6","}":"\xFC","~":"\xFB"};var Dt=4294967295,le=class extends I{constructor(e,t,r,s){super();this._hasScrollback=e;this._optionsService=t;this._bufferService=r;this._logService=s;this.ydisp=0;this.ybase=0;this.y=0;this.x=0;this.tabs={};this.savedY=0;this.savedX=0;this.savedCurAttrData=C.clone();this.savedCharset=W;this.savedCharsets=[];this.savedGlevel=0;this.savedOriginMode=!1;this.savedWraparoundMode=!0;this.markers=[];this._nullCell=P.fromCharData([0,"",1,0]);this._whitespaceCell=P.fromCharData([0," ",1,32]);this._isClearing=!1;this._memoryCleanupPosition=0;this._cols=this._bufferService.cols,this._rows=this._bufferService.rows,this.lines=new oe(this._getCorrectBufferLength(this._rows)),this.scrollTop=0,this.scrollBottom=this._rows-1,this.setupTabStops(),this._memoryCleanupQueue=new Ct(this._logService),this._register(D(()=>this._memoryCleanupQueue.clear())),this._register(D(()=>this.clearAllMarkers()))}getNullCell(e){return e?(this._nullCell.fg=e.fg,this._nullCell.bg=e.bg,this._nullCell.extended=e.extended):(this._nullCell.fg=0,this._nullCell.bg=0,this._nullCell.extended=new K),this._nullCell}getWhitespaceCell(e){return e?(this._whitespaceCell.fg=e.fg,this._whitespaceCell.bg=e.bg,this._whitespaceCell.extended=e.extended):(this._whitespaceCell.fg=0,this._whitespaceCell.bg=0,this._whitespaceCell.extended=new K),this._whitespaceCell}getBlankLine(e,t){return new U(this._bufferService.cols,this.getNullCell(e),t)}get hasScrollback(){return this._hasScrollback&&this.lines.maxLength>this._rows}get isCursorInViewport(){let t=this.ybase+this.y-this.ydisp;return t>=0&&tDt?Dt:t}fillViewportRows(e){if(this.lines.length===0){e??=C;let t=this._rows;for(;t--;)this.lines.push(this.getBlankLine(e))}}clear(){this.ydisp=0,this.ybase=0,this.y=0,this.x=0,this.lines=new oe(this._getCorrectBufferLength(this._rows)),this.scrollTop=0,this.scrollBottom=this._rows-1,this.setupTabStops()}resize(e,t){let r=this.getNullCell(C),s=0,o=this._getCorrectBufferLength(t);if(o>this.lines.maxLength&&(this.lines.maxLength=o),this.lines.length>0){if(this._cols0&&this.lines.length<=this.ybase+this.y+l+1?(this.ybase--,l++,this.ydisp>0&&this.ydisp--):this.lines.push(new U(e,r,!1)));else for(let c=this._rows;c>t;c--)this.lines.length>t+this.ybase&&(this.lines.length>this.ybase+this.y+1?this.lines.pop():(this.ybase++,this.ydisp++));if(o0&&(this.lines.trimStart(c),this.ybase=Math.max(this.ybase-c,0),this.ydisp=Math.max(this.ydisp-c,0),this.savedY=Math.max(this.savedY-c,0)),this.lines.maxLength=o}this.x=Math.min(this.x,e-1),this.y=Math.min(this.y,t-1),l&&(this.y+=l),this.savedX=Math.min(this.savedX,e-1),this.scrollTop=0}if(this.scrollBottom=t-1,this._isReflowEnabled&&(this._reflow(e,t),this._cols>e))for(let l=0;l0){let l=Math.max(0,this.lines.length-this.ybase-1);this.y=Math.min(this.y,l)}this._memoryCleanupQueue.clear(),s>.1*this.lines.length&&(this._memoryCleanupPosition=0,this._memoryCleanupQueue.enqueue(()=>this._batchedMemoryCleanup()))}_batchedMemoryCleanup(){let e=!0;this._memoryCleanupPosition>=this.lines.length&&(this._memoryCleanupPosition=0,e=!1);let t=0;for(;this._memoryCleanupPosition100)return!0;return e}get _isReflowEnabled(){let e=this._optionsService.rawOptions.windowsPty;return e&&e.buildNumber?this._hasScrollback&&e.backend==="conpty"&&e.buildNumber>=21376:this._hasScrollback}_reflow(e,t){this._cols!==e&&(e>this._cols?this._reflowLarger(e,t):this._reflowSmaller(e,t))}_reflowLarger(e,t){let r=this._optionsService.rawOptions.reflowCursorLine,s=xt(this.lines,this._cols,e,this.ybase+this.y,this.getNullCell(C),r);if(s.length>0){let o=Et(this.lines,s);yt(this.lines,o.layout),this._reflowLargerAdjustViewport(e,t,o.countRemoved)}}_reflowLargerAdjustViewport(e,t,r){let s=this.getNullCell(C),o=r;for(;o-- >0;)this.ybase===0?(this.y>0&&this.y--,this.lines.length=0;c--){let h=this.lines.get(c);if(!h||!h.isWrapped&&h.getTrimmedLength()<=e)continue;let u=[h];for(;h.isWrapped&&c>0;)h=this.lines.get(--c),u.unshift(h);if(!r){let g=this.ybase+this.y;if(g>=c&&g0&&(o.push({start:c+u.length+l,newLines:v}),l+=v.length),u.push(...v);let d=f.length-1,b=f[d];b===0&&(d--,b=f[d]);let S=u.length-_-1,O=a;for(;S>=0;){let g=Math.min(O,b);if(u[d]===void 0)break;if(u[d].copyCellsFrom(u[S],O-g,b-g,g,!0),b-=g,b===0&&(d--,b=f[d]),O-=g,O===0){S--;let A=Math.max(S,0);O=J(u,A,this._cols)}}for(let g=0;g0;)this.ybase===0?this.y0){let c=[],h=[];for(let b=0;b=0;b--)if(_&&_.start>a+p){for(let S=_.newLines.length-1;S>=0;S--)this.lines.set(b--,_.newLines[S]);b++,c.push({index:a+1,amount:_.newLines.length}),p+=_.newLines.length,_=o[++f]}else this.lines.set(b,h[a--]);let v=0;for(let b=c.length-1;b>=0;b--)c[b].index+=v,this.lines.onInsertEmitter.fire(c[b]),v+=c[b].amount;let d=Math.max(0,u+l-this.lines.maxLength);d>0&&this.lines.onTrimEmitter.fire(d)}}translateBufferLineToString(e,t,r=0,s){let o=this.lines.get(e);return o?o.translateToString(t,r,s):""}getWrappedRangeForLine(e){let t=e,r=e;for(;t>0&&this.lines.get(t).isWrapped;)t--;for(;r+10;);return e>=this._cols?this._cols-1:e<0?0:e}nextStop(e){for(e??=this.x;!this.tabs[++e]&&e=this._cols?this._cols-1:e<0?0:e}clearMarkers(e){this._isClearing=!0;for(let t=0;t{t.line-=r,t.line<0&&t.dispose()})),t.register(this.lines.onInsert(r=>{t.line>=r.index&&(t.line+=r.amount)})),t.register(this.lines.onDelete(r=>{t.line>=r.index&&t.liner.index&&(t.line-=r.amount)})),t.register(t.onDispose(()=>this._removeMarker(t))),t}_removeMarker(e){this._isClearing||this.markers.splice(this.markers.indexOf(e),1)}};var De=class extends I{constructor(e,t,r){super();this._optionsService=e;this._bufferService=t;this._logService=r;this._normalBuffer=this._register(new Y);this._altBuffer=this._register(new Y);this._onBufferActivate=this._register(new m);this.onBufferActivate=this._onBufferActivate.event;this.reset(),this._register(this._optionsService.onSpecificOptionChange("scrollback",()=>this.resize(this._bufferService.cols,this._bufferService.rows))),this._register(this._optionsService.onSpecificOptionChange("tabStopWidth",()=>this.setupTabStops()))}reset(){this._normal=new le(!0,this._optionsService,this._bufferService,this._logService),this._normalBuffer.value=this._normal,this._normal.fillViewportRows(),this._alt=new le(!1,this._optionsService,this._bufferService,this._logService),this._altBuffer.value=this._alt,this._activeBuffer=this._normal,this._onBufferActivate.fire({activeBuffer:this._normal,inactiveBuffer:this._alt}),this.setupTabStops()}get alt(){return this._alt}get active(){return this._activeBuffer}get normal(){return this._normal}activateNormalBuffer(){this._activeBuffer!==this._normal&&(this._normal.x=this._alt.x,this._normal.y=this._alt.y,this._alt.clearAllMarkers(),this._alt.clear(),this._activeBuffer=this._normal,this._onBufferActivate.fire({activeBuffer:this._normal,inactiveBuffer:this._alt}))}activateAltBuffer(e){this._activeBuffer!==this._alt&&(this._alt.fillViewportRows(e),this._alt.x=this._normal.x,this._alt.y=this._normal.y,this._activeBuffer=this._alt,this._onBufferActivate.fire({activeBuffer:this._alt,inactiveBuffer:this._normal}))}resize(e,t){this._normal.resize(e,t),this._alt.resize(e,t),this.setupTabStops(e)}setupTabStops(e){this._normal.setupTabStops(e),this._alt.setupTabStops(e)}};var Q=class extends I{constructor(e,t){super();this.isUserScrolling=!1;this._onResize=this._register(new m);this.onResize=this._onResize.event;this._onScroll=this._register(new m);this.onScroll=this._onScroll.event;this.cols=Math.max(e.rawOptions.cols||0,2),this.rows=Math.max(e.rawOptions.rows||0,1),this.buffers=this._register(new De(e,this,t)),this._register(this.buffers.onBufferActivate(r=>{this._onScroll.fire(r.activeBuffer.ydisp)}))}get buffer(){return this.buffers.active}resize(e,t){let r=this.cols!==e,s=this.rows!==t;this.cols=e,this.rows=t,this.buffers.resize(e,t),this._onResize.fire({cols:e,rows:t,colsChanged:r,rowsChanged:s})}reset(){this.buffers.reset(),this.isUserScrolling=!1}scroll(e,t=!1){let r=this.buffer,s;s=this._cachedBlankLine,(!s||s.length!==this.cols||s.getFg(0)!==e.fg||s.getBg(0)!==e.bg)&&(s=r.getBlankLine(e,t),this._cachedBlankLine=s),s.isWrapped=t;let o=r.ybase+r.scrollTop,l=r.ybase+r.scrollBottom;if(r.scrollTop===0){let c=r.lines.isFull;l===r.lines.length-1?c?r.lines.recycle().copyFrom(s,!0):r.lines.push(s.clone(!0)):r.lines.splice(l+1,0,s.clone(!0)),c?this.isUserScrolling&&(r.ydisp=Math.max(r.ydisp-1,0)):(r.ybase++,this.isUserScrolling||r.ydisp++)}else{let c=l-o+1;r.lines.shiftElements(o+1,c-1,-1),r.lines.set(l,s.clone(!0))}this.isUserScrolling||(r.ydisp=r.ybase),this._onScroll.fire(r.ydisp)}scrollLines(e,t){let r=this.buffer;if(e<0){if(r.ydisp===0)return;this.isUserScrolling=!0}else e+r.ydisp>=r.ybase&&(this.isUserScrolling=!1);let s=r.ydisp;r.ydisp=Math.max(Math.min(r.ydisp+e,r.ybase),0),s!==r.ydisp&&(t||this._onScroll.fire(r.ydisp))}};Q=H([R(0,V),R(1,z)],Q);var At=!!(typeof process<"u"&&"title"in process&&(typeof navigator>"u"||navigator.userAgent.startsWith("Node.js/"))),ce=At?"node":navigator.userAgent,tt=At?"node":navigator.platform,Pr=ce.includes("Firefox"),Br=ce.includes("Chrome"),Rr=ce.includes("Edge"),Or=/^((?!chrome|android).)*safari/i.test(ce);var Pt=["Macintosh","MacIntel","MacPPC","Mac68K"].includes(tt),Lr=["Windows","Win16","Win32","WinCE"].includes(tt),kr=tt.indexOf("Linux")>=0,wr=/\bCrOS\b/.test(ce);var ee={cols:80,rows:24,showCursorImmediately:!1,cursorBlink:!1,blinkIntervalDuration:0,cursorStyle:"block",cursorWidth:1,cursorInactiveStyle:"outline",drawBoldTextInBrightColors:!0,documentOverride:null,fastScrollSensitivity:5,fontFamily:"monospace",fontSize:15,fontWeight:"normal",fontWeightBold:"bold",ignoreBracketedPasteMode:!1,lineHeight:1,letterSpacing:0,linkHandler:null,logLevel:"info",logger:null,scrollback:1e3,scrollbar:{showScrollbar:!0},scrollOnEraseInDisplay:!1,scrollOnUserInput:!0,scrollSensitivity:1,screenReaderMode:!1,smoothScrollDuration:0,macOptionIsMeta:!1,macOptionClickForcesSelection:!1,minimumContrastRatio:1,mouseEventsRequireAlt:!1,disableStdin:!1,allowProposedApi:!1,allowTransparency:!1,tabStopWidth:8,theme:{},reflowCursorLine:!1,rescaleOverlappingGlyphs:!1,rightClickSelectsWord:Pt,windowOptions:{},windowsPty:{},wordSeparator:" ()[]{}',\"`",altClickMovesCursor:!0,convertEol:!1,termName:"xterm",quirks:{},vtExtensions:{}},Zt=["normal","bold","100","200","300","400","500","600","700","800","900"],Ae=class extends I{constructor(e){super();this._onOptionChange=this._register(new m);this.onOptionChange=this._onOptionChange.event;let t={...ee};for(let r in e)if(r in t)try{let s=e[r];t[r]=this._sanitizeAndValidateOption(r,s)}catch(s){console.error(s)}this.rawOptions=t,this.options={...t},this._setupOptions(),this._register(D(()=>{this.rawOptions.linkHandler=null,this.rawOptions.documentOverride=null}))}onSpecificOptionChange(e,t){return this.onOptionChange(r=>{r===e&&t(this.rawOptions[e])})}onMultipleOptionChange(e,t){return this.onOptionChange(r=>{e.indexOf(r)!==-1&&t()})}_setupOptions(){let e=r=>{if(!(r in ee))throw new Error(`No option with key "${r}"`);return this.rawOptions[r]},t=(r,s)=>{if(!(r in ee))throw new Error(`No option with key "${r}"`);s=this._sanitizeAndValidateOption(r,s),this.rawOptions[r]!==s&&(this.rawOptions[r]=s,this._onOptionChange.fire(r))};for(let r in this.rawOptions){let s={get:e.bind(this,r),set:t.bind(this,r)};Object.defineProperty(this.options,r,s)}}_sanitizeAndValidateOption(e,t){switch(e){case"cursorStyle":if(t||(t=ee[e]),!Jt(t))throw new Error(`"${t}" is not a valid value for ${e}`);break;case"wordSeparator":t||(t=ee[e]);break;case"fontWeight":case"fontWeightBold":if(typeof t=="number"&&1<=t&&t<=1e3)break;t=Zt.includes(t)?t:ee[e];break;case"blinkIntervalDuration":if(t=Math.floor(t),t<0)throw new Error(`${e} cannot be less than 0, value: ${t}`);break;case"cursorWidth":t=Math.floor(t);case"lineHeight":case"tabStopWidth":if(t<1)throw new Error(`${e} cannot be less than 1, value: ${t}`);break;case"minimumContrastRatio":t=Math.max(1,Math.min(21,Math.round(t*10)/10));break;case"scrollback":if(t=Math.min(t,4294967295),t<0)throw new Error(`${e} cannot be less than 0, value: ${t}`);break;case"fastScrollSensitivity":case"scrollSensitivity":if(t<=0)throw new Error(`${e} cannot be less than or equal to 0, value: ${t}`);break;case"rows":case"cols":if(!t&&t!==0)throw new Error(`${e} must be numeric, value: ${t}`);break;case"windowsPty":t=t??{};break}return t}};function Jt(n){return n==="block"||n==="underline"||n==="bar"}var Bt=Object.freeze({insertMode:!1}),Rt=Object.freeze({applicationCursorKeys:!1,applicationKeypad:!1,bracketedPasteMode:!1,colorSchemeUpdates:!1,cursorBlink:void 0,cursorStyle:void 0,origin:!1,reverseWraparound:!1,sendFocus:!1,synchronizedOutput:!1,win32InputMode:!1,wraparound:!0}),Ot=()=>({flags:0,mainFlags:0,altFlags:0,mainStack:[],altStack:[]}),te=class extends I{constructor(e,t,r){super();this._bufferService=e;this._logService=t;this._optionsService=r;this.isCursorHidden=!1;this._onData=this._register(new m);this.onData=this._onData.event;this._onUserInput=this._register(new m);this.onUserInput=this._onUserInput.event;this._onBinary=this._register(new m);this.onBinary=this._onBinary.event;this._onRequestScrollToBottom=this._register(new m);this.onRequestScrollToBottom=this._onRequestScrollToBottom.event;this.isCursorInitialized=r.rawOptions.showCursorImmediately??!1,this.modes=structuredClone(Bt),this.decPrivateModes=structuredClone(Rt),this.kittyKeyboard=Ot()}reset(){this.modes=structuredClone(Bt),this.decPrivateModes=structuredClone(Rt),this.kittyKeyboard=Ot()}triggerDataEvent(e,t=!1){if(this._optionsService.rawOptions.disableStdin)return;let r=this._bufferService.buffer;t&&this._optionsService.rawOptions.scrollOnUserInput&&r.ybase!==r.ydisp&&this._onRequestScrollToBottom.fire(),t&&this._onUserInput.fire(),this._logService.debug(`sending data "${e}"`),this._logService.trace("sending data (codes)",()=>e.split("").map(s=>s.charCodeAt(0))),this._onData.fire(e)}triggerBinaryEvent(e){this._optionsService.rawOptions.disableStdin||(this._logService.debug(`sending binary "${e}"`),this._logService.trace("sending binary (codes)",()=>e.split("").map(t=>t.charCodeAt(0))),this._onBinary.fire(e))}};te=H([R(0,q),R(1,z),R(2,V)],te);var Lt={NONE:{events:0,restrict:()=>!1},X10:{events:1,restrict:n=>n.button===4||n.action!==1?!1:(n.ctrl=!1,n.alt=!1,n.shift=!1,!0)},VT200:{events:19,restrict:n=>n.action!==32},DRAG:{events:23,restrict:n=>!(n.action===32&&n.button===3)},ANY:{events:31,restrict:n=>!0}};function it(n,i){let e=(n.ctrl?16:0)|(n.shift?4:0)|(n.alt?8:0);return n.button===4?(e|=64,e|=n.action):(e|=n.button&3,n.button&4&&(e|=64),n.button&8&&(e|=128),n.action===32?e|=32:n.action===0&&!i&&(e|=3)),e}var rt=String.fromCharCode,kt={DEFAULT:n=>{let i=[it(n,!1)+32,n.col+32,n.row+32];return i[0]>255||i[1]>255||i[2]>255?"":`\x1B[M${rt(i[0])}${rt(i[1])}${rt(i[2])}`},SGR:n=>{let i=n.action===0&&n.button!==4?"m":"M";return`\x1B[<${it(n,!0)};${n.col};${n.row}${i}`},SGR_PIXELS:n=>{let i=n.action===0&&n.button!==4?"m":"M";return`\x1B[<${it(n,!0)};${n.x};${n.y}${i}`}},Pe=class extends I{constructor(){super();this._protocols={};this._encodings={};this._activeProtocol="";this._activeEncoding="";this._onProtocolChange=this._register(new m);this.onProtocolChange=this._onProtocolChange.event;for(let e of Object.keys(Lt))this.addProtocol(e,Lt[e]);for(let e of Object.keys(kt))this.addEncoding(e,kt[e]);this.reset()}addProtocol(e,t){this._protocols[e]=t}addEncoding(e,t){this._encodings[e]=t}get activeProtocol(){return this._activeProtocol}get areMouseEventsActive(){return this._protocols[this._activeProtocol].events!==0}set activeProtocol(e){if(!this._protocols[e])throw new Error(`unknown protocol "${e}"`);this._activeProtocol=e,this._onProtocolChange.fire(this._protocols[e].events)}get activeEncoding(){return this._activeEncoding}set activeEncoding(e){if(!this._encodings[e])throw new Error(`unknown encoding "${e}"`);this._activeEncoding=e}reset(){this.activeProtocol="NONE",this.activeEncoding="DEFAULT"}setCustomWheelEventHandler(e){this._customWheelEventHandler=e}allowCustomWheelEvent(e){return this._customWheelEventHandler?this._customWheelEventHandler(e)!==!1:!0}restrictMouseEvent(e){return this._protocols[this._activeProtocol].restrict(e)}encodeMouseEvent(e){return this._encodings[this._activeEncoding](e)}get isDefaultEncoding(){return this._activeEncoding==="DEFAULT"}get isPixelEncoding(){return this._activeEncoding==="SGR_PIXELS"}};var L=class n{constructor(){this._providers=Object.create(null);this._active="";this._onChange=new m;this.onChange=this._onChange.event}static extractShouldJoin(i){return(i&1)!==0}static extractWidth(i){return i>>1&3}static extractCharKind(i){return i>>3}static createPropertyValue(i,e,t=!1){return(i&16777215)<<3|(e&3)<<1|(t?1:0)}dispose(){this._onChange.dispose()}get versions(){return Object.keys(this._providers)}get activeVersion(){return this._active}set activeVersion(i){if(!this._providers[i])throw new Error(`unknown Unicode version "${i}"`);this._active=i,this._activeProvider=this._providers[i],this._onChange.fire(i)}register(i){this._providers[i.version]=i,this._active||(this.activeVersion=i.version)}wcwidth(i){return this._activeProvider.wcwidth(i)}getStringCellWidth(i){let e=0,t=0,r=i.length;for(let s=0;s=r)return e+this.wcwidth(o);let h=i.charCodeAt(s);56320<=h&&h<=57343?o=(o-55296)*1024+h-56320+65536:e+=this.wcwidth(h)}let l=this.charProperties(o,t),c=n.extractWidth(l);n.extractShouldJoin(l)&&(c-=n.extractWidth(t)),e+=c,t=l}return e}charProperties(i,e){return this._activeProvider.charProperties(i,e)}};var st=[[768,879],[1155,1158],[1160,1161],[1425,1469],[1471,1471],[1473,1474],[1476,1477],[1479,1479],[1536,1539],[1552,1557],[1611,1630],[1648,1648],[1750,1764],[1767,1768],[1770,1773],[1807,1807],[1809,1809],[1840,1866],[1958,1968],[2027,2035],[2305,2306],[2364,2364],[2369,2376],[2381,2381],[2385,2388],[2402,2403],[2433,2433],[2492,2492],[2497,2500],[2509,2509],[2530,2531],[2561,2562],[2620,2620],[2625,2626],[2631,2632],[2635,2637],[2672,2673],[2689,2690],[2748,2748],[2753,2757],[2759,2760],[2765,2765],[2786,2787],[2817,2817],[2876,2876],[2879,2879],[2881,2883],[2893,2893],[2902,2902],[2946,2946],[3008,3008],[3021,3021],[3134,3136],[3142,3144],[3146,3149],[3157,3158],[3260,3260],[3263,3263],[3270,3270],[3276,3277],[3298,3299],[3393,3395],[3405,3405],[3530,3530],[3538,3540],[3542,3542],[3633,3633],[3636,3642],[3655,3662],[3761,3761],[3764,3769],[3771,3772],[3784,3789],[3864,3865],[3893,3893],[3895,3895],[3897,3897],[3953,3966],[3968,3972],[3974,3975],[3984,3991],[3993,4028],[4038,4038],[4141,4144],[4146,4146],[4150,4151],[4153,4153],[4184,4185],[4448,4607],[4959,4959],[5906,5908],[5938,5940],[5970,5971],[6002,6003],[6068,6069],[6071,6077],[6086,6086],[6089,6099],[6109,6109],[6155,6157],[6313,6313],[6432,6434],[6439,6440],[6450,6450],[6457,6459],[6679,6680],[6912,6915],[6964,6964],[6966,6970],[6972,6972],[6978,6978],[7019,7027],[7616,7626],[7678,7679],[8203,8207],[8234,8238],[8288,8291],[8298,8303],[8400,8431],[12330,12335],[12441,12442],[43014,43014],[43019,43019],[43045,43046],[64286,64286],[65024,65039],[65056,65059],[65279,65279],[65529,65531]],Qt=[[68097,68099],[68101,68102],[68108,68111],[68152,68154],[68159,68159],[119143,119145],[119155,119170],[119173,119179],[119210,119213],[119362,119364],[917505,917505],[917536,917631],[917760,917999]],E;function ei(n,i){let e=0,t=i.length-1,r;if(ni[t][1])return!1;for(;t>=e;)if(r=e+t>>1,n>i[r][1])e=r+1;else if(n=131072&&i<=196605||i>=196608&&i<=262141?2:1}charProperties(i,e){let t=this.wcwidth(i),r=t===0&&e!==0;if(r){let s=L.extractWidth(e);s===0?r=!1:s>t&&(t=s)}return L.createPropertyValue(0,t,r)}};var Re=class{constructor(){this.glevel=0;this._charsets=[]}get charsets(){return this._charsets}reset(){this.charset=void 0,this._charsets=[],this.glevel=0}setgLevel(i){this.glevel=i,this.charset=this._charsets[i]}setgCharset(i,e){this._charsets[i]=e,this.glevel===i&&(this.charset=e)}};function nt(n){let e=n.buffer.lines.get(n.buffer.ybase+n.buffer.y-1)?.get(n.cols-1),t=n.buffer.lines.get(n.buffer.ybase+n.buffer.y);t&&e&&(t.isWrapped=e[3]!==0&&e[3]!==32)}var ie=class n{constructor(i=32,e=32){this.maxLength=i;this.maxSubParamsLength=e;if(e>256)throw new Error("maxSubParamsLength must not be greater than 256");this.params=new Int32Array(i),this.length=0,this._subParams=new Int32Array(e),this._subParamsLength=0,this._subParamsIdx=new Uint16Array(i),this._rejectDigits=!1,this._rejectSubDigits=!1,this._digitIsSub=!1}static fromArray(i){let e=new n;if(!i.length)return e;for(let t=Array.isArray(i[0])?1:0;t>8,r=this._subParamsIdx[e]&255;r-t>0&&i.push(Array.prototype.slice.call(this._subParams,t,r))}return i}reset(){this.length=0,this._subParamsLength=0,this._rejectDigits=!1,this._rejectSubDigits=!1,this._digitIsSub=!1}resetZdm(){this.length=1,this._subParamsLength=0,this._rejectDigits=!1,this._rejectSubDigits=!1,this._digitIsSub=!1,this._subParamsIdx[0]=0,this.params[0]=0}addParam(i){if(this._digitIsSub=!1,this.length>=this.maxLength){this._rejectDigits=!0;return}if(i<-1)throw new Error("values less than -1 are not allowed");this._subParamsIdx[this.length]=this._subParamsLength<<8|this._subParamsLength,this.params[this.length++]=i>2147483647?2147483647:i}addSubParam(i){if(this._digitIsSub=!0,!!this.length){if(this._rejectDigits||this._subParamsLength>=this.maxSubParamsLength){this._rejectSubDigits=!0;return}if(i<-1)throw new Error("values less than -1 are not allowed");this._subParams[this._subParamsLength++]=i>2147483647?2147483647:i,this._subParamsIdx[this.length-1]++}}hasSubParams(i){return(this._subParamsIdx[i]&255)-(this._subParamsIdx[i]>>8)>0}getSubParams(i){let e=this._subParamsIdx[i]>>8,t=this._subParamsIdx[i]&255;return t-e>0?this._subParams.subarray(e,t):null}getSubParamsAll(){let i={};for(let e=0;e>8,r=this._subParamsIdx[e]&255;r-t>0&&(i[e]=this._subParams.slice(t,r))}return i}addDigit(i){let e;if(this._rejectDigits||!(e=this._digitIsSub?this._subParamsLength:this.length)||this._digitIsSub&&this._rejectSubDigits)return;let t=this._digitIsSub?this._subParams:this.params,r=t[e-1];t[e-1]=~r?Math.min(r*10+i,2147483647):i}};var at=class{constructor(){this._chunks=[];this._length=0}get length(){return this._length}reset(){this._chunks.length=0,this._length=0}append(i){this._chunks.push(i),this._length+=i.length}toString(){return this._chunks.join("")}},X=class{constructor(i){this._limit=i;this._builder=new at}get length(){return this._builder.length}get limit(){return this._limit}reset(){this._builder.reset()}append(i){return this._builder.append(i),this._builder.length>this._limit?(this._builder.reset(),!0):!1}toString(){return this._builder.toString()}};var he=[],Oe=class{constructor(){this._state=0;this._active=he;this._id=-1;this._handlers=Object.create(null);this._handlerFb=()=>{};this._stack={paused:!1,loopPosition:0,fallThrough:!1}}registerHandler(i,e){this._handlers[i]??=[];let t=this._handlers[i];return t.push(e),{dispose:()=>{let r=t.indexOf(e);r!==-1&&t.splice(r,1)}}}clearHandler(i){this._handlers[i]&&delete this._handlers[i]}setHandlerFallback(i){this._handlerFb=i}dispose(){this._handlers=Object.create(null),this._handlerFb=()=>{},this._active=he}reset(){if(this._state===2)for(let i=this._stack.paused?this._stack.loopPosition-1:this._active.length-1;i>=0;--i)this._active[i].end(!1);this._stack.paused=!1,this._active=he,this._id=-1,this._state=0}_start(){if(this._active=this._handlers[this._id]||he,!this._active.length)this._handlerFb(this._id,"START");else for(let i=this._active.length-1;i>=0;i--)this._active[i].start()}_put(i,e,t){if(!this._active.length)this._handlerFb(this._id,"PUT",N(i,e,t));else for(let r=this._active.length-1;r>=0;r--)this._active[r].put(i,e,t)}start(){this.reset(),this._state=1}put(i,e,t){if(this._state!==3){if(this._state===1)for(;e0&&this._put(i,e,t)}}end(i,e=!0){if(this._state!==0){if(this._state!==3)if(this._state===1&&this._start(),!this._active.length)this._handlerFb(this._id,"END",i);else{let t=!1,r=this._active.length-1,s=!1;if(this._stack.paused&&(r=this._stack.loopPosition-1,t=e,s=this._stack.fallThrough,this._stack.paused=!1),!s&&t===!1){for(;r>=0&&(t=this._active[r].end(i),t!==!0);r--)if(t instanceof Promise)return this._stack.paused=!0,this._stack.loopPosition=r,this._stack.fallThrough=!1,t;r--}for(;r>=0;r--)if(t=this._active[r].end(!1),t instanceof Promise)return this._stack.paused=!0,this._stack.loopPosition=r,this._stack.fallThrough=!0,t}this._active=he,this._id=-1,this._state=0}}},Le=class Le{constructor(i){this._handler=i;this._data=new X(Le._payloadLimit);this._hitLimit=!1}start(){this._data.reset(),this._hitLimit=!1}put(i,e,t){this._hitLimit||this._data.append(N(i,e,t))&&(this._hitLimit=!0)}end(i){let e=!1;if(this._hitLimit)e=!1;else if(i&&(e=this._handler(this._data.toString()),e instanceof Promise))return e.then(t=>(this._data.reset(),this._hitLimit=!1,t));return this._data.reset(),this._hitLimit=!1,e}};Le._payloadLimit=1e7;var y=Le;var ue=[],ke=class{constructor(){this._handlers=Object.create(null);this._active=ue;this._ident=0;this._handlerFb=()=>{};this._stack={paused:!1,loopPosition:0,fallThrough:!1}}dispose(){this._handlers=Object.create(null),this._handlerFb=()=>{},this._active=ue}registerHandler(i,e){this._handlers[i]??=[];let t=this._handlers[i];return t.push(e),{dispose:()=>{let r=t.indexOf(e);r!==-1&&t.splice(r,1)}}}clearHandler(i){this._handlers[i]&&delete this._handlers[i]}setHandlerFallback(i){this._handlerFb=i}reset(){if(this._active.length)for(let i=this._stack.paused?this._stack.loopPosition-1:this._active.length-1;i>=0;--i)this._active[i].unhook(!1);this._stack.paused=!1,this._active=ue,this._ident=0}hook(i,e){if(this.reset(),this._ident=i,this._active=this._handlers[i]||ue,!this._active.length)this._handlerFb(this._ident,"HOOK",e);else for(let t=this._active.length-1;t>=0;t--)this._active[t].hook(e)}put(i,e,t){if(!this._active.length)this._handlerFb(this._ident,"PUT",N(i,e,t));else for(let r=this._active.length-1;r>=0;r--)this._active[r].put(i,e,t)}unhook(i,e=!0){if(!this._active.length)this._handlerFb(this._ident,"UNHOOK",i);else{let t=!1,r=this._active.length-1,s=!1;if(this._stack.paused&&(r=this._stack.loopPosition-1,t=e,s=this._stack.fallThrough,this._stack.paused=!1),!s&&t===!1){for(;r>=0&&(t=this._active[r].unhook(i),t!==!0);r--)if(t instanceof Promise)return this._stack.paused=!0,this._stack.loopPosition=r,this._stack.fallThrough=!1,t;r--}for(;r>=0;r--)if(t=this._active[r].unhook(!1),t instanceof Promise)return this._stack.paused=!0,this._stack.loopPosition=r,this._stack.fallThrough=!0,t}this._active=ue,this._ident=0}},de=new ie;de.addParam(0);var we=class we{constructor(i){this._handler=i;this._data=new X(we._payloadLimit);this._params=de;this._hitLimit=!1}hook(i){this._params=i.length>1||i.params[0]?i.clone():de,this._data.reset(),this._hitLimit=!1}put(i,e,t){this._hitLimit||this._data.append(N(i,e,t))&&(this._hitLimit=!0)}unhook(i){let e=!1;if(this._hitLimit)e=!1;else if(i&&(e=this._handler(this._data.toString(),this._params),e instanceof Promise))return e.then(t=>(this._params=de,this._data.reset(),this._hitLimit=!1,t));return this._params=de,this._data.reset(),this._hitLimit=!1,e}};we._payloadLimit=1e7;var fe=we;var pe=[],He=class{constructor(){this._handlers=Object.create(null);this._active=pe;this._ident=0;this._handlerFb=()=>{};this._stack={paused:!1,loopPosition:0,fallThrough:!1}}registerHandler(i,e){this._handlers[i]??=[];let t=this._handlers[i];return t.push(e),{dispose:()=>{let r=t.indexOf(e);r!==-1&&t.splice(r,1)}}}clearHandler(i){this._handlers[i]&&delete this._handlers[i]}setHandlerFallback(i){this._handlerFb=i}dispose(){this._handlers=Object.create(null),this._handlerFb=()=>{},this._active=pe}reset(){if(this._active.length)for(let i=this._stack.paused?this._stack.loopPosition-1:this._active.length-1;i>=0;--i)this._active[i].end(!1);this._stack.paused=!1,this._active=pe,this._ident=0}start(i){if(this.reset(),this._ident=i,this._active=this._handlers[i]||pe,!this._active.length)this._handlerFb(this._ident,"START");else for(let e=this._active.length-1;e>=0;e--)this._active[e].start()}put(i,e,t){if(!this._active.length)this._handlerFb(this._ident,"PUT",N(i,e,t));else for(let r=this._active.length-1;r>=0;r--)this._active[r].put(i,e,t)}end(i,e=!0){if(!this._active.length)this._handlerFb(this._ident,"END",i);else{let t=!1,r=this._active.length-1,s=!1;if(this._stack.paused&&(r=this._stack.loopPosition-1,t=e,s=this._stack.fallThrough,this._stack.paused=!1),!s&&t===!1){for(;r>=0&&(t=this._active[r].end(i),t!==!0);r--)if(t instanceof Promise)return this._stack.paused=!0,this._stack.loopPosition=r,this._stack.fallThrough=!1,t;r--}for(;r>=0;r--)if(t=this._active[r].end(!1),t instanceof Promise)return this._stack.paused=!0,this._stack.loopPosition=r,this._stack.fallThrough=!0,t}this._active=pe,this._ident=0}},Ne=class Ne{constructor(i){this._handler=i;this._data=new X(Ne._payloadLimit);this._hitLimit=!1}start(){this._data.reset(),this._hitLimit=!1}put(i,e,t){this._hitLimit||this._data.append(N(i,e,t))&&(this._hitLimit=!0)}end(i){let e=!1;if(this._hitLimit)e=!1;else if(i&&(e=this._handler(this._data.toString()),e instanceof Promise))return e.then(t=>(this._data.reset(),this._hitLimit=!1,t));return this._data.reset(),this._hitLimit=!1,e}};Ne._payloadLimit=1e7;var Me=Ne;var ot=class{constructor(i){this.table=new Uint16Array(i)}setDefault(i,e){this.table.fill(i<<8|e)}add(i,e,t,r){this.table[e<<8|i]=t<<8|r}addMany(i,e,t,r){for(let s=0;sc),t=(l,c)=>e.slice(l,c),r=t(32,127),s=t(0,24);s.push(25),s.push.apply(s,t(28,32));let o=t(0,17);n.setDefault(1,0),n.addMany(r,0,2,0);for(let l of o)n.addMany([24,26,153,154],l,3,0),n.addMany(t(128,144),l,3,0),n.addMany(t(144,152),l,3,0),n.add(156,l,0,0),n.add(27,l,11,1),n.add(157,l,4,8),n.addMany([152,158],l,0,7),n.add(159,l,11,14),n.add(155,l,11,3),n.add(144,l,11,9);return n.addMany(s,0,3,0),n.addMany(s,1,3,1),n.add(127,1,0,1),n.addMany(s,8,0,8),n.addMany(s,3,3,3),n.add(127,3,0,3),n.addMany(s,4,3,4),n.add(127,4,0,4),n.addMany(s,6,3,6),n.addMany(s,5,3,5),n.add(127,5,0,5),n.addMany(s,2,3,2),n.add(127,2,0,2),n.add(93,1,4,8),n.addMany(r,8,5,8),n.add(127,8,5,8),n.addMany([156,27,24,26,7],8,6,0),n.addMany(t(28,32),8,0,8),n.addMany([88,94],1,0,7),n.addMany(r,7,0,7),n.addMany(s,7,0,7),n.add(156,7,0,0),n.add(127,7,0,7),n.add(95,1,11,14),n.addMany(s,14,0,14),n.add(127,14,0,14),n.addMany(t(32,48),14,9,15),n.addMany(t(48,127),14,15,16),n.addMany(t(48,127),15,15,16),n.addMany(s,15,0,15),n.addMany(t(32,48),15,9,15),n.add(127,15,0,15),n.addMany(r,16,16,16),n.addMany(s,16,0,16),n.addMany(t(8,14),16,16,16),n.add(127,16,0,16),n.addMany([27,156,24,26],16,17,0),n.add(91,1,11,3),n.addMany(t(64,127),3,7,0),n.addMany(t(48,60),3,8,4),n.addMany([60,61,62,63],3,9,4),n.addMany(t(48,60),4,8,4),n.addMany(t(64,127),4,7,0),n.addMany([60,61,62,63],4,0,6),n.addMany(t(32,64),6,0,6),n.add(127,6,0,6),n.addMany(t(64,127),6,0,0),n.addMany(t(32,48),3,9,5),n.addMany(t(32,48),5,9,5),n.addMany(t(48,64),5,0,6),n.addMany(t(64,127),5,7,0),n.addMany(t(32,48),4,9,5),n.addMany(t(32,48),1,9,2),n.addMany(t(32,48),2,9,2),n.addMany(t(48,127),2,10,0),n.addMany(t(48,80),1,10,0),n.addMany(t(81,88),1,10,0),n.addMany([89,90,92],1,10,0),n.addMany(t(96,127),1,10,0),n.add(80,1,11,9),n.addMany(s,9,0,9),n.add(127,9,0,9),n.addMany(t(32,48),9,9,12),n.addMany(t(48,60),9,8,10),n.addMany([60,61,62,63],9,9,10),n.addMany(s,11,0,11),n.addMany(t(32,128),11,0,11),n.addMany(s,10,0,10),n.add(127,10,0,10),n.addMany(t(48,60),10,8,10),n.addMany([60,61,62,63],10,0,11),n.addMany(t(32,48),10,9,12),n.addMany(s,12,0,12),n.add(127,12,0,12),n.addMany(t(32,48),12,9,12),n.addMany(t(48,64),12,0,11),n.addMany(t(64,127),12,12,13),n.addMany(t(64,127),10,12,13),n.addMany(t(64,127),9,12,13),n.addMany(s,13,13,13),n.addMany(r,13,13,13),n.add(127,13,0,13),n.addMany([27,156,24,26],13,14,0),n.add(T,0,2,0),n.add(T,8,5,8),n.add(T,6,0,6),n.add(T,11,0,11),n.add(T,13,13,13),n.add(T,16,16,16),n})(),Fe=class extends I{constructor(e=ti){super();this._transitions=e;this._parseStack={state:0,handlers:[],handlerPos:0,transition:0,chunkPos:0};this.initialState=0,this.currentState=this.initialState,this._params=new ie,this._params.addParam(0),this._collect=0,this.precedingJoinState=0,this._printHandlerFb=(t,r,s)=>{},this._executeHandlerFb=t=>{},this._csiHandlerFb=(t,r)=>{},this._escHandlerFb=t=>{},this._errorHandlerFb=t=>t,this._printHandler=this._printHandlerFb,this._executeHandlers=Object.create(null),this._executeHandlersArr=new Array(24).fill(void 0),this._csiHandlers=Object.create(null),this._escHandlers=Object.create(null),this._register(D(()=>{this._csiHandlers=Object.create(null),this._executeHandlers=Object.create(null),this._executeHandlersArr=new Array(24).fill(void 0),this._escHandlers=Object.create(null)})),this._oscParser=this._register(new Oe),this._dcsParser=this._register(new ke),this._apcParser=this._register(new He),this._errorHandler=this._errorHandlerFb,this.registerEscHandler({final:"\\"},()=>!0)}_identifier(e,t=[64,126]){let r=0;if(e.prefix){if(e.prefix.length>1)throw new Error("only one byte as prefix supported");if(r=e.prefix.charCodeAt(0),r<60||r>63)throw new Error("prefix must be in range 0x3c .. 0x3f")}if(e.intermediates){if(e.intermediates.length>2)throw new Error("only two bytes as intermediates are supported");for(let o=0;ol||l>47)throw new Error("intermediate must be in range 0x20 .. 0x2f");r<<=8,r|=l}}if(e.final.length!==1)throw new Error("final must be a single byte");let s=e.final.charCodeAt(0);if(t[0]>s||s>t[1])throw new Error(`final must be in range ${t[0]} .. ${t[1]}`);return r<<=8,r|=s,r}identToString(e){let t=[];for(;e;)t.push(String.fromCharCode(e&255)),e>>=8;return t.reverse().join("")}setPrintHandler(e){this._printHandler=e}clearPrintHandler(){this._printHandler=this._printHandlerFb}registerEscHandler(e,t){let r=this._identifier(e,[48,126]);this._escHandlers[r]??=[];let s=this._escHandlers[r];return s.push(t),{dispose:()=>{let o=s.indexOf(t);o!==-1&&s.splice(o,1)}}}clearEscHandler(e){this._escHandlers[this._identifier(e,[48,126])]&&delete this._escHandlers[this._identifier(e,[48,126])]}setEscHandlerFallback(e){this._escHandlerFb=e}setExecuteHandler(e,t){let r=e.charCodeAt(0);this._executeHandlers[r]=t,r<24&&(this._executeHandlersArr[r]=t)}clearExecuteHandler(e){let t=e.charCodeAt(0);this._executeHandlers[t]&&delete this._executeHandlers[t],t<24&&(this._executeHandlersArr[t]=void 0)}setExecuteHandlerFallback(e){this._executeHandlerFb=e}registerCsiHandler(e,t){let r=this._identifier(e);this._csiHandlers[r]??=[];let s=this._csiHandlers[r];return s.push(t),{dispose:()=>{let o=s.indexOf(t);o!==-1&&s.splice(o,1)}}}clearCsiHandler(e){this._csiHandlers[this._identifier(e)]&&delete this._csiHandlers[this._identifier(e)]}setCsiHandlerFallback(e){this._csiHandlerFb=e}registerDcsHandler(e,t){return this._dcsParser.registerHandler(this._identifier(e),t)}clearDcsHandler(e){this._dcsParser.clearHandler(this._identifier(e))}setDcsHandlerFallback(e){this._dcsParser.setHandlerFallback(e)}registerOscHandler(e,t){return this._oscParser.registerHandler(e,t)}clearOscHandler(e){this._oscParser.clearHandler(e)}setOscHandlerFallback(e){this._oscParser.setHandlerFallback(e)}registerApcHandler(e,t){return e.prefix=void 0,this._apcParser.registerHandler(this._identifier(e,[48,126]),t)}clearApcHandler(e){e.prefix=void 0,this._apcParser.clearHandler(this._identifier(e,[48,126]))}setApcHandlerFallback(e){this._apcParser.setHandlerFallback(e)}setErrorHandler(e){this._errorHandler=e}clearErrorHandler(){this._errorHandler=this._errorHandlerFb}reset(){this.currentState=this.initialState,this._oscParser.reset(),this._dcsParser.reset(),this._apcParser.reset(),this._params.resetZdm(),this._collect=0,this.precedingJoinState=0,this._parseStack.state!==0&&(this._parseStack.state=2,this._parseStack.handlers=[])}_preserveStack(e,t,r,s,o){this._parseStack.state=e,this._parseStack.handlers=t,this._parseStack.handlerPos=r,this._parseStack.transition=s,this._parseStack.chunkPos=o}parse(e,t,r){let s,o,l=0,c;if(this._parseStack.state)if(this._parseStack.state===2)this._parseStack.state=0,l=this._parseStack.chunkPos+1;else{if(r===void 0||this._parseStack.state===1)throw this._parseStack.state=1,new Error("improper continuation due to previous async handler, giving up parsing");let h=this._parseStack.handlers,u=this._parseStack.handlerPos-1;switch(this._parseStack.state){case 3:if(r===!1&&u>-1){for(;u>=0&&(c=h[u](this._params),c!==!0);u--)if(c instanceof Promise)return this._parseStack.handlerPos=u,c}this._parseStack.handlers=[];break;case 4:if(r===!1&&u>-1){for(;u>=0&&(c=h[u](),c!==!0);u--)if(c instanceof Promise)return this._parseStack.handlerPos=u,c}this._parseStack.handlers=[];break;case 6:if(s=e[this._parseStack.chunkPos],c=this._dcsParser.unhook(s!==24&&s!==26,r),c)return c;s===27&&(this._parseStack.transition|=1),this._params.resetZdm(),this._collect=0;break;case 5:if(s=e[this._parseStack.chunkPos],c=this._oscParser.end(s!==24&&s!==26,r),c)return c;s===27&&(this._parseStack.transition|=1),this._params.resetZdm(),this._collect=0;break;case 7:if(s=e[this._parseStack.chunkPos],c=this._apcParser.end(s!==24&&s!==26,r),c)return c;s===27&&(this._parseStack.transition|=1),this._params.resetZdm(),this._collect=0;break}this._parseStack.state=0,l=this._parseStack.chunkPos+1,this.precedingJoinState=0,this.currentState=this._parseStack.transition&255}for(let h=l;h=60&&a<=63&&(this._collect=a,u++);let f=!1;for(;u=48&&a<=57)this._params.addDigit(a-48);else if(a===59)this._params.addParam(0);else if(a===58)this._params.addSubParam(-1);else if(a>=64&&a<=126){let _=this._csiHandlers[this._collect<<8|a],p=_?_.length-1:-1;for(;p>=0&&(c=_[p](this._params),c!==!0);p--)if(c instanceof Promise)return o=1792,this._preserveStack(3,_,p,o,u),c;p<0&&this._csiHandlerFb(this._collect<<8|a,this._params),this.precedingJoinState=0,h=u,this.currentState=0,f=!0;break}else break;f||(h=u-1,this.currentState=4);continue}switch(o=this._transitions.table[this.currentState<<8|(s>8){case 2:let u=h,a=t-4;for(;u=32&&(e[u]<=126||e[u]>=T)&&e[++u]>=32&&(e[u]<=126||e[u]>=T)&&e[++u]>=32&&(e[u]<=126||e[u]>=T)&&e[++u]>=32&&(e[u]<=126||e[u]>=T););if(u>=a)for(;u=32&&(e[u]<=126||e[u]>=T);)u++;this._printHandler(e,h,u),h=u-1;break;case 3:this._executeHandlers[s]?this._executeHandlers[s]():this._executeHandlerFb(s),this.precedingJoinState=0;break;case 0:break;case 1:if(this._errorHandler({position:h,code:s,currentState:this.currentState,collect:this._collect,params:this._params,abort:!1}).abort)return;break;case 7:let _=this._csiHandlers[this._collect<<8|s],p=_?_.length-1:-1;for(;p>=0&&(c=_[p](this._params),c!==!0);p--)if(c instanceof Promise)return this._preserveStack(3,_,p,o,h),c;p<0&&this._csiHandlerFb(this._collect<<8|s,this._params),this.precedingJoinState=0;break;case 8:do switch(s){case 59:this._params.addParam(0);break;case 58:this._params.addSubParam(-1);break;default:this._params.addDigit(s-48)}while(++h47&&s<60);h--;break;case 9:this._collect<<=8,this._collect|=s;break;case 10:let v=this._escHandlers[this._collect<<8|s],d=v?v.length-1:-1;for(;d>=0&&(c=v[d](),c!==!0);d--)if(c instanceof Promise)return this._preserveStack(4,v,d,o,h),c;d<0&&this._escHandlerFb(this._collect<<8|s),this.precedingJoinState=0;break;case 11:this._params.resetZdm(),this._collect=0;break;case 12:this._dcsParser.hook(this._collect<<8|s,this._params);break;case 13:for(let b=h+1;;++b)if(b>=t||(s=e[b])===24||s===26||s===27||s>127&&s=t||(s=e[b])<32||s>127&&s=32&&e[b]<127||e[b]>=8&&e[b]<14||e[b]>=T))){this._apcParser.put(e,h,b),h=b-1;break}break;case 17:if(c=this._apcParser.end(s!==24&&s!==26),c)return this._preserveStack(7,[],0,o,h),c;s===27&&(o|=1),this._params.resetZdm(),this._collect=0,this.precedingJoinState=0;break}this.currentState=o&255}}};var ii=/^([\da-f])\/([\da-f])\/([\da-f])$|^([\da-f]{2})\/([\da-f]{2})\/([\da-f]{2})$|^([\da-f]{3})\/([\da-f]{3})\/([\da-f]{3})$|^([\da-f]{4})\/([\da-f]{4})\/([\da-f]{4})$/,ri=/^[\da-f]+$/;function lt(n){if(!n)return;let i=n.toLowerCase();if(i.startsWith("rgb:")){i=i.slice(4);let e=ii.exec(i);if(e){let t=e[1]?15:e[4]?255:e[7]?4095:65535;return[Math.round(parseInt(e[1]||e[4]||e[7]||e[10],16)/t*255),Math.round(parseInt(e[2]||e[5]||e[8]||e[11],16)/t*255),Math.round(parseInt(e[3]||e[6]||e[9]||e[12],16)/t*255)]}}else if(i.startsWith("#")&&(i=i.slice(1),ri.exec(i)&&[3,6,9,12].includes(i.length))){let e=i.length/3,t=[0,0,0];for(let r=0;r<3;++r){let s=parseInt(i.slice(e*r,e*r+e),16);t[r]=e===1?s<<4:e===2?s:e===3?s>>4:s>>8}return t}}var Mt="6.0.0";var si={"(":0,")":1,"*":2,"+":3,"-":1,".":2};function Nt(n,i){if(n>24)return i.setWinLines||!1;switch(n){case 1:return!!i.restoreWin;case 2:return!!i.minimizeWin;case 3:return!!i.setWinPosition;case 4:return!!i.setWinSizePixels;case 5:return!!i.raiseWin;case 6:return!!i.lowerWin;case 7:return!!i.refreshWin;case 8:return!!i.setWinSizeChars;case 9:return!!i.maximizeWin;case 10:return!!i.fullscreenWin;case 11:return!!i.getWinState;case 13:return!!i.getWinPosition;case 14:return!!i.getWinSizePixels;case 15:return!!i.getScreenSizePixels;case 16:return!!i.getCellSizePixels;case 18:return!!i.getWinSizeChars;case 19:return!!i.getScreenSizeChars;case 20:return!!i.getIconTitle;case 21:return!!i.getWinTitle;case 22:return!!i.pushTitle;case 23:return!!i.popTitle;case 24:return!!i.setWinLines}return!1}var Ft=0,Ue=class extends I{constructor(e,t,r,s,o,l,c,h,u=new Fe){super();this._bufferService=e;this._charsetService=t;this._coreService=r;this._logService=s;this._optionsService=o;this._oscLinkService=l;this._mouseStateService=c;this._unicodeService=h;this._parser=u;this._parseBuffer=new Uint32Array(4096);this._stringDecoder=new be;this._utf8Decoder=new me;this._windowTitle="";this._iconName="";this._windowTitleStack=[];this._iconNameStack=[];this._curAttrData=C.clone();this._eraseAttrDataInternal=C.clone();this._onRequestBell=this._register(new m);this.onRequestBell=this._onRequestBell.event;this._onRequestRefreshRows=this._register(new m);this.onRequestRefreshRows=this._onRequestRefreshRows.event;this._onRequestReset=this._register(new m);this.onRequestReset=this._onRequestReset.event;this._onRequestSendFocus=this._register(new m);this.onRequestSendFocus=this._onRequestSendFocus.event;this._onRequestSyncScrollBar=this._register(new m);this.onRequestSyncScrollBar=this._onRequestSyncScrollBar.event;this._onRequestWindowsOptionsReport=this._register(new m);this.onRequestWindowsOptionsReport=this._onRequestWindowsOptionsReport.event;this._onA11yChar=this._register(new m);this.onA11yChar=this._onA11yChar.event;this._onA11yTab=this._register(new m);this.onA11yTab=this._onA11yTab.event;this._onCursorMove=this._register(new m);this.onCursorMove=this._onCursorMove.event;this._onLineFeed=this._register(new m);this.onLineFeed=this._onLineFeed.event;this._onScroll=this._register(new m);this.onScroll=this._onScroll.event;this._onTitleChange=this._register(new m);this.onTitleChange=this._onTitleChange.event;this._onColor=this._register(new m);this.onColor=this._onColor.event;this._onRequestColorSchemeQuery=this._register(new m);this.onRequestColorSchemeQuery=this._onRequestColorSchemeQuery.event;this._parseStack={paused:!1,cursorStartX:0,cursorStartY:0,decodedLength:0,position:0};this._specialColors=[256,257,258];this._register(this._parser),this._dirtyRowTracker=new _e(this._bufferService),this._activeBuffer=this._bufferService.buffer,this._register(this._bufferService.buffers.onBufferActivate(a=>this._activeBuffer=a.activeBuffer)),this._parser.setCsiHandlerFallback((a,f)=>{this._logService.debug("Unknown CSI code: ",{identifier:this._parser.identToString(a),params:f.toArray()})}),this._parser.setEscHandlerFallback(a=>{this._logService.debug("Unknown ESC code: ",{identifier:this._parser.identToString(a)})}),this._parser.setExecuteHandlerFallback(a=>{this._logService.debug("Unknown EXECUTE code: ",{code:a})}),this._parser.setOscHandlerFallback((a,f,_)=>{this._logService.debug("Unknown OSC code: ",{identifier:a,action:f,data:_})}),this._parser.setDcsHandlerFallback((a,f,_)=>{f==="HOOK"&&(_=_.toArray()),this._logService.debug("Unknown DCS code: ",{identifier:this._parser.identToString(a),action:f,payload:_})}),this._parser.setApcHandlerFallback((a,f,_)=>{this._logService.debug("Unknown APC code: ",{identifier:this._parser.identToString(a),action:f,payload:_})}),this._parser.setPrintHandler((a,f,_)=>this.print(a,f,_)),this._parser.registerCsiHandler({final:"@"},a=>this.insertChars(a)),this._parser.registerCsiHandler({intermediates:" ",final:"@"},a=>this.scrollLeft(a)),this._parser.registerCsiHandler({final:"A"},a=>this.cursorUp(a)),this._parser.registerCsiHandler({intermediates:" ",final:"A"},a=>this.scrollRight(a)),this._parser.registerCsiHandler({final:"B"},a=>this.cursorDown(a)),this._parser.registerCsiHandler({final:"C"},a=>this.cursorForward(a)),this._parser.registerCsiHandler({final:"D"},a=>this.cursorBackward(a)),this._parser.registerCsiHandler({final:"E"},a=>this.cursorNextLine(a)),this._parser.registerCsiHandler({final:"F"},a=>this.cursorPrecedingLine(a)),this._parser.registerCsiHandler({final:"G"},a=>this.cursorCharAbsolute(a)),this._parser.registerCsiHandler({final:"H"},a=>this.cursorPosition(a)),this._parser.registerCsiHandler({final:"I"},a=>this.cursorForwardTab(a)),this._parser.registerCsiHandler({final:"J"},a=>this.eraseInDisplay(a,!1)),this._parser.registerCsiHandler({prefix:"?",final:"J"},a=>this.eraseInDisplay(a,!0)),this._parser.registerCsiHandler({final:"K"},a=>this.eraseInLine(a,!1)),this._parser.registerCsiHandler({prefix:"?",final:"K"},a=>this.eraseInLine(a,!0)),this._parser.registerCsiHandler({final:"L"},a=>this.insertLines(a)),this._parser.registerCsiHandler({final:"M"},a=>this.deleteLines(a)),this._parser.registerCsiHandler({final:"P"},a=>this.deleteChars(a)),this._parser.registerCsiHandler({final:"S"},a=>this.scrollUp(a)),this._parser.registerCsiHandler({final:"T"},a=>this.scrollDown(a)),this._parser.registerCsiHandler({final:"X"},a=>this.eraseChars(a)),this._parser.registerCsiHandler({final:"Z"},a=>this.cursorBackwardTab(a)),this._parser.registerCsiHandler({final:"^"},a=>this.scrollDown(a)),this._parser.registerCsiHandler({final:"`"},a=>this.charPosAbsolute(a)),this._parser.registerCsiHandler({final:"a"},a=>this.hPositionRelative(a)),this._parser.registerCsiHandler({final:"b"},a=>this.repeatPrecedingCharacter(a)),this._parser.registerCsiHandler({final:"c"},a=>this.sendDeviceAttributesPrimary(a)),this._parser.registerCsiHandler({prefix:">",final:"c"},a=>this.sendDeviceAttributesSecondary(a)),this._parser.registerCsiHandler({final:"d"},a=>this.linePosAbsolute(a)),this._parser.registerCsiHandler({final:"e"},a=>this.vPositionRelative(a)),this._parser.registerCsiHandler({final:"f"},a=>this.hVPosition(a)),this._parser.registerCsiHandler({final:"g"},a=>this.tabClear(a)),this._parser.registerCsiHandler({final:"h"},a=>this.setMode(a)),this._parser.registerCsiHandler({prefix:"?",final:"h"},a=>this.setModePrivate(a)),this._parser.registerCsiHandler({final:"l"},a=>this.resetMode(a)),this._parser.registerCsiHandler({prefix:"?",final:"l"},a=>this.resetModePrivate(a)),this._parser.registerCsiHandler({final:"m"},a=>this.charAttributes(a)),this._parser.registerCsiHandler({final:"n"},a=>this.deviceStatus(a)),this._parser.registerCsiHandler({prefix:"?",final:"n"},a=>this.deviceStatusPrivate(a)),this._parser.registerCsiHandler({intermediates:"!",final:"p"},a=>this.softReset(a)),this._parser.registerCsiHandler({prefix:">",final:"q"},a=>this.sendXtVersion(a)),this._parser.registerCsiHandler({intermediates:" ",final:"q"},a=>this.setCursorStyle(a)),this._parser.registerCsiHandler({final:"r"},a=>this.setScrollRegion(a)),this._parser.registerCsiHandler({final:"s"},a=>this.saveCursor(a)),this._parser.registerCsiHandler({final:"t"},a=>this.windowOptions(a)),this._parser.registerCsiHandler({final:"u"},a=>this.restoreCursor(a)),this._parser.registerCsiHandler({intermediates:"'",final:"}"},a=>this.insertColumns(a)),this._parser.registerCsiHandler({intermediates:"'",final:"~"},a=>this.deleteColumns(a)),this._parser.registerCsiHandler({intermediates:'"',final:"q"},a=>this.selectProtected(a)),this._parser.registerCsiHandler({intermediates:"$",final:"p"},a=>this.requestMode(a,!0)),this._parser.registerCsiHandler({prefix:"?",intermediates:"$",final:"p"},a=>this.requestMode(a,!1)),this._parser.registerCsiHandler({prefix:"=",final:"u"},a=>this.kittyKeyboardSet(a)),this._parser.registerCsiHandler({prefix:"?",final:"u"},a=>this.kittyKeyboardQuery(a)),this._parser.registerCsiHandler({prefix:">",final:"u"},a=>this.kittyKeyboardPush(a)),this._parser.registerCsiHandler({prefix:"<",final:"u"},a=>this.kittyKeyboardPop(a)),this._parser.setExecuteHandler("\x07",()=>this.bell()),this._parser.setExecuteHandler(` +-`,()=>this.lineFeed()),this._parser.setExecuteHandler("\v",()=>this.lineFeed()),this._parser.setExecuteHandler("\f",()=>this.lineFeed()),this._parser.setExecuteHandler("\r",()=>this.carriageReturn()),this._parser.setExecuteHandler("\b",()=>this.backspace()),this._parser.setExecuteHandler(" ",()=>this.tab()),this._parser.setExecuteHandler("",()=>this.shiftOut()),this._parser.setExecuteHandler("",()=>this.shiftIn()),this._parser.setExecuteHandler("\x84",()=>this.index()),this._parser.setExecuteHandler("\x85",()=>this.nextLine()),this._parser.setExecuteHandler("\x88",()=>this.tabSet()),this._parser.registerOscHandler(0,new y(a=>(this.setTitle(a),this.setIconName(a),!0))),this._parser.registerOscHandler(1,new y(a=>this.setIconName(a))),this._parser.registerOscHandler(2,new y(a=>this.setTitle(a))),this._parser.registerOscHandler(4,new y(a=>this.setOrReportIndexedColor(a))),this._parser.registerOscHandler(8,new y(a=>this.setHyperlink(a))),this._parser.registerOscHandler(10,new y(a=>this.setOrReportFgColor(a))),this._parser.registerOscHandler(11,new y(a=>this.setOrReportBgColor(a))),this._parser.registerOscHandler(12,new y(a=>this.setOrReportCursorColor(a))),this._parser.registerOscHandler(104,new y(a=>this.restoreIndexedColor(a))),this._parser.registerOscHandler(110,new y(a=>this.restoreFgColor(a))),this._parser.registerOscHandler(111,new y(a=>this.restoreBgColor(a))),this._parser.registerOscHandler(112,new y(a=>this.restoreCursorColor(a))),this._parser.registerEscHandler({final:"7"},()=>this.saveCursor()),this._parser.registerEscHandler({final:"8"},()=>this.restoreCursor()),this._parser.registerEscHandler({final:"D"},()=>this.index()),this._parser.registerEscHandler({final:"E"},()=>this.nextLine()),this._parser.registerEscHandler({final:"H"},()=>this.tabSet()),this._parser.registerEscHandler({final:"M"},()=>this.reverseIndex()),this._parser.registerEscHandler({final:"="},()=>this.keypadApplicationMode()),this._parser.registerEscHandler({final:">"},()=>this.keypadNumericMode()),this._parser.registerEscHandler({final:"c"},()=>this.fullReset()),this._parser.registerEscHandler({final:"n"},()=>this.setgLevel(2)),this._parser.registerEscHandler({final:"o"},()=>this.setgLevel(3)),this._parser.registerEscHandler({final:"|"},()=>this.setgLevel(3)),this._parser.registerEscHandler({final:"}"},()=>this.setgLevel(2)),this._parser.registerEscHandler({final:"~"},()=>this.setgLevel(1)),this._parser.registerEscHandler({intermediates:"%",final:"@"},()=>this.selectDefaultCharset()),this._parser.registerEscHandler({intermediates:"%",final:"G"},()=>this.selectDefaultCharset());for(let a in x)this._parser.registerEscHandler({intermediates:"(",final:a},()=>this.selectCharset("("+a)),this._parser.registerEscHandler({intermediates:")",final:a},()=>this.selectCharset(")"+a)),this._parser.registerEscHandler({intermediates:"*",final:a},()=>this.selectCharset("*"+a)),this._parser.registerEscHandler({intermediates:"+",final:a},()=>this.selectCharset("+"+a)),this._parser.registerEscHandler({intermediates:"-",final:a},()=>this.selectCharset("-"+a)),this._parser.registerEscHandler({intermediates:".",final:a},()=>this.selectCharset("."+a)),this._parser.registerEscHandler({intermediates:"/",final:a},()=>this.selectCharset("/"+a));this._parser.registerEscHandler({intermediates:"#",final:"8"},()=>this.screenAlignmentPattern()),this._parser.setErrorHandler(a=>(this._logService.error("Parsing error: ",a),a)),this._parser.registerDcsHandler({intermediates:"$",final:"q"},new fe((a,f)=>this.requestStatusString(a,f)))}getAttrData(){return this._curAttrData}_preserveStack(e,t,r,s){this._parseStack.paused=!0,this._parseStack.cursorStartX=e,this._parseStack.cursorStartY=t,this._parseStack.decodedLength=r,this._parseStack.position=s}_logSlowResolvingAsync(e){if(this._logService.logLevel<=3){let t,r=new Promise((s,o)=>{t=setTimeout(()=>o("#SLOW_TIMEOUT"),5e3)});Promise.race([e,r]).then(()=>{t!==void 0&&clearTimeout(t)},s=>{if(t!==void 0&&clearTimeout(t),s!=="#SLOW_TIMEOUT")throw s;console.warn("async parser handler taking longer than 5000 ms")})}}_getCurrentLinkId(){return this._curAttrData.extended.urlId}parse(e,t){let r,s=this._activeBuffer.x,o=this._activeBuffer.y,l=0,c=this._parseStack.paused;if(c){if(r=this._parser.parse(this._parseBuffer,this._parseStack.decodedLength,t))return this._logSlowResolvingAsync(r),r;s=this._parseStack.cursorStartX,o=this._parseStack.cursorStartY,this._parseStack.paused=!1,e.length>131072&&(l=this._parseStack.position+131072)}if(this._logService.logLevel<=1&&this._logService.debug(`parsing data ${typeof e=="string"?` "${e}"`:` "${Array.prototype.map.call(e,a=>String.fromCharCode(a)).join("")}"`}`),this._logService.logLevel===0&&this._logService.trace("parsing data (codes)",typeof e=="string"?e.split("").map(a=>a.charCodeAt(0)):e),this._parseBuffer.length131072)for(let a=l;a0&&_.getWidth(this._activeBuffer.x-1)===2&&_.setCellFromCodepoint(this._activeBuffer.x-1,0,1,f);let p=this._parser.precedingJoinState;for(let v=t;vh){if(u){let M=_,g=this._activeBuffer.x-S;if(this._activeBuffer.x=S,this._activeBuffer.y++,this._activeBuffer.y===this._activeBuffer.scrollBottom+1?(this._activeBuffer.y--,this._bufferService.scroll(this._eraseAttrData(),!0)):(this._activeBuffer.y>=this._bufferService.rows&&(this._activeBuffer.y=this._bufferService.rows-1),this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y).isWrapped=!0),_=this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y),!_)return;for(S>0&&_ instanceof U&&_.copyCellsFrom(M,g,0,S,!1);g=0;)_.setCellFromCodepoint(this._activeBuffer.x++,0,0,f);continue}if(a&&(_.insertCells(this._activeBuffer.x,o-S,this._activeBuffer.getNullCell(f)),_.getWidth(h-1)===2&&_.setCellFromCodepoint(h-1,0,1,f)),_.setCellFromCodepoint(this._activeBuffer.x++,s,o,f),o>0)for(;--o;)_.setCellFromCodepoint(this._activeBuffer.x++,0,0,f)}this._parser.precedingJoinState=p,this._activeBuffer.x0&&_.getWidth(this._activeBuffer.x)===0&&!_.hasContent(this._activeBuffer.x)&&_.setCellFromCodepoint(this._activeBuffer.x,0,1,f),this._dirtyRowTracker.markDirty(this._activeBuffer.y)}registerCsiHandler(e,t){return e.final==="t"&&!e.prefix&&!e.intermediates?this._parser.registerCsiHandler(e,r=>Nt(r.params[0],this._optionsService.rawOptions.windowOptions)?t(r):!0):this._parser.registerCsiHandler(e,t)}registerDcsHandler(e,t){return this._parser.registerDcsHandler(e,new fe(t))}registerEscHandler(e,t){return this._parser.registerEscHandler(e,t)}registerOscHandler(e,t){return this._parser.registerOscHandler(e,new y(t))}registerApcHandler(e,t){return this._parser.registerApcHandler(e,new Me(t))}bell(){return this._onRequestBell.fire(),!0}lineFeed(){return this._dirtyRowTracker.markDirty(this._activeBuffer.y),this._optionsService.rawOptions.convertEol&&(this._activeBuffer.x=0),this._activeBuffer.y++,this._activeBuffer.y===this._activeBuffer.scrollBottom+1?(this._activeBuffer.y--,this._bufferService.scroll(this._eraseAttrData())):this._activeBuffer.y>=this._bufferService.rows?this._activeBuffer.y=this._bufferService.rows-1:this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y).isWrapped=!1,this._activeBuffer.x>=this._bufferService.cols&&this._activeBuffer.x--,this._dirtyRowTracker.markDirty(this._activeBuffer.y),this._onLineFeed.fire(),!0}carriageReturn(){return this._activeBuffer.x=0,!0}backspace(){if(!this._coreService.decPrivateModes.reverseWraparound)return this._restrictCursor(),this._activeBuffer.x>0&&this._activeBuffer.x--,!0;if(this._restrictCursor(this._bufferService.cols),this._activeBuffer.x>0)this._activeBuffer.x--;else if(this._activeBuffer.x===0&&this._activeBuffer.y>this._activeBuffer.scrollTop&&this._activeBuffer.y<=this._activeBuffer.scrollBottom&&this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y)?.isWrapped){this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y).isWrapped=!1,this._activeBuffer.y--,this._activeBuffer.x=this._bufferService.cols-1;let e=this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y);e.hasWidth(this._activeBuffer.x)&&!e.hasContent(this._activeBuffer.x)&&this._activeBuffer.x--}return this._restrictCursor(),!0}tab(){if(this._activeBuffer.x>=this._bufferService.cols)return!0;let e=this._activeBuffer.x;return this._activeBuffer.x=this._activeBuffer.nextStop(),this._optionsService.rawOptions.screenReaderMode&&this._onA11yTab.fire(this._activeBuffer.x-e),!0}shiftOut(){return this._charsetService.setgLevel(1),!0}shiftIn(){return this._charsetService.setgLevel(0),!0}_restrictCursor(e=this._bufferService.cols-1){this._activeBuffer.x=Math.min(e,Math.max(0,this._activeBuffer.x)),this._activeBuffer.y=this._coreService.decPrivateModes.origin?Math.min(this._activeBuffer.scrollBottom,Math.max(this._activeBuffer.scrollTop,this._activeBuffer.y)):Math.min(this._bufferService.rows-1,Math.max(0,this._activeBuffer.y)),this._dirtyRowTracker.markDirty(this._activeBuffer.y)}_setCursor(e,t){this._dirtyRowTracker.markDirty(this._activeBuffer.y),this._coreService.decPrivateModes.origin?(this._activeBuffer.x=e,this._activeBuffer.y=this._activeBuffer.scrollTop+t):(this._activeBuffer.x=e,this._activeBuffer.y=t),this._restrictCursor(),this._dirtyRowTracker.markDirty(this._activeBuffer.y)}_moveCursor(e,t){this._restrictCursor(),this._setCursor(this._activeBuffer.x+e,this._activeBuffer.y+t)}cursorUp(e){let t=this._activeBuffer.y-this._activeBuffer.scrollTop;return t>=0?this._moveCursor(0,-Math.min(t,e.params[0]||1)):this._moveCursor(0,-(e.params[0]||1)),!0}cursorDown(e){let t=this._activeBuffer.scrollBottom-this._activeBuffer.y;return t>=0?this._moveCursor(0,Math.min(t,e.params[0]||1)):this._moveCursor(0,e.params[0]||1),!0}cursorForward(e){return this._moveCursor(e.params[0]||1,0),!0}cursorBackward(e){return this._moveCursor(-(e.params[0]||1),0),!0}cursorNextLine(e){return this.cursorDown(e),this._activeBuffer.x=0,!0}cursorPrecedingLine(e){return this.cursorUp(e),this._activeBuffer.x=0,!0}cursorCharAbsolute(e){return this._setCursor((e.params[0]||1)-1,this._activeBuffer.y),!0}cursorPosition(e){return this._setCursor(e.length>=2?(e.params[1]||1)-1:0,(e.params[0]||1)-1),!0}charPosAbsolute(e){return this._setCursor((e.params[0]||1)-1,this._activeBuffer.y),!0}hPositionRelative(e){return this._moveCursor(e.params[0]||1,0),!0}linePosAbsolute(e){return this._setCursor(this._activeBuffer.x,(e.params[0]||1)-1),!0}vPositionRelative(e){return this._moveCursor(0,e.params[0]||1),!0}hVPosition(e){return this.cursorPosition(e),!0}tabClear(e){let t=e.params[0];return t===0?delete this._activeBuffer.tabs[this._activeBuffer.x]:t===3&&(this._activeBuffer.tabs={}),!0}cursorForwardTab(e){if(this._activeBuffer.x>=this._bufferService.cols)return!0;let t=e.params[0]||1;for(;t--;)this._activeBuffer.x=this._activeBuffer.nextStop();return!0}cursorBackwardTab(e){if(this._activeBuffer.x>=this._bufferService.cols)return!0;let t=e.params[0]||1;for(;t--;)this._activeBuffer.x=this._activeBuffer.prevStop();return!0}selectProtected(e){let t=e.params[0];return t===1&&(this._curAttrData.bg|=536870912),(t===2||t===0)&&(this._curAttrData.bg&=-536870913),!0}_eraseInBufferLine(e,t,r,s=!1,o=!1){let l=this._activeBuffer.lines.get(this._activeBuffer.ybase+e);l&&(l.replaceCells(t,r,this._activeBuffer.getNullCell(this._eraseAttrData()),o),s&&(l.isWrapped=!1))}_resetBufferLine(e,t=!1){let r=this._activeBuffer.lines.get(this._activeBuffer.ybase+e);r&&(r.fill(this._activeBuffer.getNullCell(this._eraseAttrData()),t),this._bufferService.buffer.clearMarkers(this._activeBuffer.ybase+e),r.isWrapped=!1)}eraseInDisplay(e,t=!1){this._restrictCursor(this._bufferService.cols);let r;switch(e.params[0]){case 0:for(r=this._activeBuffer.y,this._dirtyRowTracker.markDirty(r),this._eraseInBufferLine(r++,this._activeBuffer.x,this._bufferService.cols,this._activeBuffer.x===0,t);r=this._bufferService.cols){let o=this._activeBuffer.lines.get(r+1);o&&(o.isWrapped=!1)}for(;r--;)this._resetBufferLine(r,t);this._dirtyRowTracker.markDirty(0);break;case 2:if(this._optionsService.rawOptions.scrollOnEraseInDisplay){for(r=this._bufferService.rows,this._dirtyRowTracker.markRangeDirty(0,r-1);r--&&!this._activeBuffer.lines.get(this._activeBuffer.ybase+r)?.getTrimmedLength(););for(;r>=0;r--)this._bufferService.scroll(this._eraseAttrData())}else{for(r=this._bufferService.rows,this._dirtyRowTracker.markDirty(r-1);r--;)this._resetBufferLine(r,t);this._dirtyRowTracker.markDirty(0)}break;case 3:let s=this._activeBuffer.lines.length-this._bufferService.rows;s>0&&(this._activeBuffer.lines.trimStart(s),this._activeBuffer.ybase=Math.max(this._activeBuffer.ybase-s,0),this._activeBuffer.ydisp=Math.max(this._activeBuffer.ydisp-s,0),this._activeBuffer===this._bufferService.buffers.normal&&(this._bufferService.isUserScrolling=!1),this._onScroll.fire(0));break}return!0}eraseInLine(e,t=!1){switch(this._restrictCursor(this._bufferService.cols),e.params[0]){case 0:this._eraseInBufferLine(this._activeBuffer.y,this._activeBuffer.x,this._bufferService.cols,this._activeBuffer.x===0,t);break;case 1:this._eraseInBufferLine(this._activeBuffer.y,0,this._activeBuffer.x+1,!1,t);break;case 2:this._eraseInBufferLine(this._activeBuffer.y,0,this._bufferService.cols,!0,t);break}return this._dirtyRowTracker.markDirty(this._activeBuffer.y),!0}insertLines(e){this._restrictCursor();let t=e.params[0]||1;if(this._activeBuffer.y>this._activeBuffer.scrollBottom||this._activeBuffer.ythis._activeBuffer.scrollBottom||this._activeBuffer.ythis._activeBuffer.scrollBottom||this._activeBuffer.ythis._activeBuffer.scrollBottom||this._activeBuffer.ythis._activeBuffer.scrollBottom||this._activeBuffer.ythis._activeBuffer.scrollBottom||this._activeBuffer.y65535?2:1}let a=u;for(let f=1;f0||(this._is("xterm")||this._is("rxvt-unicode")||this._is("screen")?this._coreService.triggerDataEvent("\x1B[?1;2c"):this._is("linux")&&this._coreService.triggerDataEvent("\x1B[?6c")),!0}sendDeviceAttributesSecondary(e){return e.params[0]>0||(this._is("xterm")?this._coreService.triggerDataEvent("\x1B[>0;276;0c"):this._is("rxvt-unicode")?this._coreService.triggerDataEvent("\x1B[>85;95;0c"):this._is("linux")?this._coreService.triggerDataEvent(e.params[0]+"c"):this._is("screen")&&this._coreService.triggerDataEvent("\x1B[>83;40003;0c")),!0}sendXtVersion(e){return e.params[0]>0||this._coreService.triggerDataEvent(`\x1BP>|xterm.js(${Mt})\x1B\\`),!0}_is(e){return(this._optionsService.rawOptions.termName+"").startsWith(e)}setMode(e){for(let t=0;t(A[A.NOT_RECOGNIZED=0]="NOT_RECOGNIZED",A[A.SET=1]="SET",A[A.RESET=2]="RESET",A[A.PERMANENTLY_SET=3]="PERMANENTLY_SET",A[A.PERMANENTLY_RESET=4]="PERMANENTLY_RESET"))(r||={});let s=this._coreService.decPrivateModes,{activeProtocol:o,activeEncoding:l}=this._mouseStateService,c=this._coreService,{buffers:h,cols:u}=this._bufferService,{active:a,alt:f}=h,_=this._optionsService.rawOptions,p=(b,S)=>(c.triggerDataEvent(`\x1B[${t?"":"?"}${b};${S}$y`),!0),v=b=>b?1:2,d=e.params[0];return t?d===2?p(d,4):d===4?p(d,v(c.modes.insertMode)):d===12?p(d,3):d===20?p(d,v(_.convertEol)):p(d,0):d===1?p(d,v(s.applicationCursorKeys)):d===3?p(d,_.windowOptions.setWinLines?u===80?2:u===132?1:0:0):d===6?p(d,v(s.origin)):d===7?p(d,v(s.wraparound)):d===8?p(d,3):d===9?p(d,v(o==="X10")):d===12?p(d,v(_.cursorBlink)):d===25?p(d,v(!c.isCursorHidden)):d===45?p(d,v(s.reverseWraparound)):d===66?p(d,v(s.applicationKeypad)):d===67?p(d,4):d===1e3?p(d,v(o==="VT200")):d===1002?p(d,v(o==="DRAG")):d===1003?p(d,v(o==="ANY")):d===1004?p(d,v(s.sendFocus)):d===1005?p(d,4):d===1006?p(d,v(l==="SGR")):d===1015?p(d,4):d===1016?p(d,v(l==="SGR_PIXELS")):d===1048?p(d,1):d===47||d===1047||d===1049?p(d,v(a===f)):d===2004?p(d,v(s.bracketedPasteMode)):d===2026?p(d,v(s.synchronizedOutput)):d===9001&&this._optionsService.rawOptions.vtExtensions?.win32InputMode?p(d,v(s.win32InputMode)):p(d,0)}_updateAttrColor(e,t,r,s,o){return t===2?(e|=50331648,e&=-16777216,e|=G.fromColorRGB([r,s,o])):t===5&&(e&=-67108864,e|=33554432|r&255),e}_extractColor(e,t,r){let s=[0,0,-1,0,0,0],o=0,l=0;do{if(s[l+o]=e.params[t+l],e.hasSubParams(t+l)){let c=e.getSubParams(t+l),h=0;do s[1]===5&&(o=1),s[l+h+1+o]=c[h];while(++h=2||s[1]===2&&l+o>=5)break;s[1]&&(o=1)}while(++l+t5)&&(e=1),t.extended.underlineStyle=e,t.fg|=268435456,e===0&&(t.fg&=-268435457),t.updateExtended()}_processSGR0(e){e.fg=C.fg,e.bg=C.bg,e.extended=e.extended.clone(),e.extended.underlineStyle=0,e.extended.underlineColor&=-67108864,e.updateExtended()}charAttributes(e){if(e.length===1&&e.params[0]===0)return this._processSGR0(this._curAttrData),!0;let t=e.length,r,s=this._curAttrData;for(let o=0;o=30&&r<=37?(s.fg&=-67108864,s.fg|=16777216|r-30):r>=40&&r<=47?(s.bg&=-67108864,s.bg|=16777216|r-40):r>=90&&r<=97?(s.fg&=-67108864,s.fg|=16777216|r-90|8):r>=100&&r<=107?(s.bg&=-67108864,s.bg|=16777216|r-100|8):r===0?this._processSGR0(s):r===1?s.fg|=134217728:r===3?s.bg|=67108864:r===4?(s.fg|=268435456,this._processUnderline(e.hasSubParams(o)?e.getSubParams(o)[0]:1,s)):r===5?s.fg|=536870912:r===7?s.fg|=67108864:r===8?s.fg|=1073741824:r===9?s.fg|=2147483648:r===2?s.bg|=134217728:r===21?this._processUnderline(2,s):r===22?(s.fg&=-134217729,s.bg&=-134217729):r===23?s.bg&=-67108865:r===24?(s.fg&=-268435457,this._processUnderline(0,s)):r===25?s.fg&=-536870913:r===27?s.fg&=-67108865:r===28?s.fg&=-1073741825:r===29?s.fg&=2147483647:r===39?(s.fg&=-67108864,s.fg|=C.fg&16777215):r===49?(s.bg&=-67108864,s.bg|=C.bg&16777215):r===38||r===48||r===58?o+=this._extractColor(e,o,s):r===53?s.bg|=1073741824:r===55?s.bg&=-1073741825:r===221&&(this._optionsService.rawOptions.vtExtensions?.kittySgrBoldFaintControl??!0)?s.fg&=-134217729:r===222&&(this._optionsService.rawOptions.vtExtensions?.kittySgrBoldFaintControl??!0)?s.bg&=-134217729:r===59?(s.extended=s.extended.clone(),s.extended.underlineColor=-1,s.updateExtended()):this._logService.debug("Unknown SGR attribute: %d.",r);return!0}deviceStatus(e){switch(e.params[0]){case 5:this._coreService.triggerDataEvent("\x1B[0n");break;case 6:let t=this._activeBuffer.y+1,r=this._activeBuffer.x+1;this._coreService.triggerDataEvent(`\x1B[${t};${r}R`);break}return!0}deviceStatusPrivate(e){switch(e.params[0]){case 6:let t=this._activeBuffer.y+1,r=this._activeBuffer.x+1;this._coreService.triggerDataEvent(`\x1B[?${t};${r}R`);break;case 15:break;case 25:break;case 26:break;case 53:break;case 996:(this._optionsService.rawOptions.vtExtensions?.colorSchemeQuery??!0)&&this._onRequestColorSchemeQuery.fire();break}return!0}softReset(e){return this._coreService.isCursorHidden=!1,this._onRequestSyncScrollBar.fire(),this._activeBuffer.scrollTop=0,this._activeBuffer.scrollBottom=this._bufferService.rows-1,this._curAttrData=C.clone(),this._coreService.reset(),this._charsetService.reset(),this._activeBuffer.savedX=0,this._activeBuffer.savedY=this._activeBuffer.ybase,this._activeBuffer.savedCurAttrData.fg=this._curAttrData.fg,this._activeBuffer.savedCurAttrData.bg=this._curAttrData.bg,this._activeBuffer.savedCharset=this._charsetService.charset,this._coreService.decPrivateModes.origin=!1,!0}setCursorStyle(e){let t=e.length===0?1:e.params[0];if(t===0)this._coreService.decPrivateModes.cursorStyle=void 0,this._coreService.decPrivateModes.cursorBlink=void 0;else{switch(t){case 1:case 2:this._coreService.decPrivateModes.cursorStyle="block";break;case 3:case 4:this._coreService.decPrivateModes.cursorStyle="underline";break;case 5:case 6:this._coreService.decPrivateModes.cursorStyle="bar";break}let r=t%2===1;this._coreService.decPrivateModes.cursorBlink=r}return!0}setScrollRegion(e){let t=e.params[0]||1,r;return(e.length<2||(r=e.params[1])>this._bufferService.rows||r===0)&&(r=this._bufferService.rows),r>t&&(this._activeBuffer.scrollTop=t-1,this._activeBuffer.scrollBottom=r-1,this._setCursor(0,0)),!0}windowOptions(e){if(!Nt(e.params[0],this._optionsService.rawOptions.windowOptions))return!0;let t=e.length>1?e.params[1]:0;switch(e.params[0]){case 14:t!==2&&this._onRequestWindowsOptionsReport.fire(0);break;case 16:this._onRequestWindowsOptionsReport.fire(1);break;case 18:this._bufferService&&this._coreService.triggerDataEvent(`\x1B[8;${this._bufferService.rows};${this._bufferService.cols}t`);break;case 22:(t===0||t===2)&&(this._windowTitleStack.push(this._windowTitle),this._windowTitleStack.length>10&&this._windowTitleStack.shift()),(t===0||t===1)&&(this._iconNameStack.push(this._iconName),this._iconNameStack.length>10&&this._iconNameStack.shift());break;case 23:(t===0||t===2)&&this._windowTitleStack.length&&this.setTitle(this._windowTitleStack.pop()),(t===0||t===1)&&this._iconNameStack.length&&this.setIconName(this._iconNameStack.pop());break}return!0}saveCursor(e){return this._activeBuffer.savedX=this._activeBuffer.x,this._activeBuffer.savedY=this._activeBuffer.ybase+this._activeBuffer.y,this._activeBuffer.savedCurAttrData.fg=this._curAttrData.fg,this._activeBuffer.savedCurAttrData.bg=this._curAttrData.bg,this._activeBuffer.savedCharset=this._charsetService.charset,this._activeBuffer.savedCharsets=this._charsetService.charsets.slice(),this._activeBuffer.savedGlevel=this._charsetService.glevel,this._activeBuffer.savedOriginMode=this._coreService.decPrivateModes.origin,this._activeBuffer.savedWraparoundMode=this._coreService.decPrivateModes.wraparound,!0}restoreCursor(e){this._activeBuffer.x=this._activeBuffer.savedX||0,this._activeBuffer.y=Math.max(this._activeBuffer.savedY-this._activeBuffer.ybase,0),this._curAttrData.fg=this._activeBuffer.savedCurAttrData.fg,this._curAttrData.bg=this._activeBuffer.savedCurAttrData.bg;for(let t=0;t1;){let s=r.shift(),o=r.shift();if(/^\d+$/.exec(s)){let l=parseInt(s,10);if(Ut(l))if(o==="?")t.push({type:0,index:l});else{let c=lt(o);c&&t.push({type:1,index:l,color:c})}}}return t.length&&this._onColor.fire(t),!0}setHyperlink(e){let t=e.indexOf(";");if(t===-1)return!0;let r=e.slice(0,t).trim(),s=e.slice(t+1);return s?this._createHyperlink(r,s):r.trim()?!1:this._finishHyperlink()}_createHyperlink(e,t){this._getCurrentLinkId()&&this._finishHyperlink();let r=e.split(":"),s,o=r.findIndex(l=>l.startsWith("id="));return o!==-1&&(s=r[o].slice(3)||void 0),this._curAttrData.extended=this._curAttrData.extended.clone(),this._curAttrData.extended.urlId=this._oscLinkService.registerLink({id:s,uri:t}),this._curAttrData.updateExtended(),!0}_finishHyperlink(){return this._curAttrData.extended=this._curAttrData.extended.clone(),this._curAttrData.extended.urlId=0,this._curAttrData.updateExtended(),!0}_setOrReportSpecialColor(e,t){let r=e.split(";");for(let s=0;s=this._specialColors.length);++s,++t)if(r[s]==="?")this._onColor.fire([{type:0,index:this._specialColors[t]}]);else{let o=lt(r[s]);o&&this._onColor.fire([{type:1,index:this._specialColors[t],color:o}])}return!0}setOrReportFgColor(e){return this._setOrReportSpecialColor(e,0)}setOrReportBgColor(e){return this._setOrReportSpecialColor(e,1)}setOrReportCursorColor(e){return this._setOrReportSpecialColor(e,2)}restoreIndexedColor(e){if(!e)return this._onColor.fire([{type:2}]),!0;let t=[],r=e.split(";");for(let s=0;s=this._bufferService.rows&&(this._activeBuffer.y=this._bufferService.rows-1),this._restrictCursor(),!0}tabSet(){return this._activeBuffer.tabs[this._activeBuffer.x]=!0,!0}reverseIndex(){if(this._restrictCursor(),this._activeBuffer.y===this._activeBuffer.scrollTop){let e=this._activeBuffer.scrollBottom-this._activeBuffer.scrollTop;this._activeBuffer.lines.shiftElements(this._activeBuffer.ybase+this._activeBuffer.y,e,1),this._activeBuffer.lines.set(this._activeBuffer.ybase+this._activeBuffer.y,this._activeBuffer.getBlankLine(this._eraseAttrData())),this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop,this._activeBuffer.scrollBottom)}else this._activeBuffer.y--,this._restrictCursor();return!0}fullReset(){return this._parser.reset(),this._onRequestReset.fire(),!0}reset(){this._curAttrData=C.clone(),this._eraseAttrDataInternal=C.clone()}_eraseAttrData(){return this._eraseAttrDataInternal.bg&=-67108864,this._eraseAttrDataInternal.bg|=this._curAttrData.bg&67108863,this._eraseAttrDataInternal}setgLevel(e){return this._charsetService.setgLevel(e),!0}screenAlignmentPattern(){let e=new P;e.content=1<<22|69,e.fg=this._curAttrData.fg,e.bg=this._curAttrData.bg,this._setCursor(0,0);for(let t=0;t(this._coreService.triggerDataEvent(`\x1B${c}\x1B\\`),!0),s=this._bufferService.buffer,o=this._optionsService.rawOptions,l={block:2,underline:4,bar:6};return r(e==='"q'?`P1$r${this._curAttrData.isProtected()?1:0}"q`:e==='"p'?'P1$r61;1"p':e==="r"?`P1$r${s.scrollTop+1};${s.scrollBottom+1}r`:e==="m"?"P1$r0m":e===" q"?`P1$r${l[o.cursorStyle]-(o.cursorBlink?1:0)} q`:"P0$r")}markRangeDirty(e,t){this._dirtyRowTracker.markRangeDirty(e,t)}kittyKeyboardSet(e){if(!this._optionsService.rawOptions.vtExtensions?.kittyKeyboard)return!0;let t=e.params[0]||0,r=e.length>1&&e.params[1]||1,s=this._coreService.kittyKeyboard;switch(r){case 1:s.flags=t;break;case 2:s.flags|=t;break;case 3:s.flags&=~t;break}return!0}kittyKeyboardQuery(e){if(!this._optionsService.rawOptions.vtExtensions?.kittyKeyboard)return!0;let t=this._coreService.kittyKeyboard.flags;return this._coreService.triggerDataEvent(`\x1B[?${t}u`),!0}kittyKeyboardPush(e){if(!this._optionsService.rawOptions.vtExtensions?.kittyKeyboard)return!0;let t=e.params[0]||0,r=this._coreService.kittyKeyboard,o=this._bufferService.buffer===this._bufferService.buffers.alt?r.altStack:r.mainStack;return o.length>=16&&o.shift(),o.push(r.flags),r.flags=t,!0}kittyKeyboardPop(e){if(!this._optionsService.rawOptions.vtExtensions?.kittyKeyboard)return!0;let t=Math.max(1,e.params[0]||1),r=this._coreService.kittyKeyboard,o=this._bufferService.buffer===this._bufferService.buffers.alt?r.altStack:r.mainStack;for(let l=0;l0;l++)r.flags=o.pop();return o.length===0&&t>0&&(r.flags=0),!0}},_e=class{constructor(i){this._bufferService=i;this.clearRange()}clearRange(){this.start=this._bufferService.buffer.y,this.end=this._bufferService.buffer.y}markDirty(i){ithis.end&&(this.end=i)}markRangeDirty(i,e){i>e&&(Ft=i,i=e,e=Ft),ithis.end&&(this.end=e)}markAllDirty(){this.markRangeDirty(0,this._bufferService.rows-1)}};_e=H([R(0,q)],_e);function Ut(n){return 0<=n&&n<256}var We=class{constructor(){this._token=-1;this._isDisposed=!1}dispose(){this.cancel(),this._isDisposed=!0}cancel(){this._token!==-1&&(clearTimeout(this._token),this._token=-1)}cancelAndSet(i,e){if(this._isDisposed)throw new Error("Calling cancelAndSet on a disposed TimeoutTimer");this.cancel(),this._token=setTimeout(()=>{this._token=-1,i()},e)}setIfNotSet(i,e){if(this._isDisposed)throw new Error("Calling setIfNotSet on a disposed TimeoutTimer");this._token===-1&&(this._token=setTimeout(()=>{this._token=-1,i()},e))}};var Ge=class extends I{constructor(e){super();this._action=e;this._writeBuffer=[];this._callbacks=[];this._pendingData=0;this._bufferOffset=0;this._isSyncWriting=!1;this._syncCalls=0;this._didUserInput=!1;this._innerWriteTimer=this._register(new We);this._onWriteParsed=this._register(new m);this.onWriteParsed=this._onWriteParsed.event;this._register(D(()=>{this._writeBuffer.length=0,this._callbacks.length=0,this._pendingData=0,this._bufferOffset=0}))}handleUserInput(){this._didUserInput=!0}flushSync(){if(this._store.isDisposed||this._isSyncWriting)return;this._isSyncWriting=!0;let e,t=!1;for(;e=this._writeBuffer.shift();){t=!0,this._action(e);let r=this._callbacks.shift();r&&r()}this._pendingData=0,this._bufferOffset=2147483647,this._writeBuffer.length=0,this._callbacks.length=0,this._isSyncWriting=!1,t&&this._onWriteParsed.fire()}writeSync(e,t){if(this._store.isDisposed)return;if(t!==void 0&&this._syncCalls>t){this._syncCalls=0;return}if(this._pendingData+=e.length,this._writeBuffer.push(e),this._callbacks.push(void 0),this._syncCalls++,this._isSyncWriting)return;this._isSyncWriting=!0;let r;for(;r=this._writeBuffer.shift();){this._action(r);let s=this._callbacks.shift();s&&s()}this._pendingData=0,this._bufferOffset=2147483647,this._isSyncWriting=!1,this._syncCalls=0}write(e,t){if(!this._store.isDisposed){if(this._pendingData>5e7)throw new Error("write data discarded, use flow control to avoid losing data");if(!this._writeBuffer.length){if(this._bufferOffset=0,this._didUserInput){this._didUserInput=!1,this._pendingData+=e.length,this._writeBuffer.push(e),this._callbacks.push(t),this._innerWrite();return}this._scheduleInnerWrite()}this._pendingData+=e.length,this._writeBuffer.push(e),this._callbacks.push(t)}}_scheduleInnerWrite(e=0,t=!0){this._store.isDisposed||this._innerWriteTimer.cancelAndSet(()=>this._innerWrite(e,t),0)}_innerWrite(e=0,t=!0){if(this._store.isDisposed)return;let r=e||performance.now();for(;this._writeBuffer.length>this._bufferOffset;){let s=this._writeBuffer[this._bufferOffset],o=this._action(s,t);if(o){let c=h=>{this._store.isDisposed||(performance.now()-r>=12?this._scheduleInnerWrite(0,h):this._innerWrite(r,h))};o.catch(h=>(queueMicrotask(()=>{throw h}),Promise.resolve(!1))).then(c);return}let l=this._callbacks[this._bufferOffset];if(l&&l(),this._bufferOffset++,this._pendingData-=s.length,performance.now()-r>=12)break}this._writeBuffer.length>this._bufferOffset?(this._bufferOffset>50&&(this._writeBuffer=this._writeBuffer.slice(this._bufferOffset),this._callbacks=this._callbacks.slice(this._bufferOffset),this._bufferOffset=0),this._scheduleInnerWrite()):(this._writeBuffer.length=0,this._callbacks.length=0,this._pendingData=0,this._bufferOffset=0),this._onWriteParsed.fire()}};var re=class{constructor(i){this._bufferService=i;this._nextId=1;this._entriesWithId=new Map;this._dataByLinkId=new Map}registerLink(i){let e=this._bufferService.buffer;if(i.id===void 0){let c=e.addMarker(e.ybase+e.y),h={data:i,id:this._nextId++,lines:[c]};return c.onDispose(()=>this._removeMarkerFromLink(h,c)),this._dataByLinkId.set(h.id,h),h.id}let t=i,r=this._getEntryIdKey(t),s=this._entriesWithId.get(r);if(s)return this.addLineToLink(s.id,e.ybase+e.y),s.id;let o=e.addMarker(e.ybase+e.y),l={id:this._nextId++,key:this._getEntryIdKey(t),data:t,lines:[o]};return o.onDispose(()=>this._removeMarkerFromLink(l,o)),this._entriesWithId.set(l.key,l),this._dataByLinkId.set(l.id,l),l.id}addLineToLink(i,e){let t=this._dataByLinkId.get(i);if(t&&t.lines.every(r=>r.line!==e)){let r=this._bufferService.buffer.addMarker(e);t.lines.push(r),r.onDispose(()=>this._removeMarkerFromLink(t,r))}}getLinkData(i){return this._dataByLinkId.get(i)?.data}_getEntryIdKey(i){return`${i.id};;${i.uri}`}_removeMarkerFromLink(i,e){let t=i.lines.indexOf(e);t!==-1&&(i.lines.splice(t,1),i.lines.length===0&&(i.data.id!==void 0&&this._entriesWithId.delete(i.key),this._dataByLinkId.delete(i.id)))}};re=H([R(0,q)],re);var Wt=!1,Ke=class extends I{constructor(e){super();this._windowsWrappingHeuristics=this._register(new Y);this._onBinary=this._register(new m);this.onBinary=this._onBinary.event;this._onData=this._register(new m);this.onData=this._onData.event;this._onLineFeed=this._register(new m);this.onLineFeed=this._onLineFeed.event;this._onRender=this._register(new m);this.onRender=this._onRender.event;this._onResize=this._register(new m);this.onResize=this._onResize.event;this._onWriteParsed=this._register(new m);this.onWriteParsed=this._onWriteParsed.event;this._onScroll=this._register(new m);this._instantiationService=new xe,this.optionsService=this._register(new Ae(e)),this._instantiationService.setService(V,this.optionsService),this._logService=this._register(this._instantiationService.createInstance(Z)),this._instantiationService.setService(z,this._logService),this._bufferService=this._register(this._instantiationService.createInstance(Q)),this._instantiationService.setService(q,this._bufferService),this.coreService=this._register(this._instantiationService.createInstance(te)),this._instantiationService.setService(bt,this.coreService),this.mouseStateService=this._register(this._instantiationService.createInstance(Pe)),this._instantiationService.setService(_t,this.mouseStateService),this.unicodeService=this._register(this._instantiationService.createInstance(L)),this.unicodeService.register(new Be),this._instantiationService.setService(gt,this.unicodeService),this._charsetService=this._instantiationService.createInstance(Re),this._instantiationService.setService(mt,this._charsetService),this._oscLinkService=this._instantiationService.createInstance(re),this._instantiationService.setService(It,this._oscLinkService),this._inputHandler=this._register(new Ue(this._bufferService,this._charsetService,this.coreService,this._logService,this.optionsService,this._oscLinkService,this.mouseStateService,this.unicodeService)),this._register(B.forward(this._inputHandler.onLineFeed,this._onLineFeed)),this._register(B.forward(this._bufferService.onResize,this._onResize)),this._register(B.forward(this.coreService.onData,this._onData)),this._register(B.forward(this.coreService.onBinary,this._onBinary)),this._register(this.coreService.onRequestScrollToBottom(()=>this.scrollToBottom(!0))),this._register(this.coreService.onUserInput(()=>this._writeBuffer.handleUserInput())),this._register(this.optionsService.onMultipleOptionChange(["windowsPty"],()=>this._handleWindowsPtyOptionChange())),this._register(this._bufferService.onScroll(()=>{this._onScroll.fire({position:this._bufferService.buffer.ydisp}),this._inputHandler.markRangeDirty(this._bufferService.buffer.scrollTop,this._bufferService.buffer.scrollBottom)})),this._writeBuffer=this._register(new Ge((t,r)=>this._inputHandler.parse(t,r))),this._register(B.forward(this._writeBuffer.onWriteParsed,this._onWriteParsed))}get onScroll(){return this._onScrollApi||(this._onScrollApi=this._register(new m),this._onScroll.event(e=>{this._onScrollApi?.fire(e.position)})),this._onScrollApi.event}get cols(){return this._bufferService.cols}get rows(){return this._bufferService.rows}get buffers(){return this._bufferService.buffers}get options(){return this.optionsService.options}set options(e){for(let t in e)this.optionsService.options[t]=e[t]}write(e,t){this._writeBuffer.write(e,t)}writeSync(e,t){this._logService.logLevel<=3&&!Wt&&(this._logService.warn("writeSync is unreliable and will be removed soon."),Wt=!0),this._writeBuffer.writeSync(e,t)}input(e,t=!0){this.coreService.triggerDataEvent(e,t)}resize(e,t){isNaN(e)||isNaN(t)||(e=Math.max(e,2),t=Math.max(t,1),this._writeBuffer.flushSync(),this._bufferService.resize(e,t))}scroll(e,t=!1){this._bufferService.scroll(e,t)}scrollLines(e,t){this._bufferService.scrollLines(e,t)}scrollPages(e){this.scrollLines(e*(this.rows-1))}scrollToTop(){this.scrollLines(-this._bufferService.buffer.ydisp)}scrollToBottom(e){this.scrollLines(this._bufferService.buffer.ybase-this._bufferService.buffer.ydisp)}scrollToLine(e){let t=e-this._bufferService.buffer.ydisp;t!==0&&this.scrollLines(t)}registerEscHandler(e,t){return this._inputHandler.registerEscHandler(e,t)}registerDcsHandler(e,t){return this._inputHandler.registerDcsHandler(e,t)}registerCsiHandler(e,t){return this._inputHandler.registerCsiHandler(e,t)}registerOscHandler(e,t){return this._inputHandler.registerOscHandler(e,t)}registerApcHandler(e,t){return this._inputHandler.registerApcHandler(e,t)}_setup(){this._handleWindowsPtyOptionChange()}reset(){this._inputHandler.reset(),this._bufferService.reset(),this._charsetService.reset(),this.coreService.reset(),this.mouseStateService.reset()}_handleWindowsPtyOptionChange(){let e=!1,t=this.optionsService.rawOptions.windowsPty;t&&t.backend!==void 0&&t.buildNumber!==void 0&&(e=t.backend==="conpty"&&t.buildNumber<21376),e?this._enableWindowsWrappingHeuristics():this._windowsWrappingHeuristics.clear()}_enableWindowsWrappingHeuristics(){if(!this._windowsWrappingHeuristics.value){let e=[];e.push(this.onLineFeed(nt.bind(null,this._bufferService))),e.push(this.registerCsiHandler({final:"H"},()=>(nt(this._bufferService),!1))),this._windowsWrappingHeuristics.value=D(()=>{for(let t of e)t.dispose()})}}};var qe=class extends Ke{constructor(e={}){super(e);this._onBell=this._register(new m);this.onBell=this._onBell.event;this._onCursorMove=this._register(new m);this.onCursorMove=this._onCursorMove.event;this._onTitleChange=this._register(new m);this.onTitleChange=this._onTitleChange.event;this._onA11yCharEmitter=this._register(new m);this.onA11yChar=this._onA11yCharEmitter.event;this._onA11yTabEmitter=this._register(new m);this.onA11yTab=this._onA11yTabEmitter.event;this._setup(),this._register(this._inputHandler.onRequestBell(()=>this.bell())),this._register(this._inputHandler.onRequestReset(()=>this.reset())),this._register(B.forward(this._inputHandler.onCursorMove,this._onCursorMove)),this._register(B.forward(this._inputHandler.onTitleChange,this._onTitleChange)),this._register(B.forward(this._inputHandler.onA11yChar,this._onA11yCharEmitter)),this._register(B.forward(this._inputHandler.onA11yTab,this._onA11yTabEmitter)),this._register(B.forward(B.map(this._inputHandler.onRequestRefreshRows,t=>({start:t?.start??0,end:t?.end??this.rows-1})),this._onRender))}get buffer(){return this.buffers.active}get markers(){return this.buffer.markers}registerMarker(e){return this.buffer.addMarker(this.buffer.ybase+this.buffer.y+e)}bell(){this._onBell.fire()}input(e,t=!0){this.coreService.triggerDataEvent(e,t)}resize(e,t){e===this.cols&&t===this.rows||super.resize(e,t)}clear(){this.buffer.clearAllMarkers(),this.buffer.lines.set(0,this.buffer.lines.get(this.buffer.ybase+this.buffer.y)),this.buffer.lines.length=1,this.buffer.ydisp=0,this.buffer.ybase=0,this.buffer.y=0;for(let e=1;e=0;i--)this._addons[i].instance.dispose()}loadAddon(i,e){let t={instance:e,dispose:e.dispose,isDisposed:!1};this._addons.push(t),e.dispose=()=>this._wrappedAddonDispose(t),e.activate(i)}_wrappedAddonDispose(i){if(i.isDisposed)return;let e=-1;for(let t=0;tthis._core.options[r],t=(r,s)=>{this._checkReadonlyOptions(r),this._core.options[r]=s};for(let r in this._core.options){let s={get:e.bind(this,r),set:t.bind(this,r)};Object.defineProperty(this._publicOptions,r,s)}}_checkReadonlyOptions(i){if(ni.includes(i))throw new Error(`Option "${i}" can only be set in the constructor`)}_checkProposedApi(){if(!this._core.optionsService.options.allowProposedApi)throw new Error("You must set the allowProposedApi option to true to use proposed API")}get onBell(){return this._core.onBell}get onBinary(){return this._core.onBinary}get onCursorMove(){return this._core.onCursorMove}get onData(){return this._core.onData}get onLineFeed(){return this._core.onLineFeed}get onRender(){return this._core.onRender}get onResize(){return this._core.onResize}get onScroll(){return this._core.onScroll}get onTitleChange(){return this._core.onTitleChange}get onWriteParsed(){return this._core.onWriteParsed}get parser(){return this._parser??=new ge(this._core),this._parser}get unicode(){return this._checkProposedApi(),new Se(this._core)}get rows(){return this._core.rows}get cols(){return this._core.cols}get buffer(){return this._buffer??=this._register(new Ie(this._core)),this._buffer}get markers(){return this._core.markers}get modes(){let i=this._core.coreService.decPrivateModes,e="none";switch(this._core.mouseStateService.activeProtocol){case"X10":e="x10";break;case"VT200":e="vt200";break;case"DRAG":e="drag";break;case"ANY":e="any";break}return{applicationCursorKeysMode:i.applicationCursorKeys,applicationKeypadMode:i.applicationKeypad,bracketedPasteMode:i.bracketedPasteMode,insertMode:this._core.coreService.modes.insertMode,mouseTrackingMode:e,originMode:i.origin,reverseWraparoundMode:i.reverseWraparound,sendFocusMode:i.sendFocus,showCursor:!this._core.coreService.isCursorHidden,synchronizedOutputMode:i.synchronizedOutput,win32InputMode:i.win32InputMode,wraparoundMode:i.wraparound}}get options(){return this._publicOptions}set options(i){for(let e in i)this._publicOptions[e]=i[e]}input(i,e=!0){this._core.input(i,e)}resize(i,e){this._verifyIntegers(i,e),this._core.resize(i,e)}registerMarker(i=0){return this._verifyIntegers(i),this._core.registerMarker(i)}addMarker(i){return this.registerMarker(i)}dispose(){super.dispose()}scrollLines(i){this._verifyIntegers(i),this._core.scrollLines(i)}scrollPages(i){this._verifyIntegers(i),this._core.scrollPages(i)}scrollToTop(){this._core.scrollToTop()}scrollToBottom(){this._core.scrollToBottom()}scrollToLine(i){this._verifyIntegers(i),this._core.scrollToLine(i)}clear(){this._core.clear()}write(i,e){this._core.write(i,e)}writeln(i,e){this._core.write(i),this._core.write(`\r ++`,()=>this.lineFeed()),this._parser.setExecuteHandler("\v",()=>this.lineFeed()),this._parser.setExecuteHandler("\f",()=>this.lineFeed()),this._parser.setExecuteHandler("\r",()=>this.carriageReturn()),this._parser.setExecuteHandler("\b",()=>this.backspace()),this._parser.setExecuteHandler(" ",()=>this.tab()),this._parser.setExecuteHandler("",()=>this.shiftOut()),this._parser.setExecuteHandler("",()=>this.shiftIn()),this._parser.setExecuteHandler("\x84",()=>this.index()),this._parser.setExecuteHandler("\x85",()=>this.nextLine()),this._parser.setExecuteHandler("\x88",()=>this.tabSet()),this._parser.registerOscHandler(0,new y(a=>(this.setTitle(a),this.setIconName(a),!0))),this._parser.registerOscHandler(1,new y(a=>this.setIconName(a))),this._parser.registerOscHandler(2,new y(a=>this.setTitle(a))),this._parser.registerOscHandler(4,new y(a=>this.setOrReportIndexedColor(a))),this._parser.registerOscHandler(8,new y(a=>this.setHyperlink(a))),this._parser.registerOscHandler(10,new y(a=>this.setOrReportFgColor(a))),this._parser.registerOscHandler(11,new y(a=>this.setOrReportBgColor(a))),this._parser.registerOscHandler(12,new y(a=>this.setOrReportCursorColor(a))),this._parser.registerOscHandler(104,new y(a=>this.restoreIndexedColor(a))),this._parser.registerOscHandler(110,new y(a=>this.restoreFgColor(a))),this._parser.registerOscHandler(111,new y(a=>this.restoreBgColor(a))),this._parser.registerOscHandler(112,new y(a=>this.restoreCursorColor(a))),this._parser.registerEscHandler({final:"7"},()=>this.saveCursor()),this._parser.registerEscHandler({final:"8"},()=>this.restoreCursor()),this._parser.registerEscHandler({final:"D"},()=>this.index()),this._parser.registerEscHandler({final:"E"},()=>this.nextLine()),this._parser.registerEscHandler({final:"H"},()=>this.tabSet()),this._parser.registerEscHandler({final:"M"},()=>this.reverseIndex()),this._parser.registerEscHandler({final:"="},()=>this.keypadApplicationMode()),this._parser.registerEscHandler({final:">"},()=>this.keypadNumericMode()),this._parser.registerEscHandler({final:"c"},()=>this.fullReset()),this._parser.registerEscHandler({final:"n"},()=>this.setgLevel(2)),this._parser.registerEscHandler({final:"o"},()=>this.setgLevel(3)),this._parser.registerEscHandler({final:"|"},()=>this.setgLevel(3)),this._parser.registerEscHandler({final:"}"},()=>this.setgLevel(2)),this._parser.registerEscHandler({final:"~"},()=>this.setgLevel(1)),this._parser.registerEscHandler({intermediates:"%",final:"@"},()=>this.selectDefaultCharset()),this._parser.registerEscHandler({intermediates:"%",final:"G"},()=>this.selectDefaultCharset());for(let a in x)this._parser.registerEscHandler({intermediates:"(",final:a},()=>this.selectCharset("("+a)),this._parser.registerEscHandler({intermediates:")",final:a},()=>this.selectCharset(")"+a)),this._parser.registerEscHandler({intermediates:"*",final:a},()=>this.selectCharset("*"+a)),this._parser.registerEscHandler({intermediates:"+",final:a},()=>this.selectCharset("+"+a)),this._parser.registerEscHandler({intermediates:"-",final:a},()=>this.selectCharset("-"+a)),this._parser.registerEscHandler({intermediates:".",final:a},()=>this.selectCharset("."+a)),this._parser.registerEscHandler({intermediates:"/",final:a},()=>this.selectCharset("/"+a));this._parser.registerEscHandler({intermediates:"#",final:"8"},()=>this.screenAlignmentPattern()),this._parser.setErrorHandler(a=>(this._logService.error("Parsing error: ",a),a)),this._parser.registerDcsHandler({intermediates:"$",final:"q"},new fe((a,f)=>this.requestStatusString(a,f)))}getAttrData(){return this._curAttrData}_preserveStack(e,t,r,s){this._parseStack.paused=!0,this._parseStack.cursorStartX=e,this._parseStack.cursorStartY=t,this._parseStack.decodedLength=r,this._parseStack.position=s}_logSlowResolvingAsync(e){if(this._logService.logLevel<=3){let t,r=new Promise((s,o)=>{t=setTimeout(()=>o("#SLOW_TIMEOUT"),5e3)});Promise.race([e,r]).then(()=>{t!==void 0&&clearTimeout(t)},s=>{if(t!==void 0&&clearTimeout(t),s!=="#SLOW_TIMEOUT")throw s;console.warn("async parser handler taking longer than 5000 ms")})}}_getCurrentLinkId(){return this._curAttrData.extended.urlId}parse(e,t){let r,s=this._activeBuffer.x,o=this._activeBuffer.y,l=0,c=this._parseStack.paused;if(c){if(r=this._parser.parse(this._parseBuffer,this._parseStack.decodedLength,t))return this._logSlowResolvingAsync(r),r;s=this._parseStack.cursorStartX,o=this._parseStack.cursorStartY,this._parseStack.paused=!1,e.length>131072&&(l=this._parseStack.position+131072)}if(this._logService.logLevel<=1&&this._logService.debug(`parsing data ${typeof e=="string"?` "${e}"`:` "${Array.prototype.map.call(e,a=>String.fromCharCode(a)).join("")}"`}`),this._logService.logLevel===0&&this._logService.trace("parsing data (codes)",typeof e=="string"?e.split("").map(a=>a.charCodeAt(0)):e),this._parseBuffer.length131072)for(let a=l;a0&&_.getWidth(this._activeBuffer.x-1)===2&&_.setCellFromCodepoint(this._activeBuffer.x-1,0,1,f);let p=this._parser.precedingJoinState;for(let v=t;v=32&&e[v]<127){do _.setCellFromCodepoint(this._activeBuffer.x++,e[v],1,f);while(++v=32&&e[v]<127);v--;continue}if(s=e[v],s===173)continue;if(s<127&&l){let M=l[String.fromCharCode(s)];M&&(s=M.charCodeAt(0))}let d=this._unicodeService.charProperties(s,p);o=L.extractWidth(d);let b=L.extractShouldJoin(d),S=b?L.extractWidth(p):0;p=d,c&&this._onA11yChar.fire(k(s));let O=this._getCurrentLinkId();if(O&&this._oscLinkService.addLineToLink(O,this._activeBuffer.ybase+this._activeBuffer.y),this._activeBuffer.x+o-S>h){if(u){let M=_,g=this._activeBuffer.x-S;if(this._activeBuffer.x=S,this._activeBuffer.y++,this._activeBuffer.y===this._activeBuffer.scrollBottom+1?(this._activeBuffer.y--,this._bufferService.scroll(this._eraseAttrData(),!0)):(this._activeBuffer.y>=this._bufferService.rows&&(this._activeBuffer.y=this._bufferService.rows-1),this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y).isWrapped=!0),_=this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y),!_)return;for(S>0&&_ instanceof U&&_.copyCellsFrom(M,g,0,S,!1);g=0;)_.setCellFromCodepoint(this._activeBuffer.x++,0,0,f);continue}if(a&&(_.insertCells(this._activeBuffer.x,o-S,this._activeBuffer.getNullCell(f)),_.getWidth(h-1)===2&&_.setCellFromCodepoint(h-1,0,1,f)),_.setCellFromCodepoint(this._activeBuffer.x++,s,o,f),o>0)for(;--o;)_.setCellFromCodepoint(this._activeBuffer.x++,0,0,f)}this._parser.precedingJoinState=p,this._activeBuffer.x0&&_.getWidth(this._activeBuffer.x)===0&&!_.hasContent(this._activeBuffer.x)&&_.setCellFromCodepoint(this._activeBuffer.x,0,1,f),this._dirtyRowTracker.markDirty(this._activeBuffer.y)}registerCsiHandler(e,t){return e.final==="t"&&!e.prefix&&!e.intermediates?this._parser.registerCsiHandler(e,r=>Nt(r.params[0],this._optionsService.rawOptions.windowOptions)?t(r):!0):this._parser.registerCsiHandler(e,t)}registerDcsHandler(e,t){return this._parser.registerDcsHandler(e,new fe(t))}registerEscHandler(e,t){return this._parser.registerEscHandler(e,t)}registerOscHandler(e,t){return this._parser.registerOscHandler(e,new y(t))}registerApcHandler(e,t){return this._parser.registerApcHandler(e,new Me(t))}bell(){return this._onRequestBell.fire(),!0}lineFeed(){return this._dirtyRowTracker.markDirty(this._activeBuffer.y),this._optionsService.rawOptions.convertEol&&(this._activeBuffer.x=0),this._activeBuffer.y++,this._activeBuffer.y===this._activeBuffer.scrollBottom+1?(this._activeBuffer.y--,this._bufferService.scroll(this._eraseAttrData())):this._activeBuffer.y>=this._bufferService.rows?this._activeBuffer.y=this._bufferService.rows-1:this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y).isWrapped=!1,this._activeBuffer.x>=this._bufferService.cols&&this._activeBuffer.x--,this._dirtyRowTracker.markDirty(this._activeBuffer.y),this._onLineFeed.fire(),!0}carriageReturn(){return this._activeBuffer.x=0,!0}backspace(){if(!this._coreService.decPrivateModes.reverseWraparound)return this._restrictCursor(),this._activeBuffer.x>0&&this._activeBuffer.x--,!0;if(this._restrictCursor(this._bufferService.cols),this._activeBuffer.x>0)this._activeBuffer.x--;else if(this._activeBuffer.x===0&&this._activeBuffer.y>this._activeBuffer.scrollTop&&this._activeBuffer.y<=this._activeBuffer.scrollBottom&&this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y)?.isWrapped){this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y).isWrapped=!1,this._activeBuffer.y--,this._activeBuffer.x=this._bufferService.cols-1;let e=this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y);e.hasWidth(this._activeBuffer.x)&&!e.hasContent(this._activeBuffer.x)&&this._activeBuffer.x--}return this._restrictCursor(),!0}tab(){if(this._activeBuffer.x>=this._bufferService.cols)return!0;let e=this._activeBuffer.x;return this._activeBuffer.x=this._activeBuffer.nextStop(),this._optionsService.rawOptions.screenReaderMode&&this._onA11yTab.fire(this._activeBuffer.x-e),!0}shiftOut(){return this._charsetService.setgLevel(1),!0}shiftIn(){return this._charsetService.setgLevel(0),!0}_restrictCursor(e=this._bufferService.cols-1){this._activeBuffer.x=Math.min(e,Math.max(0,this._activeBuffer.x)),this._activeBuffer.y=this._coreService.decPrivateModes.origin?Math.min(this._activeBuffer.scrollBottom,Math.max(this._activeBuffer.scrollTop,this._activeBuffer.y)):Math.min(this._bufferService.rows-1,Math.max(0,this._activeBuffer.y)),this._dirtyRowTracker.markDirty(this._activeBuffer.y)}_setCursor(e,t){this._dirtyRowTracker.markDirty(this._activeBuffer.y),this._coreService.decPrivateModes.origin?(this._activeBuffer.x=e,this._activeBuffer.y=this._activeBuffer.scrollTop+t):(this._activeBuffer.x=e,this._activeBuffer.y=t),this._restrictCursor(),this._dirtyRowTracker.markDirty(this._activeBuffer.y)}_moveCursor(e,t){this._restrictCursor(),this._setCursor(this._activeBuffer.x+e,this._activeBuffer.y+t)}cursorUp(e){let t=this._activeBuffer.y-this._activeBuffer.scrollTop;return t>=0?this._moveCursor(0,-Math.min(t,e.params[0]||1)):this._moveCursor(0,-(e.params[0]||1)),!0}cursorDown(e){let t=this._activeBuffer.scrollBottom-this._activeBuffer.y;return t>=0?this._moveCursor(0,Math.min(t,e.params[0]||1)):this._moveCursor(0,e.params[0]||1),!0}cursorForward(e){return this._moveCursor(e.params[0]||1,0),!0}cursorBackward(e){return this._moveCursor(-(e.params[0]||1),0),!0}cursorNextLine(e){return this.cursorDown(e),this._activeBuffer.x=0,!0}cursorPrecedingLine(e){return this.cursorUp(e),this._activeBuffer.x=0,!0}cursorCharAbsolute(e){return this._setCursor((e.params[0]||1)-1,this._activeBuffer.y),!0}cursorPosition(e){return this._setCursor(e.length>=2?(e.params[1]||1)-1:0,(e.params[0]||1)-1),!0}charPosAbsolute(e){return this._setCursor((e.params[0]||1)-1,this._activeBuffer.y),!0}hPositionRelative(e){return this._moveCursor(e.params[0]||1,0),!0}linePosAbsolute(e){return this._setCursor(this._activeBuffer.x,(e.params[0]||1)-1),!0}vPositionRelative(e){return this._moveCursor(0,e.params[0]||1),!0}hVPosition(e){return this.cursorPosition(e),!0}tabClear(e){let t=e.params[0];return t===0?delete this._activeBuffer.tabs[this._activeBuffer.x]:t===3&&(this._activeBuffer.tabs={}),!0}cursorForwardTab(e){if(this._activeBuffer.x>=this._bufferService.cols)return!0;let t=e.params[0]||1;for(;t--;)this._activeBuffer.x=this._activeBuffer.nextStop();return!0}cursorBackwardTab(e){if(this._activeBuffer.x>=this._bufferService.cols)return!0;let t=e.params[0]||1;for(;t--;)this._activeBuffer.x=this._activeBuffer.prevStop();return!0}selectProtected(e){let t=e.params[0];return t===1&&(this._curAttrData.bg|=536870912),(t===2||t===0)&&(this._curAttrData.bg&=-536870913),!0}_eraseInBufferLine(e,t,r,s=!1,o=!1){let l=this._activeBuffer.lines.get(this._activeBuffer.ybase+e);l&&(l.replaceCells(t,r,this._activeBuffer.getNullCell(this._eraseAttrData()),o),s&&(l.isWrapped=!1))}_resetBufferLine(e,t=!1){let r=this._activeBuffer.lines.get(this._activeBuffer.ybase+e);r&&(r.fill(this._activeBuffer.getNullCell(this._eraseAttrData()),t),this._bufferService.buffer.clearMarkers(this._activeBuffer.ybase+e),r.isWrapped=!1)}eraseInDisplay(e,t=!1){this._restrictCursor(this._bufferService.cols);let r;switch(e.params[0]){case 0:for(r=this._activeBuffer.y,this._dirtyRowTracker.markDirty(r),this._eraseInBufferLine(r++,this._activeBuffer.x,this._bufferService.cols,this._activeBuffer.x===0,t);r=this._bufferService.cols){let o=this._activeBuffer.lines.get(r+1);o&&(o.isWrapped=!1)}for(;r--;)this._resetBufferLine(r,t);this._dirtyRowTracker.markDirty(0);break;case 2:if(this._optionsService.rawOptions.scrollOnEraseInDisplay){for(r=this._bufferService.rows,this._dirtyRowTracker.markRangeDirty(0,r-1);r--&&!this._activeBuffer.lines.get(this._activeBuffer.ybase+r)?.getTrimmedLength(););for(;r>=0;r--)this._bufferService.scroll(this._eraseAttrData())}else{for(r=this._bufferService.rows,this._dirtyRowTracker.markDirty(r-1);r--;)this._resetBufferLine(r,t);this._dirtyRowTracker.markDirty(0)}break;case 3:let s=this._activeBuffer.lines.length-this._bufferService.rows;s>0&&(this._activeBuffer.lines.trimStart(s),this._activeBuffer.ybase=Math.max(this._activeBuffer.ybase-s,0),this._activeBuffer.ydisp=Math.max(this._activeBuffer.ydisp-s,0),this._activeBuffer===this._bufferService.buffers.normal&&(this._bufferService.isUserScrolling=!1),this._onScroll.fire(0));break}return!0}eraseInLine(e,t=!1){switch(this._restrictCursor(this._bufferService.cols),e.params[0]){case 0:this._eraseInBufferLine(this._activeBuffer.y,this._activeBuffer.x,this._bufferService.cols,this._activeBuffer.x===0,t);break;case 1:this._eraseInBufferLine(this._activeBuffer.y,0,this._activeBuffer.x+1,!1,t);break;case 2:this._eraseInBufferLine(this._activeBuffer.y,0,this._bufferService.cols,!0,t);break}return this._dirtyRowTracker.markDirty(this._activeBuffer.y),!0}insertLines(e){this._restrictCursor();let t=e.params[0]||1;if(this._activeBuffer.y>this._activeBuffer.scrollBottom||this._activeBuffer.ythis._activeBuffer.scrollBottom||this._activeBuffer.ythis._activeBuffer.scrollBottom||this._activeBuffer.ythis._activeBuffer.scrollBottom||this._activeBuffer.ythis._activeBuffer.scrollBottom||this._activeBuffer.ythis._activeBuffer.scrollBottom||this._activeBuffer.y65535?2:1}let a=u;for(let f=1;f0||(this._is("xterm")||this._is("rxvt-unicode")||this._is("screen")?this._coreService.triggerDataEvent("\x1B[?1;2c"):this._is("linux")&&this._coreService.triggerDataEvent("\x1B[?6c")),!0}sendDeviceAttributesSecondary(e){return e.params[0]>0||(this._is("xterm")?this._coreService.triggerDataEvent("\x1B[>0;276;0c"):this._is("rxvt-unicode")?this._coreService.triggerDataEvent("\x1B[>85;95;0c"):this._is("linux")?this._coreService.triggerDataEvent(e.params[0]+"c"):this._is("screen")&&this._coreService.triggerDataEvent("\x1B[>83;40003;0c")),!0}sendXtVersion(e){return e.params[0]>0||this._coreService.triggerDataEvent(`\x1BP>|xterm.js(${Mt})\x1B\\`),!0}_is(e){return(this._optionsService.rawOptions.termName+"").startsWith(e)}setMode(e){for(let t=0;t(A[A.NOT_RECOGNIZED=0]="NOT_RECOGNIZED",A[A.SET=1]="SET",A[A.RESET=2]="RESET",A[A.PERMANENTLY_SET=3]="PERMANENTLY_SET",A[A.PERMANENTLY_RESET=4]="PERMANENTLY_RESET"))(r||={});let s=this._coreService.decPrivateModes,{activeProtocol:o,activeEncoding:l}=this._mouseStateService,c=this._coreService,{buffers:h,cols:u}=this._bufferService,{active:a,alt:f}=h,_=this._optionsService.rawOptions,p=(b,S)=>(c.triggerDataEvent(`\x1B[${t?"":"?"}${b};${S}$y`),!0),v=b=>b?1:2,d=e.params[0];return t?d===2?p(d,4):d===4?p(d,v(c.modes.insertMode)):d===12?p(d,3):d===20?p(d,v(_.convertEol)):p(d,0):d===1?p(d,v(s.applicationCursorKeys)):d===3?p(d,_.windowOptions.setWinLines?u===80?2:u===132?1:0:0):d===6?p(d,v(s.origin)):d===7?p(d,v(s.wraparound)):d===8?p(d,3):d===9?p(d,v(o==="X10")):d===12?p(d,v(_.cursorBlink)):d===25?p(d,v(!c.isCursorHidden)):d===45?p(d,v(s.reverseWraparound)):d===66?p(d,v(s.applicationKeypad)):d===67?p(d,4):d===1e3?p(d,v(o==="VT200")):d===1002?p(d,v(o==="DRAG")):d===1003?p(d,v(o==="ANY")):d===1004?p(d,v(s.sendFocus)):d===1005?p(d,4):d===1006?p(d,v(l==="SGR")):d===1015?p(d,4):d===1016?p(d,v(l==="SGR_PIXELS")):d===1048?p(d,1):d===47||d===1047||d===1049?p(d,v(a===f)):d===2004?p(d,v(s.bracketedPasteMode)):d===2026?p(d,v(s.synchronizedOutput)):d===9001&&this._optionsService.rawOptions.vtExtensions?.win32InputMode?p(d,v(s.win32InputMode)):p(d,0)}_updateAttrColor(e,t,r,s,o){return t===2?(e|=50331648,e&=-16777216,e|=G.fromColorRGB([r,s,o])):t===5&&(e&=-67108864,e|=33554432|r&255),e}_extractColor(e,t,r){let s=[0,0,-1,0,0,0],o=0,l=0;do{if(s[l+o]=e.params[t+l],e.hasSubParams(t+l)){let c=e.getSubParams(t+l),h=0;do s[1]===5&&(o=1),s[l+h+1+o]=c[h];while(++h=2||s[1]===2&&l+o>=5)break;s[1]&&(o=1)}while(++l+t5)&&(e=1),t.extended.underlineStyle=e,t.fg|=268435456,e===0&&(t.fg&=-268435457),t.updateExtended()}_processSGR0(e){e.fg=C.fg,e.bg=C.bg,e.extended=e.extended.clone(),e.extended.underlineStyle=0,e.extended.underlineColor&=-67108864,e.updateExtended()}charAttributes(e){if(e.length===1&&e.params[0]===0)return this._processSGR0(this._curAttrData),!0;let t=e.length,r,s=this._curAttrData;for(let o=0;o=30&&r<=37?(s.fg&=-67108864,s.fg|=16777216|r-30):r>=40&&r<=47?(s.bg&=-67108864,s.bg|=16777216|r-40):r>=90&&r<=97?(s.fg&=-67108864,s.fg|=16777216|r-90|8):r>=100&&r<=107?(s.bg&=-67108864,s.bg|=16777216|r-100|8):r===0?this._processSGR0(s):r===1?s.fg|=134217728:r===3?s.bg|=67108864:r===4?(s.fg|=268435456,this._processUnderline(e.hasSubParams(o)?e.getSubParams(o)[0]:1,s)):r===5?s.fg|=536870912:r===7?s.fg|=67108864:r===8?s.fg|=1073741824:r===9?s.fg|=2147483648:r===2?s.bg|=134217728:r===21?this._processUnderline(2,s):r===22?(s.fg&=-134217729,s.bg&=-134217729):r===23?s.bg&=-67108865:r===24?(s.fg&=-268435457,this._processUnderline(0,s)):r===25?s.fg&=-536870913:r===27?s.fg&=-67108865:r===28?s.fg&=-1073741825:r===29?s.fg&=2147483647:r===39?(s.fg&=-67108864,s.fg|=C.fg&16777215):r===49?(s.bg&=-67108864,s.bg|=C.bg&16777215):r===38||r===48||r===58?o+=this._extractColor(e,o,s):r===53?s.bg|=1073741824:r===55?s.bg&=-1073741825:r===221&&(this._optionsService.rawOptions.vtExtensions?.kittySgrBoldFaintControl??!0)?s.fg&=-134217729:r===222&&(this._optionsService.rawOptions.vtExtensions?.kittySgrBoldFaintControl??!0)?s.bg&=-134217729:r===59?(s.extended=s.extended.clone(),s.extended.underlineColor=-1,s.updateExtended()):this._logService.debug("Unknown SGR attribute: %d.",r);return!0}deviceStatus(e){switch(e.params[0]){case 5:this._coreService.triggerDataEvent("\x1B[0n");break;case 6:let t=this._activeBuffer.y+1,r=this._activeBuffer.x+1;this._coreService.triggerDataEvent(`\x1B[${t};${r}R`);break}return!0}deviceStatusPrivate(e){switch(e.params[0]){case 6:let t=this._activeBuffer.y+1,r=this._activeBuffer.x+1;this._coreService.triggerDataEvent(`\x1B[?${t};${r}R`);break;case 15:break;case 25:break;case 26:break;case 53:break;case 996:(this._optionsService.rawOptions.vtExtensions?.colorSchemeQuery??!0)&&this._onRequestColorSchemeQuery.fire();break}return!0}softReset(e){return this._coreService.isCursorHidden=!1,this._onRequestSyncScrollBar.fire(),this._activeBuffer.scrollTop=0,this._activeBuffer.scrollBottom=this._bufferService.rows-1,this._curAttrData=C.clone(),this._coreService.reset(),this._charsetService.reset(),this._activeBuffer.savedX=0,this._activeBuffer.savedY=this._activeBuffer.ybase,this._activeBuffer.savedCurAttrData.fg=this._curAttrData.fg,this._activeBuffer.savedCurAttrData.bg=this._curAttrData.bg,this._activeBuffer.savedCharset=this._charsetService.charset,this._coreService.decPrivateModes.origin=!1,!0}setCursorStyle(e){let t=e.length===0?1:e.params[0];if(t===0)this._coreService.decPrivateModes.cursorStyle=void 0,this._coreService.decPrivateModes.cursorBlink=void 0;else{switch(t){case 1:case 2:this._coreService.decPrivateModes.cursorStyle="block";break;case 3:case 4:this._coreService.decPrivateModes.cursorStyle="underline";break;case 5:case 6:this._coreService.decPrivateModes.cursorStyle="bar";break}let r=t%2===1;this._coreService.decPrivateModes.cursorBlink=r}return!0}setScrollRegion(e){let t=e.params[0]||1,r;return(e.length<2||(r=e.params[1])>this._bufferService.rows||r===0)&&(r=this._bufferService.rows),r>t&&(this._activeBuffer.scrollTop=t-1,this._activeBuffer.scrollBottom=r-1,this._setCursor(0,0)),!0}windowOptions(e){if(!Nt(e.params[0],this._optionsService.rawOptions.windowOptions))return!0;let t=e.length>1?e.params[1]:0;switch(e.params[0]){case 14:t!==2&&this._onRequestWindowsOptionsReport.fire(0);break;case 16:this._onRequestWindowsOptionsReport.fire(1);break;case 18:this._bufferService&&this._coreService.triggerDataEvent(`\x1B[8;${this._bufferService.rows};${this._bufferService.cols}t`);break;case 22:(t===0||t===2)&&(this._windowTitleStack.push(this._windowTitle),this._windowTitleStack.length>10&&this._windowTitleStack.shift()),(t===0||t===1)&&(this._iconNameStack.push(this._iconName),this._iconNameStack.length>10&&this._iconNameStack.shift());break;case 23:(t===0||t===2)&&this._windowTitleStack.length&&this.setTitle(this._windowTitleStack.pop()),(t===0||t===1)&&this._iconNameStack.length&&this.setIconName(this._iconNameStack.pop());break}return!0}saveCursor(e){return this._activeBuffer.savedX=this._activeBuffer.x,this._activeBuffer.savedY=this._activeBuffer.ybase+this._activeBuffer.y,this._activeBuffer.savedCurAttrData.fg=this._curAttrData.fg,this._activeBuffer.savedCurAttrData.bg=this._curAttrData.bg,this._activeBuffer.savedCharset=this._charsetService.charset,this._activeBuffer.savedCharsets=this._charsetService.charsets.slice(),this._activeBuffer.savedGlevel=this._charsetService.glevel,this._activeBuffer.savedOriginMode=this._coreService.decPrivateModes.origin,this._activeBuffer.savedWraparoundMode=this._coreService.decPrivateModes.wraparound,!0}restoreCursor(e){this._activeBuffer.x=this._activeBuffer.savedX||0,this._activeBuffer.y=Math.max(this._activeBuffer.savedY-this._activeBuffer.ybase,0),this._curAttrData.fg=this._activeBuffer.savedCurAttrData.fg,this._curAttrData.bg=this._activeBuffer.savedCurAttrData.bg;for(let t=0;t1;){let s=r.shift(),o=r.shift();if(/^\d+$/.exec(s)){let l=parseInt(s,10);if(Ut(l))if(o==="?")t.push({type:0,index:l});else{let c=lt(o);c&&t.push({type:1,index:l,color:c})}}}return t.length&&this._onColor.fire(t),!0}setHyperlink(e){let t=e.indexOf(";");if(t===-1)return!0;let r=e.slice(0,t).trim(),s=e.slice(t+1);return s?this._createHyperlink(r,s):r.trim()?!1:this._finishHyperlink()}_createHyperlink(e,t){this._getCurrentLinkId()&&this._finishHyperlink();let r=e.split(":"),s,o=r.findIndex(l=>l.startsWith("id="));return o!==-1&&(s=r[o].slice(3)||void 0),this._curAttrData.extended=this._curAttrData.extended.clone(),this._curAttrData.extended.urlId=this._oscLinkService.registerLink({id:s,uri:t}),this._curAttrData.updateExtended(),!0}_finishHyperlink(){return this._curAttrData.extended=this._curAttrData.extended.clone(),this._curAttrData.extended.urlId=0,this._curAttrData.updateExtended(),!0}_setOrReportSpecialColor(e,t){let r=e.split(";");for(let s=0;s=this._specialColors.length);++s,++t)if(r[s]==="?")this._onColor.fire([{type:0,index:this._specialColors[t]}]);else{let o=lt(r[s]);o&&this._onColor.fire([{type:1,index:this._specialColors[t],color:o}])}return!0}setOrReportFgColor(e){return this._setOrReportSpecialColor(e,0)}setOrReportBgColor(e){return this._setOrReportSpecialColor(e,1)}setOrReportCursorColor(e){return this._setOrReportSpecialColor(e,2)}restoreIndexedColor(e){if(!e)return this._onColor.fire([{type:2}]),!0;let t=[],r=e.split(";");for(let s=0;s=this._bufferService.rows&&(this._activeBuffer.y=this._bufferService.rows-1),this._restrictCursor(),!0}tabSet(){return this._activeBuffer.tabs[this._activeBuffer.x]=!0,!0}reverseIndex(){if(this._restrictCursor(),this._activeBuffer.y===this._activeBuffer.scrollTop){let e=this._activeBuffer.scrollBottom-this._activeBuffer.scrollTop;this._activeBuffer.lines.shiftElements(this._activeBuffer.ybase+this._activeBuffer.y,e,1),this._activeBuffer.lines.set(this._activeBuffer.ybase+this._activeBuffer.y,this._activeBuffer.getBlankLine(this._eraseAttrData())),this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop,this._activeBuffer.scrollBottom)}else this._activeBuffer.y--,this._restrictCursor();return!0}fullReset(){return this._parser.reset(),this._onRequestReset.fire(),!0}reset(){this._curAttrData=C.clone(),this._eraseAttrDataInternal=C.clone()}_eraseAttrData(){return this._eraseAttrDataInternal.bg&=-67108864,this._eraseAttrDataInternal.bg|=this._curAttrData.bg&67108863,this._eraseAttrDataInternal}setgLevel(e){return this._charsetService.setgLevel(e),!0}screenAlignmentPattern(){let e=new P;e.content=1<<22|69,e.fg=this._curAttrData.fg,e.bg=this._curAttrData.bg,this._setCursor(0,0);for(let t=0;t(this._coreService.triggerDataEvent(`\x1B${c}\x1B\\`),!0),s=this._bufferService.buffer,o=this._optionsService.rawOptions,l={block:2,underline:4,bar:6};return r(e==='"q'?`P1$r${this._curAttrData.isProtected()?1:0}"q`:e==='"p'?'P1$r61;1"p':e==="r"?`P1$r${s.scrollTop+1};${s.scrollBottom+1}r`:e==="m"?"P1$r0m":e===" q"?`P1$r${l[o.cursorStyle]-(o.cursorBlink?1:0)} q`:"P0$r")}markRangeDirty(e,t){this._dirtyRowTracker.markRangeDirty(e,t)}kittyKeyboardSet(e){if(!this._optionsService.rawOptions.vtExtensions?.kittyKeyboard)return!0;let t=e.params[0]||0,r=e.length>1&&e.params[1]||1,s=this._coreService.kittyKeyboard;switch(r){case 1:s.flags=t;break;case 2:s.flags|=t;break;case 3:s.flags&=~t;break}return!0}kittyKeyboardQuery(e){if(!this._optionsService.rawOptions.vtExtensions?.kittyKeyboard)return!0;let t=this._coreService.kittyKeyboard.flags;return this._coreService.triggerDataEvent(`\x1B[?${t}u`),!0}kittyKeyboardPush(e){if(!this._optionsService.rawOptions.vtExtensions?.kittyKeyboard)return!0;let t=e.params[0]||0,r=this._coreService.kittyKeyboard,o=this._bufferService.buffer===this._bufferService.buffers.alt?r.altStack:r.mainStack;return o.length>=16&&o.shift(),o.push(r.flags),r.flags=t,!0}kittyKeyboardPop(e){if(!this._optionsService.rawOptions.vtExtensions?.kittyKeyboard)return!0;let t=Math.max(1,e.params[0]||1),r=this._coreService.kittyKeyboard,o=this._bufferService.buffer===this._bufferService.buffers.alt?r.altStack:r.mainStack;for(let l=0;l0;l++)r.flags=o.pop();return o.length===0&&t>0&&(r.flags=0),!0}},_e=class{constructor(i){this._bufferService=i;this.clearRange()}clearRange(){this.start=this._bufferService.buffer.y,this.end=this._bufferService.buffer.y}markDirty(i){ithis.end&&(this.end=i)}markRangeDirty(i,e){i>e&&(Ft=i,i=e,e=Ft),ithis.end&&(this.end=e)}markAllDirty(){this.markRangeDirty(0,this._bufferService.rows-1)}};_e=H([R(0,q)],_e);function Ut(n){return 0<=n&&n<256}var We=class{constructor(){this._token=-1;this._isDisposed=!1}dispose(){this.cancel(),this._isDisposed=!0}cancel(){this._token!==-1&&(clearTimeout(this._token),this._token=-1)}cancelAndSet(i,e){if(this._isDisposed)throw new Error("Calling cancelAndSet on a disposed TimeoutTimer");this.cancel(),this._token=setTimeout(()=>{this._token=-1,i()},e)}setIfNotSet(i,e){if(this._isDisposed)throw new Error("Calling setIfNotSet on a disposed TimeoutTimer");this._token===-1&&(this._token=setTimeout(()=>{this._token=-1,i()},e))}};var Ge=class extends I{constructor(e){super();this._action=e;this._writeBuffer=[];this._callbacks=[];this._pendingData=0;this._bufferOffset=0;this._isSyncWriting=!1;this._syncCalls=0;this._didUserInput=!1;this._innerWriteTimer=this._register(new We);this._onWriteParsed=this._register(new m);this.onWriteParsed=this._onWriteParsed.event;this._register(D(()=>{this._writeBuffer.length=0,this._callbacks.length=0,this._pendingData=0,this._bufferOffset=0}))}handleUserInput(){this._didUserInput=!0}flushSync(){if(this._store.isDisposed||this._isSyncWriting)return;this._isSyncWriting=!0;let e,t=!1;for(;e=this._writeBuffer.shift();){t=!0,this._action(e);let r=this._callbacks.shift();r&&r()}this._pendingData=0,this._bufferOffset=2147483647,this._writeBuffer.length=0,this._callbacks.length=0,this._isSyncWriting=!1,t&&this._onWriteParsed.fire()}writeSync(e,t){if(this._store.isDisposed)return;if(t!==void 0&&this._syncCalls>t){this._syncCalls=0;return}if(this._pendingData+=e.length,this._writeBuffer.push(e),this._callbacks.push(void 0),this._syncCalls++,this._isSyncWriting)return;this._isSyncWriting=!0;let r;for(;r=this._writeBuffer.shift();){this._action(r);let s=this._callbacks.shift();s&&s()}this._pendingData=0,this._bufferOffset=2147483647,this._isSyncWriting=!1,this._syncCalls=0}write(e,t){if(!this._store.isDisposed){if(this._pendingData>5e7)throw new Error("write data discarded, use flow control to avoid losing data");if(!this._writeBuffer.length){if(this._bufferOffset=0,this._didUserInput){this._didUserInput=!1,this._pendingData+=e.length,this._writeBuffer.push(e),this._callbacks.push(t),this._innerWrite();return}this._scheduleInnerWrite()}this._pendingData+=e.length,this._writeBuffer.push(e),this._callbacks.push(t)}}_scheduleInnerWrite(e=0,t=!0){this._store.isDisposed||this._innerWriteTimer.cancelAndSet(()=>this._innerWrite(e,t),0)}_innerWrite(e=0,t=!0){if(this._store.isDisposed)return;let r=e||performance.now();for(;this._writeBuffer.length>this._bufferOffset;){let s=this._writeBuffer[this._bufferOffset],o=this._action(s,t);if(o){let c=h=>{this._store.isDisposed||(performance.now()-r>=12?this._scheduleInnerWrite(0,h):this._innerWrite(r,h))};o.catch(h=>(queueMicrotask(()=>{throw h}),Promise.resolve(!1))).then(c);return}let l=this._callbacks[this._bufferOffset];if(l&&l(),this._bufferOffset++,this._pendingData-=s.length,performance.now()-r>=12)break}this._writeBuffer.length>this._bufferOffset?(this._bufferOffset>50&&(this._writeBuffer=this._writeBuffer.slice(this._bufferOffset),this._callbacks=this._callbacks.slice(this._bufferOffset),this._bufferOffset=0),this._scheduleInnerWrite()):(this._writeBuffer.length=0,this._callbacks.length=0,this._pendingData=0,this._bufferOffset=0),this._onWriteParsed.fire()}};var re=class{constructor(i){this._bufferService=i;this._nextId=1;this._entriesWithId=new Map;this._dataByLinkId=new Map}registerLink(i){let e=this._bufferService.buffer;if(i.id===void 0){let c=e.addMarker(e.ybase+e.y),h={data:i,id:this._nextId++,lines:[c]};return c.onDispose(()=>this._removeMarkerFromLink(h,c)),this._dataByLinkId.set(h.id,h),h.id}let t=i,r=this._getEntryIdKey(t),s=this._entriesWithId.get(r);if(s)return this.addLineToLink(s.id,e.ybase+e.y),s.id;let o=e.addMarker(e.ybase+e.y),l={id:this._nextId++,key:this._getEntryIdKey(t),data:t,lines:[o]};return o.onDispose(()=>this._removeMarkerFromLink(l,o)),this._entriesWithId.set(l.key,l),this._dataByLinkId.set(l.id,l),l.id}addLineToLink(i,e){let t=this._dataByLinkId.get(i);if(t&&t.lines.every(r=>r.line!==e)){let r=this._bufferService.buffer.addMarker(e);t.lines.push(r),r.onDispose(()=>this._removeMarkerFromLink(t,r))}}getLinkData(i){return this._dataByLinkId.get(i)?.data}_getEntryIdKey(i){return`${i.id};;${i.uri}`}_removeMarkerFromLink(i,e){let t=i.lines.indexOf(e);t!==-1&&(i.lines.splice(t,1),i.lines.length===0&&(i.data.id!==void 0&&this._entriesWithId.delete(i.key),this._dataByLinkId.delete(i.id)))}};re=H([R(0,q)],re);var Wt=!1,Ke=class extends I{constructor(e){super();this._windowsWrappingHeuristics=this._register(new Y);this._onBinary=this._register(new m);this.onBinary=this._onBinary.event;this._onData=this._register(new m);this.onData=this._onData.event;this._onLineFeed=this._register(new m);this.onLineFeed=this._onLineFeed.event;this._onRender=this._register(new m);this.onRender=this._onRender.event;this._onResize=this._register(new m);this.onResize=this._onResize.event;this._onWriteParsed=this._register(new m);this.onWriteParsed=this._onWriteParsed.event;this._onScroll=this._register(new m);this._instantiationService=new xe,this.optionsService=this._register(new Ae(e)),this._instantiationService.setService(V,this.optionsService),this._logService=this._register(this._instantiationService.createInstance(Z)),this._instantiationService.setService(z,this._logService),this._bufferService=this._register(this._instantiationService.createInstance(Q)),this._instantiationService.setService(q,this._bufferService),this.coreService=this._register(this._instantiationService.createInstance(te)),this._instantiationService.setService(bt,this.coreService),this.mouseStateService=this._register(this._instantiationService.createInstance(Pe)),this._instantiationService.setService(_t,this.mouseStateService),this.unicodeService=this._register(this._instantiationService.createInstance(L)),this.unicodeService.register(new Be),this._instantiationService.setService(gt,this.unicodeService),this._charsetService=this._instantiationService.createInstance(Re),this._instantiationService.setService(mt,this._charsetService),this._oscLinkService=this._instantiationService.createInstance(re),this._instantiationService.setService(It,this._oscLinkService),this._inputHandler=this._register(new Ue(this._bufferService,this._charsetService,this.coreService,this._logService,this.optionsService,this._oscLinkService,this.mouseStateService,this.unicodeService)),this._register(B.forward(this._inputHandler.onLineFeed,this._onLineFeed)),this._register(B.forward(this._bufferService.onResize,this._onResize)),this._register(B.forward(this.coreService.onData,this._onData)),this._register(B.forward(this.coreService.onBinary,this._onBinary)),this._register(this.coreService.onRequestScrollToBottom(()=>this.scrollToBottom(!0))),this._register(this.coreService.onUserInput(()=>this._writeBuffer.handleUserInput())),this._register(this.optionsService.onMultipleOptionChange(["windowsPty"],()=>this._handleWindowsPtyOptionChange())),this._register(this._bufferService.onScroll(()=>{this._onScroll.fire({position:this._bufferService.buffer.ydisp}),this._inputHandler.markRangeDirty(this._bufferService.buffer.scrollTop,this._bufferService.buffer.scrollBottom)})),this._writeBuffer=this._register(new Ge((t,r)=>this._inputHandler.parse(t,r))),this._register(B.forward(this._writeBuffer.onWriteParsed,this._onWriteParsed))}get onScroll(){return this._onScrollApi||(this._onScrollApi=this._register(new m),this._onScroll.event(e=>{this._onScrollApi?.fire(e.position)})),this._onScrollApi.event}get cols(){return this._bufferService.cols}get rows(){return this._bufferService.rows}get buffers(){return this._bufferService.buffers}get options(){return this.optionsService.options}set options(e){for(let t in e)this.optionsService.options[t]=e[t]}write(e,t){this._writeBuffer.write(e,t)}writeSync(e,t){this._logService.logLevel<=3&&!Wt&&(this._logService.warn("writeSync is unreliable and will be removed soon."),Wt=!0),this._writeBuffer.writeSync(e,t)}input(e,t=!0){this.coreService.triggerDataEvent(e,t)}resize(e,t){isNaN(e)||isNaN(t)||(e=Math.max(e,2),t=Math.max(t,1),this._writeBuffer.flushSync(),this._bufferService.resize(e,t))}scroll(e,t=!1){this._bufferService.scroll(e,t)}scrollLines(e,t){this._bufferService.scrollLines(e,t)}scrollPages(e){this.scrollLines(e*(this.rows-1))}scrollToTop(){this.scrollLines(-this._bufferService.buffer.ydisp)}scrollToBottom(e){this.scrollLines(this._bufferService.buffer.ybase-this._bufferService.buffer.ydisp)}scrollToLine(e){let t=e-this._bufferService.buffer.ydisp;t!==0&&this.scrollLines(t)}registerEscHandler(e,t){return this._inputHandler.registerEscHandler(e,t)}registerDcsHandler(e,t){return this._inputHandler.registerDcsHandler(e,t)}registerCsiHandler(e,t){return this._inputHandler.registerCsiHandler(e,t)}registerOscHandler(e,t){return this._inputHandler.registerOscHandler(e,t)}registerApcHandler(e,t){return this._inputHandler.registerApcHandler(e,t)}_setup(){this._handleWindowsPtyOptionChange()}reset(){this._inputHandler.reset(),this._bufferService.reset(),this._charsetService.reset(),this.coreService.reset(),this.mouseStateService.reset()}_handleWindowsPtyOptionChange(){let e=!1,t=this.optionsService.rawOptions.windowsPty;t&&t.backend!==void 0&&t.buildNumber!==void 0&&(e=t.backend==="conpty"&&t.buildNumber<21376),e?this._enableWindowsWrappingHeuristics():this._windowsWrappingHeuristics.clear()}_enableWindowsWrappingHeuristics(){if(!this._windowsWrappingHeuristics.value){let e=[];e.push(this.onLineFeed(nt.bind(null,this._bufferService))),e.push(this.registerCsiHandler({final:"H"},()=>(nt(this._bufferService),!1))),this._windowsWrappingHeuristics.value=D(()=>{for(let t of e)t.dispose()})}}};var qe=class extends Ke{constructor(e={}){super(e);this._onBell=this._register(new m);this.onBell=this._onBell.event;this._onCursorMove=this._register(new m);this.onCursorMove=this._onCursorMove.event;this._onTitleChange=this._register(new m);this.onTitleChange=this._onTitleChange.event;this._onA11yCharEmitter=this._register(new m);this.onA11yChar=this._onA11yCharEmitter.event;this._onA11yTabEmitter=this._register(new m);this.onA11yTab=this._onA11yTabEmitter.event;this._setup(),this._register(this._inputHandler.onRequestBell(()=>this.bell())),this._register(this._inputHandler.onRequestReset(()=>this.reset())),this._register(B.forward(this._inputHandler.onCursorMove,this._onCursorMove)),this._register(B.forward(this._inputHandler.onTitleChange,this._onTitleChange)),this._register(B.forward(this._inputHandler.onA11yChar,this._onA11yCharEmitter)),this._register(B.forward(this._inputHandler.onA11yTab,this._onA11yTabEmitter)),this._register(B.forward(B.map(this._inputHandler.onRequestRefreshRows,t=>({start:t?.start??0,end:t?.end??this.rows-1})),this._onRender))}get buffer(){return this.buffers.active}get markers(){return this.buffer.markers}registerMarker(e){return this.buffer.addMarker(this.buffer.ybase+this.buffer.y+e)}bell(){this._onBell.fire()}input(e,t=!0){this.coreService.triggerDataEvent(e,t)}resize(e,t){e===this.cols&&t===this.rows||super.resize(e,t)}clear(){this.buffer.clearAllMarkers(),this.buffer.lines.set(0,this.buffer.lines.get(this.buffer.ybase+this.buffer.y)),this.buffer.lines.length=1,this.buffer.ydisp=0,this.buffer.ybase=0,this.buffer.y=0;for(let e=1;e=0;i--)this._addons[i].instance.dispose()}loadAddon(i,e){let t={instance:e,dispose:e.dispose,isDisposed:!1};this._addons.push(t),e.dispose=()=>this._wrappedAddonDispose(t),e.activate(i)}_wrappedAddonDispose(i){if(i.isDisposed)return;let e=-1;for(let t=0;tthis._core.options[r],t=(r,s)=>{this._checkReadonlyOptions(r),this._core.options[r]=s};for(let r in this._core.options){let s={get:e.bind(this,r),set:t.bind(this,r)};Object.defineProperty(this._publicOptions,r,s)}}_checkReadonlyOptions(i){if(ni.includes(i))throw new Error(`Option "${i}" can only be set in the constructor`)}_checkProposedApi(){if(!this._core.optionsService.options.allowProposedApi)throw new Error("You must set the allowProposedApi option to true to use proposed API")}get onBell(){return this._core.onBell}get onBinary(){return this._core.onBinary}get onCursorMove(){return this._core.onCursorMove}get onData(){return this._core.onData}get onLineFeed(){return this._core.onLineFeed}get onRender(){return this._core.onRender}get onResize(){return this._core.onResize}get onScroll(){return this._core.onScroll}get onTitleChange(){return this._core.onTitleChange}get onWriteParsed(){return this._core.onWriteParsed}get parser(){return this._parser??=new ge(this._core),this._parser}get unicode(){return this._checkProposedApi(),new Se(this._core)}get rows(){return this._core.rows}get cols(){return this._core.cols}get buffer(){return this._buffer??=this._register(new Ie(this._core)),this._buffer}get markers(){return this._core.markers}get modes(){let i=this._core.coreService.decPrivateModes,e="none";switch(this._core.mouseStateService.activeProtocol){case"X10":e="x10";break;case"VT200":e="vt200";break;case"DRAG":e="drag";break;case"ANY":e="any";break}return{applicationCursorKeysMode:i.applicationCursorKeys,applicationKeypadMode:i.applicationKeypad,bracketedPasteMode:i.bracketedPasteMode,insertMode:this._core.coreService.modes.insertMode,mouseTrackingMode:e,originMode:i.origin,reverseWraparoundMode:i.reverseWraparound,sendFocusMode:i.sendFocus,showCursor:!this._core.coreService.isCursorHidden,synchronizedOutputMode:i.synchronizedOutput,win32InputMode:i.win32InputMode,wraparoundMode:i.wraparound}}get options(){return this._publicOptions}set options(i){for(let e in i)this._publicOptions[e]=i[e]}input(i,e=!0){this._core.input(i,e)}resize(i,e){this._verifyIntegers(i,e),this._core.resize(i,e)}registerMarker(i=0){return this._verifyIntegers(i),this._core.registerMarker(i)}addMarker(i){return this.registerMarker(i)}dispose(){super.dispose()}scrollLines(i){this._verifyIntegers(i),this._core.scrollLines(i)}scrollPages(i){this._verifyIntegers(i),this._core.scrollPages(i)}scrollToTop(){this._core.scrollToTop()}scrollToBottom(){this._core.scrollToBottom()}scrollToLine(i){this._verifyIntegers(i),this._core.scrollToLine(i)}clear(){this._core.clear()}write(i,e){this._core.write(i,e)}writeln(i,e){this._core.write(i),this._core.write(`\r + `,e)}reset(){this._core.reset()}loadAddon(i){this._addonManager.loadAddon(this,i)}_verifyIntegers(...i){for(let e of i)if(e===1/0||isNaN(e)||e%1!==0)throw new Error("This API only accepts integers")}};export{Gt as Terminal}; + //# sourceMappingURL=xterm-headless.mjs.map +diff --git a/lib-headless/xterm-headless.mjs.map b/lib-headless/xterm-headless.mjs.map +index 527aa509539bd81f6cfc2d45339fde5cdcc9a1a0..25d55eed2c2ef0a82651141ba6e22726065d1f80 100644 +--- a/lib-headless/xterm-headless.mjs.map ++++ b/lib-headless/xterm-headless.mjs.map +@@ -1,7 +1,7 @@ + { + "version": 3, + "sources": ["../../src/common/input/TextDecoder.ts", "../../src/common/buffer/AttributeData.ts", "../../src/common/buffer/CellData.ts", "../../src/common/public/BufferLineApiView.ts", "../../src/common/public/BufferApiView.ts", "../../src/common/Lifecycle.ts", "../../src/common/Event.ts", "../../src/common/public/BufferNamespaceApi.ts", "../../src/common/public/ParserApi.ts", "../../src/common/public/UnicodeApi.ts", "../../src/common/buffer/BufferLine.ts", "../../src/common/services/ServiceRegistry.ts", "../../src/common/services/Services.ts", "../../src/common/services/InstantiationService.ts", "../../src/common/services/LogService.ts", "../../src/common/CircularList.ts", "../../src/common/TaskQueue.ts", "../../src/common/buffer/BufferReflow.ts", "../../src/common/buffer/Marker.ts", "../../src/common/data/Charsets.ts", "../../src/common/buffer/Buffer.ts", "../../src/common/buffer/BufferSet.ts", "../../src/common/services/BufferService.ts", "../../src/common/Platform.ts", "../../src/common/services/OptionsService.ts", "../../src/common/services/CoreService.ts", "../../src/common/services/MouseStateService.ts", "../../src/common/services/UnicodeService.ts", "../../src/common/input/UnicodeV6.ts", "../../src/common/services/CharsetService.ts", "../../src/common/WindowsMode.ts", "../../src/common/parser/Params.ts", "../../src/common/StringBuilder.ts", "../../src/common/parser/OscParser.ts", "../../src/common/parser/DcsParser.ts", "../../src/common/parser/ApcParser.ts", "../../src/common/parser/EscapeSequenceParser.ts", "../../src/common/input/XParseColor.ts", "../../src/common/Version.ts", "../../src/common/InputHandler.ts", "../../src/common/Async.ts", "../../src/common/input/WriteBuffer.ts", "../../src/common/services/OscLinkService.ts", "../../src/common/CoreTerminal.ts", "../../src/headless/Terminal.ts", "../../src/common/public/AddonManager.ts", "../../src/headless/public/Terminal.ts"], +- "sourcesContent": ["/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\n/**\n * Polyfill - Convert UTF32 codepoint into JS string.\n * Note: The built-in String.fromCodePoint happens to be much slower\n * due to additional sanity checks. We can avoid them since\n * we always operate on legal UTF32 (granted by the input decoders)\n * and use this faster version instead.\n */\nexport function stringFromCodePoint(codePoint: number): string {\n if (codePoint > 0xFFFF) {\n codePoint -= 0x10000;\n return String.fromCharCode((codePoint >> 10) + 0xD800) + String.fromCharCode((codePoint % 0x400) + 0xDC00);\n }\n return String.fromCharCode(codePoint);\n}\n\n/**\n * Convert UTF32 char codes into JS string.\n * Basically the same as `stringFromCodePoint` but for multiple codepoints\n * in a loop (which is a lot faster).\n */\nexport function utf32ToString(data: Uint32Array, start: number = 0, end: number = data.length): string {\n let result = '';\n for (let i = start; i < end; ++i) {\n let codepoint = data[i];\n if (codepoint > 0xFFFF) {\n // JS strings are encoded as UTF16, thus a non BMP codepoint gets converted into a surrogate\n // pair conversion rules:\n // - subtract 0x10000 from code point, leaving a 20 bit number\n // - add high 10 bits to 0xD800 --> first surrogate\n // - add low 10 bits to 0xDC00 --> second surrogate\n codepoint -= 0x10000;\n result += String.fromCharCode((codepoint >> 10) + 0xD800) + String.fromCharCode((codepoint % 0x400) + 0xDC00);\n } else {\n result += String.fromCharCode(codepoint);\n }\n }\n return result;\n}\n\n/**\n * StringToUtf32 - decodes UTF16 sequences into UTF32 codepoints.\n * To keep the decoder in line with JS strings it handles single surrogates as UCS2.\n */\nexport class StringToUtf32 {\n private _interim: number = 0;\n\n /**\n * Clears interim and resets decoder to clean state.\n */\n public clear(): void {\n this._interim = 0;\n }\n\n /**\n * Decode JS string to UTF32 codepoints.\n * The methods assumes stream input and will store partly transmitted\n * surrogate pairs and decode them with the next data chunk.\n * Note: The method does no bound checks for target, therefore make sure\n * the provided input data does not exceed the size of `target`.\n * Returns the number of written codepoints in `target`.\n */\n public decode(input: string, target: Uint32Array): number {\n const length = input.length;\n\n if (!length) {\n return 0;\n }\n\n let size = 0;\n let startPos = 0;\n\n // handle leftover surrogate high\n if (this._interim) {\n const second = input.charCodeAt(startPos++);\n if (0xDC00 <= second && second <= 0xDFFF) {\n target[size++] = (this._interim - 0xD800) * 0x400 + second - 0xDC00 + 0x10000;\n } else {\n // illegal codepoint (USC2 handling)\n target[size++] = this._interim;\n target[size++] = second;\n }\n this._interim = 0;\n }\n\n for (let i = startPos; i < length; ++i) {\n const code = input.charCodeAt(i);\n // surrogate pair first\n if (0xD800 <= code && code <= 0xDBFF) {\n if (++i >= length) {\n this._interim = code;\n return size;\n }\n const second = input.charCodeAt(i);\n if (0xDC00 <= second && second <= 0xDFFF) {\n target[size++] = (code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000;\n } else {\n // illegal codepoint (USC2 handling)\n target[size++] = code;\n target[size++] = second;\n }\n continue;\n }\n if (code === 0xFEFF) {\n // BOM\n continue;\n }\n target[size++] = code;\n }\n return size;\n }\n}\n\n/**\n * Utf8Decoder - decodes UTF8 byte sequences into UTF32 codepoints.\n */\nexport class Utf8ToUtf32 {\n public interim: Uint8Array = new Uint8Array(3);\n\n /**\n * Clears interim bytes and resets decoder to clean state.\n */\n public clear(): void {\n this.interim.fill(0);\n }\n\n /**\n * Decodes UTF8 byte sequences in `input` to UTF32 codepoints in `target`.\n * The methods assumes stream input and will store partly transmitted bytes\n * and decode them with the next data chunk.\n * Note: The method does no bound checks for target, therefore make sure\n * the provided data chunk does not exceed the size of `target`.\n * Returns the number of written codepoints in `target`.\n */\n public decode(input: Uint8Array, target: Uint32Array): number {\n const length = input.length;\n\n if (!length) {\n return 0;\n }\n\n let size = 0;\n let byte1: number;\n let byte2: number;\n let byte3: number;\n let byte4: number;\n let codepoint;\n let startPos = 0;\n\n // handle leftover bytes\n if (this.interim[0]) {\n let discardInterim = false;\n let cp = this.interim[0];\n cp &= ((((cp & 0xE0) === 0xC0)) ? 0x1F : (((cp & 0xF0) === 0xE0)) ? 0x0F : 0x07);\n let pos = 0;\n let tmp: number;\n while ((tmp = this.interim[++pos]) && pos < 4) {\n cp <<= 6;\n cp |= tmp & 0x3F;\n }\n // missing bytes - read ahead from input\n const type = (((this.interim[0] & 0xE0) === 0xC0)) ? 2 : (((this.interim[0] & 0xF0) === 0xE0)) ? 3 : 4;\n const missing = type - pos;\n while (startPos < missing) {\n if (startPos >= length) {\n return 0;\n }\n tmp = input[startPos++];\n if ((tmp & 0xC0) !== 0x80) {\n // wrong continuation, discard interim bytes completely\n startPos--;\n discardInterim = true;\n break;\n } else {\n // need to save so we can continue short inputs in next call\n this.interim[pos++] = tmp;\n cp <<= 6;\n cp |= tmp & 0x3F;\n }\n }\n if (!discardInterim) {\n // final test is type dependent\n if (type === 2) {\n if (cp < 0x80) {\n // wrong starter byte\n startPos--;\n } else {\n target[size++] = cp;\n }\n } else if (type === 3) {\n if (cp < 0x0800 || (cp >= 0xD800 && cp <= 0xDFFF) || cp === 0xFEFF) {\n // illegal codepoint or BOM\n } else {\n target[size++] = cp;\n }\n } else {\n if (cp < 0x010000 || cp > 0x10FFFF) {\n // illegal codepoint\n } else {\n target[size++] = cp;\n }\n }\n }\n this.interim.fill(0);\n }\n\n // loop through input\n const fourStop = length - 4;\n let i = startPos;\n while (i < length) {\n /**\n * ASCII shortcut with loop unrolled to 4 consecutive ASCII chars.\n * This is a compromise between speed gain for ASCII\n * and penalty for non ASCII:\n * For best ASCII performance the char should be stored directly into target,\n * but even a single attempt to write to target and compare afterwards\n * penalizes non ASCII really bad (-50%), thus we load the char into byteX first,\n * which reduces ASCII performance by ~15%.\n * This trial for ASCII reduces non ASCII performance by ~10% which seems acceptible\n * compared to the gains.\n * Note that this optimization only takes place for 4 consecutive ASCII chars,\n * for any shorter it bails out. Worst case - all 4 bytes being read but\n * thrown away due to the last being a non ASCII char (-10% performance).\n */\n while (i < fourStop\n && !((byte1 = input[i]) & 0x80)\n && !((byte2 = input[i + 1]) & 0x80)\n && !((byte3 = input[i + 2]) & 0x80)\n && !((byte4 = input[i + 3]) & 0x80))\n {\n target[size++] = byte1;\n target[size++] = byte2;\n target[size++] = byte3;\n target[size++] = byte4;\n i += 4;\n }\n\n // reread byte1\n byte1 = input[i++];\n\n // 1 byte\n if (byte1 < 0x80) {\n target[size++] = byte1;\n\n // 2 bytes\n } else if ((byte1 & 0xE0) === 0xC0) {\n if (i >= length) {\n this.interim[0] = byte1;\n return size;\n }\n byte2 = input[i++];\n if ((byte2 & 0xC0) !== 0x80) {\n // wrong continuation\n i--;\n continue;\n }\n codepoint = (byte1 & 0x1F) << 6 | (byte2 & 0x3F);\n if (codepoint < 0x80) {\n // wrong starter byte\n i--;\n continue;\n }\n target[size++] = codepoint;\n\n // 3 bytes\n } else if ((byte1 & 0xF0) === 0xE0) {\n if (i >= length) {\n this.interim[0] = byte1;\n return size;\n }\n byte2 = input[i++];\n if ((byte2 & 0xC0) !== 0x80) {\n // wrong continuation\n i--;\n continue;\n }\n if (i >= length) {\n this.interim[0] = byte1;\n this.interim[1] = byte2;\n return size;\n }\n byte3 = input[i++];\n if ((byte3 & 0xC0) !== 0x80) {\n // wrong continuation\n i--;\n continue;\n }\n codepoint = (byte1 & 0x0F) << 12 | (byte2 & 0x3F) << 6 | (byte3 & 0x3F);\n if (codepoint < 0x0800 || (codepoint >= 0xD800 && codepoint <= 0xDFFF) || codepoint === 0xFEFF) {\n // illegal codepoint or BOM, no i-- here\n continue;\n }\n target[size++] = codepoint;\n\n // 4 bytes\n } else if ((byte1 & 0xF8) === 0xF0) {\n if (i >= length) {\n this.interim[0] = byte1;\n return size;\n }\n byte2 = input[i++];\n if ((byte2 & 0xC0) !== 0x80) {\n // wrong continuation\n i--;\n continue;\n }\n if (i >= length) {\n this.interim[0] = byte1;\n this.interim[1] = byte2;\n return size;\n }\n byte3 = input[i++];\n if ((byte3 & 0xC0) !== 0x80) {\n // wrong continuation\n i--;\n continue;\n }\n if (i >= length) {\n this.interim[0] = byte1;\n this.interim[1] = byte2;\n this.interim[2] = byte3;\n return size;\n }\n byte4 = input[i++];\n if ((byte4 & 0xC0) !== 0x80) {\n // wrong continuation\n i--;\n continue;\n }\n codepoint = (byte1 & 0x07) << 18 | (byte2 & 0x3F) << 12 | (byte3 & 0x3F) << 6 | (byte4 & 0x3F);\n if (codepoint < 0x010000 || codepoint > 0x10FFFF) {\n // illegal codepoint, no i-- here\n continue;\n }\n target[size++] = codepoint;\n } else {\n // illegal byte, just skip\n }\n }\n return size;\n }\n}\n", "/**\n * Copyright (c) 2018 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { IColorRGB } from '../Types';\nimport { IAttributeData, IExtendedAttrs } from './Types';\nimport { Attributes, FgFlags, BgFlags, UnderlineStyle, ExtFlags } from './Constants';\n\nexport class AttributeData implements IAttributeData {\n public static toColorRGB(value: number): IColorRGB {\n return [\n value >>> Attributes.RED_SHIFT & 255,\n value >>> Attributes.GREEN_SHIFT & 255,\n value & 255\n ];\n }\n\n public static fromColorRGB(value: IColorRGB): number {\n return (value[0] & 255) << Attributes.RED_SHIFT | (value[1] & 255) << Attributes.GREEN_SHIFT | value[2] & 255;\n }\n\n public clone(): IAttributeData {\n const newObj = new AttributeData();\n newObj.fg = this.fg;\n newObj.bg = this.bg;\n newObj.extended = this.extended.clone();\n return newObj;\n }\n\n // data\n public fg = 0;\n public bg = 0;\n public extended: IExtendedAttrs = new ExtendedAttrs();\n\n // flags\n public isInverse(): number { return this.fg & FgFlags.INVERSE; }\n public isBold(): number { return this.fg & FgFlags.BOLD; }\n public isUnderline(): number {\n if (this.hasExtendedAttrs() && this.extended.underlineStyle !== UnderlineStyle.NONE) {\n return 1;\n }\n return this.fg & FgFlags.UNDERLINE;\n }\n public isBlink(): number { return this.fg & FgFlags.BLINK; }\n public isInvisible(): number { return this.fg & FgFlags.INVISIBLE; }\n public isItalic(): number { return this.bg & BgFlags.ITALIC; }\n public isDim(): number { return this.bg & BgFlags.DIM; }\n public isStrikethrough(): number { return this.fg & FgFlags.STRIKETHROUGH; }\n public isProtected(): number { return this.bg & BgFlags.PROTECTED; }\n public isOverline(): number { return this.bg & BgFlags.OVERLINE; }\n\n // color modes\n public getFgColorMode(): number { return this.fg & Attributes.CM_MASK; }\n public getBgColorMode(): number { return this.bg & Attributes.CM_MASK; }\n public isFgRGB(): boolean { return (this.fg & Attributes.CM_MASK) === Attributes.CM_RGB; }\n public isBgRGB(): boolean { return (this.bg & Attributes.CM_MASK) === Attributes.CM_RGB; }\n public isFgPalette(): boolean { return (this.fg & Attributes.CM_MASK) === Attributes.CM_P16 || (this.fg & Attributes.CM_MASK) === Attributes.CM_P256; }\n public isBgPalette(): boolean { return (this.bg & Attributes.CM_MASK) === Attributes.CM_P16 || (this.bg & Attributes.CM_MASK) === Attributes.CM_P256; }\n public isFgDefault(): boolean { return (this.fg & Attributes.CM_MASK) === 0; }\n public isBgDefault(): boolean { return (this.bg & Attributes.CM_MASK) === 0; }\n public isAttributeDefault(): boolean { return this.fg === 0 && this.bg === 0; }\n\n // colors\n public getFgColor(): number {\n switch (this.fg & Attributes.CM_MASK) {\n case Attributes.CM_P16:\n case Attributes.CM_P256: return this.fg & Attributes.PCOLOR_MASK;\n case Attributes.CM_RGB: return this.fg & Attributes.RGB_MASK;\n default: return -1; // CM_DEFAULT defaults to -1\n }\n }\n public getBgColor(): number {\n switch (this.bg & Attributes.CM_MASK) {\n case Attributes.CM_P16:\n case Attributes.CM_P256: return this.bg & Attributes.PCOLOR_MASK;\n case Attributes.CM_RGB: return this.bg & Attributes.RGB_MASK;\n default: return -1; // CM_DEFAULT defaults to -1\n }\n }\n\n // extended attrs\n public hasExtendedAttrs(): number {\n return this.bg & BgFlags.HAS_EXTENDED;\n }\n public updateExtended(): void {\n if (this.extended.isEmpty()) {\n this.bg &= ~BgFlags.HAS_EXTENDED;\n } else {\n this.bg |= BgFlags.HAS_EXTENDED;\n }\n }\n public getUnderlineColor(): number {\n if ((this.bg & BgFlags.HAS_EXTENDED) && ~this.extended.underlineColor) {\n switch (this.extended.underlineColor & Attributes.CM_MASK) {\n case Attributes.CM_P16:\n case Attributes.CM_P256: return this.extended.underlineColor & Attributes.PCOLOR_MASK;\n case Attributes.CM_RGB: return this.extended.underlineColor & Attributes.RGB_MASK;\n default: return this.getFgColor();\n }\n }\n return this.getFgColor();\n }\n public getUnderlineColorMode(): number {\n return (this.bg & BgFlags.HAS_EXTENDED) && ~this.extended.underlineColor\n ? this.extended.underlineColor & Attributes.CM_MASK\n : this.getFgColorMode();\n }\n public isUnderlineColorRGB(): boolean {\n return (this.bg & BgFlags.HAS_EXTENDED) && ~this.extended.underlineColor\n ? (this.extended.underlineColor & Attributes.CM_MASK) === Attributes.CM_RGB\n : this.isFgRGB();\n }\n public isUnderlineColorPalette(): boolean {\n return (this.bg & BgFlags.HAS_EXTENDED) && ~this.extended.underlineColor\n ? (this.extended.underlineColor & Attributes.CM_MASK) === Attributes.CM_P16\n || (this.extended.underlineColor & Attributes.CM_MASK) === Attributes.CM_P256\n : this.isFgPalette();\n }\n public isUnderlineColorDefault(): boolean {\n return (this.bg & BgFlags.HAS_EXTENDED) && ~this.extended.underlineColor\n ? (this.extended.underlineColor & Attributes.CM_MASK) === 0\n : this.isFgDefault();\n }\n public getUnderlineStyle(): UnderlineStyle {\n return this.fg & FgFlags.UNDERLINE\n ? (this.bg & BgFlags.HAS_EXTENDED ? this.extended.underlineStyle : UnderlineStyle.SINGLE)\n : UnderlineStyle.NONE;\n }\n public getUnderlineVariantOffset(): number {\n return this.extended.underlineVariantOffset;\n }\n}\n\n\n/**\n * Extended attributes for a cell.\n * Holds information about different underline styles and color.\n */\nexport class ExtendedAttrs implements IExtendedAttrs {\n private _ext: number = 0;\n public get ext(): number {\n if (this._urlId) {\n return (\n (this._ext & ~ExtFlags.UNDERLINE_STYLE) |\n (this.underlineStyle << 26)\n );\n }\n return this._ext;\n }\n public set ext(value: number) { this._ext = value; }\n\n public get underlineStyle(): UnderlineStyle {\n // Always return the URL style if it has one\n if (this._urlId) {\n return UnderlineStyle.DASHED;\n }\n return (this._ext & ExtFlags.UNDERLINE_STYLE) >> 26;\n }\n public set underlineStyle(value: UnderlineStyle) {\n this._ext &= ~ExtFlags.UNDERLINE_STYLE;\n this._ext |= (value << 26) & ExtFlags.UNDERLINE_STYLE;\n }\n\n public get underlineColor(): number {\n return this._ext & (Attributes.CM_MASK | Attributes.RGB_MASK);\n }\n public set underlineColor(value: number) {\n this._ext &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);\n this._ext |= value & (Attributes.CM_MASK | Attributes.RGB_MASK);\n }\n\n private _urlId: number = 0;\n public get urlId(): number {\n return this._urlId;\n }\n public set urlId(value: number) {\n this._urlId = value;\n }\n\n public get underlineVariantOffset(): number {\n const val = (this._ext & ExtFlags.VARIANT_OFFSET) >> 29;\n if (val < 0) {\n return val ^ 0xFFFFFFF8;\n }\n return val;\n }\n public set underlineVariantOffset(value: number) {\n this._ext &= ~ExtFlags.VARIANT_OFFSET;\n this._ext |= (value << 29) & ExtFlags.VARIANT_OFFSET;\n }\n\n constructor(\n ext: number = 0,\n urlId: number = 0\n ) {\n this._ext = ext;\n this._urlId = urlId;\n }\n\n public clone(): IExtendedAttrs {\n return new ExtendedAttrs(this._ext, this._urlId);\n }\n\n /**\n * Convenient method to indicate whether the object holds no additional information,\n * that needs to be persistant in the buffer.\n */\n public isEmpty(): boolean {\n return this.underlineStyle === UnderlineStyle.NONE && this._urlId === 0;\n }\n}\n", "/**\n * Copyright (c) 2018 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { CharData, ICellData, IExtendedAttrs } from './Types';\nimport { stringFromCodePoint } from '../input/TextDecoder';\nimport { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_ATTR_INDEX, Content } from './Constants';\nimport { AttributeData, ExtendedAttrs } from './AttributeData';\nimport type { IBufferCell as IBufferCellApi } from '@xterm/xterm';\n\n/**\n * CellData - represents a single Cell in the terminal buffer.\n */\nexport class CellData extends AttributeData implements ICellData {\n /** Helper to create CellData from CharData. */\n public static fromCharData(value: CharData): CellData {\n const obj = new CellData();\n obj.setFromCharData(value);\n return obj;\n }\n /** Primitives from terminal buffer. */\n public content = 0;\n public fg = 0;\n public bg = 0;\n public extended: IExtendedAttrs = new ExtendedAttrs();\n public combinedData = '';\n /** Whether cell contains a combined string. */\n public isCombined(): number {\n return this.content & Content.IS_COMBINED_MASK;\n }\n /** Width of the cell. */\n public getWidth(): number {\n return this.content >> Content.WIDTH_SHIFT;\n }\n /** JS string of the content. */\n public getChars(): string {\n if (this.content & Content.IS_COMBINED_MASK) {\n return this.combinedData;\n }\n if (this.content & Content.CODEPOINT_MASK) {\n return stringFromCodePoint(this.content & Content.CODEPOINT_MASK);\n }\n return '';\n }\n /**\n * Codepoint of cell\n * Note this returns the UTF32 codepoint of single chars,\n * if content is a combined string it returns the codepoint\n * of the last char in string to be in line with code in CharData.\n */\n public getCode(): number {\n return (this.isCombined())\n ? this.combinedData.charCodeAt(this.combinedData.length - 1)\n : this.content & Content.CODEPOINT_MASK;\n }\n /** Set data from CharData */\n public setFromCharData(value: CharData): void {\n this.fg = value[CHAR_DATA_ATTR_INDEX];\n this.bg = 0;\n let combined = false;\n // surrogates and combined strings need special treatment\n if (value[CHAR_DATA_CHAR_INDEX].length > 2) {\n combined = true;\n }\n else if (value[CHAR_DATA_CHAR_INDEX].length === 2) {\n const code = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0);\n // if the 2-char string is a surrogate create single codepoint\n // everything else is combined\n if (0xD800 <= code && code <= 0xDBFF) {\n const second = value[CHAR_DATA_CHAR_INDEX].charCodeAt(1);\n if (0xDC00 <= second && second <= 0xDFFF) {\n this.content = ((code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);\n }\n else {\n combined = true;\n }\n }\n else {\n combined = true;\n }\n }\n else {\n this.content = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);\n }\n if (combined) {\n this.combinedData = value[CHAR_DATA_CHAR_INDEX];\n this.content = Content.IS_COMBINED_MASK | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);\n }\n }\n /** Get data as CharData. */\n public getAsCharData(): CharData {\n return [this.fg, this.getChars(), this.getWidth(), this.getCode()];\n }\n\n public attributesEquals(other: IBufferCellApi): boolean {\n if (this.getFgColorMode() !== other.getFgColorMode() || this.getFgColor() !== other.getFgColor()) {\n return false;\n }\n if (this.getBgColorMode() !== other.getBgColorMode() || this.getBgColor() !== other.getBgColor()) {\n return false;\n }\n if (this.isInverse() !== other.isInverse()) {\n return false;\n }\n if (this.isBold() !== other.isBold()) {\n return false;\n }\n if (this.isUnderline() !== other.isUnderline()) {\n return false;\n }\n if (this.isUnderline()) {\n if (this.getUnderlineStyle() !== other.getUnderlineStyle()) {\n return false;\n }\n const thisDefault = this.isUnderlineColorDefault();\n const otherDefault = other.isUnderlineColorDefault();\n if (!(thisDefault && otherDefault)) {\n if (thisDefault !== otherDefault) {\n return false;\n }\n if (this.getUnderlineColor() !== other.getUnderlineColor()) {\n return false;\n }\n if (this.getUnderlineColorMode() !== other.getUnderlineColorMode()) {\n return false;\n }\n }\n }\n if (this.isOverline() !== other.isOverline()) {\n return false;\n }\n if (this.isBlink() !== other.isBlink()) {\n return false;\n }\n if (this.isInvisible() !== other.isInvisible()) {\n return false;\n }\n if (this.isItalic() !== other.isItalic()) {\n return false;\n }\n if (this.isDim() !== other.isDim()) {\n return false;\n }\n if (this.isStrikethrough() !== other.isStrikethrough()) {\n return false;\n }\n return true;\n }\n\n}\n", "/**\n * Copyright (c) 2021 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { CellData } from '../buffer/CellData';\nimport { IBufferLine, ICellData } from '../buffer/Types';\nimport { IBufferCell as IBufferCellApi, IBufferLine as IBufferLineApi } from '@xterm/xterm';\n\nexport class BufferLineApiView implements IBufferLineApi {\n constructor(private _line: IBufferLine) { }\n\n public get isWrapped(): boolean { return this._line.isWrapped; }\n public get length(): number { return this._line.length; }\n public getCell(x: number, cell?: IBufferCellApi): IBufferCellApi | undefined {\n if (x < 0 || x >= this._line.length) {\n return undefined;\n }\n\n if (cell) {\n this._line.loadCell(x, cell as unknown as ICellData);\n return cell;\n }\n return this._line.loadCell(x, new CellData()) as unknown as IBufferCellApi;\n }\n public translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): string {\n return this._line.translateToString(trimRight, startColumn, endColumn);\n }\n}\n", "/**\n * Copyright (c) 2021 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { IBuffer as IBufferApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi } from '@xterm/xterm';\nimport { IBuffer } from '../buffer/Types';\nimport { BufferLineApiView } from './BufferLineApiView';\nimport { CellData } from '../buffer/CellData';\n\nexport class BufferApiView implements IBufferApi {\n constructor(\n private _buffer: IBuffer,\n public readonly type: 'normal' | 'alternate'\n ) { }\n\n public init(buffer: IBuffer): BufferApiView {\n this._buffer = buffer;\n return this;\n }\n\n public get cursorY(): number { return this._buffer.y; }\n public get cursorX(): number { return this._buffer.x; }\n public get viewportY(): number { return this._buffer.ydisp; }\n public get baseY(): number { return this._buffer.ybase; }\n public get length(): number { return this._buffer.lines.length; }\n public getLine(y: number): IBufferLineApi | undefined {\n const line = this._buffer.lines.get(y);\n if (!line) {\n return undefined;\n }\n return new BufferLineApiView(line);\n }\n public getNullCell(): IBufferCellApi { return new CellData(); }\n}\n", "/**\n * Copyright (c) 2024-2026 The xterm.js authors. All rights reserved.\n * @license MIT\n *\n * Minimal lifecycle utilities for xterm.js core.\n * Simplified from VS Code's lifecycle.ts - no tracking/leak detection.\n */\n\nexport interface IDisposable {\n dispose(): void;\n}\n\nexport function toDisposable(fn: () => void): IDisposable {\n return { dispose: fn };\n}\n\nexport function dispose(disposable: T): T;\nexport function dispose(disposable: T | undefined): T | undefined;\nexport function dispose(disposables: T[]): T[];\nexport function dispose(arg: T | T[] | undefined): T | T[] | undefined {\n if (!arg) {\n return arg;\n }\n if (Array.isArray(arg)) {\n for (const d of arg) {\n d.dispose();\n }\n return [];\n }\n arg.dispose();\n return arg;\n}\n\nexport function combinedDisposable(...disposables: IDisposable[]): IDisposable {\n return toDisposable(() => dispose(disposables));\n}\n\nexport class DisposableStore implements IDisposable {\n private readonly _disposables = new Set();\n private _isDisposed = false;\n\n public get isDisposed(): boolean {\n return this._isDisposed;\n }\n\n public add(o: T): T {\n if (this._isDisposed) {\n o.dispose();\n } else {\n this._disposables.add(o);\n }\n return o;\n }\n\n public dispose(): void {\n if (this._isDisposed) {\n return;\n }\n this._isDisposed = true;\n for (const d of this._disposables) {\n d.dispose();\n }\n this._disposables.clear();\n }\n\n public clear(): void {\n for (const d of this._disposables) {\n d.dispose();\n }\n this._disposables.clear();\n }\n}\n\nexport abstract class Disposable implements IDisposable {\n public static readonly None: IDisposable = Object.freeze({ dispose() { } });\n\n protected readonly _store = new DisposableStore();\n\n public dispose(): void {\n this._store.dispose();\n }\n\n protected _register(o: T): T {\n return this._store.add(o);\n }\n}\n\nexport class MutableDisposable implements IDisposable {\n private _value: T | undefined;\n private _isDisposed = false;\n\n public get value(): T | undefined {\n return this._isDisposed ? undefined : this._value;\n }\n\n public set value(value: T | undefined) {\n if (this._isDisposed || value === this._value) {\n return;\n }\n this._value?.dispose();\n this._value = value;\n }\n\n public clear(): void {\n this.value = undefined;\n }\n\n public dispose(): void {\n this._isDisposed = true;\n this._value?.dispose();\n this._value = undefined;\n }\n}\n", "/**\n * Copyright (c) 2024-2026 The xterm.js authors. All rights reserved.\n * @license MIT\n *\n * Minimal event utilities for xterm.js core.\n * Simplified from VS Code's event.ts - no leak detection/profiling.\n */\n\nimport { IDisposable, DisposableStore, toDisposable } from './Lifecycle';\n\nexport interface IEvent {\n (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore): IDisposable;\n}\n\nexport class Emitter {\n private _listeners: { fn: (e: T) => any, thisArgs: any }[] = [];\n private _disposed = false;\n private _event: IEvent | undefined;\n\n public get event(): IEvent {\n if (this._event) {\n return this._event;\n }\n this._event = (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => {\n if (this._disposed) {\n return toDisposable(() => {});\n }\n\n const entry = { fn: listener, thisArgs };\n this._listeners = this._listeners.slice();\n this._listeners.push(entry);\n\n const result = toDisposable(() => {\n const idx = this._listeners.indexOf(entry);\n if (idx !== -1) {\n this._listeners = this._listeners.slice();\n this._listeners.splice(idx, 1);\n }\n });\n\n if (disposables) {\n if (Array.isArray(disposables)) {\n disposables.push(result);\n } else {\n disposables.add(result);\n }\n }\n\n return result;\n };\n return this._event;\n }\n\n public fire(event: T): void {\n if (this._disposed || !this._listeners.length) {\n return;\n }\n if (this._listeners.length === 1) {\n this._listeners[0].fn.call(this._listeners[0].thisArgs, event);\n return;\n }\n const listeners = this._listeners;\n for (let i = 0, len = listeners.length; i < len; ++i) {\n listeners[i].fn.call(listeners[i].thisArgs, event);\n }\n }\n\n public dispose(): void {\n if (this._disposed) {\n return;\n }\n this._disposed = true;\n this._listeners.length = 0;\n }\n}\n\nexport namespace EventUtils {\n export function forward(from: IEvent, to: Emitter): IDisposable {\n return from(e => to.fire(e));\n }\n\n export function map(event: IEvent, map: (i: I) => O): IEvent {\n return (listener: (e: O) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => {\n return event(i => listener.call(thisArgs, map(i)), undefined, disposables);\n };\n }\n\n export function any(...events: IEvent[]): IEvent;\n export function any(...events: IEvent[]): IEvent;\n export function any(...events: IEvent[]): IEvent {\n return (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => {\n const store = new DisposableStore();\n for (const event of events) {\n store.add(event(e => listener.call(thisArgs, e)));\n }\n if (disposables) {\n if (Array.isArray(disposables)) {\n disposables.push(store);\n } else {\n disposables.add(store);\n }\n }\n return store;\n };\n }\n\n export function runAndSubscribe(event: IEvent, handler: (e: T) => void, initial: T): IDisposable;\n export function runAndSubscribe(event: IEvent, handler: (e: T | undefined) => void): IDisposable;\n export function runAndSubscribe(event: IEvent, handler: (e: T | undefined) => void, initial?: T): IDisposable {\n handler(initial);\n return event(e => handler(e));\n }\n}\n", "/**\n * Copyright (c) 2021 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { IBuffer as IBufferApi, IBufferNamespace as IBufferNamespaceApi } from '@xterm/xterm';\nimport { BufferApiView } from './BufferApiView';\nimport { ICoreTerminal } from '../CoreTerminal';\nimport { Disposable } from '../Lifecycle';\nimport { Emitter } from '../Event';\n\nexport class BufferNamespaceApi extends Disposable implements IBufferNamespaceApi {\n private _normal: BufferApiView;\n private _alternate: BufferApiView;\n\n private readonly _onBufferChange = this._register(new Emitter());\n public readonly onBufferChange = this._onBufferChange.event;\n\n constructor(private _core: ICoreTerminal) {\n super();\n this._normal = new BufferApiView(this._core.buffers.normal, 'normal');\n this._alternate = new BufferApiView(this._core.buffers.alt, 'alternate');\n this._register(this._core.buffers.onBufferActivate(() => this._onBufferChange.fire(this.active)));\n }\n public get active(): IBufferApi {\n if (this._core.buffers.active === this._core.buffers.normal) { return this.normal; }\n if (this._core.buffers.active === this._core.buffers.alt) { return this.alternate; }\n throw new Error('Active buffer is neither normal nor alternate');\n }\n public get normal(): IBufferApi {\n return this._normal.init(this._core.buffers.normal);\n }\n public get alternate(): IBufferApi {\n return this._alternate.init(this._core.buffers.alt);\n }\n}\n", "/**\n * Copyright (c) 2021 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { IParams } from '../parser/Types';\nimport { IDisposable, IFunctionIdentifier, IParser } from '@xterm/xterm';\nimport { ICoreTerminal } from '../CoreTerminal';\n\nexport class ParserApi implements IParser {\n constructor(private _core: ICoreTerminal) { }\n\n public registerCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean | Promise): IDisposable {\n return this._core.registerCsiHandler(id, (params: IParams) => callback(params.toArray()));\n }\n public addCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean | Promise): IDisposable {\n return this.registerCsiHandler(id, callback);\n }\n public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean | Promise): IDisposable {\n return this._core.registerDcsHandler(id, (data: string, params: IParams) => callback(data, params.toArray()));\n }\n public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean | Promise): IDisposable {\n return this.registerDcsHandler(id, callback);\n }\n public registerEscHandler(id: IFunctionIdentifier, handler: () => boolean | Promise): IDisposable {\n return this._core.registerEscHandler(id, handler);\n }\n public addEscHandler(id: IFunctionIdentifier, handler: () => boolean | Promise): IDisposable {\n return this.registerEscHandler(id, handler);\n }\n public registerOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable {\n return this._core.registerOscHandler(ident, callback);\n }\n public addOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable {\n return this.registerOscHandler(ident, callback);\n }\n public registerApcHandler(id: IFunctionIdentifier, callback: (data: string) => boolean | Promise): IDisposable {\n return this._core.registerApcHandler(id, callback);\n }\n}\n", "/**\n * Copyright (c) 2021 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { ICoreTerminal } from '../CoreTerminal';\nimport { IUnicodeHandling, IUnicodeVersionProvider } from '@xterm/xterm';\n\nexport class UnicodeApi implements IUnicodeHandling {\n constructor(private _core: ICoreTerminal) { }\n\n public register(provider: IUnicodeVersionProvider): void {\n this._core.unicodeService.register(provider);\n }\n\n public get versions(): string[] {\n return this._core.unicodeService.versions;\n }\n\n public get activeVersion(): string {\n return this._core.unicodeService.activeVersion;\n }\n\n public set activeVersion(version: string) {\n this._core.unicodeService.activeVersion = version;\n }\n}\n", "/**\n * Copyright (c) 2018 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { CharData, IAttributeData, IBufferLine, ICellData, IExtendedAttrs } from './Types';\nimport { AttributeData } from './AttributeData';\nimport { CellData } from './CellData';\nimport { Attributes, BgFlags, CHAR_DATA_ATTR_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, Content, NULL_CELL_CHAR, NULL_CELL_CODE, NULL_CELL_WIDTH, WHITESPACE_CELL_CHAR } from './Constants';\nimport { stringFromCodePoint } from '../input/TextDecoder';\n\n// Buffer memory layout:\n//\n// [0]: content `uint32_t` - wcwidth(2) comb(1) codepoint(21)\n// [1]: fg `uint32_t` - flags(8) r(8) g(8) b(8)\n// [2]: bg `uint32_t` - flags(8) r(8) g(8) b(8)\n\nconst enum Constants {\n /** The number of 32 bit array indices taken by one cell. */\n CELL_INDICIES = 3,\n /** Factor when to cleanup underlying array buffer after shrinking. */\n CLEANUP_THRESHOLD = 2\n}\n\n/**\n * Cell member indices.\n *\n * Direct access:\n * `content = data[column * Constants.CELL_INDICIES + Cell.CONTENT];`\n * `fg = data[column * Constants.CELL_INDICIES + Cell.FG];`\n * `bg = data[column * Constants.CELL_INDICIES + Cell.BG];`\n */\nconst enum Cell {\n CONTENT = 0,\n FG = 1, // currently simply holds all known attrs\n BG = 2 // currently unused\n}\n\n\ninterface IExtendedAttrsExt extends IExtendedAttrs {\n _ext: number;\n _urlId: number;\n}\n\n\nexport const DEFAULT_ATTR_DATA = Object.freeze(new AttributeData());\n\n// Work variables to avoid garbage collection\nlet $startIndex = 0;\nconst $workCell = new CellData();\nconst $extended = DEFAULT_ATTR_DATA.extended.clone() as IExtendedAttrsExt;\n\n\n/**\n * Typed array based bufferline implementation.\n *\n * There are 2 ways to insert data into the cell buffer:\n * - `setCellFromCodepoint` + `addCodepointToCell`\n * Use these for data that is already UTF32.\n * Used during normal input in `InputHandler` for faster buffer access.\n * - `setCell`\n * This method takes a CellData object and stores the data in the buffer.\n * Use `CellData.fromCharData` to create the CellData object (e.g. from JS string).\n *\n * To retrieve data from the buffer use either one of the primitive methods\n * (if only one particular value is needed) or `loadCell`. For `loadCell` in a loop\n * memory allocs / GC pressure can be greatly reduced by reusing the CellData object.\n */\nexport class BufferLine implements IBufferLine {\n protected _data: Uint32Array;\n /** Sparse cache; only read when `IS_COMBINED_MASK` is set in `_data`. */\n protected _combined: {[index: number]: string} = {};\n /** Sparse cache; only read when `HAS_EXTENDED` is set in `_data`. */\n protected _extendedAttrs: {[index: number]: IExtendedAttrs | undefined} = {};\n public length: number;\n\n /** line text cache */\n protected _cacheValid = false;\n protected _cache: string = '';\n protected _cacheTrimmed = false;\n\n constructor(\n cols: number,\n fillCellData?: ICellData,\n public isWrapped: boolean = false\n ) {\n this._data = new Uint32Array(cols * Constants.CELL_INDICIES);\n const cell = fillCellData ?? CellData.fromCharData([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);\n for (let i = 0; i < cols; ++i) {\n this.setCell(i, cell);\n }\n this.length = cols;\n }\n\n /**\n * Get cell data CharData.\n * @deprecated\n */\n public get(index: number): CharData {\n const content = this._data[index * Constants.CELL_INDICIES + Cell.CONTENT];\n const cp = content & Content.CODEPOINT_MASK;\n return [\n this._data[index * Constants.CELL_INDICIES + Cell.FG],\n (content & Content.IS_COMBINED_MASK)\n ? this._combined[index]\n : (cp) ? stringFromCodePoint(cp) : '',\n content >> Content.WIDTH_SHIFT,\n (content & Content.IS_COMBINED_MASK)\n ? this._combined[index].charCodeAt(this._combined[index].length - 1)\n : cp\n ];\n }\n\n /**\n * Set cell data from CharData.\n * @deprecated\n */\n public set(index: number, value: CharData): void {\n this._cacheValid = false;\n this._data[index * Constants.CELL_INDICIES + Cell.FG] = value[CHAR_DATA_ATTR_INDEX];\n if (value[CHAR_DATA_CHAR_INDEX].length > 1) {\n this._combined[index] = value[1];\n this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = index | Content.IS_COMBINED_MASK | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);\n } else {\n this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);\n }\n }\n\n /**\n * primitive getters\n * use these when only one value is needed, otherwise use `loadCell`\n */\n public getWidth(index: number): number {\n return this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] >> Content.WIDTH_SHIFT;\n }\n\n /** Test whether content has width. */\n public hasWidth(index: number): number {\n return this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] & Content.WIDTH_MASK;\n }\n\n /** Get FG cell component. */\n public getFg(index: number): number {\n return this._data[index * Constants.CELL_INDICIES + Cell.FG];\n }\n\n /** Get BG cell component. */\n public getBg(index: number): number {\n return this._data[index * Constants.CELL_INDICIES + Cell.BG];\n }\n\n /**\n * Test whether contains any chars.\n * Basically an empty has no content, but other cells might differ in FG/BG\n * from real empty cells.\n */\n public hasContent(index: number): number {\n return this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] & Content.HAS_CONTENT_MASK;\n }\n\n /**\n * Get codepoint of the cell.\n * To be in line with `code` in CharData this either returns\n * a single UTF32 codepoint or the last codepoint of a combined string.\n */\n public getCodePoint(index: number): number {\n const content = this._data[index * Constants.CELL_INDICIES + Cell.CONTENT];\n if (content & Content.IS_COMBINED_MASK) {\n return this._combined[index].charCodeAt(this._combined[index].length - 1);\n }\n return content & Content.CODEPOINT_MASK;\n }\n\n /** Test whether the cell contains a combined string. */\n public isCombined(index: number): number {\n return this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] & Content.IS_COMBINED_MASK;\n }\n\n /** Returns the string content of the cell. */\n public getString(index: number): string {\n const content = this._data[index * Constants.CELL_INDICIES + Cell.CONTENT];\n if (content & Content.IS_COMBINED_MASK) {\n return this._combined[index];\n }\n if (content & Content.CODEPOINT_MASK) {\n return stringFromCodePoint(content & Content.CODEPOINT_MASK);\n }\n // return empty string for empty cells\n return '';\n }\n\n /** Get state of protected flag. */\n public isProtected(index: number): number {\n return this._data[index * Constants.CELL_INDICIES + Cell.BG] & BgFlags.PROTECTED;\n }\n\n /**\n * Load data at `index` into `cell`. This is used to access cells in a way that's more friendly\n * to GC as it significantly reduced the amount of new objects/references needed.\n */\n public loadCell(index: number, cell: ICellData): ICellData {\n $startIndex = index * Constants.CELL_INDICIES;\n cell.content = this._data[$startIndex + Cell.CONTENT];\n cell.fg = this._data[$startIndex + Cell.FG];\n cell.bg = this._data[$startIndex + Cell.BG];\n if (cell.content & Content.IS_COMBINED_MASK) {\n cell.combinedData = this._combined[index];\n } else {\n cell.combinedData = '';\n }\n if (cell.bg & BgFlags.HAS_EXTENDED) {\n cell.extended = this._extendedAttrs[index]!;\n } else {\n // Do not mutate cell.extended in place: it may still reference this line's map entry from a\n // prior loadCell into a reused CellData (e.g. $workCell during insert/delete).\n // We use $extended as blueprint and reset the internals\n // mimicking the ctor to avoid a new allocation.\n $extended._ext = 0;\n $extended._urlId = 0;\n cell.extended = $extended;\n }\n return cell;\n }\n\n /**\n * Set data at `index` to `cell`.\n */\n public setCell(index: number, cell: ICellData): void {\n this._cacheValid = false;\n if (cell.content & Content.IS_COMBINED_MASK) {\n this._combined[index] = cell.combinedData;\n }\n if (cell.bg & BgFlags.HAS_EXTENDED) {\n this._extendedAttrs[index] = cell.extended;\n }\n this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = cell.content;\n this._data[index * Constants.CELL_INDICIES + Cell.FG] = cell.fg;\n this._data[index * Constants.CELL_INDICIES + Cell.BG] = cell.bg;\n }\n\n /**\n * Set cell data from input handler.\n * Since the input handler see the incoming chars as UTF32 codepoints,\n * it gets an optimized access method.\n */\n public setCellFromCodepoint(index: number, codePoint: number, width: number, attrs: IAttributeData): void {\n this._cacheValid = false;\n if (attrs.bg & BgFlags.HAS_EXTENDED) {\n this._extendedAttrs[index] = attrs.extended;\n }\n const $idx = index * Constants.CELL_INDICIES;\n this._data[$idx + Cell.CONTENT] = codePoint | (width << Content.WIDTH_SHIFT);\n this._data[$idx + Cell.FG] = attrs.fg;\n this._data[$idx + Cell.BG] = attrs.bg;\n }\n\n /**\n * Add a codepoint to a cell from input handler.\n * During input stage combining chars with a width of 0 follow and stack\n * onto a leading char. Since we already set the attrs\n * by the previous `setDataFromCodePoint` call, we can omit it here.\n */\n public addCodepointToCell(index: number, codePoint: number, width: number): void {\n this._cacheValid = false;\n let content = this._data[index * Constants.CELL_INDICIES + Cell.CONTENT];\n if (content & Content.IS_COMBINED_MASK) {\n // we already have a combined string, simply add\n this._combined[index] += stringFromCodePoint(codePoint);\n } else {\n if (content & Content.CODEPOINT_MASK) {\n // normal case for combining chars:\n // - move current leading char + new one into combined string\n // - set combined flag\n this._combined[index] = stringFromCodePoint(content & Content.CODEPOINT_MASK) + stringFromCodePoint(codePoint);\n content &= ~Content.CODEPOINT_MASK; // set codepoint in buffer to 0\n content |= Content.IS_COMBINED_MASK;\n } else {\n // should not happen - we actually have no data in the cell yet\n // simply set the data in the cell buffer with a width of 1\n content = codePoint | (1 << Content.WIDTH_SHIFT);\n }\n }\n if (width) {\n content &= ~Content.WIDTH_MASK;\n content |= width << Content.WIDTH_SHIFT;\n }\n this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = content;\n }\n\n public insertCells(pos: number, n: number, fillCellData: ICellData): void {\n this._cacheValid = false;\n pos %= this.length;\n\n // handle fullwidth at pos: reset cell one to the left if pos is second cell of a wide char\n if (pos && this.getWidth(pos - 1) === 2) {\n this.setCellFromCodepoint(pos - 1, 0, 1, fillCellData);\n }\n\n if (n < this.length - pos) {\n for (let i = this.length - pos - n - 1; i >= 0; --i) {\n this.setCell(pos + n + i, this.loadCell(pos + i, $workCell));\n }\n for (let i = 0; i < n; ++i) {\n this.setCell(pos + i, fillCellData);\n }\n } else {\n for (let i = pos; i < this.length; ++i) {\n this.setCell(i, fillCellData);\n }\n }\n\n // handle fullwidth at line end: reset last cell if it is first cell of a wide char\n if (this.getWidth(this.length - 1) === 2) {\n this.setCellFromCodepoint(this.length - 1, 0, 1, fillCellData);\n }\n }\n\n public deleteCells(pos: number, n: number, fillCellData: ICellData): void {\n this._cacheValid = false;\n pos %= this.length;\n if (n < this.length - pos) {\n for (let i = 0; i < this.length - pos - n; ++i) {\n this.setCell(pos + i, this.loadCell(pos + n + i, $workCell));\n }\n for (let i = this.length - n; i < this.length; ++i) {\n this.setCell(i, fillCellData);\n }\n } else {\n for (let i = pos; i < this.length; ++i) {\n this.setCell(i, fillCellData);\n }\n }\n\n // handle fullwidth at pos:\n // - reset pos-1 if wide char\n // - reset pos if width==0 (previous second cell of a wide char)\n if (pos && this.getWidth(pos - 1) === 2) {\n this.setCellFromCodepoint(pos - 1, 0, 1, fillCellData);\n }\n if (this.getWidth(pos) === 0 && !this.hasContent(pos)) {\n this.setCellFromCodepoint(pos, 0, 1, fillCellData);\n }\n }\n\n public replaceCells(start: number, end: number, fillCellData: ICellData, respectProtect: boolean = false): void {\n this._cacheValid = false;\n // full branching on respectProtect==true, hopefully getting fast JIT for standard case\n if (respectProtect) {\n if (start && this.getWidth(start - 1) === 2 && !this.isProtected(start - 1)) {\n this.setCellFromCodepoint(start - 1, 0, 1, fillCellData);\n }\n if (end < this.length && this.getWidth(end - 1) === 2 && !this.isProtected(end)) {\n this.setCellFromCodepoint(end, 0, 1, fillCellData);\n }\n while (start < end && start < this.length) {\n if (!this.isProtected(start)) {\n this.setCell(start, fillCellData);\n }\n start++;\n }\n return;\n }\n\n // handle fullwidth at start: reset cell one to the left if start is second cell of a wide char\n if (start && this.getWidth(start - 1) === 2) {\n this.setCellFromCodepoint(start - 1, 0, 1, fillCellData);\n }\n // handle fullwidth at last cell + 1: reset to empty cell if it is second part of a wide char\n if (end < this.length && this.getWidth(end - 1) === 2) {\n this.setCellFromCodepoint(end, 0, 1, fillCellData);\n }\n\n while (start < end && start < this.length) {\n this.setCell(start++, fillCellData);\n }\n }\n\n /**\n * Resize BufferLine to `cols` filling excess cells with `fillCellData`.\n * The underlying array buffer will not change if there is still enough space\n * to hold the new buffer line data.\n * Returns a boolean indicating, whether a `cleanupMemory` call would free\n * excess memory (true after shrinking > Constants.CLEANUP_THRESHOLD).\n */\n public resize(cols: number, fillCellData: ICellData): boolean {\n this._cacheValid = false;\n if (cols === this.length) {\n return this._data.length * 4 * Constants.CLEANUP_THRESHOLD < this._data.buffer.byteLength;\n }\n const uint32Cells = cols * Constants.CELL_INDICIES;\n if (cols > this.length) {\n if (this._data.buffer.byteLength >= uint32Cells * 4) {\n // optimization: avoid alloc and data copy if buffer has enough room\n this._data = new Uint32Array(this._data.buffer, 0, uint32Cells);\n } else {\n // slow path: new alloc and full data copy\n const data = new Uint32Array(uint32Cells);\n data.set(this._data);\n this._data = data;\n }\n for (let i = this.length; i < cols; ++i) {\n this.setCell(i, fillCellData);\n }\n } else {\n // optimization: just shrink the view on existing buffer\n this._data = this._data.subarray(0, uint32Cells);\n // Remove any cut off combined data\n const keys = Object.keys(this._combined);\n for (let i = 0; i < keys.length; i++) {\n const key = parseInt(keys[i], 10);\n if (key >= cols) {\n delete this._combined[key];\n }\n }\n // remove any cut off extended attributes\n const extKeys = Object.keys(this._extendedAttrs);\n for (let i = 0; i < extKeys.length; i++) {\n const key = parseInt(extKeys[i], 10);\n if (key >= cols) {\n delete this._extendedAttrs[key];\n }\n }\n }\n this.length = cols;\n return uint32Cells * 4 * Constants.CLEANUP_THRESHOLD < this._data.buffer.byteLength;\n }\n\n /**\n * Cleanup underlying array buffer.\n * A cleanup will be triggered if the array buffer exceeds the actual used\n * memory by a factor of Constants.CLEANUP_THRESHOLD.\n * Returns 0 or 1 indicating whether a cleanup happened.\n */\n public cleanupMemory(): number {\n if (this._data.length * 4 * Constants.CLEANUP_THRESHOLD < this._data.buffer.byteLength) {\n const data = new Uint32Array(this._data.length);\n data.set(this._data);\n this._data = data;\n return 1;\n }\n return 0;\n }\n\n /** fill a line with fillCharData */\n public fill(fillCellData: ICellData, respectProtect: boolean = false): void {\n this._cacheValid = false;\n // full branching on respectProtect==true, hopefully getting fast JIT for standard case\n if (respectProtect) {\n for (let i = 0; i < this.length; ++i) {\n if (!this.isProtected(i)) {\n this.setCell(i, fillCellData);\n }\n }\n return;\n }\n this._combined = {};\n this._extendedAttrs = {};\n for (let i = 0; i < this.length; ++i) {\n this.setCell(i, fillCellData);\n }\n }\n\n /** alter to a full copy of line */\n public copyFrom(line: BufferLine, blank?: boolean): void {\n if (this.length !== line.length) {\n this._data = new Uint32Array(line._data);\n } else {\n // use high speed copy if lengths are equal\n this._data.set(line._data);\n }\n this.length = line.length;\n if (blank) {\n // a blank line may never hold combined or extended attrs,\n // thus we can skip handling them\n this._combined = {};\n this._extendedAttrs = {};\n } else {\n this._copySparseMapsFrom(line);\n }\n this._cache = '';\n this._cacheValid = false;\n this.isWrapped = line.isWrapped;\n }\n\n /** create a new clone */\n public clone(blank?: boolean): IBufferLine {\n const newLine = new BufferLine(0, undefined, false);\n newLine._data = new Uint32Array(this._data);\n newLine.length = this.length;\n if (!blank) {\n // a blank line may never hold combined or extended attrs,\n // thus we can skip handling them\n newLine._copySparseMapsFrom(this);\n }\n newLine.isWrapped = this.isWrapped;\n return newLine;\n }\n\n public getTrimmedLength(): number {\n for (let i = this.length - 1; i >= 0; --i) {\n if ((this._data[i * Constants.CELL_INDICIES + Cell.CONTENT] & Content.HAS_CONTENT_MASK)) {\n return i + (this._data[i * Constants.CELL_INDICIES + Cell.CONTENT] >> Content.WIDTH_SHIFT);\n }\n }\n return 0;\n }\n\n public getNoBgTrimmedLength(): number {\n for (let i = this.length - 1; i >= 0; --i) {\n if ((this._data[i * Constants.CELL_INDICIES + Cell.CONTENT] & Content.HAS_CONTENT_MASK) || (this._data[i * Constants.CELL_INDICIES + Cell.BG] & Attributes.CM_MASK)) {\n return i + (this._data[i * Constants.CELL_INDICIES + Cell.CONTENT] >> Content.WIDTH_SHIFT);\n }\n }\n return 0;\n }\n\n public copyCellsFrom(src: BufferLine, srcCol: number, destCol: number, length: number, applyInReverse: boolean): void {\n this._cacheValid = false;\n const srcData = src._data;\n if (applyInReverse) {\n for (let cell = length - 1; cell >= 0; cell--) {\n for (let i = 0; i < Constants.CELL_INDICIES; i++) {\n this._data[(destCol + cell) * Constants.CELL_INDICIES + i] = srcData[(srcCol + cell) * Constants.CELL_INDICIES + i];\n }\n this._copyCellMapsFrom(src, srcCol + cell, destCol + cell);\n }\n } else {\n for (let cell = 0; cell < length; cell++) {\n for (let i = 0; i < Constants.CELL_INDICIES; i++) {\n this._data[(destCol + cell) * Constants.CELL_INDICIES + i] = srcData[(srcCol + cell) * Constants.CELL_INDICIES + i];\n }\n this._copyCellMapsFrom(src, srcCol + cell, destCol + cell);\n }\n }\n }\n\n /**\n * Translates the buffer line to a string. Caching only applies to canonical full-line translation\n * requests (regardless of `trimRight` value).\n *\n * @param trimRight Whether to trim any empty cells on the right.\n * @param startCol The column to start the string (0-based inclusive).\n * @param endCol The column to end the string (0-based exclusive).\n * @param outColumns if specified, this array will be filled with column numbers such that\n * `returnedString[i]` is displayed at `outColumns[i]` column. `outColumns[returnedString.length]`\n * is where the character following `returnedString` will be displayed.\n *\n * When a single cell is translated to multiple UTF-16 code units (e.g. surrogate pair) in the\n * returned string, the corresponding entries in `outColumns` will have the same column number.\n */\n public translateToString(trimRight?: boolean, startCol?: number, endCol?: number, outColumns?: number[]): string {\n const isCanonical = (startCol === undefined || startCol === 0) && endCol === undefined && outColumns === undefined;\n if (isCanonical && this._cacheValid) {\n if (trimRight) {\n return this._cacheTrimmed ? this._cache : this._cache.trimEnd();\n }\n if (!this._cacheTrimmed) {\n return this._cache;\n }\n }\n startCol = startCol ?? 0;\n endCol = endCol ?? this.length;\n if (trimRight) {\n endCol = Math.min(endCol, this.getTrimmedLength());\n }\n if (outColumns) {\n outColumns.length = 0;\n }\n const cellContents: string[] = [];\n while (startCol < endCol) {\n const content = this._data[startCol * Constants.CELL_INDICIES + Cell.CONTENT];\n const cp = content & Content.CODEPOINT_MASK;\n const chars = (content & Content.IS_COMBINED_MASK) ? this._combined[startCol] : (cp) ? stringFromCodePoint(cp) : WHITESPACE_CELL_CHAR;\n cellContents.push(chars);\n if (outColumns) {\n for (let i = 0; i < chars.length; ++i) {\n outColumns.push(startCol);\n }\n }\n startCol += (content >> Content.WIDTH_SHIFT) || 1; // always advance by at least 1\n }\n if (outColumns) {\n outColumns.push(startCol);\n }\n const result = cellContents.join('');\n if (isCanonical) {\n this._cache = result;\n this._cacheValid = true;\n this._cacheTrimmed = !!trimRight;\n }\n return result;\n }\n\n /** Copy sparse map entries for a single cell when `_data` flags require them. */\n private _copyCellMapsFrom(src: BufferLine, srcCol: number, destCol: number): void {\n const srcStart = srcCol * Constants.CELL_INDICIES;\n if (src._data[srcStart + Cell.CONTENT] & Content.IS_COMBINED_MASK) {\n this._combined[destCol] = src._combined[srcCol];\n }\n if (src._data[srcStart + Cell.BG] & BgFlags.HAS_EXTENDED) {\n this._extendedAttrs[destCol] = src._extendedAttrs[srcCol];\n }\n }\n\n /** Rebuild sparse maps from another line, keyed only by `_data` flags. */\n private _copySparseMapsFrom(line: BufferLine): void {\n this._combined = {};\n this._extendedAttrs = {};\n for (let i = 0; i < line.length; i++) {\n this._copyCellMapsFrom(line, i, i);\n }\n }\n}\n", "/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n *\n * This was heavily inspired from microsoft/vscode's dependency injection system (MIT).\n */\n/*---------------------------------------------------------------------------------------------\n * Copyright (c) Microsoft Corporation. All rights reserved.\n * Licensed under the MIT License. See License.txt in the project root for license information.\n *--------------------------------------------------------------------------------------------*/\n\nexport interface IServiceIdentifier {\n (...args: any[]): void;\n type: T;\n _id: string;\n}\n\nconst enum Constants {\n DI_TARGET = 'di$target',\n DI_DEPENDENCIES = 'di$dependencies'\n}\n\nexport const serviceRegistry: Map> = new Map();\n\nexport function getServiceDependencies(ctor: any): { id: IServiceIdentifier, index: number, optional: boolean }[] {\n return ctor[Constants.DI_DEPENDENCIES] || [];\n}\n\nexport function createDecorator(id: string): IServiceIdentifier {\n if (serviceRegistry.has(id)) {\n return serviceRegistry.get(id)!;\n }\n\n const decorator: any = function (target: Function, key: string, index: number): any {\n if (arguments.length !== 3) {\n throw new Error('@IServiceName-decorator can only be used to decorate a parameter');\n }\n\n storeServiceDependency(decorator, target, index);\n };\n\n decorator._id = id;\n\n serviceRegistry.set(id, decorator);\n return decorator;\n}\n\nfunction storeServiceDependency(id: Function, target: Function, index: number): void {\n if ((target as any)[Constants.DI_TARGET] === target) {\n (target as any)[Constants.DI_DEPENDENCIES].push({ id, index });\n } else {\n (target as any)[Constants.DI_DEPENDENCIES] = [{ id, index }];\n (target as any)[Constants.DI_TARGET] = target;\n }\n}\n", "/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport type { IDecoration, IDecorationOptions, ILinkHandler, ILogger, IWindowsPty, IOverviewRulerOptions } from '@xterm/xterm';\nimport { CoreMouseEncoding, CoreMouseEventType, CursorInactiveStyle, CursorStyle, ICharset, IColor, ICoreMouseEvent, ICoreMouseProtocol, IDecPrivateModes, IDisposable, IKittyKeyboardState, IModes, IOscLinkData, IWindowOptions } from '../Types';\nimport { IAttributeData, IBuffer, IBufferSet } from '../buffer/Types';\nimport { createDecorator, IServiceIdentifier } from './ServiceRegistry';\nimport type { Emitter, IEvent } from '../Event';\n\nexport const IBufferService = createDecorator('BufferService');\nexport interface IBufferService {\n serviceBrand: undefined;\n\n readonly cols: number;\n readonly rows: number;\n readonly buffer: IBuffer;\n readonly buffers: IBufferSet;\n isUserScrolling: boolean;\n onResize: IEvent;\n onScroll: IEvent;\n scroll(eraseAttr: IAttributeData, isWrapped?: boolean): void;\n scrollLines(disp: number, suppressScrollEvent?: boolean): void;\n resize(cols: number, rows: number): void;\n reset(): void;\n}\n\nexport interface IBufferResizeEvent {\n cols: number;\n rows: number;\n colsChanged: boolean;\n rowsChanged: boolean;\n}\n\nexport const IMouseStateService = createDecorator('MouseStateService');\nexport interface IMouseStateService {\n serviceBrand: undefined;\n\n activeProtocol: string;\n activeEncoding: string;\n areMouseEventsActive: boolean;\n addProtocol(name: string, protocol: ICoreMouseProtocol): void;\n addEncoding(name: string, encoding: CoreMouseEncoding): void;\n reset(): void;\n setCustomWheelEventHandler(customWheelEventHandler: ((event: WheelEvent) => boolean) | undefined): void;\n allowCustomWheelEvent(ev: WheelEvent): boolean;\n\n /**\n * Event to announce changes in mouse tracking.\n */\n onProtocolChange: IEvent;\n restrictMouseEvent(event: ICoreMouseEvent): boolean;\n encodeMouseEvent(event: ICoreMouseEvent): string;\n readonly isDefaultEncoding: boolean;\n readonly isPixelEncoding: boolean;\n}\n\nexport const ICoreService = createDecorator('CoreService');\nexport interface ICoreService {\n serviceBrand: undefined;\n\n /**\n * Initially the cursor will not be visible until the first time the terminal\n * is focused.\n */\n isCursorInitialized: boolean;\n isCursorHidden: boolean;\n\n readonly modes: IModes;\n readonly decPrivateModes: IDecPrivateModes;\n readonly kittyKeyboard: IKittyKeyboardState;\n\n readonly onData: IEvent;\n readonly onUserInput: IEvent;\n readonly onBinary: IEvent;\n readonly onRequestScrollToBottom: IEvent;\n\n reset(): void;\n\n /**\n * Triggers the onData event in the public API.\n * @param data The data that is being emitted.\n * @param wasUserInput Whether the data originated from the user (as opposed to\n * resulting from parsing incoming data). When true this will also:\n * - Scroll to the bottom of the buffer if option scrollOnUserInput is true.\n * - Fire the `onUserInput` event (so selection can be cleared).\n */\n triggerDataEvent(data: string, wasUserInput?: boolean): void;\n\n /**\n * Triggers the onBinary event in the public API.\n * @param data The data that is being emitted.\n */\n triggerBinaryEvent(data: string): void;\n}\n\nexport const ICharsetService = createDecorator('CharsetService');\nexport interface ICharsetService {\n serviceBrand: undefined;\n\n charset: ICharset | undefined;\n readonly glevel: number;\n readonly charsets: (ICharset | undefined)[];\n\n reset(): void;\n\n /**\n * Set the G level of the terminal.\n * @param g\n */\n setgLevel(g: number): void;\n\n /**\n * Set the charset for the given G level of the terminal.\n * @param g\n * @param charset\n */\n setgCharset(g: number, charset: ICharset | undefined): void;\n}\n\nexport interface IBrandedService {\n serviceBrand: undefined;\n}\n\ntype GetLeadingNonServiceArgs = TArgs extends [] ? []\n : TArgs extends [...infer TFirst, infer TLast] ? TLast extends IBrandedService ? GetLeadingNonServiceArgs : TArgs\n : never;\n\nexport const IInstantiationService = createDecorator('InstantiationService');\nexport interface IInstantiationService {\n serviceBrand: undefined;\n\n setService(id: IServiceIdentifier, instance: T): void;\n getService(id: IServiceIdentifier): T | undefined;\n createInstance any, R extends InstanceType>(t: Ctor, ...args: GetLeadingNonServiceArgs>): R;\n}\n\nexport enum LogLevelEnum {\n TRACE = 0,\n DEBUG = 1,\n INFO = 2,\n WARN = 3,\n ERROR = 4,\n OFF = 5\n}\n\nexport const ILogService = createDecorator('LogService');\nexport interface ILogService {\n serviceBrand: undefined;\n\n readonly logLevel: LogLevelEnum;\n\n trace(message: any, ...optionalParams: any[]): void;\n debug(message: any, ...optionalParams: any[]): void;\n info(message: any, ...optionalParams: any[]): void;\n warn(message: any, ...optionalParams: any[]): void;\n error(message: any, ...optionalParams: any[]): void;\n}\n\nexport const IOptionsService = createDecorator('OptionsService');\nexport interface IOptionsService {\n serviceBrand: undefined;\n\n /**\n * Read only access to the raw options object, this is an internal-only fast path for accessing\n * single options without any validation as we trust TypeScript to enforce correct usage\n * internally.\n */\n readonly rawOptions: Required;\n\n /**\n * Options as exposed through the public API, this property uses getters and setters with\n * validation which makes it safer but slower. {@link rawOptions} should be used for pretty much\n * all internal usage for performance reasons.\n */\n readonly options: Required;\n\n /**\n * Adds an event listener for when any option changes.\n */\n readonly onOptionChange: IEvent;\n\n /**\n * Adds an event listener for when a specific option changes, this is a convenience method that is\n * preferred over {@link onOptionChange} when only a single option is being listened to.\n */\n // eslint-disable-next-line @typescript-eslint/naming-convention\n onSpecificOptionChange(key: T, listener: (arg1: Required[T]) => any): IDisposable;\n\n /**\n * Adds an event listener for when a set of specific options change, this is a convenience method\n * that is preferred over {@link onOptionChange} when multiple options are being listened to and\n * handled the same way.\n */\n // eslint-disable-next-line @typescript-eslint/naming-convention\n onMultipleOptionChange(keys: (keyof ITerminalOptions)[], listener: () => any): IDisposable;\n}\n\nexport type FontWeight = 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900' | number;\nexport type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'off';\n\nexport interface ITerminalOptions {\n allowProposedApi?: boolean;\n allowTransparency?: boolean;\n altClickMovesCursor?: boolean;\n cols?: number;\n convertEol?: boolean;\n cursorBlink?: boolean;\n blinkIntervalDuration?: number;\n cursorStyle?: CursorStyle;\n cursorWidth?: number;\n cursorInactiveStyle?: CursorInactiveStyle;\n disableStdin?: boolean;\n documentOverride?: any | null;\n drawBoldTextInBrightColors?: boolean;\n fastScrollSensitivity?: number;\n fontSize?: number;\n fontFamily?: string;\n fontWeight?: FontWeight;\n fontWeightBold?: FontWeight;\n ignoreBracketedPasteMode?: boolean;\n letterSpacing?: number;\n lineHeight?: number;\n linkHandler?: ILinkHandler | null;\n logLevel?: LogLevel;\n logger?: ILogger | null;\n macOptionIsMeta?: boolean;\n macOptionClickForcesSelection?: boolean;\n minimumContrastRatio?: number;\n mouseEventsRequireAlt?: boolean;\n reflowCursorLine?: boolean;\n rescaleOverlappingGlyphs?: boolean;\n rightClickSelectsWord?: boolean;\n rows?: number;\n showCursorImmediately?: boolean;\n screenReaderMode?: boolean;\n scrollback?: number;\n scrollOnUserInput?: boolean;\n scrollSensitivity?: number;\n smoothScrollDuration?: number;\n tabStopWidth?: number;\n theme?: ITheme;\n windowsPty?: IWindowsPty;\n windowOptions?: IWindowOptions;\n wordSeparator?: string;\n quirks?: ITerminalQuirks;\n scrollbar?: IScrollbarOptions;\n scrollOnEraseInDisplay?: boolean;\n vtExtensions?: IVtExtensions;\n\n [key: string]: any;\n termName: string;\n}\n\nexport interface ITheme {\n foreground?: string;\n background?: string;\n cursor?: string;\n cursorAccent?: string;\n selectionForeground?: string;\n selectionBackground?: string;\n selectionInactiveBackground?: string;\n scrollbarSliderBackground?: string;\n scrollbarSliderHoverBackground?: string;\n scrollbarSliderActiveBackground?: string;\n overviewRulerBorder?: string;\n black?: string;\n red?: string;\n green?: string;\n yellow?: string;\n blue?: string;\n magenta?: string;\n cyan?: string;\n white?: string;\n brightBlack?: string;\n brightRed?: string;\n brightGreen?: string;\n brightYellow?: string;\n brightBlue?: string;\n brightMagenta?: string;\n brightCyan?: string;\n brightWhite?: string;\n extendedAnsi?: string[];\n}\n\nexport interface ITerminalQuirks {\n allowSetCursorBlink?: boolean;\n}\n\nexport interface IScrollbarOptions {\n showScrollbar?: boolean;\n showArrows?: boolean;\n width?: number;\n overviewRuler?: IOverviewRulerOptions;\n}\n\nexport interface IVtExtensions {\n kittyKeyboard?: boolean;\n kittySgrBoldFaintControl?: boolean;\n win32InputMode?: boolean;\n colorSchemeQuery?: boolean;\n}\n\nexport const IOscLinkService = createDecorator('OscLinkService');\nexport interface IOscLinkService {\n serviceBrand: undefined;\n /**\n * Registers a link to the service, returning the link ID. The link data is managed by this\n * service and will be freed when this current cursor position is trimmed off the buffer.\n */\n registerLink(linkData: IOscLinkData): number;\n /**\n * Adds a line to a link if needed.\n */\n addLineToLink(linkId: number, y: number): void;\n /** Get the link data associated with a link ID. */\n getLinkData(linkId: number): IOscLinkData | undefined;\n}\n\n/*\n * Width and Grapheme_Cluster_Break properties of a character as a bit mask.\n *\n * bit 0: shouldJoin - should combine with preceding character.\n * bit 1..2: wcwidth - see UnicodeCharWidth.\n * bit 3..31: class of character (currently only 4 bits are used).\n * This is used to determined grapheme clustering - i.e. which codepoints\n * are to be combined into a single compound character.\n *\n * Use the UnicodeService static function createPropertyValue to create a\n * UnicodeCharProperties; use extractShouldJoin, extractWidth, and\n * extractCharKind to extract the components.\n */\nexport type UnicodeCharProperties = number;\n\n/**\n * Width in columns of a character.\n * In a CJK context, \"half-width\" characters (such as Latin) are width 1,\n * while \"full-width\" characters (such as Kanji) are 2 columns wide.\n * Combining characters (such as accents) are width 0.\n */\nexport type UnicodeCharWidth = 0 | 1 | 2;\n\nexport const IUnicodeService = createDecorator('UnicodeService');\nexport interface IUnicodeService {\n serviceBrand: undefined;\n /** Register a Unicode version provider. */\n register(provider: IUnicodeVersionProvider): void;\n /** Registered Unicode versions. */\n readonly versions: string[];\n /** Currently active version. */\n activeVersion: string;\n /** Event triggered when the active version changes. */\n readonly onChange: IEvent;\n\n /**\n * Unicode version dependent\n */\n wcwidth(codepoint: number): UnicodeCharWidth;\n getStringCellWidth(s: string): number;\n /**\n * Return character width and type for grapheme clustering.\n * If preceding != 0, it is the return code from the previous character;\n * in that case the result specifies if the characters should be joined.\n */\n charProperties(codepoint: number, preceding: UnicodeCharProperties): UnicodeCharProperties;\n}\n\nexport interface IUnicodeVersionProvider {\n readonly version: string;\n wcwidth(ucs: number): UnicodeCharWidth;\n charProperties(codepoint: number, preceding: UnicodeCharProperties): UnicodeCharProperties;\n}\n\nexport const IDecorationService = createDecorator('DecorationService');\nexport interface IDecorationService extends IDisposable {\n serviceBrand: undefined;\n readonly decorations: IterableIterator;\n readonly onDecorationRegistered: IEvent;\n readonly onDecorationRemoved: IEvent;\n registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined;\n reset(): void;\n /**\n * Trigger a callback over the decoration at a cell (in no particular order). This uses a callback\n * instead of an iterator as it's typically used in hot code paths.\n */\n forEachDecorationAtCell(x: number, line: number, layer: 'bottom' | 'top' | undefined, callback: (decoration: IInternalDecoration) => void): void;\n}\nexport interface IInternalDecoration extends IDecoration {\n readonly options: IDecorationOptions;\n readonly backgroundColorRGB: IColor | undefined;\n readonly foregroundColorRGB: IColor | undefined;\n readonly onRenderEmitter: Emitter;\n /** @internal Start line for line-index removal; kept in sync on buffer line shifts. */\n _indexedStartLine: number;\n}\n", "/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n *\n * This was heavily inspired from microsoft/vscode's dependency injection system (MIT).\n */\n/*---------------------------------------------------------------------------------------------\n * Copyright (c) Microsoft Corporation. All rights reserved.\n * Licensed under the MIT License. See License.txt in the project root for license information.\n *--------------------------------------------------------------------------------------------*/\n\nimport { IInstantiationService } from './Services';\nimport { IServiceIdentifier, getServiceDependencies } from './ServiceRegistry';\n\nexport class ServiceCollection {\n\n private _entries = new Map, any>();\n\n constructor(...entries: [IServiceIdentifier, any][]) {\n for (const [id, service] of entries) {\n this.set(id, service);\n }\n }\n\n public set(id: IServiceIdentifier, instance: T): T {\n const result = this._entries.get(id);\n this._entries.set(id, instance);\n return result;\n }\n\n public forEach(callback: (id: IServiceIdentifier, instance: any) => any): void {\n for (const [key, value] of this._entries.entries()) {\n callback(key, value);\n }\n }\n\n public has(id: IServiceIdentifier): boolean {\n return this._entries.has(id);\n }\n\n public get(id: IServiceIdentifier): T | undefined {\n return this._entries.get(id);\n }\n}\n\nexport class InstantiationService implements IInstantiationService {\n public serviceBrand: undefined;\n\n private readonly _services: ServiceCollection = new ServiceCollection();\n\n constructor() {\n this._services.set(IInstantiationService, this);\n }\n\n public setService(id: IServiceIdentifier, instance: T): void {\n this._services.set(id, instance);\n }\n\n public getService(id: IServiceIdentifier): T | undefined {\n return this._services.get(id);\n }\n\n public createInstance(ctor: any, ...args: any[]): T {\n const serviceDependencies = getServiceDependencies(ctor).sort((a, b) => a.index - b.index);\n\n const serviceArgs: any[] = [];\n for (const dependency of serviceDependencies) {\n const service = this._services.get(dependency.id);\n if (!service) {\n throw new Error(`[createInstance] ${ctor.name} depends on UNKNOWN service ${dependency.id._id}.`);\n }\n serviceArgs.push(service);\n }\n\n const firstServiceArgPos = serviceDependencies.length > 0 ? serviceDependencies[0].index : args.length;\n\n // check for argument mismatches, adjust static args if needed\n if (args.length !== firstServiceArgPos) {\n throw new Error(`[createInstance] First service dependency of ${ctor.name} at position ${firstServiceArgPos + 1} conflicts with ${args.length} static arguments`);\n }\n\n // now create the instance\n return new ctor(...[...args, ...serviceArgs]);\n }\n}\n", "/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { Disposable } from '../Lifecycle';\nimport { ILogService, IOptionsService, LogLevelEnum } from './Services';\n\ntype LogType = (message?: any, ...optionalParams: any[]) => void;\n\ninterface IConsole {\n log: LogType;\n error: LogType;\n info: LogType;\n trace: LogType;\n warn: LogType;\n}\n\n// console is available on both node.js and browser contexts but the common\n// module doesn't depend on them so we need to explicitly declare it.\ndeclare const console: IConsole;\n\nconst optionsKeyToLogLevel: { [key: string]: LogLevelEnum } = {\n trace: LogLevelEnum.TRACE,\n debug: LogLevelEnum.DEBUG,\n info: LogLevelEnum.INFO,\n warn: LogLevelEnum.WARN,\n error: LogLevelEnum.ERROR,\n off: LogLevelEnum.OFF\n};\n\nconst LOG_PREFIX = 'xterm.js: ';\n\nexport class LogService extends Disposable implements ILogService {\n public serviceBrand: any;\n\n private _logLevel: LogLevelEnum = LogLevelEnum.OFF;\n public get logLevel(): LogLevelEnum { return this._logLevel; }\n\n constructor(\n @IOptionsService private readonly _optionsService: IOptionsService\n ) {\n super();\n this._updateLogLevel();\n this._register(this._optionsService.onSpecificOptionChange('logLevel', () => this._updateLogLevel()));\n }\n\n private _updateLogLevel(): void {\n this._logLevel = optionsKeyToLogLevel[this._optionsService.rawOptions.logLevel];\n }\n\n private _evalLazyOptionalParams(optionalParams: any[]): void {\n for (let i = 0; i < optionalParams.length; i++) {\n if (typeof optionalParams[i] === 'function') {\n optionalParams[i] = optionalParams[i]();\n }\n }\n }\n\n private _log(type: LogType, message: string, optionalParams: any[]): void {\n this._evalLazyOptionalParams(optionalParams);\n type.call(console, (this._optionsService.options.logger ? '' : LOG_PREFIX) + message, ...optionalParams);\n }\n\n public trace(message: string, ...optionalParams: any[]): void {\n if (this._logLevel <= LogLevelEnum.TRACE) {\n this._log(this._optionsService.options.logger?.trace.bind(this._optionsService.options.logger) ?? console.log, message, optionalParams);\n }\n }\n\n public debug(message: string, ...optionalParams: any[]): void {\n if (this._logLevel <= LogLevelEnum.DEBUG) {\n this._log(this._optionsService.options.logger?.debug.bind(this._optionsService.options.logger) ?? console.log, message, optionalParams);\n }\n }\n\n public info(message: string, ...optionalParams: any[]): void {\n if (this._logLevel <= LogLevelEnum.INFO) {\n this._log(this._optionsService.options.logger?.info.bind(this._optionsService.options.logger) ?? console.info, message, optionalParams);\n }\n }\n\n public warn(message: string, ...optionalParams: any[]): void {\n if (this._logLevel <= LogLevelEnum.WARN) {\n this._log(this._optionsService.options.logger?.warn.bind(this._optionsService.options.logger) ?? console.warn, message, optionalParams);\n }\n }\n\n public error(message: string, ...optionalParams: any[]): void {\n if (this._logLevel <= LogLevelEnum.ERROR) {\n this._log(this._optionsService.options.logger?.error.bind(this._optionsService.options.logger) ?? console.error, message, optionalParams);\n }\n }\n}\n", "/**\n * Copyright (c) 2016 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { Disposable } from './Lifecycle';\nimport { Emitter, type IEvent } from './Event';\n\nexport interface IInsertEvent {\n index: number;\n amount: number;\n}\n\nexport interface IDeleteEvent {\n index: number;\n amount: number;\n}\n\nexport interface ICircularList {\n length: number;\n maxLength: number;\n isFull: boolean;\n\n onDeleteEmitter: Emitter;\n onDelete: IEvent;\n onInsertEmitter: Emitter;\n onInsert: IEvent;\n onTrimEmitter: Emitter;\n onTrim: IEvent;\n\n get(index: number): T | undefined;\n set(index: number, value: T): void;\n push(value: T): void;\n recycle(): T;\n pop(): T | undefined;\n splice(start: number, deleteCount: number, ...items: T[]): void;\n trimStart(count: number): void;\n shiftElements(start: number, count: number, offset: number): void;\n}\n\n/**\n * Represents a circular list; a list with a maximum size that wraps around when push is called,\n * overriding values at the start of the list.\n */\nexport class CircularList extends Disposable implements ICircularList {\n protected _array: (T | undefined)[];\n private _startIndex: number;\n private _length: number;\n\n public readonly onDeleteEmitter = this._register(new Emitter());\n public readonly onDelete = this.onDeleteEmitter.event;\n public readonly onInsertEmitter = this._register(new Emitter());\n public readonly onInsert = this.onInsertEmitter.event;\n public readonly onTrimEmitter = this._register(new Emitter());\n public readonly onTrim = this.onTrimEmitter.event;\n\n constructor(\n private _maxLength: number\n ) {\n super();\n this._array = new Array(this._maxLength);\n this._startIndex = 0;\n this._length = 0;\n }\n\n public get maxLength(): number {\n return this._maxLength;\n }\n\n public set maxLength(newMaxLength: number) {\n // There was no change in maxLength, return early.\n if (this._maxLength === newMaxLength) {\n return;\n }\n\n // Reconstruct array, starting at index 0. Only transfer values from the\n // indexes 0 to length.\n const newArray = new Array(newMaxLength);\n for (let i = 0; i < Math.min(newMaxLength, this.length); i++) {\n newArray[i] = this._array[this._getCyclicIndex(i)];\n }\n this._array = newArray;\n this._maxLength = newMaxLength;\n this._startIndex = 0;\n }\n\n public get length(): number {\n return this._length;\n }\n\n public set length(newLength: number) {\n if (newLength > this._length) {\n for (let i = this._length; i < newLength; i++) {\n this._array[i] = undefined;\n }\n }\n this._length = newLength;\n }\n\n /**\n * Gets the value at an index.\n *\n * Note that for performance reasons there is no bounds checking here, the index reference is\n * circular so this should always return a value and never throw.\n * @param index The index of the value to get.\n * @returns The value corresponding to the index.\n */\n public get(index: number): T | undefined {\n return this._array[this._getCyclicIndex(index)];\n }\n\n /**\n * Sets the value at an index.\n *\n * Note that for performance reasons there is no bounds checking here, the index reference is\n * circular so this should always return a value and never throw.\n * @param index The index to set.\n * @param value The value to set.\n */\n public set(index: number, value: T | undefined): void {\n this._array[this._getCyclicIndex(index)] = value;\n }\n\n /**\n * Pushes a new value onto the list, wrapping around to the start of the array, overriding index 0\n * if the maximum length is reached.\n * @param value The value to push onto the list.\n */\n public push(value: T): void {\n this._array[this._getCyclicIndex(this._length)] = value;\n if (this._length === this._maxLength) {\n this._startIndex = ++this._startIndex % this._maxLength;\n this.onTrimEmitter.fire(1);\n } else {\n this._length++;\n }\n }\n\n /**\n * Advance ringbuffer index and return current element for recycling.\n * Note: The buffer must be full for this method to work.\n * @throws When the buffer is not full.\n */\n public recycle(): T {\n if (this._length !== this._maxLength) {\n throw new Error('Can only recycle when the buffer is full');\n }\n this._startIndex = ++this._startIndex % this._maxLength;\n this.onTrimEmitter.fire(1);\n return this._array[this._getCyclicIndex(this._length - 1)]!;\n }\n\n /**\n * Ringbuffer is at max length.\n */\n public get isFull(): boolean {\n return this._length === this._maxLength;\n }\n\n /**\n * Removes and returns the last value on the list.\n * @returns The popped value.\n */\n public pop(): T | undefined {\n return this._array[this._getCyclicIndex(this._length-- - 1)];\n }\n\n /**\n * Deletes and/or inserts items at a particular index (in that order). Unlike\n * Array.prototype.splice, this operation does not return the deleted items as a new array in\n * order to save creating a new array. Note that this operation may shift all values in the list\n * in the worst case.\n * @param start The index to delete and/or insert.\n * @param deleteCount The number of elements to delete.\n * @param items The items to insert.\n */\n public splice(start: number, deleteCount: number, ...items: T[]): void {\n // Delete items\n if (deleteCount) {\n for (let i = start; i < this._length - deleteCount; i++) {\n this._array[this._getCyclicIndex(i)] = this._array[this._getCyclicIndex(i + deleteCount)];\n }\n this._length -= deleteCount;\n this.onDeleteEmitter.fire({ index: start, amount: deleteCount });\n }\n\n // Add items\n for (let i = this._length - 1; i >= start; i--) {\n this._array[this._getCyclicIndex(i + items.length)] = this._array[this._getCyclicIndex(i)];\n }\n for (let i = 0; i < items.length; i++) {\n this._array[this._getCyclicIndex(start + i)] = items[i];\n }\n if (items.length) {\n this.onInsertEmitter.fire({ index: start, amount: items.length });\n }\n\n // Adjust length as needed\n if (this._length + items.length > this._maxLength) {\n const countToTrim = (this._length + items.length) - this._maxLength;\n this._startIndex += countToTrim;\n this._length = this._maxLength;\n this.onTrimEmitter.fire(countToTrim);\n } else {\n this._length += items.length;\n }\n }\n\n /**\n * Trims a number of items from the start of the list.\n * @param count The number of items to remove.\n */\n public trimStart(count: number): void {\n if (count > this._length) {\n count = this._length;\n }\n this._startIndex += count;\n this._length -= count;\n this.onTrimEmitter.fire(count);\n }\n\n public shiftElements(start: number, count: number, offset: number): void {\n if (count <= 0) {\n return;\n }\n if (start < 0 || start >= this._length) {\n throw new Error('start argument out of range');\n }\n if (start + offset < 0) {\n throw new Error('Cannot shift elements in list beyond index 0');\n }\n\n if (offset > 0) {\n for (let i = count - 1; i >= 0; i--) {\n this.set(start + i + offset, this.get(start + i));\n }\n const expandListBy = (start + count + offset) - this._length;\n if (expandListBy > 0) {\n this._length += expandListBy;\n while (this._length > this._maxLength) {\n this._length--;\n this._startIndex++;\n this.onTrimEmitter.fire(1);\n }\n }\n } else {\n for (let i = 0; i < count; i++) {\n this.set(start + i + offset, this.get(start + i));\n }\n }\n }\n\n /**\n * Gets the cyclic index for the specified regular index. The cyclic index can then be used on the\n * backing array to get the element associated with the regular index.\n * @param index The regular index.\n * @returns The cyclic index.\n */\n private _getCyclicIndex(index: number): number {\n return (this._startIndex + index) % this._maxLength;\n }\n}\n", "/**\n * Copyright (c) 2022 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport type { ILogService } from './services/Services';\n\ninterface ITaskQueue {\n /**\n * Adds a task to the queue which will run in a future idle callback.\n * To avoid perceivable stalls on the main thread, tasks with heavy workload\n * should split their work into smaller pieces and return `true` to get\n * called again until the work is done (on falsy return value).\n */\n enqueue(task: () => boolean | void): void;\n\n /**\n * Flushes the queue, running all remaining tasks synchronously.\n */\n flush(): void;\n\n /**\n * Clears any remaining tasks from the queue, these will not be run.\n */\n clear(): void;\n}\n\ninterface ITaskDeadline {\n timeRemaining(): number;\n}\ntype CallbackWithDeadline = (deadline: ITaskDeadline) => void;\n\nabstract class TaskQueue implements ITaskQueue {\n private _tasks: (() => boolean | void)[] = [];\n private _idleCallback?: number;\n private _i = 0;\n protected readonly _logService: ILogService;\n\n constructor(logService: ILogService) {\n this._logService = logService;\n }\n\n protected abstract _requestCallback(callback: CallbackWithDeadline): number;\n protected abstract _cancelCallback(identifier: number): void;\n\n public enqueue(task: () => boolean | void): void {\n this._tasks.push(task);\n this._start();\n }\n\n public flush(): void {\n while (this._i < this._tasks.length) {\n if (!this._tasks[this._i]()) {\n this._i++;\n }\n }\n this.clear();\n }\n\n public clear(): void {\n if (this._idleCallback) {\n this._cancelCallback(this._idleCallback);\n this._idleCallback = undefined;\n }\n this._i = 0;\n this._tasks.length = 0;\n }\n\n private _start(): void {\n if (!this._idleCallback) {\n this._idleCallback = this._requestCallback(this._process.bind(this));\n }\n }\n\n private _process(deadline: ITaskDeadline): void {\n this._idleCallback = undefined;\n let taskDuration: number;\n let longestTask = 0;\n let lastDeadlineRemaining = deadline.timeRemaining();\n let deadlineRemaining: number;\n while (this._i < this._tasks.length) {\n taskDuration = performance.now();\n if (!this._tasks[this._i]()) {\n this._i++;\n }\n // other than performance.now, performance.now might not be stable (changes on wall clock\n // changes), this is not an issue here as a clock change during a short running task is very\n // unlikely in case it still happened and leads to negative duration, simply assume 1 msec\n taskDuration = Math.max(1, performance.now() - taskDuration);\n longestTask = Math.max(taskDuration, longestTask);\n // Guess the following task will take a similar time to the longest task in this batch, allow\n // additional room to try avoid exceeding the deadline\n deadlineRemaining = deadline.timeRemaining();\n if (longestTask * 1.5 > deadlineRemaining) {\n // Warn when the time exceeding the deadline is over 20ms, if this happens in practice the\n // task should be split into sub-tasks to ensure the UI remains responsive.\n if (lastDeadlineRemaining - taskDuration < -20) {\n this._logService.warn(`task queue exceeded allotted deadline by ${Math.abs(Math.round(lastDeadlineRemaining - taskDuration))}ms`);\n }\n this._start();\n return;\n }\n lastDeadlineRemaining = deadlineRemaining;\n }\n this.clear();\n }\n}\n\n/**\n * A queue of that runs tasks over several tasks via setTimeout, trying to maintain above 60 frames\n * per second. The tasks will run in the order they are enqueued, but they will run some time later,\n * and care should be taken to ensure they're non-urgent and will not introduce race conditions.\n */\nexport class PriorityTaskQueue extends TaskQueue {\n protected _requestCallback(callback: CallbackWithDeadline): number {\n return setTimeout(() => callback(this._createDeadline(16)));\n }\n\n protected _cancelCallback(identifier: number): void {\n clearTimeout(identifier);\n }\n\n private _createDeadline(duration: number): ITaskDeadline {\n const end = performance.now() + duration;\n return {\n timeRemaining: () => Math.max(0, end - performance.now())\n };\n }\n}\n\nclass IdleTaskQueueInternal extends TaskQueue {\n protected _requestCallback(callback: IdleRequestCallback): number {\n return requestIdleCallback(callback);\n }\n\n protected _cancelCallback(identifier: number): void {\n cancelIdleCallback(identifier);\n }\n}\n\n/**\n * A queue of that runs tasks over several idle callbacks, trying to respect the idle callback's\n * deadline given by the environment. The tasks will run in the order they are enqueued, but they\n * will run some time later, and care should be taken to ensure they're non-urgent and will not\n * introduce race conditions.\n *\n * This reverts to a {@link PriorityTaskQueue} if the environment does not support idle callbacks.\n */\n// eslint-disable-next-line @typescript-eslint/naming-convention\nexport const IdleTaskQueue = ('requestIdleCallback' in globalThis) ? IdleTaskQueueInternal : PriorityTaskQueue;\n\n/**\n * An object that tracks a single debounced task that will run on the next idle frame. When called\n * multiple times, only the last set task will run.\n */\nexport class DebouncedIdleTask {\n private _queue: ITaskQueue;\n\n constructor(logService: ILogService) {\n this._queue = new IdleTaskQueue(logService);\n }\n\n public set(task: () => boolean | void): void {\n this._queue.clear();\n this._queue.enqueue(task);\n }\n\n public flush(): void {\n this._queue.flush();\n }\n\n public dispose(): void {\n this._queue.clear();\n }\n}\n", "/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { BufferLine } from './BufferLine';\nimport { CircularList } from '../CircularList';\nimport { IBufferLine, ICellData } from './Types';\n\nexport interface INewLayoutResult {\n layout: number[];\n countRemoved: number;\n}\n\n/**\n * Evaluates and returns indexes to be removed after a reflow larger occurs. Lines will be removed\n * when a wrapped line unwraps.\n * @param lines The buffer lines.\n * @param oldCols The columns before resize\n * @param newCols The columns after resize.\n * @param bufferAbsoluteY The absolute y position of the cursor (baseY + cursorY).\n * @param nullCell The cell data to use when filling in empty cells.\n * @param reflowCursorLine Whether to reflow the line containing the cursor.\n */\nexport function reflowLargerGetLinesToRemove(lines: CircularList, oldCols: number, newCols: number, bufferAbsoluteY: number, nullCell: ICellData, reflowCursorLine: boolean): number[] {\n // Gather all BufferLines that need to be removed from the Buffer here so that they can be\n // batched up and only committed once\n const toRemove: number[] = [];\n\n for (let y = 0; y < lines.length - 1; y++) {\n // Check if this row is wrapped\n let i = y;\n let nextLine = lines.get(++i) as BufferLine;\n if (!nextLine.isWrapped) {\n continue;\n }\n\n // Check how many lines it's wrapped for\n const wrappedLines: BufferLine[] = [lines.get(y) as BufferLine];\n while (i < lines.length && nextLine.isWrapped) {\n wrappedLines.push(nextLine);\n nextLine = lines.get(++i) as BufferLine;\n }\n\n if (!reflowCursorLine) {\n // If these lines contain the cursor don't touch them, the program will handle fixing up\n // wrapped lines with the cursor\n if (bufferAbsoluteY >= y && bufferAbsoluteY < i) {\n y += wrappedLines.length - 1;\n continue;\n }\n }\n\n // Copy buffer data to new locations\n let destLineIndex = 0;\n let destCol = getWrappedLineTrimmedLength(wrappedLines, destLineIndex, oldCols);\n let srcLineIndex = 1;\n let srcCol = 0;\n while (srcLineIndex < wrappedLines.length) {\n const srcTrimmedTineLength = getWrappedLineTrimmedLength(wrappedLines, srcLineIndex, oldCols);\n const srcRemainingCells = srcTrimmedTineLength - srcCol;\n const destRemainingCells = newCols - destCol;\n const cellsToCopy = Math.min(srcRemainingCells, destRemainingCells);\n\n wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol, destCol, cellsToCopy, false);\n\n destCol += cellsToCopy;\n if (destCol === newCols) {\n destLineIndex++;\n destCol = 0;\n }\n srcCol += cellsToCopy;\n if (srcCol === srcTrimmedTineLength) {\n srcLineIndex++;\n srcCol = 0;\n }\n\n // Make sure the last cell isn't wide, if it is copy it to the current dest\n if (destCol === 0 && destLineIndex !== 0) {\n if (wrappedLines[destLineIndex - 1].getWidth(newCols - 1) === 2) {\n wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[destLineIndex - 1], newCols - 1, destCol++, 1, false);\n // Null out the end of the last row\n wrappedLines[destLineIndex - 1].setCell(newCols - 1, nullCell);\n }\n }\n }\n\n // Clear out remaining cells or fragments could remain;\n wrappedLines[destLineIndex].replaceCells(destCol, newCols, nullCell);\n\n // Work backwards and remove any rows at the end that only contain null cells\n let countToRemove = 0;\n for (let i = wrappedLines.length - 1; i > 0; i--) {\n if (i > destLineIndex || wrappedLines[i].getTrimmedLength() === 0) {\n countToRemove++;\n } else {\n break;\n }\n }\n\n if (countToRemove > 0) {\n toRemove.push(y + wrappedLines.length - countToRemove); // index\n toRemove.push(countToRemove);\n }\n\n y += wrappedLines.length - 1;\n }\n return toRemove;\n}\n\n/**\n * Creates and return the new layout for lines given an array of indexes to be removed.\n * @param lines The buffer lines.\n * @param toRemove The indexes to remove.\n */\nexport function reflowLargerCreateNewLayout(lines: CircularList, toRemove: number[]): INewLayoutResult {\n const layout: number[] = [];\n // First iterate through the list and get the actual indexes to use for rows\n let nextToRemoveIndex = 0;\n let nextToRemoveStart = toRemove[nextToRemoveIndex];\n let countRemovedSoFar = 0;\n for (let i = 0; i < lines.length; i++) {\n if (nextToRemoveStart === i) {\n const countToRemove = toRemove[++nextToRemoveIndex];\n\n // Tell markers that there was a deletion\n lines.onDeleteEmitter.fire({\n index: i - countRemovedSoFar,\n amount: countToRemove\n });\n\n i += countToRemove - 1;\n countRemovedSoFar += countToRemove;\n nextToRemoveStart = toRemove[++nextToRemoveIndex];\n } else {\n layout.push(i);\n }\n }\n return {\n layout,\n countRemoved: countRemovedSoFar\n };\n}\n\n/**\n * Applies a new layout to the buffer. This essentially does the same as many splice calls but it's\n * done all at once in a single iteration through the list since splice is very expensive.\n * @param lines The buffer lines.\n * @param newLayout The new layout to apply.\n */\nexport function reflowLargerApplyNewLayout(lines: CircularList, newLayout: number[]): void {\n // Record original lines so they don't get overridden when we rearrange the list\n const newLayoutLines: BufferLine[] = [];\n for (let i = 0; i < newLayout.length; i++) {\n newLayoutLines.push(lines.get(newLayout[i]) as BufferLine);\n }\n\n // Rearrange the list\n for (let i = 0; i < newLayoutLines.length; i++) {\n lines.set(i, newLayoutLines[i]);\n }\n lines.length = newLayout.length;\n}\n\n/**\n * Gets the new line lengths for a given wrapped line. The purpose of this function it to pre-\n * compute the wrapping points since wide characters may need to be wrapped onto the following line.\n * This function will return an array of numbers of where each line wraps to, the resulting array\n * will only contain the values `newCols` (when the line does not end with a wide character) and\n * `newCols - 1` (when the line does end with a wide character), except for the last value which\n * will contain the remaining items to fill the line.\n *\n * Calling this with a `newCols` value of `1` will lock up.\n *\n * @param wrappedLines The wrapped lines to evaluate.\n * @param oldCols The columns before resize.\n * @param newCols The columns after resize.\n */\nexport function reflowSmallerGetNewLineLengths(wrappedLines: BufferLine[], oldCols: number, newCols: number): number[] {\n const newLineLengths: number[] = [];\n let cellsNeeded = 0;\n for (let i = 0; i < wrappedLines.length; i++) {\n cellsNeeded += getWrappedLineTrimmedLength(wrappedLines, i, oldCols);\n }\n\n // Use srcCol and srcLine to find the new wrapping point, use that to get the cellsAvailable and\n // linesNeeded\n let srcCol = 0;\n let srcLine = 0;\n let cellsAvailable = 0;\n while (cellsAvailable < cellsNeeded) {\n if (cellsNeeded - cellsAvailable < newCols) {\n // Add the final line and exit the loop\n newLineLengths.push(cellsNeeded - cellsAvailable);\n break;\n }\n srcCol += newCols;\n const oldTrimmedLength = getWrappedLineTrimmedLength(wrappedLines, srcLine, oldCols);\n if (srcCol > oldTrimmedLength) {\n srcCol -= oldTrimmedLength;\n srcLine++;\n }\n const endsWithWide = wrappedLines[srcLine].getWidth(srcCol - 1) === 2;\n if (endsWithWide) {\n srcCol--;\n }\n const lineLength = endsWithWide ? newCols - 1 : newCols;\n newLineLengths.push(lineLength);\n cellsAvailable += lineLength;\n }\n\n return newLineLengths;\n}\n\nexport function getWrappedLineTrimmedLength(lines: BufferLine[], i: number, cols: number): number {\n // If this is the last row in the wrapped line, get the actual trimmed length\n if (i === lines.length - 1) {\n return lines[i].getTrimmedLength();\n }\n // Detect whether the following line starts with a wide character and the end of the current line\n // is null, if so then we can be pretty sure the null character should be excluded from the line\n // length]\n const endsInNull = !(lines[i].hasContent(cols - 1)) && lines[i].getWidth(cols - 1) === 1;\n const followingLineStartsWithWide = lines[i + 1].getWidth(0) === 2;\n if (endsInNull && followingLineStartsWithWide) {\n return cols - 1;\n }\n return cols;\n}\n", "/**\n * Copyright (c) 2018 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { dispose, IDisposable } from '../Lifecycle';\nimport { IMarker } from './Types';\nimport { Emitter } from '../Event';\n\nexport class Marker implements IMarker {\n private static _nextId = 1;\n\n public isDisposed: boolean = false;\n private readonly _disposables: IDisposable[] = [];\n\n private readonly _id: number = Marker._nextId++;\n public get id(): number { return this._id; }\n\n private readonly _onDispose = this.register(new Emitter());\n public readonly onDispose = this._onDispose.event;\n\n constructor(\n public line: number\n ) {\n }\n\n public dispose(): void {\n if (this.isDisposed) {\n return;\n }\n this.isDisposed = true;\n this.line = -1;\n // Emit before super.dispose such that dispose listeners get a chance to react\n this._onDispose.fire();\n dispose(this._disposables);\n this._disposables.length = 0;\n }\n\n public register(disposable: T): T {\n this._disposables.push(disposable);\n return disposable;\n }\n}\n", "/**\n * Copyright (c) 2016 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { ICharset } from '../Types';\n\n/**\n * The character sets supported by the terminal. These enable several languages\n * to be represented within the terminal with only 8-bit encoding. See ISO 2022\n * for a discussion on character sets. Only VT100 character sets are supported.\n */\nexport const CHARSETS: { [key: string]: ICharset | undefined } = {};\n\n/**\n * The default character set, US.\n */\nexport const DEFAULT_CHARSET: ICharset | undefined = CHARSETS['B'];\n\n/**\n * DEC Special Character and Line Drawing Set.\n * Reference: http://vt100.net/docs/vt102-ug/table5-13.html\n * A lot of curses apps use this if they see TERM=xterm.\n * testing: echo -e '\\e(0a\\e(B'\n * The xterm output sometimes seems to conflict with the\n * reference above. xterm seems in line with the reference\n * when running vttest however.\n * The table below now uses xterm's output from vttest.\n */\nCHARSETS['0'] = {\n '`': '\\u25c6', // '\u25C6'\n 'a': '\\u2592', // '\u2592'\n 'b': '\\u2409', // '\u2409' (HT)\n 'c': '\\u240c', // '\u240C' (FF)\n 'd': '\\u240d', // '\u240D' (CR)\n 'e': '\\u240a', // '\u240A' (LF)\n 'f': '\\u00b0', // '\u00B0'\n 'g': '\\u00b1', // '\u00B1'\n 'h': '\\u2424', // '\u2424' (NL)\n 'i': '\\u240b', // '\u240B' (VT)\n 'j': '\\u2518', // '\u2518'\n 'k': '\\u2510', // '\u2510'\n 'l': '\\u250c', // '\u250C'\n 'm': '\\u2514', // '\u2514'\n 'n': '\\u253c', // '\u253C'\n 'o': '\\u23ba', // '\u23BA'\n 'p': '\\u23bb', // '\u23BB'\n 'q': '\\u2500', // '\u2500'\n 'r': '\\u23bc', // '\u23BC'\n 's': '\\u23bd', // '\u23BD'\n 't': '\\u251c', // '\u251C'\n 'u': '\\u2524', // '\u2524'\n 'v': '\\u2534', // '\u2534'\n 'w': '\\u252c', // '\u252C'\n 'x': '\\u2502', // '\u2502'\n 'y': '\\u2264', // '\u2264'\n 'z': '\\u2265', // '\u2265'\n '{': '\\u03c0', // '\u03C0'\n '|': '\\u2260', // '\u2260'\n '}': '\\u00a3', // '\u00A3'\n '~': '\\u00b7' // '\u00B7'\n};\n\n/**\n * British character set\n * ESC (A\n * Reference: http://vt100.net/docs/vt220-rm/table2-5.html\n */\nCHARSETS['A'] = {\n '#': '\u00A3'\n};\n\n/**\n * United States character set\n * ESC (B\n */\nCHARSETS['B'] = undefined;\n\n/**\n * Dutch character set\n * ESC (4\n * Reference: http://vt100.net/docs/vt220-rm/table2-6.html\n */\nCHARSETS['4'] = {\n '#': '\u00A3',\n '@': '\u00BE',\n '[': 'ij',\n '\\\\': '\u00BD',\n ']': '|',\n '{': '\u00A8',\n '|': 'f',\n '}': '\u00BC',\n '~': '\u00B4'\n};\n\n/**\n * Finnish character set\n * ESC (C or ESC (5\n * Reference: http://vt100.net/docs/vt220-rm/table2-7.html\n */\nCHARSETS['C'] = CHARSETS['5'] = {\n '[': '\u00C4',\n '\\\\': '\u00D6',\n ']': '\u00C5',\n '^': '\u00DC',\n '`': '\u00E9',\n '{': '\u00E4',\n '|': '\u00F6',\n '}': '\u00E5',\n '~': '\u00FC'\n};\n\n/**\n * French character set\n * ESC (R\n * Reference: http://vt100.net/docs/vt220-rm/table2-8.html\n */\nCHARSETS['R'] = {\n '#': '\u00A3',\n '@': '\u00E0',\n '[': '\u00B0',\n '\\\\': '\u00E7',\n ']': '\u00A7',\n '{': '\u00E9',\n '|': '\u00F9',\n '}': '\u00E8',\n '~': '\u00A8'\n};\n\n/**\n * French Canadian character set\n * ESC (Q\n * Reference: http://vt100.net/docs/vt220-rm/table2-9.html\n */\nCHARSETS['Q'] = {\n '@': '\u00E0',\n '[': '\u00E2',\n '\\\\': '\u00E7',\n ']': '\u00EA',\n '^': '\u00EE',\n '`': '\u00F4',\n '{': '\u00E9',\n '|': '\u00F9',\n '}': '\u00E8',\n '~': '\u00FB'\n};\n\n/**\n * German character set\n * ESC (K\n * Reference: http://vt100.net/docs/vt220-rm/table2-10.html\n */\nCHARSETS['K'] = {\n '@': '\u00A7',\n '[': '\u00C4',\n '\\\\': '\u00D6',\n ']': '\u00DC',\n '{': '\u00E4',\n '|': '\u00F6',\n '}': '\u00FC',\n '~': '\u00DF'\n};\n\n/**\n * Italian character set\n * ESC (Y\n * Reference: http://vt100.net/docs/vt220-rm/table2-11.html\n */\nCHARSETS['Y'] = {\n '#': '\u00A3',\n '@': '\u00A7',\n '[': '\u00B0',\n '\\\\': '\u00E7',\n ']': '\u00E9',\n '`': '\u00F9',\n '{': '\u00E0',\n '|': '\u00F2',\n '}': '\u00E8',\n '~': '\u00EC'\n};\n\n/**\n * Norwegian/Danish character set\n * ESC (E or ESC (6\n * Reference: http://vt100.net/docs/vt220-rm/table2-12.html\n */\nCHARSETS['E'] = CHARSETS['6'] = {\n '@': '\u00C4',\n '[': '\u00C6',\n '\\\\': '\u00D8',\n ']': '\u00C5',\n '^': '\u00DC',\n '`': '\u00E4',\n '{': '\u00E6',\n '|': '\u00F8',\n '}': '\u00E5',\n '~': '\u00FC'\n};\n\n/**\n * Spanish character set\n * ESC (Z\n * Reference: http://vt100.net/docs/vt220-rm/table2-13.html\n */\nCHARSETS['Z'] = {\n '#': '\u00A3',\n '@': '\u00A7',\n '[': '\u00A1',\n '\\\\': '\u00D1',\n ']': '\u00BF',\n '{': '\u00B0',\n '|': '\u00F1',\n '}': '\u00E7'\n};\n\n/**\n * Swedish character set\n * ESC (H or ESC (7\n * Reference: http://vt100.net/docs/vt220-rm/table2-14.html\n */\nCHARSETS['H'] = CHARSETS['7'] = {\n '@': '\u00C9',\n '[': '\u00C4',\n '\\\\': '\u00D6',\n ']': '\u00C5',\n '^': '\u00DC',\n '`': '\u00E9',\n '{': '\u00E4',\n '|': '\u00F6',\n '}': '\u00E5',\n '~': '\u00FC'\n};\n\n/**\n * Swiss character set\n * ESC (=\n * Reference: http://vt100.net/docs/vt220-rm/table2-15.html\n */\nCHARSETS['='] = {\n '#': '\u00F9',\n '@': '\u00E0',\n '[': '\u00E9',\n '\\\\': '\u00E7',\n ']': '\u00EA',\n '^': '\u00EE',\n\n '_': '\u00E8',\n '`': '\u00F4',\n '{': '\u00E4',\n '|': '\u00F6',\n '}': '\u00FC',\n '~': '\u00FB'\n};\n", "/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { CircularList, IInsertEvent } from '../CircularList';\nimport { Disposable, toDisposable } from '../Lifecycle';\nimport { IdleTaskQueue } from '../TaskQueue';\nimport { ICharset } from '../Types';\nimport { IAttributeData, IBuffer, IBufferLine, ICellData } from './Types';\nimport { ExtendedAttrs } from './AttributeData';\nimport { BufferLine, DEFAULT_ATTR_DATA } from './BufferLine';\nimport { getWrappedLineTrimmedLength, reflowLargerApplyNewLayout, reflowLargerCreateNewLayout, reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths } from './BufferReflow';\nimport { CellData } from './CellData';\nimport { NULL_CELL_CHAR, NULL_CELL_CODE, NULL_CELL_WIDTH, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE, WHITESPACE_CELL_WIDTH } from './Constants';\nimport { Marker } from './Marker';\nimport { DEFAULT_CHARSET } from '../data/Charsets';\nimport { IBufferService, ILogService, IOptionsService } from '../services/Services';\n\nexport const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1\n\n/**\n * This class represents a terminal buffer (an internal state of the terminal), where the\n * following information is stored (in high-level):\n * - text content of this particular buffer\n * - cursor position\n * - scroll position\n */\nexport class Buffer extends Disposable implements IBuffer {\n public lines: CircularList;\n public ydisp: number = 0;\n public ybase: number = 0;\n public y: number = 0;\n public x: number = 0;\n public scrollBottom: number;\n public scrollTop: number;\n public tabs: { [column: number]: boolean | undefined } = {};\n public savedY: number = 0;\n public savedX: number = 0;\n public savedCurAttrData = DEFAULT_ATTR_DATA.clone();\n public savedCharset: ICharset | undefined = DEFAULT_CHARSET;\n public savedCharsets: (ICharset | undefined)[] = [];\n public savedGlevel: number = 0;\n public savedOriginMode: boolean = false;\n public savedWraparoundMode: boolean = true;\n public markers: Marker[] = [];\n private _nullCell: ICellData = CellData.fromCharData([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);\n private _whitespaceCell: ICellData = CellData.fromCharData([0, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_WIDTH, WHITESPACE_CELL_CODE]);\n private _cols: number;\n private _rows: number;\n private _isClearing: boolean = false;\n private _memoryCleanupQueue: InstanceType;\n private _memoryCleanupPosition = 0;\n\n constructor(\n private _hasScrollback: boolean,\n private _optionsService: IOptionsService,\n private _bufferService: IBufferService,\n private readonly _logService: ILogService\n ) {\n super();\n this._cols = this._bufferService.cols;\n this._rows = this._bufferService.rows;\n this.lines = new CircularList(this._getCorrectBufferLength(this._rows));\n this.scrollTop = 0;\n this.scrollBottom = this._rows - 1;\n this.setupTabStops();\n this._memoryCleanupQueue = new IdleTaskQueue(this._logService);\n this._register(toDisposable(() => this._memoryCleanupQueue.clear()));\n this._register(toDisposable(() => this.clearAllMarkers()));\n }\n\n public getNullCell(attr?: IAttributeData): ICellData {\n if (attr) {\n this._nullCell.fg = attr.fg;\n this._nullCell.bg = attr.bg;\n this._nullCell.extended = attr.extended;\n } else {\n this._nullCell.fg = 0;\n this._nullCell.bg = 0;\n this._nullCell.extended = new ExtendedAttrs();\n }\n return this._nullCell;\n }\n\n public getWhitespaceCell(attr?: IAttributeData): ICellData {\n if (attr) {\n this._whitespaceCell.fg = attr.fg;\n this._whitespaceCell.bg = attr.bg;\n this._whitespaceCell.extended = attr.extended;\n } else {\n this._whitespaceCell.fg = 0;\n this._whitespaceCell.bg = 0;\n this._whitespaceCell.extended = new ExtendedAttrs();\n }\n return this._whitespaceCell;\n }\n\n public getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine {\n return new BufferLine(this._bufferService.cols, this.getNullCell(attr), isWrapped);\n }\n\n public get hasScrollback(): boolean {\n return this._hasScrollback && this.lines.maxLength > this._rows;\n }\n\n public get isCursorInViewport(): boolean {\n const absoluteY = this.ybase + this.y;\n const relativeY = absoluteY - this.ydisp;\n return (relativeY >= 0 && relativeY < this._rows);\n }\n\n /**\n * Gets the correct buffer length based on the rows provided, the terminal's\n * scrollback and whether this buffer is flagged to have scrollback or not.\n * @param rows The terminal rows to use in the calculation.\n */\n private _getCorrectBufferLength(rows: number): number {\n if (!this._hasScrollback) {\n return rows;\n }\n\n const correctBufferLength = rows + this._optionsService.rawOptions.scrollback;\n\n return correctBufferLength > MAX_BUFFER_SIZE ? MAX_BUFFER_SIZE : correctBufferLength;\n }\n\n /**\n * Fills the buffer's viewport with blank lines.\n */\n public fillViewportRows(fillAttr?: IAttributeData): void {\n if (this.lines.length === 0) {\n fillAttr ??= DEFAULT_ATTR_DATA;\n let i = this._rows;\n while (i--) {\n this.lines.push(this.getBlankLine(fillAttr));\n }\n }\n }\n\n /**\n * Clears the buffer to its initial state, discarding all previous data.\n */\n public clear(): void {\n this.ydisp = 0;\n this.ybase = 0;\n this.y = 0;\n this.x = 0;\n this.lines = new CircularList(this._getCorrectBufferLength(this._rows));\n this.scrollTop = 0;\n this.scrollBottom = this._rows - 1;\n this.setupTabStops();\n }\n\n /**\n * Resizes the buffer, adjusting its data accordingly.\n * @param newCols The new number of columns.\n * @param newRows The new number of rows.\n */\n public resize(newCols: number, newRows: number): void {\n // store reference to null cell with default attrs\n const nullCell = this.getNullCell(DEFAULT_ATTR_DATA);\n\n // count bufferlines with overly big memory to be cleaned afterwards\n let dirtyMemoryLines = 0;\n\n // Increase max length if needed before adjustments to allow space to fill\n // as required.\n const newMaxLength = this._getCorrectBufferLength(newRows);\n if (newMaxLength > this.lines.maxLength) {\n this.lines.maxLength = newMaxLength;\n }\n\n // if (this._cols > newCols) {\n // console.log('increase!');\n // }\n\n // The following adjustments should only happen if the buffer has been\n // initialized/filled.\n if (this.lines.length > 0) {\n // Deal with columns increasing (reducing needs to happen after reflow)\n if (this._cols < newCols) {\n for (let i = 0; i < this.lines.length; i++) {\n // +boolean for fast 0 or 1 conversion\n dirtyMemoryLines += +this.lines.get(i)!.resize(newCols, nullCell);\n }\n }\n\n // Resize rows in both directions as needed\n let addToY = 0;\n if (this._rows < newRows) {\n for (let y = this._rows; y < newRows; y++) {\n if (this.lines.length < newRows + this.ybase) {\n if (this._optionsService.rawOptions.windowsPty.backend !== undefined || this._optionsService.rawOptions.windowsPty.buildNumber !== undefined) {\n // Just add the new missing rows on Windows as conpty reprints the screen with its\n // view of the world. Once a line enters scrollback for conpty it remains there\n this.lines.push(new BufferLine(newCols, nullCell, false));\n } else {\n if (this.ybase > 0 && this.lines.length <= this.ybase + this.y + addToY + 1) {\n // There is room above the buffer and there are no empty elements below the line,\n // scroll up\n this.ybase--;\n addToY++;\n if (this.ydisp > 0) {\n // Viewport is at the top of the buffer, must increase downwards\n this.ydisp--;\n }\n } else {\n // Add a blank line if there is no buffer left at the top to scroll to, or if there\n // are blank lines after the cursor\n this.lines.push(new BufferLine(newCols, nullCell, false));\n }\n }\n }\n }\n } else { // (this._rows >= newRows)\n for (let y = this._rows; y > newRows; y--) {\n if (this.lines.length > newRows + this.ybase) {\n if (this.lines.length > this.ybase + this.y + 1) {\n // The line is a blank line below the cursor, remove it\n this.lines.pop();\n } else {\n // The line is the cursor, scroll down\n this.ybase++;\n this.ydisp++;\n }\n }\n }\n }\n\n // Reduce max length if needed after adjustments, this is done after as it\n // would otherwise cut data from the bottom of the buffer.\n if (newMaxLength < this.lines.maxLength) {\n // Trim from the top of the buffer and adjust ybase and ydisp.\n const amountToTrim = this.lines.length - newMaxLength;\n if (amountToTrim > 0) {\n this.lines.trimStart(amountToTrim);\n this.ybase = Math.max(this.ybase - amountToTrim, 0);\n this.ydisp = Math.max(this.ydisp - amountToTrim, 0);\n this.savedY = Math.max(this.savedY - amountToTrim, 0);\n }\n this.lines.maxLength = newMaxLength;\n }\n\n // Make sure that the cursor stays on screen\n this.x = Math.min(this.x, newCols - 1);\n this.y = Math.min(this.y, newRows - 1);\n if (addToY) {\n this.y += addToY;\n }\n this.savedX = Math.min(this.savedX, newCols - 1);\n\n this.scrollTop = 0;\n }\n\n this.scrollBottom = newRows - 1;\n\n if (this._isReflowEnabled) {\n this._reflow(newCols, newRows);\n\n // Trim the end of the line off if cols shrunk\n if (this._cols > newCols) {\n for (let i = 0; i < this.lines.length; i++) {\n // +boolean for fast 0 or 1 conversion\n dirtyMemoryLines += +this.lines.get(i)!.resize(newCols, nullCell);\n }\n }\n }\n\n this._cols = newCols;\n this._rows = newRows;\n\n // Ensure the cursor position invariant: ybase + y must be within buffer bounds\n // This can be violated during reflow or when shrinking rows\n if (this.lines.length > 0) {\n const maxY = Math.max(0, this.lines.length - this.ybase - 1);\n this.y = Math.min(this.y, maxY);\n }\n\n this._memoryCleanupQueue.clear();\n // schedule memory cleanup only, if more than 10% of the lines are affected\n if (dirtyMemoryLines > 0.1 * this.lines.length) {\n this._memoryCleanupPosition = 0;\n this._memoryCleanupQueue.enqueue(() => this._batchedMemoryCleanup());\n }\n }\n\n private _batchedMemoryCleanup(): boolean {\n let normalRun = true;\n if (this._memoryCleanupPosition >= this.lines.length) {\n // cleanup made it once through all lines, thus rescan in loop below to also catch shifted\n // lines, which should finish rather quick if there are no more cleanups pending\n this._memoryCleanupPosition = 0;\n normalRun = false;\n }\n let counted = 0;\n while (this._memoryCleanupPosition < this.lines.length) {\n counted += this.lines.get(this._memoryCleanupPosition++)!.cleanupMemory();\n // cleanup max 100 lines per batch\n if (counted > 100) {\n return true;\n }\n }\n // normal runs always need another rescan afterwards\n // if we made it here with normalRun=false, we are in a final run\n // and can end the cleanup task for sure\n return normalRun;\n }\n\n private get _isReflowEnabled(): boolean {\n const windowsPty = this._optionsService.rawOptions.windowsPty;\n if (windowsPty && windowsPty.buildNumber) {\n return this._hasScrollback && windowsPty.backend === 'conpty' && windowsPty.buildNumber >= 21376;\n }\n return this._hasScrollback;\n }\n\n private _reflow(newCols: number, newRows: number): void {\n if (this._cols === newCols) {\n return;\n }\n\n // Iterate through rows, ignore the last one as it cannot be wrapped\n if (newCols > this._cols) {\n this._reflowLarger(newCols, newRows);\n } else {\n this._reflowSmaller(newCols, newRows);\n }\n }\n\n private _reflowLarger(newCols: number, newRows: number): void {\n const reflowCursorLine = this._optionsService.rawOptions.reflowCursorLine;\n const toRemove: number[] = reflowLargerGetLinesToRemove(this.lines, this._cols, newCols, this.ybase + this.y, this.getNullCell(DEFAULT_ATTR_DATA), reflowCursorLine);\n if (toRemove.length > 0) {\n const newLayoutResult = reflowLargerCreateNewLayout(this.lines, toRemove);\n reflowLargerApplyNewLayout(this.lines, newLayoutResult.layout);\n this._reflowLargerAdjustViewport(newCols, newRows, newLayoutResult.countRemoved);\n }\n }\n\n private _reflowLargerAdjustViewport(newCols: number, newRows: number, countRemoved: number): void {\n const nullCell = this.getNullCell(DEFAULT_ATTR_DATA);\n // Adjust viewport based on number of items removed\n let viewportAdjustments = countRemoved;\n while (viewportAdjustments-- > 0) {\n if (this.ybase === 0) {\n if (this.y > 0) {\n this.y--;\n }\n if (this.lines.length < newRows) {\n // Add an extra row at the bottom of the viewport\n this.lines.push(new BufferLine(newCols, nullCell, false));\n }\n } else {\n if (this.ydisp === this.ybase) {\n this.ydisp--;\n }\n this.ybase--;\n }\n }\n this.savedY = Math.max(this.savedY - countRemoved, 0);\n }\n\n private _reflowSmaller(newCols: number, newRows: number): void {\n const reflowCursorLine = this._optionsService.rawOptions.reflowCursorLine;\n const nullCell = this.getNullCell(DEFAULT_ATTR_DATA);\n // Gather all BufferLines that need to be inserted into the Buffer here so that they can be\n // batched up and only committed once\n const toInsert = [];\n let countToInsert = 0;\n // Go backwards as many lines may be trimmed and this will avoid considering them\n for (let y = this.lines.length - 1; y >= 0; y--) {\n // Check whether this line is a problem\n let nextLine = this.lines.get(y) as BufferLine;\n if (!nextLine || !nextLine.isWrapped && nextLine.getTrimmedLength() <= newCols) {\n continue;\n }\n\n // Gather wrapped lines and adjust y to be the starting line\n const wrappedLines: BufferLine[] = [nextLine];\n while (nextLine.isWrapped && y > 0) {\n nextLine = this.lines.get(--y) as BufferLine;\n wrappedLines.unshift(nextLine);\n }\n\n if (!reflowCursorLine) {\n // If these lines contain the cursor don't touch them, the program will handle fixing up\n // wrapped lines with the cursor\n const absoluteY = this.ybase + this.y;\n if (absoluteY >= y && absoluteY < y + wrappedLines.length) {\n continue;\n }\n }\n\n const lastLineLength = wrappedLines[wrappedLines.length - 1].getTrimmedLength();\n const destLineLengths = reflowSmallerGetNewLineLengths(wrappedLines, this._cols, newCols);\n const linesToAdd = destLineLengths.length - wrappedLines.length;\n let trimmedLines: number;\n if (this.ybase === 0 && this.y !== this.lines.length - 1) {\n // If the top section of the buffer is not yet filled\n trimmedLines = Math.max(0, this.y - this.lines.maxLength + linesToAdd);\n } else {\n trimmedLines = Math.max(0, this.lines.length - this.lines.maxLength + linesToAdd);\n }\n\n // Add the new lines\n const newLines: BufferLine[] = [];\n for (let i = 0; i < linesToAdd; i++) {\n const newLine = this.getBlankLine(DEFAULT_ATTR_DATA, true) as BufferLine;\n newLines.push(newLine);\n }\n if (newLines.length > 0) {\n toInsert.push({\n // countToInsert here gets the actual index, taking into account other inserted items.\n // using this we can iterate through the list forwards\n start: y + wrappedLines.length + countToInsert,\n newLines\n });\n countToInsert += newLines.length;\n }\n wrappedLines.push(...newLines);\n\n // Copy buffer data to new locations, this needs to happen backwards to do in-place\n let destLineIndex = destLineLengths.length - 1; // Math.floor(cellsNeeded / newCols);\n let destCol = destLineLengths[destLineIndex]; // cellsNeeded % newCols;\n if (destCol === 0) {\n destLineIndex--;\n destCol = destLineLengths[destLineIndex];\n }\n let srcLineIndex = wrappedLines.length - linesToAdd - 1;\n let srcCol = lastLineLength;\n while (srcLineIndex >= 0) {\n const cellsToCopy = Math.min(srcCol, destCol);\n if (wrappedLines[destLineIndex] === undefined) {\n // Sanity check that the line exists, this has been known to fail for an unknown reason\n // which would stop the reflow from happening if an exception would throw.\n break;\n }\n wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol - cellsToCopy, destCol - cellsToCopy, cellsToCopy, true);\n destCol -= cellsToCopy;\n if (destCol === 0) {\n destLineIndex--;\n destCol = destLineLengths[destLineIndex];\n }\n srcCol -= cellsToCopy;\n if (srcCol === 0) {\n srcLineIndex--;\n const wrappedLinesIndex = Math.max(srcLineIndex, 0);\n srcCol = getWrappedLineTrimmedLength(wrappedLines, wrappedLinesIndex, this._cols);\n }\n }\n\n // Null out the end of the line ends if a wide character wrapped to the following line\n for (let i = 0; i < wrappedLines.length; i++) {\n if (destLineLengths[i] < newCols) {\n wrappedLines[i].setCell(destLineLengths[i], nullCell);\n }\n }\n\n // Adjust viewport as needed\n let viewportAdjustments = linesToAdd - trimmedLines;\n while (viewportAdjustments-- > 0) {\n if (this.ybase === 0) {\n if (this.y < newRows - 1) {\n this.y++;\n this.lines.pop();\n } else {\n this.ybase++;\n this.ydisp++;\n }\n } else {\n // Ensure ybase does not exceed its maximum value\n if (this.ybase < Math.min(this.lines.maxLength, this.lines.length + countToInsert) - newRows) {\n if (this.ybase === this.ydisp) {\n this.ydisp++;\n }\n this.ybase++;\n }\n }\n }\n this.savedY = Math.min(this.savedY + linesToAdd, this.ybase + newRows - 1);\n }\n\n // Rearrange lines in the buffer if there are any insertions, this is done at the end rather\n // than earlier so that it's a single O(n) pass through the buffer, instead of O(n^2) from many\n // costly calls to CircularList.splice.\n if (toInsert.length > 0) {\n // Record buffer insert events and then play them back backwards so that the indexes are\n // correct\n const insertEvents: IInsertEvent[] = [];\n\n // Record original lines so they don't get overridden when we rearrange the list\n const originalLines: BufferLine[] = [];\n for (let i = 0; i < this.lines.length; i++) {\n originalLines.push(this.lines.get(i) as BufferLine);\n }\n const originalLinesLength = this.lines.length;\n\n let originalLineIndex = originalLinesLength - 1;\n let nextToInsertIndex = 0;\n let nextToInsert = toInsert[nextToInsertIndex];\n this.lines.length = Math.min(this.lines.maxLength, this.lines.length + countToInsert);\n let countInsertedSoFar = 0;\n for (let i = Math.min(this.lines.maxLength - 1, originalLinesLength + countToInsert - 1); i >= 0; i--) {\n if (nextToInsert && nextToInsert.start > originalLineIndex + countInsertedSoFar) {\n // Insert extra lines here, adjusting i as needed\n for (let nextI = nextToInsert.newLines.length - 1; nextI >= 0; nextI--) {\n this.lines.set(i--, nextToInsert.newLines[nextI]);\n }\n i++;\n\n // Create insert events for later\n insertEvents.push({\n index: originalLineIndex + 1,\n amount: nextToInsert.newLines.length\n });\n\n countInsertedSoFar += nextToInsert.newLines.length;\n nextToInsert = toInsert[++nextToInsertIndex];\n } else {\n this.lines.set(i, originalLines[originalLineIndex--]);\n }\n }\n\n // Update markers\n let insertCountEmitted = 0;\n for (let i = insertEvents.length - 1; i >= 0; i--) {\n insertEvents[i].index += insertCountEmitted;\n this.lines.onInsertEmitter.fire(insertEvents[i]);\n insertCountEmitted += insertEvents[i].amount;\n }\n const amountToTrim = Math.max(0, originalLinesLength + countToInsert - this.lines.maxLength);\n if (amountToTrim > 0) {\n this.lines.onTrimEmitter.fire(amountToTrim);\n }\n }\n }\n\n /**\n * Translates a buffer line to a string, with optional start and end columns.\n * Wide characters will count as two columns in the resulting string. This\n * function is useful for getting the actual text underneath the raw selection\n * position.\n * @param lineIndex The absolute index of the line being translated.\n * @param trimRight Whether to trim whitespace to the right.\n * @param startCol The column to start at.\n * @param endCol The column to end at.\n */\n public translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol: number = 0, endCol?: number): string {\n const line = this.lines.get(lineIndex);\n if (!line) {\n return '';\n }\n return line.translateToString(trimRight, startCol, endCol);\n }\n\n public getWrappedRangeForLine(y: number): { first: number, last: number } {\n let first = y;\n let last = y;\n // Scan upwards for wrapped lines\n while (first > 0 && this.lines.get(first)!.isWrapped) {\n first--;\n }\n // Scan downwards for wrapped lines\n while (last + 1 < this.lines.length && this.lines.get(last + 1)!.isWrapped) {\n last++;\n }\n return { first, last };\n }\n\n /**\n * Setup the tab stops.\n * @param i The index to start setting up tab stops from.\n */\n public setupTabStops(i?: number): void {\n if (i !== null && i !== undefined) {\n if (!this.tabs[i]) {\n i = this.prevStop(i);\n }\n } else {\n this.tabs = {};\n i = 0;\n }\n\n for (; i < this._cols; i += this._optionsService.rawOptions.tabStopWidth) {\n this.tabs[i] = true;\n }\n }\n\n /**\n * Move the cursor to the previous tab stop from the given position (default is current).\n * @param x The position to move the cursor to the previous tab stop.\n */\n public prevStop(x?: number): number {\n x ??= this.x;\n while (!this.tabs[--x] && x > 0);\n return x >= this._cols ? this._cols - 1 : x < 0 ? 0 : x;\n }\n\n /**\n * Move the cursor one tab stop forward from the given position (default is current).\n * @param x The position to move the cursor one tab stop forward.\n */\n public nextStop(x?: number): number {\n x ??= this.x;\n while (!this.tabs[++x] && x < this._cols);\n return x >= this._cols ? this._cols - 1 : x < 0 ? 0 : x;\n }\n\n /**\n * Clears markers on single line.\n * @param y The line to clear.\n */\n public clearMarkers(y: number): void {\n this._isClearing = true;\n for (let i = 0; i < this.markers.length; i++) {\n if (this.markers[i].line === y) {\n this.markers[i].dispose();\n this.markers.splice(i--, 1);\n }\n }\n this._isClearing = false;\n }\n\n /**\n * Clears markers on all lines\n */\n public clearAllMarkers(): void {\n this._isClearing = true;\n for (let i = 0; i < this.markers.length; i++) {\n this.markers[i].dispose();\n }\n this.markers.length = 0;\n this._isClearing = false;\n }\n\n public addMarker(y: number): Marker {\n const marker = new Marker(y);\n this.markers.push(marker);\n marker.register(this.lines.onTrim(amount => {\n marker.line -= amount;\n // The marker should be disposed when the line is trimmed from the buffer\n if (marker.line < 0) {\n marker.dispose();\n }\n }));\n marker.register(this.lines.onInsert(event => {\n if (marker.line >= event.index) {\n marker.line += event.amount;\n }\n }));\n marker.register(this.lines.onDelete(event => {\n // Delete the marker if it's within the range\n if (marker.line >= event.index && marker.line < event.index + event.amount) {\n marker.dispose();\n }\n\n // Shift the marker if it's after the deleted range\n if (marker.line > event.index) {\n marker.line -= event.amount;\n }\n }));\n marker.register(marker.onDispose(() => this._removeMarker(marker)));\n return marker;\n }\n\n private _removeMarker(marker: Marker): void {\n if (!this._isClearing) {\n this.markers.splice(this.markers.indexOf(marker), 1);\n }\n }\n}\n", "/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { Disposable, MutableDisposable } from '../Lifecycle';\nimport { Buffer } from './Buffer';\nimport { IAttributeData, IBuffer, IBufferSet } from './Types';\nimport { IBufferService, ILogService, IOptionsService } from '../services/Services';\nimport { Emitter } from '../Event';\n\n/**\n * The BufferSet represents the set of two buffers used by xterm terminals (normal and alt) and\n * provides also utilities for working with them.\n */\nexport class BufferSet extends Disposable implements IBufferSet {\n private _normal!: Buffer;\n private _alt!: Buffer;\n private _activeBuffer!: Buffer;\n private readonly _normalBuffer = this._register(new MutableDisposable());\n private readonly _altBuffer = this._register(new MutableDisposable());\n\n private readonly _onBufferActivate = this._register(new Emitter<{ activeBuffer: IBuffer, inactiveBuffer: IBuffer }>());\n public readonly onBufferActivate = this._onBufferActivate.event;\n\n /**\n * Create a new BufferSet for the given terminal.\n */\n constructor(\n private readonly _optionsService: IOptionsService,\n private readonly _bufferService: IBufferService,\n private readonly _logService: ILogService\n ) {\n super();\n this.reset();\n this._register(this._optionsService.onSpecificOptionChange('scrollback', () => this.resize(this._bufferService.cols, this._bufferService.rows)));\n this._register(this._optionsService.onSpecificOptionChange('tabStopWidth', () => this.setupTabStops()));\n }\n\n public reset(): void {\n this._normal = new Buffer(true, this._optionsService, this._bufferService, this._logService);\n this._normalBuffer.value = this._normal;\n this._normal.fillViewportRows();\n\n // The alt buffer should never have scrollback.\n // See http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-The-Alternate-Screen-Buffer\n this._alt = new Buffer(false, this._optionsService, this._bufferService, this._logService);\n this._altBuffer.value = this._alt;\n this._activeBuffer = this._normal;\n this._onBufferActivate.fire({\n activeBuffer: this._normal,\n inactiveBuffer: this._alt\n });\n\n this.setupTabStops();\n }\n\n /**\n * Returns the alt Buffer of the BufferSet\n */\n public get alt(): Buffer {\n return this._alt;\n }\n\n /**\n * Returns the currently active Buffer of the BufferSet\n */\n public get active(): Buffer {\n return this._activeBuffer;\n }\n\n /**\n * Returns the normal Buffer of the BufferSet\n */\n public get normal(): Buffer {\n return this._normal;\n }\n\n /**\n * Sets the normal Buffer of the BufferSet as its currently active Buffer\n */\n public activateNormalBuffer(): void {\n if (this._activeBuffer === this._normal) {\n return;\n }\n this._normal.x = this._alt.x;\n this._normal.y = this._alt.y;\n // The alt buffer should always be cleared when we switch to the normal\n // buffer. This frees up memory since the alt buffer should always be new\n // when activated.\n this._alt.clearAllMarkers();\n this._alt.clear();\n this._activeBuffer = this._normal;\n this._onBufferActivate.fire({\n activeBuffer: this._normal,\n inactiveBuffer: this._alt\n });\n }\n\n /**\n * Sets the alt Buffer of the BufferSet as its currently active Buffer\n */\n public activateAltBuffer(fillAttr?: IAttributeData): void {\n if (this._activeBuffer === this._alt) {\n return;\n }\n // Since the alt buffer is always cleared when the normal buffer is\n // activated, we want to fill it when switching to it.\n this._alt.fillViewportRows(fillAttr);\n this._alt.x = this._normal.x;\n this._alt.y = this._normal.y;\n this._activeBuffer = this._alt;\n this._onBufferActivate.fire({\n activeBuffer: this._alt,\n inactiveBuffer: this._normal\n });\n }\n\n /**\n * Resizes both normal and alt buffers, adjusting their data accordingly.\n * @param newCols The new number of columns.\n * @param newRows The new number of rows.\n */\n public resize(newCols: number, newRows: number): void {\n this._normal.resize(newCols, newRows);\n this._alt.resize(newCols, newRows);\n this.setupTabStops(newCols);\n }\n\n /**\n * Setup the tab stops.\n * @param i The index to start setting up tab stops from.\n */\n public setupTabStops(i?: number): void {\n this._normal.setupTabStops(i);\n this._alt.setupTabStops(i);\n }\n}\n", "/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { Disposable } from '../Lifecycle';\nimport { IAttributeData, IBuffer, IBufferLine, IBufferSet } from '../buffer/Types';\nimport { BufferSet } from '../buffer/BufferSet';\nimport { IBufferService, ILogService, IOptionsService, type IBufferResizeEvent } from './Services';\nimport { Emitter } from '../Event';\n\nexport const enum BufferServiceConstants {\n MINIMUM_COLS = 2, // Less than 2 can mess with wide chars\n MINIMUM_ROWS = 1\n}\n\nexport class BufferService extends Disposable implements IBufferService {\n public serviceBrand: any;\n\n public cols: number;\n public rows: number;\n public buffers: IBufferSet;\n /** Whether the user is scrolling (locks the scroll position) */\n public isUserScrolling: boolean = false;\n\n private readonly _onResize = this._register(new Emitter());\n public readonly onResize = this._onResize.event;\n private readonly _onScroll = this._register(new Emitter());\n public readonly onScroll = this._onScroll.event;\n\n public get buffer(): IBuffer { return this.buffers.active; }\n\n /** An IBufferline to clone/copy from for new blank lines */\n private _cachedBlankLine: IBufferLine | undefined;\n\n constructor(\n @IOptionsService optionsService: IOptionsService,\n @ILogService logService: ILogService\n ) {\n super();\n this.cols = Math.max(optionsService.rawOptions.cols || 0, BufferServiceConstants.MINIMUM_COLS);\n this.rows = Math.max(optionsService.rawOptions.rows || 0, BufferServiceConstants.MINIMUM_ROWS);\n this.buffers = this._register(new BufferSet(optionsService, this, logService));\n this._register(this.buffers.onBufferActivate(e => {\n this._onScroll.fire(e.activeBuffer.ydisp);\n }));\n }\n\n public resize(cols: number, rows: number): void {\n const colsChanged = this.cols !== cols;\n const rowsChanged = this.rows !== rows;\n this.cols = cols;\n this.rows = rows;\n this.buffers.resize(cols, rows);\n this._onResize.fire({ cols, rows, colsChanged, rowsChanged });\n }\n\n public reset(): void {\n this.buffers.reset();\n this.isUserScrolling = false;\n }\n\n /**\n * Scroll the terminal down 1 row, creating a blank line.\n * @param eraseAttr The attribute data to use the for blank line.\n * @param isWrapped Whether the new line is wrapped from the previous line.\n */\n public scroll(eraseAttr: IAttributeData, isWrapped: boolean = false): void {\n const buffer = this.buffer;\n\n let newLine: IBufferLine | undefined;\n newLine = this._cachedBlankLine;\n if (!newLine || newLine.length !== this.cols || newLine.getFg(0) !== eraseAttr.fg || newLine.getBg(0) !== eraseAttr.bg) {\n newLine = buffer.getBlankLine(eraseAttr, isWrapped);\n this._cachedBlankLine = newLine;\n }\n newLine.isWrapped = isWrapped;\n\n const topRow = buffer.ybase + buffer.scrollTop;\n const bottomRow = buffer.ybase + buffer.scrollBottom;\n\n if (buffer.scrollTop === 0) {\n // Determine whether the buffer is going to be trimmed after insertion.\n const willBufferBeTrimmed = buffer.lines.isFull;\n\n // Insert the line using the fastest method\n if (bottomRow === buffer.lines.length - 1) {\n if (willBufferBeTrimmed) {\n buffer.lines.recycle().copyFrom(newLine, true);\n } else {\n buffer.lines.push(newLine.clone(true));\n }\n } else {\n buffer.lines.splice(bottomRow + 1, 0, newLine.clone(true));\n }\n\n // Only adjust ybase and ydisp when the buffer is not trimmed\n if (!willBufferBeTrimmed) {\n buffer.ybase++;\n // Only scroll the ydisp with ybase if the user has not scrolled up\n if (!this.isUserScrolling) {\n buffer.ydisp++;\n }\n } else {\n // When the buffer is full and the user has scrolled up, keep the text\n // stable unless ydisp is right at the top\n if (this.isUserScrolling) {\n buffer.ydisp = Math.max(buffer.ydisp - 1, 0);\n }\n }\n } else {\n // scrollTop is non-zero which means no line will be going to the\n // scrollback, instead we can just shift them in-place.\n const scrollRegionHeight = bottomRow - topRow + 1 /* as it's zero-based */;\n buffer.lines.shiftElements(topRow + 1, scrollRegionHeight - 1, -1);\n buffer.lines.set(bottomRow, newLine.clone(true));\n }\n\n // Move the viewport to the bottom of the buffer unless the user is\n // scrolling.\n if (!this.isUserScrolling) {\n buffer.ydisp = buffer.ybase;\n }\n\n this._onScroll.fire(buffer.ydisp);\n }\n\n /**\n * Scroll the display of the terminal\n * @param disp The number of lines to scroll down (negative scroll up).\n * @param suppressScrollEvent Don't emit the scroll event as scrollLines. This is used\n * to avoid unwanted events being handled by the viewport when the event was triggered from the\n * viewport originally.\n */\n public scrollLines(disp: number, suppressScrollEvent?: boolean): void {\n const buffer = this.buffer;\n if (disp < 0) {\n if (buffer.ydisp === 0) {\n return;\n }\n this.isUserScrolling = true;\n } else if (disp + buffer.ydisp >= buffer.ybase) {\n this.isUserScrolling = false;\n }\n\n const oldYdisp = buffer.ydisp;\n buffer.ydisp = Math.max(Math.min(buffer.ydisp + disp, buffer.ybase), 0);\n\n // No change occurred, don't trigger scroll/refresh\n if (oldYdisp === buffer.ydisp) {\n return;\n }\n\n if (!suppressScrollEvent) {\n this._onScroll.fire(buffer.ydisp);\n }\n }\n}\n", "/**\n * Copyright (c) 2016 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\ninterface INavigator {\n userAgent: string;\n language: string;\n platform: string;\n}\n\n// We're declaring a navigator global here as we expect it in all runtimes (node and browser), but\n// we want this module to live in common.\ndeclare const navigator: INavigator;\ndeclare const process: unknown;\n\n// navigator.userAgent is also checked here because bundling with the process module can cause\n// issues otherwise. Note that navigator exists in Node.js 21+ but the userAgent is\n// \"Node.js/\".\nexport const isNode = (typeof process !== 'undefined' && 'title' in (process as any) && (typeof navigator === 'undefined' || navigator.userAgent.startsWith('Node.js/'))) ? true : false;\nconst userAgent = (isNode) ? 'node' : navigator.userAgent;\nconst platform = (isNode) ? 'node' : navigator.platform;\n\nexport const isFirefox = userAgent.includes('Firefox');\nexport const isChrome = userAgent.includes('Chrome');\nexport const isLegacyEdge = userAgent.includes('Edge');\nexport const isSafari = /^((?!chrome|android).)*safari/i.test(userAgent);\n\ninterface IZoomWindow {\n devicePixelRatio?: number;\n}\n\nexport function getZoomFactor(_targetWindow: IZoomWindow): number {\n return 1;\n}\nexport function getSafariVersion(): number {\n if (!isSafari) {\n return 0;\n }\n const majorVersion = userAgent.match(/Version\\/(\\d+)/);\n if (majorVersion === null || majorVersion.length < 2) {\n return 0;\n }\n return parseInt(majorVersion[1], 10);\n}\n\n// Find the user's platform. We use this to interpret the meta key\n// and ISO third level shifts.\n// http://stackoverflow.com/q/19877924/577598\nexport const isMac = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'].includes(platform);\nexport const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].includes(platform);\nexport const isLinux = platform.indexOf('Linux') >= 0;\n// Note that when this is true, isLinux will also be true.\nexport const isChromeOS = /\\bCrOS\\b/.test(userAgent);\n", "/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { Disposable, toDisposable } from '../Lifecycle';\nimport { isMac } from '../Platform';\nimport { CursorStyle, IDisposable } from '../Types';\nimport { FontWeight, IOptionsService, ITerminalOptions } from './Services';\nimport { Emitter } from '../Event';\n\nexport const DEFAULT_OPTIONS: Readonly> = {\n cols: 80,\n rows: 24,\n showCursorImmediately: false,\n cursorBlink: false,\n blinkIntervalDuration: 0,\n cursorStyle: 'block',\n cursorWidth: 1,\n cursorInactiveStyle: 'outline',\n drawBoldTextInBrightColors: true,\n documentOverride: null,\n fastScrollSensitivity: 5,\n fontFamily: 'monospace',\n fontSize: 15,\n fontWeight: 'normal',\n fontWeightBold: 'bold',\n ignoreBracketedPasteMode: false,\n lineHeight: 1.0,\n letterSpacing: 0,\n linkHandler: null,\n logLevel: 'info',\n logger: null,\n scrollback: 1000,\n scrollbar: { showScrollbar: true },\n scrollOnEraseInDisplay: false,\n scrollOnUserInput: true,\n scrollSensitivity: 1,\n screenReaderMode: false,\n smoothScrollDuration: 0,\n macOptionIsMeta: false,\n macOptionClickForcesSelection: false,\n minimumContrastRatio: 1,\n mouseEventsRequireAlt: false,\n disableStdin: false,\n allowProposedApi: false,\n allowTransparency: false,\n tabStopWidth: 8,\n theme: {},\n reflowCursorLine: false,\n rescaleOverlappingGlyphs: false,\n rightClickSelectsWord: isMac,\n windowOptions: {},\n windowsPty: {},\n wordSeparator: ' ()[]{}\\',\"`',\n altClickMovesCursor: true,\n convertEol: false,\n termName: 'xterm',\n quirks: {},\n vtExtensions: {}\n};\n\nconst FONT_WEIGHT_OPTIONS: Extract[] = ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'];\n\nexport class OptionsService extends Disposable implements IOptionsService {\n public serviceBrand: any;\n\n public readonly rawOptions: Required;\n public options: Required;\n\n private readonly _onOptionChange = this._register(new Emitter());\n public readonly onOptionChange = this._onOptionChange.event;\n\n constructor(options: Partial) {\n super();\n // set the default value of each option\n const defaultOptions = { ...DEFAULT_OPTIONS };\n for (const key in options) {\n if (key in defaultOptions) {\n try {\n const newValue = options[key];\n defaultOptions[key] = this._sanitizeAndValidateOption(key, newValue);\n } catch (e) {\n console.error(e);\n }\n }\n }\n\n // set up getters and setters for each option\n this.rawOptions = defaultOptions;\n this.options = { ... defaultOptions };\n this._setupOptions();\n\n // Clear out options that could link outside xterm.js as they could easily cause an embedder\n // memory leak\n this._register(toDisposable(() => {\n this.rawOptions.linkHandler = null;\n this.rawOptions.documentOverride = null;\n }));\n }\n\n // eslint-disable-next-line @typescript-eslint/naming-convention\n public onSpecificOptionChange(key: T, listener: (value: ITerminalOptions[T]) => any): IDisposable {\n return this.onOptionChange(eventKey => {\n if (eventKey === key) {\n listener(this.rawOptions[key]);\n }\n });\n }\n\n // eslint-disable-next-line @typescript-eslint/naming-convention\n public onMultipleOptionChange(keys: (keyof ITerminalOptions)[], listener: () => any): IDisposable {\n return this.onOptionChange(eventKey => {\n if (keys.indexOf(eventKey) !== -1) {\n listener();\n }\n });\n }\n\n private _setupOptions(): void {\n const getter = (propName: string): any => {\n if (!(propName in DEFAULT_OPTIONS)) {\n throw new Error(`No option with key \"${propName}\"`);\n }\n return this.rawOptions[propName];\n };\n\n const setter = (propName: string, value: any): void => {\n if (!(propName in DEFAULT_OPTIONS)) {\n throw new Error(`No option with key \"${propName}\"`);\n }\n\n value = this._sanitizeAndValidateOption(propName, value);\n // Don't fire an option change event if they didn't change\n if (this.rawOptions[propName] !== value) {\n this.rawOptions[propName] = value;\n this._onOptionChange.fire(propName);\n }\n };\n\n for (const propName in this.rawOptions) {\n const desc = {\n get: getter.bind(this, propName),\n set: setter.bind(this, propName)\n };\n Object.defineProperty(this.options, propName, desc);\n }\n }\n\n private _sanitizeAndValidateOption(key: string, value: any): any {\n switch (key) {\n case 'cursorStyle':\n if (!value) {\n value = DEFAULT_OPTIONS[key];\n }\n if (!isCursorStyle(value)) {\n throw new Error(`\"${value}\" is not a valid value for ${key}`);\n }\n break;\n case 'wordSeparator':\n if (!value) {\n value = DEFAULT_OPTIONS[key];\n }\n break;\n case 'fontWeight':\n case 'fontWeightBold':\n if (typeof value === 'number' && 1 <= value && value <= 1000) {\n // already valid numeric value\n break;\n }\n value = FONT_WEIGHT_OPTIONS.includes(value) ? value : DEFAULT_OPTIONS[key];\n break;\n case 'blinkIntervalDuration':\n value = Math.floor(value);\n if (value < 0) {\n throw new Error(`${key} cannot be less than 0, value: ${value}`);\n }\n break;\n case 'cursorWidth':\n value = Math.floor(value);\n // Fall through for bounds check\n case 'lineHeight':\n case 'tabStopWidth':\n if (value < 1) {\n throw new Error(`${key} cannot be less than 1, value: ${value}`);\n }\n break;\n case 'minimumContrastRatio':\n value = Math.max(1, Math.min(21, Math.round(value * 10) / 10));\n break;\n case 'scrollback':\n value = Math.min(value, 4294967295);\n if (value < 0) {\n throw new Error(`${key} cannot be less than 0, value: ${value}`);\n }\n break;\n case 'fastScrollSensitivity':\n case 'scrollSensitivity':\n if (value <= 0) {\n throw new Error(`${key} cannot be less than or equal to 0, value: ${value}`);\n }\n break;\n case 'rows':\n case 'cols':\n if (!value && value !== 0) {\n throw new Error(`${key} must be numeric, value: ${value}`);\n }\n break;\n case 'windowsPty':\n value = value ?? {};\n break;\n }\n return value;\n }\n}\n\nfunction isCursorStyle(value: unknown): value is CursorStyle {\n return value === 'block' || value === 'underline' || value === 'bar';\n}\n", "/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { Disposable } from '../Lifecycle';\nimport { IDecPrivateModes, IKittyKeyboardState, IModes } from '../Types';\nimport { IBufferService, ICoreService, ILogService, IOptionsService } from './Services';\nimport { Emitter } from '../Event';\n\nconst DEFAULT_MODES: IModes = Object.freeze({\n insertMode: false\n});\n\nconst DEFAULT_DEC_PRIVATE_MODES: IDecPrivateModes = Object.freeze({\n applicationCursorKeys: false,\n applicationKeypad: false,\n bracketedPasteMode: false,\n colorSchemeUpdates: false,\n cursorBlink: undefined,\n cursorStyle: undefined,\n origin: false,\n reverseWraparound: false,\n sendFocus: false,\n synchronizedOutput: false,\n win32InputMode: false,\n wraparound: true // defaults: xterm - true, vt100 - false\n});\n\nconst DEFAULT_KITTY_KEYBOARD_STATE = (): IKittyKeyboardState => ({\n flags: 0,\n mainFlags: 0,\n altFlags: 0,\n mainStack: [],\n altStack: []\n});\n\nexport class CoreService extends Disposable implements ICoreService {\n public serviceBrand: any;\n\n public isCursorInitialized: boolean;\n public isCursorHidden: boolean = false;\n public modes: IModes;\n public decPrivateModes: IDecPrivateModes;\n public kittyKeyboard: IKittyKeyboardState;\n\n private readonly _onData = this._register(new Emitter());\n public readonly onData = this._onData.event;\n private readonly _onUserInput = this._register(new Emitter());\n public readonly onUserInput = this._onUserInput.event;\n private readonly _onBinary = this._register(new Emitter());\n public readonly onBinary = this._onBinary.event;\n private readonly _onRequestScrollToBottom = this._register(new Emitter());\n public readonly onRequestScrollToBottom = this._onRequestScrollToBottom.event;\n\n constructor(\n @IBufferService private readonly _bufferService: IBufferService,\n @ILogService private readonly _logService: ILogService,\n @IOptionsService private readonly _optionsService: IOptionsService\n ) {\n super();\n this.isCursorInitialized = _optionsService.rawOptions.showCursorImmediately ?? false;\n this.modes = structuredClone(DEFAULT_MODES);\n this.decPrivateModes = structuredClone(DEFAULT_DEC_PRIVATE_MODES);\n this.kittyKeyboard = DEFAULT_KITTY_KEYBOARD_STATE();\n }\n\n public reset(): void {\n this.modes = structuredClone(DEFAULT_MODES);\n this.decPrivateModes = structuredClone(DEFAULT_DEC_PRIVATE_MODES);\n this.kittyKeyboard = DEFAULT_KITTY_KEYBOARD_STATE();\n }\n\n public triggerDataEvent(data: string, wasUserInput: boolean = false): void {\n // Prevents all events to pty process if stdin is disabled\n if (this._optionsService.rawOptions.disableStdin) {\n return;\n }\n\n // Input is being sent to the terminal, the terminal should focus the prompt.\n const buffer = this._bufferService.buffer;\n if (wasUserInput && this._optionsService.rawOptions.scrollOnUserInput && buffer.ybase !== buffer.ydisp) {\n this._onRequestScrollToBottom.fire();\n }\n\n // Fire onUserInput so listeners can react as well (eg. clear selection)\n if (wasUserInput) {\n this._onUserInput.fire();\n }\n\n // Fire onData API\n this._logService.debug(`sending data \"${data}\"`);\n this._logService.trace(`sending data (codes)`, () => data.split('').map(e => e.charCodeAt(0)));\n this._onData.fire(data);\n }\n\n public triggerBinaryEvent(data: string): void {\n if (this._optionsService.rawOptions.disableStdin) {\n return;\n }\n this._logService.debug(`sending binary \"${data}\"`);\n this._logService.trace(`sending binary (codes)`, () => data.split('').map(e => e.charCodeAt(0)));\n this._onBinary.fire(data);\n }\n}\n", "/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\nimport { IMouseStateService } from './Services';\nimport { ICoreMouseProtocol, ICoreMouseEvent, CoreMouseEncoding, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from '../Types';\nimport { Disposable } from '../Lifecycle';\nimport { Emitter } from '../Event';\n\n/**\n * Supported default protocols.\n */\nconst DEFAULT_PROTOCOLS: { [key: string]: ICoreMouseProtocol } = {\n /**\n * NONE\n * Events: none\n * Modifiers: none\n */\n NONE: {\n events: CoreMouseEventType.NONE,\n restrict: () => false\n },\n /**\n * X10\n * Events: mousedown\n * Modifiers: none\n */\n X10: {\n events: CoreMouseEventType.DOWN,\n restrict: (e: ICoreMouseEvent) => {\n // no wheel, no move, no up\n if (e.button === CoreMouseButton.WHEEL || e.action !== CoreMouseAction.DOWN) {\n return false;\n }\n // no modifiers\n e.ctrl = false;\n e.alt = false;\n e.shift = false;\n return true;\n }\n },\n /**\n * VT200\n * Events: mousedown / mouseup / wheel\n * Modifiers: all\n */\n VT200: {\n events: CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL,\n restrict: (e: ICoreMouseEvent) => {\n // no move\n if (e.action === CoreMouseAction.MOVE) {\n return false;\n }\n return true;\n }\n },\n /**\n * DRAG\n * Events: mousedown / mouseup / wheel / mousedrag\n * Modifiers: all\n */\n DRAG: {\n events: CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL | CoreMouseEventType.DRAG,\n restrict: (e: ICoreMouseEvent) => {\n // no move without button\n if (e.action === CoreMouseAction.MOVE && e.button === CoreMouseButton.NONE) {\n return false;\n }\n return true;\n }\n },\n /**\n * ANY\n * Events: all mouse related events\n * Modifiers: all\n */\n ANY: {\n events:\n CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL\n | CoreMouseEventType.DRAG | CoreMouseEventType.MOVE,\n restrict: (e: ICoreMouseEvent) => true\n }\n};\n\nconst enum Modifiers {\n SHIFT = 4,\n ALT = 8,\n CTRL = 16\n}\n\n// helper for default encoders to generate the event code.\nfunction eventCode(e: ICoreMouseEvent, isSGR: boolean): number {\n let code = (e.ctrl ? Modifiers.CTRL : 0) | (e.shift ? Modifiers.SHIFT : 0) | (e.alt ? Modifiers.ALT : 0);\n if (e.button === CoreMouseButton.WHEEL) {\n code |= 64;\n code |= e.action;\n } else {\n code |= e.button & 3;\n if (e.button & 4) {\n code |= 64;\n }\n if (e.button & 8) {\n code |= 128;\n }\n if (e.action === CoreMouseAction.MOVE) {\n code |= CoreMouseAction.MOVE;\n } else if (e.action === CoreMouseAction.UP && !isSGR) {\n // special case - only SGR can report button on release\n // all others have to go with NONE\n code |= CoreMouseButton.NONE;\n }\n }\n return code;\n}\n\nconst S = String.fromCharCode;\n\n/**\n * Supported default encodings.\n */\nconst DEFAULT_ENCODINGS: { [key: string]: CoreMouseEncoding } = {\n /**\n * DEFAULT - CSI M Pb Px Py\n * Single byte encoding for coords and event code.\n * Can encode values up to 223 (1-based).\n */\n DEFAULT: (e: ICoreMouseEvent) => {\n const params = [eventCode(e, false) + 32, e.col + 32, e.row + 32];\n // supress mouse report if we exceed addressible range\n // Note this is handled differently by emulators\n // - xterm: sends 0;0 coords instead\n // - vte, konsole: no report\n if (params[0] > 255 || params[1] > 255 || params[2] > 255) {\n return '';\n }\n return `\\x1b[M${S(params[0])}${S(params[1])}${S(params[2])}`;\n },\n /**\n * SGR - CSI < Pb ; Px ; Py M|m\n * No encoding limitation.\n * Can report button on release and works with a well formed sequence.\n */\n SGR: (e: ICoreMouseEvent) => {\n const final = (e.action === CoreMouseAction.UP && e.button !== CoreMouseButton.WHEEL) ? 'm' : 'M';\n return `\\x1b[<${eventCode(e, true)};${e.col};${e.row}${final}`;\n },\n SGR_PIXELS: (e: ICoreMouseEvent) => {\n const final = (e.action === CoreMouseAction.UP && e.button !== CoreMouseButton.WHEEL) ? 'm' : 'M';\n return `\\x1b[<${eventCode(e, true)};${e.x};${e.y}${final}`;\n }\n};\n\n/**\n * MouseStateService\n *\n * Provides mouse tracking reports with different protocols and encodings.\n * - protocols: NONE (default), X10, VT200, DRAG, ANY\n * - encodings: DEFAULT, SGR (UTF8, URXVT removed in #2507)\n *\n * Custom protocols/encodings can be added by `addProtocol` / `addEncoding`.\n * To activate a protocol/encoding, set `activeProtocol` / `activeEncoding`.\n * Switching a protocol will send a notification event `onProtocolChange`\n * with a list of needed events to track.\n *\n * The service handles the mouse tracking state and decides whether to send\n * a tracking report to the backend based on protocol and encoding limitations.\n * To send a mouse event call `triggerMouseEvent`.\n */\nexport class MouseStateService extends Disposable implements IMouseStateService {\n public serviceBrand: any;\n\n private _protocols: { [name: string]: ICoreMouseProtocol } = {};\n private _encodings: { [name: string]: CoreMouseEncoding } = {};\n private _activeProtocol: string = '';\n private _activeEncoding: string = '';\n private _customWheelEventHandler: ((event: WheelEvent) => boolean) | undefined;\n\n private readonly _onProtocolChange = this._register(new Emitter());\n public readonly onProtocolChange = this._onProtocolChange.event;\n\n constructor() {\n super();\n\n // register default protocols and encodings\n for (const name of Object.keys(DEFAULT_PROTOCOLS)) this.addProtocol(name, DEFAULT_PROTOCOLS[name]);\n for (const name of Object.keys(DEFAULT_ENCODINGS)) this.addEncoding(name, DEFAULT_ENCODINGS[name]);\n // call reset to set defaults\n this.reset();\n }\n\n public addProtocol(name: string, protocol: ICoreMouseProtocol): void {\n this._protocols[name] = protocol;\n }\n\n public addEncoding(name: string, encoding: CoreMouseEncoding): void {\n this._encodings[name] = encoding;\n }\n\n public get activeProtocol(): string {\n return this._activeProtocol;\n }\n\n public get areMouseEventsActive(): boolean {\n return this._protocols[this._activeProtocol].events !== 0;\n }\n\n public set activeProtocol(name: string) {\n if (!this._protocols[name]) {\n throw new Error(`unknown protocol \"${name}\"`);\n }\n this._activeProtocol = name;\n this._onProtocolChange.fire(this._protocols[name].events);\n }\n\n public get activeEncoding(): string {\n return this._activeEncoding;\n }\n\n public set activeEncoding(name: string) {\n if (!this._encodings[name]) {\n throw new Error(`unknown encoding \"${name}\"`);\n }\n this._activeEncoding = name;\n }\n\n public reset(): void {\n this.activeProtocol = 'NONE';\n this.activeEncoding = 'DEFAULT';\n }\n\n public setCustomWheelEventHandler(customWheelEventHandler: ((event: WheelEvent) => boolean) | undefined): void {\n this._customWheelEventHandler = customWheelEventHandler;\n }\n\n public allowCustomWheelEvent(ev: WheelEvent): boolean {\n return this._customWheelEventHandler ? this._customWheelEventHandler(ev) !== false : true;\n }\n\n public restrictMouseEvent(e: ICoreMouseEvent): boolean {\n return this._protocols[this._activeProtocol].restrict(e);\n }\n\n public encodeMouseEvent(e: ICoreMouseEvent): string {\n return this._encodings[this._activeEncoding](e);\n }\n\n public get isDefaultEncoding(): boolean {\n return this._activeEncoding === 'DEFAULT';\n }\n\n public get isPixelEncoding(): boolean {\n return this._activeEncoding === 'SGR_PIXELS';\n }\n}\n", "/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { IUnicodeService, IUnicodeVersionProvider, UnicodeCharProperties, UnicodeCharWidth } from './Services';\nimport { Emitter } from '../Event';\n\nexport class UnicodeService implements IUnicodeService {\n public serviceBrand: any;\n\n private _providers: {[key: string]: IUnicodeVersionProvider} = Object.create(null);\n private _active: string = '';\n private _activeProvider!: IUnicodeVersionProvider;\n\n private readonly _onChange = new Emitter();\n public readonly onChange = this._onChange.event;\n\n public static extractShouldJoin(value: UnicodeCharProperties): boolean {\n return (value & 1) !== 0;\n }\n public static extractWidth(value: UnicodeCharProperties): UnicodeCharWidth {\n return ((value >> 1) & 0x3) as UnicodeCharWidth;\n }\n public static extractCharKind(value: UnicodeCharProperties): number {\n return value >> 3;\n }\n public static createPropertyValue(state: number, width: number, shouldJoin: boolean = false): UnicodeCharProperties {\n return ((state & 0xffffff) << 3) | ((width & 3) << 1) | (shouldJoin?1:0);\n }\n\n public dispose(): void {\n this._onChange.dispose();\n }\n\n public get versions(): string[] {\n return Object.keys(this._providers);\n }\n\n public get activeVersion(): string {\n return this._active;\n }\n\n public set activeVersion(version: string) {\n if (!this._providers[version]) {\n throw new Error(`unknown Unicode version \"${version}\"`);\n }\n this._active = version;\n this._activeProvider = this._providers[version];\n this._onChange.fire(version);\n }\n\n public register(provider: IUnicodeVersionProvider): void {\n this._providers[provider.version] = provider;\n if (!this._active) {\n this.activeVersion = provider.version;\n }\n }\n\n /**\n * Unicode version dependent interface.\n */\n public wcwidth(num: number): UnicodeCharWidth {\n return this._activeProvider.wcwidth(num);\n }\n\n public getStringCellWidth(s: string): number {\n let result = 0;\n let precedingInfo = 0;\n const length = s.length;\n for (let i = 0; i < length; ++i) {\n let code = s.charCodeAt(i);\n // surrogate pair first\n if (0xD800 <= code && code <= 0xDBFF) {\n if (++i >= length) {\n // this should not happen with strings retrieved from\n // Buffer.translateToString as it converts from UTF-32\n // and therefore always should contain the second part\n // for any other string we still have to handle it somehow:\n // simply treat the lonely surrogate first as a single char (UCS-2 behavior)\n return result + this.wcwidth(code);\n }\n const second = s.charCodeAt(i);\n // convert surrogate pair to high codepoint only for valid second part (UTF-16)\n // otherwise treat them independently (UCS-2 behavior)\n if (0xDC00 <= second && second <= 0xDFFF) {\n code = (code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000;\n } else {\n result += this.wcwidth(second);\n }\n }\n const currentInfo = this.charProperties(code, precedingInfo);\n let chWidth = UnicodeService.extractWidth(currentInfo);\n if (UnicodeService.extractShouldJoin(currentInfo)) {\n chWidth -= UnicodeService.extractWidth(precedingInfo);\n }\n result += chWidth;\n precedingInfo = currentInfo;\n }\n return result;\n }\n\n public charProperties(codepoint: number, preceding: UnicodeCharProperties): UnicodeCharProperties {\n return this._activeProvider.charProperties(codepoint, preceding);\n }\n}\n", "/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\nimport { IUnicodeVersionProvider, UnicodeCharProperties, UnicodeCharWidth } from '../services/Services';\nimport { UnicodeService } from '../services/UnicodeService';\n\nconst BMP_COMBINING = [\n [0x0300, 0x036F], [0x0483, 0x0486], [0x0488, 0x0489],\n [0x0591, 0x05BD], [0x05BF, 0x05BF], [0x05C1, 0x05C2],\n [0x05C4, 0x05C5], [0x05C7, 0x05C7], [0x0600, 0x0603],\n [0x0610, 0x0615], [0x064B, 0x065E], [0x0670, 0x0670],\n [0x06D6, 0x06E4], [0x06E7, 0x06E8], [0x06EA, 0x06ED],\n [0x070F, 0x070F], [0x0711, 0x0711], [0x0730, 0x074A],\n [0x07A6, 0x07B0], [0x07EB, 0x07F3], [0x0901, 0x0902],\n [0x093C, 0x093C], [0x0941, 0x0948], [0x094D, 0x094D],\n [0x0951, 0x0954], [0x0962, 0x0963], [0x0981, 0x0981],\n [0x09BC, 0x09BC], [0x09C1, 0x09C4], [0x09CD, 0x09CD],\n [0x09E2, 0x09E3], [0x0A01, 0x0A02], [0x0A3C, 0x0A3C],\n [0x0A41, 0x0A42], [0x0A47, 0x0A48], [0x0A4B, 0x0A4D],\n [0x0A70, 0x0A71], [0x0A81, 0x0A82], [0x0ABC, 0x0ABC],\n [0x0AC1, 0x0AC5], [0x0AC7, 0x0AC8], [0x0ACD, 0x0ACD],\n [0x0AE2, 0x0AE3], [0x0B01, 0x0B01], [0x0B3C, 0x0B3C],\n [0x0B3F, 0x0B3F], [0x0B41, 0x0B43], [0x0B4D, 0x0B4D],\n [0x0B56, 0x0B56], [0x0B82, 0x0B82], [0x0BC0, 0x0BC0],\n [0x0BCD, 0x0BCD], [0x0C3E, 0x0C40], [0x0C46, 0x0C48],\n [0x0C4A, 0x0C4D], [0x0C55, 0x0C56], [0x0CBC, 0x0CBC],\n [0x0CBF, 0x0CBF], [0x0CC6, 0x0CC6], [0x0CCC, 0x0CCD],\n [0x0CE2, 0x0CE3], [0x0D41, 0x0D43], [0x0D4D, 0x0D4D],\n [0x0DCA, 0x0DCA], [0x0DD2, 0x0DD4], [0x0DD6, 0x0DD6],\n [0x0E31, 0x0E31], [0x0E34, 0x0E3A], [0x0E47, 0x0E4E],\n [0x0EB1, 0x0EB1], [0x0EB4, 0x0EB9], [0x0EBB, 0x0EBC],\n [0x0EC8, 0x0ECD], [0x0F18, 0x0F19], [0x0F35, 0x0F35],\n [0x0F37, 0x0F37], [0x0F39, 0x0F39], [0x0F71, 0x0F7E],\n [0x0F80, 0x0F84], [0x0F86, 0x0F87], [0x0F90, 0x0F97],\n [0x0F99, 0x0FBC], [0x0FC6, 0x0FC6], [0x102D, 0x1030],\n [0x1032, 0x1032], [0x1036, 0x1037], [0x1039, 0x1039],\n [0x1058, 0x1059], [0x1160, 0x11FF], [0x135F, 0x135F],\n [0x1712, 0x1714], [0x1732, 0x1734], [0x1752, 0x1753],\n [0x1772, 0x1773], [0x17B4, 0x17B5], [0x17B7, 0x17BD],\n [0x17C6, 0x17C6], [0x17C9, 0x17D3], [0x17DD, 0x17DD],\n [0x180B, 0x180D], [0x18A9, 0x18A9], [0x1920, 0x1922],\n [0x1927, 0x1928], [0x1932, 0x1932], [0x1939, 0x193B],\n [0x1A17, 0x1A18], [0x1B00, 0x1B03], [0x1B34, 0x1B34],\n [0x1B36, 0x1B3A], [0x1B3C, 0x1B3C], [0x1B42, 0x1B42],\n [0x1B6B, 0x1B73], [0x1DC0, 0x1DCA], [0x1DFE, 0x1DFF],\n [0x200B, 0x200F], [0x202A, 0x202E], [0x2060, 0x2063],\n [0x206A, 0x206F], [0x20D0, 0x20EF], [0x302A, 0x302F],\n [0x3099, 0x309A], [0xA806, 0xA806], [0xA80B, 0xA80B],\n [0xA825, 0xA826], [0xFB1E, 0xFB1E], [0xFE00, 0xFE0F],\n [0xFE20, 0xFE23], [0xFEFF, 0xFEFF], [0xFFF9, 0xFFFB]\n];\nconst HIGH_COMBINING = [\n [0x10A01, 0x10A03], [0x10A05, 0x10A06], [0x10A0C, 0x10A0F],\n [0x10A38, 0x10A3A], [0x10A3F, 0x10A3F], [0x1D167, 0x1D169],\n [0x1D173, 0x1D182], [0x1D185, 0x1D18B], [0x1D1AA, 0x1D1AD],\n [0x1D242, 0x1D244], [0xE0001, 0xE0001], [0xE0020, 0xE007F],\n [0xE0100, 0xE01EF]\n];\n\n// BMP lookup table, lazy initialized during first addon loading\nlet table: Uint8Array;\n\nfunction bisearch(ucs: number, data: number[][]): boolean {\n let min = 0;\n let max = data.length - 1;\n let mid;\n if (ucs < data[0][0] || ucs > data[max][1]) {\n return false;\n }\n while (max >= min) {\n mid = (min + max) >> 1;\n if (ucs > data[mid][1]) {\n min = mid + 1;\n } else if (ucs < data[mid][0]) {\n max = mid - 1;\n } else {\n return true;\n }\n }\n return false;\n}\n\nexport class UnicodeV6 implements IUnicodeVersionProvider {\n public readonly version = '6';\n\n constructor() {\n // init lookup table once\n if (!table) {\n table = new Uint8Array(65536);\n table.fill(1);\n table[0] = 0;\n // control chars\n table.fill(0, 1, 32);\n table.fill(0, 0x7f, 0xa0);\n\n // apply wide char rules first\n // wide chars\n table.fill(2, 0x1100, 0x1160);\n table[0x2329] = 2;\n table[0x232a] = 2;\n table.fill(2, 0x2e80, 0xa4d0);\n table[0x303f] = 1; // wrongly in last line\n\n table.fill(2, 0xac00, 0xd7a4);\n table.fill(2, 0xf900, 0xfb00);\n table.fill(2, 0xfe10, 0xfe1a);\n table.fill(2, 0xfe30, 0xfe70);\n table.fill(2, 0xff00, 0xff61);\n table.fill(2, 0xffe0, 0xffe7);\n\n // apply combining last to ensure we overwrite\n // wrongly wide set chars:\n // the original algo evals combining first and falls\n // through to wide check so we simply do here the opposite\n // combining 0\n for (let r = 0; r < BMP_COMBINING.length; ++r) {\n table.fill(0, BMP_COMBINING[r][0], BMP_COMBINING[r][1] + 1);\n }\n }\n }\n\n public wcwidth(num: number): UnicodeCharWidth {\n if (num < 32) return 0;\n if (num < 127) return 1;\n if (num < 65536) return table[num] as UnicodeCharWidth;\n if (bisearch(num, HIGH_COMBINING)) return 0;\n if ((num >= 0x20000 && num <= 0x2fffd) || (num >= 0x30000 && num <= 0x3fffd)) return 2;\n return 1;\n }\n\n public charProperties(codepoint: number, preceding: UnicodeCharProperties): UnicodeCharProperties {\n let width = this.wcwidth(codepoint);\n let shouldJoin = width === 0 && preceding !== 0;\n // HACK: Ideally this file would not depend on the service which uses it\n if (shouldJoin) {\n const oldWidth = UnicodeService.extractWidth(preceding);\n if (oldWidth === 0) {\n shouldJoin = false;\n } else if (oldWidth > width) {\n width = oldWidth;\n }\n }\n return UnicodeService.createPropertyValue(0, width, shouldJoin);\n }\n}\n", "/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { ICharsetService } from './Services';\nimport { ICharset } from '../Types';\n\nexport class CharsetService implements ICharsetService {\n public serviceBrand: any;\n\n public charset: ICharset | undefined;\n public glevel: number = 0;\n\n private _charsets: (ICharset | undefined)[] = [];\n\n public get charsets(): (ICharset | undefined)[] {\n return this._charsets;\n }\n\n public reset(): void {\n this.charset = undefined;\n this._charsets = [];\n this.glevel = 0;\n }\n\n public setgLevel(g: number): void {\n this.glevel = g;\n this.charset = this._charsets[g];\n }\n\n public setgCharset(g: number, charset: ICharset | undefined): void {\n this._charsets[g] = charset;\n if (this.glevel === g) {\n this.charset = charset;\n }\n }\n}\n", "/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { CHAR_DATA_CODE_INDEX, NULL_CELL_CODE, WHITESPACE_CELL_CODE } from './buffer/Constants';\nimport { IBufferService } from './services/Services';\n\nexport function updateWindowsModeWrappedState(bufferService: IBufferService): void {\n // Winpty does not support wraparound mode which means that lines will never\n // be marked as wrapped. This causes issues for things like copying a line\n // retaining the wrapped new line characters or if consumers are listening\n // in on the data stream.\n //\n // The workaround for this is to listen to every incoming line feed and mark\n // the line as wrapped if the last character in the previous line is not a\n // space. This is certainly not without its problems, but generally on\n // Windows when text reaches the end of the terminal it's likely going to be\n // wrapped.\n const line = bufferService.buffer.lines.get(bufferService.buffer.ybase + bufferService.buffer.y - 1);\n const lastChar = line?.get(bufferService.cols - 1);\n\n const nextLine = bufferService.buffer.lines.get(bufferService.buffer.ybase + bufferService.buffer.y);\n if (nextLine && lastChar) {\n nextLine.isWrapped = (lastChar[CHAR_DATA_CODE_INDEX] !== NULL_CELL_CODE && lastChar[CHAR_DATA_CODE_INDEX] !== WHITESPACE_CELL_CODE);\n }\n}\n", "/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\nimport { IParams, ParamsArray } from './Types';\n\nconst enum Constants {\n /**\n * Max value supported for a single param/subparam (clamped to positive int32 range)\n */\n MAX_VALUE = 0x7FFFFFFF,\n /**\n * Max allowed subparams for a single sequence (hardcoded limitation)\n */\n MAX_SUBPARAMS = 256\n}\n\n/**\n * Params storage class.\n * This type is used by the parser to accumulate sequence parameters and sub parameters\n * and transmit them to the input handler actions.\n *\n * NOTES:\n * - params object for action handlers is borrowed, use `.toArray` or `.clone` to get a copy\n * - never read beyond `params.length - 1` (likely to contain arbitrary data)\n * - `.getSubParams` returns a borrowed typed array, use `.getSubParamsAll` for cloned sub params\n * - hardcoded limitations:\n * - max. value for a single (sub) param is 2^31 - 1 (greater values are clamped to that)\n * - max. 256 sub params possible\n * - negative values are not allowed beside -1 (placeholder for default value)\n *\n * About ZDM (Zero Default Mode):\n * ZDM is not orchestrated by this class. If the parser is in ZDM,\n * it should add 0 for empty params, otherwise -1. This does not apply\n * to subparams, empty subparams should always be added with -1.\n */\nexport class Params implements IParams {\n // params store and length\n public params: Int32Array;\n public length: number;\n\n // sub params store and length\n protected _subParams: Int32Array;\n protected _subParamsLength: number;\n\n // sub params offsets from param: param idx --> [start, end] offset\n private _subParamsIdx: Uint16Array;\n private _rejectDigits: boolean;\n private _rejectSubDigits: boolean;\n private _digitIsSub: boolean;\n\n /**\n * Create a `Params` type from JS array representation.\n */\n public static fromArray(values: ParamsArray): Params {\n const params = new Params();\n if (!values.length) {\n return params;\n }\n // skip leading sub params\n for (let i = (Array.isArray(values[0])) ? 1 : 0; i < values.length; ++i) {\n const value = values[i];\n if (Array.isArray(value)) {\n for (let k = 0; k < value.length; ++k) {\n params.addSubParam(value[k]);\n }\n } else {\n params.addParam(value);\n }\n }\n return params;\n }\n\n /**\n * @param maxLength max length of storable parameters\n * @param maxSubParamsLength max length of storable sub parameters\n */\n constructor(public maxLength: number = 32, public maxSubParamsLength: number = 32) {\n if (maxSubParamsLength > Constants.MAX_SUBPARAMS) {\n throw new Error('maxSubParamsLength must not be greater than 256');\n }\n this.params = new Int32Array(maxLength);\n this.length = 0;\n this._subParams = new Int32Array(maxSubParamsLength);\n this._subParamsLength = 0;\n this._subParamsIdx = new Uint16Array(maxLength);\n this._rejectDigits = false;\n this._rejectSubDigits = false;\n this._digitIsSub = false;\n }\n\n /**\n * Clone object.\n */\n public clone(): Params {\n const newParams = new Params(this.maxLength, this.maxSubParamsLength);\n newParams.params.set(this.params);\n newParams.length = this.length;\n newParams._subParams.set(this._subParams);\n newParams._subParamsLength = this._subParamsLength;\n newParams._subParamsIdx.set(this._subParamsIdx);\n newParams._rejectDigits = this._rejectDigits;\n newParams._rejectSubDigits = this._rejectSubDigits;\n newParams._digitIsSub = this._digitIsSub;\n return newParams;\n }\n\n /**\n * Get a JS array representation of the current parameters and sub parameters.\n * The array is structured as follows:\n * sequence: \"1;2:3:4;5::6\"\n * array : [1, 2, [3, 4], 5, [-1, 6]]\n */\n public toArray(): ParamsArray {\n const res: ParamsArray = [];\n for (let i = 0; i < this.length; ++i) {\n res.push(this.params[i]);\n const start = this._subParamsIdx[i] >> 8;\n const end = this._subParamsIdx[i] & 0xFF;\n if (end - start > 0) {\n res.push(Array.prototype.slice.call(this._subParams, start, end));\n }\n }\n return res;\n }\n\n /**\n * Reset to initial empty state.\n */\n public reset(): void {\n this.length = 0;\n this._subParamsLength = 0;\n this._rejectDigits = false;\n this._rejectSubDigits = false;\n this._digitIsSub = false;\n }\n\n /**\n * Reset and add 0 as first param (ZDM).\n */\n public resetZdm(): void {\n this.length = 1;\n this._subParamsLength = 0;\n this._rejectDigits = false;\n this._rejectSubDigits = false;\n this._digitIsSub = false;\n this._subParamsIdx[0] = 0;\n this.params[0] = 0;\n }\n\n /**\n * Add a parameter value.\n * `Params` only stores up to `maxLength` parameters, any later\n * parameter will be ignored.\n * Note: VT devices only stored up to 16 values, xterm seems to\n * store up to 30.\n */\n public addParam(value: number): void {\n this._digitIsSub = false;\n if (this.length >= this.maxLength) {\n this._rejectDigits = true;\n return;\n }\n if (value < -1) {\n throw new Error('values less than -1 are not allowed');\n }\n this._subParamsIdx[this.length] = this._subParamsLength << 8 | this._subParamsLength;\n this.params[this.length++] = value > Constants.MAX_VALUE ? Constants.MAX_VALUE : value;\n }\n\n /**\n * Add a sub parameter value.\n * The sub parameter is automatically associated with the last parameter value.\n * Thus it is not possible to add a subparameter without any parameter added yet.\n * `Params` only stores up to `maxSubParamsLength` sub parameters, any later\n * sub parameter will be ignored.\n */\n public addSubParam(value: number): void {\n this._digitIsSub = true;\n if (!this.length) {\n return;\n }\n if (this._rejectDigits || this._subParamsLength >= this.maxSubParamsLength) {\n this._rejectSubDigits = true;\n return;\n }\n if (value < -1) {\n throw new Error('values less than -1 are not allowed');\n }\n this._subParams[this._subParamsLength++] = value > Constants.MAX_VALUE ? Constants.MAX_VALUE : value;\n this._subParamsIdx[this.length - 1]++;\n }\n\n /**\n * Whether parameter at index `idx` has sub parameters.\n */\n public hasSubParams(idx: number): boolean {\n return ((this._subParamsIdx[idx] & 0xFF) - (this._subParamsIdx[idx] >> 8) > 0);\n }\n\n /**\n * Return sub parameters for parameter at index `idx`.\n * Note: The values are borrowed, thus you need to copy\n * the values if you need to hold them in nonlocal scope.\n */\n public getSubParams(idx: number): Int32Array | null {\n const start = this._subParamsIdx[idx] >> 8;\n const end = this._subParamsIdx[idx] & 0xFF;\n if (end - start > 0) {\n return this._subParams.subarray(start, end);\n }\n return null;\n }\n\n /**\n * Return all sub parameters as {idx: subparams} mapping.\n * Note: The values are not borrowed.\n */\n public getSubParamsAll(): {[idx: number]: Int32Array} {\n const result: {[idx: number]: Int32Array} = {};\n for (let i = 0; i < this.length; ++i) {\n const start = this._subParamsIdx[i] >> 8;\n const end = this._subParamsIdx[i] & 0xFF;\n if (end - start > 0) {\n result[i] = this._subParams.slice(start, end);\n }\n }\n return result;\n }\n\n /**\n * Add a single digit value to current parameter.\n * This is used by the parser to account digits on a char by char basis.\n */\n public addDigit(value: number): void {\n let length;\n if (this._rejectDigits\n || !(length = this._digitIsSub ? this._subParamsLength : this.length)\n || (this._digitIsSub && this._rejectSubDigits)\n ) {\n return;\n }\n\n const store = this._digitIsSub ? this._subParams : this.params;\n const cur = store[length - 1];\n store[length - 1] = ~cur ? Math.min(cur * 10 + value, Constants.MAX_VALUE) : value;\n }\n}\n", "/**\n * Copyright (c) 2026 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\n/**\n * Accumulates string data from multiple chunks without O(n\u00B2) string concatenation.\n */\nexport class StringBuilder {\n private _chunks: string[] = [];\n private _length = 0;\n\n public get length(): number {\n return this._length;\n }\n\n public reset(): void {\n this._chunks.length = 0;\n this._length = 0;\n }\n\n public append(chunk: string): void {\n this._chunks.push(chunk);\n this._length += chunk.length;\n }\n\n public toString(): string {\n return this._chunks.join('');\n }\n}\n\n/**\n * String builder that rejects payloads larger than a fixed limit.\n */\nexport class LimitedStringBuilder {\n private readonly _builder = new StringBuilder();\n\n constructor(private readonly _limit: number) { }\n\n public get length(): number {\n return this._builder.length;\n }\n\n public get limit(): number {\n return this._limit;\n }\n\n public reset(): void {\n this._builder.reset();\n }\n\n /**\n * @returns true if the limit was exceeded (buffer is cleared in that case)\n */\n public append(chunk: string): boolean {\n this._builder.append(chunk);\n if (this._builder.length > this._limit) {\n this._builder.reset();\n return true;\n }\n return false;\n }\n\n public toString(): string {\n return this._builder.toString();\n }\n}\n", "/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { IOscHandler, IHandlerCollection, OscFallbackHandlerType, IOscParser, ISubParserStackState } from './Types';\nimport { OscState, ParserConstants } from './Constants';\nimport { utf32ToString } from '../input/TextDecoder';\nimport { IDisposable } from '../Types';\nimport { LimitedStringBuilder } from '../StringBuilder';\n\nconst EMPTY_HANDLERS: IOscHandler[] = [];\n\nexport class OscParser implements IOscParser {\n private _state = OscState.START;\n private _active = EMPTY_HANDLERS;\n private _id = -1;\n private _handlers: IHandlerCollection = Object.create(null);\n private _handlerFb: OscFallbackHandlerType = () => { };\n private _stack: ISubParserStackState = {\n paused: false,\n loopPosition: 0,\n fallThrough: false\n };\n\n public registerHandler(ident: number, handler: IOscHandler): IDisposable {\n this._handlers[ident] ??= [];\n const handlerList = this._handlers[ident];\n handlerList.push(handler);\n return {\n dispose: () => {\n const handlerIndex = handlerList.indexOf(handler);\n if (handlerIndex !== -1) {\n handlerList.splice(handlerIndex, 1);\n }\n }\n };\n }\n public clearHandler(ident: number): void {\n if (this._handlers[ident]) delete this._handlers[ident];\n }\n public setHandlerFallback(handler: OscFallbackHandlerType): void {\n this._handlerFb = handler;\n }\n\n public dispose(): void {\n this._handlers = Object.create(null);\n this._handlerFb = () => { };\n this._active = EMPTY_HANDLERS;\n }\n\n public reset(): void {\n // force cleanup handlers if payload was already sent\n if (this._state === OscState.PAYLOAD) {\n for (let j = this._stack.paused ? this._stack.loopPosition - 1 : this._active.length - 1; j >= 0; --j) {\n this._active[j].end(false);\n }\n }\n this._stack.paused = false;\n this._active = EMPTY_HANDLERS;\n this._id = -1;\n this._state = OscState.START;\n }\n\n private _start(): void {\n this._active = this._handlers[this._id] || EMPTY_HANDLERS;\n if (!this._active.length) {\n this._handlerFb(this._id, 'START');\n } else {\n for (let j = this._active.length - 1; j >= 0; j--) {\n this._active[j].start();\n }\n }\n }\n\n private _put(data: Uint32Array, start: number, end: number): void {\n if (!this._active.length) {\n this._handlerFb(this._id, 'PUT', utf32ToString(data, start, end));\n } else {\n for (let j = this._active.length - 1; j >= 0; j--) {\n this._active[j].put(data, start, end);\n }\n }\n }\n\n public start(): void {\n // always reset leftover handlers\n this.reset();\n this._state = OscState.ID;\n }\n\n /**\n * Put data to current OSC command.\n * Expects the identifier of the OSC command in the form\n * OSC id ; payload ST/BEL\n * Payload chunks are not further processed and get\n * directly passed to the handlers.\n */\n public put(data: Uint32Array, start: number, end: number): void {\n if (this._state === OscState.ABORT) {\n return;\n }\n if (this._state === OscState.ID) {\n while (start < end) {\n const code = data[start++];\n if (code === 0x3b) {\n this._state = OscState.PAYLOAD;\n this._start();\n break;\n }\n if (code < 0x30 || 0x39 < code) {\n this._state = OscState.ABORT;\n return;\n }\n if (this._id === -1) {\n this._id = 0;\n }\n this._id = this._id * 10 + code - 48;\n }\n }\n if (this._state === OscState.PAYLOAD && end - start > 0) {\n this._put(data, start, end);\n }\n }\n\n /**\n * Indicates end of an OSC command.\n * Whether the OSC got aborted or finished normally\n * is indicated by `success`.\n */\n public end(success: boolean, promiseResult: boolean = true): void | Promise {\n if (this._state === OscState.START) {\n return;\n }\n // do nothing if command was faulty\n if (this._state !== OscState.ABORT) {\n // if we are still in ID state and get an early end\n // means that the command has no payload thus we still have\n // to announce START and send END right after\n if (this._state === OscState.ID) {\n this._start();\n }\n\n if (!this._active.length) {\n this._handlerFb(this._id, 'END', success);\n } else {\n let handlerResult: boolean | Promise = false;\n let j = this._active.length - 1;\n let fallThrough = false;\n if (this._stack.paused) {\n j = this._stack.loopPosition - 1;\n handlerResult = promiseResult;\n fallThrough = this._stack.fallThrough;\n this._stack.paused = false;\n }\n if (!fallThrough && handlerResult === false) {\n for (; j >= 0; j--) {\n handlerResult = this._active[j].end(success);\n if (handlerResult === true) {\n break;\n } else if (handlerResult instanceof Promise) {\n this._stack.paused = true;\n this._stack.loopPosition = j;\n this._stack.fallThrough = false;\n return handlerResult;\n }\n }\n j--;\n }\n // cleanup left over handlers\n // we always have to call .end for proper cleanup,\n // here we use `success` to indicate whether a handler should execute\n for (; j >= 0; j--) {\n handlerResult = this._active[j].end(false);\n if (handlerResult instanceof Promise) {\n this._stack.paused = true;\n this._stack.loopPosition = j;\n this._stack.fallThrough = true;\n return handlerResult;\n }\n }\n }\n\n }\n this._active = EMPTY_HANDLERS;\n this._id = -1;\n this._state = OscState.START;\n }\n}\n\n/**\n * Convenient class to allow attaching string based handler functions\n * as OSC handlers.\n */\nexport class OscHandler implements IOscHandler {\n private static _payloadLimit = ParserConstants.PAYLOAD_LIMIT;\n\n private _data = new LimitedStringBuilder(OscHandler._payloadLimit);\n private _hitLimit: boolean = false;\n\n constructor(private _handler: (data: string) => boolean | Promise) { }\n\n public start(): void {\n this._data.reset();\n this._hitLimit = false;\n }\n\n public put(data: Uint32Array, start: number, end: number): void {\n if (this._hitLimit) {\n return;\n }\n if (this._data.append(utf32ToString(data, start, end))) {\n this._hitLimit = true;\n }\n }\n\n public end(success: boolean): boolean | Promise {\n let ret: boolean | Promise = false;\n if (this._hitLimit) {\n ret = false;\n } else if (success) {\n ret = this._handler(this._data.toString());\n if (ret instanceof Promise) {\n // need to hold data until `ret` got resolved\n // dont care for errors, data will be freed anyway on next start\n return ret.then(res => {\n this._data.reset();\n this._hitLimit = false;\n return res;\n });\n }\n }\n this._data.reset();\n this._hitLimit = false;\n return ret;\n }\n}\n", "/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { IDisposable } from '../Types';\nimport { IDcsHandler, IParams, IHandlerCollection, IDcsParser, DcsFallbackHandlerType, ISubParserStackState } from './Types';\nimport { utf32ToString } from '../input/TextDecoder';\nimport { Params } from './Params';\nimport { ParserConstants } from './Constants';\nimport { LimitedStringBuilder } from '../StringBuilder';\n\nconst EMPTY_HANDLERS: IDcsHandler[] = [];\n\nexport class DcsParser implements IDcsParser {\n private _handlers: IHandlerCollection = Object.create(null);\n private _active: IDcsHandler[] = EMPTY_HANDLERS;\n private _ident: number = 0;\n private _handlerFb: DcsFallbackHandlerType = () => { };\n private _stack: ISubParserStackState = {\n paused: false,\n loopPosition: 0,\n fallThrough: false\n };\n\n public dispose(): void {\n this._handlers = Object.create(null);\n this._handlerFb = () => { };\n this._active = EMPTY_HANDLERS;\n }\n\n public registerHandler(ident: number, handler: IDcsHandler): IDisposable {\n this._handlers[ident] ??= [];\n const handlerList = this._handlers[ident];\n handlerList.push(handler);\n return {\n dispose: () => {\n const handlerIndex = handlerList.indexOf(handler);\n if (handlerIndex !== -1) {\n handlerList.splice(handlerIndex, 1);\n }\n }\n };\n }\n\n public clearHandler(ident: number): void {\n if (this._handlers[ident]) delete this._handlers[ident];\n }\n\n public setHandlerFallback(handler: DcsFallbackHandlerType): void {\n this._handlerFb = handler;\n }\n\n public reset(): void {\n // force cleanup leftover handlers\n if (this._active.length) {\n for (let j = this._stack.paused ? this._stack.loopPosition - 1 : this._active.length - 1; j >= 0; --j) {\n this._active[j].unhook(false);\n }\n }\n this._stack.paused = false;\n this._active = EMPTY_HANDLERS;\n this._ident = 0;\n }\n\n public hook(ident: number, params: IParams): void {\n // always reset leftover handlers\n this.reset();\n this._ident = ident;\n this._active = this._handlers[ident] || EMPTY_HANDLERS;\n if (!this._active.length) {\n this._handlerFb(this._ident, 'HOOK', params);\n } else {\n for (let j = this._active.length - 1; j >= 0; j--) {\n this._active[j].hook(params);\n }\n }\n }\n\n public put(data: Uint32Array, start: number, end: number): void {\n if (!this._active.length) {\n this._handlerFb(this._ident, 'PUT', utf32ToString(data, start, end));\n } else {\n for (let j = this._active.length - 1; j >= 0; j--) {\n this._active[j].put(data, start, end);\n }\n }\n }\n\n public unhook(success: boolean, promiseResult: boolean = true): void | Promise {\n if (!this._active.length) {\n this._handlerFb(this._ident, 'UNHOOK', success);\n } else {\n let handlerResult: boolean | Promise = false;\n let j = this._active.length - 1;\n let fallThrough = false;\n if (this._stack.paused) {\n j = this._stack.loopPosition - 1;\n handlerResult = promiseResult;\n fallThrough = this._stack.fallThrough;\n this._stack.paused = false;\n }\n if (!fallThrough && handlerResult === false) {\n for (; j >= 0; j--) {\n handlerResult = this._active[j].unhook(success);\n if (handlerResult === true) {\n break;\n } else if (handlerResult instanceof Promise) {\n this._stack.paused = true;\n this._stack.loopPosition = j;\n this._stack.fallThrough = false;\n return handlerResult;\n }\n }\n j--;\n }\n // cleanup left over handlers (fallThrough for async)\n for (; j >= 0; j--) {\n handlerResult = this._active[j].unhook(false);\n if (handlerResult instanceof Promise) {\n this._stack.paused = true;\n this._stack.loopPosition = j;\n this._stack.fallThrough = true;\n return handlerResult;\n }\n }\n }\n this._active = EMPTY_HANDLERS;\n this._ident = 0;\n }\n}\n\n// predefine empty params as [0] (ZDM)\nconst EMPTY_PARAMS = new Params();\nEMPTY_PARAMS.addParam(0);\n\n/**\n * Convenient class to create a DCS handler from a single callback function.\n * Note: The payload is currently limited to 50 MB (hardcoded).\n */\nexport class DcsHandler implements IDcsHandler {\n private static _payloadLimit = ParserConstants.PAYLOAD_LIMIT;\n\n private _data = new LimitedStringBuilder(DcsHandler._payloadLimit);\n private _params: IParams = EMPTY_PARAMS;\n private _hitLimit: boolean = false;\n\n constructor(private _handler: (data: string, params: IParams) => boolean | Promise) { }\n\n public hook(params: IParams): void {\n // since we need to preserve params until `unhook`, we have to clone it\n // (only borrowed from parser and spans multiple parser states)\n // perf optimization:\n // clone only, if we have non empty params, otherwise stick with default\n this._params = (params.length > 1 || params.params[0]) ? params.clone() : EMPTY_PARAMS;\n this._data.reset();\n this._hitLimit = false;\n }\n\n public put(data: Uint32Array, start: number, end: number): void {\n if (this._hitLimit) {\n return;\n }\n if (this._data.append(utf32ToString(data, start, end))) {\n this._hitLimit = true;\n }\n }\n\n public unhook(success: boolean): boolean | Promise {\n let ret: boolean | Promise = false;\n if (this._hitLimit) {\n ret = false;\n } else if (success) {\n ret = this._handler(this._data.toString(), this._params);\n if (ret instanceof Promise) {\n // need to hold data and params until `ret` got resolved\n // dont care for errors, data will be freed anyway on next start\n return ret.then(res => {\n this._params = EMPTY_PARAMS;\n this._data.reset();\n this._hitLimit = false;\n return res;\n });\n }\n }\n this._params = EMPTY_PARAMS;\n this._data.reset();\n this._hitLimit = false;\n return ret;\n }\n}\n", "/**\n * Copyright (c) 2025 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { IApcHandler, IHandlerCollection, ApcFallbackHandlerType, IApcParser, ISubParserStackState } from './Types';\nimport { ParserConstants } from './Constants';\nimport { utf32ToString } from '../input/TextDecoder';\nimport { IDisposable } from '../Types';\nimport { LimitedStringBuilder } from '../StringBuilder';\n\nconst EMPTY_HANDLERS: IApcHandler[] = [];\n\n/**\n * APC Parser for handling Application Program Command sequences.\n * APC sequences use the format: ESC _ ESC \\\n *\n * Unlike OSC which uses numeric identifiers (e.g., OSC 1337),\n * APC uses the first character as the identifier (e.g., 'G' for Kitty graphics).\n * The identifier is the character code of the first byte after ESC _.\n */\nexport class ApcParser implements IApcParser {\n private _handlers: IHandlerCollection = Object.create(null);\n private _active = EMPTY_HANDLERS;\n private _ident: number = 0;\n private _handlerFb: ApcFallbackHandlerType = () => { };\n private _stack: ISubParserStackState = {\n paused: false,\n loopPosition: 0,\n fallThrough: false\n };\n\n /**\n * Register an APC handler for a specific identifier.\n * @param ident The character code of the first byte (e.g., 0x47 for 'G')\n * @param handler The handler to register\n */\n public registerHandler(ident: number, handler: IApcHandler): IDisposable {\n this._handlers[ident] ??= [];\n const handlerList = this._handlers[ident];\n handlerList.push(handler);\n return {\n dispose: () => {\n const handlerIndex = handlerList.indexOf(handler);\n if (handlerIndex !== -1) {\n handlerList.splice(handlerIndex, 1);\n }\n }\n };\n }\n\n public clearHandler(ident: number): void {\n if (this._handlers[ident]) delete this._handlers[ident];\n }\n\n public setHandlerFallback(handler: ApcFallbackHandlerType): void {\n this._handlerFb = handler;\n }\n\n public dispose(): void {\n this._handlers = Object.create(null);\n this._handlerFb = () => { };\n this._active = EMPTY_HANDLERS;\n }\n\n public reset(): void {\n // force cleanup handlers\n if (this._active.length) {\n for (let j = this._stack.paused ? this._stack.loopPosition - 1 : this._active.length - 1; j >= 0; --j) {\n this._active[j].end(false);\n }\n }\n this._stack.paused = false;\n this._active = EMPTY_HANDLERS;\n this._ident = 0;\n }\n\n public start(ident: number): void {\n // always reset leftover handlers\n this.reset();\n this._ident = ident;\n this._active = this._handlers[ident] || EMPTY_HANDLERS;\n if (!this._active.length) {\n this._handlerFb(this._ident, 'START');\n } else {\n for (let j = this._active.length - 1; j >= 0; j--) {\n this._active[j].start();\n }\n }\n }\n\n public put(data: Uint32Array, start: number, end: number): void {\n if (!this._active.length) {\n this._handlerFb(this._ident, 'PUT', utf32ToString(data, start, end));\n } else {\n for (let j = this._active.length - 1; j >= 0; j--) {\n this._active[j].put(data, start, end);\n }\n }\n }\n\n /**\n * Indicates end of an APC command.\n * Whether the APC got aborted or finished normally\n * is indicated by `success`.\n */\n public end(success: boolean, promiseResult: boolean = true): void | Promise {\n if (!this._active.length) {\n this._handlerFb(this._ident, 'END', success);\n } else {\n let handlerResult: boolean | Promise = false;\n let j = this._active.length - 1;\n let fallThrough = false;\n if (this._stack.paused) {\n j = this._stack.loopPosition - 1;\n handlerResult = promiseResult;\n fallThrough = this._stack.fallThrough;\n this._stack.paused = false;\n }\n if (!fallThrough && handlerResult === false) {\n for (; j >= 0; j--) {\n handlerResult = this._active[j].end(success);\n if (handlerResult === true) {\n break;\n } else if (handlerResult instanceof Promise) {\n this._stack.paused = true;\n this._stack.loopPosition = j;\n this._stack.fallThrough = false;\n return handlerResult;\n }\n }\n j--;\n }\n // cleanup left over handlers (fallThrough for async)\n for (; j >= 0; j--) {\n handlerResult = this._active[j].end(false);\n if (handlerResult instanceof Promise) {\n this._stack.paused = true;\n this._stack.loopPosition = j;\n this._stack.fallThrough = true;\n return handlerResult;\n }\n }\n }\n this._active = EMPTY_HANDLERS;\n this._ident = 0;\n }\n}\n\n/**\n * Convenient class to allow attaching string based handler functions\n * as APC handlers.\n */\nexport class ApcHandler implements IApcHandler {\n private static _payloadLimit = ParserConstants.PAYLOAD_LIMIT;\n\n private _data = new LimitedStringBuilder(ApcHandler._payloadLimit);\n private _hitLimit: boolean = false;\n\n constructor(private _handler: (data: string) => boolean | Promise) { }\n\n public start(): void {\n this._data.reset();\n this._hitLimit = false;\n }\n\n public put(data: Uint32Array, start: number, end: number): void {\n if (this._hitLimit) {\n return;\n }\n if (this._data.append(utf32ToString(data, start, end))) {\n this._hitLimit = true;\n }\n }\n\n public end(success: boolean): boolean | Promise {\n let ret: boolean | Promise = false;\n if (this._hitLimit) {\n ret = false;\n } else if (success) {\n ret = this._handler(this._data.toString());\n if (ret instanceof Promise) {\n // need to hold data until `ret` got resolved\n // dont care for errors, data will be freed anyway on next start\n return ret.then(res => {\n this._data.reset();\n this._hitLimit = false;\n return res;\n });\n }\n }\n this._data.reset();\n this._hitLimit = false;\n return ret;\n }\n}\n", "/**\n * Copyright (c) 2018 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandlerType, OscFallbackHandlerType, IOscParser, EscHandlerType, IDcsParser, DcsFallbackHandlerType, IFunctionIdentifier, ExecuteFallbackHandlerType, CsiFallbackHandlerType, EscFallbackHandlerType, PrintHandlerType, PrintFallbackHandlerType, ExecuteHandlerType, IParserStackState, ParserStackType, ResumableHandlersType, IApcHandler, IApcParser, ApcFallbackHandlerType } from './Types';\nimport { ParserState, ParserAction } from './Constants';\nimport { Disposable, toDisposable } from '../Lifecycle';\nimport { IDisposable } from '../Types';\nimport { Params } from './Params';\nimport { OscParser } from './OscParser';\nimport { DcsParser } from './DcsParser';\nimport { ApcParser } from './ApcParser';\n\n/**\n * VT commands done by the parser\n */\n// @vt: #Y ESC CSI \"Control Sequence Introducer\" \"ESC [\" \"Start of a CSI sequence.\"\n// @vt: #Y ESC OSC \"Operating System Command\" \"ESC ]\" \"Start of an OSC sequence.\"\n// @vt: #Y ESC DCS \"Device Control String\" \"ESC P\" \"Start of a DCS sequence.\"\n// @vt: #Y ESC ST \"String Terminator\" \"ESC \\\\\" \"Terminator used for string type sequences.\"\n// @vt: #Y ESC PM \"Privacy Message\" \"ESC ^\" \"Start of a privacy message.\"\n// @vt: #Y ESC APC \"Application Program Command\" \"ESC _\" \"Start of an APC sequence.\"\n// @vt: #Y C1 CSI \"Control Sequence Introducer\" \"\\x9B\" \"Start of a CSI sequence.\"\n// @vt: #Y C1 OSC \"Operating System Command\" \"\\x9D\" \"Start of an OSC sequence.\"\n// @vt: #Y C1 DCS \"Device Control String\" \"\\x90\" \"Start of a DCS sequence.\"\n// @vt: #Y C1 ST \"String Terminator\" \"\\x9C\" \"Terminator used for string type sequences.\"\n// @vt: #Y C1 PM \"Privacy Message\" \"\\x9E\" \"Start of a privacy message.\"\n// @vt: #Y C1 APC \"Application Program Command\" \"\\x9F\" \"Start of an APC sequence.\"\n// @vt: #Y C0 NUL \"Null\" \"\\0, \\x00\" \"NUL is ignored.\"\n// @vt: #Y C0 ESC \"Escape\" \"\\e, \\x1B\" \"Start of a sequence. Cancels any other sequence.\"\n\n/**\n * Table values are generated like this:\n * index: currentState << TableValue.INDEX_STATE_SHIFT | charCode\n * value: action << TableValue.TRANSITION_ACTION_SHIFT | nextState\n */\nconst enum TableAccess {\n TRANSITION_ACTION_SHIFT = 8,\n TRANSITION_STATE_MASK = 255,\n INDEX_STATE_SHIFT = 8\n}\n\n/**\n * Transition table for EscapeSequenceParser.\n */\nexport class TransitionTable {\n public table: Uint16Array;\n\n constructor(length: number) {\n this.table = new Uint16Array(length);\n }\n\n /**\n * Set default transition.\n * @param action default action\n * @param next default next state\n */\n public setDefault(action: ParserAction, next: ParserState): void {\n this.table.fill(action << TableAccess.TRANSITION_ACTION_SHIFT | next);\n }\n\n /**\n * Add a transition to the transition table.\n * @param code input character code\n * @param state current parser state\n * @param action parser action to be done\n * @param next next parser state\n */\n public add(code: number, state: ParserState, action: ParserAction, next: ParserState): void {\n this.table[state << TableAccess.INDEX_STATE_SHIFT | code] = action << TableAccess.TRANSITION_ACTION_SHIFT | next;\n }\n\n /**\n * Add transitions for multiple input character codes.\n * @param codes input character code array\n * @param state current parser state\n * @param action parser action to be done\n * @param next next parser state\n */\n public addMany(codes: number[], state: ParserState, action: ParserAction, next: ParserState): void {\n for (let i = 0; i < codes.length; i++) {\n this.table[state << TableAccess.INDEX_STATE_SHIFT | codes[i]] = action << TableAccess.TRANSITION_ACTION_SHIFT | next;\n }\n }\n}\n\n\n// Pseudo-character placeholder for printable non-ascii characters (unicode).\nconst NON_ASCII_PRINTABLE = 0xA0;\n\n\n/**\n * VT500 compatible transition table.\n * Taken from https://vt100.net/emu/dec_ansi_parser.\n */\nexport const VT500_TRANSITION_TABLE = (function (): TransitionTable {\n // table size:\n // (ParserState.STATE_LENGTH - 1) << TableAccess.INDEX_STATE_SHIFT | NON_ASCII_PRINTABLE + 1\n const table: TransitionTable = new TransitionTable(4257);\n\n // range macro for byte\n const BYTE_VALUES = 256;\n const blueprint = Array.apply(null, Array(BYTE_VALUES)).map((unused: any, i: number) => i);\n const r = (start: number, end: number): number[] => blueprint.slice(start, end);\n\n // Default definitions.\n const PRINTABLES = r(0x20, 0x7f); // 0x20 (SP) included, 0x7F (DEL) excluded\n const EXECUTABLES = r(0x00, 0x18);\n EXECUTABLES.push(0x19);\n EXECUTABLES.push.apply(EXECUTABLES, r(0x1c, 0x20));\n\n const states: number[] = r(ParserState.GROUND, ParserState.STATE_LENGTH);\n\n // set default transition\n table.setDefault(ParserAction.ERROR, ParserState.GROUND);\n // printables\n table.addMany(PRINTABLES, ParserState.GROUND, ParserAction.PRINT, ParserState.GROUND);\n // global anywhere rules\n for (const state of states) {\n table.addMany([0x18, 0x1a, 0x99, 0x9a], state, ParserAction.EXECUTE, ParserState.GROUND);\n table.addMany(r(0x80, 0x90), state, ParserAction.EXECUTE, ParserState.GROUND);\n table.addMany(r(0x90, 0x98), state, ParserAction.EXECUTE, ParserState.GROUND);\n table.add(0x9c, state, ParserAction.IGNORE, ParserState.GROUND); // ST as terminator\n table.add(0x1b, state, ParserAction.CLEAR, ParserState.ESCAPE); // ESC\n table.add(0x9d, state, ParserAction.OSC_START, ParserState.OSC_STRING); // OSC\n table.addMany([0x98, 0x9e], state, ParserAction.IGNORE, ParserState.SOS_PM_STRING); // SOS, PM\n table.add(0x9f, state, ParserAction.CLEAR, ParserState.APC_ENTRY); // APC\n table.add(0x9b, state, ParserAction.CLEAR, ParserState.CSI_ENTRY); // CSI\n table.add(0x90, state, ParserAction.CLEAR, ParserState.DCS_ENTRY); // DCS\n }\n // rules for executables and 7f\n table.addMany(EXECUTABLES, ParserState.GROUND, ParserAction.EXECUTE, ParserState.GROUND);\n table.addMany(EXECUTABLES, ParserState.ESCAPE, ParserAction.EXECUTE, ParserState.ESCAPE);\n table.add(0x7f, ParserState.ESCAPE, ParserAction.IGNORE, ParserState.ESCAPE);\n table.addMany(EXECUTABLES, ParserState.OSC_STRING, ParserAction.IGNORE, ParserState.OSC_STRING);\n table.addMany(EXECUTABLES, ParserState.CSI_ENTRY, ParserAction.EXECUTE, ParserState.CSI_ENTRY);\n table.add(0x7f, ParserState.CSI_ENTRY, ParserAction.IGNORE, ParserState.CSI_ENTRY);\n table.addMany(EXECUTABLES, ParserState.CSI_PARAM, ParserAction.EXECUTE, ParserState.CSI_PARAM);\n table.add(0x7f, ParserState.CSI_PARAM, ParserAction.IGNORE, ParserState.CSI_PARAM);\n table.addMany(EXECUTABLES, ParserState.CSI_IGNORE, ParserAction.EXECUTE, ParserState.CSI_IGNORE);\n table.addMany(EXECUTABLES, ParserState.CSI_INTERMEDIATE, ParserAction.EXECUTE, ParserState.CSI_INTERMEDIATE);\n table.add(0x7f, ParserState.CSI_INTERMEDIATE, ParserAction.IGNORE, ParserState.CSI_INTERMEDIATE);\n table.addMany(EXECUTABLES, ParserState.ESCAPE_INTERMEDIATE, ParserAction.EXECUTE, ParserState.ESCAPE_INTERMEDIATE);\n table.add(0x7f, ParserState.ESCAPE_INTERMEDIATE, ParserAction.IGNORE, ParserState.ESCAPE_INTERMEDIATE);\n // osc\n table.add(0x5d, ParserState.ESCAPE, ParserAction.OSC_START, ParserState.OSC_STRING);\n table.addMany(PRINTABLES, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING);\n table.add(0x7f, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING);\n table.addMany([0x9c, 0x1b, 0x18, 0x1a, 0x07], ParserState.OSC_STRING, ParserAction.OSC_END, ParserState.GROUND);\n table.addMany(r(0x1c, 0x20), ParserState.OSC_STRING, ParserAction.IGNORE, ParserState.OSC_STRING);\n // sos/pm\n table.addMany([0x58, 0x5e], ParserState.ESCAPE, ParserAction.IGNORE, ParserState.SOS_PM_STRING);\n table.addMany(PRINTABLES, ParserState.SOS_PM_STRING, ParserAction.IGNORE, ParserState.SOS_PM_STRING);\n table.addMany(EXECUTABLES, ParserState.SOS_PM_STRING, ParserAction.IGNORE, ParserState.SOS_PM_STRING);\n table.add(0x9c, ParserState.SOS_PM_STRING, ParserAction.IGNORE, ParserState.GROUND);\n table.add(0x7f, ParserState.SOS_PM_STRING, ParserAction.IGNORE, ParserState.SOS_PM_STRING);\n // apc\n table.add(0x5f, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.APC_ENTRY);\n table.addMany(EXECUTABLES, ParserState.APC_ENTRY, ParserAction.IGNORE, ParserState.APC_ENTRY);\n table.add(0x7f, ParserState.APC_ENTRY, ParserAction.IGNORE, ParserState.APC_ENTRY);\n table.addMany(r(0x20, 0x30), ParserState.APC_ENTRY, ParserAction.COLLECT, ParserState.APC_INTERMEDIATE);\n table.addMany(r(0x30, 0x7f), ParserState.APC_ENTRY, ParserAction.APC_START, ParserState.APC_PASSTHROUGH);\n table.addMany(r(0x30, 0x7f), ParserState.APC_INTERMEDIATE, ParserAction.APC_START, ParserState.APC_PASSTHROUGH);\n table.addMany(EXECUTABLES, ParserState.APC_INTERMEDIATE, ParserAction.IGNORE, ParserState.APC_INTERMEDIATE);\n table.addMany(r(0x20, 0x30), ParserState.APC_INTERMEDIATE, ParserAction.COLLECT, ParserState.APC_INTERMEDIATE);\n table.add(0x7f, ParserState.APC_INTERMEDIATE, ParserAction.IGNORE, ParserState.APC_INTERMEDIATE);\n table.addMany(PRINTABLES, ParserState.APC_PASSTHROUGH, ParserAction.APC_PUT, ParserState.APC_PASSTHROUGH);\n table.addMany(EXECUTABLES, ParserState.APC_PASSTHROUGH, ParserAction.IGNORE, ParserState.APC_PASSTHROUGH);\n table.addMany(r(0x08, 0x0e), ParserState.APC_PASSTHROUGH, ParserAction.APC_PUT, ParserState.APC_PASSTHROUGH);\n table.add(0x7f, ParserState.APC_PASSTHROUGH, ParserAction.IGNORE, ParserState.APC_PASSTHROUGH);\n table.addMany([0x1b, 0x9c, 0x18, 0x1a], ParserState.APC_PASSTHROUGH, ParserAction.APC_END, ParserState.GROUND);\n // csi entries\n table.add(0x5b, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.CSI_ENTRY);\n table.addMany(r(0x40, 0x7f), ParserState.CSI_ENTRY, ParserAction.CSI_DISPATCH, ParserState.GROUND);\n table.addMany(r(0x30, 0x3c), ParserState.CSI_ENTRY, ParserAction.PARAM, ParserState.CSI_PARAM);\n table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.CSI_ENTRY, ParserAction.COLLECT, ParserState.CSI_PARAM);\n table.addMany(r(0x30, 0x3c), ParserState.CSI_PARAM, ParserAction.PARAM, ParserState.CSI_PARAM);\n table.addMany(r(0x40, 0x7f), ParserState.CSI_PARAM, ParserAction.CSI_DISPATCH, ParserState.GROUND);\n table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.CSI_PARAM, ParserAction.IGNORE, ParserState.CSI_IGNORE);\n table.addMany(r(0x20, 0x40), ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE);\n table.add(0x7f, ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE);\n table.addMany(r(0x40, 0x7f), ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.GROUND);\n table.addMany(r(0x20, 0x30), ParserState.CSI_ENTRY, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE);\n table.addMany(r(0x20, 0x30), ParserState.CSI_INTERMEDIATE, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE);\n table.addMany(r(0x30, 0x40), ParserState.CSI_INTERMEDIATE, ParserAction.IGNORE, ParserState.CSI_IGNORE);\n table.addMany(r(0x40, 0x7f), ParserState.CSI_INTERMEDIATE, ParserAction.CSI_DISPATCH, ParserState.GROUND);\n table.addMany(r(0x20, 0x30), ParserState.CSI_PARAM, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE);\n // esc_intermediate\n table.addMany(r(0x20, 0x30), ParserState.ESCAPE, ParserAction.COLLECT, ParserState.ESCAPE_INTERMEDIATE);\n table.addMany(r(0x20, 0x30), ParserState.ESCAPE_INTERMEDIATE, ParserAction.COLLECT, ParserState.ESCAPE_INTERMEDIATE);\n table.addMany(r(0x30, 0x7f), ParserState.ESCAPE_INTERMEDIATE, ParserAction.ESC_DISPATCH, ParserState.GROUND);\n table.addMany(r(0x30, 0x50), ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND);\n table.addMany(r(0x51, 0x58), ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND);\n table.addMany([0x59, 0x5a, 0x5c], ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND);\n table.addMany(r(0x60, 0x7f), ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND);\n // dcs entry\n table.add(0x50, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.DCS_ENTRY);\n table.addMany(EXECUTABLES, ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY);\n table.add(0x7f, ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY);\n table.addMany(r(0x20, 0x30), ParserState.DCS_ENTRY, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE);\n table.addMany(r(0x30, 0x3c), ParserState.DCS_ENTRY, ParserAction.PARAM, ParserState.DCS_PARAM);\n table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.DCS_ENTRY, ParserAction.COLLECT, ParserState.DCS_PARAM);\n table.addMany(EXECUTABLES, ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE);\n table.addMany(r(0x20, 0x80), ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE);\n table.addMany(EXECUTABLES, ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM);\n table.add(0x7f, ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM);\n table.addMany(r(0x30, 0x3c), ParserState.DCS_PARAM, ParserAction.PARAM, ParserState.DCS_PARAM);\n table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_IGNORE);\n table.addMany(r(0x20, 0x30), ParserState.DCS_PARAM, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE);\n table.addMany(EXECUTABLES, ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE);\n table.add(0x7f, ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE);\n table.addMany(r(0x20, 0x30), ParserState.DCS_INTERMEDIATE, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE);\n table.addMany(r(0x30, 0x40), ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_IGNORE);\n table.addMany(r(0x40, 0x7f), ParserState.DCS_INTERMEDIATE, ParserAction.DCS_HOOK, ParserState.DCS_PASSTHROUGH);\n table.addMany(r(0x40, 0x7f), ParserState.DCS_PARAM, ParserAction.DCS_HOOK, ParserState.DCS_PASSTHROUGH);\n table.addMany(r(0x40, 0x7f), ParserState.DCS_ENTRY, ParserAction.DCS_HOOK, ParserState.DCS_PASSTHROUGH);\n table.addMany(EXECUTABLES, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH);\n table.addMany(PRINTABLES, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH);\n table.add(0x7f, ParserState.DCS_PASSTHROUGH, ParserAction.IGNORE, ParserState.DCS_PASSTHROUGH);\n table.addMany([0x1b, 0x9c, 0x18, 0x1a], ParserState.DCS_PASSTHROUGH, ParserAction.DCS_UNHOOK, ParserState.GROUND);\n // special handling of unicode chars\n table.add(NON_ASCII_PRINTABLE, ParserState.GROUND, ParserAction.PRINT, ParserState.GROUND);\n table.add(NON_ASCII_PRINTABLE, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING);\n table.add(NON_ASCII_PRINTABLE, ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE);\n table.add(NON_ASCII_PRINTABLE, ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE);\n table.add(NON_ASCII_PRINTABLE, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH);\n table.add(NON_ASCII_PRINTABLE, ParserState.APC_PASSTHROUGH, ParserAction.APC_PUT, ParserState.APC_PASSTHROUGH);\n return table;\n})();\n\n\n/**\n * EscapeSequenceParser.\n * This class implements the ANSI/DEC compatible parser described by\n * Paul Williams (https://vt100.net/emu/dec_ansi_parser).\n *\n * To implement custom ANSI compliant escape sequences it is not needed to\n * alter this parser, instead consider registering a custom handler.\n * For non ANSI compliant sequences change the transition table with\n * the optional `transitions` constructor argument and\n * reimplement the `parse` method.\n *\n * This parser is currently hardcoded to operate in ZDM (Zero Default Mode)\n * as suggested by the original parser, thus empty parameters are set to 0.\n * This is not in line with the latest ECMA-48 specification\n * (ZDM was part of the early specs and got completely removed later on).\n *\n * Other than the original parser from vt100.net this parser supports\n * sub parameters in digital parameters separated by colons. Empty sub parameters\n * are set to -1 (no ZDM for sub parameters).\n *\n * About prefix and intermediate bytes:\n * This parser follows the assumptions of the vt100.net parser with these restrictions:\n * - only one prefix byte is allowed as first parameter byte, byte range 0x3c .. 0x3f\n * - max. two intermediates are respected, byte range 0x20 .. 0x2f\n * Note that this is not in line with ECMA-48 which does not limit either of those.\n * Furthermore ECMA-48 allows the prefix byte range at any param byte position. Currently\n * there are no known sequences that follow the broader definition of the specification.\n *\n * TODO: implement error recovery hook via error handler return values\n */\nexport class EscapeSequenceParser extends Disposable implements IEscapeSequenceParser {\n public initialState: number;\n public currentState: number;\n public precedingJoinState: number; // UnicodeJoinProperties\n\n // buffers over several parse calls\n protected _params: Params;\n protected _collect: number;\n\n // handler lookup containers\n protected _printHandler: PrintHandlerType;\n protected _executeHandlers: { [flag: number]: ExecuteHandlerType };\n // fast path for EXE bytes < 0x18\n protected _executeHandlersArr: (ExecuteHandlerType | undefined)[];\n protected _csiHandlers: IHandlerCollection;\n protected _escHandlers: IHandlerCollection;\n protected readonly _oscParser: IOscParser;\n protected readonly _dcsParser: IDcsParser;\n protected readonly _apcParser: IApcParser;\n protected _errorHandler: (state: IParsingState) => IParsingState;\n\n // fallback handlers\n protected _printHandlerFb: PrintFallbackHandlerType;\n protected _executeHandlerFb: ExecuteFallbackHandlerType;\n protected _csiHandlerFb: CsiFallbackHandlerType;\n protected _escHandlerFb: EscFallbackHandlerType;\n protected _errorHandlerFb: (state: IParsingState) => IParsingState;\n\n // parser stack save for async handler support\n protected _parseStack: IParserStackState = {\n state: ParserStackType.NONE,\n handlers: [],\n handlerPos: 0,\n transition: 0,\n chunkPos: 0\n };\n\n constructor(\n protected readonly _transitions: TransitionTable = VT500_TRANSITION_TABLE\n ) {\n super();\n\n this.initialState = ParserState.GROUND;\n this.currentState = this.initialState;\n this._params = new Params(); // defaults to 32 storable params/subparams\n this._params.addParam(0); // ZDM\n this._collect = 0;\n this.precedingJoinState = 0;\n\n // set default fallback handlers and handler lookup containers\n this._printHandlerFb = (data, start, end): void => { };\n this._executeHandlerFb = (code: number): void => { };\n this._csiHandlerFb = (ident: number, params: IParams): void => { };\n this._escHandlerFb = (ident: number): void => { };\n this._errorHandlerFb = (state: IParsingState): IParsingState => state;\n this._printHandler = this._printHandlerFb;\n this._executeHandlers = Object.create(null);\n this._executeHandlersArr = new Array(0x18).fill(undefined);\n this._csiHandlers = Object.create(null);\n this._escHandlers = Object.create(null);\n this._register(toDisposable(() => {\n this._csiHandlers = Object.create(null);\n this._executeHandlers = Object.create(null);\n this._executeHandlersArr = new Array(0x18).fill(undefined);\n this._escHandlers = Object.create(null);\n }));\n this._oscParser = this._register(new OscParser());\n this._dcsParser = this._register(new DcsParser());\n this._apcParser = this._register(new ApcParser());\n this._errorHandler = this._errorHandlerFb;\n\n // swallow 7bit ST (ESC+\\)\n this.registerEscHandler({ final: '\\\\' }, () => true);\n }\n\n protected _identifier(id: IFunctionIdentifier, finalRange: number[] = [0x40, 0x7e]): number {\n let res = 0;\n if (id.prefix) {\n if (id.prefix.length > 1) {\n throw new Error('only one byte as prefix supported');\n }\n res = id.prefix.charCodeAt(0);\n if (res < 0x3c || res > 0x3f) {\n throw new Error('prefix must be in range 0x3c .. 0x3f');\n }\n }\n if (id.intermediates) {\n if (id.intermediates.length > 2) {\n throw new Error('only two bytes as intermediates are supported');\n }\n for (let i = 0; i < id.intermediates.length; ++i) {\n const intermediate = id.intermediates.charCodeAt(i);\n if (0x20 > intermediate || intermediate > 0x2f) {\n throw new Error('intermediate must be in range 0x20 .. 0x2f');\n }\n res <<= 8;\n res |= intermediate;\n }\n }\n if (id.final.length !== 1) {\n throw new Error('final must be a single byte');\n }\n const finalCode = id.final.charCodeAt(0);\n if (finalRange[0] > finalCode || finalCode > finalRange[1]) {\n throw new Error(`final must be in range ${finalRange[0]} .. ${finalRange[1]}`);\n }\n res <<= 8;\n res |= finalCode;\n\n return res;\n }\n\n public identToString(ident: number): string {\n const res: string[] = [];\n while (ident) {\n res.push(String.fromCharCode(ident & 0xFF));\n ident >>= 8;\n }\n return res.reverse().join('');\n }\n\n public setPrintHandler(handler: PrintHandlerType): void {\n this._printHandler = handler;\n }\n public clearPrintHandler(): void {\n this._printHandler = this._printHandlerFb;\n }\n\n public registerEscHandler(id: IFunctionIdentifier, handler: EscHandlerType): IDisposable {\n const ident = this._identifier(id, [0x30, 0x7e]);\n this._escHandlers[ident] ??= [];\n const handlerList = this._escHandlers[ident];\n handlerList.push(handler);\n return {\n dispose: () => {\n const handlerIndex = handlerList.indexOf(handler);\n if (handlerIndex !== -1) {\n handlerList.splice(handlerIndex, 1);\n }\n }\n };\n }\n public clearEscHandler(id: IFunctionIdentifier): void {\n if (this._escHandlers[this._identifier(id, [0x30, 0x7e])]) delete this._escHandlers[this._identifier(id, [0x30, 0x7e])];\n }\n public setEscHandlerFallback(handler: EscFallbackHandlerType): void {\n this._escHandlerFb = handler;\n }\n\n public setExecuteHandler(flag: string, handler: ExecuteHandlerType): void {\n const code = flag.charCodeAt(0);\n this._executeHandlers[code] = handler;\n if (code < 0x18) this._executeHandlersArr[code] = handler;\n }\n public clearExecuteHandler(flag: string): void {\n const code = flag.charCodeAt(0);\n if (this._executeHandlers[code]) delete this._executeHandlers[code];\n if (code < 0x18) this._executeHandlersArr[code] = undefined;\n }\n public setExecuteHandlerFallback(handler: ExecuteFallbackHandlerType): void {\n this._executeHandlerFb = handler;\n }\n\n public registerCsiHandler(id: IFunctionIdentifier, handler: CsiHandlerType): IDisposable {\n const ident = this._identifier(id);\n this._csiHandlers[ident] ??= [];\n const handlerList = this._csiHandlers[ident];\n handlerList.push(handler);\n return {\n dispose: () => {\n const handlerIndex = handlerList.indexOf(handler);\n if (handlerIndex !== -1) {\n handlerList.splice(handlerIndex, 1);\n }\n }\n };\n }\n public clearCsiHandler(id: IFunctionIdentifier): void {\n if (this._csiHandlers[this._identifier(id)]) delete this._csiHandlers[this._identifier(id)];\n }\n public setCsiHandlerFallback(callback: (ident: number, params: IParams) => void): void {\n this._csiHandlerFb = callback;\n }\n\n public registerDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): IDisposable {\n return this._dcsParser.registerHandler(this._identifier(id), handler);\n }\n public clearDcsHandler(id: IFunctionIdentifier): void {\n this._dcsParser.clearHandler(this._identifier(id));\n }\n public setDcsHandlerFallback(handler: DcsFallbackHandlerType): void {\n this._dcsParser.setHandlerFallback(handler);\n }\n\n public registerOscHandler(ident: number, handler: IOscHandler): IDisposable {\n return this._oscParser.registerHandler(ident, handler);\n }\n public clearOscHandler(ident: number): void {\n this._oscParser.clearHandler(ident);\n }\n public setOscHandlerFallback(handler: OscFallbackHandlerType): void {\n this._oscParser.setHandlerFallback(handler);\n }\n\n public registerApcHandler(id: IFunctionIdentifier, handler: IApcHandler): IDisposable {\n id.prefix = undefined; // APC does not support prefix byte\n return this._apcParser.registerHandler(this._identifier(id, [0x30, 0x7e]), handler);\n }\n public clearApcHandler(id: IFunctionIdentifier): void {\n id.prefix = undefined; // APC does not support prefix byte\n this._apcParser.clearHandler(this._identifier(id, [0x30, 0x7e]));\n }\n public setApcHandlerFallback(handler: ApcFallbackHandlerType): void {\n this._apcParser.setHandlerFallback(handler);\n }\n\n public setErrorHandler(callback: (state: IParsingState) => IParsingState): void {\n this._errorHandler = callback;\n }\n public clearErrorHandler(): void {\n this._errorHandler = this._errorHandlerFb;\n }\n\n /**\n * Reset parser to initial values.\n *\n * This can also be used to lift the improper continuation error condition\n * when dealing with async handlers. Use this only as a last resort to silence\n * that error when the terminal has no pending data to be processed. Note that\n * the interrupted async handler might continue its work in the future messing\n * up the terminal state even further.\n */\n public reset(): void {\n this.currentState = this.initialState;\n this._oscParser.reset();\n this._dcsParser.reset();\n this._apcParser.reset();\n this._params.resetZdm();\n this._collect = 0;\n this.precedingJoinState = 0;\n // abort pending continuation from async handler\n // Here the RESET type indicates, that the next parse call will\n // ignore any saved stack, instead continues sync with next codepoint from GROUND\n if (this._parseStack.state !== ParserStackType.NONE) {\n this._parseStack.state = ParserStackType.RESET;\n this._parseStack.handlers = []; // also release handlers ref\n }\n }\n\n /**\n * Async parse support.\n */\n protected _preserveStack(\n state: ParserStackType,\n handlers: ResumableHandlersType,\n handlerPos: number,\n transition: number,\n chunkPos: number\n ): void {\n this._parseStack.state = state;\n this._parseStack.handlers = handlers;\n this._parseStack.handlerPos = handlerPos;\n this._parseStack.transition = transition;\n this._parseStack.chunkPos = chunkPos;\n }\n\n /**\n * Parse UTF32 codepoints in `data` up to `length`.\n *\n * Note: For several actions with high data load the parsing is optimized\n * by using local read ahead loops with hardcoded conditions to\n * avoid costly table lookups. Make sure that any change of table values\n * will be reflected in the loop conditions as well and vice versa.\n * Affected states/actions:\n * - GROUND:PRINT\n * - CSI_PARAM:PARAM\n * - DCS_PARAM:PARAM\n * - OSC_STRING:OSC_PUT\n * - DCS_PASSTHROUGH:DCS_PUT\n *\n * Additionally the following fast paths exist before the table lookup:\n * - EXE bytes < 0x18 in non-payload states (avoids table lookup entirely)\n * - 7-bit CSI sequences without intermediates (ESC [ params final)\n *\n * Note on asynchronous handler support:\n * Any handler returning a promise will be treated as asynchronous.\n * To keep the in-band blocking working for async handlers, `parse` pauses execution,\n * creates a stack save and returns the promise to the caller.\n * For proper continuation of the paused state it is important\n * to await the promise resolving. On resolve the parse must be repeated\n * with the same chunk of data and the resolved value in `promiseResult`\n * until no promise is returned.\n *\n * Important: With only sync handlers defined, parsing is completely synchronous as well.\n * As soon as an async handler is involved, synchronous parsing is not possible anymore.\n *\n * Boilerplate for proper parsing of multiple chunks with async handlers:\n *\n * ```typescript\n * async function parseMultipleChunks(chunks: Uint32Array[]): Promise {\n * for (const chunk of chunks) {\n * let result: void | Promise;\n * let prev: boolean | undefined;\n * while (result = parser.parse(chunk, chunk.length, prev)) {\n * prev = await result;\n * }\n * }\n * // finished parsing all chunks...\n * }\n * ```\n */\n public parse(data: Uint32Array, length: number, promiseResult?: boolean): void | Promise {\n let code: number;\n let transition: number;\n let start = 0;\n let handlerResult: void | boolean | Promise;\n\n // resume from async handler\n if (this._parseStack.state) {\n // allow sync parser reset even in continuation mode\n // Note: can be used to recover parser from improper continuation error below\n if (this._parseStack.state === ParserStackType.RESET) {\n this._parseStack.state = ParserStackType.NONE;\n start = this._parseStack.chunkPos + 1; // continue with next codepoint in GROUND\n } else {\n if (promiseResult === undefined || this._parseStack.state === ParserStackType.FAIL) {\n /**\n * Reject further parsing on improper continuation after pausing. This is a really bad\n * condition with screwed up execution order and prolly messed up terminal state,\n * therefore we exit hard with an exception and reject any further parsing.\n *\n * Note: With `Terminal.write` usage this exception should never occur, as the top level\n * calls are guaranteed to handle async conditions properly. If you ever encounter this\n * exception in your terminal integration it indicates, that you injected data chunks to\n * `InputHandler.parse` or `EscapeSequenceParser.parse` synchronously without waiting for\n * continuation of a running async handler.\n *\n * It is possible to get rid of this error by calling `reset`. But dont rely on that, as\n * the pending async handler still might mess up the terminal later. Instead fix the\n * faulty async handling, so this error will not be thrown anymore.\n */\n this._parseStack.state = ParserStackType.FAIL;\n throw new Error('improper continuation due to previous async handler, giving up parsing');\n }\n\n // we have to resume the old handler loop if:\n // - return value of the promise was `false`\n // - handlers are not exhausted yet\n const handlers = this._parseStack.handlers;\n let handlerPos = this._parseStack.handlerPos - 1;\n switch (this._parseStack.state) {\n case ParserStackType.CSI:\n if (promiseResult === false && handlerPos > -1) {\n for (; handlerPos >= 0; handlerPos--) {\n handlerResult = (handlers as CsiHandlerType[])[handlerPos](this._params);\n if (handlerResult === true) {\n break;\n } else if (handlerResult instanceof Promise) {\n this._parseStack.handlerPos = handlerPos;\n return handlerResult;\n }\n }\n }\n this._parseStack.handlers = [];\n break;\n case ParserStackType.ESC:\n if (promiseResult === false && handlerPos > -1) {\n for (; handlerPos >= 0; handlerPos--) {\n handlerResult = (handlers as EscHandlerType[])[handlerPos]();\n if (handlerResult === true) {\n break;\n } else if (handlerResult instanceof Promise) {\n this._parseStack.handlerPos = handlerPos;\n return handlerResult;\n }\n }\n }\n this._parseStack.handlers = [];\n break;\n case ParserStackType.DCS:\n code = data[this._parseStack.chunkPos];\n handlerResult = this._dcsParser.unhook(code !== 0x18 && code !== 0x1a, promiseResult);\n if (handlerResult) {\n return handlerResult;\n }\n if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE;\n this._params.resetZdm();\n this._collect = 0;\n break;\n case ParserStackType.OSC:\n code = data[this._parseStack.chunkPos];\n handlerResult = this._oscParser.end(code !== 0x18 && code !== 0x1a, promiseResult);\n if (handlerResult) {\n return handlerResult;\n }\n if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE;\n this._params.resetZdm();\n this._collect = 0;\n break;\n case ParserStackType.APC:\n code = data[this._parseStack.chunkPos];\n handlerResult = this._apcParser.end(code !== 0x18 && code !== 0x1a, promiseResult);\n if (handlerResult) {\n return handlerResult;\n }\n if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE;\n this._params.resetZdm();\n this._collect = 0;\n break;\n }\n // cleanup before continuing with the main sync loop\n this._parseStack.state = ParserStackType.NONE;\n start = this._parseStack.chunkPos + 1;\n this.precedingJoinState = 0;\n this.currentState = this._parseStack.transition & TableAccess.TRANSITION_STATE_MASK;\n }\n }\n\n // continue with main sync loop\n\n // process input string\n for (let i = start; i < length; ++i) {\n code = data[i];\n\n // EXE fast-path: common control bytes (0x00-0x17) in non-payload states\n if (code < 0x18 && this.currentState <= ParserState.CSI_IGNORE) {\n (this._executeHandlersArr[code] ?? this._executeHandlerFb)(code);\n this.precedingJoinState = 0;\n continue;\n }\n\n // CSI fast-path: collapse ESC [ into a single entry, parse params+final in a tight loop\n if (code === 0x1b\n && this.currentState < ParserState.OSC_STRING\n && i + 2 < length && data[i + 1] === 0x5b\n ) {\n this._params.resetZdm();\n this._collect = 0;\n let k = i + 2;\n let ch = data[k];\n if (ch >= 0x3c && ch <= 0x3f) {\n this._collect = ch;\n k++;\n }\n let csiDone = false;\n for (; k < length; k++) {\n ch = data[k];\n if (ch >= 0x30 && ch <= 0x39) {\n this._params.addDigit(ch - 48);\n } else if (ch === 0x3b) {\n this._params.addParam(0);\n } else if (ch === 0x3a) {\n this._params.addSubParam(-1);\n } else if (ch >= 0x40 && ch <= 0x7e) {\n const handlers = this._csiHandlers[this._collect << 8 | ch];\n let j = handlers ? handlers.length - 1 : -1;\n for (; j >= 0; j--) {\n handlerResult = handlers[j](this._params);\n if (handlerResult === true) {\n break;\n } else if (handlerResult instanceof Promise) {\n transition = ParserAction.CSI_DISPATCH << TableAccess.TRANSITION_ACTION_SHIFT | ParserState.GROUND;\n this._preserveStack(ParserStackType.CSI, handlers, j, transition, k);\n return handlerResult;\n }\n }\n if (j < 0) {\n this._csiHandlerFb(this._collect << 8 | ch, this._params);\n }\n this.precedingJoinState = 0;\n i = k;\n this.currentState = ParserState.GROUND;\n csiDone = true;\n break;\n } else {\n break;\n }\n }\n if (!csiDone) {\n i = k - 1;\n this.currentState = ParserState.CSI_PARAM;\n }\n continue;\n }\n\n // normal transition & action lookup\n transition = this._transitions.table[\n this.currentState << TableAccess.INDEX_STATE_SHIFT |\n (code < NON_ASCII_PRINTABLE ? code : NON_ASCII_PRINTABLE)\n ];\n switch (transition >> TableAccess.TRANSITION_ACTION_SHIFT) {\n case ParserAction.PRINT:\n // Note: 0x20 (SP) is included, 0x7F (DEL) is excluded\n let c = i;\n const l4 = length - 4;\n while (c < l4\n && data[++c] >= 0x20 && (data[c] <= 0x7e || data[c] >= NON_ASCII_PRINTABLE)\n && data[++c] >= 0x20 && (data[c] <= 0x7e || data[c] >= NON_ASCII_PRINTABLE)\n && data[++c] >= 0x20 && (data[c] <= 0x7e || data[c] >= NON_ASCII_PRINTABLE)\n && data[++c] >= 0x20 && (data[c] <= 0x7e || data[c] >= NON_ASCII_PRINTABLE)\n ) {}\n if (c >= l4) {\n while (c < length && data[c] >= 0x20 && (data[c] <= 0x7e || data[c] >= NON_ASCII_PRINTABLE)) {\n c++;\n }\n }\n this._printHandler(data, i, c);\n i = c - 1;\n break;\n case ParserAction.EXECUTE:\n if (this._executeHandlers[code]) this._executeHandlers[code]();\n else this._executeHandlerFb(code);\n this.precedingJoinState = 0;\n break;\n case ParserAction.IGNORE:\n break;\n case ParserAction.ERROR:\n const inject: IParsingState = this._errorHandler(\n {\n position: i,\n code,\n currentState: this.currentState,\n collect: this._collect,\n params: this._params,\n abort: false\n });\n if (inject.abort) return;\n // inject values: currently not implemented\n break;\n case ParserAction.CSI_DISPATCH:\n // Trigger CSI Handler\n const handlers = this._csiHandlers[this._collect << 8 | code];\n let j = handlers ? handlers.length - 1 : -1;\n for (; j >= 0; j--) {\n // true means success and to stop bubbling\n // a promise indicates an async handler that needs to finish before progressing\n handlerResult = handlers[j](this._params);\n if (handlerResult === true) {\n break;\n } else if (handlerResult instanceof Promise) {\n this._preserveStack(ParserStackType.CSI, handlers, j, transition, i);\n return handlerResult;\n }\n }\n if (j < 0) {\n this._csiHandlerFb(this._collect << 8 | code, this._params);\n }\n this.precedingJoinState = 0;\n break;\n case ParserAction.PARAM:\n // inner loop: digits (0x30 - 0x39) and ; (0x3b) and : (0x3a)\n do {\n switch (code) {\n case 0x3b:\n this._params.addParam(0); // ZDM\n break;\n case 0x3a:\n this._params.addSubParam(-1);\n break;\n default: // 0x30 - 0x39\n this._params.addDigit(code - 48);\n }\n } while (++i < length && (code = data[i]) > 0x2f && code < 0x3c);\n i--;\n break;\n case ParserAction.COLLECT:\n this._collect <<= 8;\n this._collect |= code;\n break;\n case ParserAction.ESC_DISPATCH:\n const handlersEsc = this._escHandlers[this._collect << 8 | code];\n let jj = handlersEsc ? handlersEsc.length - 1 : -1;\n for (; jj >= 0; jj--) {\n // true means success and to stop bubbling\n // a promise indicates an async handler that needs to finish before progressing\n handlerResult = handlersEsc[jj]();\n if (handlerResult === true) {\n break;\n } else if (handlerResult instanceof Promise) {\n this._preserveStack(ParserStackType.ESC, handlersEsc, jj, transition, i);\n return handlerResult;\n }\n }\n if (jj < 0) {\n this._escHandlerFb(this._collect << 8 | code);\n }\n this.precedingJoinState = 0;\n break;\n case ParserAction.CLEAR:\n this._params.resetZdm();\n this._collect = 0;\n break;\n case ParserAction.DCS_HOOK:\n this._dcsParser.hook(this._collect << 8 | code, this._params);\n break;\n case ParserAction.DCS_PUT:\n // inner loop - exit DCS_PUT: 0x18, 0x1a, 0x1b, 0x7f, 0x80 - 0x9f\n // unhook triggered by: 0x1b, 0x9c (success) and 0x18, 0x1a (abort)\n for (let j = i + 1; ; ++j) {\n if (j >= length || (code = data[j]) === 0x18 || code === 0x1a || code === 0x1b || (code > 0x7f && code < NON_ASCII_PRINTABLE)) {\n this._dcsParser.put(data, i, j);\n i = j - 1;\n break;\n }\n }\n break;\n case ParserAction.DCS_UNHOOK:\n handlerResult = this._dcsParser.unhook(code !== 0x18 && code !== 0x1a);\n if (handlerResult) {\n this._preserveStack(ParserStackType.DCS, [], 0, transition, i);\n return handlerResult;\n }\n if (code === 0x1b) transition |= ParserState.ESCAPE;\n this._params.resetZdm();\n this._collect = 0;\n this.precedingJoinState = 0;\n break;\n case ParserAction.OSC_START:\n this._oscParser.start();\n break;\n case ParserAction.OSC_PUT:\n // inner loop: 0x20 (SP) included, 0x7F (DEL) included\n for (let j = i + 1; ; j++) {\n if (j >= length || (code = data[j]) < 0x20 || (code > 0x7f && code < NON_ASCII_PRINTABLE)) {\n this._oscParser.put(data, i, j);\n i = j - 1;\n break;\n }\n }\n break;\n case ParserAction.OSC_END:\n handlerResult = this._oscParser.end(code !== 0x18 && code !== 0x1a);\n if (handlerResult) {\n this._preserveStack(ParserStackType.OSC, [], 0, transition, i);\n return handlerResult;\n }\n if (code === 0x1b) transition |= ParserState.ESCAPE;\n this._params.resetZdm();\n this._collect = 0;\n this.precedingJoinState = 0;\n break;\n case ParserAction.APC_START:\n this._apcParser.start(this._collect << 8 | code);\n break;\n case ParserAction.APC_PUT:\n // inner loop - exit APC_PUT: 0x18, 0x1a, 0x1b, 0x9c\n // allowed: 00/08 .. 00/13, 02/00 .. 07/14 + NON_ASCII_PRINTABLE\n for (let j = i + 1; ; ++j) {\n if (j < length && (\n (data[j] >= 0x20 && data[j] < 0x7f) || (data[j] >= 0x08 && data[j] < 0x0e) || data[j] >= NON_ASCII_PRINTABLE\n )) continue;\n this._apcParser.put(data, i, j);\n i = j - 1;\n break;\n }\n break;\n case ParserAction.APC_END:\n handlerResult = this._apcParser.end(code !== 0x18 && code !== 0x1a);\n if (handlerResult) {\n this._preserveStack(ParserStackType.APC, [], 0, transition, i);\n return handlerResult;\n }\n if (code === 0x1b) transition |= ParserState.ESCAPE;\n this._params.resetZdm();\n this._collect = 0;\n this.precedingJoinState = 0;\n break;\n }\n this.currentState = transition & TableAccess.TRANSITION_STATE_MASK;\n }\n }\n}\n", "/**\n * Copyright (c) 2021 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\n\n// 'rgb:' rule - matching: r/g/b | rr/gg/bb | rrr/ggg/bbb | rrrr/gggg/bbbb (hex digits)\nconst RGB_REX = /^([\\da-f])\\/([\\da-f])\\/([\\da-f])$|^([\\da-f]{2})\\/([\\da-f]{2})\\/([\\da-f]{2})$|^([\\da-f]{3})\\/([\\da-f]{3})\\/([\\da-f]{3})$|^([\\da-f]{4})\\/([\\da-f]{4})\\/([\\da-f]{4})$/;\n// '#...' rule - matching any hex digits\nconst HASH_REX = /^[\\da-f]+$/;\n\n/**\n * Parse color spec to RGB values (8 bit per channel).\n * See `man xparsecolor` for details about certain format specifications.\n *\n * Supported formats:\n * - rgb:// with , , in h | hh | hhh | hhhh\n * - #RGB, #RRGGBB, #RRRGGGBBB, #RRRRGGGGBBBB\n *\n * All other formats like rgbi: or device-independent string specifications\n * with float numbering are not supported.\n */\nexport function parseColor(data: string): [number, number, number] | undefined {\n if (!data) return;\n // also handle uppercases\n let low = data.toLowerCase();\n if (low.startsWith('rgb:')) {\n // 'rgb:' specifier\n low = low.slice(4);\n const m = RGB_REX.exec(low);\n if (m) {\n const base = m[1] ? 15 : m[4] ? 255 : m[7] ? 4095 : 65535;\n return [\n Math.round(parseInt(m[1] || m[4] || m[7] || m[10], 16) / base * 255),\n Math.round(parseInt(m[2] || m[5] || m[8] || m[11], 16) / base * 255),\n Math.round(parseInt(m[3] || m[6] || m[9] || m[12], 16) / base * 255)\n ];\n }\n } else if (low.startsWith('#')) {\n // '#' specifier\n low = low.slice(1);\n if (HASH_REX.exec(low) && [3, 6, 9, 12].includes(low.length)) {\n const adv = low.length / 3;\n const result: [number, number, number] = [0, 0, 0];\n for (let i = 0; i < 3; ++i) {\n const c = parseInt(low.slice(adv * i, adv * i + adv), 16);\n result[i] = adv === 1 ? c << 4 : adv === 2 ? c : adv === 3 ? c >> 4 : c >> 8;\n }\n return result;\n }\n }\n\n // Named colors are currently not supported due to the large addition to the xterm.js bundle size\n // they would add. In order to support named colors, we would need some way of optionally loading\n // additional payloads so startup/download time is not bloated (see #3530).\n}\n\n// pad hex output to requested bit width\nfunction pad(n: number, bits: number): string {\n const s = n.toString(16);\n const s2 = s.length < 2 ? '0' + s : s;\n switch (bits) {\n case 4:\n return s[0];\n case 8:\n return s2;\n case 12:\n return (s2 + s2).slice(0, 3);\n default:\n return s2 + s2;\n }\n}\n\n/**\n * Convert a given color to rgb:../../.. string of `bits` depth.\n */\nexport function toRgbString(color: [number, number, number], bits: number = 16): string {\n const [r, g, b] = color;\n return `rgb:${pad(r, bits)}/${pad(g, bits)}/${pad(b, bits)}`;\n}\n", "/**\n * Copyright (c) 2025 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\n/**\n * The xterm.js version. This is updated by the publish script from package.json.\n */\nexport const XTERM_VERSION = '6.0.0';\n", "/**\n * Copyright (c) 2014 The xterm.js authors. All rights reserved.\n * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)\n * @license MIT\n */\n\nimport { IInputHandler, IDisposable, IWindowOptions, IColorEvent, IParseStack, ColorIndex, ColorRequestType, SpecialColorIndex } from './Types';\nimport { IAttributeData, IBuffer } from './buffer/Types';\nimport { C0, C1 } from './data/EscapeSequences';\nimport { CHARSETS, DEFAULT_CHARSET } from './data/Charsets';\nimport { EscapeSequenceParser } from './parser/EscapeSequenceParser';\nimport { Disposable } from './Lifecycle';\nimport { StringToUtf32, stringFromCodePoint, Utf8ToUtf32 } from './input/TextDecoder';\nimport { BufferLine, DEFAULT_ATTR_DATA } from './buffer/BufferLine';\nimport { IParsingState, IEscapeSequenceParser, IParams, IFunctionIdentifier } from './parser/Types';\nimport { NULL_CELL_CODE, NULL_CELL_WIDTH, Attributes, FgFlags, BgFlags, Content, UnderlineStyle } from './buffer/Constants';\nimport { CellData } from './buffer/CellData';\nimport { AttributeData } from './buffer/AttributeData';\nimport { ICoreService, IBufferService, IOptionsService, ILogService, IMouseStateService, ICharsetService, IUnicodeService, LogLevelEnum, IOscLinkService } from './services/Services';\nimport { UnicodeService } from './services/UnicodeService';\nimport { OscHandler } from './parser/OscParser';\nimport { DcsHandler } from './parser/DcsParser';\nimport { ApcHandler } from './parser/ApcParser';\nimport { parseColor } from './input/XParseColor';\nimport { Emitter } from './Event';\nimport { XTERM_VERSION } from './Version';\n\n/**\n * Map collect to glevel. Used in `selectCharset`.\n */\nconst GLEVEL: { [key: string]: number } = { '(': 0, ')': 1, '*': 2, '+': 3, '-': 1, '.': 2 };\n\n/**\n * Document xterm VT features here that are currently unsupported\n */\n// @vt: #N DCS DECUDK \"User Defined Keys\" \"DCS Ps ; Ps \\| Pt ST\" \"Definitions for user-defined keys.\"\n// @vt: #N DCS XTGETTCAP \"Request Terminfo String\" \"DCS + q Pt ST\" \"Request Terminfo String.\"\n// @vt: #N DCS XTSETTCAP \"Set Terminfo Data\" \"DCS + p Pt ST\" \"Set Terminfo Data.\"\n// @vt: #N OSC 1 \"Set Icon Name\" \"OSC 1 ; Pt BEL\" \"Set icon name.\"\n\n/**\n * Max length of the UTF32 input buffer. Real memory consumption is 4 times higher.\n */\nconst enum Constants {\n MAX_PARSEBUFFER_LENGTH = 131072,\n /** Limit length of title and icon name stacks. */\n STACK_LIMIT = 10,\n // create a warning log if an async handler takes longer than the limit (in ms)\n SLOW_ASYNC_LIMIT = 5000\n}\n\n// map params to window option\nfunction paramToWindowOption(n: number, opts: IWindowOptions): boolean {\n if (n > 24) {\n return opts.setWinLines || false;\n }\n switch (n) {\n case 1: return !!opts.restoreWin;\n case 2: return !!opts.minimizeWin;\n case 3: return !!opts.setWinPosition;\n case 4: return !!opts.setWinSizePixels;\n case 5: return !!opts.raiseWin;\n case 6: return !!opts.lowerWin;\n case 7: return !!opts.refreshWin;\n case 8: return !!opts.setWinSizeChars;\n case 9: return !!opts.maximizeWin;\n case 10: return !!opts.fullscreenWin;\n case 11: return !!opts.getWinState;\n case 13: return !!opts.getWinPosition;\n case 14: return !!opts.getWinSizePixels;\n case 15: return !!opts.getScreenSizePixels;\n case 16: return !!opts.getCellSizePixels;\n case 18: return !!opts.getWinSizeChars;\n case 19: return !!opts.getScreenSizeChars;\n case 20: return !!opts.getIconTitle;\n case 21: return !!opts.getWinTitle;\n case 22: return !!opts.pushTitle;\n case 23: return !!opts.popTitle;\n case 24: return !!opts.setWinLines;\n }\n return false;\n}\n\nexport enum WindowsOptionsReportType {\n GET_WIN_SIZE_PIXELS = 0,\n GET_CELL_SIZE_PIXELS = 1\n}\n\n// Work variables to avoid garbage collection\nlet $temp = 0;\n\n/**\n * The terminal's standard implementation of IInputHandler, this handles all\n * input from the Parser.\n *\n * Refer to http://invisible-island.net/xterm/ctlseqs/ctlseqs.html to understand\n * each function's header comment.\n */\nexport class InputHandler extends Disposable implements IInputHandler {\n private _parseBuffer: Uint32Array = new Uint32Array(4096);\n private _stringDecoder: StringToUtf32 = new StringToUtf32();\n private _utf8Decoder: Utf8ToUtf32 = new Utf8ToUtf32();\n private _windowTitle = '';\n private _iconName = '';\n private _dirtyRowTracker: IDirtyRowTracker;\n protected _windowTitleStack: string[] = [];\n protected _iconNameStack: string[] = [];\n\n private _curAttrData: IAttributeData = DEFAULT_ATTR_DATA.clone();\n public getAttrData(): IAttributeData { return this._curAttrData; }\n private _eraseAttrDataInternal: IAttributeData = DEFAULT_ATTR_DATA.clone();\n\n private _activeBuffer: IBuffer;\n\n private readonly _onRequestBell = this._register(new Emitter());\n public readonly onRequestBell = this._onRequestBell.event;\n private readonly _onRequestRefreshRows = this._register(new Emitter<{ start: number, end: number } | undefined>());\n public readonly onRequestRefreshRows = this._onRequestRefreshRows.event;\n private readonly _onRequestReset = this._register(new Emitter());\n public readonly onRequestReset = this._onRequestReset.event;\n private readonly _onRequestSendFocus = this._register(new Emitter());\n public readonly onRequestSendFocus = this._onRequestSendFocus.event;\n private readonly _onRequestSyncScrollBar = this._register(new Emitter());\n public readonly onRequestSyncScrollBar = this._onRequestSyncScrollBar.event;\n private readonly _onRequestWindowsOptionsReport = this._register(new Emitter());\n public readonly onRequestWindowsOptionsReport = this._onRequestWindowsOptionsReport.event;\n\n private readonly _onA11yChar = this._register(new Emitter());\n public readonly onA11yChar = this._onA11yChar.event;\n private readonly _onA11yTab = this._register(new Emitter());\n public readonly onA11yTab = this._onA11yTab.event;\n private readonly _onCursorMove = this._register(new Emitter());\n public readonly onCursorMove = this._onCursorMove.event;\n private readonly _onLineFeed = this._register(new Emitter());\n public readonly onLineFeed = this._onLineFeed.event;\n private readonly _onScroll = this._register(new Emitter());\n public readonly onScroll = this._onScroll.event;\n private readonly _onTitleChange = this._register(new Emitter());\n public readonly onTitleChange = this._onTitleChange.event;\n private readonly _onColor = this._register(new Emitter());\n public readonly onColor = this._onColor.event;\n private readonly _onRequestColorSchemeQuery = this._register(new Emitter());\n public readonly onRequestColorSchemeQuery = this._onRequestColorSchemeQuery.event;\n\n private _parseStack: IParseStack = {\n paused: false,\n cursorStartX: 0,\n cursorStartY: 0,\n decodedLength: 0,\n position: 0\n };\n\n constructor(\n private readonly _bufferService: IBufferService,\n private readonly _charsetService: ICharsetService,\n private readonly _coreService: ICoreService,\n private readonly _logService: ILogService,\n private readonly _optionsService: IOptionsService,\n private readonly _oscLinkService: IOscLinkService,\n private readonly _mouseStateService: IMouseStateService,\n private readonly _unicodeService: IUnicodeService,\n private readonly _parser: IEscapeSequenceParser = new EscapeSequenceParser()\n ) {\n super();\n this._register(this._parser);\n this._dirtyRowTracker = new DirtyRowTracker(this._bufferService);\n\n // Track properties used in performance critical code manually to avoid using slow getters\n this._activeBuffer = this._bufferService.buffer;\n this._register(this._bufferService.buffers.onBufferActivate(e => this._activeBuffer = e.activeBuffer));\n\n /**\n * custom fallback handlers\n */\n this._parser.setCsiHandlerFallback((ident, params) => {\n this._logService.debug('Unknown CSI code: ', { identifier: this._parser.identToString(ident), params: params.toArray() });\n });\n this._parser.setEscHandlerFallback(ident => {\n this._logService.debug('Unknown ESC code: ', { identifier: this._parser.identToString(ident) });\n });\n this._parser.setExecuteHandlerFallback(code => {\n this._logService.debug('Unknown EXECUTE code: ', { code });\n });\n this._parser.setOscHandlerFallback((identifier, action, data) => {\n this._logService.debug('Unknown OSC code: ', { identifier, action, data });\n });\n this._parser.setDcsHandlerFallback((ident, action, payload) => {\n if (action === 'HOOK') {\n payload = payload.toArray();\n }\n this._logService.debug('Unknown DCS code: ', { identifier: this._parser.identToString(ident), action, payload });\n });\n this._parser.setApcHandlerFallback((ident, action, payload) => {\n this._logService.debug('Unknown APC code: ', { identifier: this._parser.identToString(ident), action, payload });\n });\n\n /**\n * print handler\n */\n this._parser.setPrintHandler((data, start, end) => this.print(data, start, end));\n\n /**\n * CSI handler\n */\n this._parser.registerCsiHandler({ final: '@' }, params => this.insertChars(params));\n this._parser.registerCsiHandler({ intermediates: ' ', final: '@' }, params => this.scrollLeft(params));\n this._parser.registerCsiHandler({ final: 'A' }, params => this.cursorUp(params));\n this._parser.registerCsiHandler({ intermediates: ' ', final: 'A' }, params => this.scrollRight(params));\n this._parser.registerCsiHandler({ final: 'B' }, params => this.cursorDown(params));\n this._parser.registerCsiHandler({ final: 'C' }, params => this.cursorForward(params));\n this._parser.registerCsiHandler({ final: 'D' }, params => this.cursorBackward(params));\n this._parser.registerCsiHandler({ final: 'E' }, params => this.cursorNextLine(params));\n this._parser.registerCsiHandler({ final: 'F' }, params => this.cursorPrecedingLine(params));\n this._parser.registerCsiHandler({ final: 'G' }, params => this.cursorCharAbsolute(params));\n this._parser.registerCsiHandler({ final: 'H' }, params => this.cursorPosition(params));\n this._parser.registerCsiHandler({ final: 'I' }, params => this.cursorForwardTab(params));\n this._parser.registerCsiHandler({ final: 'J' }, params => this.eraseInDisplay(params, false));\n this._parser.registerCsiHandler({ prefix: '?', final: 'J' }, params => this.eraseInDisplay(params, true));\n this._parser.registerCsiHandler({ final: 'K' }, params => this.eraseInLine(params, false));\n this._parser.registerCsiHandler({ prefix: '?', final: 'K' }, params => this.eraseInLine(params, true));\n this._parser.registerCsiHandler({ final: 'L' }, params => this.insertLines(params));\n this._parser.registerCsiHandler({ final: 'M' }, params => this.deleteLines(params));\n this._parser.registerCsiHandler({ final: 'P' }, params => this.deleteChars(params));\n this._parser.registerCsiHandler({ final: 'S' }, params => this.scrollUp(params));\n this._parser.registerCsiHandler({ final: 'T' }, params => this.scrollDown(params));\n this._parser.registerCsiHandler({ final: 'X' }, params => this.eraseChars(params));\n this._parser.registerCsiHandler({ final: 'Z' }, params => this.cursorBackwardTab(params));\n this._parser.registerCsiHandler({ final: '^' }, params => this.scrollDown(params));\n this._parser.registerCsiHandler({ final: '`' }, params => this.charPosAbsolute(params));\n this._parser.registerCsiHandler({ final: 'a' }, params => this.hPositionRelative(params));\n this._parser.registerCsiHandler({ final: 'b' }, params => this.repeatPrecedingCharacter(params));\n this._parser.registerCsiHandler({ final: 'c' }, params => this.sendDeviceAttributesPrimary(params));\n this._parser.registerCsiHandler({ prefix: '>', final: 'c' }, params => this.sendDeviceAttributesSecondary(params));\n this._parser.registerCsiHandler({ final: 'd' }, params => this.linePosAbsolute(params));\n this._parser.registerCsiHandler({ final: 'e' }, params => this.vPositionRelative(params));\n this._parser.registerCsiHandler({ final: 'f' }, params => this.hVPosition(params));\n this._parser.registerCsiHandler({ final: 'g' }, params => this.tabClear(params));\n this._parser.registerCsiHandler({ final: 'h' }, params => this.setMode(params));\n this._parser.registerCsiHandler({ prefix: '?', final: 'h' }, params => this.setModePrivate(params));\n this._parser.registerCsiHandler({ final: 'l' }, params => this.resetMode(params));\n this._parser.registerCsiHandler({ prefix: '?', final: 'l' }, params => this.resetModePrivate(params));\n this._parser.registerCsiHandler({ final: 'm' }, params => this.charAttributes(params));\n this._parser.registerCsiHandler({ final: 'n' }, params => this.deviceStatus(params));\n this._parser.registerCsiHandler({ prefix: '?', final: 'n' }, params => this.deviceStatusPrivate(params));\n this._parser.registerCsiHandler({ intermediates: '!', final: 'p' }, params => this.softReset(params));\n this._parser.registerCsiHandler({ prefix: '>', final: 'q' }, params => this.sendXtVersion(params));\n this._parser.registerCsiHandler({ intermediates: ' ', final: 'q' }, params => this.setCursorStyle(params));\n this._parser.registerCsiHandler({ final: 'r' }, params => this.setScrollRegion(params));\n this._parser.registerCsiHandler({ final: 's' }, params => this.saveCursor(params));\n this._parser.registerCsiHandler({ final: 't' }, params => this.windowOptions(params));\n this._parser.registerCsiHandler({ final: 'u' }, params => this.restoreCursor(params));\n this._parser.registerCsiHandler({ intermediates: '\\'', final: '}' }, params => this.insertColumns(params));\n this._parser.registerCsiHandler({ intermediates: '\\'', final: '~' }, params => this.deleteColumns(params));\n this._parser.registerCsiHandler({ intermediates: '\"', final: 'q' }, params => this.selectProtected(params));\n this._parser.registerCsiHandler({ intermediates: '$', final: 'p' }, params => this.requestMode(params, true));\n this._parser.registerCsiHandler({ prefix: '?', intermediates: '$', final: 'p' }, params => this.requestMode(params, false));\n\n // Kitty keyboard protocol handlers\n this._parser.registerCsiHandler({ prefix: '=', final: 'u' }, params => this.kittyKeyboardSet(params));\n this._parser.registerCsiHandler({ prefix: '?', final: 'u' }, params => this.kittyKeyboardQuery(params));\n this._parser.registerCsiHandler({ prefix: '>', final: 'u' }, params => this.kittyKeyboardPush(params));\n this._parser.registerCsiHandler({ prefix: '<', final: 'u' }, params => this.kittyKeyboardPop(params));\n\n /**\n * execute handler\n */\n this._parser.setExecuteHandler(C0.BEL, () => this.bell());\n this._parser.setExecuteHandler(C0.LF, () => this.lineFeed());\n this._parser.setExecuteHandler(C0.VT, () => this.lineFeed());\n this._parser.setExecuteHandler(C0.FF, () => this.lineFeed());\n this._parser.setExecuteHandler(C0.CR, () => this.carriageReturn());\n this._parser.setExecuteHandler(C0.BS, () => this.backspace());\n this._parser.setExecuteHandler(C0.HT, () => this.tab());\n this._parser.setExecuteHandler(C0.SO, () => this.shiftOut());\n this._parser.setExecuteHandler(C0.SI, () => this.shiftIn());\n // FIXME: What do to with missing? Old code just added those to print.\n\n this._parser.setExecuteHandler(C1.IND, () => this.index());\n this._parser.setExecuteHandler(C1.NEL, () => this.nextLine());\n this._parser.setExecuteHandler(C1.HTS, () => this.tabSet());\n\n /**\n * OSC handler\n */\n // 0 - icon name + title\n this._parser.registerOscHandler(0, new OscHandler(data => { this.setTitle(data); this.setIconName(data); return true; }));\n // 1 - icon name\n this._parser.registerOscHandler(1, new OscHandler(data => this.setIconName(data)));\n // 2 - title\n this._parser.registerOscHandler(2, new OscHandler(data => this.setTitle(data)));\n // 3 - set property X in the form \"prop=value\"\n // 4 - Change Color Number\n this._parser.registerOscHandler(4, new OscHandler(data => this.setOrReportIndexedColor(data)));\n // 5 - Change Special Color Number\n // 6 - Enable/disable Special Color Number c\n // 7 - current directory? (not in xterm spec, see https://gitlab.com/gnachman/iterm2/issues/3939)\n // 8 - create hyperlink (not in xterm spec, see https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda)\n this._parser.registerOscHandler(8, new OscHandler(data => this.setHyperlink(data)));\n // 10 - Change VT100 text foreground color to Pt.\n this._parser.registerOscHandler(10, new OscHandler(data => this.setOrReportFgColor(data)));\n // 11 - Change VT100 text background color to Pt.\n this._parser.registerOscHandler(11, new OscHandler(data => this.setOrReportBgColor(data)));\n // 12 - Change text cursor color to Pt.\n this._parser.registerOscHandler(12, new OscHandler(data => this.setOrReportCursorColor(data)));\n // 13 - Change mouse foreground color to Pt.\n // 14 - Change mouse background color to Pt.\n // 15 - Change Tektronix foreground color to Pt.\n // 16 - Change Tektronix background color to Pt.\n // 17 - Change highlight background color to Pt.\n // 18 - Change Tektronix cursor color to Pt.\n // 19 - Change highlight foreground color to Pt.\n // 46 - Change Log File to Pt.\n // 50 - Set Font to Pt.\n // 51 - reserved for Emacs shell.\n // 52 - Manipulate Selection Data.\n // 104 ; c - Reset Color Number c.\n this._parser.registerOscHandler(104, new OscHandler(data => this.restoreIndexedColor(data)));\n // 105 ; c - Reset Special Color Number c.\n // 106 ; c; f - Enable/disable Special Color Number c.\n // 110 - Reset VT100 text foreground color.\n this._parser.registerOscHandler(110, new OscHandler(data => this.restoreFgColor(data)));\n // 111 - Reset VT100 text background color.\n this._parser.registerOscHandler(111, new OscHandler(data => this.restoreBgColor(data)));\n // 112 - Reset text cursor color.\n this._parser.registerOscHandler(112, new OscHandler(data => this.restoreCursorColor(data)));\n // 113 - Reset mouse foreground color.\n // 114 - Reset mouse background color.\n // 115 - Reset Tektronix foreground color.\n // 116 - Reset Tektronix background color.\n // 117 - Reset highlight color.\n // 118 - Reset Tektronix cursor color.\n // 119 - Reset highlight foreground color.\n\n /**\n * ESC handlers\n */\n this._parser.registerEscHandler({ final: '7' }, () => this.saveCursor());\n this._parser.registerEscHandler({ final: '8' }, () => this.restoreCursor());\n this._parser.registerEscHandler({ final: 'D' }, () => this.index());\n this._parser.registerEscHandler({ final: 'E' }, () => this.nextLine());\n this._parser.registerEscHandler({ final: 'H' }, () => this.tabSet());\n this._parser.registerEscHandler({ final: 'M' }, () => this.reverseIndex());\n this._parser.registerEscHandler({ final: '=' }, () => this.keypadApplicationMode());\n this._parser.registerEscHandler({ final: '>' }, () => this.keypadNumericMode());\n this._parser.registerEscHandler({ final: 'c' }, () => this.fullReset());\n this._parser.registerEscHandler({ final: 'n' }, () => this.setgLevel(2));\n this._parser.registerEscHandler({ final: 'o' }, () => this.setgLevel(3));\n this._parser.registerEscHandler({ final: '|' }, () => this.setgLevel(3));\n this._parser.registerEscHandler({ final: '}' }, () => this.setgLevel(2));\n this._parser.registerEscHandler({ final: '~' }, () => this.setgLevel(1));\n this._parser.registerEscHandler({ intermediates: '%', final: '@' }, () => this.selectDefaultCharset());\n this._parser.registerEscHandler({ intermediates: '%', final: 'G' }, () => this.selectDefaultCharset());\n for (const flag in CHARSETS) {\n this._parser.registerEscHandler({ intermediates: '(', final: flag }, () => this.selectCharset('(' + flag));\n this._parser.registerEscHandler({ intermediates: ')', final: flag }, () => this.selectCharset(')' + flag));\n this._parser.registerEscHandler({ intermediates: '*', final: flag }, () => this.selectCharset('*' + flag));\n this._parser.registerEscHandler({ intermediates: '+', final: flag }, () => this.selectCharset('+' + flag));\n this._parser.registerEscHandler({ intermediates: '-', final: flag }, () => this.selectCharset('-' + flag));\n this._parser.registerEscHandler({ intermediates: '.', final: flag }, () => this.selectCharset('.' + flag));\n this._parser.registerEscHandler({ intermediates: '/', final: flag }, () => this.selectCharset('/' + flag)); // TODO: supported?\n }\n this._parser.registerEscHandler({ intermediates: '#', final: '8' }, () => this.screenAlignmentPattern());\n\n /**\n * error handler\n */\n this._parser.setErrorHandler((state: IParsingState) => {\n this._logService.error('Parsing error: ', state);\n return state;\n });\n\n /**\n * DCS handler\n */\n this._parser.registerDcsHandler({ intermediates: '$', final: 'q' }, new DcsHandler((data, params) => this.requestStatusString(data, params)));\n }\n\n /**\n * Async parse support.\n */\n private _preserveStack(cursorStartX: number, cursorStartY: number, decodedLength: number, position: number): void {\n this._parseStack.paused = true;\n this._parseStack.cursorStartX = cursorStartX;\n this._parseStack.cursorStartY = cursorStartY;\n this._parseStack.decodedLength = decodedLength;\n this._parseStack.position = position;\n }\n\n private _logSlowResolvingAsync(p: Promise): void {\n // log a limited warning about an async handler taking too long\n if (this._logService.logLevel <= LogLevelEnum.WARN) {\n let slowTimeout: ReturnType | undefined;\n const slowPromise = new Promise((_res, rej) => {\n slowTimeout = setTimeout(() => rej('#SLOW_TIMEOUT'), Constants.SLOW_ASYNC_LIMIT);\n });\n Promise.race([p, slowPromise])\n .then(() => {\n if (slowTimeout !== undefined) {\n clearTimeout(slowTimeout);\n }\n }, err => {\n if (slowTimeout !== undefined) {\n clearTimeout(slowTimeout);\n }\n if (err !== '#SLOW_TIMEOUT') {\n throw err;\n }\n console.warn(`async parser handler taking longer than ${Constants.SLOW_ASYNC_LIMIT} ms`);\n });\n }\n }\n\n private _getCurrentLinkId(): number {\n return this._curAttrData.extended.urlId;\n }\n\n /**\n * Parse call with async handler support.\n *\n * Whether the stack state got preserved for the next call, is indicated by the return value:\n * - undefined (void):\n * all handlers were sync, no stack save, continue normally with next chunk\n * - Promise\\:\n * execution stopped at async handler, stack saved, continue with same chunk and the promise\n * resolve value as `promiseResult` until the method returns `undefined`\n *\n * Note: This method should only be called by `Terminal.write` to ensure correct execution order\n * and proper continuation of async parser handlers.\n */\n public parse(data: string | Uint8Array, promiseResult?: boolean): void | Promise {\n let result: void | Promise;\n let cursorStartX = this._activeBuffer.x;\n let cursorStartY = this._activeBuffer.y;\n let start = 0;\n const wasPaused = this._parseStack.paused;\n\n if (wasPaused) {\n // assumption: _parseBuffer never mutates between async calls\n if (result = this._parser.parse(this._parseBuffer, this._parseStack.decodedLength, promiseResult)) {\n this._logSlowResolvingAsync(result);\n return result;\n }\n cursorStartX = this._parseStack.cursorStartX;\n cursorStartY = this._parseStack.cursorStartY;\n this._parseStack.paused = false;\n if (data.length > Constants.MAX_PARSEBUFFER_LENGTH) {\n start = this._parseStack.position + Constants.MAX_PARSEBUFFER_LENGTH;\n }\n }\n\n // Log debug data, the log level gate is to prevent extra work in this hot path\n if (this._logService.logLevel <= LogLevelEnum.DEBUG) {\n this._logService.debug(`parsing data ${typeof data === 'string' ? ` \"${data}\"` : ` \"${Array.prototype.map.call(data, e => String.fromCharCode(e)).join('')}\"`}`);\n }\n if (this._logService.logLevel === LogLevelEnum.TRACE) {\n this._logService.trace(`parsing data (codes)`, typeof data === 'string'\n ? data.split('').map(e => e.charCodeAt(0))\n : data\n );\n }\n\n // resize input buffer if needed\n if (this._parseBuffer.length < data.length) {\n if (this._parseBuffer.length < Constants.MAX_PARSEBUFFER_LENGTH) {\n this._parseBuffer = new Uint32Array(Math.min(data.length, Constants.MAX_PARSEBUFFER_LENGTH));\n }\n }\n\n // Clear the dirty row service so we know which lines changed as a result of parsing\n // Important: do not clear between async calls, otherwise we lost pending update information.\n if (!wasPaused) {\n this._dirtyRowTracker.clearRange();\n }\n\n // process big data in smaller chunks\n if (data.length > Constants.MAX_PARSEBUFFER_LENGTH) {\n for (let i = start; i < data.length; i += Constants.MAX_PARSEBUFFER_LENGTH) {\n const end = i + Constants.MAX_PARSEBUFFER_LENGTH < data.length ? i + Constants.MAX_PARSEBUFFER_LENGTH : data.length;\n const len = (typeof data === 'string')\n ? this._stringDecoder.decode(data.substring(i, end), this._parseBuffer)\n : this._utf8Decoder.decode(data.subarray(i, end), this._parseBuffer);\n if (result = this._parser.parse(this._parseBuffer, len)) {\n this._preserveStack(cursorStartX, cursorStartY, len, i);\n this._logSlowResolvingAsync(result);\n return result;\n }\n }\n } else {\n if (!wasPaused) {\n const len = (typeof data === 'string')\n ? this._stringDecoder.decode(data, this._parseBuffer)\n : this._utf8Decoder.decode(data, this._parseBuffer);\n if (result = this._parser.parse(this._parseBuffer, len)) {\n this._preserveStack(cursorStartX, cursorStartY, len, 0);\n this._logSlowResolvingAsync(result);\n return result;\n }\n }\n }\n\n if (this._activeBuffer.x !== cursorStartX || this._activeBuffer.y !== cursorStartY) {\n this._onCursorMove.fire();\n }\n\n // Refresh any dirty rows accumulated as part of parsing, fire only for rows within the\n // _viewport_ which is relative to ydisp, not relative to ybase.\n const viewportEnd = this._dirtyRowTracker.end + (this._bufferService.buffer.ybase - this._bufferService.buffer.ydisp);\n const viewportStart = this._dirtyRowTracker.start + (this._bufferService.buffer.ybase - this._bufferService.buffer.ydisp);\n if (viewportStart < this._bufferService.rows) {\n this._onRequestRefreshRows.fire({\n start: Math.min(viewportStart, this._bufferService.rows - 1),\n end: Math.min(viewportEnd, this._bufferService.rows - 1)\n });\n }\n }\n\n public print(data: Uint32Array, start: number, end: number): void {\n let code: number;\n let chWidth: number;\n const charset = this._charsetService.charset;\n const screenReaderMode = this._optionsService.rawOptions.screenReaderMode;\n const cols = this._bufferService.cols;\n const wraparoundMode = this._coreService.decPrivateModes.wraparound;\n const insertMode = this._coreService.modes.insertMode;\n const curAttr = this._curAttrData;\n let bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y);\n\n // Defensive check: bufferRow can be undefined if a resize occurred mid-write due to async\n // scheduling gaps in WriteBuffer. See https://github.com/xtermjs/xterm.js/issues/5597\n if (!bufferRow) {\n return;\n }\n\n this._dirtyRowTracker.markDirty(this._activeBuffer.y);\n\n // handle wide chars: reset start_cell-1 if we would overwrite the second cell of a wide char\n if (this._activeBuffer.x && end - start > 0 && bufferRow.getWidth(this._activeBuffer.x - 1) === 2) {\n bufferRow.setCellFromCodepoint(this._activeBuffer.x - 1, 0, 1, curAttr);\n }\n\n let precedingJoinState = this._parser.precedingJoinState;\n for (let pos = start; pos < end; ++pos) {\n code = data[pos];\n\n // Soft hyphen's (U+00AD) behavior is ambiguous and differs across terminals. We opt to treat\n // it as a zero-width hint to text layout engines and simply ignore it.\n if (code === 0xAD) {\n continue;\n }\n\n // get charset replacement character\n // charset is only defined for ASCII, therefore we only\n // search for an replacement char if code < 127\n if (code < 127 && charset) {\n const ch = charset[String.fromCharCode(code)];\n if (ch) {\n code = ch.charCodeAt(0);\n }\n }\n\n const currentInfo = this._unicodeService.charProperties(code, precedingJoinState);\n chWidth = UnicodeService.extractWidth(currentInfo);\n const shouldJoin = UnicodeService.extractShouldJoin(currentInfo);\n const oldWidth = shouldJoin ? UnicodeService.extractWidth(precedingJoinState) : 0;\n precedingJoinState = currentInfo;\n\n if (screenReaderMode) {\n this._onA11yChar.fire(stringFromCodePoint(code));\n }\n const linkId = this._getCurrentLinkId();\n if (linkId) {\n this._oscLinkService.addLineToLink(linkId, this._activeBuffer.ybase + this._activeBuffer.y);\n }\n\n // goto next line if ch would overflow\n // NOTE: To avoid costly width checks here,\n // the terminal does not allow a cols < 2.\n if (this._activeBuffer.x + chWidth - oldWidth > cols) {\n // autowrap - DECAWM\n // automatically wraps to the beginning of the next line\n if (wraparoundMode) {\n const oldRow = bufferRow;\n let oldCol = this._activeBuffer.x - oldWidth;\n this._activeBuffer.x = oldWidth;\n this._activeBuffer.y++;\n if (this._activeBuffer.y === this._activeBuffer.scrollBottom + 1) {\n this._activeBuffer.y--;\n this._bufferService.scroll(this._eraseAttrData(), true);\n } else {\n if (this._activeBuffer.y >= this._bufferService.rows) {\n this._activeBuffer.y = this._bufferService.rows - 1;\n }\n // The line already exists (eg. the initial viewport), mark it as a\n // wrapped line\n this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!.isWrapped = true;\n }\n // row changed, get it again\n bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y);\n if (!bufferRow) {\n return;\n }\n if (oldWidth > 0 && bufferRow instanceof BufferLine) {\n // Combining character widens 1 column to 2.\n // Move old character to next line.\n bufferRow.copyCellsFrom(oldRow as BufferLine,\n oldCol, 0, oldWidth, false);\n }\n // clear left over cells to the right\n while (oldCol < cols) {\n oldRow.setCellFromCodepoint(oldCol++, 0, 1, curAttr);\n }\n } else {\n this._activeBuffer.x = cols - 1;\n if (chWidth === 2) {\n // FIXME: check for xterm behavior\n // What to do here? We got a wide char that does not fit into last cell\n continue;\n }\n }\n }\n\n // insert combining char at last cursor position\n // this._activeBuffer.x should never be 0 for a combining char\n // since they always follow a cell consuming char\n // therefore we can test for this._activeBuffer.x to avoid overflow left\n if (shouldJoin && this._activeBuffer.x) {\n const offset = bufferRow.getWidth(this._activeBuffer.x - 1) ? 1 : 2;\n // if empty cell after fullwidth, need to go 2 cells back\n // it is save to step 2 cells back here\n // since an empty cell is only set by fullwidth chars\n bufferRow.addCodepointToCell(this._activeBuffer.x - offset,\n code, chWidth);\n for (let delta = chWidth - oldWidth; --delta >= 0;) {\n bufferRow.setCellFromCodepoint(this._activeBuffer.x++, 0, 0, curAttr);\n }\n continue;\n }\n\n // insert mode: move characters to right\n if (insertMode) {\n // right shift cells according to the width\n bufferRow.insertCells(this._activeBuffer.x, chWidth - oldWidth, this._activeBuffer.getNullCell(curAttr));\n // test last cell - since the last cell has only room for\n // a halfwidth char any fullwidth shifted there is lost\n // and will be set to empty cell\n if (bufferRow.getWidth(cols - 1) === 2) {\n bufferRow.setCellFromCodepoint(cols - 1, NULL_CELL_CODE, NULL_CELL_WIDTH, curAttr);\n }\n }\n\n // write current char to buffer and advance cursor\n bufferRow.setCellFromCodepoint(this._activeBuffer.x++, code, chWidth, curAttr);\n\n // fullwidth char - also set next cell to placeholder stub and advance cursor\n // for graphemes bigger than fullwidth we can simply loop to zero\n // we already made sure above, that this._activeBuffer.x + chWidth will not overflow right\n if (chWidth > 0) {\n while (--chWidth) {\n // other than a regular empty cell a cell following a wide char has no width\n bufferRow.setCellFromCodepoint(this._activeBuffer.x++, 0, 0, curAttr);\n }\n }\n }\n\n this._parser.precedingJoinState = precedingJoinState;\n\n // handle wide chars: reset cell to the right if it is second cell of a wide char\n if (this._activeBuffer.x < cols && end - start > 0 && bufferRow.getWidth(this._activeBuffer.x) === 0 && !bufferRow.hasContent(this._activeBuffer.x)) {\n bufferRow.setCellFromCodepoint(this._activeBuffer.x, 0, 1, curAttr);\n }\n\n this._dirtyRowTracker.markDirty(this._activeBuffer.y);\n }\n\n /**\n * Forward registerCsiHandler from parser.\n */\n public registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise): IDisposable {\n if (id.final === 't' && !id.prefix && !id.intermediates) {\n // security: always check whether window option is allowed\n return this._parser.registerCsiHandler(id, params => {\n if (!paramToWindowOption(params.params[0], this._optionsService.rawOptions.windowOptions)) {\n return true;\n }\n return callback(params);\n });\n }\n return this._parser.registerCsiHandler(id, callback);\n }\n\n /**\n * Forward registerDcsHandler from parser.\n */\n public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise): IDisposable {\n return this._parser.registerDcsHandler(id, new DcsHandler(callback));\n }\n\n /**\n * Forward registerEscHandler from parser.\n */\n public registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise): IDisposable {\n return this._parser.registerEscHandler(id, callback);\n }\n\n /**\n * Forward registerOscHandler from parser.\n */\n public registerOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable {\n return this._parser.registerOscHandler(ident, new OscHandler(callback));\n }\n\n /**\n * Forward registerApcHandler from parser.\n */\n public registerApcHandler(id: IFunctionIdentifier, callback: (data: string) => boolean | Promise): IDisposable {\n return this._parser.registerApcHandler(id, new ApcHandler(callback));\n }\n\n /**\n * BEL\n * Bell (Ctrl-G).\n *\n * @vt: #Y C0 BEL \"Bell\" \"\\a, \\x07\" \"Ring the bell.\"\n * The behavior of the bell is further customizable with `ITerminalOptions.bellStyle`\n * and `ITerminalOptions.bellSound`.\n */\n public bell(): boolean {\n this._onRequestBell.fire();\n return true;\n }\n\n /**\n * LF\n * Line Feed or New Line (NL). (LF is Ctrl-J).\n *\n * @vt: #Y C0 LF \"Line Feed\" \"\\n, \\x0A\" \"Move the cursor one row down, scrolling if needed.\"\n * Scrolling is restricted to scroll margins and will only happen on the bottom line.\n *\n * @vt: #Y C0 VT \"Vertical Tabulation\" \"\\v, \\x0B\" \"Treated as LF.\"\n * @vt: #Y C0 FF \"Form Feed\" \"\\f, \\x0C\" \"Treated as LF.\"\n */\n public lineFeed(): boolean {\n this._dirtyRowTracker.markDirty(this._activeBuffer.y);\n if (this._optionsService.rawOptions.convertEol) {\n this._activeBuffer.x = 0;\n }\n this._activeBuffer.y++;\n if (this._activeBuffer.y === this._activeBuffer.scrollBottom + 1) {\n this._activeBuffer.y--;\n this._bufferService.scroll(this._eraseAttrData());\n } else if (this._activeBuffer.y >= this._bufferService.rows) {\n this._activeBuffer.y = this._bufferService.rows - 1;\n } else {\n // There was an explicit line feed (not just a carriage return), so clear the wrapped state of\n // the line. This is particularly important on conpty/Windows where revisiting lines to\n // reprint is common, especially on resize. Note that the windowsMode wrapped line heuristics\n // can mess with this so windowsMode should be disabled, which is recommended on Windows build\n // 21376 and above.\n this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!.isWrapped = false;\n }\n // If the end of the line is hit, prevent this action from wrapping around to the next line.\n if (this._activeBuffer.x >= this._bufferService.cols) {\n this._activeBuffer.x--;\n }\n this._dirtyRowTracker.markDirty(this._activeBuffer.y);\n\n this._onLineFeed.fire();\n return true;\n }\n\n /**\n * CR\n * Carriage Return (Ctrl-M).\n *\n * @vt: #Y C0 CR \"Carriage Return\" \"\\r, \\x0D\" \"Move the cursor to the beginning of the row.\"\n */\n public carriageReturn(): boolean {\n this._activeBuffer.x = 0;\n return true;\n }\n\n /**\n * BS\n * Backspace (Ctrl-H).\n *\n * @vt: #Y C0 BS \"Backspace\" \"\\b, \\x08\" \"Move the cursor one position to the left.\"\n * By default it is not possible to move the cursor past the leftmost position.\n * If `reverse wrap-around` (`CSI ? 45 h`) is set, a previous soft line wrap (DECAWM)\n * can be undone with BS within the scroll margins. In that case the cursor will wrap back\n * to the end of the previous row. Note that it is not possible to peek back into the scrollbuffer\n * with the cursor, thus at the home position (top-leftmost cell) this has no effect.\n */\n public backspace(): boolean {\n // reverse wrap-around is disabled\n if (!this._coreService.decPrivateModes.reverseWraparound) {\n this._restrictCursor();\n if (this._activeBuffer.x > 0) {\n this._activeBuffer.x--;\n }\n return true;\n }\n\n // reverse wrap-around is enabled\n // other than for normal operation mode, reverse wrap-around allows the cursor\n // to be at x=cols to be able to address the last cell of a row by BS\n this._restrictCursor(this._bufferService.cols);\n\n if (this._activeBuffer.x > 0) {\n this._activeBuffer.x--;\n } else {\n /**\n * reverse wrap-around handling:\n * Our implementation deviates from xterm on purpose. Details:\n * - only previous soft NLs can be reversed (isWrapped=true)\n * - only works within scrollborders (top/bottom, left/right not yet supported)\n * - cannot peek into scrollbuffer\n * - any cursor movement sequence keeps working as expected\n */\n if (this._activeBuffer.x === 0\n && this._activeBuffer.y > this._activeBuffer.scrollTop\n && this._activeBuffer.y <= this._activeBuffer.scrollBottom\n && this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)?.isWrapped) {\n this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!.isWrapped = false;\n this._activeBuffer.y--;\n this._activeBuffer.x = this._bufferService.cols - 1;\n // find last taken cell - last cell can have 3 different states:\n // - hasContent(true) + hasWidth(1): narrow char - we are done\n // - hasWidth(0): second part of wide char - we are done\n // - hasContent(false) + hasWidth(1): empty cell due to early wrapping wide char, go one\n // cell further back\n const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!;\n if (line.hasWidth(this._activeBuffer.x) && !line.hasContent(this._activeBuffer.x)) {\n this._activeBuffer.x--;\n // We do this only once, since width=1 + hasContent=false currently happens only once\n // before early wrapping of a wide char.\n // This needs to be fixed once we support graphemes taking more than 2 cells.\n }\n }\n }\n this._restrictCursor();\n return true;\n }\n\n /**\n * TAB\n * Horizontal Tab (HT) (Ctrl-I).\n *\n * @vt: #Y C0 HT \"Horizontal Tabulation\" \"\\t, \\x09\" \"Move the cursor to the next character tab stop.\"\n */\n public tab(): boolean {\n if (this._activeBuffer.x >= this._bufferService.cols) {\n return true;\n }\n const originalX = this._activeBuffer.x;\n this._activeBuffer.x = this._activeBuffer.nextStop();\n if (this._optionsService.rawOptions.screenReaderMode) {\n this._onA11yTab.fire(this._activeBuffer.x - originalX);\n }\n return true;\n }\n\n /**\n * SO\n * Shift Out (Ctrl-N) -> Switch to Alternate Character Set. This invokes the\n * G1 character set.\n *\n * @vt: #P[Only limited ISO-2022 charset support.] C0 SO \"Shift Out\" \"\\x0E\" \"Switch to an alternative character set.\"\n */\n public shiftOut(): boolean {\n this._charsetService.setgLevel(1);\n return true;\n }\n\n /**\n * SI\n * Shift In (Ctrl-O) -> Switch to Standard Character Set. This invokes the G0\n * character set (the default).\n *\n * @vt: #Y C0 SI \"Shift In\" \"\\x0F\" \"Return to regular character set after Shift Out.\"\n */\n public shiftIn(): boolean {\n this._charsetService.setgLevel(0);\n return true;\n }\n\n /**\n * Restrict cursor to viewport size / scroll margin (origin mode).\n */\n private _restrictCursor(maxCol: number = this._bufferService.cols - 1): void {\n this._activeBuffer.x = Math.min(maxCol, Math.max(0, this._activeBuffer.x));\n this._activeBuffer.y = this._coreService.decPrivateModes.origin\n ? Math.min(this._activeBuffer.scrollBottom, Math.max(this._activeBuffer.scrollTop, this._activeBuffer.y))\n : Math.min(this._bufferService.rows - 1, Math.max(0, this._activeBuffer.y));\n this._dirtyRowTracker.markDirty(this._activeBuffer.y);\n }\n\n /**\n * Set absolute cursor position.\n */\n private _setCursor(x: number, y: number): void {\n this._dirtyRowTracker.markDirty(this._activeBuffer.y);\n if (this._coreService.decPrivateModes.origin) {\n this._activeBuffer.x = x;\n this._activeBuffer.y = this._activeBuffer.scrollTop + y;\n } else {\n this._activeBuffer.x = x;\n this._activeBuffer.y = y;\n }\n this._restrictCursor();\n this._dirtyRowTracker.markDirty(this._activeBuffer.y);\n }\n\n /**\n * Set relative cursor position.\n */\n private _moveCursor(x: number, y: number): void {\n // for relative changes we have to make sure we are within 0 .. cols/rows - 1\n // before calculating the new position\n this._restrictCursor();\n this._setCursor(this._activeBuffer.x + x, this._activeBuffer.y + y);\n }\n\n /**\n * CSI Ps A\n * Cursor Up Ps Times (default = 1) (CUU).\n *\n * @vt: #Y CSI CUU \"Cursor Up\" \"CSI Ps A\" \"Move cursor `Ps` times up (default=1).\"\n * If the cursor would pass the top scroll margin, it will stop there.\n */\n public cursorUp(params: IParams): boolean {\n // stop at scrollTop\n const diffToTop = this._activeBuffer.y - this._activeBuffer.scrollTop;\n if (diffToTop >= 0) {\n this._moveCursor(0, -Math.min(diffToTop, params.params[0] || 1));\n } else {\n this._moveCursor(0, -(params.params[0] || 1));\n }\n return true;\n }\n\n /**\n * CSI Ps B\n * Cursor Down Ps Times (default = 1) (CUD).\n *\n * @vt: #Y CSI CUD \"Cursor Down\" \"CSI Ps B\" \"Move cursor `Ps` times down (default=1).\"\n * If the cursor would pass the bottom scroll margin, it will stop there.\n */\n public cursorDown(params: IParams): boolean {\n // stop at scrollBottom\n const diffToBottom = this._activeBuffer.scrollBottom - this._activeBuffer.y;\n if (diffToBottom >= 0) {\n this._moveCursor(0, Math.min(diffToBottom, params.params[0] || 1));\n } else {\n this._moveCursor(0, params.params[0] || 1);\n }\n return true;\n }\n\n /**\n * CSI Ps C\n * Cursor Forward Ps Times (default = 1) (CUF).\n *\n * @vt: #Y CSI CUF \"Cursor Forward\" \"CSI Ps C\" \"Move cursor `Ps` times forward (default=1).\"\n */\n public cursorForward(params: IParams): boolean {\n this._moveCursor(params.params[0] || 1, 0);\n return true;\n }\n\n /**\n * CSI Ps D\n * Cursor Backward Ps Times (default = 1) (CUB).\n *\n * @vt: #Y CSI CUB \"Cursor Backward\" \"CSI Ps D\" \"Move cursor `Ps` times backward (default=1).\"\n */\n public cursorBackward(params: IParams): boolean {\n this._moveCursor(-(params.params[0] || 1), 0);\n return true;\n }\n\n /**\n * CSI Ps E\n * Cursor Next Line Ps Times (default = 1) (CNL).\n * Other than cursorDown (CUD) also set the cursor to first column.\n *\n * @vt: #Y CSI CNL \"Cursor Next Line\" \"CSI Ps E\" \"Move cursor `Ps` times down (default=1) and to the first column.\"\n * Same as CUD, additionally places the cursor at the first column.\n */\n public cursorNextLine(params: IParams): boolean {\n this.cursorDown(params);\n this._activeBuffer.x = 0;\n return true;\n }\n\n /**\n * CSI Ps F\n * Cursor Previous Line Ps Times (default = 1) (CPL).\n * Other than cursorUp (CUU) also set the cursor to first column.\n *\n * @vt: #Y CSI CPL \"Cursor Backward\" \"CSI Ps F\" \"Move cursor `Ps` times up (default=1) and to the first column.\"\n * Same as CUU, additionally places the cursor at the first column.\n */\n public cursorPrecedingLine(params: IParams): boolean {\n this.cursorUp(params);\n this._activeBuffer.x = 0;\n return true;\n }\n\n /**\n * CSI Ps G\n * Cursor Character Absolute [column] (default = [row,1]) (CHA).\n *\n * @vt: #Y CSI CHA \"Cursor Horizontal Absolute\" \"CSI Ps G\" \"Move cursor to `Ps`-th column of the active row (default=1).\"\n */\n public cursorCharAbsolute(params: IParams): boolean {\n this._setCursor((params.params[0] || 1) - 1, this._activeBuffer.y);\n return true;\n }\n\n /**\n * CSI Ps ; Ps H\n * Cursor Position [row;column] (default = [1,1]) (CUP).\n *\n * @vt: #Y CSI CUP \"Cursor Position\" \"CSI Ps ; Ps H\" \"Set cursor to position [`Ps`, `Ps`] (default = [1, 1]).\"\n * If ORIGIN mode is set, places the cursor to the absolute position within the scroll margins.\n * If ORIGIN mode is not set, places the cursor to the absolute position within the viewport.\n * Note that the coordinates are 1-based, thus the top left position starts at `1 ; 1`.\n */\n public cursorPosition(params: IParams): boolean {\n this._setCursor(\n // col\n (params.length >= 2) ? (params.params[1] || 1) - 1 : 0,\n // row\n (params.params[0] || 1) - 1\n );\n return true;\n }\n\n /**\n * CSI Pm ` Character Position Absolute\n * [column] (default = [row,1]) (HPA).\n * Currently same functionality as CHA.\n *\n * @vt: #Y CSI HPA \"Horizontal Position Absolute\" \"CSI Ps ` \" \"Same as CHA.\"\n */\n public charPosAbsolute(params: IParams): boolean {\n this._setCursor((params.params[0] || 1) - 1, this._activeBuffer.y);\n return true;\n }\n\n /**\n * CSI Pm a Character Position Relative\n * [columns] (default = [row,col+1]) (HPR)\n *\n * @vt: #Y CSI HPR \"Horizontal Position Relative\" \"CSI Ps a\" \"Same as CUF.\"\n */\n public hPositionRelative(params: IParams): boolean {\n this._moveCursor(params.params[0] || 1, 0);\n return true;\n }\n\n /**\n * CSI Pm d Vertical Position Absolute (VPA)\n * [row] (default = [1,column])\n *\n * @vt: #Y CSI VPA \"Vertical Position Absolute\" \"CSI Ps d\" \"Move cursor to `Ps`-th row (default=1).\"\n */\n public linePosAbsolute(params: IParams): boolean {\n this._setCursor(this._activeBuffer.x, (params.params[0] || 1) - 1);\n return true;\n }\n\n /**\n * CSI Pm e Vertical Position Relative (VPR)\n * [rows] (default = [row+1,column])\n * reuse CSI Ps B ?\n *\n * @vt: #Y CSI VPR \"Vertical Position Relative\" \"CSI Ps e\" \"Move cursor `Ps` times down (default=1).\"\n */\n public vPositionRelative(params: IParams): boolean {\n this._moveCursor(0, params.params[0] || 1);\n return true;\n }\n\n /**\n * CSI Ps ; Ps f\n * Horizontal and Vertical Position [row;column] (default =\n * [1,1]) (HVP).\n * Same as CUP.\n *\n * @vt: #Y CSI HVP \"Horizontal and Vertical Position\" \"CSI Ps ; Ps f\" \"Same as CUP.\"\n */\n public hVPosition(params: IParams): boolean {\n this.cursorPosition(params);\n return true;\n }\n\n /**\n * CSI Ps g Tab Clear (TBC).\n * Ps = 0 -> Clear Current Column (default).\n * Ps = 3 -> Clear All.\n * Potentially:\n * Ps = 2 -> Clear Stops on Line.\n * http://vt100.net/annarbor/aaa-ug/section6.html\n *\n * @vt: #Y CSI TBC \"Tab Clear\" \"CSI Ps g\" \"Clear tab stops at current position (0) or all (3) (default=0).\"\n * Clearing tabstops off the active row (Ps = 2, VT100) is currently not supported.\n */\n public tabClear(params: IParams): boolean {\n const param = params.params[0];\n if (param === 0) {\n delete this._activeBuffer.tabs[this._activeBuffer.x];\n } else if (param === 3) {\n this._activeBuffer.tabs = {};\n }\n return true;\n }\n\n /**\n * CSI Ps I\n * Cursor Forward Tabulation Ps tab stops (default = 1) (CHT).\n *\n * @vt: #Y CSI CHT \"Cursor Horizontal Tabulation\" \"CSI Ps I\" \"Move cursor `Ps` times tabs forward (default=1).\"\n */\n public cursorForwardTab(params: IParams): boolean {\n if (this._activeBuffer.x >= this._bufferService.cols) {\n return true;\n }\n let param = params.params[0] || 1;\n while (param--) {\n this._activeBuffer.x = this._activeBuffer.nextStop();\n }\n return true;\n }\n\n /**\n * CSI Ps Z Cursor Backward Tabulation Ps tab stops (default = 1) (CBT).\n *\n * @vt: #Y CSI CBT \"Cursor Backward Tabulation\" \"CSI Ps Z\" \"Move cursor `Ps` tabs backward (default=1).\"\n */\n public cursorBackwardTab(params: IParams): boolean {\n if (this._activeBuffer.x >= this._bufferService.cols) {\n return true;\n }\n let param = params.params[0] || 1;\n\n while (param--) {\n this._activeBuffer.x = this._activeBuffer.prevStop();\n }\n return true;\n }\n\n /**\n * CSI Ps \" q Select Character Protection Attribute (DECSCA).\n *\n * @vt: #Y CSI DECSCA \"Select Character Protection Attribute\" \"CSI Ps \" q\" \"Whether DECSED and DECSEL can erase (0=default, 2) or not (1).\"\n */\n public selectProtected(params: IParams): boolean {\n const p = params.params[0];\n if (p === 1) this._curAttrData.bg |= BgFlags.PROTECTED;\n if (p === 2 || p === 0) this._curAttrData.bg &= ~BgFlags.PROTECTED;\n return true;\n }\n\n\n /**\n * Helper method to erase cells in a terminal row.\n * The cell gets replaced with the eraseChar of the terminal.\n * @param y The row index relative to the viewport.\n * @param start The start x index of the range to be erased.\n * @param end The end x index of the range to be erased (exclusive).\n * @param clearWrap clear the isWrapped flag\n * @param respectProtect Whether to respect the protection attribute (DECSCA).\n */\n private _eraseInBufferLine(y: number, start: number, end: number, clearWrap: boolean = false, respectProtect: boolean = false): void {\n const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y);\n if (!line) {\n return;\n }\n line.replaceCells(\n start,\n end,\n this._activeBuffer.getNullCell(this._eraseAttrData()),\n respectProtect\n );\n if (clearWrap) {\n line.isWrapped = false;\n }\n }\n\n /**\n * Helper method to reset cells in a terminal row. The cell gets replaced with the eraseChar of\n * the terminal and the isWrapped property is set to false.\n * @param y row index\n */\n private _resetBufferLine(y: number, respectProtect: boolean = false): void {\n const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y);\n if (line) {\n line.fill(this._activeBuffer.getNullCell(this._eraseAttrData()), respectProtect);\n this._bufferService.buffer.clearMarkers(this._activeBuffer.ybase + y);\n line.isWrapped = false;\n }\n }\n\n /**\n * CSI Ps J Erase in Display (ED).\n * Ps = 0 -> Erase Below (default).\n * Ps = 1 -> Erase Above.\n * Ps = 2 -> Erase All.\n * Ps = 3 -> Erase Saved Lines (xterm).\n * CSI ? Ps J\n * Erase in Display (DECSED).\n * Ps = 0 -> Selective Erase Below (default).\n * Ps = 1 -> Selective Erase Above.\n * Ps = 2 -> Selective Erase All.\n *\n * @vt: #Y CSI ED \"Erase In Display\" \"CSI Ps J\" \"Erase various parts of the viewport.\"\n * Supported param values:\n *\n * | Ps | Effect |\n * | -- | ------------------------------------------------------------ |\n * | 0 | Erase from the cursor through the end of the viewport. |\n * | 1 | Erase from the beginning of the viewport through the cursor. |\n * | 2 | Erase complete viewport. |\n * | 3 | Erase scrollback. |\n *\n * @vt: #Y CSI DECSED \"Selective Erase In Display\" \"CSI ? Ps J\" \"Same as ED with respecting protection flag.\"\n */\n public eraseInDisplay(params: IParams, respectProtect: boolean = false): boolean {\n this._restrictCursor(this._bufferService.cols);\n let j;\n switch (params.params[0]) {\n case 0:\n j = this._activeBuffer.y;\n this._dirtyRowTracker.markDirty(j);\n this._eraseInBufferLine(j++, this._activeBuffer.x, this._bufferService.cols, this._activeBuffer.x === 0, respectProtect);\n for (; j < this._bufferService.rows; j++) {\n this._resetBufferLine(j, respectProtect);\n }\n this._dirtyRowTracker.markDirty(j);\n break;\n case 1:\n j = this._activeBuffer.y;\n this._dirtyRowTracker.markDirty(j);\n // Deleted front part of line and everything before. This line will no longer be wrapped.\n this._eraseInBufferLine(j, 0, this._activeBuffer.x + 1, true, respectProtect);\n if (this._activeBuffer.x + 1 >= this._bufferService.cols) {\n // Deleted entire previous line. This next line can no longer be wrapped.\n const nextLine = this._activeBuffer.lines.get(j + 1);\n if (nextLine) {\n nextLine.isWrapped = false;\n }\n }\n while (j--) {\n this._resetBufferLine(j, respectProtect);\n }\n this._dirtyRowTracker.markDirty(0);\n break;\n case 2:\n if (this._optionsService.rawOptions.scrollOnEraseInDisplay) {\n j = this._bufferService.rows;\n this._dirtyRowTracker.markRangeDirty(0, j - 1);\n while (j--) {\n const currentLine = this._activeBuffer.lines.get(this._activeBuffer.ybase + j);\n if (currentLine?.getTrimmedLength()) {\n break;\n }\n }\n for (; j >= 0; j--) {\n this._bufferService.scroll(this._eraseAttrData());\n }\n }\n else {\n j = this._bufferService.rows;\n this._dirtyRowTracker.markDirty(j - 1);\n while (j--) {\n this._resetBufferLine(j, respectProtect);\n }\n this._dirtyRowTracker.markDirty(0);\n }\n break;\n case 3:\n // Clear scrollback (everything not in viewport)\n const scrollBackSize = this._activeBuffer.lines.length - this._bufferService.rows;\n if (scrollBackSize > 0) {\n this._activeBuffer.lines.trimStart(scrollBackSize);\n this._activeBuffer.ybase = Math.max(this._activeBuffer.ybase - scrollBackSize, 0);\n this._activeBuffer.ydisp = Math.max(this._activeBuffer.ydisp - scrollBackSize, 0);\n // isUserScrolling tracks the normal buffer's viewport, so ED3 on the alt\n // screen must not touch it\n if (this._activeBuffer === this._bufferService.buffers.normal) {\n this._bufferService.isUserScrolling = false;\n }\n // Force a scroll event to refresh viewport\n this._onScroll.fire(0);\n }\n break;\n }\n return true;\n }\n\n /**\n * CSI Ps K Erase in Line (EL).\n * Ps = 0 -> Erase to Right (default).\n * Ps = 1 -> Erase to Left.\n * Ps = 2 -> Erase All.\n * CSI ? Ps K\n * Erase in Line (DECSEL).\n * Ps = 0 -> Selective Erase to Right (default).\n * Ps = 1 -> Selective Erase to Left.\n * Ps = 2 -> Selective Erase All.\n *\n * @vt: #Y CSI EL \"Erase In Line\" \"CSI Ps K\" \"Erase various parts of the active row.\"\n * Supported param values:\n *\n * | Ps | Effect |\n * | -- | -------------------------------------------------------- |\n * | 0 | Erase from the cursor through the end of the row. |\n * | 1 | Erase from the beginning of the line through the cursor. |\n * | 2 | Erase complete line. |\n *\n * @vt: #Y CSI DECSEL \"Selective Erase In Line\" \"CSI ? Ps K\" \"Same as EL with respecting protecting flag.\"\n */\n public eraseInLine(params: IParams, respectProtect: boolean = false): boolean {\n this._restrictCursor(this._bufferService.cols);\n switch (params.params[0]) {\n case 0:\n this._eraseInBufferLine(this._activeBuffer.y, this._activeBuffer.x, this._bufferService.cols, this._activeBuffer.x === 0, respectProtect);\n break;\n case 1:\n this._eraseInBufferLine(this._activeBuffer.y, 0, this._activeBuffer.x + 1, false, respectProtect);\n break;\n case 2:\n this._eraseInBufferLine(this._activeBuffer.y, 0, this._bufferService.cols, true, respectProtect);\n break;\n }\n this._dirtyRowTracker.markDirty(this._activeBuffer.y);\n return true;\n }\n\n /**\n * CSI Ps L\n * Insert Ps Line(s) (default = 1) (IL).\n *\n * @vt: #Y CSI IL \"Insert Line\" \"CSI Ps L\" \"Insert `Ps` blank lines at active row (default=1).\"\n * For every inserted line at the scroll top one line at the scroll bottom gets removed.\n * The cursor is set to the first column.\n * IL has no effect if the cursor is outside the scroll margins.\n */\n public insertLines(params: IParams): boolean {\n this._restrictCursor();\n let param = params.params[0] || 1;\n\n if (this._activeBuffer.y > this._activeBuffer.scrollBottom || this._activeBuffer.y < this._activeBuffer.scrollTop) {\n return true;\n }\n\n const row: number = this._activeBuffer.ybase + this._activeBuffer.y;\n\n const scrollBottomRowsOffset = this._bufferService.rows - 1 - this._activeBuffer.scrollBottom;\n const scrollBottomAbsolute = this._bufferService.rows - 1 + this._activeBuffer.ybase - scrollBottomRowsOffset + 1;\n while (param--) {\n // test: echo -e '\\e[44m\\e[1L\\e[0m'\n // blankLine(true) - xterm/linux behavior\n this._activeBuffer.lines.splice(scrollBottomAbsolute - 1, 1);\n this._activeBuffer.lines.splice(row, 0, this._activeBuffer.getBlankLine(this._eraseAttrData()));\n }\n\n this._dirtyRowTracker.markRangeDirty(this._activeBuffer.y, this._activeBuffer.scrollBottom);\n this._activeBuffer.x = 0; // see https://vt100.net/docs/vt220-rm/chapter4.html - vt220 only?\n return true;\n }\n\n /**\n * CSI Ps M\n * Delete Ps Line(s) (default = 1) (DL).\n *\n * @vt: #Y CSI DL \"Delete Line\" \"CSI Ps M\" \"Delete `Ps` lines at active row (default=1).\"\n * For every deleted line at the scroll top one blank line at the scroll bottom gets appended.\n * The cursor is set to the first column.\n * DL has no effect if the cursor is outside the scroll margins.\n */\n public deleteLines(params: IParams): boolean {\n this._restrictCursor();\n let param = params.params[0] || 1;\n\n if (this._activeBuffer.y > this._activeBuffer.scrollBottom || this._activeBuffer.y < this._activeBuffer.scrollTop) {\n return true;\n }\n\n const row: number = this._activeBuffer.ybase + this._activeBuffer.y;\n\n let j: number;\n j = this._bufferService.rows - 1 - this._activeBuffer.scrollBottom;\n j = this._bufferService.rows - 1 + this._activeBuffer.ybase - j;\n while (param--) {\n // test: echo -e '\\e[44m\\e[1M\\e[0m'\n // blankLine(true) - xterm/linux behavior\n this._activeBuffer.lines.splice(row, 1);\n this._activeBuffer.lines.splice(j, 0, this._activeBuffer.getBlankLine(this._eraseAttrData()));\n }\n\n this._dirtyRowTracker.markRangeDirty(this._activeBuffer.y, this._activeBuffer.scrollBottom);\n this._activeBuffer.x = 0; // see https://vt100.net/docs/vt220-rm/chapter4.html - vt220 only?\n return true;\n }\n\n /**\n * CSI Ps @\n * Insert Ps (Blank) Character(s) (default = 1) (ICH).\n *\n * @vt: #Y CSI ICH \"Insert Characters\" \"CSI Ps @\" \"Insert `Ps` (blank) characters (default = 1).\"\n * The ICH sequence inserts `Ps` blank characters. The cursor remains at the beginning of the\n * blank characters. Text between the cursor and right margin moves to the right. Characters moved\n * past the right margin are lost.\n *\n *\n * FIXME: check against xterm - should not work outside of scroll margins (see VT520 manual)\n */\n public insertChars(params: IParams): boolean {\n this._restrictCursor();\n const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y);\n if (line) {\n line.insertCells(\n this._activeBuffer.x,\n params.params[0] || 1,\n this._activeBuffer.getNullCell(this._eraseAttrData())\n );\n this._dirtyRowTracker.markDirty(this._activeBuffer.y);\n }\n return true;\n }\n\n /**\n * CSI Ps P\n * Delete Ps Character(s) (default = 1) (DCH).\n *\n * @vt: #Y CSI DCH \"Delete Character\" \"CSI Ps P\" \"Delete `Ps` characters (default=1).\"\n * As characters are deleted, the remaining characters between the cursor and right margin move to\n * the left. Character attributes move with the characters. The terminal adds blank characters at\n * the right margin.\n *\n *\n * FIXME: check against xterm - should not work outside of scroll margins (see VT520 manual)\n */\n public deleteChars(params: IParams): boolean {\n this._restrictCursor();\n const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y);\n if (line) {\n line.deleteCells(\n this._activeBuffer.x,\n params.params[0] || 1,\n this._activeBuffer.getNullCell(this._eraseAttrData())\n );\n this._dirtyRowTracker.markDirty(this._activeBuffer.y);\n }\n return true;\n }\n\n /**\n * CSI Ps S Scroll up Ps lines (default = 1) (SU).\n *\n * @vt: #Y CSI SU \"Scroll Up\" \"CSI Ps S\" \"Scroll `Ps` lines up (default=1).\"\n *\n *\n * FIXME: scrolled out lines at top = 1 should add to scrollback (xterm)\n */\n public scrollUp(params: IParams): boolean {\n let param = params.params[0] || 1;\n\n while (param--) {\n this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollTop, 1);\n this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollBottom, 0, this._activeBuffer.getBlankLine(this._eraseAttrData()));\n }\n this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);\n return true;\n }\n\n /**\n * CSI Ps T Scroll down Ps lines (default = 1) (SD).\n *\n * @vt: #Y CSI SD \"Scroll Down\" \"CSI Ps T\" \"Scroll `Ps` lines down (default=1).\"\n */\n public scrollDown(params: IParams): boolean {\n let param = params.params[0] || 1;\n\n while (param--) {\n this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollBottom, 1);\n this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollTop, 0, this._activeBuffer.getBlankLine(DEFAULT_ATTR_DATA));\n }\n this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);\n return true;\n }\n\n /**\n * CSI Ps SP @ Scroll left Ps columns (default = 1) (SL) ECMA-48\n *\n * Notation: (Pn)\n * Representation: CSI Pn 02/00 04/00\n * Parameter default value: Pn = 1\n * SL causes the data in the presentation component to be moved by n character positions\n * if the line orientation is horizontal, or by n line positions if the line orientation\n * is vertical, such that the data appear to move to the left; where n equals the value of Pn.\n * The active presentation position is not affected by this control function.\n *\n * Supported:\n * - always left shift (no line orientation setting respected)\n *\n * @vt: #Y CSI SL \"Scroll Left\" \"CSI Ps SP @\" \"Scroll viewport `Ps` times to the left.\"\n * SL moves the content of all lines within the scroll margins `Ps` times to the left.\n * SL has no effect outside of the scroll margins.\n */\n public scrollLeft(params: IParams): boolean {\n if (this._activeBuffer.y > this._activeBuffer.scrollBottom || this._activeBuffer.y < this._activeBuffer.scrollTop) {\n return true;\n }\n const param = params.params[0] || 1;\n for (let y = this._activeBuffer.scrollTop; y <= this._activeBuffer.scrollBottom; ++y) {\n const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!;\n line.deleteCells(0, param, this._activeBuffer.getNullCell(this._eraseAttrData()));\n line.isWrapped = false;\n }\n this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);\n return true;\n }\n\n /**\n * CSI Ps SP A Scroll right Ps columns (default = 1) (SR) ECMA-48\n *\n * Notation: (Pn)\n * Representation: CSI Pn 02/00 04/01\n * Parameter default value: Pn = 1\n * SR causes the data in the presentation component to be moved by n character positions\n * if the line orientation is horizontal, or by n line positions if the line orientation\n * is vertical, such that the data appear to move to the right; where n equals the value of Pn.\n * The active presentation position is not affected by this control function.\n *\n * Supported:\n * - always right shift (no line orientation setting respected)\n *\n * @vt: #Y CSI SR \"Scroll Right\" \"CSI Ps SP A\" \"Scroll viewport `Ps` times to the right.\"\n * SL moves the content of all lines within the scroll margins `Ps` times to the right.\n * Content at the right margin is lost.\n * SL has no effect outside of the scroll margins.\n */\n public scrollRight(params: IParams): boolean {\n if (this._activeBuffer.y > this._activeBuffer.scrollBottom || this._activeBuffer.y < this._activeBuffer.scrollTop) {\n return true;\n }\n const param = params.params[0] || 1;\n for (let y = this._activeBuffer.scrollTop; y <= this._activeBuffer.scrollBottom; ++y) {\n const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!;\n line.insertCells(0, param, this._activeBuffer.getNullCell(this._eraseAttrData()));\n line.isWrapped = false;\n }\n this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);\n return true;\n }\n\n /**\n * CSI Pm ' }\n * Insert Ps Column(s) (default = 1) (DECIC), VT420 and up.\n *\n * @vt: #Y CSI DECIC \"Insert Columns\" \"CSI Ps ' }\" \"Insert `Ps` columns at cursor position.\"\n * DECIC inserts `Ps` times blank columns at the cursor position for all lines with the scroll\n * margins, moving content to the right. Content at the right margin is lost. DECIC has no effect\n * outside the scrolling margins.\n */\n public insertColumns(params: IParams): boolean {\n if (this._activeBuffer.y > this._activeBuffer.scrollBottom || this._activeBuffer.y < this._activeBuffer.scrollTop) {\n return true;\n }\n const param = params.params[0] || 1;\n for (let y = this._activeBuffer.scrollTop; y <= this._activeBuffer.scrollBottom; ++y) {\n const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!;\n line.insertCells(this._activeBuffer.x, param, this._activeBuffer.getNullCell(this._eraseAttrData()));\n line.isWrapped = false;\n }\n this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);\n return true;\n }\n\n /**\n * CSI Pm ' ~\n * Delete Ps Column(s) (default = 1) (DECDC), VT420 and up.\n *\n * @vt: #Y CSI DECDC \"Delete Columns\" \"CSI Ps ' ~\" \"Delete `Ps` columns at cursor position.\"\n * DECDC deletes `Ps` times columns at the cursor position for all lines with the scroll margins,\n * moving content to the left. Blank columns are added at the right margin.\n * DECDC has no effect outside the scrolling margins.\n */\n public deleteColumns(params: IParams): boolean {\n if (this._activeBuffer.y > this._activeBuffer.scrollBottom || this._activeBuffer.y < this._activeBuffer.scrollTop) {\n return true;\n }\n const param = params.params[0] || 1;\n for (let y = this._activeBuffer.scrollTop; y <= this._activeBuffer.scrollBottom; ++y) {\n const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!;\n line.deleteCells(this._activeBuffer.x, param, this._activeBuffer.getNullCell(this._eraseAttrData()));\n line.isWrapped = false;\n }\n this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);\n return true;\n }\n\n /**\n * CSI Ps X\n * Erase Ps Character(s) (default = 1) (ECH).\n *\n * @vt: #Y CSI ECH \"Erase Character\" \"CSI Ps X\" \"Erase `Ps` characters from current cursor position to the right (default=1).\"\n * ED erases `Ps` characters from current cursor position to the right.\n * ED works inside or outside the scrolling margins.\n */\n public eraseChars(params: IParams): boolean {\n this._restrictCursor();\n const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y);\n if (line) {\n line.replaceCells(\n this._activeBuffer.x,\n this._activeBuffer.x + (params.params[0] || 1),\n this._activeBuffer.getNullCell(this._eraseAttrData())\n );\n this._dirtyRowTracker.markDirty(this._activeBuffer.y);\n }\n return true;\n }\n\n /**\n * CSI Ps b Repeat the preceding graphic character Ps times (REP).\n * From ECMA 48 (@see http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-048.pdf)\n * Notation: (Pn)\n * Representation: CSI Pn 06/02\n * Parameter default value: Pn = 1\n * REP is used to indicate that the preceding character in the data stream,\n * if it is a graphic character (represented by one or more bit combinations) including SPACE,\n * is to be repeated n times, where n equals the value of Pn.\n * If the character preceding REP is a control function or part of a control function,\n * the effect of REP is not defined by this Standard.\n *\n * We extend xterm's behavior to allow repeating entire grapheme clusters.\n * This isn't 100% xterm-compatible, but it seems saner and more useful.\n * - text attrs are applied normally\n * - wrap around is respected\n * - any valid sequence resets the carried forward char\n *\n * Note: To get reset on a valid sequence working correctly without much runtime penalty, the\n * preceding codepoint is stored on the parser in `this.print` and reset during `parser.parse`.\n *\n * @vt: #Y CSI REP \"Repeat Preceding Character\" \"CSI Ps b\" \"Repeat preceding character `Ps` times (default=1).\"\n * REP repeats the previous character `Ps` times advancing the cursor, also wrapping if DECAWM is\n * set. REP has no effect if the sequence does not follow a printable ASCII character\n * (NOOP for any other sequence in between or NON ASCII characters).\n */\n public repeatPrecedingCharacter(params: IParams): boolean {\n const joinState = this._parser.precedingJoinState;\n if (!joinState) {\n return true;\n }\n // call print to insert the chars and handle correct wrapping\n const length = params.params[0] || 1;\n const chWidth = UnicodeService.extractWidth(joinState);\n const x = this._activeBuffer.x - chWidth;\n const bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!;\n const text = bufferRow.getString(x);\n const data = new Uint32Array(text.length * length);\n let idata = 0;\n for (let itext = 0; itext < text.length;) {\n const ch = text.codePointAt(itext) || 0;\n data[idata++] = ch;\n itext += ch > 0xffff ? 2 : 1;\n }\n let tlength = idata;\n for (let i = 1; i < length; ++i) {\n data.copyWithin(tlength, 0, idata);\n tlength += idata;\n }\n this.print(data, 0, tlength);\n return true;\n }\n\n /**\n * CSI Ps c Send Device Attributes (Primary DA).\n * Ps = 0 or omitted -> request attributes from terminal. The\n * response depends on the decTerminalID resource setting.\n * -> CSI ? 1 ; 2 c (``VT100 with Advanced Video Option'')\n * -> CSI ? 1 ; 0 c (``VT101 with No Options'')\n * -> CSI ? 6 c (``VT102'')\n * -> CSI ? 6 0 ; 1 ; 2 ; 6 ; 8 ; 9 ; 1 5 ; c (``VT220'')\n * The VT100-style response parameters do not mean anything by\n * themselves. VT220 parameters do, telling the host what fea-\n * tures the terminal supports:\n * Ps = 1 -> 132-columns.\n * Ps = 2 -> Printer.\n * Ps = 6 -> Selective erase.\n * Ps = 8 -> User-defined keys.\n * Ps = 9 -> National replacement character sets.\n * Ps = 1 5 -> Technical characters.\n * Ps = 2 2 -> ANSI color, e.g., VT525.\n * Ps = 2 9 -> ANSI text locator (i.e., DEC Locator mode).\n *\n * @vt: #Y CSI DA1 \"Primary Device Attributes\" \"CSI c\" \"Send primary device attributes.\"\n *\n *\n * TODO: fix and cleanup response\n */\n public sendDeviceAttributesPrimary(params: IParams): boolean {\n if (params.params[0] > 0) {\n return true;\n }\n if (this._is('xterm') || this._is('rxvt-unicode') || this._is('screen')) {\n this._coreService.triggerDataEvent(C0.ESC + '[?1;2c');\n } else if (this._is('linux')) {\n this._coreService.triggerDataEvent(C0.ESC + '[?6c');\n }\n return true;\n }\n\n /**\n * CSI > Ps c\n * Send Device Attributes (Secondary DA).\n * Ps = 0 or omitted -> request the terminal's identification\n * code. The response depends on the decTerminalID resource set-\n * ting. It should apply only to VT220 and up, but xterm extends\n * this to VT100.\n * -> CSI > Pp ; Pv ; Pc c\n * where Pp denotes the terminal type\n * Pp = 0 -> ``VT100''.\n * Pp = 1 -> ``VT220''.\n * and Pv is the firmware version (for xterm, this was originally\n * the XFree86 patch number, starting with 95). In a DEC termi-\n * nal, Pc indicates the ROM cartridge registration number and is\n * always zero.\n * More information:\n * xterm/charproc.c - line 2012, for more information.\n * vim responds with ^[[?0c or ^[[?1c after the terminal's response (?)\n *\n * @vt: #Y CSI DA2 \"Secondary Device Attributes\" \"CSI > c\" \"Send primary device attributes.\"\n *\n *\n * TODO: fix and cleanup response\n */\n public sendDeviceAttributesSecondary(params: IParams): boolean {\n if (params.params[0] > 0) {\n return true;\n }\n // xterm and urxvt\n // seem to spit this\n // out around ~370 times (?).\n if (this._is('xterm')) {\n this._coreService.triggerDataEvent(C0.ESC + '[>0;276;0c');\n } else if (this._is('rxvt-unicode')) {\n this._coreService.triggerDataEvent(C0.ESC + '[>85;95;0c');\n } else if (this._is('linux')) {\n // not supported by linux console.\n // linux console echoes parameters.\n this._coreService.triggerDataEvent(params.params[0] + 'c');\n } else if (this._is('screen')) {\n this._coreService.triggerDataEvent(C0.ESC + '[>83;40003;0c');\n }\n return true;\n }\n\n /**\n * CSI > Ps q\n * Ps = 0 => Report xterm name and version (XTVERSION).\n *\n * The response is a DCS sequence identifying the version: DCS > | text ST\n *\n * @vt: #Y CSI XTVERSION \"Report Xterm Version\" \"CSI > q\" \"Report the terminal name and version.\"\n */\n public sendXtVersion(params: IParams): boolean {\n if (params.params[0] > 0) {\n return true;\n }\n this._coreService.triggerDataEvent(`${C0.ESC}P>|xterm.js(${XTERM_VERSION})${C0.ESC}\\\\`);\n return true;\n }\n\n /**\n * Evaluate if the current terminal is the given argument.\n * @param term The terminal name to evaluate\n */\n private _is(term: string): boolean {\n return (this._optionsService.rawOptions.termName + '').startsWith(term);\n }\n\n /**\n * CSI Pm h Set Mode (SM).\n * Ps = 2 -> Keyboard Action Mode (AM).\n * Ps = 4 -> Insert Mode (IRM).\n * Ps = 1 2 -> Send/receive (SRM).\n * Ps = 2 0 -> Automatic Newline (LNM).\n *\n * @vt: #P[Only IRM is supported.] CSI SM \"Set Mode\" \"CSI Pm h\" \"Set various terminal modes.\"\n * Supported param values by SM:\n *\n * | Param | Action | Support |\n * | ----- | -------------------------------------- | ------- |\n * | 2 | Keyboard Action Mode (KAM). Always on. | #N |\n * | 4 | Insert Mode (IRM). | #Y |\n * | 12 | Send/receive (SRM). Always off. | #N |\n * | 20 | Automatic Newline (LNM). | #Y |\n */\n public setMode(params: IParams): boolean {\n for (let i = 0; i < params.length; i++) {\n switch (params.params[i]) {\n case 4:\n this._coreService.modes.insertMode = true;\n break;\n case 20:\n this._optionsService.options.convertEol = true;\n break;\n }\n }\n return true;\n }\n\n /**\n * CSI ? Pm h\n * DEC Private Mode Set (DECSET).\n * Ps = 1 -> Application Cursor Keys (DECCKM).\n * Ps = 2 -> Designate USASCII for character sets G0-G3\n * (DECANM), and set VT100 mode.\n * Ps = 3 -> 132 Column Mode (DECCOLM).\n * Ps = 4 -> Smooth (Slow) Scroll (DECSCLM).\n * Ps = 5 -> Reverse Video (DECSCNM).\n * Ps = 6 -> Origin Mode (DECOM).\n * Ps = 7 -> Wraparound Mode (DECAWM).\n * Ps = 8 -> Auto-repeat Keys (DECARM).\n * Ps = 9 -> Send Mouse X & Y on button press. See the sec-\n * tion Mouse Tracking.\n * Ps = 1 0 -> Show toolbar (rxvt).\n * Ps = 1 2 -> Start Blinking Cursor (att610).\n * Ps = 1 8 -> Print form feed (DECPFF).\n * Ps = 1 9 -> Set print extent to full screen (DECPEX).\n * Ps = 2 5 -> Show Cursor (DECTCEM).\n * Ps = 3 0 -> Show scrollbar (rxvt).\n * Ps = 3 5 -> Enable font-shifting functions (rxvt).\n * Ps = 3 8 -> Enter Tektronix Mode (DECTEK).\n * Ps = 4 0 -> Allow 80 -> 132 Mode.\n * Ps = 4 1 -> more(1) fix (see curses resource).\n * Ps = 4 2 -> Enable Nation Replacement Character sets (DECN-\n * RCM).\n * Ps = 4 4 -> Turn On Margin Bell.\n * Ps = 4 5 -> Reverse-wraparound Mode.\n * Ps = 4 6 -> Start Logging. This is normally disabled by a\n * compile-time option.\n * Ps = 4 7 -> Use Alternate Screen Buffer. (This may be dis-\n * abled by the titeInhibit resource).\n * Ps = 6 6 -> Application keypad (DECNKM).\n * Ps = 6 7 -> Backarrow key sends backspace (DECBKM).\n * Ps = 1 0 0 0 -> Send Mouse X & Y on button press and\n * release. See the section Mouse Tracking.\n * Ps = 1 0 0 1 -> Use Hilite Mouse Tracking.\n * Ps = 1 0 0 2 -> Use Cell Motion Mouse Tracking.\n * Ps = 1 0 0 3 -> Use All Motion Mouse Tracking.\n * Ps = 1 0 0 4 -> Send FocusIn/FocusOut events.\n * Ps = 1 0 0 5 -> Enable Extended Mouse Mode.\n * Ps = 1 0 1 0 -> Scroll to bottom on tty output (rxvt).\n * Ps = 1 0 1 1 -> Scroll to bottom on key press (rxvt).\n * Ps = 1 0 3 4 -> Interpret \"meta\" key, sets eighth bit.\n * (enables the eightBitInput resource).\n * Ps = 1 0 3 5 -> Enable special modifiers for Alt and Num-\n * Lock keys. (This enables the numLock resource).\n * Ps = 1 0 3 6 -> Send ESC when Meta modifies a key. (This\n * enables the metaSendsEscape resource).\n * Ps = 1 0 3 7 -> Send DEL from the editing-keypad Delete\n * key.\n * Ps = 1 0 3 9 -> Send ESC when Alt modifies a key. (This\n * enables the altSendsEscape resource).\n * Ps = 1 0 4 0 -> Keep selection even if not highlighted.\n * (This enables the keepSelection resource).\n * Ps = 1 0 4 1 -> Use the CLIPBOARD selection. (This enables\n * the selectToClipboard resource).\n * Ps = 1 0 4 2 -> Enable Urgency window manager hint when\n * Control-G is received. (This enables the bellIsUrgent\n * resource).\n * Ps = 1 0 4 3 -> Enable raising of the window when Control-G\n * is received. (enables the popOnBell resource).\n * Ps = 1 0 4 7 -> Use Alternate Screen Buffer. (This may be\n * disabled by the titeInhibit resource).\n * Ps = 1 0 4 8 -> Save cursor as in DECSC. (This may be dis-\n * abled by the titeInhibit resource).\n * Ps = 1 0 4 9 -> Save cursor as in DECSC and use Alternate\n * Screen Buffer, clearing it first. (This may be disabled by\n * the titeInhibit resource). This combines the effects of the 1\n * 0 4 7 and 1 0 4 8 modes. Use this with terminfo-based\n * applications rather than the 4 7 mode.\n * Ps = 1 0 5 0 -> Set terminfo/termcap function-key mode.\n * Ps = 1 0 5 1 -> Set Sun function-key mode.\n * Ps = 1 0 5 2 -> Set HP function-key mode.\n * Ps = 1 0 5 3 -> Set SCO function-key mode.\n * Ps = 1 0 6 0 -> Set legacy keyboard emulation (X11R6).\n * Ps = 1 0 6 1 -> Set VT220 keyboard emulation.\n * Ps = 2 0 0 4 -> Set bracketed paste mode.\n * Modes:\n * http: *vt100.net/docs/vt220-rm/chapter4.html\n *\n * @vt: #P[See below for supported modes.] CSI DECSET \"DEC Private Set Mode\" \"CSI ? Pm h\" \"Set various terminal attributes.\"\n * Supported param values by DECSET:\n *\n * | param | Action | Support |\n * | ----- | ------------------------------------------------------- | --------|\n * | 1 | Application Cursor Keys (DECCKM). | #Y |\n * | 2 | Designate US-ASCII for character sets G0-G3 (DECANM). | #Y |\n * | 3 | 132 Column Mode (DECCOLM). | #Y |\n * | 6 | Origin Mode (DECOM). | #Y |\n * | 7 | Auto-wrap Mode (DECAWM). | #Y |\n * | 8 | Auto-repeat Keys (DECARM). Always on. | #N |\n * | 9 | X10 xterm mouse protocol. | #Y |\n * | 12 | Start Blinking Cursor. | #P[Requires the allowSetCursorBlink quirk option enabled.] |\n * | 25 | Show Cursor (DECTCEM). | #Y |\n * | 45 | Reverse wrap-around. | #Y |\n * | 47 | Use Alternate Screen Buffer. | #Y |\n * | 66 | Application keypad (DECNKM). | #Y |\n * | 1000 | X11 xterm mouse protocol. | #Y |\n * | 1002 | Use Cell Motion Mouse Tracking. | #Y |\n * | 1003 | Use All Motion Mouse Tracking. | #Y |\n * | 1004 | Send FocusIn/FocusOut events | #Y |\n * | 1005 | Enable UTF-8 Mouse Mode. | #N |\n * | 1006 | Enable SGR Mouse Mode. | #Y |\n * | 1015 | Enable urxvt Mouse Mode. | #N |\n * | 1016 | Enable SGR-Pixels Mouse Mode. | #Y |\n * | 1047 | Use Alternate Screen Buffer. | #Y |\n * | 1048 | Save cursor as in DECSC. | #Y |\n * | 1049 | Save cursor and switch to alternate buffer clearing it. | #P[Does not clear the alternate buffer.] |\n * | 2004 | Set bracketed paste mode. | #Y |\n *\n *\n * FIXME: implement DECSCNM, 1049 should clear altbuffer\n */\n public setModePrivate(params: IParams): boolean {\n for (let i = 0; i < params.length; i++) {\n switch (params.params[i]) {\n case 1:\n this._coreService.decPrivateModes.applicationCursorKeys = true;\n break;\n case 2:\n this._charsetService.setgCharset(0, DEFAULT_CHARSET);\n this._charsetService.setgCharset(1, DEFAULT_CHARSET);\n this._charsetService.setgCharset(2, DEFAULT_CHARSET);\n this._charsetService.setgCharset(3, DEFAULT_CHARSET);\n // set VT100 mode here\n break;\n case 3:\n /**\n * DECCOLM - 132 column mode.\n * This is only active if 'SetWinLines' (24) is enabled\n * through `options.windowsOptions`.\n */\n if (this._optionsService.rawOptions.windowOptions.setWinLines) {\n this._bufferService.resize(132, this._bufferService.rows);\n this._onRequestReset.fire();\n }\n break;\n case 6:\n this._coreService.decPrivateModes.origin = true;\n this._setCursor(0, 0);\n break;\n case 7:\n this._coreService.decPrivateModes.wraparound = true;\n break;\n case 12:\n if (this._optionsService.rawOptions.quirks?.allowSetCursorBlink) {\n this._optionsService.options.cursorBlink = true;\n }\n break;\n case 45:\n this._coreService.decPrivateModes.reverseWraparound = true;\n break;\n case 66:\n this._logService.debug('Serial port requested application keypad.');\n this._coreService.decPrivateModes.applicationKeypad = true;\n this._onRequestSyncScrollBar.fire();\n break;\n case 9: // X10 Mouse\n // no release, no motion, no wheel, no modifiers.\n this._mouseStateService.activeProtocol = 'X10';\n break;\n case 1000: // vt200 mouse\n // no motion.\n this._mouseStateService.activeProtocol = 'VT200';\n break;\n case 1002: // button event mouse\n this._mouseStateService.activeProtocol = 'DRAG';\n break;\n case 1003: // any event mouse\n // any event - sends motion events,\n // even if there is no button held down.\n this._mouseStateService.activeProtocol = 'ANY';\n break;\n case 1004: // send focusin/focusout events\n // focusin: ^[[I\n // focusout: ^[[O\n this._coreService.decPrivateModes.sendFocus = true;\n this._onRequestSendFocus.fire();\n break;\n case 1005: // utf8 ext mode mouse - removed in #2507\n this._logService.debug('DECSET 1005 not supported (see #2507)');\n break;\n case 1006: // sgr ext mode mouse\n this._mouseStateService.activeEncoding = 'SGR';\n break;\n case 1015: // urxvt ext mode mouse - removed in #2507\n this._logService.debug('DECSET 1015 not supported (see #2507)');\n break;\n case 1016: // sgr pixels mode mouse\n this._mouseStateService.activeEncoding = 'SGR_PIXELS';\n break;\n case 25: // show cursor\n this._coreService.isCursorHidden = false;\n break;\n case 1048: // alt screen cursor\n this.saveCursor();\n break;\n case 1049: // alt screen buffer cursor\n this.saveCursor();\n // FALL-THROUGH\n case 47: // alt screen buffer\n case 1047: // alt screen buffer\n // Swap kitty keyboard flags: save main, restore alt\n if (this._optionsService.rawOptions.vtExtensions?.kittyKeyboard) {\n const state = this._coreService.kittyKeyboard;\n state.mainFlags = state.flags;\n state.flags = state.altFlags;\n }\n this._bufferService.buffers.activateAltBuffer(this._eraseAttrData());\n this._coreService.isCursorInitialized = true;\n this._onRequestRefreshRows.fire(undefined);\n this._onRequestSyncScrollBar.fire();\n break;\n case 2004: // bracketed paste mode (https://cirw.in/blog/bracketed-paste)\n this._coreService.decPrivateModes.bracketedPasteMode = true;\n break;\n case 2026: // synchronized output (https://github.com/contour-terminal/vt-extensions/blob/master/synchronized-output.md)\n this._coreService.decPrivateModes.synchronizedOutput = true;\n break;\n case 2031: // color scheme updates (https://contour-terminal.org/vt-extensions/color-palette-update-notifications/)\n if (this._optionsService.rawOptions.vtExtensions?.colorSchemeQuery ?? true) {\n this._coreService.decPrivateModes.colorSchemeUpdates = true;\n }\n break;\n case 9001: // win32-input-mode (https://github.com/microsoft/terminal/blob/main/doc/specs/%234999%20-%20Improved%20keyboard%20handling%20in%20Conpty.md)\n if (this._optionsService.rawOptions.vtExtensions?.win32InputMode) {\n this._coreService.decPrivateModes.win32InputMode = true;\n }\n break;\n }\n }\n return true;\n }\n\n\n /**\n * CSI Pm l Reset Mode (RM).\n * Ps = 2 -> Keyboard Action Mode (AM).\n * Ps = 4 -> Replace Mode (IRM).\n * Ps = 1 2 -> Send/receive (SRM).\n * Ps = 2 0 -> Normal Linefeed (LNM).\n *\n * @vt: #P[Only IRM is supported.] CSI RM \"Reset Mode\" \"CSI Pm l\" \"Set various terminal attributes.\"\n * Supported param values by RM:\n *\n * | Param | Action | Support |\n * | ----- | -------------------------------------- | ------- |\n * | 2 | Keyboard Action Mode (KAM). Always on. | #N |\n * | 4 | Replace Mode (IRM). (default) | #Y |\n * | 12 | Send/receive (SRM). Always off. | #N |\n * | 20 | Normal Linefeed (LNM). | #Y |\n *\n *\n * FIXME: why is LNM commented out?\n */\n public resetMode(params: IParams): boolean {\n for (let i = 0; i < params.length; i++) {\n switch (params.params[i]) {\n case 4:\n this._coreService.modes.insertMode = false;\n break;\n case 20:\n this._optionsService.options.convertEol = false;\n break;\n }\n }\n return true;\n }\n\n /**\n * CSI ? Pm l\n * DEC Private Mode Reset (DECRST).\n * Ps = 1 -> Normal Cursor Keys (DECCKM).\n * Ps = 2 -> Designate VT52 mode (DECANM).\n * Ps = 3 -> 80 Column Mode (DECCOLM).\n * Ps = 4 -> Jump (Fast) Scroll (DECSCLM).\n * Ps = 5 -> Normal Video (DECSCNM).\n * Ps = 6 -> Normal Cursor Mode (DECOM).\n * Ps = 7 -> No Wraparound Mode (DECAWM).\n * Ps = 8 -> No Auto-repeat Keys (DECARM).\n * Ps = 9 -> Don't send Mouse X & Y on button press.\n * Ps = 1 0 -> Hide toolbar (rxvt).\n * Ps = 1 2 -> Stop Blinking Cursor (att610).\n * Ps = 1 8 -> Don't print form feed (DECPFF).\n * Ps = 1 9 -> Limit print to scrolling region (DECPEX).\n * Ps = 2 5 -> Hide Cursor (DECTCEM).\n * Ps = 3 0 -> Don't show scrollbar (rxvt).\n * Ps = 3 5 -> Disable font-shifting functions (rxvt).\n * Ps = 4 0 -> Disallow 80 -> 132 Mode.\n * Ps = 4 1 -> No more(1) fix (see curses resource).\n * Ps = 4 2 -> Disable Nation Replacement Character sets (DEC-\n * NRCM).\n * Ps = 4 4 -> Turn Off Margin Bell.\n * Ps = 4 5 -> No Reverse-wraparound Mode.\n * Ps = 4 6 -> Stop Logging. (This is normally disabled by a\n * compile-time option).\n * Ps = 4 7 -> Use Normal Screen Buffer.\n * Ps = 6 6 -> Numeric keypad (DECNKM).\n * Ps = 6 7 -> Backarrow key sends delete (DECBKM).\n * Ps = 1 0 0 0 -> Don't send Mouse X & Y on button press and\n * release. See the section Mouse Tracking.\n * Ps = 1 0 0 1 -> Don't use Hilite Mouse Tracking.\n * Ps = 1 0 0 2 -> Don't use Cell Motion Mouse Tracking.\n * Ps = 1 0 0 3 -> Don't use All Motion Mouse Tracking.\n * Ps = 1 0 0 4 -> Don't send FocusIn/FocusOut events.\n * Ps = 1 0 0 5 -> Disable Extended Mouse Mode.\n * Ps = 1 0 1 0 -> Don't scroll to bottom on tty output\n * (rxvt).\n * Ps = 1 0 1 1 -> Don't scroll to bottom on key press (rxvt).\n * Ps = 1 0 3 4 -> Don't interpret \"meta\" key. (This disables\n * the eightBitInput resource).\n * Ps = 1 0 3 5 -> Disable special modifiers for Alt and Num-\n * Lock keys. (This disables the numLock resource).\n * Ps = 1 0 3 6 -> Don't send ESC when Meta modifies a key.\n * (This disables the metaSendsEscape resource).\n * Ps = 1 0 3 7 -> Send VT220 Remove from the editing-keypad\n * Delete key.\n * Ps = 1 0 3 9 -> Don't send ESC when Alt modifies a key.\n * (This disables the altSendsEscape resource).\n * Ps = 1 0 4 0 -> Do not keep selection when not highlighted.\n * (This disables the keepSelection resource).\n * Ps = 1 0 4 1 -> Use the PRIMARY selection. (This disables\n * the selectToClipboard resource).\n * Ps = 1 0 4 2 -> Disable Urgency window manager hint when\n * Control-G is received. (This disables the bellIsUrgent\n * resource).\n * Ps = 1 0 4 3 -> Disable raising of the window when Control-\n * G is received. (This disables the popOnBell resource).\n * Ps = 1 0 4 7 -> Use Normal Screen Buffer, clearing screen\n * first if in the Alternate Screen. (This may be disabled by\n * the titeInhibit resource).\n * Ps = 1 0 4 8 -> Restore cursor as in DECRC. (This may be\n * disabled by the titeInhibit resource).\n * Ps = 1 0 4 9 -> Use Normal Screen Buffer and restore cursor\n * as in DECRC. (This may be disabled by the titeInhibit\n * resource). This combines the effects of the 1 0 4 7 and 1 0\n * 4 8 modes. Use this with terminfo-based applications rather\n * than the 4 7 mode.\n * Ps = 1 0 5 0 -> Reset terminfo/termcap function-key mode.\n * Ps = 1 0 5 1 -> Reset Sun function-key mode.\n * Ps = 1 0 5 2 -> Reset HP function-key mode.\n * Ps = 1 0 5 3 -> Reset SCO function-key mode.\n * Ps = 1 0 6 0 -> Reset legacy keyboard emulation (X11R6).\n * Ps = 1 0 6 1 -> Reset keyboard emulation to Sun/PC style.\n * Ps = 2 0 0 4 -> Reset bracketed paste mode.\n *\n * @vt: #P[See below for supported modes.] CSI DECRST \"DEC Private Reset Mode\" \"CSI ? Pm l\" \"Reset various terminal attributes.\"\n * Supported param values by DECRST:\n *\n * | param | Action | Support |\n * | ----- | ------------------------------------------------------- | ------- |\n * | 1 | Normal Cursor Keys (DECCKM). | #Y |\n * | 2 | Designate VT52 mode (DECANM). | #N |\n * | 3 | 80 Column Mode (DECCOLM). | #B[Switches to old column width instead of 80.] |\n * | 6 | Normal Cursor Mode (DECOM). | #Y |\n * | 7 | No Wraparound Mode (DECAWM). | #Y |\n * | 8 | No Auto-repeat Keys (DECARM). | #N |\n * | 9 | Don't send Mouse X & Y on button press. | #Y |\n * | 12 | Stop Blinking Cursor. | #P[Requires the allowSetCursorBlink quirk option enabled.] |\n * | 25 | Hide Cursor (DECTCEM). | #Y |\n * | 45 | No reverse wrap-around. | #Y |\n * | 47 | Use Normal Screen Buffer. | #Y |\n * | 66 | Numeric keypad (DECNKM). | #Y |\n * | 1000 | Don't send Mouse reports. | #Y |\n * | 1002 | Don't use Cell Motion Mouse Tracking. | #Y |\n * | 1003 | Don't use All Motion Mouse Tracking. | #Y |\n * | 1004 | Don't send FocusIn/FocusOut events. | #Y |\n * | 1005 | Disable UTF-8 Mouse Mode. | #N |\n * | 1006 | Disable SGR Mouse Mode. | #Y |\n * | 1015 | Disable urxvt Mouse Mode. | #N |\n * | 1016 | Disable SGR-Pixels Mouse Mode. | #Y |\n * | 1047 | Use Normal Screen Buffer (clearing screen if in alt). | #Y |\n * | 1048 | Restore cursor as in DECRC. | #Y |\n * | 1049 | Use Normal Screen Buffer and restore cursor. | #Y |\n * | 2004 | Reset bracketed paste mode. | #Y |\n *\n *\n * FIXME: DECCOLM is currently broken (already fixed in window options PR)\n */\n public resetModePrivate(params: IParams): boolean {\n for (let i = 0; i < params.length; i++) {\n switch (params.params[i]) {\n case 1:\n this._coreService.decPrivateModes.applicationCursorKeys = false;\n break;\n case 3:\n /**\n * DECCOLM - 80 column mode.\n * This is only active if 'SetWinLines' (24) is enabled\n * through `options.windowsOptions`.\n */\n if (this._optionsService.rawOptions.windowOptions.setWinLines) {\n this._bufferService.resize(80, this._bufferService.rows);\n this._onRequestReset.fire();\n }\n break;\n case 6:\n this._coreService.decPrivateModes.origin = false;\n this._setCursor(0, 0);\n break;\n case 7:\n this._coreService.decPrivateModes.wraparound = false;\n break;\n case 12:\n if (this._optionsService.rawOptions.quirks?.allowSetCursorBlink) {\n this._optionsService.options.cursorBlink = false;\n }\n break;\n case 45:\n this._coreService.decPrivateModes.reverseWraparound = false;\n break;\n case 66:\n this._logService.debug('Switching back to normal keypad.');\n this._coreService.decPrivateModes.applicationKeypad = false;\n this._onRequestSyncScrollBar.fire();\n break;\n case 9: // X10 Mouse\n case 1000: // vt200 mouse\n case 1002: // button event mouse\n case 1003: // any event mouse\n this._mouseStateService.activeProtocol = 'NONE';\n break;\n case 1004: // send focusin/focusout events\n this._coreService.decPrivateModes.sendFocus = false;\n break;\n case 1005: // utf8 ext mode mouse - removed in #2507\n this._logService.debug('DECRST 1005 not supported (see #2507)');\n break;\n case 1006: // sgr ext mode mouse\n this._mouseStateService.activeEncoding = 'DEFAULT';\n break;\n case 1015: // urxvt ext mode mouse - removed in #2507\n this._logService.debug('DECRST 1015 not supported (see #2507)');\n break;\n case 1016: // sgr pixels mode mouse\n this._mouseStateService.activeEncoding = 'DEFAULT';\n break;\n case 25: // hide cursor\n this._coreService.isCursorHidden = true;\n break;\n case 1048: // alt screen cursor\n this.restoreCursor();\n break;\n case 1049: // alt screen buffer cursor\n // FALL-THROUGH\n case 47: // normal screen buffer\n case 1047: // normal screen buffer - clearing it first\n // Swap kitty keyboard flags: save alt, restore main\n if (this._optionsService.rawOptions.vtExtensions?.kittyKeyboard) {\n const state = this._coreService.kittyKeyboard;\n state.altFlags = state.flags;\n state.flags = state.mainFlags;\n }\n // Ensure the selection manager has the correct buffer\n this._bufferService.buffers.activateNormalBuffer();\n if (params.params[i] === 1049) {\n this.restoreCursor();\n }\n this._coreService.isCursorInitialized = true;\n this._onRequestRefreshRows.fire(undefined);\n this._onRequestSyncScrollBar.fire();\n break;\n case 2004: // bracketed paste mode (https://cirw.in/blog/bracketed-paste)\n this._coreService.decPrivateModes.bracketedPasteMode = false;\n break;\n case 2026: // synchronized output (https://github.com/contour-terminal/vt-extensions/blob/master/synchronized-output.md)\n this._coreService.decPrivateModes.synchronizedOutput = false;\n this._onRequestRefreshRows.fire(undefined);\n break;\n case 2031: // color scheme updates (https://contour-terminal.org/vt-extensions/color-palette-update-notifications/)\n if (this._optionsService.rawOptions.vtExtensions?.colorSchemeQuery ?? true) {\n this._coreService.decPrivateModes.colorSchemeUpdates = false;\n }\n break;\n case 9001: // win32-input-mode\n if (this._optionsService.rawOptions.vtExtensions?.win32InputMode) {\n this._coreService.decPrivateModes.win32InputMode = false;\n }\n break;\n }\n }\n return true;\n }\n\n /**\n * CSI Ps $ p Request ANSI Mode (DECRQM).\n *\n * Reports CSI Ps; Pm $ y (DECRPM), where Ps is the mode number as in SM/RM,\n * and Pm is the mode value:\n * 0 - not recognized\n * 1 - set\n * 2 - reset\n * 3 - permanently set\n * 4 - permanently reset\n *\n * @vt: #Y CSI DECRQM \"Request Mode\" \"CSI Ps $p\" \"Request mode state.\"\n * Returns a report as `CSI Ps; Pm $ y` (DECRPM), where `Ps` is the mode number as in SM/RM\n * or DECSET/DECRST, and `Pm` is the mode value:\n * - 0: not recognized\n * - 1: set\n * - 2: reset\n * - 3: permanently set\n * - 4: permanently reset\n *\n * For modes not understood xterm.js always returns `notRecognized`. In general this means,\n * that a certain operation mode is not implemented and cannot be used.\n *\n * Modes changing the active terminal buffer (47, 1047, 1049) are not subqueried\n * and only report, whether the alternate buffer is set.\n *\n * Mouse encodings and mouse protocols are handled mutual exclusive,\n * thus only one of each of those can be set at a given time.\n *\n * There is a chance, that some mode reports are not fully in line with xterm.js' behavior,\n * e.g. if the default implementation already exposes a certain behavior. If you find\n * discrepancies in the mode reports, please file a bug.\n */\n public requestMode(params: IParams, ansi: boolean): boolean {\n // return value as in DECRPM\n const enum V {\n NOT_RECOGNIZED = 0,\n SET = 1,\n RESET = 2,\n PERMANENTLY_SET = 3,\n PERMANENTLY_RESET = 4\n }\n\n // access helpers\n const dm = this._coreService.decPrivateModes;\n const { activeProtocol: mouseProtocol, activeEncoding: mouseEncoding } = this._mouseStateService;\n const cs = this._coreService;\n const { buffers, cols } = this._bufferService;\n const { active, alt } = buffers;\n const opts = this._optionsService.rawOptions;\n\n const f = (m: number, v: V): boolean => {\n cs.triggerDataEvent(`${C0.ESC}[${ansi ? '' : '?'}${m};${v}$y`);\n return true;\n };\n const b2v = (value: boolean): V => value ? V.SET : V.RESET;\n\n const p = params.params[0];\n\n if (ansi) {\n if (p === 2) return f(p, V.PERMANENTLY_RESET);\n if (p === 4) return f(p, b2v(cs.modes.insertMode));\n if (p === 12) return f(p, V.PERMANENTLY_SET);\n if (p === 20) return f(p, b2v(opts.convertEol));\n return f(p, V.NOT_RECOGNIZED);\n }\n\n if (p === 1) return f(p, b2v(dm.applicationCursorKeys));\n if (p === 3) return f(p, opts.windowOptions.setWinLines ? (cols === 80 ? V.RESET : cols === 132 ? V.SET : V.NOT_RECOGNIZED) : V.NOT_RECOGNIZED);\n if (p === 6) return f(p, b2v(dm.origin));\n if (p === 7) return f(p, b2v(dm.wraparound));\n if (p === 8) return f(p, V.PERMANENTLY_SET);\n if (p === 9) return f(p, b2v(mouseProtocol === 'X10'));\n if (p === 12) return f(p, b2v(opts.cursorBlink));\n if (p === 25) return f(p, b2v(!cs.isCursorHidden));\n if (p === 45) return f(p, b2v(dm.reverseWraparound));\n if (p === 66) return f(p, b2v(dm.applicationKeypad));\n if (p === 67) return f(p, V.PERMANENTLY_RESET);\n if (p === 1000) return f(p, b2v(mouseProtocol === 'VT200'));\n if (p === 1002) return f(p, b2v(mouseProtocol === 'DRAG'));\n if (p === 1003) return f(p, b2v(mouseProtocol === 'ANY'));\n if (p === 1004) return f(p, b2v(dm.sendFocus));\n if (p === 1005) return f(p, V.PERMANENTLY_RESET);\n if (p === 1006) return f(p, b2v(mouseEncoding === 'SGR'));\n if (p === 1015) return f(p, V.PERMANENTLY_RESET);\n if (p === 1016) return f(p, b2v(mouseEncoding === 'SGR_PIXELS'));\n if (p === 1048) return f(p, V.SET); // xterm always returns SET here\n if (p === 47 || p === 1047 || p === 1049) return f(p, b2v(active === alt));\n if (p === 2004) return f(p, b2v(dm.bracketedPasteMode));\n if (p === 2026) return f(p, b2v(dm.synchronizedOutput));\n if (p === 9001) return this._optionsService.rawOptions.vtExtensions?.win32InputMode ? f(p, b2v(dm.win32InputMode)) : f(p, V.NOT_RECOGNIZED);\n return f(p, V.NOT_RECOGNIZED);\n }\n\n /**\n * Helper to write color information packed with color mode.\n */\n private _updateAttrColor(color: number, mode: number, c1: number, c2: number, c3: number): number {\n if (mode === 2) {\n color |= Attributes.CM_RGB;\n color &= ~Attributes.RGB_MASK;\n color |= AttributeData.fromColorRGB([c1, c2, c3]);\n } else if (mode === 5) {\n color &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);\n color |= Attributes.CM_P256 | (c1 & 0xff);\n }\n return color;\n }\n\n /**\n * Helper to extract and apply color params/subparams.\n * Returns advance for params index.\n */\n private _extractColor(params: IParams, pos: number, attr: IAttributeData): number {\n // normalize params\n // meaning: [target, CM, ign, val, val, val]\n // RGB : [ 38/48, 2, ign, r, g, b]\n // P256 : [ 38/48, 5, ign, v, ign, ign]\n const accu = [0, 0, -1, 0, 0, 0];\n\n // alignment placeholder for non color space sequences\n let cSpace = 0;\n\n // return advance we took in params\n let advance = 0;\n\n do {\n accu[advance + cSpace] = params.params[pos + advance];\n if (params.hasSubParams(pos + advance)) {\n const subparams = params.getSubParams(pos + advance)!;\n let i = 0;\n do {\n if (accu[1] === 5) {\n cSpace = 1;\n }\n accu[advance + i + 1 + cSpace] = subparams[i];\n } while (++i < subparams.length && i + advance + 1 + cSpace < accu.length);\n break;\n }\n // exit early if can decide color mode with semicolons\n if ((accu[1] === 5 && advance + cSpace >= 2)\n || (accu[1] === 2 && advance + cSpace >= 5)) {\n break;\n }\n // offset colorSpace slot for semicolon mode\n if (accu[1]) {\n cSpace = 1;\n }\n } while (++advance + pos < params.length && advance + cSpace < accu.length);\n\n // set default values to 0\n for (let i = 2; i < accu.length; ++i) {\n if (accu[i] === -1) {\n accu[i] = 0;\n }\n }\n\n // apply colors\n switch (accu[0]) {\n case 38:\n attr.fg = this._updateAttrColor(attr.fg, accu[1], accu[3], accu[4], accu[5]);\n break;\n case 48:\n attr.bg = this._updateAttrColor(attr.bg, accu[1], accu[3], accu[4], accu[5]);\n break;\n case 58:\n attr.extended = attr.extended.clone();\n attr.extended.underlineColor = this._updateAttrColor(attr.extended.underlineColor, accu[1], accu[3], accu[4], accu[5]);\n }\n\n return advance;\n }\n\n /**\n * SGR 4 subparams:\n * 4:0 - equal to SGR 24 (turn off all underline)\n * 4:1 - equal to SGR 4 (single underline)\n * 4:2 - equal to SGR 21 (double underline)\n * 4:3 - curly underline\n * 4:4 - dotted underline\n * 4:5 - dashed underline\n */\n private _processUnderline(style: number, attr: IAttributeData): void {\n // treat extended attrs as immutable, thus always clone from old one\n // this is needed since the buffer only holds references to it\n attr.extended = attr.extended.clone();\n\n // default to 1 == single underline\n if (!~style || style > 5) {\n style = 1;\n }\n attr.extended.underlineStyle = style;\n attr.fg |= FgFlags.UNDERLINE;\n\n // 0 deactivates underline\n if (style === 0) {\n attr.fg &= ~FgFlags.UNDERLINE;\n }\n\n // update HAS_EXTENDED in BG\n attr.updateExtended();\n }\n\n private _processSGR0(attr: IAttributeData): void {\n attr.fg = DEFAULT_ATTR_DATA.fg;\n attr.bg = DEFAULT_ATTR_DATA.bg;\n attr.extended = attr.extended.clone();\n // Reset underline style and color. Note that we don't want to reset other\n // fields such as the url id.\n attr.extended.underlineStyle = UnderlineStyle.NONE;\n attr.extended.underlineColor &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);\n attr.updateExtended();\n }\n\n /**\n * CSI Pm m Character Attributes (SGR).\n *\n * @vt: #P[See below for supported attributes.] CSI SGR \"Select Graphic Rendition\" \"CSI Pm m\" \"Set/Reset various text attributes.\"\n * SGR selects one or more character attributes at the same time. Multiple params (up to 32)\n * are applied in order from left to right. The changed attributes are applied to all new\n * characters received. If you move characters in the viewport by scrolling or any other means,\n * then the attributes move with the characters.\n *\n * Supported param values by SGR:\n *\n * | Param | Meaning | Support |\n * | --------- | -------------------------------------------------------- | ------- |\n * | 0 | Normal (default). Resets any other preceding SGR. | #Y |\n * | 1 | Bold. (also see `options.drawBoldTextInBrightColors`) | #Y |\n * | 2 | Faint, decreased intensity. | #Y |\n * | 3 | Italic. | #Y |\n * | 4 | Underlined (see below for style support). | #Y |\n * | 5 | Slowly blinking. | #N |\n * | 6 | Rapidly blinking. | #N |\n * | 7 | Inverse. Flips foreground and background color. | #Y |\n * | 8 | Invisible (hidden). | #Y |\n * | 9 | Crossed-out characters (strikethrough). | #Y |\n * | 21 | Doubly underlined. | #Y |\n * | 22 | Normal (neither bold nor faint). | #Y |\n * | 23 | No italic. | #Y |\n * | 24 | Not underlined. | #Y |\n * | 25 | Steady (not blinking). | #Y |\n * | 27 | Positive (not inverse). | #Y |\n * | 28 | Visible (not hidden). | #Y |\n * | 29 | Not Crossed-out (strikethrough). | #Y |\n * | 30 | Foreground color: Black. | #Y |\n * | 31 | Foreground color: Red. | #Y |\n * | 32 | Foreground color: Green. | #Y |\n * | 33 | Foreground color: Yellow. | #Y |\n * | 34 | Foreground color: Blue. | #Y |\n * | 35 | Foreground color: Magenta. | #Y |\n * | 36 | Foreground color: Cyan. | #Y |\n * | 37 | Foreground color: White. | #Y |\n * | 38 | Foreground color: Extended color. | #P[Support for RGB and indexed colors, see below.] |\n * | 39 | Foreground color: Default (original). | #Y |\n * | 40 | Background color: Black. | #Y |\n * | 41 | Background color: Red. | #Y |\n * | 42 | Background color: Green. | #Y |\n * | 43 | Background color: Yellow. | #Y |\n * | 44 | Background color: Blue. | #Y |\n * | 45 | Background color: Magenta. | #Y |\n * | 46 | Background color: Cyan. | #Y |\n * | 47 | Background color: White. | #Y |\n * | 48 | Background color: Extended color. | #P[Support for RGB and indexed colors, see below.] |\n * | 49 | Background color: Default (original). | #Y |\n * | 53 | Overlined. | #Y |\n * | 55 | Not Overlined. | #Y |\n * | 58 | Underline color: Extended color. | #P[Support for RGB and indexed colors, see below.] |\n * | 221 | Not bold (kitty extension). | #Y |\n * | 222 | Not faint (kitty extension). | #Y |\n * | 90 - 97 | Bright foreground color (analogous to 30 - 37). | #Y |\n * | 100 - 107 | Bright background color (analogous to 40 - 47). | #Y |\n *\n * Underline supports subparams to denote the style in the form `4 : x`:\n *\n * | x | Meaning | Support |\n * | ------ | ------------------------------------------------------------- | ------- |\n * | 0 | No underline. Same as `SGR 24 m`. | #Y |\n * | 1 | Single underline. Same as `SGR 4 m`. | #Y |\n * | 2 | Double underline. | #Y |\n * | 3 | Curly underline. | #Y |\n * | 4 | Dotted underline. | #Y |\n * | 5 | Dashed underline. | #Y |\n * | other | Single underline. Same as `SGR 4 m`. | #Y |\n *\n * Extended colors are supported for foreground (Ps=38), background (Ps=48) and underline (Ps=58)\n * as follows:\n *\n * | Ps + 1 | Meaning | Support |\n * | ------ | ------------------------------------------------------------- | ------- |\n * | 0 | Implementation defined. | #N |\n * | 1 | Transparent. | #N |\n * | 2 | RGB color as `Ps ; 2 ; R ; G ; B` or `Ps : 2 : : R : G : B`. | #Y |\n * | 3 | CMY color. | #N |\n * | 4 | CMYK color. | #N |\n * | 5 | Indexed (256 colors) as `Ps ; 5 ; INDEX` or `Ps : 5 : INDEX`. | #Y |\n */\n public charAttributes(params: IParams): boolean {\n // Optimize a single SGR0.\n if (params.length === 1 && params.params[0] === 0) {\n this._processSGR0(this._curAttrData);\n return true;\n }\n\n const l = params.length;\n let p;\n const attr = this._curAttrData;\n\n for (let i = 0; i < l; i++) {\n p = params.params[i];\n if (p >= 30 && p <= 37) {\n // fg color 8\n attr.fg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);\n attr.fg |= Attributes.CM_P16 | (p - 30);\n } else if (p >= 40 && p <= 47) {\n // bg color 8\n attr.bg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);\n attr.bg |= Attributes.CM_P16 | (p - 40);\n } else if (p >= 90 && p <= 97) {\n // fg color 16\n attr.fg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);\n attr.fg |= Attributes.CM_P16 | (p - 90) | 8;\n } else if (p >= 100 && p <= 107) {\n // bg color 16\n attr.bg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);\n attr.bg |= Attributes.CM_P16 | (p - 100) | 8;\n } else if (p === 0) {\n // default\n this._processSGR0(attr);\n } else if (p === 1) {\n // bold text\n attr.fg |= FgFlags.BOLD;\n } else if (p === 3) {\n // italic text\n attr.bg |= BgFlags.ITALIC;\n } else if (p === 4) {\n // underlined text\n attr.fg |= FgFlags.UNDERLINE;\n this._processUnderline(params.hasSubParams(i) ? params.getSubParams(i)![0] : UnderlineStyle.SINGLE, attr);\n } else if (p === 5) {\n // blink\n attr.fg |= FgFlags.BLINK;\n } else if (p === 7) {\n // inverse and positive\n // test with: echo -e '\\e[31m\\e[42mhello\\e[7mworld\\e[27mhi\\e[m'\n attr.fg |= FgFlags.INVERSE;\n } else if (p === 8) {\n // invisible\n attr.fg |= FgFlags.INVISIBLE;\n } else if (p === 9) {\n // strikethrough\n attr.fg |= FgFlags.STRIKETHROUGH;\n } else if (p === 2) {\n // dimmed text\n attr.bg |= BgFlags.DIM;\n } else if (p === 21) {\n // double underline\n this._processUnderline(UnderlineStyle.DOUBLE, attr);\n } else if (p === 22) {\n // not bold nor faint\n attr.fg &= ~FgFlags.BOLD;\n attr.bg &= ~BgFlags.DIM;\n } else if (p === 23) {\n // not italic\n attr.bg &= ~BgFlags.ITALIC;\n } else if (p === 24) {\n // not underlined\n attr.fg &= ~FgFlags.UNDERLINE;\n this._processUnderline(UnderlineStyle.NONE, attr);\n } else if (p === 25) {\n // not blink\n attr.fg &= ~FgFlags.BLINK;\n } else if (p === 27) {\n // not inverse\n attr.fg &= ~FgFlags.INVERSE;\n } else if (p === 28) {\n // not invisible\n attr.fg &= ~FgFlags.INVISIBLE;\n } else if (p === 29) {\n // not strikethrough\n attr.fg &= ~FgFlags.STRIKETHROUGH;\n } else if (p === 39) {\n // reset fg\n attr.fg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);\n attr.fg |= DEFAULT_ATTR_DATA.fg & Attributes.RGB_MASK;\n } else if (p === 49) {\n // reset bg\n attr.bg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);\n attr.bg |= DEFAULT_ATTR_DATA.bg & Attributes.RGB_MASK;\n } else if (p === 38 || p === 48 || p === 58) {\n // fg color 256 and RGB\n i += this._extractColor(params, i, attr);\n } else if (p === 53) {\n // overline\n attr.bg |= BgFlags.OVERLINE;\n } else if (p === 55) {\n // not overline\n attr.bg &= ~BgFlags.OVERLINE;\n } else if (p === 221 && (this._optionsService.rawOptions.vtExtensions?.kittySgrBoldFaintControl ?? true)) {\n // not bold (kitty extension)\n attr.fg &= ~FgFlags.BOLD;\n } else if (p === 222 && (this._optionsService.rawOptions.vtExtensions?.kittySgrBoldFaintControl ?? true)) {\n // not faint (kitty extension)\n attr.bg &= ~BgFlags.DIM;\n } else if (p === 59) {\n attr.extended = attr.extended.clone();\n attr.extended.underlineColor = -1;\n attr.updateExtended();\n } else {\n this._logService.debug('Unknown SGR attribute: %d.', p);\n }\n }\n return true;\n }\n\n /**\n * CSI Ps n Device Status Report (DSR).\n * Ps = 5 -> Status Report. Result (``OK'') is\n * CSI 0 n\n * Ps = 6 -> Report Cursor Position (CPR) [row;column].\n * Result is\n * CSI r ; c R\n * CSI ? Ps n\n * Device Status Report (DSR, DEC-specific).\n * Ps = 6 -> Report Cursor Position (CPR) [row;column] as CSI\n * ? r ; c R (assumes page is zero).\n * Ps = 1 5 -> Report Printer status as CSI ? 1 0 n (ready).\n * or CSI ? 1 1 n (not ready).\n * Ps = 2 5 -> Report UDK status as CSI ? 2 0 n (unlocked)\n * or CSI ? 2 1 n (locked).\n * Ps = 2 6 -> Report Keyboard status as\n * CSI ? 2 7 ; 1 ; 0 ; 0 n (North American).\n * The last two parameters apply to VT400 & up, and denote key-\n * board ready and LK01 respectively.\n * Ps = 5 3 -> Report Locator status as\n * CSI ? 5 3 n Locator available, if compiled-in, or\n * CSI ? 5 0 n No Locator, if not.\n *\n * @vt: #Y CSI DSR \"Device Status Report\" \"CSI Ps n\" \"Request cursor position (CPR) with `Ps` = 6.\"\n */\n public deviceStatus(params: IParams): boolean {\n switch (params.params[0]) {\n case 5:\n // status report\n this._coreService.triggerDataEvent(`${C0.ESC}[0n`);\n break;\n case 6:\n // cursor position\n const y = this._activeBuffer.y + 1;\n const x = this._activeBuffer.x + 1;\n this._coreService.triggerDataEvent(`${C0.ESC}[${y};${x}R`);\n break;\n }\n return true;\n }\n\n // @vt: #P[Only CPR is supported.] CSI DECDSR \"DEC Device Status Report\" \"CSI ? Ps n\" \"Only CPR is supported (same as DSR).\"\n public deviceStatusPrivate(params: IParams): boolean {\n // modern xterm doesnt seem to\n // respond to any of these except ?6, 6, and 5\n switch (params.params[0]) {\n case 6:\n // cursor position\n const y = this._activeBuffer.y + 1;\n const x = this._activeBuffer.x + 1;\n this._coreService.triggerDataEvent(`${C0.ESC}[?${y};${x}R`);\n break;\n case 15:\n // no printer\n // this.handler(C0.ESC + '[?11n');\n break;\n case 25:\n // dont support user defined keys\n // this.handler(C0.ESC + '[?21n');\n break;\n case 26:\n // north american keyboard\n // this.handler(C0.ESC + '[?27;1;0;0n');\n break;\n case 53:\n // no dec locator/mouse\n // this.handler(C0.ESC + '[?50n');\n break;\n case 996:\n // color scheme query (https://contour-terminal.org/vt-extensions/color-palette-update-notifications/)\n if (this._optionsService.rawOptions.vtExtensions?.colorSchemeQuery ?? true) {\n this._onRequestColorSchemeQuery.fire();\n }\n break;\n }\n return true;\n }\n\n /**\n * CSI ! p Soft terminal reset (DECSTR).\n * http://vt100.net/docs/vt220-rm/table4-10.html\n *\n * @vt: #Y CSI DECSTR \"Soft Terminal Reset\" \"CSI ! p\" \"Reset several terminal attributes to initial state.\"\n * There are two terminal reset sequences - RIS and DECSTR. While RIS performs almost a full\n * terminal bootstrap, DECSTR only resets certain attributes. For most needs DECSTR should be\n * sufficient.\n *\n * The following terminal attributes are reset to default values:\n * - IRM is reset (dafault = false)\n * - scroll margins are reset (default = viewport size)\n * - erase attributes are reset to default\n * - charsets are reset\n * - DECSC data is reset to initial values\n * - DECOM is reset to absolute mode\n *\n *\n * FIXME: there are several more attributes missing (see VT520 manual)\n */\n public softReset(params: IParams): boolean {\n this._coreService.isCursorHidden = false;\n this._onRequestSyncScrollBar.fire();\n this._activeBuffer.scrollTop = 0;\n this._activeBuffer.scrollBottom = this._bufferService.rows - 1;\n this._curAttrData = DEFAULT_ATTR_DATA.clone();\n this._coreService.reset();\n this._charsetService.reset();\n\n // reset DECSC data\n this._activeBuffer.savedX = 0;\n this._activeBuffer.savedY = this._activeBuffer.ybase;\n this._activeBuffer.savedCurAttrData.fg = this._curAttrData.fg;\n this._activeBuffer.savedCurAttrData.bg = this._curAttrData.bg;\n this._activeBuffer.savedCharset = this._charsetService.charset;\n\n // reset DECOM\n this._coreService.decPrivateModes.origin = false;\n return true;\n }\n\n /**\n * CSI Ps SP q Set cursor style (DECSCUSR, VT520).\n * Ps = 0 -> reset to option.\n * Ps = 1 -> blinking block (default).\n * Ps = 2 -> steady block.\n * Ps = 3 -> blinking underline.\n * Ps = 4 -> steady underline.\n * Ps = 5 -> blinking bar (xterm).\n * Ps = 6 -> steady bar (xterm).\n *\n * @vt: #Y CSI DECSCUSR \"Set Cursor Style\" \"CSI Ps SP q\" \"Set cursor style.\"\n * Supported cursor styles:\n * - 0: reset to option\n * - empty, 1: blinking block\n * - 2: steady block\n * - 3: blinking underline\n * - 4: steady underline\n * - 5: blinking bar\n * - 6: steady bar\n */\n public setCursorStyle(params: IParams): boolean {\n const param = params.length === 0 ? 1 : params.params[0];\n if (param === 0) {\n this._coreService.decPrivateModes.cursorStyle = undefined;\n this._coreService.decPrivateModes.cursorBlink = undefined;\n } else {\n switch (param) {\n case 1:\n case 2:\n this._coreService.decPrivateModes.cursorStyle = 'block';\n break;\n case 3:\n case 4:\n this._coreService.decPrivateModes.cursorStyle = 'underline';\n break;\n case 5:\n case 6:\n this._coreService.decPrivateModes.cursorStyle = 'bar';\n break;\n }\n const isBlinking = param % 2 === 1;\n this._coreService.decPrivateModes.cursorBlink = isBlinking;\n }\n return true;\n }\n\n /**\n * CSI Ps ; Ps r\n * Set Scrolling Region [top;bottom] (default = full size of win-\n * dow) (DECSTBM).\n *\n * @vt: #Y CSI DECSTBM \"Set Top and Bottom Margin\" \"CSI Ps ; Ps r\" \"Set top and bottom margins of the viewport [top;bottom] (default = viewport size).\"\n */\n public setScrollRegion(params: IParams): boolean {\n const top = params.params[0] || 1;\n let bottom: number;\n\n if (params.length < 2 || (bottom = params.params[1]) > this._bufferService.rows || bottom === 0) {\n bottom = this._bufferService.rows;\n }\n\n if (bottom > top) {\n this._activeBuffer.scrollTop = top - 1;\n this._activeBuffer.scrollBottom = bottom - 1;\n this._setCursor(0, 0);\n }\n return true;\n }\n\n /**\n * CSI Ps ; Ps ; Ps t - Various window manipulations and reports (xterm)\n *\n * Note: Only those listed below are supported. All others are left to integrators and\n * need special treatment based on the embedding environment.\n *\n * Ps = 1 4 supported\n * Report xterm text area size in pixels.\n * Result is CSI 4 ; height ; width t\n * Ps = 14 ; 2 not implemented\n * Ps = 16 supported\n * Report xterm character cell size in pixels.\n * Result is CSI 6 ; height ; width t\n * Ps = 18 supported\n * Report the size of the text area in characters.\n * Result is CSI 8 ; height ; width t\n * Ps = 20 supported\n * Report xterm window's icon label.\n * Result is OSC L label ST\n * Ps = 21 supported\n * Report xterm window's title.\n * Result is OSC l label ST\n * Ps = 22 ; 0 -> Save xterm icon and window title on stack. supported\n * Ps = 22 ; 1 -> Save xterm icon title on stack. supported\n * Ps = 22 ; 2 -> Save xterm window title on stack. supported\n * Ps = 23 ; 0 -> Restore xterm icon and window title from stack. supported\n * Ps = 23 ; 1 -> Restore xterm icon title from stack. supported\n * Ps = 23 ; 2 -> Restore xterm window title from stack. supported\n * Ps >= 24 not implemented\n */\n public windowOptions(params: IParams): boolean {\n if (!paramToWindowOption(params.params[0], this._optionsService.rawOptions.windowOptions)) {\n return true;\n }\n const second = (params.length > 1) ? params.params[1] : 0;\n switch (params.params[0]) {\n case 14: // GetWinSizePixels, returns CSI 4 ; height ; width t\n if (second !== 2) {\n this._onRequestWindowsOptionsReport.fire(WindowsOptionsReportType.GET_WIN_SIZE_PIXELS);\n }\n break;\n case 16: // GetCellSizePixels, returns CSI 6 ; height ; width t\n this._onRequestWindowsOptionsReport.fire(WindowsOptionsReportType.GET_CELL_SIZE_PIXELS);\n break;\n case 18: // GetWinSizeChars, returns CSI 8 ; height ; width t\n if (this._bufferService) {\n this._coreService.triggerDataEvent(`${C0.ESC}[8;${this._bufferService.rows};${this._bufferService.cols}t`);\n }\n break;\n case 22: // PushTitle\n if (second === 0 || second === 2) {\n this._windowTitleStack.push(this._windowTitle);\n if (this._windowTitleStack.length > Constants.STACK_LIMIT) {\n this._windowTitleStack.shift();\n }\n }\n if (second === 0 || second === 1) {\n this._iconNameStack.push(this._iconName);\n if (this._iconNameStack.length > Constants.STACK_LIMIT) {\n this._iconNameStack.shift();\n }\n }\n break;\n case 23: // PopTitle\n if (second === 0 || second === 2) {\n if (this._windowTitleStack.length) {\n this.setTitle(this._windowTitleStack.pop()!);\n }\n }\n if (second === 0 || second === 1) {\n if (this._iconNameStack.length) {\n this.setIconName(this._iconNameStack.pop()!);\n }\n }\n break;\n }\n return true;\n }\n\n\n /**\n * CSI s\n * ESC 7\n * Save cursor (ANSI.SYS).\n *\n * @vt: #P[TODO...] CSI SCOSC \"Save Cursor\" \"CSI s\" \"Save cursor position, charmap and text attributes.\"\n * @vt: #Y ESC SC \"Save Cursor\" \"ESC 7\" \"Save cursor position, charmap and text attributes.\"\n */\n public saveCursor(params?: IParams): boolean {\n this._activeBuffer.savedX = this._activeBuffer.x;\n this._activeBuffer.savedY = this._activeBuffer.ybase + this._activeBuffer.y;\n this._activeBuffer.savedCurAttrData.fg = this._curAttrData.fg;\n this._activeBuffer.savedCurAttrData.bg = this._curAttrData.bg;\n this._activeBuffer.savedCharset = this._charsetService.charset;\n this._activeBuffer.savedCharsets = this._charsetService.charsets.slice();\n this._activeBuffer.savedGlevel = this._charsetService.glevel;\n this._activeBuffer.savedOriginMode = this._coreService.decPrivateModes.origin;\n this._activeBuffer.savedWraparoundMode = this._coreService.decPrivateModes.wraparound;\n return true;\n }\n\n\n /**\n * CSI u\n * ESC 8\n * Restore cursor (ANSI.SYS).\n *\n * @vt: #P[TODO...] CSI SCORC \"Restore Cursor\" \"CSI u\" \"Restore cursor position, charmap and text attributes.\"\n * @vt: #Y ESC RC \"Restore Cursor\" \"ESC 8\" \"Restore cursor position, charmap and text attributes.\"\n */\n public restoreCursor(params?: IParams): boolean {\n this._activeBuffer.x = this._activeBuffer.savedX || 0;\n this._activeBuffer.y = Math.max(this._activeBuffer.savedY - this._activeBuffer.ybase, 0);\n this._curAttrData.fg = this._activeBuffer.savedCurAttrData.fg;\n this._curAttrData.bg = this._activeBuffer.savedCurAttrData.bg;\n for (let i = 0; i < this._activeBuffer.savedCharsets.length; i++) {\n this._charsetService.setgCharset(i, this._activeBuffer.savedCharsets[i]);\n }\n this._charsetService.setgLevel(this._activeBuffer.savedGlevel);\n this._coreService.decPrivateModes.origin = this._activeBuffer.savedOriginMode;\n this._coreService.decPrivateModes.wraparound = this._activeBuffer.savedWraparoundMode;\n this._restrictCursor();\n return true;\n }\n\n /**\n * OSC 2; ST (set window title)\n * Proxy to set window title.\n *\n * @vt: #P[Icon name is not exposed.] OSC 0 \"Set Windows Title and Icon Name\" \"OSC 0 ; Pt BEL\" \"Set window title and icon name.\"\n * Icon name is not supported. For Window Title see below.\n *\n * @vt: #Y OSC 2 \"Set Windows Title\" \"OSC 2 ; Pt BEL\" \"Set window title.\"\n * xterm.js does not manipulate the title directly, instead exposes changes via the event\n * `Terminal.onTitleChange`.\n */\n public setTitle(data: string): boolean {\n this._windowTitle = data;\n this._onTitleChange.fire(data);\n return true;\n }\n\n /**\n * OSC 1; ST\n * Note: Icon name is not exposed.\n */\n public setIconName(data: string): boolean {\n this._iconName = data;\n return true;\n }\n\n /**\n * OSC 4; ; ST (set ANSI color to )\n *\n * @vt: #Y OSC 4 \"Set ANSI color\" \"OSC 4 ; c ; spec BEL\" \"Change color number `c` to the color specified by `spec`.\"\n * `c` is the color index between 0 and 255. The color format of `spec` is derived from\n * `XParseColor` (see OSC 10 for supported formats). There may be multipe `c ; spec` pairs present\n * in the same instruction. If `spec` contains `?` the terminal returns a sequence with the\n * currently set color.\n */\n public setOrReportIndexedColor(data: string): boolean {\n const event: IColorEvent = [];\n const slots = data.split(';');\n while (slots.length > 1) {\n const idx = slots.shift() as string;\n const spec = slots.shift() as string;\n if (/^\\d+$/.exec(idx)) {\n const index = parseInt(idx, 10);\n if (isValidColorIndex(index)) {\n if (spec === '?') {\n event.push({ type: ColorRequestType.REPORT, index });\n } else {\n const color = parseColor(spec);\n if (color) {\n event.push({ type: ColorRequestType.SET, index, color });\n }\n }\n }\n }\n }\n if (event.length) {\n this._onColor.fire(event);\n }\n return true;\n }\n\n /**\n * OSC 8 ; ; ST - create hyperlink\n * OSC 8 ; ; ST - finish hyperlink\n *\n * Test case:\n *\n * ```sh\n * printf '\\e]8;;http://example.com\\e\\\\This is a link\\e]8;;\\e\\\\\\n'\n * ```\n *\n * @vt: #Y OSC 8 \"Create hyperlink\" \"OSC 8 ; params ; uri BEL\" \"Create a hyperlink to `uri` using `params`.\"\n * `uri` is a hyperlink starting with `http://`, `https://`, `ftp://`, `file://` or `mailto://`. `params` is an\n * optional list of key=value assignments, separated by the : character.\n * Example: `id=xyz123:foo=bar:baz=quux`.\n * Currently only the id key is defined. Cells that share the same ID and URI share hover\n * feedback. Use `OSC 8 ; ; BEL` to finish the current hyperlink.\n */\n public setHyperlink(data: string): boolean {\n // Arg parsing is special cases to support unencoded semi-colons in the URIs (#4944)\n const idx = data.indexOf(';');\n if (idx === -1) {\n // malformed sequence, just return as handled\n return true;\n }\n const id = data.slice(0, idx).trim();\n const uri = data.slice(idx + 1);\n if (uri) {\n return this._createHyperlink(id, uri);\n }\n if (id.trim()) {\n return false;\n }\n return this._finishHyperlink();\n }\n\n private _createHyperlink(params: string, uri: string): boolean {\n // It's legal to open a new hyperlink without explicitly finishing the previous one\n if (this._getCurrentLinkId()) {\n this._finishHyperlink();\n }\n const parsedParams = params.split(':');\n let id: string | undefined;\n const idParamIndex = parsedParams.findIndex(e => e.startsWith('id='));\n if (idParamIndex !== -1) {\n id = parsedParams[idParamIndex].slice(3) || undefined;\n }\n this._curAttrData.extended = this._curAttrData.extended.clone();\n this._curAttrData.extended.urlId = this._oscLinkService.registerLink({ id, uri });\n this._curAttrData.updateExtended();\n return true;\n }\n\n private _finishHyperlink(): boolean {\n this._curAttrData.extended = this._curAttrData.extended.clone();\n this._curAttrData.extended.urlId = 0;\n this._curAttrData.updateExtended();\n return true;\n }\n\n // special colors - OSC 10 | 11 | 12\n private _specialColors = [SpecialColorIndex.FOREGROUND, SpecialColorIndex.BACKGROUND, SpecialColorIndex.CURSOR];\n\n /**\n * Apply colors requests for special colors in OSC 10 | 11 | 12.\n * Since these commands are stacking from multiple parameters,\n * we handle them in a loop with an entry offset to `_specialColors`.\n */\n private _setOrReportSpecialColor(data: string, offset: number): boolean {\n const slots = data.split(';');\n for (let i = 0; i < slots.length; ++i, ++offset) {\n if (offset >= this._specialColors.length) break;\n if (slots[i] === '?') {\n this._onColor.fire([{ type: ColorRequestType.REPORT, index: this._specialColors[offset] }]);\n } else {\n const color = parseColor(slots[i]);\n if (color) {\n this._onColor.fire([{ type: ColorRequestType.SET, index: this._specialColors[offset], color }]);\n }\n }\n }\n return true;\n }\n\n /**\n * OSC 10 ; | ST - set or query default foreground color\n *\n * @vt: #Y OSC 10 \"Set or query default foreground color\" \"OSC 10 ; Pt BEL\" \"Set or query default foreground color.\"\n * To set the color, the following color specification formats are supported:\n * - `rgb://` for `, , ` in `h | hh | hhh | hhhh`, where\n * `h` is a single hexadecimal digit (case insignificant). The different widths scale\n * from 4 bit (`h`) to 16 bit (`hhhh`) and get converted to 8 bit (`hh`).\n * - `#RGB` - 4 bits per channel, expanded to `#R0G0B0`\n * - `#RRGGBB` - 8 bits per channel\n * - `#RRRGGGBBB` - 12 bits per channel, truncated to `#RRGGBB`\n * - `#RRRRGGGGBBBB` - 16 bits per channel, truncated to `#RRGGBB`\n *\n * **Note:** X11 named colors are currently unsupported.\n *\n * If `Pt` contains `?` instead of a color specification, the terminal\n * returns a sequence with the current default foreground color\n * (use that sequence to restore the color after changes).\n *\n * **Note:** Other than xterm, xterm.js does not support OSC 12 - 19.\n * Therefore stacking multiple `Pt` separated by `;` only works for the first two entries.\n */\n public setOrReportFgColor(data: string): boolean {\n return this._setOrReportSpecialColor(data, 0);\n }\n\n /**\n * OSC 11 ; | ST - set or query default background color\n *\n * @vt: #Y OSC 11 \"Set or query default background color\" \"OSC 11 ; Pt BEL\" \"Same as OSC 10, but for default background.\"\n */\n public setOrReportBgColor(data: string): boolean {\n return this._setOrReportSpecialColor(data, 1);\n }\n\n /**\n * OSC 12 ; | ST - set or query default cursor color\n *\n * @vt: #Y OSC 12 \"Set or query default cursor color\" \"OSC 12 ; Pt BEL\" \"Same as OSC 10, but for default cursor color.\"\n */\n public setOrReportCursorColor(data: string): boolean {\n return this._setOrReportSpecialColor(data, 2);\n }\n\n /**\n * OSC 104 ; ST - restore ANSI color \n *\n * @vt: #Y OSC 104 \"Reset ANSI color\" \"OSC 104 ; c BEL\" \"Reset color number `c` to themed color.\"\n * `c` is the color index between 0 and 255. This function restores the default color for `c` as\n * specified by the loaded theme. Any number of `c` parameters may be given.\n * If no parameters are given, the entire indexed color table will be reset.\n */\n public restoreIndexedColor(data: string): boolean {\n if (!data) {\n this._onColor.fire([{ type: ColorRequestType.RESTORE }]);\n return true;\n }\n const event: IColorEvent = [];\n const slots = data.split(';');\n for (let i = 0; i < slots.length; ++i) {\n if (/^\\d+$/.exec(slots[i])) {\n const index = parseInt(slots[i], 10);\n if (isValidColorIndex(index)) {\n event.push({ type: ColorRequestType.RESTORE, index });\n }\n }\n }\n if (event.length) {\n this._onColor.fire(event);\n }\n return true;\n }\n\n /**\n * OSC 110 ST - restore default foreground color\n *\n * @vt: #Y OSC 110 \"Restore default foreground color\" \"OSC 110 BEL\" \"Restore default foreground to themed color.\"\n */\n public restoreFgColor(data: string): boolean {\n this._onColor.fire([{ type: ColorRequestType.RESTORE, index: SpecialColorIndex.FOREGROUND }]);\n return true;\n }\n\n /**\n * OSC 111 ST - restore default background color\n *\n * @vt: #Y OSC 111 \"Restore default background color\" \"OSC 111 BEL\" \"Restore default background to themed color.\"\n */\n public restoreBgColor(data: string): boolean {\n this._onColor.fire([{ type: ColorRequestType.RESTORE, index: SpecialColorIndex.BACKGROUND }]);\n return true;\n }\n\n /**\n * OSC 112 ST - restore default cursor color\n *\n * @vt: #Y OSC 112 \"Restore default cursor color\" \"OSC 112 BEL\" \"Restore default cursor to themed color.\"\n */\n public restoreCursorColor(data: string): boolean {\n this._onColor.fire([{ type: ColorRequestType.RESTORE, index: SpecialColorIndex.CURSOR }]);\n return true;\n }\n\n /**\n * ESC E\n * C1.NEL\n * DEC mnemonic: NEL (https://vt100.net/docs/vt510-rm/NEL)\n * Moves cursor to first position on next line.\n *\n * @vt: #Y C1 NEL \"Next Line\" \"\\x85\" \"Move the cursor to the beginning of the next row.\"\n * @vt: #Y ESC NEL \"Next Line\" \"ESC E\" \"Move the cursor to the beginning of the next row.\"\n */\n public nextLine(): boolean {\n this._activeBuffer.x = 0;\n this.index();\n return true;\n }\n\n /**\n * ESC =\n * DEC mnemonic: DECKPAM (https://vt100.net/docs/vt510-rm/DECKPAM.html)\n * Enables the numeric keypad to send application sequences to the host.\n */\n public keypadApplicationMode(): boolean {\n this._logService.debug('Serial port requested application keypad.');\n this._coreService.decPrivateModes.applicationKeypad = true;\n this._onRequestSyncScrollBar.fire();\n return true;\n }\n\n /**\n * ESC >\n * DEC mnemonic: DECKPNM (https://vt100.net/docs/vt510-rm/DECKPNM.html)\n * Enables the keypad to send numeric characters to the host.\n */\n public keypadNumericMode(): boolean {\n this._logService.debug('Switching back to normal keypad.');\n this._coreService.decPrivateModes.applicationKeypad = false;\n this._onRequestSyncScrollBar.fire();\n return true;\n }\n\n /**\n * ESC % @\n * ESC % G\n * Select default character set. UTF-8 is not supported (string are unicode anyways)\n * therefore ESC % G does the same.\n */\n public selectDefaultCharset(): boolean {\n this._charsetService.setgLevel(0);\n this._charsetService.setgCharset(0, DEFAULT_CHARSET); // US (default)\n return true;\n }\n\n /**\n * ESC ( C\n * Designate G0 Character Set, VT100, ISO 2022.\n * ESC ) C\n * Designate G1 Character Set (ISO 2022, VT100).\n * ESC * C\n * Designate G2 Character Set (ISO 2022, VT220).\n * ESC + C\n * Designate G3 Character Set (ISO 2022, VT220).\n * ESC - C\n * Designate G1 Character Set (VT300).\n * ESC . C\n * Designate G2 Character Set (VT300).\n * ESC / C\n * Designate G3 Character Set (VT300). C = A -> ISO Latin-1 Supplemental. - Supported?\n */\n public selectCharset(collectAndFlag: string): boolean {\n if (collectAndFlag.length !== 2) {\n this.selectDefaultCharset();\n return true;\n }\n if (collectAndFlag[0] === '/') {\n return true; // TODO: Is this supported?\n }\n this._charsetService.setgCharset(GLEVEL[collectAndFlag[0]], CHARSETS[collectAndFlag[1]] ?? DEFAULT_CHARSET);\n return true;\n }\n\n /**\n * ESC D\n * C1.IND\n * DEC mnemonic: IND (https://vt100.net/docs/vt510-rm/IND.html)\n * Moves the cursor down one line in the same column.\n *\n * @vt: #Y C1 IND \"Index\" \"\\x84\" \"Move the cursor one line down scrolling if needed.\"\n * @vt: #Y ESC IND \"Index\" \"ESC D\" \"Move the cursor one line down scrolling if needed.\"\n */\n public index(): boolean {\n this._restrictCursor();\n this._activeBuffer.y++;\n if (this._activeBuffer.y === this._activeBuffer.scrollBottom + 1) {\n this._activeBuffer.y--;\n this._bufferService.scroll(this._eraseAttrData());\n } else if (this._activeBuffer.y >= this._bufferService.rows) {\n this._activeBuffer.y = this._bufferService.rows - 1;\n }\n this._restrictCursor();\n return true;\n }\n\n /**\n * ESC H\n * C1.HTS\n * DEC mnemonic: HTS (https://vt100.net/docs/vt510-rm/HTS.html)\n * Sets a horizontal tab stop at the column position indicated by\n * the value of the active column when the terminal receives an HTS.\n *\n * @vt: #Y C1 HTS \"Horizontal Tabulation Set\" \"\\x88\" \"Places a tab stop at the current cursor position.\"\n * @vt: #Y ESC HTS \"Horizontal Tabulation Set\" \"ESC H\" \"Places a tab stop at the current cursor position.\"\n */\n public tabSet(): boolean {\n this._activeBuffer.tabs[this._activeBuffer.x] = true;\n return true;\n }\n\n /**\n * ESC M\n * C1.RI\n * DEC mnemonic: HTS\n * Moves the cursor up one line in the same column. If the cursor is at the top margin,\n * the page scrolls down.\n *\n * @vt: #Y ESC IR \"Reverse Index\" \"ESC M\" \"Move the cursor one line up scrolling if needed.\"\n */\n public reverseIndex(): boolean {\n this._restrictCursor();\n if (this._activeBuffer.y === this._activeBuffer.scrollTop) {\n // possibly move the code below to term.reverseScroll();\n // test: echo -ne '\\e[1;1H\\e[44m\\eM\\e[0m'\n // blankLine(true) is xterm/linux behavior\n const scrollRegionHeight = this._activeBuffer.scrollBottom - this._activeBuffer.scrollTop;\n this._activeBuffer.lines.shiftElements(this._activeBuffer.ybase + this._activeBuffer.y, scrollRegionHeight, 1);\n this._activeBuffer.lines.set(this._activeBuffer.ybase + this._activeBuffer.y, this._activeBuffer.getBlankLine(this._eraseAttrData()));\n this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);\n } else {\n this._activeBuffer.y--;\n this._restrictCursor(); // quickfix to not run out of bounds\n }\n return true;\n }\n\n /**\n * ESC c\n * DEC mnemonic: RIS (https://vt100.net/docs/vt510-rm/RIS.html)\n * Reset to initial state.\n *\n * @vt: #Y ESC RIS \"Full Reset\" \"ESC c\" \"Reset to initial state.\"\n */\n public fullReset(): boolean {\n this._parser.reset();\n this._onRequestReset.fire();\n return true;\n }\n\n public reset(): void {\n this._curAttrData = DEFAULT_ATTR_DATA.clone();\n this._eraseAttrDataInternal = DEFAULT_ATTR_DATA.clone();\n }\n\n /**\n * back_color_erase feature for xterm.\n */\n private _eraseAttrData(): IAttributeData {\n this._eraseAttrDataInternal.bg &= ~(Attributes.CM_MASK | 0xFFFFFF);\n this._eraseAttrDataInternal.bg |= this._curAttrData.bg & ~0xFC000000;\n return this._eraseAttrDataInternal;\n }\n\n /**\n * ESC n\n * ESC o\n * ESC |\n * ESC }\n * ESC ~\n * DEC mnemonic: LS (https://vt100.net/docs/vt510-rm/LS.html)\n * When you use a locking shift, the character set remains in GL or GR until\n * you use another locking shift. (partly supported)\n */\n public setgLevel(level: number): boolean {\n this._charsetService.setgLevel(level);\n return true;\n }\n\n /**\n * ESC # 8\n * DEC mnemonic: DECALN (https://vt100.net/docs/vt510-rm/DECALN.html)\n * This control function fills the complete screen area with\n * a test pattern (E) used for adjusting screen alignment.\n *\n * @vt: #Y ESC DECALN \"Screen Alignment Pattern\" \"ESC # 8\" \"Fill viewport with a test pattern (E).\"\n */\n public screenAlignmentPattern(): boolean {\n // prepare cell data\n const cell = new CellData();\n cell.content = 1 << Content.WIDTH_SHIFT | 'E'.charCodeAt(0);\n cell.fg = this._curAttrData.fg;\n cell.bg = this._curAttrData.bg;\n\n\n this._setCursor(0, 0);\n for (let yOffset = 0; yOffset < this._bufferService.rows; ++yOffset) {\n const row = this._activeBuffer.ybase + this._activeBuffer.y + yOffset;\n const line = this._activeBuffer.lines.get(row);\n if (line) {\n line.fill(cell);\n line.isWrapped = false;\n }\n }\n this._dirtyRowTracker.markAllDirty();\n this._setCursor(0, 0);\n return true;\n }\n\n\n /**\n * DCS $ q Pt ST\n * DECRQSS (https://vt100.net/docs/vt510-rm/DECRQSS.html)\n * Request Status String (DECRQSS), VT420 and up.\n * Response: DECRPSS (https://vt100.net/docs/vt510-rm/DECRPSS.html)\n *\n * @vt: #P[Limited support, see below.] DCS DECRQSS \"Request Selection or Setting\" \"DCS $ q Pt ST\" \"Request several terminal settings.\"\n * Response is in the form `ESC P 1 $ r Pt ST` for valid requests, where `Pt` contains the\n * corresponding CSI string, `ESC P 0 ST` for invalid requests.\n *\n * Supported requests and responses:\n *\n * | Type | Request | Response (`Pt`) |\n * | -------------------------------- | ----------------- | ----------------------------------------------------- |\n * | Graphic Rendition (SGR) | `DCS $ q m ST` | always reporting `0m` (currently broken) |\n * | Top and Bottom Margins (DECSTBM) | `DCS $ q r ST` | `Ps ; Ps r` |\n * | Cursor Style (DECSCUSR) | `DCS $ q SP q ST` | `Ps SP q` |\n * | Protection Attribute (DECSCA) | `DCS $ q \" q ST` | `Ps \" q` (DECSCA 2 is reported as Ps = 0) |\n * | Conformance Level (DECSCL) | `DCS $ q \" p ST` | always reporting `61 ; 1 \" p` (DECSCL is unsupported) |\n *\n *\n * TODO:\n * - fix SGR report\n * - either check which conformance is better suited or remove the report completely\n * --> we are currently a mixture of all up to VT400 but dont follow anyone strictly\n */\n public requestStatusString(data: string, params: IParams): boolean {\n const f = (s: string): boolean => {\n this._coreService.triggerDataEvent(`${C0.ESC}${s}${C0.ESC}\\\\`);\n return true;\n };\n\n // access helpers\n const b = this._bufferService.buffer;\n const opts = this._optionsService.rawOptions;\n const STYLES: { [key: string]: number } = { 'block': 2, 'underline': 4, 'bar': 6 };\n\n if (data === '\"q') return f(`P1$r${this._curAttrData.isProtected() ? 1 : 0}\"q`);\n if (data === '\"p') return f(`P1$r61;1\"p`);\n if (data === 'r') return f(`P1$r${b.scrollTop + 1};${b.scrollBottom + 1}r`);\n // FIXME: report real SGR settings instead of 0m\n if (data === 'm') return f(`P1$r0m`);\n if (data === ' q') return f(`P1$r${STYLES[opts.cursorStyle] - (opts.cursorBlink ? 1 : 0)} q`);\n return f(`P0$r`);\n }\n\n public markRangeDirty(y1: number, y2: number): void {\n this._dirtyRowTracker.markRangeDirty(y1, y2);\n }\n\n // #region Kitty keyboard\n\n /**\n * CSI = flags ; mode u\n * Set Kitty keyboard protocol flags.\n * mode: 1=set, 2=set-only-specified, 3=reset-only-specified\n *\n * @vt: #Y CSI KKBDSET \"Kitty Keyboard Set\" \"CSI = Ps ; Pm u\" \"Set Kitty keyboard protocol flags.\"\n */\n public kittyKeyboardSet(params: IParams): boolean {\n if (!this._optionsService.rawOptions.vtExtensions?.kittyKeyboard) {\n return true;\n }\n const flags = params.params[0] || 0;\n const mode = params.length > 1 ? (params.params[1] || 1) : 1;\n const state = this._coreService.kittyKeyboard;\n\n switch (mode) {\n case 1: // Set all flags\n state.flags = flags;\n break;\n case 2: // Set only specified flags (OR)\n state.flags |= flags;\n break;\n case 3: // Reset only specified flags (AND NOT)\n state.flags &= ~flags;\n break;\n }\n return true;\n }\n\n /**\n * CSI ? u\n * Query Kitty keyboard protocol flags.\n * Terminal responds with CSI ? flags u\n *\n * @vt: #Y CSI KKBDQUERY \"Kitty Keyboard Query\" \"CSI ? u\" \"Query Kitty keyboard protocol flags.\"\n */\n public kittyKeyboardQuery(params: IParams): boolean {\n if (!this._optionsService.rawOptions.vtExtensions?.kittyKeyboard) {\n return true;\n }\n const flags = this._coreService.kittyKeyboard.flags;\n this._coreService.triggerDataEvent(`${C0.ESC}[?${flags}u`);\n return true;\n }\n\n /**\n * CSI > flags u\n * Push Kitty keyboard flags onto stack and set new flags.\n *\n * @vt: #Y CSI KKBDPUSH \"Kitty Keyboard Push\" \"CSI > Ps u\" \"Push keyboard flags to stack and set new flags.\"\n */\n public kittyKeyboardPush(params: IParams): boolean {\n if (!this._optionsService.rawOptions.vtExtensions?.kittyKeyboard) {\n return true;\n }\n const flags = params.params[0] || 0;\n const state = this._coreService.kittyKeyboard;\n const isAlt = this._bufferService.buffer === this._bufferService.buffers.alt;\n const stack = isAlt ? state.altStack : state.mainStack;\n\n // Evict oldest entry if stack is full (DoS protection, limit of 16)\n if (stack.length >= 16) {\n stack.shift();\n }\n\n // Push current flags onto stack and set new flags\n stack.push(state.flags);\n state.flags = flags;\n return true;\n }\n\n /**\n * CSI < count u\n * Pop Kitty keyboard flags from stack.\n *\n * @vt: #Y CSI KKBDPOP \"Kitty Keyboard Pop\" \"CSI < Ps u\" \"Pop keyboard flags from stack.\"\n */\n public kittyKeyboardPop(params: IParams): boolean {\n if (!this._optionsService.rawOptions.vtExtensions?.kittyKeyboard) {\n return true;\n }\n const count = Math.max(1, params.params[0] || 1);\n const state = this._coreService.kittyKeyboard;\n const isAlt = this._bufferService.buffer === this._bufferService.buffers.alt;\n const stack = isAlt ? state.altStack : state.mainStack;\n\n // Pop specified number of entries from stack\n for (let i = 0; i < count && stack.length > 0; i++) {\n state.flags = stack.pop()!;\n }\n // If stack is empty after popping, reset to 0\n if (stack.length === 0 && count > 0) {\n state.flags = 0;\n }\n return true;\n }\n\n // #endregion\n}\n\nexport interface IDirtyRowTracker {\n readonly start: number;\n readonly end: number;\n\n clearRange(): void;\n markDirty(y: number): void;\n markRangeDirty(y1: number, y2: number): void;\n markAllDirty(): void;\n}\n\nclass DirtyRowTracker implements IDirtyRowTracker {\n public start!: number;\n public end!: number;\n\n constructor(\n @IBufferService private readonly _bufferService: IBufferService\n ) {\n this.clearRange();\n }\n\n public clearRange(): void {\n this.start = this._bufferService.buffer.y;\n this.end = this._bufferService.buffer.y;\n }\n\n public markDirty(y: number): void {\n if (y < this.start) {\n this.start = y;\n } else if (y > this.end) {\n this.end = y;\n }\n }\n\n public markRangeDirty(y1: number, y2: number): void {\n if (y1 > y2) {\n $temp = y1;\n y1 = y2;\n y2 = $temp;\n }\n if (y1 < this.start) {\n this.start = y1;\n }\n if (y2 > this.end) {\n this.end = y2;\n }\n }\n\n public markAllDirty(): void {\n this.markRangeDirty(0, this._bufferService.rows - 1);\n }\n}\n\nexport function isValidColorIndex(value: number): value is ColorIndex {\n return 0 <= value && value < 256;\n}\n", "/**\n * Copyright (c) 2026 The xterm.js authors. All rights reserved.\n * @license MIT\n *\n * Minimal async helpers for xterm.js core.\n */\n\nimport { DisposableStore, IDisposable, toDisposable } from './Lifecycle';\n\nexport function timeout(millis: number): Promise {\n return new Promise(resolve => setTimeout(resolve, millis));\n}\n\n/**\n * Creates a timeout that can be disposed using its returned value.\n * @param handler The timeout handler.\n * @param timeout An optional timeout in milliseconds.\n * @param store An optional {@link DisposableStore} that will have the timeout disposable managed\n * automatically.\n */\nexport function disposableTimeout(handler: () => void, timeout = 0, store?: DisposableStore): IDisposable {\n const timer = setTimeout(() => {\n handler();\n if (store) {\n disposable.dispose();\n }\n }, timeout);\n const disposable = toDisposable(() => {\n clearTimeout(timer);\n });\n store?.add(disposable);\n return disposable;\n}\n\nexport class TimeoutTimer implements IDisposable {\n private _token: any = -1;\n private _isDisposed = false;\n\n public dispose(): void {\n this.cancel();\n this._isDisposed = true;\n }\n\n public cancel(): void {\n if (this._token !== -1) {\n clearTimeout(this._token);\n this._token = -1;\n }\n }\n\n public cancelAndSet(runner: () => void, timeout: number): void {\n if (this._isDisposed) {\n throw new Error('Calling cancelAndSet on a disposed TimeoutTimer');\n }\n this.cancel();\n this._token = setTimeout(() => {\n this._token = -1;\n runner();\n }, timeout);\n }\n\n public setIfNotSet(runner: () => void, timeout: number): void {\n if (this._isDisposed) {\n throw new Error('Calling setIfNotSet on a disposed TimeoutTimer');\n }\n if (this._token !== -1) {\n return;\n }\n this._token = setTimeout(() => {\n this._token = -1;\n runner();\n }, timeout);\n }\n}\n\n/**\n * Schedules a single runner on the microtask queue. Unlike {@link TimeoutTimer}, a scheduled\n * microtask cannot be unqueued; {@link cancel} prevents the runner from executing if it has not\n * run yet.\n */\nexport class MicrotaskTimer implements IDisposable {\n private _isScheduled = false;\n private _isDisposed = false;\n\n public dispose(): void {\n this.cancel();\n this._isDisposed = true;\n }\n\n public cancel(): void {\n this._isScheduled = false;\n }\n\n public set(runner: () => void): void {\n if (this._isDisposed) {\n throw new Error('Calling set on a disposed MicrotaskTimer');\n }\n if (this._isScheduled) {\n return;\n }\n this._isScheduled = true;\n queueMicrotask(() => {\n if (!this._isScheduled) {\n return;\n }\n this._isScheduled = false;\n runner();\n });\n }\n}\n\nexport class IntervalTimer implements IDisposable {\n private _disposable: IDisposable | undefined;\n private _isDisposed = false;\n\n public cancel(): void {\n this._disposable?.dispose();\n this._disposable = undefined;\n }\n\n public cancelAndSet(runner: () => void, interval: number, context: Window | typeof globalThis = globalThis): void {\n if (this._isDisposed) {\n throw new Error('Calling cancelAndSet on a disposed IntervalTimer');\n }\n this.cancel();\n const handle = context.setInterval(() => {\n runner();\n }, interval);\n this._disposable = {\n dispose: () => {\n context.clearInterval(handle as any);\n this._disposable = undefined;\n }\n };\n }\n\n public dispose(): void {\n this.cancel();\n this._isDisposed = true;\n }\n}\n", "\n/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { TimeoutTimer } from '../Async';\nimport { Disposable, toDisposable } from '../Lifecycle';\nimport { Emitter } from '../Event';\n\nconst enum Constants {\n /**\n * Safety watermark to avoid memory exhaustion and browser engine crash on fast data input.\n * Enable flow control to avoid this limit and make sure that your backend correctly\n * propagates this to the underlying pty. (see docs for further instructions)\n * Since this limit is meant as a safety parachute to prevent browser crashs,\n * it is set to a very high number. Typically xterm.js gets unresponsive with\n * a 100 times lower number (>500 kB).\n */\n DISCARD_WATERMARK = 50000000, // ~50 MB\n /**\n * The max number of ms to spend on writes before allowing the renderer to\n * catch up with a 0ms setTimeout. A value of < 33 to keep us close to\n * 30fps, and a value of < 16 to try to run at 60fps. Of course, the real FPS\n * depends on the time it takes for the renderer to draw the frame.\n */\n WRITE_TIMEOUT_MS = 12,\n /**\n * Threshold of max held chunks in the write buffer, that were already processed.\n * This is a tradeoff between extensive write buffer shifts (bad runtime) and high\n * memory consumption by data thats not used anymore.\n */\n WRITE_BUFFER_LENGTH_THRESHOLD = 50\n}\n\nexport class WriteBuffer extends Disposable {\n private _writeBuffer: (string | Uint8Array)[] = [];\n private _callbacks: ((() => void) | undefined)[] = [];\n private _pendingData = 0;\n private _bufferOffset = 0;\n private _isSyncWriting = false;\n private _syncCalls = 0;\n private _didUserInput = false;\n\n private readonly _innerWriteTimer = this._register(new TimeoutTimer());\n private readonly _onWriteParsed = this._register(new Emitter());\n public readonly onWriteParsed = this._onWriteParsed.event;\n\n constructor(private _action: (data: string | Uint8Array, promiseResult?: boolean) => void | Promise) {\n super();\n this._register(toDisposable(() => {\n this._writeBuffer.length = 0;\n this._callbacks.length = 0;\n this._pendingData = 0;\n this._bufferOffset = 0;\n }));\n }\n\n public handleUserInput(): void {\n this._didUserInput = true;\n }\n\n /**\n * Flushes all pending writes synchronously. This is useful when you need to\n * ensure all queued data is processed before performing an operation that\n * depends upon everything being parsed like resize.\n *\n * Note: This is unreliable with async parser handlers as it does not wait for\n * promises to resolve.\n */\n public flushSync(): void {\n if (this._store.isDisposed) {\n return;\n }\n // exit early if another sync write loop is active\n if (this._isSyncWriting) {\n return;\n }\n this._isSyncWriting = true;\n\n // Process all pending chunks synchronously\n let chunk: string | Uint8Array | undefined;\n let didProcess = false;\n while (chunk = this._writeBuffer.shift()) {\n didProcess = true;\n this._action(chunk);\n const cb = this._callbacks.shift();\n if (cb) cb();\n }\n\n // Reset buffer state\n this._pendingData = 0;\n this._bufferOffset = 0x7FFFFFFF;\n this._writeBuffer.length = 0;\n this._callbacks.length = 0;\n\n this._isSyncWriting = false;\n if (didProcess) {\n this._onWriteParsed.fire();\n }\n }\n\n /**\n * @deprecated Unreliable, to be removed soon.\n */\n public writeSync(data: string | Uint8Array, maxSubsequentCalls?: number): void {\n if (this._store.isDisposed) {\n return;\n }\n // stop writeSync recursions with maxSubsequentCalls argument\n // This is dangerous to use as it will lose the current data chunk\n // and return immediately.\n if (maxSubsequentCalls !== undefined && this._syncCalls > maxSubsequentCalls) {\n // comment next line if a whole loop block should only contain x `writeSync` calls\n // (total flat vs. deep nested limit)\n this._syncCalls = 0;\n return;\n }\n // append chunk to buffer\n this._pendingData += data.length;\n this._writeBuffer.push(data);\n this._callbacks.push(undefined);\n\n // increase recursion counter\n this._syncCalls++;\n // exit early if another writeSync loop is active\n if (this._isSyncWriting) {\n return;\n }\n this._isSyncWriting = true;\n\n // force sync processing on pending data chunks to avoid in-band data scrambling\n // does the same as innerWrite but without event loop\n // we have to do it here as single loop steps to not corrupt loop subject\n // by another writeSync call triggered from _action\n let chunk: string | Uint8Array | undefined;\n while (chunk = this._writeBuffer.shift()) {\n this._action(chunk);\n const cb = this._callbacks.shift();\n if (cb) cb();\n }\n // reset to avoid reprocessing of chunks with scheduled innerWrite call\n // stopping scheduled innerWrite by offset > length condition\n this._pendingData = 0;\n this._bufferOffset = 0x7FFFFFFF;\n\n // allow another writeSync to loop\n this._isSyncWriting = false;\n this._syncCalls = 0;\n }\n\n public write(data: string | Uint8Array, callback?: () => void): void {\n if (this._store.isDisposed) {\n return;\n }\n if (this._pendingData > Constants.DISCARD_WATERMARK) {\n throw new Error('write data discarded, use flow control to avoid losing data');\n }\n\n // schedule chunk processing for next event loop run\n if (!this._writeBuffer.length) {\n this._bufferOffset = 0;\n\n // If this is the first write call after the user has done some input,\n // parse it immediately to minimize input latency,\n // otherwise schedule for the next event\n if (this._didUserInput) {\n this._didUserInput = false;\n this._pendingData += data.length;\n this._writeBuffer.push(data);\n this._callbacks.push(callback);\n this._innerWrite();\n return;\n }\n\n this._scheduleInnerWrite();\n }\n\n this._pendingData += data.length;\n this._writeBuffer.push(data);\n this._callbacks.push(callback);\n }\n\n /**\n * Inner write call, that enters the sliced chunk processing by timing.\n *\n * `lastTime` indicates, when the last _innerWrite call had started.\n * It is used to aggregate async handler execution under a timeout constraint\n * effectively lowering the redrawing needs, schematically:\n *\n * macroTask _innerWrite:\n * if (performance.now() - (lastTime | 0) < Constants.WRITE_TIMEOUT_MS):\n * schedule microTask _innerWrite(lastTime)\n * else:\n * schedule macroTask _innerWrite(0)\n *\n * overall execution order on task queues:\n *\n * macrotasks: [...] --> _innerWrite(0) --> [...] --> screenUpdate --> [...]\n * m t: |\n * i a: [...]\n * c s: |\n * r k: while < timeout:\n * o s: _innerWrite(timeout)\n *\n * `promiseResult` depicts the promise resolve value of an async handler.\n * This value gets carried forward through all saved stack states of the\n * paused parser for proper continuation.\n *\n * Note, for pure sync code `lastTime` and `promiseResult` have no meaning.\n */\n private _scheduleInnerWrite(lastTime: number = 0, promiseResult: boolean = true): void {\n if (this._store.isDisposed) {\n return;\n }\n this._innerWriteTimer.cancelAndSet(() => this._innerWrite(lastTime, promiseResult), 0);\n }\n\n protected _innerWrite(lastTime: number = 0, promiseResult: boolean = true): void {\n if (this._store.isDisposed) {\n return;\n }\n const startTime = lastTime || performance.now();\n while (this._writeBuffer.length > this._bufferOffset) {\n const data = this._writeBuffer[this._bufferOffset];\n const result = this._action(data, promiseResult);\n if (result) {\n /**\n * If we get a promise as return value, we re-schedule the continuation\n * as thenable on the promise and exit right away.\n *\n * The exit here means, that we block input processing at the current active chunk,\n * the exact execution position within the chunk is preserved by the saved\n * stack content in InputHandler and EscapeSequenceParser.\n *\n * Resuming happens automatically from that saved stack state.\n * Also the resolved promise value is passed along the callstack to\n * `EscapeSequenceParser.parse` to correctly resume the stopped handler loop.\n *\n * Exceptions on async handlers will be logged to console async, but do not interrupt\n * the input processing (continues with next handler at the current input position).\n */\n\n /**\n * If a promise takes long to resolve, we should schedule continuation behind setTimeout.\n * This might already be too late, if our .then enters really late (executor + prev thens\n * took very long). This cannot be solved here for the handler itself (it is the handlers\n * responsibility to slice hard work), but we can at least schedule a screen update as we\n * gain control.\n */\n const continuation: (r: boolean) => void = (r: boolean) => {\n if (this._store.isDisposed) {\n return;\n }\n if (performance.now() - startTime >= Constants.WRITE_TIMEOUT_MS) {\n this._scheduleInnerWrite(0, r);\n } else {\n this._innerWrite(startTime, r);\n }\n };\n\n /**\n * Optimization considerations:\n * The continuation above favors FPS over throughput by eval'ing `startTime` on resolve.\n * This might schedule too many screen updates with bad throughput drops (in case a slow\n * resolving handler sliced its work properly behind setTimeout calls). We cannot spot\n * this condition here, also the renderer has no way to spot nonsense updates either.\n * FIXME: A proper fix for this would track the FPS at the renderer entry level separately.\n *\n * If favoring of FPS shows bad throughput impact, use the following instead. It favors\n * throughput by eval'ing `startTime` upfront pulling at least one more chunk into the\n * current microtask queue (executed before setTimeout).\n */\n // const continuation: (r: boolean) => void = performance.now() - startTime >=\n // Constants.WRITE_TIMEOUT_MS\n // ? r => setTimeout(() => this._innerWrite(0, r))\n // : r => this._innerWrite(startTime, r);\n\n // Handle exceptions synchronously to current band position, idea:\n // 1. spawn a single microtask which we allow to throw hard\n // 2. spawn a promise immediately resolving to `true`\n // (executed on the same queue, thus properly aligned before continuation happens)\n result.catch(err => {\n queueMicrotask(() => {throw err;});\n return Promise.resolve(false);\n }).then(continuation);\n return;\n }\n\n const cb = this._callbacks[this._bufferOffset];\n if (cb) cb();\n this._bufferOffset++;\n this._pendingData -= data.length;\n\n if (performance.now() - startTime >= Constants.WRITE_TIMEOUT_MS) {\n break;\n }\n }\n if (this._writeBuffer.length > this._bufferOffset) {\n // Allow renderer to catch up before processing the next batch\n // trim already processed chunks if we are above threshold\n if (this._bufferOffset > Constants.WRITE_BUFFER_LENGTH_THRESHOLD) {\n this._writeBuffer = this._writeBuffer.slice(this._bufferOffset);\n this._callbacks = this._callbacks.slice(this._bufferOffset);\n this._bufferOffset = 0;\n }\n this._scheduleInnerWrite();\n } else {\n this._writeBuffer.length = 0;\n this._callbacks.length = 0;\n this._pendingData = 0;\n this._bufferOffset = 0;\n }\n this._onWriteParsed.fire();\n }\n}\n", "/**\n * Copyright (c) 2022 The xterm.js authors. All rights reserved.\n * @license MIT\n */\nimport { IBufferService, IOscLinkService } from './Services';\nimport { IOscLinkData } from '../Types';\nimport { IMarker } from '../buffer/Types';\n\nexport class OscLinkService implements IOscLinkService {\n public serviceBrand: any;\n\n private _nextId = 1;\n\n /**\n * A map of the link key to link entry. This is used to add additional lines to links with ids.\n */\n private _entriesWithId: Map = new Map();\n\n /**\n * A map of the link id to the link entry. The \"link id\" (number) which is the numberic\n * representation of a unique link should not be confused with \"id\" (string) which comes in with\n * `id=` in the OSC link's properties.\n */\n private _dataByLinkId: Map = new Map();\n\n constructor(\n @IBufferService private readonly _bufferService: IBufferService\n ) {\n }\n\n public registerLink(data: IOscLinkData): number {\n const buffer = this._bufferService.buffer;\n\n // Links with no id will only ever be registered a single time\n if (data.id === undefined) {\n const marker = buffer.addMarker(buffer.ybase + buffer.y);\n const entry: IOscLinkEntryNoId = {\n data,\n id: this._nextId++,\n lines: [marker]\n };\n marker.onDispose(() => this._removeMarkerFromLink(entry, marker));\n this._dataByLinkId.set(entry.id, entry);\n return entry.id;\n }\n\n // Add the line to the link if it already exists\n const castData = data as Required;\n const key = this._getEntryIdKey(castData);\n const match = this._entriesWithId.get(key);\n if (match) {\n this.addLineToLink(match.id, buffer.ybase + buffer.y);\n return match.id;\n }\n\n // Create the link\n const marker = buffer.addMarker(buffer.ybase + buffer.y);\n const entry: IOscLinkEntryWithId = {\n id: this._nextId++,\n key: this._getEntryIdKey(castData),\n data: castData,\n lines: [marker]\n };\n marker.onDispose(() => this._removeMarkerFromLink(entry, marker));\n this._entriesWithId.set(entry.key, entry);\n this._dataByLinkId.set(entry.id, entry);\n return entry.id;\n }\n\n public addLineToLink(linkId: number, y: number): void {\n const entry = this._dataByLinkId.get(linkId);\n if (!entry) {\n return;\n }\n if (entry.lines.every(e => e.line !== y)) {\n const marker = this._bufferService.buffer.addMarker(y);\n entry.lines.push(marker);\n marker.onDispose(() => this._removeMarkerFromLink(entry, marker));\n }\n }\n\n public getLinkData(linkId: number): IOscLinkData | undefined {\n return this._dataByLinkId.get(linkId)?.data;\n }\n\n private _getEntryIdKey(linkData: Required): string {\n return `${linkData.id};;${linkData.uri}`;\n }\n\n private _removeMarkerFromLink(entry: IOscLinkEntryNoId | IOscLinkEntryWithId, marker: IMarker): void {\n const index = entry.lines.indexOf(marker);\n if (index === -1) {\n return;\n }\n entry.lines.splice(index, 1);\n if (entry.lines.length === 0) {\n if (entry.data.id !== undefined) {\n this._entriesWithId.delete((entry as IOscLinkEntryWithId).key);\n }\n this._dataByLinkId.delete(entry.id);\n }\n }\n}\n\ninterface IOscLinkEntry {\n data: T;\n id: number;\n lines: IMarker[];\n}\n\ninterface IOscLinkEntryNoId extends IOscLinkEntry {\n}\n\ninterface IOscLinkEntryWithId extends IOscLinkEntry> {\n key: string;\n}\n", "/**\n * Copyright (c) 2014-2020 The xterm.js authors. All rights reserved.\n * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)\n * @license MIT\n *\n * Originally forked from (with the author's permission):\n * Fabrice Bellard's javascript vt100 for jslinux:\n * http://bellard.org/jslinux/\n * Copyright (c) 2011 Fabrice Bellard\n * The original design remains. The terminal itself\n * has been extended to include xterm CSI codes, among\n * other features.\n *\n * Terminal Emulation References:\n * http://vt100.net/\n * http://invisible-island.net/xterm/ctlseqs/ctlseqs.txt\n * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html\n * http://invisible-island.net/vttest/\n * http://www.inwap.com/pdp10/ansicode.txt\n * http://linux.die.net/man/4/console_codes\n * http://linux.die.net/man/7/urxvt\n */\n\nimport { IInstantiationService, IOptionsService, IBufferService, ILogService, ICharsetService, ICoreService, IMouseStateService, IUnicodeService, LogLevelEnum, IOscLinkService } from './services/Services';\nimport { InstantiationService } from './services/InstantiationService';\nimport { LogService } from './services/LogService';\nimport { BufferService, BufferServiceConstants } from './services/BufferService';\nimport { OptionsService } from './services/OptionsService';\nimport { IDisposable, IScrollEvent, ITerminalOptions, IParams } from './Types';\nimport { IAttributeData, IBufferSet } from './buffer/Types';\nimport { CoreService } from './services/CoreService';\nimport { MouseStateService } from './services/MouseStateService';\nimport { UnicodeV6 } from './input/UnicodeV6';\nimport { UnicodeService } from './services/UnicodeService';\nimport { CharsetService } from './services/CharsetService';\nimport { updateWindowsModeWrappedState } from './WindowsMode';\nimport { IFunctionIdentifier } from './parser/Types';\nimport { InputHandler } from './InputHandler';\nimport { WriteBuffer } from './input/WriteBuffer';\nimport { OscLinkService } from './services/OscLinkService';\nimport { Emitter, EventUtils, type IEvent } from './Event';\nimport { Disposable, MutableDisposable, toDisposable } from './Lifecycle';\n\n// Only trigger this warning a single time per session\nlet hasWriteSyncWarnHappened = false;\n\nexport interface ICoreTerminal {\n mouseStateService: IMouseStateService;\n coreService: ICoreService;\n optionsService: IOptionsService;\n unicodeService: IUnicodeService;\n buffers: IBufferSet;\n options: Required;\n registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise): IDisposable;\n registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise): IDisposable;\n registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise): IDisposable;\n registerOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable;\n registerApcHandler(id: IFunctionIdentifier, callback: (data: string) => boolean | Promise): IDisposable;\n}\n\nexport abstract class CoreTerminal extends Disposable implements ICoreTerminal {\n protected readonly _instantiationService: IInstantiationService;\n protected readonly _bufferService: IBufferService;\n protected readonly _logService: ILogService;\n protected readonly _charsetService: ICharsetService;\n protected readonly _oscLinkService: IOscLinkService;\n\n public readonly mouseStateService: IMouseStateService;\n public readonly coreService: ICoreService;\n public readonly unicodeService: IUnicodeService;\n public readonly optionsService: IOptionsService;\n\n protected _inputHandler: InputHandler;\n private _writeBuffer: WriteBuffer;\n private _windowsWrappingHeuristics = this._register(new MutableDisposable());\n\n private readonly _onBinary = this._register(new Emitter());\n public readonly onBinary = this._onBinary.event;\n private readonly _onData = this._register(new Emitter());\n public readonly onData = this._onData.event;\n protected _onLineFeed = this._register(new Emitter());\n public readonly onLineFeed = this._onLineFeed.event;\n protected readonly _onRender = this._register(new Emitter<{ start: number, end: number }>());\n public readonly onRender = this._onRender.event;\n private readonly _onResize = this._register(new Emitter<{ cols: number, rows: number }>());\n public readonly onResize = this._onResize.event;\n protected readonly _onWriteParsed = this._register(new Emitter());\n public readonly onWriteParsed = this._onWriteParsed.event;\n\n /**\n * Internally we track the source of the scroll but this is meaningless outside the library so\n * it's filtered out.\n */\n protected _onScrollApi?: Emitter;\n protected _onScroll = this._register(new Emitter());\n public get onScroll(): IEvent {\n if (!this._onScrollApi) {\n this._onScrollApi = this._register(new Emitter());\n this._onScroll.event(ev => {\n this._onScrollApi?.fire(ev.position);\n });\n }\n return this._onScrollApi.event;\n }\n\n public get cols(): number { return this._bufferService.cols; }\n public get rows(): number { return this._bufferService.rows; }\n public get buffers(): IBufferSet { return this._bufferService.buffers; }\n public get options(): Required { return this.optionsService.options; }\n public set options(options: ITerminalOptions) {\n for (const key in options) {\n this.optionsService.options[key] = options[key];\n }\n }\n\n constructor(\n options: Partial\n ) {\n super();\n\n // Setup and initialize services\n this._instantiationService = new InstantiationService();\n this.optionsService = this._register(new OptionsService(options));\n this._instantiationService.setService(IOptionsService, this.optionsService);\n this._logService = this._register(this._instantiationService.createInstance(LogService));\n this._instantiationService.setService(ILogService, this._logService);\n this._bufferService = this._register(this._instantiationService.createInstance(BufferService));\n this._instantiationService.setService(IBufferService, this._bufferService);\n this.coreService = this._register(this._instantiationService.createInstance(CoreService));\n this._instantiationService.setService(ICoreService, this.coreService);\n this.mouseStateService = this._register(this._instantiationService.createInstance(MouseStateService));\n this._instantiationService.setService(IMouseStateService, this.mouseStateService);\n this.unicodeService = this._register(this._instantiationService.createInstance(UnicodeService));\n this.unicodeService.register(new UnicodeV6());\n this._instantiationService.setService(IUnicodeService, this.unicodeService);\n this._charsetService = this._instantiationService.createInstance(CharsetService);\n this._instantiationService.setService(ICharsetService, this._charsetService);\n this._oscLinkService = this._instantiationService.createInstance(OscLinkService);\n this._instantiationService.setService(IOscLinkService, this._oscLinkService);\n\n\n // Register input handler and handle/forward events\n this._inputHandler = this._register(new InputHandler(this._bufferService, this._charsetService, this.coreService, this._logService, this.optionsService, this._oscLinkService, this.mouseStateService, this.unicodeService));\n this._register(EventUtils.forward(this._inputHandler.onLineFeed, this._onLineFeed));\n\n // Setup listeners\n this._register(EventUtils.forward(this._bufferService.onResize, this._onResize));\n this._register(EventUtils.forward(this.coreService.onData, this._onData));\n this._register(EventUtils.forward(this.coreService.onBinary, this._onBinary));\n this._register(this.coreService.onRequestScrollToBottom(() => this.scrollToBottom(true)));\n this._register(this.coreService.onUserInput(() => this._writeBuffer.handleUserInput()));\n this._register(this.optionsService.onMultipleOptionChange(['windowsPty'], () => this._handleWindowsPtyOptionChange()));\n this._register(this._bufferService.onScroll(() => {\n this._onScroll.fire({ position: this._bufferService.buffer.ydisp });\n this._inputHandler.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom);\n }));\n // Setup WriteBuffer\n this._writeBuffer = this._register(new WriteBuffer((data, promiseResult) => this._inputHandler.parse(data, promiseResult)));\n this._register(EventUtils.forward(this._writeBuffer.onWriteParsed, this._onWriteParsed));\n }\n\n public write(data: string | Uint8Array, callback?: () => void): void {\n this._writeBuffer.write(data, callback);\n }\n\n /**\n * Write data to terminal synchonously.\n *\n * This method is unreliable with async parser handlers, thus should not\n * be used anymore. If you need blocking semantics on data input consider\n * `write` with a callback instead.\n *\n * @deprecated Unreliable, will be removed soon.\n */\n public writeSync(data: string | Uint8Array, maxSubsequentCalls?: number): void {\n if (this._logService.logLevel <= LogLevelEnum.WARN && !hasWriteSyncWarnHappened) {\n this._logService.warn('writeSync is unreliable and will be removed soon.');\n hasWriteSyncWarnHappened = true;\n }\n this._writeBuffer.writeSync(data, maxSubsequentCalls);\n }\n\n public input(data: string, wasUserInput: boolean = true): void {\n this.coreService.triggerDataEvent(data, wasUserInput);\n }\n\n public resize(x: number, y: number): void {\n if (isNaN(x) || isNaN(y)) {\n return;\n }\n\n x = Math.max(x, BufferServiceConstants.MINIMUM_COLS);\n y = Math.max(y, BufferServiceConstants.MINIMUM_ROWS);\n\n // Flush pending writes before resize to avoid race conditions where async\n // writes are processed with incorrect dimensions\n this._writeBuffer.flushSync();\n\n this._bufferService.resize(x, y);\n }\n\n /**\n * Scroll the terminal down 1 row, creating a blank line.\n * @param eraseAttr The attribute data to use the for blank line.\n * @param isWrapped Whether the new line is wrapped from the previous line.\n */\n public scroll(eraseAttr: IAttributeData, isWrapped: boolean = false): void {\n this._bufferService.scroll(eraseAttr, isWrapped);\n }\n\n /**\n * Scroll the display of the terminal\n * @param disp The number of lines to scroll down (negative scroll up).\n * @param suppressScrollEvent Don't emit the scroll event as scrollLines. This is used to avoid\n * unwanted events being handled by the viewport when the event was triggered from the viewport\n * originally.\n */\n public scrollLines(disp: number, suppressScrollEvent?: boolean): void {\n this._bufferService.scrollLines(disp, suppressScrollEvent);\n }\n\n public scrollPages(pageCount: number): void {\n this.scrollLines(pageCount * (this.rows - 1));\n }\n\n public scrollToTop(): void {\n this.scrollLines(-this._bufferService.buffer.ydisp);\n }\n\n public scrollToBottom(disableSmoothScroll?: boolean): void {\n this.scrollLines(this._bufferService.buffer.ybase - this._bufferService.buffer.ydisp);\n }\n\n public scrollToLine(line: number): void {\n const scrollAmount = line - this._bufferService.buffer.ydisp;\n if (scrollAmount !== 0) {\n this.scrollLines(scrollAmount);\n }\n }\n\n /** Add handler for ESC escape sequence. See xterm.d.ts for details. */\n public registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise): IDisposable {\n return this._inputHandler.registerEscHandler(id, callback);\n }\n\n /** Add handler for DCS escape sequence. See xterm.d.ts for details. */\n public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise): IDisposable {\n return this._inputHandler.registerDcsHandler(id, callback);\n }\n\n /** Add handler for CSI escape sequence. See xterm.d.ts for details. */\n public registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise): IDisposable {\n return this._inputHandler.registerCsiHandler(id, callback);\n }\n\n /** Add handler for OSC escape sequence. See xterm.d.ts for details. */\n public registerOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable {\n return this._inputHandler.registerOscHandler(ident, callback);\n }\n\n /** Add handler for APC escape sequence. See xterm.d.ts for details. */\n public registerApcHandler(id: IFunctionIdentifier, callback: (data: string) => boolean | Promise): IDisposable {\n return this._inputHandler.registerApcHandler(id, callback);\n }\n\n protected _setup(): void {\n this._handleWindowsPtyOptionChange();\n }\n\n public reset(): void {\n this._inputHandler.reset();\n this._bufferService.reset();\n this._charsetService.reset();\n this.coreService.reset();\n this.mouseStateService.reset();\n }\n\n\n private _handleWindowsPtyOptionChange(): void {\n let value = false;\n const windowsPty = this.optionsService.rawOptions.windowsPty;\n if (windowsPty && windowsPty.backend !== undefined && windowsPty.buildNumber !== undefined) {\n value = !!(windowsPty.backend === 'conpty' && windowsPty.buildNumber < 21376);\n }\n if (value) {\n this._enableWindowsWrappingHeuristics();\n } else {\n this._windowsWrappingHeuristics.clear();\n }\n }\n\n protected _enableWindowsWrappingHeuristics(): void {\n if (!this._windowsWrappingHeuristics.value) {\n const disposables: IDisposable[] = [];\n disposables.push(this.onLineFeed(updateWindowsModeWrappedState.bind(null, this._bufferService)));\n disposables.push(this.registerCsiHandler({ final: 'H' }, () => {\n updateWindowsModeWrappedState(this._bufferService);\n return false;\n }));\n this._windowsWrappingHeuristics.value = toDisposable(() => {\n for (const d of disposables) {\n d.dispose();\n }\n });\n }\n }\n}\n", "/**\n * Copyright (c) 2014 The xterm.js authors. All rights reserved.\n * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)\n * @license MIT\n *\n * Originally forked from (with the author's permission):\n * Fabrice Bellard's javascript vt100 for jslinux:\n * http://bellard.org/jslinux/\n * Copyright (c) 2011 Fabrice Bellard\n * The original design remains. The terminal itself\n * has been extended to include xterm CSI codes, among\n * other features.\n *\n * Terminal Emulation References:\n * http://vt100.net/\n * http://invisible-island.net/xterm/ctlseqs/ctlseqs.txt\n * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html\n * http://invisible-island.net/vttest/\n * http://www.inwap.com/pdp10/ansicode.txt\n * http://linux.die.net/man/4/console_codes\n * http://linux.die.net/man/7/urxvt\n */\n\nimport { DEFAULT_ATTR_DATA } from '../common/buffer/BufferLine';\nimport { IBuffer, IMarker } from '../common/buffer/Types';\nimport { CoreTerminal } from '../common/CoreTerminal';\nimport { ITerminalOptions } from '../common/Types';\nimport { Emitter, EventUtils } from '../common/Event';\n\nexport class Terminal extends CoreTerminal {\n private readonly _onBell = this._register(new Emitter());\n public readonly onBell = this._onBell.event;\n private readonly _onCursorMove = this._register(new Emitter());\n public readonly onCursorMove = this._onCursorMove.event;\n private readonly _onTitleChange = this._register(new Emitter());\n public readonly onTitleChange = this._onTitleChange.event;\n private readonly _onA11yCharEmitter = this._register(new Emitter());\n public readonly onA11yChar = this._onA11yCharEmitter.event;\n private readonly _onA11yTabEmitter = this._register(new Emitter());\n public readonly onA11yTab = this._onA11yTabEmitter.event;\n\n constructor(\n options: ITerminalOptions = {}\n ) {\n super(options);\n\n this._setup();\n\n // Setup InputHandler listeners\n this._register(this._inputHandler.onRequestBell(() => this.bell()));\n this._register(this._inputHandler.onRequestReset(() => this.reset()));\n this._register(EventUtils.forward(this._inputHandler.onCursorMove, this._onCursorMove));\n this._register(EventUtils.forward(this._inputHandler.onTitleChange, this._onTitleChange));\n this._register(EventUtils.forward(this._inputHandler.onA11yChar, this._onA11yCharEmitter));\n this._register(EventUtils.forward(this._inputHandler.onA11yTab, this._onA11yTabEmitter));\n this._register(EventUtils.forward(EventUtils.map(this._inputHandler.onRequestRefreshRows, e => ({ start: e?.start ?? 0, end: e?.end ?? this.rows - 1 })), this._onRender));\n }\n\n /**\n * Convenience property to active buffer.\n */\n public get buffer(): IBuffer {\n return this.buffers.active;\n }\n\n // TODO: Support paste here?\n\n public get markers(): IMarker[] {\n return this.buffer.markers;\n }\n\n public registerMarker(cursorYOffset: number): IMarker {\n return this.buffer.addMarker(this.buffer.ybase + this.buffer.y + cursorYOffset);\n }\n\n public bell(): void {\n this._onBell.fire();\n }\n\n public input(data: string, wasUserInput: boolean = true): void {\n this.coreService.triggerDataEvent(data, wasUserInput);\n }\n\n /**\n * Resizes the terminal.\n *\n * @param x The number of columns to resize to.\n * @param y The number of rows to resize to.\n */\n public resize(x: number, y: number): void {\n if (x === this.cols && y === this.rows) {\n return;\n }\n\n super.resize(x, y);\n }\n\n /**\n * Clear the entire buffer, making the prompt line the new first line.\n */\n public clear(): void {\n this.buffer.clearAllMarkers();\n this.buffer.lines.set(0, this.buffer.lines.get(this.buffer.ybase + this.buffer.y)!);\n this.buffer.lines.length = 1;\n this.buffer.ydisp = 0;\n this.buffer.ybase = 0;\n this.buffer.y = 0;\n for (let i = 1; i < this.rows; i++) {\n this.buffer.lines.push(this.buffer.getBlankLine(DEFAULT_ATTR_DATA));\n }\n this._onScroll.fire({ position: this.buffer.ydisp });\n }\n\n /**\n * Reset terminal.\n * Note: Calling this directly from JS is synchronous but does not clear\n * input buffers and does not reset the parser, thus the terminal will\n * continue to apply pending input data.\n * If you need in band reset (synchronous with input data) consider\n * using DECSTR (soft reset, CSI ! p) or RIS instead (hard reset, ESC c).\n */\n public reset(): void {\n /**\n * Since _setup handles a full terminal creation, we have to carry forward\n * a few things that should not reset.\n */\n this.options.rows = this.rows;\n this.options.cols = this.cols;\n\n this._setup();\n super.reset();\n }\n}\n", "/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { ITerminalAddon, IDisposable, Terminal } from '@xterm/xterm';\n\nexport interface ILoadedAddon {\n instance: ITerminalAddon;\n dispose: () => void;\n isDisposed: boolean;\n}\n\nexport class AddonManager implements IDisposable {\n protected _addons: ILoadedAddon[] = [];\n\n public dispose(): void {\n for (let i = this._addons.length - 1; i >= 0; i--) {\n this._addons[i].instance.dispose();\n }\n }\n\n public loadAddon(terminal: Terminal, instance: ITerminalAddon): void {\n const loadedAddon: ILoadedAddon = {\n instance,\n dispose: instance.dispose,\n isDisposed: false\n };\n this._addons.push(loadedAddon);\n instance.dispose = () => this._wrappedAddonDispose(loadedAddon);\n instance.activate(terminal as any);\n }\n\n private _wrappedAddonDispose(loadedAddon: ILoadedAddon): void {\n if (loadedAddon.isDisposed) {\n // Do nothing if already disposed\n return;\n }\n let index = -1;\n for (let i = 0; i < this._addons.length; i++) {\n if (this._addons[i] === loadedAddon) {\n index = i;\n break;\n }\n }\n if (index === -1) {\n throw new Error('Could not dispose an addon that has not been loaded');\n }\n loadedAddon.isDisposed = true;\n loadedAddon.dispose.apply(loadedAddon.instance);\n this._addons.splice(index, 1);\n }\n}\n", "/**\n * Copyright (c) 2018 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { BufferNamespaceApi } from '../../common/public/BufferNamespaceApi';\nimport { ParserApi } from '../../common/public/ParserApi';\nimport { UnicodeApi } from '../../common/public/UnicodeApi';\nimport { IBufferNamespace as IBufferNamespaceApi, IMarker, IModes, IParser, ITerminalAddon, ITerminalInitOnlyOptions, IUnicodeHandling, Terminal as ITerminalApi } from '@xterm/headless';\nimport { Terminal as TerminalCore } from '../Terminal';\nimport { AddonManager } from '../../common/public/AddonManager';\nimport { ITerminalOptions } from '../../common/Types';\nimport { Disposable } from '../../common/Lifecycle';\nimport type { IEvent } from '../../common/Event';\n/**\n * The set of options that only have an effect when set in the Terminal constructor.\n */\nconst CONSTRUCTOR_ONLY_OPTIONS = ['cols', 'rows'];\n\nexport class Terminal extends Disposable implements ITerminalApi {\n private _core: TerminalCore;\n private _addonManager: AddonManager;\n private _parser: IParser | undefined;\n private _buffer: BufferNamespaceApi | undefined;\n private _publicOptions: Required;\n\n constructor(options?: ITerminalOptions & ITerminalInitOnlyOptions) {\n super();\n\n this._core = this._register(new TerminalCore(options));\n this._addonManager = this._register(new AddonManager());\n\n this._publicOptions = { ... this._core.options };\n const getter = (propName: string): any => {\n return this._core.options[propName];\n };\n const setter = (propName: string, value: any): void => {\n this._checkReadonlyOptions(propName);\n this._core.options[propName] = value;\n };\n\n for (const propName in this._core.options) {\n const desc = {\n get: getter.bind(this, propName),\n set: setter.bind(this, propName)\n };\n Object.defineProperty(this._publicOptions, propName, desc);\n }\n }\n\n private _checkReadonlyOptions(propName: string): void {\n // Throw an error if any constructor only option is modified\n // from terminal.options\n // Modifications from anywhere else are allowed\n if (CONSTRUCTOR_ONLY_OPTIONS.includes(propName)) {\n throw new Error(`Option \"${propName}\" can only be set in the constructor`);\n }\n }\n\n private _checkProposedApi(): void {\n if (!this._core.optionsService.options.allowProposedApi) {\n throw new Error('You must set the allowProposedApi option to true to use proposed API');\n }\n }\n\n public get onBell(): IEvent { return this._core.onBell; }\n public get onBinary(): IEvent { return this._core.onBinary; }\n public get onCursorMove(): IEvent { return this._core.onCursorMove; }\n public get onData(): IEvent { return this._core.onData; }\n public get onLineFeed(): IEvent { return this._core.onLineFeed; }\n public get onRender(): IEvent<{ start: number, end: number }> { return this._core.onRender; }\n public get onResize(): IEvent<{ cols: number, rows: number }> { return this._core.onResize; }\n public get onScroll(): IEvent { return this._core.onScroll; }\n public get onTitleChange(): IEvent { return this._core.onTitleChange; }\n public get onWriteParsed(): IEvent { return this._core.onWriteParsed; }\n\n public get parser(): IParser {\n this._parser ??= new ParserApi(this._core);\n return this._parser;\n }\n public get unicode(): IUnicodeHandling {\n this._checkProposedApi();\n return new UnicodeApi(this._core);\n }\n public get rows(): number { return this._core.rows; }\n public get cols(): number { return this._core.cols; }\n public get buffer(): IBufferNamespaceApi {\n this._buffer ??= this._register(new BufferNamespaceApi(this._core));\n return this._buffer;\n }\n public get markers(): ReadonlyArray {\n return this._core.markers;\n }\n public get modes(): IModes {\n const m = this._core.coreService.decPrivateModes;\n let mouseTrackingMode: 'none' | 'x10' | 'vt200' | 'drag' | 'any' = 'none';\n switch (this._core.mouseStateService.activeProtocol) {\n case 'X10': mouseTrackingMode = 'x10'; break;\n case 'VT200': mouseTrackingMode = 'vt200'; break;\n case 'DRAG': mouseTrackingMode = 'drag'; break;\n case 'ANY': mouseTrackingMode = 'any'; break;\n }\n return {\n applicationCursorKeysMode: m.applicationCursorKeys,\n applicationKeypadMode: m.applicationKeypad,\n bracketedPasteMode: m.bracketedPasteMode,\n insertMode: this._core.coreService.modes.insertMode,\n mouseTrackingMode: mouseTrackingMode,\n originMode: m.origin,\n reverseWraparoundMode: m.reverseWraparound,\n sendFocusMode: m.sendFocus,\n showCursor: !this._core.coreService.isCursorHidden,\n synchronizedOutputMode: m.synchronizedOutput,\n win32InputMode: m.win32InputMode,\n wraparoundMode: m.wraparound\n };\n }\n public get options(): Required {\n return this._publicOptions;\n }\n public set options(options: ITerminalOptions) {\n for (const propName in options) {\n this._publicOptions[propName] = options[propName];\n }\n }\n public input(data: string, wasUserInput: boolean = true): void {\n this._core.input(data, wasUserInput);\n }\n public resize(columns: number, rows: number): void {\n this._verifyIntegers(columns, rows);\n this._core.resize(columns, rows);\n }\n public registerMarker(cursorYOffset: number = 0): IMarker {\n this._verifyIntegers(cursorYOffset);\n return this._core.registerMarker(cursorYOffset);\n }\n public addMarker(cursorYOffset: number): IMarker {\n return this.registerMarker(cursorYOffset);\n }\n public dispose(): void {\n super.dispose();\n }\n public scrollLines(amount: number): void {\n this._verifyIntegers(amount);\n this._core.scrollLines(amount);\n }\n public scrollPages(pageCount: number): void {\n this._verifyIntegers(pageCount);\n this._core.scrollPages(pageCount);\n }\n public scrollToTop(): void {\n this._core.scrollToTop();\n }\n public scrollToBottom(): void {\n this._core.scrollToBottom();\n }\n public scrollToLine(line: number): void {\n this._verifyIntegers(line);\n this._core.scrollToLine(line);\n }\n public clear(): void {\n this._core.clear();\n }\n public write(data: string | Uint8Array, callback?: () => void): void {\n this._core.write(data, callback);\n }\n public writeln(data: string | Uint8Array, callback?: () => void): void {\n this._core.write(data);\n this._core.write('\\r\\n', callback);\n }\n public reset(): void {\n this._core.reset();\n }\n public loadAddon(addon: ITerminalAddon): void {\n // TODO: This could cause issues if the addon calls renderer apis\n this._addonManager.loadAddon(this as any, addon);\n }\n\n private _verifyIntegers(...values: number[]): void {\n for (const value of values) {\n if (value === Infinity || isNaN(value) || value % 1 !== 0) {\n throw new Error('This API only accepts integers');\n }\n }\n }\n}\n"], +- "mappings": "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M,EAAG,CAAC,KAAQ,IAAM,EACnD,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EACnD,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EACnD,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EACnD,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EACnD,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EACnD,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EACnD,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EACnD,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EACnD,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EACnD,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EACnD,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EACnD,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EACnD,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EACnD,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EACnD,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EACnD,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EACnD,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EACnD,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EACnD,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EACnD,CAAC,KAAQ,IAA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", ++ "sourcesContent": ["/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\n/**\n * Polyfill - Convert UTF32 codepoint into JS string.\n * Note: The built-in String.fromCodePoint happens to be much slower\n * due to additional sanity checks. We can avoid them since\n * we always operate on legal UTF32 (granted by the input decoders)\n * and use this faster version instead.\n */\nexport function stringFromCodePoint(codePoint: number): string {\n if (codePoint > 0xFFFF) {\n codePoint -= 0x10000;\n return String.fromCharCode((codePoint >> 10) + 0xD800) + String.fromCharCode((codePoint % 0x400) + 0xDC00);\n }\n return String.fromCharCode(codePoint);\n}\n\n/**\n * Convert UTF32 char codes into JS string.\n * Basically the same as `stringFromCodePoint` but for multiple codepoints\n * in a loop (which is a lot faster).\n */\nexport function utf32ToString(data: Uint32Array, start: number = 0, end: number = data.length): string {\n let result = '';\n for (let i = start; i < end; ++i) {\n let codepoint = data[i];\n if (codepoint > 0xFFFF) {\n // JS strings are encoded as UTF16, thus a non BMP codepoint gets converted into a surrogate\n // pair conversion rules:\n // - subtract 0x10000 from code point, leaving a 20 bit number\n // - add high 10 bits to 0xD800 --> first surrogate\n // - add low 10 bits to 0xDC00 --> second surrogate\n codepoint -= 0x10000;\n result += String.fromCharCode((codepoint >> 10) + 0xD800) + String.fromCharCode((codepoint % 0x400) + 0xDC00);\n } else {\n result += String.fromCharCode(codepoint);\n }\n }\n return result;\n}\n\n/**\n * StringToUtf32 - decodes UTF16 sequences into UTF32 codepoints.\n * To keep the decoder in line with JS strings it handles single surrogates as UCS2.\n */\nexport class StringToUtf32 {\n private _interim: number = 0;\n\n /**\n * Clears interim and resets decoder to clean state.\n */\n public clear(): void {\n this._interim = 0;\n }\n\n /**\n * Decode JS string to UTF32 codepoints.\n * The methods assumes stream input and will store partly transmitted\n * surrogate pairs and decode them with the next data chunk.\n * Note: The method does no bound checks for target, therefore make sure\n * the provided input data does not exceed the size of `target`.\n * Returns the number of written codepoints in `target`.\n */\n public decode(input: string, target: Uint32Array): number {\n const length = input.length;\n\n if (!length) {\n return 0;\n }\n\n let size = 0;\n let startPos = 0;\n\n // handle leftover surrogate high\n if (this._interim) {\n const second = input.charCodeAt(startPos++);\n if (0xDC00 <= second && second <= 0xDFFF) {\n target[size++] = (this._interim - 0xD800) * 0x400 + second - 0xDC00 + 0x10000;\n } else {\n // illegal codepoint (USC2 handling)\n target[size++] = this._interim;\n target[size++] = second;\n }\n this._interim = 0;\n }\n\n for (let i = startPos; i < length; ++i) {\n const code = input.charCodeAt(i);\n // surrogate pair first\n if (0xD800 <= code && code <= 0xDBFF) {\n if (++i >= length) {\n this._interim = code;\n return size;\n }\n const second = input.charCodeAt(i);\n if (0xDC00 <= second && second <= 0xDFFF) {\n target[size++] = (code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000;\n } else {\n // illegal codepoint (USC2 handling)\n target[size++] = code;\n target[size++] = second;\n }\n continue;\n }\n if (code === 0xFEFF) {\n // BOM\n continue;\n }\n target[size++] = code;\n }\n return size;\n }\n}\n\n/**\n * Utf8Decoder - decodes UTF8 byte sequences into UTF32 codepoints.\n */\nexport class Utf8ToUtf32 {\n public interim: Uint8Array = new Uint8Array(3);\n\n /**\n * Clears interim bytes and resets decoder to clean state.\n */\n public clear(): void {\n this.interim.fill(0);\n }\n\n /**\n * Decodes UTF8 byte sequences in `input` to UTF32 codepoints in `target`.\n * The methods assumes stream input and will store partly transmitted bytes\n * and decode them with the next data chunk.\n * Note: The method does no bound checks for target, therefore make sure\n * the provided data chunk does not exceed the size of `target`.\n * Returns the number of written codepoints in `target`.\n */\n public decode(input: Uint8Array, target: Uint32Array): number {\n const length = input.length;\n\n if (!length) {\n return 0;\n }\n\n let size = 0;\n let byte1: number;\n let byte2: number;\n let byte3: number;\n let byte4: number;\n let codepoint;\n let startPos = 0;\n\n // handle leftover bytes\n if (this.interim[0]) {\n let discardInterim = false;\n let cp = this.interim[0];\n cp &= ((((cp & 0xE0) === 0xC0)) ? 0x1F : (((cp & 0xF0) === 0xE0)) ? 0x0F : 0x07);\n let pos = 0;\n let tmp: number;\n while ((tmp = this.interim[++pos]) && pos < 4) {\n cp <<= 6;\n cp |= tmp & 0x3F;\n }\n // missing bytes - read ahead from input\n const type = (((this.interim[0] & 0xE0) === 0xC0)) ? 2 : (((this.interim[0] & 0xF0) === 0xE0)) ? 3 : 4;\n const missing = type - pos;\n while (startPos < missing) {\n if (startPos >= length) {\n return 0;\n }\n tmp = input[startPos++];\n if ((tmp & 0xC0) !== 0x80) {\n // wrong continuation, discard interim bytes completely\n startPos--;\n discardInterim = true;\n break;\n } else {\n // need to save so we can continue short inputs in next call\n this.interim[pos++] = tmp;\n cp <<= 6;\n cp |= tmp & 0x3F;\n }\n }\n if (!discardInterim) {\n // final test is type dependent\n if (type === 2) {\n if (cp < 0x80) {\n // wrong starter byte\n startPos--;\n } else {\n target[size++] = cp;\n }\n } else if (type === 3) {\n if (cp < 0x0800 || (cp >= 0xD800 && cp <= 0xDFFF) || cp === 0xFEFF) {\n // illegal codepoint or BOM\n } else {\n target[size++] = cp;\n }\n } else {\n if (cp < 0x010000 || cp > 0x10FFFF) {\n // illegal codepoint\n } else {\n target[size++] = cp;\n }\n }\n }\n this.interim.fill(0);\n }\n\n // loop through input\n const fourStop = length - 4;\n let i = startPos;\n while (i < length) {\n /**\n * ASCII shortcut with loop unrolled to 4 consecutive ASCII chars.\n * This is a compromise between speed gain for ASCII\n * and penalty for non ASCII:\n * For best ASCII performance the char should be stored directly into target,\n * but even a single attempt to write to target and compare afterwards\n * penalizes non ASCII really bad (-50%), thus we load the char into byteX first,\n * which reduces ASCII performance by ~15%.\n * This trial for ASCII reduces non ASCII performance by ~10% which seems acceptible\n * compared to the gains.\n * Note that this optimization only takes place for 4 consecutive ASCII chars,\n * for any shorter it bails out. Worst case - all 4 bytes being read but\n * thrown away due to the last being a non ASCII char (-10% performance).\n */\n while (i < fourStop\n && !((byte1 = input[i]) & 0x80)\n && !((byte2 = input[i + 1]) & 0x80)\n && !((byte3 = input[i + 2]) & 0x80)\n && !((byte4 = input[i + 3]) & 0x80))\n {\n target[size++] = byte1;\n target[size++] = byte2;\n target[size++] = byte3;\n target[size++] = byte4;\n i += 4;\n }\n\n // reread byte1\n byte1 = input[i++];\n\n // 1 byte\n if (byte1 < 0x80) {\n target[size++] = byte1;\n\n // 2 bytes\n } else if ((byte1 & 0xE0) === 0xC0) {\n if (i >= length) {\n this.interim[0] = byte1;\n return size;\n }\n byte2 = input[i++];\n if ((byte2 & 0xC0) !== 0x80) {\n // wrong continuation\n i--;\n continue;\n }\n codepoint = (byte1 & 0x1F) << 6 | (byte2 & 0x3F);\n if (codepoint < 0x80) {\n // wrong starter byte\n i--;\n continue;\n }\n target[size++] = codepoint;\n\n // 3 bytes\n } else if ((byte1 & 0xF0) === 0xE0) {\n if (i >= length) {\n this.interim[0] = byte1;\n return size;\n }\n byte2 = input[i++];\n if ((byte2 & 0xC0) !== 0x80) {\n // wrong continuation\n i--;\n continue;\n }\n if (i >= length) {\n this.interim[0] = byte1;\n this.interim[1] = byte2;\n return size;\n }\n byte3 = input[i++];\n if ((byte3 & 0xC0) !== 0x80) {\n // wrong continuation\n i--;\n continue;\n }\n codepoint = (byte1 & 0x0F) << 12 | (byte2 & 0x3F) << 6 | (byte3 & 0x3F);\n if (codepoint < 0x0800 || (codepoint >= 0xD800 && codepoint <= 0xDFFF) || codepoint === 0xFEFF) {\n // illegal codepoint or BOM, no i-- here\n continue;\n }\n target[size++] = codepoint;\n\n // 4 bytes\n } else if ((byte1 & 0xF8) === 0xF0) {\n if (i >= length) {\n this.interim[0] = byte1;\n return size;\n }\n byte2 = input[i++];\n if ((byte2 & 0xC0) !== 0x80) {\n // wrong continuation\n i--;\n continue;\n }\n if (i >= length) {\n this.interim[0] = byte1;\n this.interim[1] = byte2;\n return size;\n }\n byte3 = input[i++];\n if ((byte3 & 0xC0) !== 0x80) {\n // wrong continuation\n i--;\n continue;\n }\n if (i >= length) {\n this.interim[0] = byte1;\n this.interim[1] = byte2;\n this.interim[2] = byte3;\n return size;\n }\n byte4 = input[i++];\n if ((byte4 & 0xC0) !== 0x80) {\n // wrong continuation\n i--;\n continue;\n }\n codepoint = (byte1 & 0x07) << 18 | (byte2 & 0x3F) << 12 | (byte3 & 0x3F) << 6 | (byte4 & 0x3F);\n if (codepoint < 0x010000 || codepoint > 0x10FFFF) {\n // illegal codepoint, no i-- here\n continue;\n }\n target[size++] = codepoint;\n } else {\n // illegal byte, just skip\n }\n }\n return size;\n }\n}\n", "/**\n * Copyright (c) 2018 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { IColorRGB } from '../Types';\nimport { IAttributeData, IExtendedAttrs } from './Types';\nimport { Attributes, FgFlags, BgFlags, UnderlineStyle, ExtFlags } from './Constants';\n\nexport class AttributeData implements IAttributeData {\n public static toColorRGB(value: number): IColorRGB {\n return [\n value >>> Attributes.RED_SHIFT & 255,\n value >>> Attributes.GREEN_SHIFT & 255,\n value & 255\n ];\n }\n\n public static fromColorRGB(value: IColorRGB): number {\n return (value[0] & 255) << Attributes.RED_SHIFT | (value[1] & 255) << Attributes.GREEN_SHIFT | value[2] & 255;\n }\n\n public clone(): IAttributeData {\n const newObj = new AttributeData();\n newObj.fg = this.fg;\n newObj.bg = this.bg;\n newObj.extended = this.extended.clone();\n return newObj;\n }\n\n // data\n public fg = 0;\n public bg = 0;\n public extended: IExtendedAttrs = new ExtendedAttrs();\n\n // flags\n public isInverse(): number { return this.fg & FgFlags.INVERSE; }\n public isBold(): number { return this.fg & FgFlags.BOLD; }\n public isUnderline(): number {\n if (this.hasExtendedAttrs() && this.extended.underlineStyle !== UnderlineStyle.NONE) {\n return 1;\n }\n return this.fg & FgFlags.UNDERLINE;\n }\n public isBlink(): number { return this.fg & FgFlags.BLINK; }\n public isInvisible(): number { return this.fg & FgFlags.INVISIBLE; }\n public isItalic(): number { return this.bg & BgFlags.ITALIC; }\n public isDim(): number { return this.bg & BgFlags.DIM; }\n public isStrikethrough(): number { return this.fg & FgFlags.STRIKETHROUGH; }\n public isProtected(): number { return this.bg & BgFlags.PROTECTED; }\n public isOverline(): number { return this.bg & BgFlags.OVERLINE; }\n\n // color modes\n public getFgColorMode(): number { return this.fg & Attributes.CM_MASK; }\n public getBgColorMode(): number { return this.bg & Attributes.CM_MASK; }\n public isFgRGB(): boolean { return (this.fg & Attributes.CM_MASK) === Attributes.CM_RGB; }\n public isBgRGB(): boolean { return (this.bg & Attributes.CM_MASK) === Attributes.CM_RGB; }\n public isFgPalette(): boolean { return (this.fg & Attributes.CM_MASK) === Attributes.CM_P16 || (this.fg & Attributes.CM_MASK) === Attributes.CM_P256; }\n public isBgPalette(): boolean { return (this.bg & Attributes.CM_MASK) === Attributes.CM_P16 || (this.bg & Attributes.CM_MASK) === Attributes.CM_P256; }\n public isFgDefault(): boolean { return (this.fg & Attributes.CM_MASK) === 0; }\n public isBgDefault(): boolean { return (this.bg & Attributes.CM_MASK) === 0; }\n public isAttributeDefault(): boolean { return this.fg === 0 && this.bg === 0; }\n\n // colors\n public getFgColor(): number {\n switch (this.fg & Attributes.CM_MASK) {\n case Attributes.CM_P16:\n case Attributes.CM_P256: return this.fg & Attributes.PCOLOR_MASK;\n case Attributes.CM_RGB: return this.fg & Attributes.RGB_MASK;\n default: return -1; // CM_DEFAULT defaults to -1\n }\n }\n public getBgColor(): number {\n switch (this.bg & Attributes.CM_MASK) {\n case Attributes.CM_P16:\n case Attributes.CM_P256: return this.bg & Attributes.PCOLOR_MASK;\n case Attributes.CM_RGB: return this.bg & Attributes.RGB_MASK;\n default: return -1; // CM_DEFAULT defaults to -1\n }\n }\n\n // extended attrs\n public hasExtendedAttrs(): number {\n return this.bg & BgFlags.HAS_EXTENDED;\n }\n public updateExtended(): void {\n if (this.extended.isEmpty()) {\n this.bg &= ~BgFlags.HAS_EXTENDED;\n } else {\n this.bg |= BgFlags.HAS_EXTENDED;\n }\n }\n public getUnderlineColor(): number {\n if ((this.bg & BgFlags.HAS_EXTENDED) && ~this.extended.underlineColor) {\n switch (this.extended.underlineColor & Attributes.CM_MASK) {\n case Attributes.CM_P16:\n case Attributes.CM_P256: return this.extended.underlineColor & Attributes.PCOLOR_MASK;\n case Attributes.CM_RGB: return this.extended.underlineColor & Attributes.RGB_MASK;\n default: return this.getFgColor();\n }\n }\n return this.getFgColor();\n }\n public getUnderlineColorMode(): number {\n return (this.bg & BgFlags.HAS_EXTENDED) && ~this.extended.underlineColor\n ? this.extended.underlineColor & Attributes.CM_MASK\n : this.getFgColorMode();\n }\n public isUnderlineColorRGB(): boolean {\n return (this.bg & BgFlags.HAS_EXTENDED) && ~this.extended.underlineColor\n ? (this.extended.underlineColor & Attributes.CM_MASK) === Attributes.CM_RGB\n : this.isFgRGB();\n }\n public isUnderlineColorPalette(): boolean {\n return (this.bg & BgFlags.HAS_EXTENDED) && ~this.extended.underlineColor\n ? (this.extended.underlineColor & Attributes.CM_MASK) === Attributes.CM_P16\n || (this.extended.underlineColor & Attributes.CM_MASK) === Attributes.CM_P256\n : this.isFgPalette();\n }\n public isUnderlineColorDefault(): boolean {\n return (this.bg & BgFlags.HAS_EXTENDED) && ~this.extended.underlineColor\n ? (this.extended.underlineColor & Attributes.CM_MASK) === 0\n : this.isFgDefault();\n }\n public getUnderlineStyle(): UnderlineStyle {\n return this.fg & FgFlags.UNDERLINE\n ? (this.bg & BgFlags.HAS_EXTENDED ? this.extended.underlineStyle : UnderlineStyle.SINGLE)\n : UnderlineStyle.NONE;\n }\n public getUnderlineVariantOffset(): number {\n return this.extended.underlineVariantOffset;\n }\n}\n\n\n/**\n * Extended attributes for a cell.\n * Holds information about different underline styles and color.\n */\nexport class ExtendedAttrs implements IExtendedAttrs {\n private _ext: number = 0;\n public get ext(): number {\n if (this._urlId) {\n return (\n (this._ext & ~ExtFlags.UNDERLINE_STYLE) |\n (this.underlineStyle << 26)\n );\n }\n return this._ext;\n }\n public set ext(value: number) { this._ext = value; }\n\n public get underlineStyle(): UnderlineStyle {\n // Always return the URL style if it has one\n if (this._urlId) {\n return UnderlineStyle.DASHED;\n }\n return (this._ext & ExtFlags.UNDERLINE_STYLE) >> 26;\n }\n public set underlineStyle(value: UnderlineStyle) {\n this._ext &= ~ExtFlags.UNDERLINE_STYLE;\n this._ext |= (value << 26) & ExtFlags.UNDERLINE_STYLE;\n }\n\n public get underlineColor(): number {\n return this._ext & (Attributes.CM_MASK | Attributes.RGB_MASK);\n }\n public set underlineColor(value: number) {\n this._ext &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);\n this._ext |= value & (Attributes.CM_MASK | Attributes.RGB_MASK);\n }\n\n private _urlId: number = 0;\n public get urlId(): number {\n return this._urlId;\n }\n public set urlId(value: number) {\n this._urlId = value;\n }\n\n public get underlineVariantOffset(): number {\n const val = (this._ext & ExtFlags.VARIANT_OFFSET) >> 29;\n if (val < 0) {\n return val ^ 0xFFFFFFF8;\n }\n return val;\n }\n public set underlineVariantOffset(value: number) {\n this._ext &= ~ExtFlags.VARIANT_OFFSET;\n this._ext |= (value << 29) & ExtFlags.VARIANT_OFFSET;\n }\n\n constructor(\n ext: number = 0,\n urlId: number = 0\n ) {\n this._ext = ext;\n this._urlId = urlId;\n }\n\n public clone(): IExtendedAttrs {\n return new ExtendedAttrs(this._ext, this._urlId);\n }\n\n /**\n * Convenient method to indicate whether the object holds no additional information,\n * that needs to be persistant in the buffer.\n */\n public isEmpty(): boolean {\n return this.underlineStyle === UnderlineStyle.NONE && this._urlId === 0;\n }\n}\n", "/**\n * Copyright (c) 2018 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { CharData, ICellData, IExtendedAttrs } from './Types';\nimport { stringFromCodePoint } from '../input/TextDecoder';\nimport { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_ATTR_INDEX, Content } from './Constants';\nimport { AttributeData, ExtendedAttrs } from './AttributeData';\nimport type { IBufferCell as IBufferCellApi } from '@xterm/xterm';\n\n/**\n * CellData - represents a single Cell in the terminal buffer.\n */\nexport class CellData extends AttributeData implements ICellData {\n /** Helper to create CellData from CharData. */\n public static fromCharData(value: CharData): CellData {\n const obj = new CellData();\n obj.setFromCharData(value);\n return obj;\n }\n /** Primitives from terminal buffer. */\n public content = 0;\n public fg = 0;\n public bg = 0;\n public extended: IExtendedAttrs = new ExtendedAttrs();\n public combinedData = '';\n /** Whether cell contains a combined string. */\n public isCombined(): number {\n return this.content & Content.IS_COMBINED_MASK;\n }\n /** Width of the cell. */\n public getWidth(): number {\n return this.content >> Content.WIDTH_SHIFT;\n }\n /** JS string of the content. */\n public getChars(): string {\n if (this.content & Content.IS_COMBINED_MASK) {\n return this.combinedData;\n }\n if (this.content & Content.CODEPOINT_MASK) {\n return stringFromCodePoint(this.content & Content.CODEPOINT_MASK);\n }\n return '';\n }\n /**\n * Codepoint of cell\n * Note this returns the UTF32 codepoint of single chars,\n * if content is a combined string it returns the codepoint\n * of the last char in string to be in line with code in CharData.\n */\n public getCode(): number {\n return (this.isCombined())\n ? this.combinedData.charCodeAt(this.combinedData.length - 1)\n : this.content & Content.CODEPOINT_MASK;\n }\n /** Set data from CharData */\n public setFromCharData(value: CharData): void {\n this.fg = value[CHAR_DATA_ATTR_INDEX];\n this.bg = 0;\n let combined = false;\n // surrogates and combined strings need special treatment\n if (value[CHAR_DATA_CHAR_INDEX].length > 2) {\n combined = true;\n }\n else if (value[CHAR_DATA_CHAR_INDEX].length === 2) {\n const code = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0);\n // if the 2-char string is a surrogate create single codepoint\n // everything else is combined\n if (0xD800 <= code && code <= 0xDBFF) {\n const second = value[CHAR_DATA_CHAR_INDEX].charCodeAt(1);\n if (0xDC00 <= second && second <= 0xDFFF) {\n this.content = ((code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);\n }\n else {\n combined = true;\n }\n }\n else {\n combined = true;\n }\n }\n else {\n this.content = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);\n }\n if (combined) {\n this.combinedData = value[CHAR_DATA_CHAR_INDEX];\n this.content = Content.IS_COMBINED_MASK | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);\n }\n }\n /** Get data as CharData. */\n public getAsCharData(): CharData {\n return [this.fg, this.getChars(), this.getWidth(), this.getCode()];\n }\n\n public attributesEquals(other: IBufferCellApi): boolean {\n if (this.getFgColorMode() !== other.getFgColorMode() || this.getFgColor() !== other.getFgColor()) {\n return false;\n }\n if (this.getBgColorMode() !== other.getBgColorMode() || this.getBgColor() !== other.getBgColor()) {\n return false;\n }\n if (this.isInverse() !== other.isInverse()) {\n return false;\n }\n if (this.isBold() !== other.isBold()) {\n return false;\n }\n if (this.isUnderline() !== other.isUnderline()) {\n return false;\n }\n if (this.isUnderline()) {\n if (this.getUnderlineStyle() !== other.getUnderlineStyle()) {\n return false;\n }\n const thisDefault = this.isUnderlineColorDefault();\n const otherDefault = other.isUnderlineColorDefault();\n if (!(thisDefault && otherDefault)) {\n if (thisDefault !== otherDefault) {\n return false;\n }\n if (this.getUnderlineColor() !== other.getUnderlineColor()) {\n return false;\n }\n if (this.getUnderlineColorMode() !== other.getUnderlineColorMode()) {\n return false;\n }\n }\n }\n if (this.isOverline() !== other.isOverline()) {\n return false;\n }\n if (this.isBlink() !== other.isBlink()) {\n return false;\n }\n if (this.isInvisible() !== other.isInvisible()) {\n return false;\n }\n if (this.isItalic() !== other.isItalic()) {\n return false;\n }\n if (this.isDim() !== other.isDim()) {\n return false;\n }\n if (this.isStrikethrough() !== other.isStrikethrough()) {\n return false;\n }\n return true;\n }\n\n}\n", "/**\n * Copyright (c) 2021 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { CellData } from '../buffer/CellData';\nimport { IBufferLine, ICellData } from '../buffer/Types';\nimport { IBufferCell as IBufferCellApi, IBufferLine as IBufferLineApi } from '@xterm/xterm';\n\nexport class BufferLineApiView implements IBufferLineApi {\n constructor(private _line: IBufferLine) { }\n\n public get isWrapped(): boolean { return this._line.isWrapped; }\n public get length(): number { return this._line.length; }\n public getCell(x: number, cell?: IBufferCellApi): IBufferCellApi | undefined {\n if (x < 0 || x >= this._line.length) {\n return undefined;\n }\n\n if (cell) {\n this._line.loadCell(x, cell as unknown as ICellData);\n return cell;\n }\n return this._line.loadCell(x, new CellData()) as unknown as IBufferCellApi;\n }\n public translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): string {\n return this._line.translateToString(trimRight, startColumn, endColumn);\n }\n}\n", "/**\n * Copyright (c) 2021 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { IBuffer as IBufferApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi } from '@xterm/xterm';\nimport { IBuffer } from '../buffer/Types';\nimport { BufferLineApiView } from './BufferLineApiView';\nimport { CellData } from '../buffer/CellData';\n\nexport class BufferApiView implements IBufferApi {\n constructor(\n private _buffer: IBuffer,\n public readonly type: 'normal' | 'alternate'\n ) { }\n\n public init(buffer: IBuffer): BufferApiView {\n this._buffer = buffer;\n return this;\n }\n\n public get cursorY(): number { return this._buffer.y; }\n public get cursorX(): number { return this._buffer.x; }\n public get viewportY(): number { return this._buffer.ydisp; }\n public get baseY(): number { return this._buffer.ybase; }\n public get length(): number { return this._buffer.lines.length; }\n public getLine(y: number): IBufferLineApi | undefined {\n const line = this._buffer.lines.get(y);\n if (!line) {\n return undefined;\n }\n return new BufferLineApiView(line);\n }\n public getNullCell(): IBufferCellApi { return new CellData(); }\n}\n", "/**\n * Copyright (c) 2024-2026 The xterm.js authors. All rights reserved.\n * @license MIT\n *\n * Minimal lifecycle utilities for xterm.js core.\n * Simplified from VS Code's lifecycle.ts - no tracking/leak detection.\n */\n\nexport interface IDisposable {\n dispose(): void;\n}\n\nexport function toDisposable(fn: () => void): IDisposable {\n return { dispose: fn };\n}\n\nexport function dispose(disposable: T): T;\nexport function dispose(disposable: T | undefined): T | undefined;\nexport function dispose(disposables: T[]): T[];\nexport function dispose(arg: T | T[] | undefined): T | T[] | undefined {\n if (!arg) {\n return arg;\n }\n if (Array.isArray(arg)) {\n for (const d of arg) {\n d.dispose();\n }\n return [];\n }\n arg.dispose();\n return arg;\n}\n\nexport function combinedDisposable(...disposables: IDisposable[]): IDisposable {\n return toDisposable(() => dispose(disposables));\n}\n\nexport class DisposableStore implements IDisposable {\n private readonly _disposables = new Set();\n private _isDisposed = false;\n\n public get isDisposed(): boolean {\n return this._isDisposed;\n }\n\n public add(o: T): T {\n if (this._isDisposed) {\n o.dispose();\n } else {\n this._disposables.add(o);\n }\n return o;\n }\n\n public dispose(): void {\n if (this._isDisposed) {\n return;\n }\n this._isDisposed = true;\n for (const d of this._disposables) {\n d.dispose();\n }\n this._disposables.clear();\n }\n\n public clear(): void {\n for (const d of this._disposables) {\n d.dispose();\n }\n this._disposables.clear();\n }\n}\n\nexport abstract class Disposable implements IDisposable {\n public static readonly None: IDisposable = Object.freeze({ dispose() { } });\n\n protected readonly _store = new DisposableStore();\n\n public dispose(): void {\n this._store.dispose();\n }\n\n protected _register(o: T): T {\n return this._store.add(o);\n }\n}\n\nexport class MutableDisposable implements IDisposable {\n private _value: T | undefined;\n private _isDisposed = false;\n\n public get value(): T | undefined {\n return this._isDisposed ? undefined : this._value;\n }\n\n public set value(value: T | undefined) {\n if (this._isDisposed || value === this._value) {\n return;\n }\n this._value?.dispose();\n this._value = value;\n }\n\n public clear(): void {\n this.value = undefined;\n }\n\n public dispose(): void {\n this._isDisposed = true;\n this._value?.dispose();\n this._value = undefined;\n }\n}\n", "/**\n * Copyright (c) 2024-2026 The xterm.js authors. All rights reserved.\n * @license MIT\n *\n * Minimal event utilities for xterm.js core.\n * Simplified from VS Code's event.ts - no leak detection/profiling.\n */\n\nimport { IDisposable, DisposableStore, toDisposable } from './Lifecycle';\n\nexport interface IEvent {\n (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore): IDisposable;\n}\n\nexport class Emitter {\n private _listeners: { fn: (e: T) => any, thisArgs: any }[] = [];\n private _disposed = false;\n private _event: IEvent | undefined;\n\n public get event(): IEvent {\n if (this._event) {\n return this._event;\n }\n this._event = (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => {\n if (this._disposed) {\n return toDisposable(() => {});\n }\n\n const entry = { fn: listener, thisArgs };\n this._listeners = this._listeners.slice();\n this._listeners.push(entry);\n\n const result = toDisposable(() => {\n const idx = this._listeners.indexOf(entry);\n if (idx !== -1) {\n this._listeners = this._listeners.slice();\n this._listeners.splice(idx, 1);\n }\n });\n\n if (disposables) {\n if (Array.isArray(disposables)) {\n disposables.push(result);\n } else {\n disposables.add(result);\n }\n }\n\n return result;\n };\n return this._event;\n }\n\n public fire(event: T): void {\n if (this._disposed || !this._listeners.length) {\n return;\n }\n if (this._listeners.length === 1) {\n this._listeners[0].fn.call(this._listeners[0].thisArgs, event);\n return;\n }\n const listeners = this._listeners;\n for (let i = 0, len = listeners.length; i < len; ++i) {\n listeners[i].fn.call(listeners[i].thisArgs, event);\n }\n }\n\n public dispose(): void {\n if (this._disposed) {\n return;\n }\n this._disposed = true;\n this._listeners.length = 0;\n }\n}\n\nexport namespace EventUtils {\n export function forward(from: IEvent, to: Emitter): IDisposable {\n return from(e => to.fire(e));\n }\n\n export function map(event: IEvent, map: (i: I) => O): IEvent {\n return (listener: (e: O) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => {\n return event(i => listener.call(thisArgs, map(i)), undefined, disposables);\n };\n }\n\n export function any(...events: IEvent[]): IEvent;\n export function any(...events: IEvent[]): IEvent;\n export function any(...events: IEvent[]): IEvent {\n return (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => {\n const store = new DisposableStore();\n for (const event of events) {\n store.add(event(e => listener.call(thisArgs, e)));\n }\n if (disposables) {\n if (Array.isArray(disposables)) {\n disposables.push(store);\n } else {\n disposables.add(store);\n }\n }\n return store;\n };\n }\n\n export function runAndSubscribe(event: IEvent, handler: (e: T) => void, initial: T): IDisposable;\n export function runAndSubscribe(event: IEvent, handler: (e: T | undefined) => void): IDisposable;\n export function runAndSubscribe(event: IEvent, handler: (e: T | undefined) => void, initial?: T): IDisposable {\n handler(initial);\n return event(e => handler(e));\n }\n}\n", "/**\n * Copyright (c) 2021 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { IBuffer as IBufferApi, IBufferNamespace as IBufferNamespaceApi } from '@xterm/xterm';\nimport { BufferApiView } from './BufferApiView';\nimport { ICoreTerminal } from '../CoreTerminal';\nimport { Disposable } from '../Lifecycle';\nimport { Emitter } from '../Event';\n\nexport class BufferNamespaceApi extends Disposable implements IBufferNamespaceApi {\n private _normal: BufferApiView;\n private _alternate: BufferApiView;\n\n private readonly _onBufferChange = this._register(new Emitter());\n public readonly onBufferChange = this._onBufferChange.event;\n\n constructor(private _core: ICoreTerminal) {\n super();\n this._normal = new BufferApiView(this._core.buffers.normal, 'normal');\n this._alternate = new BufferApiView(this._core.buffers.alt, 'alternate');\n this._register(this._core.buffers.onBufferActivate(() => this._onBufferChange.fire(this.active)));\n }\n public get active(): IBufferApi {\n if (this._core.buffers.active === this._core.buffers.normal) { return this.normal; }\n if (this._core.buffers.active === this._core.buffers.alt) { return this.alternate; }\n throw new Error('Active buffer is neither normal nor alternate');\n }\n public get normal(): IBufferApi {\n return this._normal.init(this._core.buffers.normal);\n }\n public get alternate(): IBufferApi {\n return this._alternate.init(this._core.buffers.alt);\n }\n}\n", "/**\n * Copyright (c) 2021 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { IParams } from '../parser/Types';\nimport { IDisposable, IFunctionIdentifier, IParser } from '@xterm/xterm';\nimport { ICoreTerminal } from '../CoreTerminal';\n\nexport class ParserApi implements IParser {\n constructor(private _core: ICoreTerminal) { }\n\n public registerCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean | Promise): IDisposable {\n return this._core.registerCsiHandler(id, (params: IParams) => callback(params.toArray()));\n }\n public addCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean | Promise): IDisposable {\n return this.registerCsiHandler(id, callback);\n }\n public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean | Promise): IDisposable {\n return this._core.registerDcsHandler(id, (data: string, params: IParams) => callback(data, params.toArray()));\n }\n public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean | Promise): IDisposable {\n return this.registerDcsHandler(id, callback);\n }\n public registerEscHandler(id: IFunctionIdentifier, handler: () => boolean | Promise): IDisposable {\n return this._core.registerEscHandler(id, handler);\n }\n public addEscHandler(id: IFunctionIdentifier, handler: () => boolean | Promise): IDisposable {\n return this.registerEscHandler(id, handler);\n }\n public registerOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable {\n return this._core.registerOscHandler(ident, callback);\n }\n public addOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable {\n return this.registerOscHandler(ident, callback);\n }\n public registerApcHandler(id: IFunctionIdentifier, callback: (data: string) => boolean | Promise): IDisposable {\n return this._core.registerApcHandler(id, callback);\n }\n}\n", "/**\n * Copyright (c) 2021 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { ICoreTerminal } from '../CoreTerminal';\nimport { IUnicodeHandling, IUnicodeVersionProvider } from '@xterm/xterm';\n\nexport class UnicodeApi implements IUnicodeHandling {\n constructor(private _core: ICoreTerminal) { }\n\n public register(provider: IUnicodeVersionProvider): void {\n this._core.unicodeService.register(provider);\n }\n\n public get versions(): string[] {\n return this._core.unicodeService.versions;\n }\n\n public get activeVersion(): string {\n return this._core.unicodeService.activeVersion;\n }\n\n public set activeVersion(version: string) {\n this._core.unicodeService.activeVersion = version;\n }\n}\n", "/**\n * Copyright (c) 2018 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { CharData, IAttributeData, IBufferLine, ICellData, IExtendedAttrs } from './Types';\nimport { AttributeData } from './AttributeData';\nimport { CellData } from './CellData';\nimport { Attributes, BgFlags, CHAR_DATA_ATTR_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, Content, NULL_CELL_CHAR, NULL_CELL_CODE, NULL_CELL_WIDTH, WHITESPACE_CELL_CHAR } from './Constants';\nimport { stringFromCodePoint } from '../input/TextDecoder';\n\n// Buffer memory layout:\n//\n// [0]: content `uint32_t` - wcwidth(2) comb(1) codepoint(21)\n// [1]: fg `uint32_t` - flags(8) r(8) g(8) b(8)\n// [2]: bg `uint32_t` - flags(8) r(8) g(8) b(8)\n\nconst enum Constants {\n /** The number of 32 bit array indices taken by one cell. */\n CELL_INDICIES = 3,\n /** Factor when to cleanup underlying array buffer after shrinking. */\n CLEANUP_THRESHOLD = 2\n}\n\n/**\n * Cell member indices.\n *\n * Direct access:\n * `content = data[column * Constants.CELL_INDICIES + Cell.CONTENT];`\n * `fg = data[column * Constants.CELL_INDICIES + Cell.FG];`\n * `bg = data[column * Constants.CELL_INDICIES + Cell.BG];`\n */\nconst enum Cell {\n CONTENT = 0,\n FG = 1, // currently simply holds all known attrs\n BG = 2 // currently unused\n}\n\n\ninterface IExtendedAttrsExt extends IExtendedAttrs {\n _ext: number;\n _urlId: number;\n}\n\n\nexport const DEFAULT_ATTR_DATA = Object.freeze(new AttributeData());\n\n// Work variables to avoid garbage collection\nlet $startIndex = 0;\nconst $workCell = new CellData();\nconst $extended = DEFAULT_ATTR_DATA.extended.clone() as IExtendedAttrsExt;\n\n\n/**\n * Typed array based bufferline implementation.\n *\n * There are 2 ways to insert data into the cell buffer:\n * - `setCellFromCodepoint` + `addCodepointToCell`\n * Use these for data that is already UTF32.\n * Used during normal input in `InputHandler` for faster buffer access.\n * - `setCell`\n * This method takes a CellData object and stores the data in the buffer.\n * Use `CellData.fromCharData` to create the CellData object (e.g. from JS string).\n *\n * To retrieve data from the buffer use either one of the primitive methods\n * (if only one particular value is needed) or `loadCell`. For `loadCell` in a loop\n * memory allocs / GC pressure can be greatly reduced by reusing the CellData object.\n */\nexport class BufferLine implements IBufferLine {\n protected _data: Uint32Array;\n /** Sparse cache; only read when `IS_COMBINED_MASK` is set in `_data`. */\n protected _combined: {[index: number]: string} = {};\n /** Sparse cache; only read when `HAS_EXTENDED` is set in `_data`. */\n protected _extendedAttrs: {[index: number]: IExtendedAttrs | undefined} = {};\n public length: number;\n\n /** line text cache */\n protected _cacheValid = false;\n protected _cache: string = '';\n protected _cacheTrimmed = false;\n\n constructor(\n cols: number,\n fillCellData?: ICellData,\n public isWrapped: boolean = false\n ) {\n this._data = new Uint32Array(cols * Constants.CELL_INDICIES);\n const cell = fillCellData ?? CellData.fromCharData([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);\n for (let i = 0; i < cols; ++i) {\n this.setCell(i, cell);\n }\n this.length = cols;\n }\n\n /**\n * Get cell data CharData.\n * @deprecated\n */\n public get(index: number): CharData {\n const content = this._data[index * Constants.CELL_INDICIES + Cell.CONTENT];\n const cp = content & Content.CODEPOINT_MASK;\n return [\n this._data[index * Constants.CELL_INDICIES + Cell.FG],\n (content & Content.IS_COMBINED_MASK)\n ? this._combined[index]\n : (cp) ? stringFromCodePoint(cp) : '',\n content >> Content.WIDTH_SHIFT,\n (content & Content.IS_COMBINED_MASK)\n ? this._combined[index].charCodeAt(this._combined[index].length - 1)\n : cp\n ];\n }\n\n /**\n * Set cell data from CharData.\n * @deprecated\n */\n public set(index: number, value: CharData): void {\n this._cacheValid = false;\n this._data[index * Constants.CELL_INDICIES + Cell.FG] = value[CHAR_DATA_ATTR_INDEX];\n if (value[CHAR_DATA_CHAR_INDEX].length > 1) {\n this._combined[index] = value[1];\n this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = index | Content.IS_COMBINED_MASK | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);\n } else {\n this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);\n }\n }\n\n /**\n * primitive getters\n * use these when only one value is needed, otherwise use `loadCell`\n */\n public getWidth(index: number): number {\n return this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] >> Content.WIDTH_SHIFT;\n }\n\n /** Test whether content has width. */\n public hasWidth(index: number): number {\n return this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] & Content.WIDTH_MASK;\n }\n\n /** Get FG cell component. */\n public getFg(index: number): number {\n return this._data[index * Constants.CELL_INDICIES + Cell.FG];\n }\n\n /** Get BG cell component. */\n public getBg(index: number): number {\n return this._data[index * Constants.CELL_INDICIES + Cell.BG];\n }\n\n /**\n * Test whether contains any chars.\n * Basically an empty has no content, but other cells might differ in FG/BG\n * from real empty cells.\n */\n public hasContent(index: number): number {\n return this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] & Content.HAS_CONTENT_MASK;\n }\n\n /**\n * Get codepoint of the cell.\n * To be in line with `code` in CharData this either returns\n * a single UTF32 codepoint or the last codepoint of a combined string.\n */\n public getCodePoint(index: number): number {\n const content = this._data[index * Constants.CELL_INDICIES + Cell.CONTENT];\n if (content & Content.IS_COMBINED_MASK) {\n return this._combined[index].charCodeAt(this._combined[index].length - 1);\n }\n return content & Content.CODEPOINT_MASK;\n }\n\n /** Test whether the cell contains a combined string. */\n public isCombined(index: number): number {\n return this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] & Content.IS_COMBINED_MASK;\n }\n\n /** Returns the string content of the cell. */\n public getString(index: number): string {\n const content = this._data[index * Constants.CELL_INDICIES + Cell.CONTENT];\n if (content & Content.IS_COMBINED_MASK) {\n return this._combined[index];\n }\n if (content & Content.CODEPOINT_MASK) {\n return stringFromCodePoint(content & Content.CODEPOINT_MASK);\n }\n // return empty string for empty cells\n return '';\n }\n\n /** Get state of protected flag. */\n public isProtected(index: number): number {\n return this._data[index * Constants.CELL_INDICIES + Cell.BG] & BgFlags.PROTECTED;\n }\n\n /**\n * Load data at `index` into `cell`. This is used to access cells in a way that's more friendly\n * to GC as it significantly reduced the amount of new objects/references needed.\n */\n public loadCell(index: number, cell: ICellData): ICellData {\n $startIndex = index * Constants.CELL_INDICIES;\n cell.content = this._data[$startIndex + Cell.CONTENT];\n cell.fg = this._data[$startIndex + Cell.FG];\n cell.bg = this._data[$startIndex + Cell.BG];\n if (cell.content & Content.IS_COMBINED_MASK) {\n cell.combinedData = this._combined[index];\n } else {\n cell.combinedData = '';\n }\n if (cell.bg & BgFlags.HAS_EXTENDED) {\n cell.extended = this._extendedAttrs[index]!;\n } else {\n // Do not mutate cell.extended in place: it may still reference this line's map entry from a\n // prior loadCell into a reused CellData (e.g. $workCell during insert/delete).\n // We use $extended as blueprint and reset the internals\n // mimicking the ctor to avoid a new allocation.\n $extended._ext = 0;\n $extended._urlId = 0;\n cell.extended = $extended;\n }\n return cell;\n }\n\n /**\n * Set data at `index` to `cell`.\n */\n public setCell(index: number, cell: ICellData): void {\n this._cacheValid = false;\n if (cell.content & Content.IS_COMBINED_MASK) {\n this._combined[index] = cell.combinedData;\n }\n if (cell.bg & BgFlags.HAS_EXTENDED) {\n this._extendedAttrs[index] = cell.extended;\n }\n this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = cell.content;\n this._data[index * Constants.CELL_INDICIES + Cell.FG] = cell.fg;\n this._data[index * Constants.CELL_INDICIES + Cell.BG] = cell.bg;\n }\n\n /**\n * Set cell data from input handler.\n * Since the input handler see the incoming chars as UTF32 codepoints,\n * it gets an optimized access method.\n */\n public setCellFromCodepoint(index: number, codePoint: number, width: number, attrs: IAttributeData): void {\n this._cacheValid = false;\n if (attrs.bg & BgFlags.HAS_EXTENDED) {\n this._extendedAttrs[index] = attrs.extended;\n }\n const $idx = index * Constants.CELL_INDICIES;\n this._data[$idx + Cell.CONTENT] = codePoint | (width << Content.WIDTH_SHIFT);\n this._data[$idx + Cell.FG] = attrs.fg;\n this._data[$idx + Cell.BG] = attrs.bg;\n }\n\n /**\n * Add a codepoint to a cell from input handler.\n * During input stage combining chars with a width of 0 follow and stack\n * onto a leading char. Since we already set the attrs\n * by the previous `setDataFromCodePoint` call, we can omit it here.\n */\n public addCodepointToCell(index: number, codePoint: number, width: number): void {\n this._cacheValid = false;\n let content = this._data[index * Constants.CELL_INDICIES + Cell.CONTENT];\n if (content & Content.IS_COMBINED_MASK) {\n // we already have a combined string, simply add\n this._combined[index] += stringFromCodePoint(codePoint);\n } else {\n if (content & Content.CODEPOINT_MASK) {\n // normal case for combining chars:\n // - move current leading char + new one into combined string\n // - set combined flag\n this._combined[index] = stringFromCodePoint(content & Content.CODEPOINT_MASK) + stringFromCodePoint(codePoint);\n content &= ~Content.CODEPOINT_MASK; // set codepoint in buffer to 0\n content |= Content.IS_COMBINED_MASK;\n } else {\n // should not happen - we actually have no data in the cell yet\n // simply set the data in the cell buffer with a width of 1\n content = codePoint | (1 << Content.WIDTH_SHIFT);\n }\n }\n if (width) {\n content &= ~Content.WIDTH_MASK;\n content |= width << Content.WIDTH_SHIFT;\n }\n this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = content;\n }\n\n public insertCells(pos: number, n: number, fillCellData: ICellData): void {\n this._cacheValid = false;\n pos %= this.length;\n\n // handle fullwidth at pos: reset cell one to the left if pos is second cell of a wide char\n if (pos && this.getWidth(pos - 1) === 2) {\n this.setCellFromCodepoint(pos - 1, 0, 1, fillCellData);\n }\n\n if (n < this.length - pos) {\n for (let i = this.length - pos - n - 1; i >= 0; --i) {\n this.setCell(pos + n + i, this.loadCell(pos + i, $workCell));\n }\n for (let i = 0; i < n; ++i) {\n this.setCell(pos + i, fillCellData);\n }\n } else {\n for (let i = pos; i < this.length; ++i) {\n this.setCell(i, fillCellData);\n }\n }\n\n // handle fullwidth at line end: reset last cell if it is first cell of a wide char\n if (this.getWidth(this.length - 1) === 2) {\n this.setCellFromCodepoint(this.length - 1, 0, 1, fillCellData);\n }\n }\n\n public deleteCells(pos: number, n: number, fillCellData: ICellData): void {\n this._cacheValid = false;\n pos %= this.length;\n if (n < this.length - pos) {\n for (let i = 0; i < this.length - pos - n; ++i) {\n this.setCell(pos + i, this.loadCell(pos + n + i, $workCell));\n }\n for (let i = this.length - n; i < this.length; ++i) {\n this.setCell(i, fillCellData);\n }\n } else {\n for (let i = pos; i < this.length; ++i) {\n this.setCell(i, fillCellData);\n }\n }\n\n // handle fullwidth at pos:\n // - reset pos-1 if wide char\n // - reset pos if width==0 (previous second cell of a wide char)\n if (pos && this.getWidth(pos - 1) === 2) {\n this.setCellFromCodepoint(pos - 1, 0, 1, fillCellData);\n }\n if (this.getWidth(pos) === 0 && !this.hasContent(pos)) {\n this.setCellFromCodepoint(pos, 0, 1, fillCellData);\n }\n }\n\n public replaceCells(start: number, end: number, fillCellData: ICellData, respectProtect: boolean = false): void {\n this._cacheValid = false;\n // full branching on respectProtect==true, hopefully getting fast JIT for standard case\n if (respectProtect) {\n if (start && this.getWidth(start - 1) === 2 && !this.isProtected(start - 1)) {\n this.setCellFromCodepoint(start - 1, 0, 1, fillCellData);\n }\n if (end < this.length && this.getWidth(end - 1) === 2 && !this.isProtected(end)) {\n this.setCellFromCodepoint(end, 0, 1, fillCellData);\n }\n while (start < end && start < this.length) {\n if (!this.isProtected(start)) {\n this.setCell(start, fillCellData);\n }\n start++;\n }\n return;\n }\n\n // handle fullwidth at start: reset cell one to the left if start is second cell of a wide char\n if (start && this.getWidth(start - 1) === 2) {\n this.setCellFromCodepoint(start - 1, 0, 1, fillCellData);\n }\n // handle fullwidth at last cell + 1: reset to empty cell if it is second part of a wide char\n if (end < this.length && this.getWidth(end - 1) === 2) {\n this.setCellFromCodepoint(end, 0, 1, fillCellData);\n }\n\n while (start < end && start < this.length) {\n this.setCell(start++, fillCellData);\n }\n }\n\n /**\n * Resize BufferLine to `cols` filling excess cells with `fillCellData`.\n * The underlying array buffer will not change if there is still enough space\n * to hold the new buffer line data.\n * Returns a boolean indicating, whether a `cleanupMemory` call would free\n * excess memory (true after shrinking > Constants.CLEANUP_THRESHOLD).\n */\n public resize(cols: number, fillCellData: ICellData): boolean {\n this._cacheValid = false;\n if (cols === this.length) {\n return this._data.length * 4 * Constants.CLEANUP_THRESHOLD < this._data.buffer.byteLength;\n }\n const uint32Cells = cols * Constants.CELL_INDICIES;\n if (cols > this.length) {\n if (this._data.buffer.byteLength >= uint32Cells * 4) {\n // optimization: avoid alloc and data copy if buffer has enough room\n this._data = new Uint32Array(this._data.buffer, 0, uint32Cells);\n } else {\n // slow path: new alloc and full data copy\n const data = new Uint32Array(uint32Cells);\n data.set(this._data);\n this._data = data;\n }\n for (let i = this.length; i < cols; ++i) {\n this.setCell(i, fillCellData);\n }\n } else {\n // optimization: just shrink the view on existing buffer\n this._data = this._data.subarray(0, uint32Cells);\n // Remove any cut off combined data\n const keys = Object.keys(this._combined);\n for (let i = 0; i < keys.length; i++) {\n const key = parseInt(keys[i], 10);\n if (key >= cols) {\n delete this._combined[key];\n }\n }\n // remove any cut off extended attributes\n const extKeys = Object.keys(this._extendedAttrs);\n for (let i = 0; i < extKeys.length; i++) {\n const key = parseInt(extKeys[i], 10);\n if (key >= cols) {\n delete this._extendedAttrs[key];\n }\n }\n }\n this.length = cols;\n return uint32Cells * 4 * Constants.CLEANUP_THRESHOLD < this._data.buffer.byteLength;\n }\n\n /**\n * Cleanup underlying array buffer.\n * A cleanup will be triggered if the array buffer exceeds the actual used\n * memory by a factor of Constants.CLEANUP_THRESHOLD.\n * Returns 0 or 1 indicating whether a cleanup happened.\n */\n public cleanupMemory(): number {\n if (this._data.length * 4 * Constants.CLEANUP_THRESHOLD < this._data.buffer.byteLength) {\n const data = new Uint32Array(this._data.length);\n data.set(this._data);\n this._data = data;\n return 1;\n }\n return 0;\n }\n\n /** fill a line with fillCharData */\n public fill(fillCellData: ICellData, respectProtect: boolean = false): void {\n this._cacheValid = false;\n // full branching on respectProtect==true, hopefully getting fast JIT for standard case\n if (respectProtect) {\n for (let i = 0; i < this.length; ++i) {\n if (!this.isProtected(i)) {\n this.setCell(i, fillCellData);\n }\n }\n return;\n }\n this._combined = {};\n this._extendedAttrs = {};\n for (let i = 0; i < this.length; ++i) {\n this.setCell(i, fillCellData);\n }\n }\n\n /** alter to a full copy of line */\n public copyFrom(line: BufferLine, blank?: boolean): void {\n if (this.length !== line.length) {\n this._data = new Uint32Array(line._data);\n } else {\n // use high speed copy if lengths are equal\n this._data.set(line._data);\n }\n this.length = line.length;\n if (blank) {\n // a blank line may never hold combined or extended attrs,\n // thus we can skip handling them\n this._combined = {};\n this._extendedAttrs = {};\n } else {\n this._copySparseMapsFrom(line);\n }\n this._cache = '';\n this._cacheValid = false;\n this.isWrapped = line.isWrapped;\n }\n\n /** create a new clone */\n public clone(blank?: boolean): IBufferLine {\n const newLine = new BufferLine(0, undefined, false);\n newLine._data = new Uint32Array(this._data);\n newLine.length = this.length;\n if (!blank) {\n // a blank line may never hold combined or extended attrs,\n // thus we can skip handling them\n newLine._copySparseMapsFrom(this);\n }\n newLine.isWrapped = this.isWrapped;\n return newLine;\n }\n\n public getTrimmedLength(): number {\n for (let i = this.length - 1; i >= 0; --i) {\n if ((this._data[i * Constants.CELL_INDICIES + Cell.CONTENT] & Content.HAS_CONTENT_MASK)) {\n return i + (this._data[i * Constants.CELL_INDICIES + Cell.CONTENT] >> Content.WIDTH_SHIFT);\n }\n }\n return 0;\n }\n\n public getNoBgTrimmedLength(): number {\n for (let i = this.length - 1; i >= 0; --i) {\n if ((this._data[i * Constants.CELL_INDICIES + Cell.CONTENT] & Content.HAS_CONTENT_MASK) || (this._data[i * Constants.CELL_INDICIES + Cell.BG] & Attributes.CM_MASK)) {\n return i + (this._data[i * Constants.CELL_INDICIES + Cell.CONTENT] >> Content.WIDTH_SHIFT);\n }\n }\n return 0;\n }\n\n public copyCellsFrom(src: BufferLine, srcCol: number, destCol: number, length: number, applyInReverse: boolean): void {\n this._cacheValid = false;\n const srcData = src._data;\n if (applyInReverse) {\n for (let cell = length - 1; cell >= 0; cell--) {\n for (let i = 0; i < Constants.CELL_INDICIES; i++) {\n this._data[(destCol + cell) * Constants.CELL_INDICIES + i] = srcData[(srcCol + cell) * Constants.CELL_INDICIES + i];\n }\n this._copyCellMapsFrom(src, srcCol + cell, destCol + cell);\n }\n } else {\n for (let cell = 0; cell < length; cell++) {\n for (let i = 0; i < Constants.CELL_INDICIES; i++) {\n this._data[(destCol + cell) * Constants.CELL_INDICIES + i] = srcData[(srcCol + cell) * Constants.CELL_INDICIES + i];\n }\n this._copyCellMapsFrom(src, srcCol + cell, destCol + cell);\n }\n }\n }\n\n /**\n * Translates the buffer line to a string. Caching only applies to canonical full-line translation\n * requests (regardless of `trimRight` value).\n *\n * @param trimRight Whether to trim any empty cells on the right.\n * @param startCol The column to start the string (0-based inclusive).\n * @param endCol The column to end the string (0-based exclusive).\n * @param outColumns if specified, this array will be filled with column numbers such that\n * `returnedString[i]` is displayed at `outColumns[i]` column. `outColumns[returnedString.length]`\n * is where the character following `returnedString` will be displayed.\n *\n * When a single cell is translated to multiple UTF-16 code units (e.g. surrogate pair) in the\n * returned string, the corresponding entries in `outColumns` will have the same column number.\n */\n public translateToString(trimRight?: boolean, startCol?: number, endCol?: number, outColumns?: number[]): string {\n const isCanonical = (startCol === undefined || startCol === 0) && endCol === undefined && outColumns === undefined;\n if (isCanonical && this._cacheValid) {\n if (trimRight) {\n return this._cacheTrimmed ? this._cache : this._cache.trimEnd();\n }\n if (!this._cacheTrimmed) {\n return this._cache;\n }\n }\n startCol = startCol ?? 0;\n endCol = endCol ?? this.length;\n if (trimRight) {\n endCol = Math.min(endCol, this.getTrimmedLength());\n }\n if (outColumns) {\n outColumns.length = 0;\n }\n const cellContents: string[] = [];\n while (startCol < endCol) {\n const content = this._data[startCol * Constants.CELL_INDICIES + Cell.CONTENT];\n const cp = content & Content.CODEPOINT_MASK;\n const chars = (content & Content.IS_COMBINED_MASK) ? this._combined[startCol] : (cp) ? stringFromCodePoint(cp) : WHITESPACE_CELL_CHAR;\n cellContents.push(chars);\n if (outColumns) {\n for (let i = 0; i < chars.length; ++i) {\n outColumns.push(startCol);\n }\n }\n startCol += (content >> Content.WIDTH_SHIFT) || 1; // always advance by at least 1\n }\n if (outColumns) {\n outColumns.push(startCol);\n }\n const result = cellContents.join('');\n if (isCanonical) {\n this._cache = result;\n this._cacheValid = true;\n this._cacheTrimmed = !!trimRight;\n }\n return result;\n }\n\n /** Copy sparse map entries for a single cell when `_data` flags require them. */\n private _copyCellMapsFrom(src: BufferLine, srcCol: number, destCol: number): void {\n const srcStart = srcCol * Constants.CELL_INDICIES;\n if (src._data[srcStart + Cell.CONTENT] & Content.IS_COMBINED_MASK) {\n this._combined[destCol] = src._combined[srcCol];\n }\n if (src._data[srcStart + Cell.BG] & BgFlags.HAS_EXTENDED) {\n this._extendedAttrs[destCol] = src._extendedAttrs[srcCol];\n }\n }\n\n /** Rebuild sparse maps from another line, keyed only by `_data` flags. */\n private _copySparseMapsFrom(line: BufferLine): void {\n this._combined = {};\n this._extendedAttrs = {};\n for (let i = 0; i < line.length; i++) {\n this._copyCellMapsFrom(line, i, i);\n }\n }\n}\n", "/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n *\n * This was heavily inspired from microsoft/vscode's dependency injection system (MIT).\n */\n/*---------------------------------------------------------------------------------------------\n * Copyright (c) Microsoft Corporation. All rights reserved.\n * Licensed under the MIT License. See License.txt in the project root for license information.\n *--------------------------------------------------------------------------------------------*/\n\nexport interface IServiceIdentifier {\n (...args: any[]): void;\n type: T;\n _id: string;\n}\n\nconst enum Constants {\n DI_TARGET = 'di$target',\n DI_DEPENDENCIES = 'di$dependencies'\n}\n\nexport const serviceRegistry: Map> = new Map();\n\nexport function getServiceDependencies(ctor: any): { id: IServiceIdentifier, index: number, optional: boolean }[] {\n return ctor[Constants.DI_DEPENDENCIES] || [];\n}\n\nexport function createDecorator(id: string): IServiceIdentifier {\n if (serviceRegistry.has(id)) {\n return serviceRegistry.get(id)!;\n }\n\n const decorator: any = function (target: Function, key: string, index: number): any {\n if (arguments.length !== 3) {\n throw new Error('@IServiceName-decorator can only be used to decorate a parameter');\n }\n\n storeServiceDependency(decorator, target, index);\n };\n\n decorator._id = id;\n\n serviceRegistry.set(id, decorator);\n return decorator;\n}\n\nfunction storeServiceDependency(id: Function, target: Function, index: number): void {\n if ((target as any)[Constants.DI_TARGET] === target) {\n (target as any)[Constants.DI_DEPENDENCIES].push({ id, index });\n } else {\n (target as any)[Constants.DI_DEPENDENCIES] = [{ id, index }];\n (target as any)[Constants.DI_TARGET] = target;\n }\n}\n", "/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport type { IDecoration, IDecorationOptions, ILinkHandler, ILogger, IWindowsPty, IOverviewRulerOptions } from '@xterm/xterm';\nimport { CoreMouseEncoding, CoreMouseEventType, CursorInactiveStyle, CursorStyle, ICharset, IColor, ICoreMouseEvent, ICoreMouseProtocol, IDecPrivateModes, IDisposable, IKittyKeyboardState, IModes, IOscLinkData, IWindowOptions } from '../Types';\nimport { IAttributeData, IBuffer, IBufferSet } from '../buffer/Types';\nimport { createDecorator, IServiceIdentifier } from './ServiceRegistry';\nimport type { Emitter, IEvent } from '../Event';\n\nexport const IBufferService = createDecorator('BufferService');\nexport interface IBufferService {\n serviceBrand: undefined;\n\n readonly cols: number;\n readonly rows: number;\n readonly buffer: IBuffer;\n readonly buffers: IBufferSet;\n isUserScrolling: boolean;\n onResize: IEvent;\n onScroll: IEvent;\n scroll(eraseAttr: IAttributeData, isWrapped?: boolean): void;\n scrollLines(disp: number, suppressScrollEvent?: boolean): void;\n resize(cols: number, rows: number): void;\n reset(): void;\n}\n\nexport interface IBufferResizeEvent {\n cols: number;\n rows: number;\n colsChanged: boolean;\n rowsChanged: boolean;\n}\n\nexport const IMouseStateService = createDecorator('MouseStateService');\nexport interface IMouseStateService {\n serviceBrand: undefined;\n\n activeProtocol: string;\n activeEncoding: string;\n areMouseEventsActive: boolean;\n addProtocol(name: string, protocol: ICoreMouseProtocol): void;\n addEncoding(name: string, encoding: CoreMouseEncoding): void;\n reset(): void;\n setCustomWheelEventHandler(customWheelEventHandler: ((event: WheelEvent) => boolean) | undefined): void;\n allowCustomWheelEvent(ev: WheelEvent): boolean;\n\n /**\n * Event to announce changes in mouse tracking.\n */\n onProtocolChange: IEvent;\n restrictMouseEvent(event: ICoreMouseEvent): boolean;\n encodeMouseEvent(event: ICoreMouseEvent): string;\n readonly isDefaultEncoding: boolean;\n readonly isPixelEncoding: boolean;\n}\n\nexport const ICoreService = createDecorator('CoreService');\nexport interface ICoreService {\n serviceBrand: undefined;\n\n /**\n * Initially the cursor will not be visible until the first time the terminal\n * is focused.\n */\n isCursorInitialized: boolean;\n isCursorHidden: boolean;\n\n readonly modes: IModes;\n readonly decPrivateModes: IDecPrivateModes;\n readonly kittyKeyboard: IKittyKeyboardState;\n\n readonly onData: IEvent;\n readonly onUserInput: IEvent;\n readonly onBinary: IEvent;\n readonly onRequestScrollToBottom: IEvent;\n\n reset(): void;\n\n /**\n * Triggers the onData event in the public API.\n * @param data The data that is being emitted.\n * @param wasUserInput Whether the data originated from the user (as opposed to\n * resulting from parsing incoming data). When true this will also:\n * - Scroll to the bottom of the buffer if option scrollOnUserInput is true.\n * - Fire the `onUserInput` event (so selection can be cleared).\n */\n triggerDataEvent(data: string, wasUserInput?: boolean): void;\n\n /**\n * Triggers the onBinary event in the public API.\n * @param data The data that is being emitted.\n */\n triggerBinaryEvent(data: string): void;\n}\n\nexport const ICharsetService = createDecorator('CharsetService');\nexport interface ICharsetService {\n serviceBrand: undefined;\n\n charset: ICharset | undefined;\n readonly glevel: number;\n readonly charsets: (ICharset | undefined)[];\n\n reset(): void;\n\n /**\n * Set the G level of the terminal.\n * @param g\n */\n setgLevel(g: number): void;\n\n /**\n * Set the charset for the given G level of the terminal.\n * @param g\n * @param charset\n */\n setgCharset(g: number, charset: ICharset | undefined): void;\n}\n\nexport interface IBrandedService {\n serviceBrand: undefined;\n}\n\ntype GetLeadingNonServiceArgs = TArgs extends [] ? []\n : TArgs extends [...infer TFirst, infer TLast] ? TLast extends IBrandedService ? GetLeadingNonServiceArgs : TArgs\n : never;\n\nexport const IInstantiationService = createDecorator('InstantiationService');\nexport interface IInstantiationService {\n serviceBrand: undefined;\n\n setService(id: IServiceIdentifier, instance: T): void;\n getService(id: IServiceIdentifier): T | undefined;\n createInstance any, R extends InstanceType>(t: Ctor, ...args: GetLeadingNonServiceArgs>): R;\n}\n\nexport enum LogLevelEnum {\n TRACE = 0,\n DEBUG = 1,\n INFO = 2,\n WARN = 3,\n ERROR = 4,\n OFF = 5\n}\n\nexport const ILogService = createDecorator('LogService');\nexport interface ILogService {\n serviceBrand: undefined;\n\n readonly logLevel: LogLevelEnum;\n\n trace(message: any, ...optionalParams: any[]): void;\n debug(message: any, ...optionalParams: any[]): void;\n info(message: any, ...optionalParams: any[]): void;\n warn(message: any, ...optionalParams: any[]): void;\n error(message: any, ...optionalParams: any[]): void;\n}\n\nexport const IOptionsService = createDecorator('OptionsService');\nexport interface IOptionsService {\n serviceBrand: undefined;\n\n /**\n * Read only access to the raw options object, this is an internal-only fast path for accessing\n * single options without any validation as we trust TypeScript to enforce correct usage\n * internally.\n */\n readonly rawOptions: Required;\n\n /**\n * Options as exposed through the public API, this property uses getters and setters with\n * validation which makes it safer but slower. {@link rawOptions} should be used for pretty much\n * all internal usage for performance reasons.\n */\n readonly options: Required;\n\n /**\n * Adds an event listener for when any option changes.\n */\n readonly onOptionChange: IEvent;\n\n /**\n * Adds an event listener for when a specific option changes, this is a convenience method that is\n * preferred over {@link onOptionChange} when only a single option is being listened to.\n */\n // eslint-disable-next-line @typescript-eslint/naming-convention\n onSpecificOptionChange(key: T, listener: (arg1: Required[T]) => any): IDisposable;\n\n /**\n * Adds an event listener for when a set of specific options change, this is a convenience method\n * that is preferred over {@link onOptionChange} when multiple options are being listened to and\n * handled the same way.\n */\n // eslint-disable-next-line @typescript-eslint/naming-convention\n onMultipleOptionChange(keys: (keyof ITerminalOptions)[], listener: () => any): IDisposable;\n}\n\nexport type FontWeight = 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900' | number;\nexport type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'off';\n\nexport interface ITerminalOptions {\n allowProposedApi?: boolean;\n allowTransparency?: boolean;\n altClickMovesCursor?: boolean;\n cols?: number;\n convertEol?: boolean;\n cursorBlink?: boolean;\n blinkIntervalDuration?: number;\n cursorStyle?: CursorStyle;\n cursorWidth?: number;\n cursorInactiveStyle?: CursorInactiveStyle;\n disableStdin?: boolean;\n documentOverride?: any | null;\n drawBoldTextInBrightColors?: boolean;\n fastScrollSensitivity?: number;\n fontSize?: number;\n fontFamily?: string;\n fontWeight?: FontWeight;\n fontWeightBold?: FontWeight;\n ignoreBracketedPasteMode?: boolean;\n letterSpacing?: number;\n lineHeight?: number;\n linkHandler?: ILinkHandler | null;\n logLevel?: LogLevel;\n logger?: ILogger | null;\n macOptionIsMeta?: boolean;\n macOptionClickForcesSelection?: boolean;\n minimumContrastRatio?: number;\n mouseEventsRequireAlt?: boolean;\n reflowCursorLine?: boolean;\n rescaleOverlappingGlyphs?: boolean;\n rightClickSelectsWord?: boolean;\n rows?: number;\n showCursorImmediately?: boolean;\n screenReaderMode?: boolean;\n scrollback?: number;\n scrollOnUserInput?: boolean;\n scrollSensitivity?: number;\n smoothScrollDuration?: number;\n tabStopWidth?: number;\n theme?: ITheme;\n windowsPty?: IWindowsPty;\n windowOptions?: IWindowOptions;\n wordSeparator?: string;\n quirks?: ITerminalQuirks;\n scrollbar?: IScrollbarOptions;\n scrollOnEraseInDisplay?: boolean;\n vtExtensions?: IVtExtensions;\n\n [key: string]: any;\n termName: string;\n}\n\nexport interface ITheme {\n foreground?: string;\n background?: string;\n cursor?: string;\n cursorAccent?: string;\n selectionForeground?: string;\n selectionBackground?: string;\n selectionInactiveBackground?: string;\n scrollbarSliderBackground?: string;\n scrollbarSliderHoverBackground?: string;\n scrollbarSliderActiveBackground?: string;\n overviewRulerBorder?: string;\n black?: string;\n red?: string;\n green?: string;\n yellow?: string;\n blue?: string;\n magenta?: string;\n cyan?: string;\n white?: string;\n brightBlack?: string;\n brightRed?: string;\n brightGreen?: string;\n brightYellow?: string;\n brightBlue?: string;\n brightMagenta?: string;\n brightCyan?: string;\n brightWhite?: string;\n extendedAnsi?: string[];\n}\n\nexport interface ITerminalQuirks {\n allowSetCursorBlink?: boolean;\n}\n\nexport interface IScrollbarOptions {\n showScrollbar?: boolean;\n showArrows?: boolean;\n width?: number;\n overviewRuler?: IOverviewRulerOptions;\n}\n\nexport interface IVtExtensions {\n kittyKeyboard?: boolean;\n kittySgrBoldFaintControl?: boolean;\n win32InputMode?: boolean;\n colorSchemeQuery?: boolean;\n}\n\nexport const IOscLinkService = createDecorator('OscLinkService');\nexport interface IOscLinkService {\n serviceBrand: undefined;\n /**\n * Registers a link to the service, returning the link ID. The link data is managed by this\n * service and will be freed when this current cursor position is trimmed off the buffer.\n */\n registerLink(linkData: IOscLinkData): number;\n /**\n * Adds a line to a link if needed.\n */\n addLineToLink(linkId: number, y: number): void;\n /** Get the link data associated with a link ID. */\n getLinkData(linkId: number): IOscLinkData | undefined;\n}\n\n/*\n * Width and Grapheme_Cluster_Break properties of a character as a bit mask.\n *\n * bit 0: shouldJoin - should combine with preceding character.\n * bit 1..2: wcwidth - see UnicodeCharWidth.\n * bit 3..31: class of character (currently only 4 bits are used).\n * This is used to determined grapheme clustering - i.e. which codepoints\n * are to be combined into a single compound character.\n *\n * Use the UnicodeService static function createPropertyValue to create a\n * UnicodeCharProperties; use extractShouldJoin, extractWidth, and\n * extractCharKind to extract the components.\n */\nexport type UnicodeCharProperties = number;\n\n/**\n * Width in columns of a character.\n * In a CJK context, \"half-width\" characters (such as Latin) are width 1,\n * while \"full-width\" characters (such as Kanji) are 2 columns wide.\n * Combining characters (such as accents) are width 0.\n */\nexport type UnicodeCharWidth = 0 | 1 | 2;\n\nexport const IUnicodeService = createDecorator('UnicodeService');\nexport interface IUnicodeService {\n serviceBrand: undefined;\n /** Register a Unicode version provider. */\n register(provider: IUnicodeVersionProvider): void;\n /** Registered Unicode versions. */\n readonly versions: string[];\n /** Currently active version. */\n activeVersion: string;\n /** Event triggered when the active version changes. */\n readonly onChange: IEvent;\n\n /**\n * Unicode version dependent\n */\n wcwidth(codepoint: number): UnicodeCharWidth;\n getStringCellWidth(s: string): number;\n /**\n * Return character width and type for grapheme clustering.\n * If preceding != 0, it is the return code from the previous character;\n * in that case the result specifies if the characters should be joined.\n */\n charProperties(codepoint: number, preceding: UnicodeCharProperties): UnicodeCharProperties;\n}\n\nexport interface IUnicodeVersionProvider {\n readonly version: string;\n wcwidth(ucs: number): UnicodeCharWidth;\n charProperties(codepoint: number, preceding: UnicodeCharProperties): UnicodeCharProperties;\n}\n\nexport const IDecorationService = createDecorator('DecorationService');\nexport interface IDecorationService extends IDisposable {\n serviceBrand: undefined;\n readonly decorations: IterableIterator;\n readonly onDecorationRegistered: IEvent;\n readonly onDecorationRemoved: IEvent;\n registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined;\n reset(): void;\n /**\n * Trigger a callback over the decoration at a cell (in no particular order). This uses a callback\n * instead of an iterator as it's typically used in hot code paths.\n */\n forEachDecorationAtCell(x: number, line: number, layer: 'bottom' | 'top' | undefined, callback: (decoration: IInternalDecoration) => void): void;\n}\nexport interface IInternalDecoration extends IDecoration {\n readonly options: IDecorationOptions;\n readonly backgroundColorRGB: IColor | undefined;\n readonly foregroundColorRGB: IColor | undefined;\n readonly onRenderEmitter: Emitter;\n /** @internal Start line for line-index removal; kept in sync on buffer line shifts. */\n _indexedStartLine: number;\n}\n", "/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n *\n * This was heavily inspired from microsoft/vscode's dependency injection system (MIT).\n */\n/*---------------------------------------------------------------------------------------------\n * Copyright (c) Microsoft Corporation. All rights reserved.\n * Licensed under the MIT License. See License.txt in the project root for license information.\n *--------------------------------------------------------------------------------------------*/\n\nimport { IInstantiationService } from './Services';\nimport { IServiceIdentifier, getServiceDependencies } from './ServiceRegistry';\n\nexport class ServiceCollection {\n\n private _entries = new Map, any>();\n\n constructor(...entries: [IServiceIdentifier, any][]) {\n for (const [id, service] of entries) {\n this.set(id, service);\n }\n }\n\n public set(id: IServiceIdentifier, instance: T): T {\n const result = this._entries.get(id);\n this._entries.set(id, instance);\n return result;\n }\n\n public forEach(callback: (id: IServiceIdentifier, instance: any) => any): void {\n for (const [key, value] of this._entries.entries()) {\n callback(key, value);\n }\n }\n\n public has(id: IServiceIdentifier): boolean {\n return this._entries.has(id);\n }\n\n public get(id: IServiceIdentifier): T | undefined {\n return this._entries.get(id);\n }\n}\n\nexport class InstantiationService implements IInstantiationService {\n public serviceBrand: undefined;\n\n private readonly _services: ServiceCollection = new ServiceCollection();\n\n constructor() {\n this._services.set(IInstantiationService, this);\n }\n\n public setService(id: IServiceIdentifier, instance: T): void {\n this._services.set(id, instance);\n }\n\n public getService(id: IServiceIdentifier): T | undefined {\n return this._services.get(id);\n }\n\n public createInstance(ctor: any, ...args: any[]): T {\n const serviceDependencies = getServiceDependencies(ctor).sort((a, b) => a.index - b.index);\n\n const serviceArgs: any[] = [];\n for (const dependency of serviceDependencies) {\n const service = this._services.get(dependency.id);\n if (!service) {\n throw new Error(`[createInstance] ${ctor.name} depends on UNKNOWN service ${dependency.id._id}.`);\n }\n serviceArgs.push(service);\n }\n\n const firstServiceArgPos = serviceDependencies.length > 0 ? serviceDependencies[0].index : args.length;\n\n // check for argument mismatches, adjust static args if needed\n if (args.length !== firstServiceArgPos) {\n throw new Error(`[createInstance] First service dependency of ${ctor.name} at position ${firstServiceArgPos + 1} conflicts with ${args.length} static arguments`);\n }\n\n // now create the instance\n return new ctor(...[...args, ...serviceArgs]);\n }\n}\n", "/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { Disposable } from '../Lifecycle';\nimport { ILogService, IOptionsService, LogLevelEnum } from './Services';\n\ntype LogType = (message?: any, ...optionalParams: any[]) => void;\n\ninterface IConsole {\n log: LogType;\n error: LogType;\n info: LogType;\n trace: LogType;\n warn: LogType;\n}\n\n// console is available on both node.js and browser contexts but the common\n// module doesn't depend on them so we need to explicitly declare it.\ndeclare const console: IConsole;\n\nconst optionsKeyToLogLevel: { [key: string]: LogLevelEnum } = {\n trace: LogLevelEnum.TRACE,\n debug: LogLevelEnum.DEBUG,\n info: LogLevelEnum.INFO,\n warn: LogLevelEnum.WARN,\n error: LogLevelEnum.ERROR,\n off: LogLevelEnum.OFF\n};\n\nconst LOG_PREFIX = 'xterm.js: ';\n\nexport class LogService extends Disposable implements ILogService {\n public serviceBrand: any;\n\n private _logLevel: LogLevelEnum = LogLevelEnum.OFF;\n public get logLevel(): LogLevelEnum { return this._logLevel; }\n\n constructor(\n @IOptionsService private readonly _optionsService: IOptionsService\n ) {\n super();\n this._updateLogLevel();\n this._register(this._optionsService.onSpecificOptionChange('logLevel', () => this._updateLogLevel()));\n }\n\n private _updateLogLevel(): void {\n this._logLevel = optionsKeyToLogLevel[this._optionsService.rawOptions.logLevel];\n }\n\n private _evalLazyOptionalParams(optionalParams: any[]): void {\n for (let i = 0; i < optionalParams.length; i++) {\n if (typeof optionalParams[i] === 'function') {\n optionalParams[i] = optionalParams[i]();\n }\n }\n }\n\n private _log(type: LogType, message: string, optionalParams: any[]): void {\n this._evalLazyOptionalParams(optionalParams);\n type.call(console, (this._optionsService.options.logger ? '' : LOG_PREFIX) + message, ...optionalParams);\n }\n\n public trace(message: string, ...optionalParams: any[]): void {\n if (this._logLevel <= LogLevelEnum.TRACE) {\n this._log(this._optionsService.options.logger?.trace.bind(this._optionsService.options.logger) ?? console.log, message, optionalParams);\n }\n }\n\n public debug(message: string, ...optionalParams: any[]): void {\n if (this._logLevel <= LogLevelEnum.DEBUG) {\n this._log(this._optionsService.options.logger?.debug.bind(this._optionsService.options.logger) ?? console.log, message, optionalParams);\n }\n }\n\n public info(message: string, ...optionalParams: any[]): void {\n if (this._logLevel <= LogLevelEnum.INFO) {\n this._log(this._optionsService.options.logger?.info.bind(this._optionsService.options.logger) ?? console.info, message, optionalParams);\n }\n }\n\n public warn(message: string, ...optionalParams: any[]): void {\n if (this._logLevel <= LogLevelEnum.WARN) {\n this._log(this._optionsService.options.logger?.warn.bind(this._optionsService.options.logger) ?? console.warn, message, optionalParams);\n }\n }\n\n public error(message: string, ...optionalParams: any[]): void {\n if (this._logLevel <= LogLevelEnum.ERROR) {\n this._log(this._optionsService.options.logger?.error.bind(this._optionsService.options.logger) ?? console.error, message, optionalParams);\n }\n }\n}\n", "/**\n * Copyright (c) 2016 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { Disposable } from './Lifecycle';\nimport { Emitter, type IEvent } from './Event';\n\nexport interface IInsertEvent {\n index: number;\n amount: number;\n}\n\nexport interface IDeleteEvent {\n index: number;\n amount: number;\n}\n\nexport interface ICircularList {\n length: number;\n maxLength: number;\n isFull: boolean;\n\n onDeleteEmitter: Emitter;\n onDelete: IEvent;\n onInsertEmitter: Emitter;\n onInsert: IEvent;\n onTrimEmitter: Emitter;\n onTrim: IEvent;\n\n get(index: number): T | undefined;\n set(index: number, value: T): void;\n push(value: T): void;\n recycle(): T;\n pop(): T | undefined;\n splice(start: number, deleteCount: number, ...items: T[]): void;\n trimStart(count: number): void;\n shiftElements(start: number, count: number, offset: number): void;\n}\n\n/**\n * Represents a circular list; a list with a maximum size that wraps around when push is called,\n * overriding values at the start of the list.\n */\nexport class CircularList extends Disposable implements ICircularList {\n protected _array: (T | undefined)[];\n private _startIndex: number;\n private _length: number;\n\n public readonly onDeleteEmitter = this._register(new Emitter());\n public readonly onDelete = this.onDeleteEmitter.event;\n public readonly onInsertEmitter = this._register(new Emitter());\n public readonly onInsert = this.onInsertEmitter.event;\n public readonly onTrimEmitter = this._register(new Emitter());\n public readonly onTrim = this.onTrimEmitter.event;\n\n constructor(\n private _maxLength: number\n ) {\n super();\n this._array = new Array(this._maxLength);\n this._startIndex = 0;\n this._length = 0;\n }\n\n public get maxLength(): number {\n return this._maxLength;\n }\n\n public set maxLength(newMaxLength: number) {\n // There was no change in maxLength, return early.\n if (this._maxLength === newMaxLength) {\n return;\n }\n\n // Reconstruct array, starting at index 0. Only transfer values from the\n // indexes 0 to length.\n const newArray = new Array(newMaxLength);\n for (let i = 0; i < Math.min(newMaxLength, this.length); i++) {\n newArray[i] = this._array[this._getCyclicIndex(i)];\n }\n this._array = newArray;\n this._maxLength = newMaxLength;\n this._startIndex = 0;\n }\n\n public get length(): number {\n return this._length;\n }\n\n public set length(newLength: number) {\n if (newLength > this._length) {\n for (let i = this._length; i < newLength; i++) {\n this._array[i] = undefined;\n }\n }\n this._length = newLength;\n }\n\n /**\n * Gets the value at an index.\n *\n * Note that for performance reasons there is no bounds checking here, the index reference is\n * circular so this should always return a value and never throw.\n * @param index The index of the value to get.\n * @returns The value corresponding to the index.\n */\n public get(index: number): T | undefined {\n return this._array[this._getCyclicIndex(index)];\n }\n\n /**\n * Sets the value at an index.\n *\n * Note that for performance reasons there is no bounds checking here, the index reference is\n * circular so this should always return a value and never throw.\n * @param index The index to set.\n * @param value The value to set.\n */\n public set(index: number, value: T | undefined): void {\n this._array[this._getCyclicIndex(index)] = value;\n }\n\n /**\n * Pushes a new value onto the list, wrapping around to the start of the array, overriding index 0\n * if the maximum length is reached.\n * @param value The value to push onto the list.\n */\n public push(value: T): void {\n this._array[this._getCyclicIndex(this._length)] = value;\n if (this._length === this._maxLength) {\n this._startIndex = ++this._startIndex % this._maxLength;\n this.onTrimEmitter.fire(1);\n } else {\n this._length++;\n }\n }\n\n /**\n * Advance ringbuffer index and return current element for recycling.\n * Note: The buffer must be full for this method to work.\n * @throws When the buffer is not full.\n */\n public recycle(): T {\n if (this._length !== this._maxLength) {\n throw new Error('Can only recycle when the buffer is full');\n }\n this._startIndex = ++this._startIndex % this._maxLength;\n this.onTrimEmitter.fire(1);\n return this._array[this._getCyclicIndex(this._length - 1)]!;\n }\n\n /**\n * Ringbuffer is at max length.\n */\n public get isFull(): boolean {\n return this._length === this._maxLength;\n }\n\n /**\n * Removes and returns the last value on the list.\n * @returns The popped value.\n */\n public pop(): T | undefined {\n return this._array[this._getCyclicIndex(this._length-- - 1)];\n }\n\n /**\n * Deletes and/or inserts items at a particular index (in that order). Unlike\n * Array.prototype.splice, this operation does not return the deleted items as a new array in\n * order to save creating a new array. Note that this operation may shift all values in the list\n * in the worst case.\n * @param start The index to delete and/or insert.\n * @param deleteCount The number of elements to delete.\n * @param items The items to insert.\n */\n public splice(start: number, deleteCount: number, ...items: T[]): void {\n // Delete items\n if (deleteCount) {\n for (let i = start; i < this._length - deleteCount; i++) {\n this._array[this._getCyclicIndex(i)] = this._array[this._getCyclicIndex(i + deleteCount)];\n }\n this._length -= deleteCount;\n this.onDeleteEmitter.fire({ index: start, amount: deleteCount });\n }\n\n // Add items\n for (let i = this._length - 1; i >= start; i--) {\n this._array[this._getCyclicIndex(i + items.length)] = this._array[this._getCyclicIndex(i)];\n }\n for (let i = 0; i < items.length; i++) {\n this._array[this._getCyclicIndex(start + i)] = items[i];\n }\n if (items.length) {\n this.onInsertEmitter.fire({ index: start, amount: items.length });\n }\n\n // Adjust length as needed\n if (this._length + items.length > this._maxLength) {\n const countToTrim = (this._length + items.length) - this._maxLength;\n this._startIndex += countToTrim;\n this._length = this._maxLength;\n this.onTrimEmitter.fire(countToTrim);\n } else {\n this._length += items.length;\n }\n }\n\n /**\n * Trims a number of items from the start of the list.\n * @param count The number of items to remove.\n */\n public trimStart(count: number): void {\n if (count > this._length) {\n count = this._length;\n }\n this._startIndex += count;\n this._length -= count;\n this.onTrimEmitter.fire(count);\n }\n\n public shiftElements(start: number, count: number, offset: number): void {\n if (count <= 0) {\n return;\n }\n if (start < 0 || start >= this._length) {\n throw new Error('start argument out of range');\n }\n if (start + offset < 0) {\n throw new Error('Cannot shift elements in list beyond index 0');\n }\n\n if (offset > 0) {\n for (let i = count - 1; i >= 0; i--) {\n this.set(start + i + offset, this.get(start + i));\n }\n const expandListBy = (start + count + offset) - this._length;\n if (expandListBy > 0) {\n this._length += expandListBy;\n while (this._length > this._maxLength) {\n this._length--;\n this._startIndex++;\n this.onTrimEmitter.fire(1);\n }\n }\n } else {\n for (let i = 0; i < count; i++) {\n this.set(start + i + offset, this.get(start + i));\n }\n }\n }\n\n /**\n * Gets the cyclic index for the specified regular index. The cyclic index can then be used on the\n * backing array to get the element associated with the regular index.\n * @param index The regular index.\n * @returns The cyclic index.\n */\n private _getCyclicIndex(index: number): number {\n return (this._startIndex + index) % this._maxLength;\n }\n}\n", "/**\n * Copyright (c) 2022 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport type { ILogService } from './services/Services';\n\ninterface ITaskQueue {\n /**\n * Adds a task to the queue which will run in a future idle callback.\n * To avoid perceivable stalls on the main thread, tasks with heavy workload\n * should split their work into smaller pieces and return `true` to get\n * called again until the work is done (on falsy return value).\n */\n enqueue(task: () => boolean | void): void;\n\n /**\n * Flushes the queue, running all remaining tasks synchronously.\n */\n flush(): void;\n\n /**\n * Clears any remaining tasks from the queue, these will not be run.\n */\n clear(): void;\n}\n\ninterface ITaskDeadline {\n timeRemaining(): number;\n}\ntype CallbackWithDeadline = (deadline: ITaskDeadline) => void;\n\nabstract class TaskQueue implements ITaskQueue {\n private _tasks: (() => boolean | void)[] = [];\n private _idleCallback?: number;\n private _i = 0;\n protected readonly _logService: ILogService;\n\n constructor(logService: ILogService) {\n this._logService = logService;\n }\n\n protected abstract _requestCallback(callback: CallbackWithDeadline): number;\n protected abstract _cancelCallback(identifier: number): void;\n\n public enqueue(task: () => boolean | void): void {\n this._tasks.push(task);\n this._start();\n }\n\n public flush(): void {\n while (this._i < this._tasks.length) {\n if (!this._tasks[this._i]()) {\n this._i++;\n }\n }\n this.clear();\n }\n\n public clear(): void {\n if (this._idleCallback) {\n this._cancelCallback(this._idleCallback);\n this._idleCallback = undefined;\n }\n this._i = 0;\n this._tasks.length = 0;\n }\n\n private _start(): void {\n if (!this._idleCallback) {\n this._idleCallback = this._requestCallback(this._process.bind(this));\n }\n }\n\n private _process(deadline: ITaskDeadline): void {\n this._idleCallback = undefined;\n let taskDuration: number;\n let longestTask = 0;\n let lastDeadlineRemaining = deadline.timeRemaining();\n let deadlineRemaining: number;\n while (this._i < this._tasks.length) {\n taskDuration = performance.now();\n if (!this._tasks[this._i]()) {\n this._i++;\n }\n // other than performance.now, performance.now might not be stable (changes on wall clock\n // changes), this is not an issue here as a clock change during a short running task is very\n // unlikely in case it still happened and leads to negative duration, simply assume 1 msec\n taskDuration = Math.max(1, performance.now() - taskDuration);\n longestTask = Math.max(taskDuration, longestTask);\n // Guess the following task will take a similar time to the longest task in this batch, allow\n // additional room to try avoid exceeding the deadline\n deadlineRemaining = deadline.timeRemaining();\n if (longestTask * 1.5 > deadlineRemaining) {\n // Warn when the time exceeding the deadline is over 20ms, if this happens in practice the\n // task should be split into sub-tasks to ensure the UI remains responsive.\n if (lastDeadlineRemaining - taskDuration < -20) {\n this._logService.warn(`task queue exceeded allotted deadline by ${Math.abs(Math.round(lastDeadlineRemaining - taskDuration))}ms`);\n }\n this._start();\n return;\n }\n lastDeadlineRemaining = deadlineRemaining;\n }\n this.clear();\n }\n}\n\n/**\n * A queue of that runs tasks over several tasks via setTimeout, trying to maintain above 60 frames\n * per second. The tasks will run in the order they are enqueued, but they will run some time later,\n * and care should be taken to ensure they're non-urgent and will not introduce race conditions.\n */\nexport class PriorityTaskQueue extends TaskQueue {\n protected _requestCallback(callback: CallbackWithDeadline): number {\n return setTimeout(() => callback(this._createDeadline(16)));\n }\n\n protected _cancelCallback(identifier: number): void {\n clearTimeout(identifier);\n }\n\n private _createDeadline(duration: number): ITaskDeadline {\n const end = performance.now() + duration;\n return {\n timeRemaining: () => Math.max(0, end - performance.now())\n };\n }\n}\n\nclass IdleTaskQueueInternal extends TaskQueue {\n protected _requestCallback(callback: IdleRequestCallback): number {\n return requestIdleCallback(callback);\n }\n\n protected _cancelCallback(identifier: number): void {\n cancelIdleCallback(identifier);\n }\n}\n\n/**\n * A queue of that runs tasks over several idle callbacks, trying to respect the idle callback's\n * deadline given by the environment. The tasks will run in the order they are enqueued, but they\n * will run some time later, and care should be taken to ensure they're non-urgent and will not\n * introduce race conditions.\n *\n * This reverts to a {@link PriorityTaskQueue} if the environment does not support idle callbacks.\n */\n// eslint-disable-next-line @typescript-eslint/naming-convention\nexport const IdleTaskQueue = ('requestIdleCallback' in globalThis) ? IdleTaskQueueInternal : PriorityTaskQueue;\n\n/**\n * An object that tracks a single debounced task that will run on the next idle frame. When called\n * multiple times, only the last set task will run.\n */\nexport class DebouncedIdleTask {\n private _queue: ITaskQueue;\n\n constructor(logService: ILogService) {\n this._queue = new IdleTaskQueue(logService);\n }\n\n public set(task: () => boolean | void): void {\n this._queue.clear();\n this._queue.enqueue(task);\n }\n\n public flush(): void {\n this._queue.flush();\n }\n\n public dispose(): void {\n this._queue.clear();\n }\n}\n", "/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { BufferLine } from './BufferLine';\nimport { CircularList } from '../CircularList';\nimport { IBufferLine, ICellData } from './Types';\n\nexport interface INewLayoutResult {\n layout: number[];\n countRemoved: number;\n}\n\n/**\n * Evaluates and returns indexes to be removed after a reflow larger occurs. Lines will be removed\n * when a wrapped line unwraps.\n * @param lines The buffer lines.\n * @param oldCols The columns before resize\n * @param newCols The columns after resize.\n * @param bufferAbsoluteY The absolute y position of the cursor (baseY + cursorY).\n * @param nullCell The cell data to use when filling in empty cells.\n * @param reflowCursorLine Whether to reflow the line containing the cursor.\n */\nexport function reflowLargerGetLinesToRemove(lines: CircularList, oldCols: number, newCols: number, bufferAbsoluteY: number, nullCell: ICellData, reflowCursorLine: boolean): number[] {\n // Gather all BufferLines that need to be removed from the Buffer here so that they can be\n // batched up and only committed once\n const toRemove: number[] = [];\n\n for (let y = 0; y < lines.length - 1; y++) {\n // Check if this row is wrapped\n let i = y;\n let nextLine = lines.get(++i) as BufferLine;\n if (!nextLine.isWrapped) {\n continue;\n }\n\n // Check how many lines it's wrapped for\n const wrappedLines: BufferLine[] = [lines.get(y) as BufferLine];\n while (i < lines.length && nextLine.isWrapped) {\n wrappedLines.push(nextLine);\n nextLine = lines.get(++i) as BufferLine;\n }\n\n if (!reflowCursorLine) {\n // If these lines contain the cursor don't touch them, the program will handle fixing up\n // wrapped lines with the cursor\n if (bufferAbsoluteY >= y && bufferAbsoluteY < i) {\n y += wrappedLines.length - 1;\n continue;\n }\n }\n\n // Copy buffer data to new locations\n let destLineIndex = 0;\n let destCol = getWrappedLineTrimmedLength(wrappedLines, destLineIndex, oldCols);\n let srcLineIndex = 1;\n let srcCol = 0;\n while (srcLineIndex < wrappedLines.length) {\n const srcTrimmedTineLength = getWrappedLineTrimmedLength(wrappedLines, srcLineIndex, oldCols);\n const srcRemainingCells = srcTrimmedTineLength - srcCol;\n const destRemainingCells = newCols - destCol;\n const cellsToCopy = Math.min(srcRemainingCells, destRemainingCells);\n\n wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol, destCol, cellsToCopy, false);\n\n destCol += cellsToCopy;\n if (destCol === newCols) {\n destLineIndex++;\n destCol = 0;\n }\n srcCol += cellsToCopy;\n if (srcCol === srcTrimmedTineLength) {\n srcLineIndex++;\n srcCol = 0;\n }\n\n // Make sure the last cell isn't wide, if it is copy it to the current dest\n if (destCol === 0 && destLineIndex !== 0) {\n if (wrappedLines[destLineIndex - 1].getWidth(newCols - 1) === 2) {\n wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[destLineIndex - 1], newCols - 1, destCol++, 1, false);\n // Null out the end of the last row\n wrappedLines[destLineIndex - 1].setCell(newCols - 1, nullCell);\n }\n }\n }\n\n // Clear out remaining cells or fragments could remain;\n wrappedLines[destLineIndex].replaceCells(destCol, newCols, nullCell);\n\n // Work backwards and remove any rows at the end that only contain null cells\n let countToRemove = 0;\n for (let i = wrappedLines.length - 1; i > 0; i--) {\n if (i > destLineIndex || wrappedLines[i].getTrimmedLength() === 0) {\n countToRemove++;\n } else {\n break;\n }\n }\n\n if (countToRemove > 0) {\n toRemove.push(y + wrappedLines.length - countToRemove); // index\n toRemove.push(countToRemove);\n }\n\n y += wrappedLines.length - 1;\n }\n return toRemove;\n}\n\n/**\n * Creates and return the new layout for lines given an array of indexes to be removed.\n * @param lines The buffer lines.\n * @param toRemove The indexes to remove.\n */\nexport function reflowLargerCreateNewLayout(lines: CircularList, toRemove: number[]): INewLayoutResult {\n const layout: number[] = [];\n // First iterate through the list and get the actual indexes to use for rows\n let nextToRemoveIndex = 0;\n let nextToRemoveStart = toRemove[nextToRemoveIndex];\n let countRemovedSoFar = 0;\n for (let i = 0; i < lines.length; i++) {\n if (nextToRemoveStart === i) {\n const countToRemove = toRemove[++nextToRemoveIndex];\n\n // Tell markers that there was a deletion\n lines.onDeleteEmitter.fire({\n index: i - countRemovedSoFar,\n amount: countToRemove\n });\n\n i += countToRemove - 1;\n countRemovedSoFar += countToRemove;\n nextToRemoveStart = toRemove[++nextToRemoveIndex];\n } else {\n layout.push(i);\n }\n }\n return {\n layout,\n countRemoved: countRemovedSoFar\n };\n}\n\n/**\n * Applies a new layout to the buffer. This essentially does the same as many splice calls but it's\n * done all at once in a single iteration through the list since splice is very expensive.\n * @param lines The buffer lines.\n * @param newLayout The new layout to apply.\n */\nexport function reflowLargerApplyNewLayout(lines: CircularList, newLayout: number[]): void {\n // Record original lines so they don't get overridden when we rearrange the list\n const newLayoutLines: BufferLine[] = [];\n for (let i = 0; i < newLayout.length; i++) {\n newLayoutLines.push(lines.get(newLayout[i]) as BufferLine);\n }\n\n // Rearrange the list\n for (let i = 0; i < newLayoutLines.length; i++) {\n lines.set(i, newLayoutLines[i]);\n }\n lines.length = newLayout.length;\n}\n\n/**\n * Gets the new line lengths for a given wrapped line. The purpose of this function it to pre-\n * compute the wrapping points since wide characters may need to be wrapped onto the following line.\n * This function will return an array of numbers of where each line wraps to, the resulting array\n * will only contain the values `newCols` (when the line does not end with a wide character) and\n * `newCols - 1` (when the line does end with a wide character), except for the last value which\n * will contain the remaining items to fill the line.\n *\n * Calling this with a `newCols` value of `1` will lock up.\n *\n * @param wrappedLines The wrapped lines to evaluate.\n * @param oldCols The columns before resize.\n * @param newCols The columns after resize.\n */\nexport function reflowSmallerGetNewLineLengths(wrappedLines: BufferLine[], oldCols: number, newCols: number): number[] {\n const newLineLengths: number[] = [];\n let cellsNeeded = 0;\n for (let i = 0; i < wrappedLines.length; i++) {\n cellsNeeded += getWrappedLineTrimmedLength(wrappedLines, i, oldCols);\n }\n\n // Use srcCol and srcLine to find the new wrapping point, use that to get the cellsAvailable and\n // linesNeeded\n let srcCol = 0;\n let srcLine = 0;\n let cellsAvailable = 0;\n while (cellsAvailable < cellsNeeded) {\n if (cellsNeeded - cellsAvailable < newCols) {\n // Add the final line and exit the loop\n newLineLengths.push(cellsNeeded - cellsAvailable);\n break;\n }\n srcCol += newCols;\n const oldTrimmedLength = getWrappedLineTrimmedLength(wrappedLines, srcLine, oldCols);\n if (srcCol > oldTrimmedLength) {\n srcCol -= oldTrimmedLength;\n srcLine++;\n }\n const endsWithWide = wrappedLines[srcLine].getWidth(srcCol - 1) === 2;\n if (endsWithWide) {\n srcCol--;\n }\n const lineLength = endsWithWide ? newCols - 1 : newCols;\n newLineLengths.push(lineLength);\n cellsAvailable += lineLength;\n }\n\n return newLineLengths;\n}\n\nexport function getWrappedLineTrimmedLength(lines: BufferLine[], i: number, cols: number): number {\n // If this is the last row in the wrapped line, get the actual trimmed length\n if (i === lines.length - 1) {\n return lines[i].getTrimmedLength();\n }\n // Detect whether the following line starts with a wide character and the end of the current line\n // is null, if so then we can be pretty sure the null character should be excluded from the line\n // length]\n const endsInNull = !(lines[i].hasContent(cols - 1)) && lines[i].getWidth(cols - 1) === 1;\n const followingLineStartsWithWide = lines[i + 1].getWidth(0) === 2;\n if (endsInNull && followingLineStartsWithWide) {\n return cols - 1;\n }\n return cols;\n}\n", "/**\n * Copyright (c) 2018 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { dispose, IDisposable } from '../Lifecycle';\nimport { IMarker } from './Types';\nimport { Emitter } from '../Event';\n\nexport class Marker implements IMarker {\n private static _nextId = 1;\n\n public isDisposed: boolean = false;\n private readonly _disposables: IDisposable[] = [];\n\n private readonly _id: number = Marker._nextId++;\n public get id(): number { return this._id; }\n\n private readonly _onDispose = this.register(new Emitter());\n public readonly onDispose = this._onDispose.event;\n\n constructor(\n public line: number\n ) {\n }\n\n public dispose(): void {\n if (this.isDisposed) {\n return;\n }\n this.isDisposed = true;\n this.line = -1;\n // Emit before super.dispose such that dispose listeners get a chance to react\n this._onDispose.fire();\n dispose(this._disposables);\n this._disposables.length = 0;\n }\n\n public register(disposable: T): T {\n this._disposables.push(disposable);\n return disposable;\n }\n}\n", "/**\n * Copyright (c) 2016 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { ICharset } from '../Types';\n\n/**\n * The character sets supported by the terminal. These enable several languages\n * to be represented within the terminal with only 8-bit encoding. See ISO 2022\n * for a discussion on character sets. Only VT100 character sets are supported.\n */\nexport const CHARSETS: { [key: string]: ICharset | undefined } = {};\n\n/**\n * The default character set, US.\n */\nexport const DEFAULT_CHARSET: ICharset | undefined = CHARSETS['B'];\n\n/**\n * DEC Special Character and Line Drawing Set.\n * Reference: http://vt100.net/docs/vt102-ug/table5-13.html\n * A lot of curses apps use this if they see TERM=xterm.\n * testing: echo -e '\\e(0a\\e(B'\n * The xterm output sometimes seems to conflict with the\n * reference above. xterm seems in line with the reference\n * when running vttest however.\n * The table below now uses xterm's output from vttest.\n */\nCHARSETS['0'] = {\n '`': '\\u25c6', // '\u25C6'\n 'a': '\\u2592', // '\u2592'\n 'b': '\\u2409', // '\u2409' (HT)\n 'c': '\\u240c', // '\u240C' (FF)\n 'd': '\\u240d', // '\u240D' (CR)\n 'e': '\\u240a', // '\u240A' (LF)\n 'f': '\\u00b0', // '\u00B0'\n 'g': '\\u00b1', // '\u00B1'\n 'h': '\\u2424', // '\u2424' (NL)\n 'i': '\\u240b', // '\u240B' (VT)\n 'j': '\\u2518', // '\u2518'\n 'k': '\\u2510', // '\u2510'\n 'l': '\\u250c', // '\u250C'\n 'm': '\\u2514', // '\u2514'\n 'n': '\\u253c', // '\u253C'\n 'o': '\\u23ba', // '\u23BA'\n 'p': '\\u23bb', // '\u23BB'\n 'q': '\\u2500', // '\u2500'\n 'r': '\\u23bc', // '\u23BC'\n 's': '\\u23bd', // '\u23BD'\n 't': '\\u251c', // '\u251C'\n 'u': '\\u2524', // '\u2524'\n 'v': '\\u2534', // '\u2534'\n 'w': '\\u252c', // '\u252C'\n 'x': '\\u2502', // '\u2502'\n 'y': '\\u2264', // '\u2264'\n 'z': '\\u2265', // '\u2265'\n '{': '\\u03c0', // '\u03C0'\n '|': '\\u2260', // '\u2260'\n '}': '\\u00a3', // '\u00A3'\n '~': '\\u00b7' // '\u00B7'\n};\n\n/**\n * British character set\n * ESC (A\n * Reference: http://vt100.net/docs/vt220-rm/table2-5.html\n */\nCHARSETS['A'] = {\n '#': '\u00A3'\n};\n\n/**\n * United States character set\n * ESC (B\n */\nCHARSETS['B'] = undefined;\n\n/**\n * Dutch character set\n * ESC (4\n * Reference: http://vt100.net/docs/vt220-rm/table2-6.html\n */\nCHARSETS['4'] = {\n '#': '\u00A3',\n '@': '\u00BE',\n '[': 'ij',\n '\\\\': '\u00BD',\n ']': '|',\n '{': '\u00A8',\n '|': 'f',\n '}': '\u00BC',\n '~': '\u00B4'\n};\n\n/**\n * Finnish character set\n * ESC (C or ESC (5\n * Reference: http://vt100.net/docs/vt220-rm/table2-7.html\n */\nCHARSETS['C'] = CHARSETS['5'] = {\n '[': '\u00C4',\n '\\\\': '\u00D6',\n ']': '\u00C5',\n '^': '\u00DC',\n '`': '\u00E9',\n '{': '\u00E4',\n '|': '\u00F6',\n '}': '\u00E5',\n '~': '\u00FC'\n};\n\n/**\n * French character set\n * ESC (R\n * Reference: http://vt100.net/docs/vt220-rm/table2-8.html\n */\nCHARSETS['R'] = {\n '#': '\u00A3',\n '@': '\u00E0',\n '[': '\u00B0',\n '\\\\': '\u00E7',\n ']': '\u00A7',\n '{': '\u00E9',\n '|': '\u00F9',\n '}': '\u00E8',\n '~': '\u00A8'\n};\n\n/**\n * French Canadian character set\n * ESC (Q\n * Reference: http://vt100.net/docs/vt220-rm/table2-9.html\n */\nCHARSETS['Q'] = {\n '@': '\u00E0',\n '[': '\u00E2',\n '\\\\': '\u00E7',\n ']': '\u00EA',\n '^': '\u00EE',\n '`': '\u00F4',\n '{': '\u00E9',\n '|': '\u00F9',\n '}': '\u00E8',\n '~': '\u00FB'\n};\n\n/**\n * German character set\n * ESC (K\n * Reference: http://vt100.net/docs/vt220-rm/table2-10.html\n */\nCHARSETS['K'] = {\n '@': '\u00A7',\n '[': '\u00C4',\n '\\\\': '\u00D6',\n ']': '\u00DC',\n '{': '\u00E4',\n '|': '\u00F6',\n '}': '\u00FC',\n '~': '\u00DF'\n};\n\n/**\n * Italian character set\n * ESC (Y\n * Reference: http://vt100.net/docs/vt220-rm/table2-11.html\n */\nCHARSETS['Y'] = {\n '#': '\u00A3',\n '@': '\u00A7',\n '[': '\u00B0',\n '\\\\': '\u00E7',\n ']': '\u00E9',\n '`': '\u00F9',\n '{': '\u00E0',\n '|': '\u00F2',\n '}': '\u00E8',\n '~': '\u00EC'\n};\n\n/**\n * Norwegian/Danish character set\n * ESC (E or ESC (6\n * Reference: http://vt100.net/docs/vt220-rm/table2-12.html\n */\nCHARSETS['E'] = CHARSETS['6'] = {\n '@': '\u00C4',\n '[': '\u00C6',\n '\\\\': '\u00D8',\n ']': '\u00C5',\n '^': '\u00DC',\n '`': '\u00E4',\n '{': '\u00E6',\n '|': '\u00F8',\n '}': '\u00E5',\n '~': '\u00FC'\n};\n\n/**\n * Spanish character set\n * ESC (Z\n * Reference: http://vt100.net/docs/vt220-rm/table2-13.html\n */\nCHARSETS['Z'] = {\n '#': '\u00A3',\n '@': '\u00A7',\n '[': '\u00A1',\n '\\\\': '\u00D1',\n ']': '\u00BF',\n '{': '\u00B0',\n '|': '\u00F1',\n '}': '\u00E7'\n};\n\n/**\n * Swedish character set\n * ESC (H or ESC (7\n * Reference: http://vt100.net/docs/vt220-rm/table2-14.html\n */\nCHARSETS['H'] = CHARSETS['7'] = {\n '@': '\u00C9',\n '[': '\u00C4',\n '\\\\': '\u00D6',\n ']': '\u00C5',\n '^': '\u00DC',\n '`': '\u00E9',\n '{': '\u00E4',\n '|': '\u00F6',\n '}': '\u00E5',\n '~': '\u00FC'\n};\n\n/**\n * Swiss character set\n * ESC (=\n * Reference: http://vt100.net/docs/vt220-rm/table2-15.html\n */\nCHARSETS['='] = {\n '#': '\u00F9',\n '@': '\u00E0',\n '[': '\u00E9',\n '\\\\': '\u00E7',\n ']': '\u00EA',\n '^': '\u00EE',\n\n '_': '\u00E8',\n '`': '\u00F4',\n '{': '\u00E4',\n '|': '\u00F6',\n '}': '\u00FC',\n '~': '\u00FB'\n};\n", "/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { CircularList, IInsertEvent } from '../CircularList';\nimport { Disposable, toDisposable } from '../Lifecycle';\nimport { IdleTaskQueue } from '../TaskQueue';\nimport { ICharset } from '../Types';\nimport { IAttributeData, IBuffer, IBufferLine, ICellData } from './Types';\nimport { ExtendedAttrs } from './AttributeData';\nimport { BufferLine, DEFAULT_ATTR_DATA } from './BufferLine';\nimport { getWrappedLineTrimmedLength, reflowLargerApplyNewLayout, reflowLargerCreateNewLayout, reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths } from './BufferReflow';\nimport { CellData } from './CellData';\nimport { NULL_CELL_CHAR, NULL_CELL_CODE, NULL_CELL_WIDTH, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE, WHITESPACE_CELL_WIDTH } from './Constants';\nimport { Marker } from './Marker';\nimport { DEFAULT_CHARSET } from '../data/Charsets';\nimport { IBufferService, ILogService, IOptionsService } from '../services/Services';\n\nexport const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1\n\n/**\n * This class represents a terminal buffer (an internal state of the terminal), where the\n * following information is stored (in high-level):\n * - text content of this particular buffer\n * - cursor position\n * - scroll position\n */\nexport class Buffer extends Disposable implements IBuffer {\n public lines: CircularList;\n public ydisp: number = 0;\n public ybase: number = 0;\n public y: number = 0;\n public x: number = 0;\n public scrollBottom: number;\n public scrollTop: number;\n public tabs: { [column: number]: boolean | undefined } = {};\n public savedY: number = 0;\n public savedX: number = 0;\n public savedCurAttrData = DEFAULT_ATTR_DATA.clone();\n public savedCharset: ICharset | undefined = DEFAULT_CHARSET;\n public savedCharsets: (ICharset | undefined)[] = [];\n public savedGlevel: number = 0;\n public savedOriginMode: boolean = false;\n public savedWraparoundMode: boolean = true;\n public markers: Marker[] = [];\n private _nullCell: ICellData = CellData.fromCharData([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);\n private _whitespaceCell: ICellData = CellData.fromCharData([0, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_WIDTH, WHITESPACE_CELL_CODE]);\n private _cols: number;\n private _rows: number;\n private _isClearing: boolean = false;\n private _memoryCleanupQueue: InstanceType;\n private _memoryCleanupPosition = 0;\n\n constructor(\n private _hasScrollback: boolean,\n private _optionsService: IOptionsService,\n private _bufferService: IBufferService,\n private readonly _logService: ILogService\n ) {\n super();\n this._cols = this._bufferService.cols;\n this._rows = this._bufferService.rows;\n this.lines = new CircularList(this._getCorrectBufferLength(this._rows));\n this.scrollTop = 0;\n this.scrollBottom = this._rows - 1;\n this.setupTabStops();\n this._memoryCleanupQueue = new IdleTaskQueue(this._logService);\n this._register(toDisposable(() => this._memoryCleanupQueue.clear()));\n this._register(toDisposable(() => this.clearAllMarkers()));\n }\n\n public getNullCell(attr?: IAttributeData): ICellData {\n if (attr) {\n this._nullCell.fg = attr.fg;\n this._nullCell.bg = attr.bg;\n this._nullCell.extended = attr.extended;\n } else {\n this._nullCell.fg = 0;\n this._nullCell.bg = 0;\n this._nullCell.extended = new ExtendedAttrs();\n }\n return this._nullCell;\n }\n\n public getWhitespaceCell(attr?: IAttributeData): ICellData {\n if (attr) {\n this._whitespaceCell.fg = attr.fg;\n this._whitespaceCell.bg = attr.bg;\n this._whitespaceCell.extended = attr.extended;\n } else {\n this._whitespaceCell.fg = 0;\n this._whitespaceCell.bg = 0;\n this._whitespaceCell.extended = new ExtendedAttrs();\n }\n return this._whitespaceCell;\n }\n\n public getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine {\n return new BufferLine(this._bufferService.cols, this.getNullCell(attr), isWrapped);\n }\n\n public get hasScrollback(): boolean {\n return this._hasScrollback && this.lines.maxLength > this._rows;\n }\n\n public get isCursorInViewport(): boolean {\n const absoluteY = this.ybase + this.y;\n const relativeY = absoluteY - this.ydisp;\n return (relativeY >= 0 && relativeY < this._rows);\n }\n\n /**\n * Gets the correct buffer length based on the rows provided, the terminal's\n * scrollback and whether this buffer is flagged to have scrollback or not.\n * @param rows The terminal rows to use in the calculation.\n */\n private _getCorrectBufferLength(rows: number): number {\n if (!this._hasScrollback) {\n return rows;\n }\n\n const correctBufferLength = rows + this._optionsService.rawOptions.scrollback;\n\n return correctBufferLength > MAX_BUFFER_SIZE ? MAX_BUFFER_SIZE : correctBufferLength;\n }\n\n /**\n * Fills the buffer's viewport with blank lines.\n */\n public fillViewportRows(fillAttr?: IAttributeData): void {\n if (this.lines.length === 0) {\n fillAttr ??= DEFAULT_ATTR_DATA;\n let i = this._rows;\n while (i--) {\n this.lines.push(this.getBlankLine(fillAttr));\n }\n }\n }\n\n /**\n * Clears the buffer to its initial state, discarding all previous data.\n */\n public clear(): void {\n this.ydisp = 0;\n this.ybase = 0;\n this.y = 0;\n this.x = 0;\n this.lines = new CircularList(this._getCorrectBufferLength(this._rows));\n this.scrollTop = 0;\n this.scrollBottom = this._rows - 1;\n this.setupTabStops();\n }\n\n /**\n * Resizes the buffer, adjusting its data accordingly.\n * @param newCols The new number of columns.\n * @param newRows The new number of rows.\n */\n public resize(newCols: number, newRows: number): void {\n // store reference to null cell with default attrs\n const nullCell = this.getNullCell(DEFAULT_ATTR_DATA);\n\n // count bufferlines with overly big memory to be cleaned afterwards\n let dirtyMemoryLines = 0;\n\n // Increase max length if needed before adjustments to allow space to fill\n // as required.\n const newMaxLength = this._getCorrectBufferLength(newRows);\n if (newMaxLength > this.lines.maxLength) {\n this.lines.maxLength = newMaxLength;\n }\n\n // if (this._cols > newCols) {\n // console.log('increase!');\n // }\n\n // The following adjustments should only happen if the buffer has been\n // initialized/filled.\n if (this.lines.length > 0) {\n // Deal with columns increasing (reducing needs to happen after reflow)\n if (this._cols < newCols) {\n for (let i = 0; i < this.lines.length; i++) {\n // +boolean for fast 0 or 1 conversion\n dirtyMemoryLines += +this.lines.get(i)!.resize(newCols, nullCell);\n }\n }\n\n // Resize rows in both directions as needed\n let addToY = 0;\n if (this._rows < newRows) {\n for (let y = this._rows; y < newRows; y++) {\n if (this.lines.length < newRows + this.ybase) {\n if (this._optionsService.rawOptions.windowsPty.backend !== undefined || this._optionsService.rawOptions.windowsPty.buildNumber !== undefined) {\n // Just add the new missing rows on Windows as conpty reprints the screen with its\n // view of the world. Once a line enters scrollback for conpty it remains there\n this.lines.push(new BufferLine(newCols, nullCell, false));\n } else {\n if (this.ybase > 0 && this.lines.length <= this.ybase + this.y + addToY + 1) {\n // There is room above the buffer and there are no empty elements below the line,\n // scroll up\n this.ybase--;\n addToY++;\n if (this.ydisp > 0) {\n // Viewport is at the top of the buffer, must increase downwards\n this.ydisp--;\n }\n } else {\n // Add a blank line if there is no buffer left at the top to scroll to, or if there\n // are blank lines after the cursor\n this.lines.push(new BufferLine(newCols, nullCell, false));\n }\n }\n }\n }\n } else { // (this._rows >= newRows)\n for (let y = this._rows; y > newRows; y--) {\n if (this.lines.length > newRows + this.ybase) {\n if (this.lines.length > this.ybase + this.y + 1) {\n // The line is a blank line below the cursor, remove it\n this.lines.pop();\n } else {\n // The line is the cursor, scroll down\n this.ybase++;\n this.ydisp++;\n }\n }\n }\n }\n\n // Reduce max length if needed after adjustments, this is done after as it\n // would otherwise cut data from the bottom of the buffer.\n if (newMaxLength < this.lines.maxLength) {\n // Trim from the top of the buffer and adjust ybase and ydisp.\n const amountToTrim = this.lines.length - newMaxLength;\n if (amountToTrim > 0) {\n this.lines.trimStart(amountToTrim);\n this.ybase = Math.max(this.ybase - amountToTrim, 0);\n this.ydisp = Math.max(this.ydisp - amountToTrim, 0);\n this.savedY = Math.max(this.savedY - amountToTrim, 0);\n }\n this.lines.maxLength = newMaxLength;\n }\n\n // Make sure that the cursor stays on screen\n this.x = Math.min(this.x, newCols - 1);\n this.y = Math.min(this.y, newRows - 1);\n if (addToY) {\n this.y += addToY;\n }\n this.savedX = Math.min(this.savedX, newCols - 1);\n\n this.scrollTop = 0;\n }\n\n this.scrollBottom = newRows - 1;\n\n if (this._isReflowEnabled) {\n this._reflow(newCols, newRows);\n\n // Trim the end of the line off if cols shrunk\n if (this._cols > newCols) {\n for (let i = 0; i < this.lines.length; i++) {\n // +boolean for fast 0 or 1 conversion\n dirtyMemoryLines += +this.lines.get(i)!.resize(newCols, nullCell);\n }\n }\n }\n\n this._cols = newCols;\n this._rows = newRows;\n\n // Ensure the cursor position invariant: ybase + y must be within buffer bounds\n // This can be violated during reflow or when shrinking rows\n if (this.lines.length > 0) {\n const maxY = Math.max(0, this.lines.length - this.ybase - 1);\n this.y = Math.min(this.y, maxY);\n }\n\n this._memoryCleanupQueue.clear();\n // schedule memory cleanup only, if more than 10% of the lines are affected\n if (dirtyMemoryLines > 0.1 * this.lines.length) {\n this._memoryCleanupPosition = 0;\n this._memoryCleanupQueue.enqueue(() => this._batchedMemoryCleanup());\n }\n }\n\n private _batchedMemoryCleanup(): boolean {\n let normalRun = true;\n if (this._memoryCleanupPosition >= this.lines.length) {\n // cleanup made it once through all lines, thus rescan in loop below to also catch shifted\n // lines, which should finish rather quick if there are no more cleanups pending\n this._memoryCleanupPosition = 0;\n normalRun = false;\n }\n let counted = 0;\n while (this._memoryCleanupPosition < this.lines.length) {\n counted += this.lines.get(this._memoryCleanupPosition++)!.cleanupMemory();\n // cleanup max 100 lines per batch\n if (counted > 100) {\n return true;\n }\n }\n // normal runs always need another rescan afterwards\n // if we made it here with normalRun=false, we are in a final run\n // and can end the cleanup task for sure\n return normalRun;\n }\n\n private get _isReflowEnabled(): boolean {\n const windowsPty = this._optionsService.rawOptions.windowsPty;\n if (windowsPty && windowsPty.buildNumber) {\n return this._hasScrollback && windowsPty.backend === 'conpty' && windowsPty.buildNumber >= 21376;\n }\n return this._hasScrollback;\n }\n\n private _reflow(newCols: number, newRows: number): void {\n if (this._cols === newCols) {\n return;\n }\n\n // Iterate through rows, ignore the last one as it cannot be wrapped\n if (newCols > this._cols) {\n this._reflowLarger(newCols, newRows);\n } else {\n this._reflowSmaller(newCols, newRows);\n }\n }\n\n private _reflowLarger(newCols: number, newRows: number): void {\n const reflowCursorLine = this._optionsService.rawOptions.reflowCursorLine;\n const toRemove: number[] = reflowLargerGetLinesToRemove(this.lines, this._cols, newCols, this.ybase + this.y, this.getNullCell(DEFAULT_ATTR_DATA), reflowCursorLine);\n if (toRemove.length > 0) {\n const newLayoutResult = reflowLargerCreateNewLayout(this.lines, toRemove);\n reflowLargerApplyNewLayout(this.lines, newLayoutResult.layout);\n this._reflowLargerAdjustViewport(newCols, newRows, newLayoutResult.countRemoved);\n }\n }\n\n private _reflowLargerAdjustViewport(newCols: number, newRows: number, countRemoved: number): void {\n const nullCell = this.getNullCell(DEFAULT_ATTR_DATA);\n // Adjust viewport based on number of items removed\n let viewportAdjustments = countRemoved;\n while (viewportAdjustments-- > 0) {\n if (this.ybase === 0) {\n if (this.y > 0) {\n this.y--;\n }\n if (this.lines.length < newRows) {\n // Add an extra row at the bottom of the viewport\n this.lines.push(new BufferLine(newCols, nullCell, false));\n }\n } else {\n if (this.ydisp === this.ybase) {\n this.ydisp--;\n }\n this.ybase--;\n }\n }\n this.savedY = Math.max(this.savedY - countRemoved, 0);\n }\n\n private _reflowSmaller(newCols: number, newRows: number): void {\n const reflowCursorLine = this._optionsService.rawOptions.reflowCursorLine;\n const nullCell = this.getNullCell(DEFAULT_ATTR_DATA);\n // Gather all BufferLines that need to be inserted into the Buffer here so that they can be\n // batched up and only committed once\n const toInsert = [];\n let countToInsert = 0;\n // Go backwards as many lines may be trimmed and this will avoid considering them\n for (let y = this.lines.length - 1; y >= 0; y--) {\n // Check whether this line is a problem\n let nextLine = this.lines.get(y) as BufferLine;\n if (!nextLine || !nextLine.isWrapped && nextLine.getTrimmedLength() <= newCols) {\n continue;\n }\n\n // Gather wrapped lines and adjust y to be the starting line\n const wrappedLines: BufferLine[] = [nextLine];\n while (nextLine.isWrapped && y > 0) {\n nextLine = this.lines.get(--y) as BufferLine;\n wrappedLines.unshift(nextLine);\n }\n\n if (!reflowCursorLine) {\n // If these lines contain the cursor don't touch them, the program will handle fixing up\n // wrapped lines with the cursor\n const absoluteY = this.ybase + this.y;\n if (absoluteY >= y && absoluteY < y + wrappedLines.length) {\n continue;\n }\n }\n\n const lastLineLength = wrappedLines[wrappedLines.length - 1].getTrimmedLength();\n const destLineLengths = reflowSmallerGetNewLineLengths(wrappedLines, this._cols, newCols);\n const linesToAdd = destLineLengths.length - wrappedLines.length;\n let trimmedLines: number;\n if (this.ybase === 0 && this.y !== this.lines.length - 1) {\n // If the top section of the buffer is not yet filled\n trimmedLines = Math.max(0, this.y - this.lines.maxLength + linesToAdd);\n } else {\n trimmedLines = Math.max(0, this.lines.length - this.lines.maxLength + linesToAdd);\n }\n\n // Add the new lines\n const newLines: BufferLine[] = [];\n for (let i = 0; i < linesToAdd; i++) {\n const newLine = this.getBlankLine(DEFAULT_ATTR_DATA, true) as BufferLine;\n newLines.push(newLine);\n }\n if (newLines.length > 0) {\n toInsert.push({\n // countToInsert here gets the actual index, taking into account other inserted items.\n // using this we can iterate through the list forwards\n start: y + wrappedLines.length + countToInsert,\n newLines\n });\n countToInsert += newLines.length;\n }\n wrappedLines.push(...newLines);\n\n // Copy buffer data to new locations, this needs to happen backwards to do in-place\n let destLineIndex = destLineLengths.length - 1; // Math.floor(cellsNeeded / newCols);\n let destCol = destLineLengths[destLineIndex]; // cellsNeeded % newCols;\n if (destCol === 0) {\n destLineIndex--;\n destCol = destLineLengths[destLineIndex];\n }\n let srcLineIndex = wrappedLines.length - linesToAdd - 1;\n let srcCol = lastLineLength;\n while (srcLineIndex >= 0) {\n const cellsToCopy = Math.min(srcCol, destCol);\n if (wrappedLines[destLineIndex] === undefined) {\n // Sanity check that the line exists, this has been known to fail for an unknown reason\n // which would stop the reflow from happening if an exception would throw.\n break;\n }\n wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol - cellsToCopy, destCol - cellsToCopy, cellsToCopy, true);\n destCol -= cellsToCopy;\n if (destCol === 0) {\n destLineIndex--;\n destCol = destLineLengths[destLineIndex];\n }\n srcCol -= cellsToCopy;\n if (srcCol === 0) {\n srcLineIndex--;\n const wrappedLinesIndex = Math.max(srcLineIndex, 0);\n srcCol = getWrappedLineTrimmedLength(wrappedLines, wrappedLinesIndex, this._cols);\n }\n }\n\n // Null out the end of the line ends if a wide character wrapped to the following line\n for (let i = 0; i < wrappedLines.length; i++) {\n if (destLineLengths[i] < newCols) {\n wrappedLines[i].setCell(destLineLengths[i], nullCell);\n }\n }\n\n // Adjust viewport as needed\n let viewportAdjustments = linesToAdd - trimmedLines;\n while (viewportAdjustments-- > 0) {\n if (this.ybase === 0) {\n if (this.y < newRows - 1) {\n this.y++;\n this.lines.pop();\n } else {\n this.ybase++;\n this.ydisp++;\n }\n } else {\n // Ensure ybase does not exceed its maximum value\n if (this.ybase < Math.min(this.lines.maxLength, this.lines.length + countToInsert) - newRows) {\n if (this.ybase === this.ydisp) {\n this.ydisp++;\n }\n this.ybase++;\n }\n }\n }\n this.savedY = Math.min(this.savedY + linesToAdd, this.ybase + newRows - 1);\n }\n\n // Rearrange lines in the buffer if there are any insertions, this is done at the end rather\n // than earlier so that it's a single O(n) pass through the buffer, instead of O(n^2) from many\n // costly calls to CircularList.splice.\n if (toInsert.length > 0) {\n // Record buffer insert events and then play them back backwards so that the indexes are\n // correct\n const insertEvents: IInsertEvent[] = [];\n\n // Record original lines so they don't get overridden when we rearrange the list\n const originalLines: BufferLine[] = [];\n for (let i = 0; i < this.lines.length; i++) {\n originalLines.push(this.lines.get(i) as BufferLine);\n }\n const originalLinesLength = this.lines.length;\n\n let originalLineIndex = originalLinesLength - 1;\n let nextToInsertIndex = 0;\n let nextToInsert = toInsert[nextToInsertIndex];\n this.lines.length = Math.min(this.lines.maxLength, this.lines.length + countToInsert);\n let countInsertedSoFar = 0;\n for (let i = Math.min(this.lines.maxLength - 1, originalLinesLength + countToInsert - 1); i >= 0; i--) {\n if (nextToInsert && nextToInsert.start > originalLineIndex + countInsertedSoFar) {\n // Insert extra lines here, adjusting i as needed\n for (let nextI = nextToInsert.newLines.length - 1; nextI >= 0; nextI--) {\n this.lines.set(i--, nextToInsert.newLines[nextI]);\n }\n i++;\n\n // Create insert events for later\n insertEvents.push({\n index: originalLineIndex + 1,\n amount: nextToInsert.newLines.length\n });\n\n countInsertedSoFar += nextToInsert.newLines.length;\n nextToInsert = toInsert[++nextToInsertIndex];\n } else {\n this.lines.set(i, originalLines[originalLineIndex--]);\n }\n }\n\n // Update markers\n let insertCountEmitted = 0;\n for (let i = insertEvents.length - 1; i >= 0; i--) {\n insertEvents[i].index += insertCountEmitted;\n this.lines.onInsertEmitter.fire(insertEvents[i]);\n insertCountEmitted += insertEvents[i].amount;\n }\n const amountToTrim = Math.max(0, originalLinesLength + countToInsert - this.lines.maxLength);\n if (amountToTrim > 0) {\n this.lines.onTrimEmitter.fire(amountToTrim);\n }\n }\n }\n\n /**\n * Translates a buffer line to a string, with optional start and end columns.\n * Wide characters will count as two columns in the resulting string. This\n * function is useful for getting the actual text underneath the raw selection\n * position.\n * @param lineIndex The absolute index of the line being translated.\n * @param trimRight Whether to trim whitespace to the right.\n * @param startCol The column to start at.\n * @param endCol The column to end at.\n */\n public translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol: number = 0, endCol?: number): string {\n const line = this.lines.get(lineIndex);\n if (!line) {\n return '';\n }\n return line.translateToString(trimRight, startCol, endCol);\n }\n\n public getWrappedRangeForLine(y: number): { first: number, last: number } {\n let first = y;\n let last = y;\n // Scan upwards for wrapped lines\n while (first > 0 && this.lines.get(first)!.isWrapped) {\n first--;\n }\n // Scan downwards for wrapped lines\n while (last + 1 < this.lines.length && this.lines.get(last + 1)!.isWrapped) {\n last++;\n }\n return { first, last };\n }\n\n /**\n * Setup the tab stops.\n * @param i The index to start setting up tab stops from.\n */\n public setupTabStops(i?: number): void {\n if (i !== null && i !== undefined) {\n if (!this.tabs[i]) {\n i = this.prevStop(i);\n }\n } else {\n this.tabs = {};\n i = 0;\n }\n\n for (; i < this._cols; i += this._optionsService.rawOptions.tabStopWidth) {\n this.tabs[i] = true;\n }\n }\n\n /**\n * Move the cursor to the previous tab stop from the given position (default is current).\n * @param x The position to move the cursor to the previous tab stop.\n */\n public prevStop(x?: number): number {\n x ??= this.x;\n while (!this.tabs[--x] && x > 0);\n return x >= this._cols ? this._cols - 1 : x < 0 ? 0 : x;\n }\n\n /**\n * Move the cursor one tab stop forward from the given position (default is current).\n * @param x The position to move the cursor one tab stop forward.\n */\n public nextStop(x?: number): number {\n x ??= this.x;\n while (!this.tabs[++x] && x < this._cols);\n return x >= this._cols ? this._cols - 1 : x < 0 ? 0 : x;\n }\n\n /**\n * Clears markers on single line.\n * @param y The line to clear.\n */\n public clearMarkers(y: number): void {\n this._isClearing = true;\n for (let i = 0; i < this.markers.length; i++) {\n if (this.markers[i].line === y) {\n this.markers[i].dispose();\n this.markers.splice(i--, 1);\n }\n }\n this._isClearing = false;\n }\n\n /**\n * Clears markers on all lines\n */\n public clearAllMarkers(): void {\n this._isClearing = true;\n for (let i = 0; i < this.markers.length; i++) {\n this.markers[i].dispose();\n }\n this.markers.length = 0;\n this._isClearing = false;\n }\n\n public addMarker(y: number): Marker {\n const marker = new Marker(y);\n this.markers.push(marker);\n marker.register(this.lines.onTrim(amount => {\n marker.line -= amount;\n // The marker should be disposed when the line is trimmed from the buffer\n if (marker.line < 0) {\n marker.dispose();\n }\n }));\n marker.register(this.lines.onInsert(event => {\n if (marker.line >= event.index) {\n marker.line += event.amount;\n }\n }));\n marker.register(this.lines.onDelete(event => {\n // Delete the marker if it's within the range\n if (marker.line >= event.index && marker.line < event.index + event.amount) {\n marker.dispose();\n }\n\n // Shift the marker if it's after the deleted range\n if (marker.line > event.index) {\n marker.line -= event.amount;\n }\n }));\n marker.register(marker.onDispose(() => this._removeMarker(marker)));\n return marker;\n }\n\n private _removeMarker(marker: Marker): void {\n if (!this._isClearing) {\n this.markers.splice(this.markers.indexOf(marker), 1);\n }\n }\n}\n", "/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { Disposable, MutableDisposable } from '../Lifecycle';\nimport { Buffer } from './Buffer';\nimport { IAttributeData, IBuffer, IBufferSet } from './Types';\nimport { IBufferService, ILogService, IOptionsService } from '../services/Services';\nimport { Emitter } from '../Event';\n\n/**\n * The BufferSet represents the set of two buffers used by xterm terminals (normal and alt) and\n * provides also utilities for working with them.\n */\nexport class BufferSet extends Disposable implements IBufferSet {\n private _normal!: Buffer;\n private _alt!: Buffer;\n private _activeBuffer!: Buffer;\n private readonly _normalBuffer = this._register(new MutableDisposable());\n private readonly _altBuffer = this._register(new MutableDisposable());\n\n private readonly _onBufferActivate = this._register(new Emitter<{ activeBuffer: IBuffer, inactiveBuffer: IBuffer }>());\n public readonly onBufferActivate = this._onBufferActivate.event;\n\n /**\n * Create a new BufferSet for the given terminal.\n */\n constructor(\n private readonly _optionsService: IOptionsService,\n private readonly _bufferService: IBufferService,\n private readonly _logService: ILogService\n ) {\n super();\n this.reset();\n this._register(this._optionsService.onSpecificOptionChange('scrollback', () => this.resize(this._bufferService.cols, this._bufferService.rows)));\n this._register(this._optionsService.onSpecificOptionChange('tabStopWidth', () => this.setupTabStops()));\n }\n\n public reset(): void {\n this._normal = new Buffer(true, this._optionsService, this._bufferService, this._logService);\n this._normalBuffer.value = this._normal;\n this._normal.fillViewportRows();\n\n // The alt buffer should never have scrollback.\n // See http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-The-Alternate-Screen-Buffer\n this._alt = new Buffer(false, this._optionsService, this._bufferService, this._logService);\n this._altBuffer.value = this._alt;\n this._activeBuffer = this._normal;\n this._onBufferActivate.fire({\n activeBuffer: this._normal,\n inactiveBuffer: this._alt\n });\n\n this.setupTabStops();\n }\n\n /**\n * Returns the alt Buffer of the BufferSet\n */\n public get alt(): Buffer {\n return this._alt;\n }\n\n /**\n * Returns the currently active Buffer of the BufferSet\n */\n public get active(): Buffer {\n return this._activeBuffer;\n }\n\n /**\n * Returns the normal Buffer of the BufferSet\n */\n public get normal(): Buffer {\n return this._normal;\n }\n\n /**\n * Sets the normal Buffer of the BufferSet as its currently active Buffer\n */\n public activateNormalBuffer(): void {\n if (this._activeBuffer === this._normal) {\n return;\n }\n this._normal.x = this._alt.x;\n this._normal.y = this._alt.y;\n // The alt buffer should always be cleared when we switch to the normal\n // buffer. This frees up memory since the alt buffer should always be new\n // when activated.\n this._alt.clearAllMarkers();\n this._alt.clear();\n this._activeBuffer = this._normal;\n this._onBufferActivate.fire({\n activeBuffer: this._normal,\n inactiveBuffer: this._alt\n });\n }\n\n /**\n * Sets the alt Buffer of the BufferSet as its currently active Buffer\n */\n public activateAltBuffer(fillAttr?: IAttributeData): void {\n if (this._activeBuffer === this._alt) {\n return;\n }\n // Since the alt buffer is always cleared when the normal buffer is\n // activated, we want to fill it when switching to it.\n this._alt.fillViewportRows(fillAttr);\n this._alt.x = this._normal.x;\n this._alt.y = this._normal.y;\n this._activeBuffer = this._alt;\n this._onBufferActivate.fire({\n activeBuffer: this._alt,\n inactiveBuffer: this._normal\n });\n }\n\n /**\n * Resizes both normal and alt buffers, adjusting their data accordingly.\n * @param newCols The new number of columns.\n * @param newRows The new number of rows.\n */\n public resize(newCols: number, newRows: number): void {\n this._normal.resize(newCols, newRows);\n this._alt.resize(newCols, newRows);\n this.setupTabStops(newCols);\n }\n\n /**\n * Setup the tab stops.\n * @param i The index to start setting up tab stops from.\n */\n public setupTabStops(i?: number): void {\n this._normal.setupTabStops(i);\n this._alt.setupTabStops(i);\n }\n}\n", "/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { Disposable } from '../Lifecycle';\nimport { IAttributeData, IBuffer, IBufferLine, IBufferSet } from '../buffer/Types';\nimport { BufferSet } from '../buffer/BufferSet';\nimport { IBufferService, ILogService, IOptionsService, type IBufferResizeEvent } from './Services';\nimport { Emitter } from '../Event';\n\nexport const enum BufferServiceConstants {\n MINIMUM_COLS = 2, // Less than 2 can mess with wide chars\n MINIMUM_ROWS = 1\n}\n\nexport class BufferService extends Disposable implements IBufferService {\n public serviceBrand: any;\n\n public cols: number;\n public rows: number;\n public buffers: IBufferSet;\n /** Whether the user is scrolling (locks the scroll position) */\n public isUserScrolling: boolean = false;\n\n private readonly _onResize = this._register(new Emitter());\n public readonly onResize = this._onResize.event;\n private readonly _onScroll = this._register(new Emitter());\n public readonly onScroll = this._onScroll.event;\n\n public get buffer(): IBuffer { return this.buffers.active; }\n\n /** An IBufferline to clone/copy from for new blank lines */\n private _cachedBlankLine: IBufferLine | undefined;\n\n constructor(\n @IOptionsService optionsService: IOptionsService,\n @ILogService logService: ILogService\n ) {\n super();\n this.cols = Math.max(optionsService.rawOptions.cols || 0, BufferServiceConstants.MINIMUM_COLS);\n this.rows = Math.max(optionsService.rawOptions.rows || 0, BufferServiceConstants.MINIMUM_ROWS);\n this.buffers = this._register(new BufferSet(optionsService, this, logService));\n this._register(this.buffers.onBufferActivate(e => {\n this._onScroll.fire(e.activeBuffer.ydisp);\n }));\n }\n\n public resize(cols: number, rows: number): void {\n const colsChanged = this.cols !== cols;\n const rowsChanged = this.rows !== rows;\n this.cols = cols;\n this.rows = rows;\n this.buffers.resize(cols, rows);\n this._onResize.fire({ cols, rows, colsChanged, rowsChanged });\n }\n\n public reset(): void {\n this.buffers.reset();\n this.isUserScrolling = false;\n }\n\n /**\n * Scroll the terminal down 1 row, creating a blank line.\n * @param eraseAttr The attribute data to use the for blank line.\n * @param isWrapped Whether the new line is wrapped from the previous line.\n */\n public scroll(eraseAttr: IAttributeData, isWrapped: boolean = false): void {\n const buffer = this.buffer;\n\n let newLine: IBufferLine | undefined;\n newLine = this._cachedBlankLine;\n if (!newLine || newLine.length !== this.cols || newLine.getFg(0) !== eraseAttr.fg || newLine.getBg(0) !== eraseAttr.bg) {\n newLine = buffer.getBlankLine(eraseAttr, isWrapped);\n this._cachedBlankLine = newLine;\n }\n newLine.isWrapped = isWrapped;\n\n const topRow = buffer.ybase + buffer.scrollTop;\n const bottomRow = buffer.ybase + buffer.scrollBottom;\n\n if (buffer.scrollTop === 0) {\n // Determine whether the buffer is going to be trimmed after insertion.\n const willBufferBeTrimmed = buffer.lines.isFull;\n\n // Insert the line using the fastest method\n if (bottomRow === buffer.lines.length - 1) {\n if (willBufferBeTrimmed) {\n buffer.lines.recycle().copyFrom(newLine, true);\n } else {\n buffer.lines.push(newLine.clone(true));\n }\n } else {\n buffer.lines.splice(bottomRow + 1, 0, newLine.clone(true));\n }\n\n // Only adjust ybase and ydisp when the buffer is not trimmed\n if (!willBufferBeTrimmed) {\n buffer.ybase++;\n // Only scroll the ydisp with ybase if the user has not scrolled up\n if (!this.isUserScrolling) {\n buffer.ydisp++;\n }\n } else {\n // When the buffer is full and the user has scrolled up, keep the text\n // stable unless ydisp is right at the top\n if (this.isUserScrolling) {\n buffer.ydisp = Math.max(buffer.ydisp - 1, 0);\n }\n }\n } else {\n // scrollTop is non-zero which means no line will be going to the\n // scrollback, instead we can just shift them in-place.\n const scrollRegionHeight = bottomRow - topRow + 1 /* as it's zero-based */;\n buffer.lines.shiftElements(topRow + 1, scrollRegionHeight - 1, -1);\n buffer.lines.set(bottomRow, newLine.clone(true));\n }\n\n // Move the viewport to the bottom of the buffer unless the user is\n // scrolling.\n if (!this.isUserScrolling) {\n buffer.ydisp = buffer.ybase;\n }\n\n this._onScroll.fire(buffer.ydisp);\n }\n\n /**\n * Scroll the display of the terminal\n * @param disp The number of lines to scroll down (negative scroll up).\n * @param suppressScrollEvent Don't emit the scroll event as scrollLines. This is used\n * to avoid unwanted events being handled by the viewport when the event was triggered from the\n * viewport originally.\n */\n public scrollLines(disp: number, suppressScrollEvent?: boolean): void {\n const buffer = this.buffer;\n if (disp < 0) {\n if (buffer.ydisp === 0) {\n return;\n }\n this.isUserScrolling = true;\n } else if (disp + buffer.ydisp >= buffer.ybase) {\n this.isUserScrolling = false;\n }\n\n const oldYdisp = buffer.ydisp;\n buffer.ydisp = Math.max(Math.min(buffer.ydisp + disp, buffer.ybase), 0);\n\n // No change occurred, don't trigger scroll/refresh\n if (oldYdisp === buffer.ydisp) {\n return;\n }\n\n if (!suppressScrollEvent) {\n this._onScroll.fire(buffer.ydisp);\n }\n }\n}\n", "/**\n * Copyright (c) 2016 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\ninterface INavigator {\n userAgent: string;\n language: string;\n platform: string;\n}\n\n// We're declaring a navigator global here as we expect it in all runtimes (node and browser), but\n// we want this module to live in common.\ndeclare const navigator: INavigator;\ndeclare const process: unknown;\n\n// navigator.userAgent is also checked here because bundling with the process module can cause\n// issues otherwise. Note that navigator exists in Node.js 21+ but the userAgent is\n// \"Node.js/\".\nexport const isNode = (typeof process !== 'undefined' && 'title' in (process as any) && (typeof navigator === 'undefined' || navigator.userAgent.startsWith('Node.js/'))) ? true : false;\nconst userAgent = (isNode) ? 'node' : navigator.userAgent;\nconst platform = (isNode) ? 'node' : navigator.platform;\n\nexport const isFirefox = userAgent.includes('Firefox');\nexport const isChrome = userAgent.includes('Chrome');\nexport const isLegacyEdge = userAgent.includes('Edge');\nexport const isSafari = /^((?!chrome|android).)*safari/i.test(userAgent);\n\ninterface IZoomWindow {\n devicePixelRatio?: number;\n}\n\nexport function getZoomFactor(_targetWindow: IZoomWindow): number {\n return 1;\n}\nexport function getSafariVersion(): number {\n if (!isSafari) {\n return 0;\n }\n const majorVersion = userAgent.match(/Version\\/(\\d+)/);\n if (majorVersion === null || majorVersion.length < 2) {\n return 0;\n }\n return parseInt(majorVersion[1], 10);\n}\n\n// Find the user's platform. We use this to interpret the meta key\n// and ISO third level shifts.\n// http://stackoverflow.com/q/19877924/577598\nexport const isMac = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'].includes(platform);\nexport const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].includes(platform);\nexport const isLinux = platform.indexOf('Linux') >= 0;\n// Note that when this is true, isLinux will also be true.\nexport const isChromeOS = /\\bCrOS\\b/.test(userAgent);\n", "/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { Disposable, toDisposable } from '../Lifecycle';\nimport { isMac } from '../Platform';\nimport { CursorStyle, IDisposable } from '../Types';\nimport { FontWeight, IOptionsService, ITerminalOptions } from './Services';\nimport { Emitter } from '../Event';\n\nexport const DEFAULT_OPTIONS: Readonly> = {\n cols: 80,\n rows: 24,\n showCursorImmediately: false,\n cursorBlink: false,\n blinkIntervalDuration: 0,\n cursorStyle: 'block',\n cursorWidth: 1,\n cursorInactiveStyle: 'outline',\n drawBoldTextInBrightColors: true,\n documentOverride: null,\n fastScrollSensitivity: 5,\n fontFamily: 'monospace',\n fontSize: 15,\n fontWeight: 'normal',\n fontWeightBold: 'bold',\n ignoreBracketedPasteMode: false,\n lineHeight: 1.0,\n letterSpacing: 0,\n linkHandler: null,\n logLevel: 'info',\n logger: null,\n scrollback: 1000,\n scrollbar: { showScrollbar: true },\n scrollOnEraseInDisplay: false,\n scrollOnUserInput: true,\n scrollSensitivity: 1,\n screenReaderMode: false,\n smoothScrollDuration: 0,\n macOptionIsMeta: false,\n macOptionClickForcesSelection: false,\n minimumContrastRatio: 1,\n mouseEventsRequireAlt: false,\n disableStdin: false,\n allowProposedApi: false,\n allowTransparency: false,\n tabStopWidth: 8,\n theme: {},\n reflowCursorLine: false,\n rescaleOverlappingGlyphs: false,\n rightClickSelectsWord: isMac,\n windowOptions: {},\n windowsPty: {},\n wordSeparator: ' ()[]{}\\',\"`',\n altClickMovesCursor: true,\n convertEol: false,\n termName: 'xterm',\n quirks: {},\n vtExtensions: {}\n};\n\nconst FONT_WEIGHT_OPTIONS: Extract[] = ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'];\n\nexport class OptionsService extends Disposable implements IOptionsService {\n public serviceBrand: any;\n\n public readonly rawOptions: Required;\n public options: Required;\n\n private readonly _onOptionChange = this._register(new Emitter());\n public readonly onOptionChange = this._onOptionChange.event;\n\n constructor(options: Partial) {\n super();\n // set the default value of each option\n const defaultOptions = { ...DEFAULT_OPTIONS };\n for (const key in options) {\n if (key in defaultOptions) {\n try {\n const newValue = options[key];\n defaultOptions[key] = this._sanitizeAndValidateOption(key, newValue);\n } catch (e) {\n console.error(e);\n }\n }\n }\n\n // set up getters and setters for each option\n this.rawOptions = defaultOptions;\n this.options = { ... defaultOptions };\n this._setupOptions();\n\n // Clear out options that could link outside xterm.js as they could easily cause an embedder\n // memory leak\n this._register(toDisposable(() => {\n this.rawOptions.linkHandler = null;\n this.rawOptions.documentOverride = null;\n }));\n }\n\n // eslint-disable-next-line @typescript-eslint/naming-convention\n public onSpecificOptionChange(key: T, listener: (value: ITerminalOptions[T]) => any): IDisposable {\n return this.onOptionChange(eventKey => {\n if (eventKey === key) {\n listener(this.rawOptions[key]);\n }\n });\n }\n\n // eslint-disable-next-line @typescript-eslint/naming-convention\n public onMultipleOptionChange(keys: (keyof ITerminalOptions)[], listener: () => any): IDisposable {\n return this.onOptionChange(eventKey => {\n if (keys.indexOf(eventKey) !== -1) {\n listener();\n }\n });\n }\n\n private _setupOptions(): void {\n const getter = (propName: string): any => {\n if (!(propName in DEFAULT_OPTIONS)) {\n throw new Error(`No option with key \"${propName}\"`);\n }\n return this.rawOptions[propName];\n };\n\n const setter = (propName: string, value: any): void => {\n if (!(propName in DEFAULT_OPTIONS)) {\n throw new Error(`No option with key \"${propName}\"`);\n }\n\n value = this._sanitizeAndValidateOption(propName, value);\n // Don't fire an option change event if they didn't change\n if (this.rawOptions[propName] !== value) {\n this.rawOptions[propName] = value;\n this._onOptionChange.fire(propName);\n }\n };\n\n for (const propName in this.rawOptions) {\n const desc = {\n get: getter.bind(this, propName),\n set: setter.bind(this, propName)\n };\n Object.defineProperty(this.options, propName, desc);\n }\n }\n\n private _sanitizeAndValidateOption(key: string, value: any): any {\n switch (key) {\n case 'cursorStyle':\n if (!value) {\n value = DEFAULT_OPTIONS[key];\n }\n if (!isCursorStyle(value)) {\n throw new Error(`\"${value}\" is not a valid value for ${key}`);\n }\n break;\n case 'wordSeparator':\n if (!value) {\n value = DEFAULT_OPTIONS[key];\n }\n break;\n case 'fontWeight':\n case 'fontWeightBold':\n if (typeof value === 'number' && 1 <= value && value <= 1000) {\n // already valid numeric value\n break;\n }\n value = FONT_WEIGHT_OPTIONS.includes(value) ? value : DEFAULT_OPTIONS[key];\n break;\n case 'blinkIntervalDuration':\n value = Math.floor(value);\n if (value < 0) {\n throw new Error(`${key} cannot be less than 0, value: ${value}`);\n }\n break;\n case 'cursorWidth':\n value = Math.floor(value);\n // Fall through for bounds check\n case 'lineHeight':\n case 'tabStopWidth':\n if (value < 1) {\n throw new Error(`${key} cannot be less than 1, value: ${value}`);\n }\n break;\n case 'minimumContrastRatio':\n value = Math.max(1, Math.min(21, Math.round(value * 10) / 10));\n break;\n case 'scrollback':\n value = Math.min(value, 4294967295);\n if (value < 0) {\n throw new Error(`${key} cannot be less than 0, value: ${value}`);\n }\n break;\n case 'fastScrollSensitivity':\n case 'scrollSensitivity':\n if (value <= 0) {\n throw new Error(`${key} cannot be less than or equal to 0, value: ${value}`);\n }\n break;\n case 'rows':\n case 'cols':\n if (!value && value !== 0) {\n throw new Error(`${key} must be numeric, value: ${value}`);\n }\n break;\n case 'windowsPty':\n value = value ?? {};\n break;\n }\n return value;\n }\n}\n\nfunction isCursorStyle(value: unknown): value is CursorStyle {\n return value === 'block' || value === 'underline' || value === 'bar';\n}\n", "/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { Disposable } from '../Lifecycle';\nimport { IDecPrivateModes, IKittyKeyboardState, IModes } from '../Types';\nimport { IBufferService, ICoreService, ILogService, IOptionsService } from './Services';\nimport { Emitter } from '../Event';\n\nconst DEFAULT_MODES: IModes = Object.freeze({\n insertMode: false\n});\n\nconst DEFAULT_DEC_PRIVATE_MODES: IDecPrivateModes = Object.freeze({\n applicationCursorKeys: false,\n applicationKeypad: false,\n bracketedPasteMode: false,\n colorSchemeUpdates: false,\n cursorBlink: undefined,\n cursorStyle: undefined,\n origin: false,\n reverseWraparound: false,\n sendFocus: false,\n synchronizedOutput: false,\n win32InputMode: false,\n wraparound: true // defaults: xterm - true, vt100 - false\n});\n\nconst DEFAULT_KITTY_KEYBOARD_STATE = (): IKittyKeyboardState => ({\n flags: 0,\n mainFlags: 0,\n altFlags: 0,\n mainStack: [],\n altStack: []\n});\n\nexport class CoreService extends Disposable implements ICoreService {\n public serviceBrand: any;\n\n public isCursorInitialized: boolean;\n public isCursorHidden: boolean = false;\n public modes: IModes;\n public decPrivateModes: IDecPrivateModes;\n public kittyKeyboard: IKittyKeyboardState;\n\n private readonly _onData = this._register(new Emitter());\n public readonly onData = this._onData.event;\n private readonly _onUserInput = this._register(new Emitter());\n public readonly onUserInput = this._onUserInput.event;\n private readonly _onBinary = this._register(new Emitter());\n public readonly onBinary = this._onBinary.event;\n private readonly _onRequestScrollToBottom = this._register(new Emitter());\n public readonly onRequestScrollToBottom = this._onRequestScrollToBottom.event;\n\n constructor(\n @IBufferService private readonly _bufferService: IBufferService,\n @ILogService private readonly _logService: ILogService,\n @IOptionsService private readonly _optionsService: IOptionsService\n ) {\n super();\n this.isCursorInitialized = _optionsService.rawOptions.showCursorImmediately ?? false;\n this.modes = structuredClone(DEFAULT_MODES);\n this.decPrivateModes = structuredClone(DEFAULT_DEC_PRIVATE_MODES);\n this.kittyKeyboard = DEFAULT_KITTY_KEYBOARD_STATE();\n }\n\n public reset(): void {\n this.modes = structuredClone(DEFAULT_MODES);\n this.decPrivateModes = structuredClone(DEFAULT_DEC_PRIVATE_MODES);\n this.kittyKeyboard = DEFAULT_KITTY_KEYBOARD_STATE();\n }\n\n public triggerDataEvent(data: string, wasUserInput: boolean = false): void {\n // Prevents all events to pty process if stdin is disabled\n if (this._optionsService.rawOptions.disableStdin) {\n return;\n }\n\n // Input is being sent to the terminal, the terminal should focus the prompt.\n const buffer = this._bufferService.buffer;\n if (wasUserInput && this._optionsService.rawOptions.scrollOnUserInput && buffer.ybase !== buffer.ydisp) {\n this._onRequestScrollToBottom.fire();\n }\n\n // Fire onUserInput so listeners can react as well (eg. clear selection)\n if (wasUserInput) {\n this._onUserInput.fire();\n }\n\n // Fire onData API\n this._logService.debug(`sending data \"${data}\"`);\n this._logService.trace(`sending data (codes)`, () => data.split('').map(e => e.charCodeAt(0)));\n this._onData.fire(data);\n }\n\n public triggerBinaryEvent(data: string): void {\n if (this._optionsService.rawOptions.disableStdin) {\n return;\n }\n this._logService.debug(`sending binary \"${data}\"`);\n this._logService.trace(`sending binary (codes)`, () => data.split('').map(e => e.charCodeAt(0)));\n this._onBinary.fire(data);\n }\n}\n", "/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\nimport { IMouseStateService } from './Services';\nimport { ICoreMouseProtocol, ICoreMouseEvent, CoreMouseEncoding, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from '../Types';\nimport { Disposable } from '../Lifecycle';\nimport { Emitter } from '../Event';\n\n/**\n * Supported default protocols.\n */\nconst DEFAULT_PROTOCOLS: { [key: string]: ICoreMouseProtocol } = {\n /**\n * NONE\n * Events: none\n * Modifiers: none\n */\n NONE: {\n events: CoreMouseEventType.NONE,\n restrict: () => false\n },\n /**\n * X10\n * Events: mousedown\n * Modifiers: none\n */\n X10: {\n events: CoreMouseEventType.DOWN,\n restrict: (e: ICoreMouseEvent) => {\n // no wheel, no move, no up\n if (e.button === CoreMouseButton.WHEEL || e.action !== CoreMouseAction.DOWN) {\n return false;\n }\n // no modifiers\n e.ctrl = false;\n e.alt = false;\n e.shift = false;\n return true;\n }\n },\n /**\n * VT200\n * Events: mousedown / mouseup / wheel\n * Modifiers: all\n */\n VT200: {\n events: CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL,\n restrict: (e: ICoreMouseEvent) => {\n // no move\n if (e.action === CoreMouseAction.MOVE) {\n return false;\n }\n return true;\n }\n },\n /**\n * DRAG\n * Events: mousedown / mouseup / wheel / mousedrag\n * Modifiers: all\n */\n DRAG: {\n events: CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL | CoreMouseEventType.DRAG,\n restrict: (e: ICoreMouseEvent) => {\n // no move without button\n if (e.action === CoreMouseAction.MOVE && e.button === CoreMouseButton.NONE) {\n return false;\n }\n return true;\n }\n },\n /**\n * ANY\n * Events: all mouse related events\n * Modifiers: all\n */\n ANY: {\n events:\n CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL\n | CoreMouseEventType.DRAG | CoreMouseEventType.MOVE,\n restrict: (e: ICoreMouseEvent) => true\n }\n};\n\nconst enum Modifiers {\n SHIFT = 4,\n ALT = 8,\n CTRL = 16\n}\n\n// helper for default encoders to generate the event code.\nfunction eventCode(e: ICoreMouseEvent, isSGR: boolean): number {\n let code = (e.ctrl ? Modifiers.CTRL : 0) | (e.shift ? Modifiers.SHIFT : 0) | (e.alt ? Modifiers.ALT : 0);\n if (e.button === CoreMouseButton.WHEEL) {\n code |= 64;\n code |= e.action;\n } else {\n code |= e.button & 3;\n if (e.button & 4) {\n code |= 64;\n }\n if (e.button & 8) {\n code |= 128;\n }\n if (e.action === CoreMouseAction.MOVE) {\n code |= CoreMouseAction.MOVE;\n } else if (e.action === CoreMouseAction.UP && !isSGR) {\n // special case - only SGR can report button on release\n // all others have to go with NONE\n code |= CoreMouseButton.NONE;\n }\n }\n return code;\n}\n\nconst S = String.fromCharCode;\n\n/**\n * Supported default encodings.\n */\nconst DEFAULT_ENCODINGS: { [key: string]: CoreMouseEncoding } = {\n /**\n * DEFAULT - CSI M Pb Px Py\n * Single byte encoding for coords and event code.\n * Can encode values up to 223 (1-based).\n */\n DEFAULT: (e: ICoreMouseEvent) => {\n const params = [eventCode(e, false) + 32, e.col + 32, e.row + 32];\n // supress mouse report if we exceed addressible range\n // Note this is handled differently by emulators\n // - xterm: sends 0;0 coords instead\n // - vte, konsole: no report\n if (params[0] > 255 || params[1] > 255 || params[2] > 255) {\n return '';\n }\n return `\\x1b[M${S(params[0])}${S(params[1])}${S(params[2])}`;\n },\n /**\n * SGR - CSI < Pb ; Px ; Py M|m\n * No encoding limitation.\n * Can report button on release and works with a well formed sequence.\n */\n SGR: (e: ICoreMouseEvent) => {\n const final = (e.action === CoreMouseAction.UP && e.button !== CoreMouseButton.WHEEL) ? 'm' : 'M';\n return `\\x1b[<${eventCode(e, true)};${e.col};${e.row}${final}`;\n },\n SGR_PIXELS: (e: ICoreMouseEvent) => {\n const final = (e.action === CoreMouseAction.UP && e.button !== CoreMouseButton.WHEEL) ? 'm' : 'M';\n return `\\x1b[<${eventCode(e, true)};${e.x};${e.y}${final}`;\n }\n};\n\n/**\n * MouseStateService\n *\n * Provides mouse tracking reports with different protocols and encodings.\n * - protocols: NONE (default), X10, VT200, DRAG, ANY\n * - encodings: DEFAULT, SGR (UTF8, URXVT removed in #2507)\n *\n * Custom protocols/encodings can be added by `addProtocol` / `addEncoding`.\n * To activate a protocol/encoding, set `activeProtocol` / `activeEncoding`.\n * Switching a protocol will send a notification event `onProtocolChange`\n * with a list of needed events to track.\n *\n * The service handles the mouse tracking state and decides whether to send\n * a tracking report to the backend based on protocol and encoding limitations.\n * To send a mouse event call `triggerMouseEvent`.\n */\nexport class MouseStateService extends Disposable implements IMouseStateService {\n public serviceBrand: any;\n\n private _protocols: { [name: string]: ICoreMouseProtocol } = {};\n private _encodings: { [name: string]: CoreMouseEncoding } = {};\n private _activeProtocol: string = '';\n private _activeEncoding: string = '';\n private _customWheelEventHandler: ((event: WheelEvent) => boolean) | undefined;\n\n private readonly _onProtocolChange = this._register(new Emitter());\n public readonly onProtocolChange = this._onProtocolChange.event;\n\n constructor() {\n super();\n\n // register default protocols and encodings\n for (const name of Object.keys(DEFAULT_PROTOCOLS)) this.addProtocol(name, DEFAULT_PROTOCOLS[name]);\n for (const name of Object.keys(DEFAULT_ENCODINGS)) this.addEncoding(name, DEFAULT_ENCODINGS[name]);\n // call reset to set defaults\n this.reset();\n }\n\n public addProtocol(name: string, protocol: ICoreMouseProtocol): void {\n this._protocols[name] = protocol;\n }\n\n public addEncoding(name: string, encoding: CoreMouseEncoding): void {\n this._encodings[name] = encoding;\n }\n\n public get activeProtocol(): string {\n return this._activeProtocol;\n }\n\n public get areMouseEventsActive(): boolean {\n return this._protocols[this._activeProtocol].events !== 0;\n }\n\n public set activeProtocol(name: string) {\n if (!this._protocols[name]) {\n throw new Error(`unknown protocol \"${name}\"`);\n }\n this._activeProtocol = name;\n this._onProtocolChange.fire(this._protocols[name].events);\n }\n\n public get activeEncoding(): string {\n return this._activeEncoding;\n }\n\n public set activeEncoding(name: string) {\n if (!this._encodings[name]) {\n throw new Error(`unknown encoding \"${name}\"`);\n }\n this._activeEncoding = name;\n }\n\n public reset(): void {\n this.activeProtocol = 'NONE';\n this.activeEncoding = 'DEFAULT';\n }\n\n public setCustomWheelEventHandler(customWheelEventHandler: ((event: WheelEvent) => boolean) | undefined): void {\n this._customWheelEventHandler = customWheelEventHandler;\n }\n\n public allowCustomWheelEvent(ev: WheelEvent): boolean {\n return this._customWheelEventHandler ? this._customWheelEventHandler(ev) !== false : true;\n }\n\n public restrictMouseEvent(e: ICoreMouseEvent): boolean {\n return this._protocols[this._activeProtocol].restrict(e);\n }\n\n public encodeMouseEvent(e: ICoreMouseEvent): string {\n return this._encodings[this._activeEncoding](e);\n }\n\n public get isDefaultEncoding(): boolean {\n return this._activeEncoding === 'DEFAULT';\n }\n\n public get isPixelEncoding(): boolean {\n return this._activeEncoding === 'SGR_PIXELS';\n }\n}\n", "/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { IUnicodeService, IUnicodeVersionProvider, UnicodeCharProperties, UnicodeCharWidth } from './Services';\nimport { Emitter } from '../Event';\n\nexport class UnicodeService implements IUnicodeService {\n public serviceBrand: any;\n\n private _providers: {[key: string]: IUnicodeVersionProvider} = Object.create(null);\n private _active: string = '';\n private _activeProvider!: IUnicodeVersionProvider;\n\n private readonly _onChange = new Emitter();\n public readonly onChange = this._onChange.event;\n\n public static extractShouldJoin(value: UnicodeCharProperties): boolean {\n return (value & 1) !== 0;\n }\n public static extractWidth(value: UnicodeCharProperties): UnicodeCharWidth {\n return ((value >> 1) & 0x3) as UnicodeCharWidth;\n }\n public static extractCharKind(value: UnicodeCharProperties): number {\n return value >> 3;\n }\n public static createPropertyValue(state: number, width: number, shouldJoin: boolean = false): UnicodeCharProperties {\n return ((state & 0xffffff) << 3) | ((width & 3) << 1) | (shouldJoin?1:0);\n }\n\n public dispose(): void {\n this._onChange.dispose();\n }\n\n public get versions(): string[] {\n return Object.keys(this._providers);\n }\n\n public get activeVersion(): string {\n return this._active;\n }\n\n public set activeVersion(version: string) {\n if (!this._providers[version]) {\n throw new Error(`unknown Unicode version \"${version}\"`);\n }\n this._active = version;\n this._activeProvider = this._providers[version];\n this._onChange.fire(version);\n }\n\n public register(provider: IUnicodeVersionProvider): void {\n this._providers[provider.version] = provider;\n if (!this._active) {\n this.activeVersion = provider.version;\n }\n }\n\n /**\n * Unicode version dependent interface.\n */\n public wcwidth(num: number): UnicodeCharWidth {\n return this._activeProvider.wcwidth(num);\n }\n\n public getStringCellWidth(s: string): number {\n let result = 0;\n let precedingInfo = 0;\n const length = s.length;\n for (let i = 0; i < length; ++i) {\n let code = s.charCodeAt(i);\n // surrogate pair first\n if (0xD800 <= code && code <= 0xDBFF) {\n if (++i >= length) {\n // this should not happen with strings retrieved from\n // Buffer.translateToString as it converts from UTF-32\n // and therefore always should contain the second part\n // for any other string we still have to handle it somehow:\n // simply treat the lonely surrogate first as a single char (UCS-2 behavior)\n return result + this.wcwidth(code);\n }\n const second = s.charCodeAt(i);\n // convert surrogate pair to high codepoint only for valid second part (UTF-16)\n // otherwise treat them independently (UCS-2 behavior)\n if (0xDC00 <= second && second <= 0xDFFF) {\n code = (code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000;\n } else {\n result += this.wcwidth(second);\n }\n }\n const currentInfo = this.charProperties(code, precedingInfo);\n let chWidth = UnicodeService.extractWidth(currentInfo);\n if (UnicodeService.extractShouldJoin(currentInfo)) {\n chWidth -= UnicodeService.extractWidth(precedingInfo);\n }\n result += chWidth;\n precedingInfo = currentInfo;\n }\n return result;\n }\n\n public charProperties(codepoint: number, preceding: UnicodeCharProperties): UnicodeCharProperties {\n return this._activeProvider.charProperties(codepoint, preceding);\n }\n}\n", "/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\nimport { IUnicodeVersionProvider, UnicodeCharProperties, UnicodeCharWidth } from '../services/Services';\nimport { UnicodeService } from '../services/UnicodeService';\n\nconst BMP_COMBINING = [\n [0x0300, 0x036F], [0x0483, 0x0486], [0x0488, 0x0489],\n [0x0591, 0x05BD], [0x05BF, 0x05BF], [0x05C1, 0x05C2],\n [0x05C4, 0x05C5], [0x05C7, 0x05C7], [0x0600, 0x0603],\n [0x0610, 0x0615], [0x064B, 0x065E], [0x0670, 0x0670],\n [0x06D6, 0x06E4], [0x06E7, 0x06E8], [0x06EA, 0x06ED],\n [0x070F, 0x070F], [0x0711, 0x0711], [0x0730, 0x074A],\n [0x07A6, 0x07B0], [0x07EB, 0x07F3], [0x0901, 0x0902],\n [0x093C, 0x093C], [0x0941, 0x0948], [0x094D, 0x094D],\n [0x0951, 0x0954], [0x0962, 0x0963], [0x0981, 0x0981],\n [0x09BC, 0x09BC], [0x09C1, 0x09C4], [0x09CD, 0x09CD],\n [0x09E2, 0x09E3], [0x0A01, 0x0A02], [0x0A3C, 0x0A3C],\n [0x0A41, 0x0A42], [0x0A47, 0x0A48], [0x0A4B, 0x0A4D],\n [0x0A70, 0x0A71], [0x0A81, 0x0A82], [0x0ABC, 0x0ABC],\n [0x0AC1, 0x0AC5], [0x0AC7, 0x0AC8], [0x0ACD, 0x0ACD],\n [0x0AE2, 0x0AE3], [0x0B01, 0x0B01], [0x0B3C, 0x0B3C],\n [0x0B3F, 0x0B3F], [0x0B41, 0x0B43], [0x0B4D, 0x0B4D],\n [0x0B56, 0x0B56], [0x0B82, 0x0B82], [0x0BC0, 0x0BC0],\n [0x0BCD, 0x0BCD], [0x0C3E, 0x0C40], [0x0C46, 0x0C48],\n [0x0C4A, 0x0C4D], [0x0C55, 0x0C56], [0x0CBC, 0x0CBC],\n [0x0CBF, 0x0CBF], [0x0CC6, 0x0CC6], [0x0CCC, 0x0CCD],\n [0x0CE2, 0x0CE3], [0x0D41, 0x0D43], [0x0D4D, 0x0D4D],\n [0x0DCA, 0x0DCA], [0x0DD2, 0x0DD4], [0x0DD6, 0x0DD6],\n [0x0E31, 0x0E31], [0x0E34, 0x0E3A], [0x0E47, 0x0E4E],\n [0x0EB1, 0x0EB1], [0x0EB4, 0x0EB9], [0x0EBB, 0x0EBC],\n [0x0EC8, 0x0ECD], [0x0F18, 0x0F19], [0x0F35, 0x0F35],\n [0x0F37, 0x0F37], [0x0F39, 0x0F39], [0x0F71, 0x0F7E],\n [0x0F80, 0x0F84], [0x0F86, 0x0F87], [0x0F90, 0x0F97],\n [0x0F99, 0x0FBC], [0x0FC6, 0x0FC6], [0x102D, 0x1030],\n [0x1032, 0x1032], [0x1036, 0x1037], [0x1039, 0x1039],\n [0x1058, 0x1059], [0x1160, 0x11FF], [0x135F, 0x135F],\n [0x1712, 0x1714], [0x1732, 0x1734], [0x1752, 0x1753],\n [0x1772, 0x1773], [0x17B4, 0x17B5], [0x17B7, 0x17BD],\n [0x17C6, 0x17C6], [0x17C9, 0x17D3], [0x17DD, 0x17DD],\n [0x180B, 0x180D], [0x18A9, 0x18A9], [0x1920, 0x1922],\n [0x1927, 0x1928], [0x1932, 0x1932], [0x1939, 0x193B],\n [0x1A17, 0x1A18], [0x1B00, 0x1B03], [0x1B34, 0x1B34],\n [0x1B36, 0x1B3A], [0x1B3C, 0x1B3C], [0x1B42, 0x1B42],\n [0x1B6B, 0x1B73], [0x1DC0, 0x1DCA], [0x1DFE, 0x1DFF],\n [0x200B, 0x200F], [0x202A, 0x202E], [0x2060, 0x2063],\n [0x206A, 0x206F], [0x20D0, 0x20EF], [0x302A, 0x302F],\n [0x3099, 0x309A], [0xA806, 0xA806], [0xA80B, 0xA80B],\n [0xA825, 0xA826], [0xFB1E, 0xFB1E], [0xFE00, 0xFE0F],\n [0xFE20, 0xFE23], [0xFEFF, 0xFEFF], [0xFFF9, 0xFFFB]\n];\nconst HIGH_COMBINING = [\n [0x10A01, 0x10A03], [0x10A05, 0x10A06], [0x10A0C, 0x10A0F],\n [0x10A38, 0x10A3A], [0x10A3F, 0x10A3F], [0x1D167, 0x1D169],\n [0x1D173, 0x1D182], [0x1D185, 0x1D18B], [0x1D1AA, 0x1D1AD],\n [0x1D242, 0x1D244], [0xE0001, 0xE0001], [0xE0020, 0xE007F],\n [0xE0100, 0xE01EF]\n];\n\n// BMP lookup table, lazy initialized during first addon loading\nlet table: Uint8Array;\n\nfunction bisearch(ucs: number, data: number[][]): boolean {\n let min = 0;\n let max = data.length - 1;\n let mid;\n if (ucs < data[0][0] || ucs > data[max][1]) {\n return false;\n }\n while (max >= min) {\n mid = (min + max) >> 1;\n if (ucs > data[mid][1]) {\n min = mid + 1;\n } else if (ucs < data[mid][0]) {\n max = mid - 1;\n } else {\n return true;\n }\n }\n return false;\n}\n\nexport class UnicodeV6 implements IUnicodeVersionProvider {\n public readonly version = '6';\n\n constructor() {\n // init lookup table once\n if (!table) {\n table = new Uint8Array(65536);\n table.fill(1);\n table[0] = 0;\n // control chars\n table.fill(0, 1, 32);\n table.fill(0, 0x7f, 0xa0);\n\n // apply wide char rules first\n // wide chars\n table.fill(2, 0x1100, 0x1160);\n table[0x2329] = 2;\n table[0x232a] = 2;\n table.fill(2, 0x2e80, 0xa4d0);\n table[0x303f] = 1; // wrongly in last line\n\n table.fill(2, 0xac00, 0xd7a4);\n table.fill(2, 0xf900, 0xfb00);\n table.fill(2, 0xfe10, 0xfe1a);\n table.fill(2, 0xfe30, 0xfe70);\n table.fill(2, 0xff00, 0xff61);\n table.fill(2, 0xffe0, 0xffe7);\n\n // apply combining last to ensure we overwrite\n // wrongly wide set chars:\n // the original algo evals combining first and falls\n // through to wide check so we simply do here the opposite\n // combining 0\n for (let r = 0; r < BMP_COMBINING.length; ++r) {\n table.fill(0, BMP_COMBINING[r][0], BMP_COMBINING[r][1] + 1);\n }\n }\n }\n\n public wcwidth(num: number): UnicodeCharWidth {\n if (num < 32) return 0;\n if (num < 127) return 1;\n if (num < 65536) return table[num] as UnicodeCharWidth;\n if (bisearch(num, HIGH_COMBINING)) return 0;\n if ((num >= 0x20000 && num <= 0x2fffd) || (num >= 0x30000 && num <= 0x3fffd)) return 2;\n return 1;\n }\n\n public charProperties(codepoint: number, preceding: UnicodeCharProperties): UnicodeCharProperties {\n let width = this.wcwidth(codepoint);\n let shouldJoin = width === 0 && preceding !== 0;\n // HACK: Ideally this file would not depend on the service which uses it\n if (shouldJoin) {\n const oldWidth = UnicodeService.extractWidth(preceding);\n if (oldWidth === 0) {\n shouldJoin = false;\n } else if (oldWidth > width) {\n width = oldWidth;\n }\n }\n return UnicodeService.createPropertyValue(0, width, shouldJoin);\n }\n}\n", "/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { ICharsetService } from './Services';\nimport { ICharset } from '../Types';\n\nexport class CharsetService implements ICharsetService {\n public serviceBrand: any;\n\n public charset: ICharset | undefined;\n public glevel: number = 0;\n\n private _charsets: (ICharset | undefined)[] = [];\n\n public get charsets(): (ICharset | undefined)[] {\n return this._charsets;\n }\n\n public reset(): void {\n this.charset = undefined;\n this._charsets = [];\n this.glevel = 0;\n }\n\n public setgLevel(g: number): void {\n this.glevel = g;\n this.charset = this._charsets[g];\n }\n\n public setgCharset(g: number, charset: ICharset | undefined): void {\n this._charsets[g] = charset;\n if (this.glevel === g) {\n this.charset = charset;\n }\n }\n}\n", "/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { CHAR_DATA_CODE_INDEX, NULL_CELL_CODE, WHITESPACE_CELL_CODE } from './buffer/Constants';\nimport { IBufferService } from './services/Services';\n\nexport function updateWindowsModeWrappedState(bufferService: IBufferService): void {\n // Winpty does not support wraparound mode which means that lines will never\n // be marked as wrapped. This causes issues for things like copying a line\n // retaining the wrapped new line characters or if consumers are listening\n // in on the data stream.\n //\n // The workaround for this is to listen to every incoming line feed and mark\n // the line as wrapped if the last character in the previous line is not a\n // space. This is certainly not without its problems, but generally on\n // Windows when text reaches the end of the terminal it's likely going to be\n // wrapped.\n const line = bufferService.buffer.lines.get(bufferService.buffer.ybase + bufferService.buffer.y - 1);\n const lastChar = line?.get(bufferService.cols - 1);\n\n const nextLine = bufferService.buffer.lines.get(bufferService.buffer.ybase + bufferService.buffer.y);\n if (nextLine && lastChar) {\n nextLine.isWrapped = (lastChar[CHAR_DATA_CODE_INDEX] !== NULL_CELL_CODE && lastChar[CHAR_DATA_CODE_INDEX] !== WHITESPACE_CELL_CODE);\n }\n}\n", "/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\nimport { IParams, ParamsArray } from './Types';\n\nconst enum Constants {\n /**\n * Max value supported for a single param/subparam (clamped to positive int32 range)\n */\n MAX_VALUE = 0x7FFFFFFF,\n /**\n * Max allowed subparams for a single sequence (hardcoded limitation)\n */\n MAX_SUBPARAMS = 256\n}\n\n/**\n * Params storage class.\n * This type is used by the parser to accumulate sequence parameters and sub parameters\n * and transmit them to the input handler actions.\n *\n * NOTES:\n * - params object for action handlers is borrowed, use `.toArray` or `.clone` to get a copy\n * - never read beyond `params.length - 1` (likely to contain arbitrary data)\n * - `.getSubParams` returns a borrowed typed array, use `.getSubParamsAll` for cloned sub params\n * - hardcoded limitations:\n * - max. value for a single (sub) param is 2^31 - 1 (greater values are clamped to that)\n * - max. 256 sub params possible\n * - negative values are not allowed beside -1 (placeholder for default value)\n *\n * About ZDM (Zero Default Mode):\n * ZDM is not orchestrated by this class. If the parser is in ZDM,\n * it should add 0 for empty params, otherwise -1. This does not apply\n * to subparams, empty subparams should always be added with -1.\n */\nexport class Params implements IParams {\n // params store and length\n public params: Int32Array;\n public length: number;\n\n // sub params store and length\n protected _subParams: Int32Array;\n protected _subParamsLength: number;\n\n // sub params offsets from param: param idx --> [start, end] offset\n private _subParamsIdx: Uint16Array;\n private _rejectDigits: boolean;\n private _rejectSubDigits: boolean;\n private _digitIsSub: boolean;\n\n /**\n * Create a `Params` type from JS array representation.\n */\n public static fromArray(values: ParamsArray): Params {\n const params = new Params();\n if (!values.length) {\n return params;\n }\n // skip leading sub params\n for (let i = (Array.isArray(values[0])) ? 1 : 0; i < values.length; ++i) {\n const value = values[i];\n if (Array.isArray(value)) {\n for (let k = 0; k < value.length; ++k) {\n params.addSubParam(value[k]);\n }\n } else {\n params.addParam(value);\n }\n }\n return params;\n }\n\n /**\n * @param maxLength max length of storable parameters\n * @param maxSubParamsLength max length of storable sub parameters\n */\n constructor(public maxLength: number = 32, public maxSubParamsLength: number = 32) {\n if (maxSubParamsLength > Constants.MAX_SUBPARAMS) {\n throw new Error('maxSubParamsLength must not be greater than 256');\n }\n this.params = new Int32Array(maxLength);\n this.length = 0;\n this._subParams = new Int32Array(maxSubParamsLength);\n this._subParamsLength = 0;\n this._subParamsIdx = new Uint16Array(maxLength);\n this._rejectDigits = false;\n this._rejectSubDigits = false;\n this._digitIsSub = false;\n }\n\n /**\n * Clone object.\n */\n public clone(): Params {\n const newParams = new Params(this.maxLength, this.maxSubParamsLength);\n newParams.params.set(this.params);\n newParams.length = this.length;\n newParams._subParams.set(this._subParams);\n newParams._subParamsLength = this._subParamsLength;\n newParams._subParamsIdx.set(this._subParamsIdx);\n newParams._rejectDigits = this._rejectDigits;\n newParams._rejectSubDigits = this._rejectSubDigits;\n newParams._digitIsSub = this._digitIsSub;\n return newParams;\n }\n\n /**\n * Get a JS array representation of the current parameters and sub parameters.\n * The array is structured as follows:\n * sequence: \"1;2:3:4;5::6\"\n * array : [1, 2, [3, 4], 5, [-1, 6]]\n */\n public toArray(): ParamsArray {\n const res: ParamsArray = [];\n for (let i = 0; i < this.length; ++i) {\n res.push(this.params[i]);\n const start = this._subParamsIdx[i] >> 8;\n const end = this._subParamsIdx[i] & 0xFF;\n if (end - start > 0) {\n res.push(Array.prototype.slice.call(this._subParams, start, end));\n }\n }\n return res;\n }\n\n /**\n * Reset to initial empty state.\n */\n public reset(): void {\n this.length = 0;\n this._subParamsLength = 0;\n this._rejectDigits = false;\n this._rejectSubDigits = false;\n this._digitIsSub = false;\n }\n\n /**\n * Reset and add 0 as first param (ZDM).\n */\n public resetZdm(): void {\n this.length = 1;\n this._subParamsLength = 0;\n this._rejectDigits = false;\n this._rejectSubDigits = false;\n this._digitIsSub = false;\n this._subParamsIdx[0] = 0;\n this.params[0] = 0;\n }\n\n /**\n * Add a parameter value.\n * `Params` only stores up to `maxLength` parameters, any later\n * parameter will be ignored.\n * Note: VT devices only stored up to 16 values, xterm seems to\n * store up to 30.\n */\n public addParam(value: number): void {\n this._digitIsSub = false;\n if (this.length >= this.maxLength) {\n this._rejectDigits = true;\n return;\n }\n if (value < -1) {\n throw new Error('values less than -1 are not allowed');\n }\n this._subParamsIdx[this.length] = this._subParamsLength << 8 | this._subParamsLength;\n this.params[this.length++] = value > Constants.MAX_VALUE ? Constants.MAX_VALUE : value;\n }\n\n /**\n * Add a sub parameter value.\n * The sub parameter is automatically associated with the last parameter value.\n * Thus it is not possible to add a subparameter without any parameter added yet.\n * `Params` only stores up to `maxSubParamsLength` sub parameters, any later\n * sub parameter will be ignored.\n */\n public addSubParam(value: number): void {\n this._digitIsSub = true;\n if (!this.length) {\n return;\n }\n if (this._rejectDigits || this._subParamsLength >= this.maxSubParamsLength) {\n this._rejectSubDigits = true;\n return;\n }\n if (value < -1) {\n throw new Error('values less than -1 are not allowed');\n }\n this._subParams[this._subParamsLength++] = value > Constants.MAX_VALUE ? Constants.MAX_VALUE : value;\n this._subParamsIdx[this.length - 1]++;\n }\n\n /**\n * Whether parameter at index `idx` has sub parameters.\n */\n public hasSubParams(idx: number): boolean {\n return ((this._subParamsIdx[idx] & 0xFF) - (this._subParamsIdx[idx] >> 8) > 0);\n }\n\n /**\n * Return sub parameters for parameter at index `idx`.\n * Note: The values are borrowed, thus you need to copy\n * the values if you need to hold them in nonlocal scope.\n */\n public getSubParams(idx: number): Int32Array | null {\n const start = this._subParamsIdx[idx] >> 8;\n const end = this._subParamsIdx[idx] & 0xFF;\n if (end - start > 0) {\n return this._subParams.subarray(start, end);\n }\n return null;\n }\n\n /**\n * Return all sub parameters as {idx: subparams} mapping.\n * Note: The values are not borrowed.\n */\n public getSubParamsAll(): {[idx: number]: Int32Array} {\n const result: {[idx: number]: Int32Array} = {};\n for (let i = 0; i < this.length; ++i) {\n const start = this._subParamsIdx[i] >> 8;\n const end = this._subParamsIdx[i] & 0xFF;\n if (end - start > 0) {\n result[i] = this._subParams.slice(start, end);\n }\n }\n return result;\n }\n\n /**\n * Add a single digit value to current parameter.\n * This is used by the parser to account digits on a char by char basis.\n */\n public addDigit(value: number): void {\n let length;\n if (this._rejectDigits\n || !(length = this._digitIsSub ? this._subParamsLength : this.length)\n || (this._digitIsSub && this._rejectSubDigits)\n ) {\n return;\n }\n\n const store = this._digitIsSub ? this._subParams : this.params;\n const cur = store[length - 1];\n store[length - 1] = ~cur ? Math.min(cur * 10 + value, Constants.MAX_VALUE) : value;\n }\n}\n", "/**\n * Copyright (c) 2026 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\n/**\n * Accumulates string data from multiple chunks without O(n\u00B2) string concatenation.\n */\nexport class StringBuilder {\n private _chunks: string[] = [];\n private _length = 0;\n\n public get length(): number {\n return this._length;\n }\n\n public reset(): void {\n this._chunks.length = 0;\n this._length = 0;\n }\n\n public append(chunk: string): void {\n this._chunks.push(chunk);\n this._length += chunk.length;\n }\n\n public toString(): string {\n return this._chunks.join('');\n }\n}\n\n/**\n * String builder that rejects payloads larger than a fixed limit.\n */\nexport class LimitedStringBuilder {\n private readonly _builder = new StringBuilder();\n\n constructor(private readonly _limit: number) { }\n\n public get length(): number {\n return this._builder.length;\n }\n\n public get limit(): number {\n return this._limit;\n }\n\n public reset(): void {\n this._builder.reset();\n }\n\n /**\n * @returns true if the limit was exceeded (buffer is cleared in that case)\n */\n public append(chunk: string): boolean {\n this._builder.append(chunk);\n if (this._builder.length > this._limit) {\n this._builder.reset();\n return true;\n }\n return false;\n }\n\n public toString(): string {\n return this._builder.toString();\n }\n}\n", "/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { IOscHandler, IHandlerCollection, OscFallbackHandlerType, IOscParser, ISubParserStackState } from './Types';\nimport { OscState, ParserConstants } from './Constants';\nimport { utf32ToString } from '../input/TextDecoder';\nimport { IDisposable } from '../Types';\nimport { LimitedStringBuilder } from '../StringBuilder';\n\nconst EMPTY_HANDLERS: IOscHandler[] = [];\n\nexport class OscParser implements IOscParser {\n private _state = OscState.START;\n private _active = EMPTY_HANDLERS;\n private _id = -1;\n private _handlers: IHandlerCollection = Object.create(null);\n private _handlerFb: OscFallbackHandlerType = () => { };\n private _stack: ISubParserStackState = {\n paused: false,\n loopPosition: 0,\n fallThrough: false\n };\n\n public registerHandler(ident: number, handler: IOscHandler): IDisposable {\n this._handlers[ident] ??= [];\n const handlerList = this._handlers[ident];\n handlerList.push(handler);\n return {\n dispose: () => {\n const handlerIndex = handlerList.indexOf(handler);\n if (handlerIndex !== -1) {\n handlerList.splice(handlerIndex, 1);\n }\n }\n };\n }\n public clearHandler(ident: number): void {\n if (this._handlers[ident]) delete this._handlers[ident];\n }\n public setHandlerFallback(handler: OscFallbackHandlerType): void {\n this._handlerFb = handler;\n }\n\n public dispose(): void {\n this._handlers = Object.create(null);\n this._handlerFb = () => { };\n this._active = EMPTY_HANDLERS;\n }\n\n public reset(): void {\n // force cleanup handlers if payload was already sent\n if (this._state === OscState.PAYLOAD) {\n for (let j = this._stack.paused ? this._stack.loopPosition - 1 : this._active.length - 1; j >= 0; --j) {\n this._active[j].end(false);\n }\n }\n this._stack.paused = false;\n this._active = EMPTY_HANDLERS;\n this._id = -1;\n this._state = OscState.START;\n }\n\n private _start(): void {\n this._active = this._handlers[this._id] || EMPTY_HANDLERS;\n if (!this._active.length) {\n this._handlerFb(this._id, 'START');\n } else {\n for (let j = this._active.length - 1; j >= 0; j--) {\n this._active[j].start();\n }\n }\n }\n\n private _put(data: Uint32Array, start: number, end: number): void {\n if (!this._active.length) {\n this._handlerFb(this._id, 'PUT', utf32ToString(data, start, end));\n } else {\n for (let j = this._active.length - 1; j >= 0; j--) {\n this._active[j].put(data, start, end);\n }\n }\n }\n\n public start(): void {\n // always reset leftover handlers\n this.reset();\n this._state = OscState.ID;\n }\n\n /**\n * Put data to current OSC command.\n * Expects the identifier of the OSC command in the form\n * OSC id ; payload ST/BEL\n * Payload chunks are not further processed and get\n * directly passed to the handlers.\n */\n public put(data: Uint32Array, start: number, end: number): void {\n if (this._state === OscState.ABORT) {\n return;\n }\n if (this._state === OscState.ID) {\n while (start < end) {\n const code = data[start++];\n if (code === 0x3b) {\n this._state = OscState.PAYLOAD;\n this._start();\n break;\n }\n if (code < 0x30 || 0x39 < code) {\n this._state = OscState.ABORT;\n return;\n }\n if (this._id === -1) {\n this._id = 0;\n }\n this._id = this._id * 10 + code - 48;\n }\n }\n if (this._state === OscState.PAYLOAD && end - start > 0) {\n this._put(data, start, end);\n }\n }\n\n /**\n * Indicates end of an OSC command.\n * Whether the OSC got aborted or finished normally\n * is indicated by `success`.\n */\n public end(success: boolean, promiseResult: boolean = true): void | Promise {\n if (this._state === OscState.START) {\n return;\n }\n // do nothing if command was faulty\n if (this._state !== OscState.ABORT) {\n // if we are still in ID state and get an early end\n // means that the command has no payload thus we still have\n // to announce START and send END right after\n if (this._state === OscState.ID) {\n this._start();\n }\n\n if (!this._active.length) {\n this._handlerFb(this._id, 'END', success);\n } else {\n let handlerResult: boolean | Promise = false;\n let j = this._active.length - 1;\n let fallThrough = false;\n if (this._stack.paused) {\n j = this._stack.loopPosition - 1;\n handlerResult = promiseResult;\n fallThrough = this._stack.fallThrough;\n this._stack.paused = false;\n }\n if (!fallThrough && handlerResult === false) {\n for (; j >= 0; j--) {\n handlerResult = this._active[j].end(success);\n if (handlerResult === true) {\n break;\n } else if (handlerResult instanceof Promise) {\n this._stack.paused = true;\n this._stack.loopPosition = j;\n this._stack.fallThrough = false;\n return handlerResult;\n }\n }\n j--;\n }\n // cleanup left over handlers\n // we always have to call .end for proper cleanup,\n // here we use `success` to indicate whether a handler should execute\n for (; j >= 0; j--) {\n handlerResult = this._active[j].end(false);\n if (handlerResult instanceof Promise) {\n this._stack.paused = true;\n this._stack.loopPosition = j;\n this._stack.fallThrough = true;\n return handlerResult;\n }\n }\n }\n\n }\n this._active = EMPTY_HANDLERS;\n this._id = -1;\n this._state = OscState.START;\n }\n}\n\n/**\n * Convenient class to allow attaching string based handler functions\n * as OSC handlers.\n */\nexport class OscHandler implements IOscHandler {\n private static _payloadLimit = ParserConstants.PAYLOAD_LIMIT;\n\n private _data = new LimitedStringBuilder(OscHandler._payloadLimit);\n private _hitLimit: boolean = false;\n\n constructor(private _handler: (data: string) => boolean | Promise) { }\n\n public start(): void {\n this._data.reset();\n this._hitLimit = false;\n }\n\n public put(data: Uint32Array, start: number, end: number): void {\n if (this._hitLimit) {\n return;\n }\n if (this._data.append(utf32ToString(data, start, end))) {\n this._hitLimit = true;\n }\n }\n\n public end(success: boolean): boolean | Promise {\n let ret: boolean | Promise = false;\n if (this._hitLimit) {\n ret = false;\n } else if (success) {\n ret = this._handler(this._data.toString());\n if (ret instanceof Promise) {\n // need to hold data until `ret` got resolved\n // dont care for errors, data will be freed anyway on next start\n return ret.then(res => {\n this._data.reset();\n this._hitLimit = false;\n return res;\n });\n }\n }\n this._data.reset();\n this._hitLimit = false;\n return ret;\n }\n}\n", "/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { IDisposable } from '../Types';\nimport { IDcsHandler, IParams, IHandlerCollection, IDcsParser, DcsFallbackHandlerType, ISubParserStackState } from './Types';\nimport { utf32ToString } from '../input/TextDecoder';\nimport { Params } from './Params';\nimport { ParserConstants } from './Constants';\nimport { LimitedStringBuilder } from '../StringBuilder';\n\nconst EMPTY_HANDLERS: IDcsHandler[] = [];\n\nexport class DcsParser implements IDcsParser {\n private _handlers: IHandlerCollection = Object.create(null);\n private _active: IDcsHandler[] = EMPTY_HANDLERS;\n private _ident: number = 0;\n private _handlerFb: DcsFallbackHandlerType = () => { };\n private _stack: ISubParserStackState = {\n paused: false,\n loopPosition: 0,\n fallThrough: false\n };\n\n public dispose(): void {\n this._handlers = Object.create(null);\n this._handlerFb = () => { };\n this._active = EMPTY_HANDLERS;\n }\n\n public registerHandler(ident: number, handler: IDcsHandler): IDisposable {\n this._handlers[ident] ??= [];\n const handlerList = this._handlers[ident];\n handlerList.push(handler);\n return {\n dispose: () => {\n const handlerIndex = handlerList.indexOf(handler);\n if (handlerIndex !== -1) {\n handlerList.splice(handlerIndex, 1);\n }\n }\n };\n }\n\n public clearHandler(ident: number): void {\n if (this._handlers[ident]) delete this._handlers[ident];\n }\n\n public setHandlerFallback(handler: DcsFallbackHandlerType): void {\n this._handlerFb = handler;\n }\n\n public reset(): void {\n // force cleanup leftover handlers\n if (this._active.length) {\n for (let j = this._stack.paused ? this._stack.loopPosition - 1 : this._active.length - 1; j >= 0; --j) {\n this._active[j].unhook(false);\n }\n }\n this._stack.paused = false;\n this._active = EMPTY_HANDLERS;\n this._ident = 0;\n }\n\n public hook(ident: number, params: IParams): void {\n // always reset leftover handlers\n this.reset();\n this._ident = ident;\n this._active = this._handlers[ident] || EMPTY_HANDLERS;\n if (!this._active.length) {\n this._handlerFb(this._ident, 'HOOK', params);\n } else {\n for (let j = this._active.length - 1; j >= 0; j--) {\n this._active[j].hook(params);\n }\n }\n }\n\n public put(data: Uint32Array, start: number, end: number): void {\n if (!this._active.length) {\n this._handlerFb(this._ident, 'PUT', utf32ToString(data, start, end));\n } else {\n for (let j = this._active.length - 1; j >= 0; j--) {\n this._active[j].put(data, start, end);\n }\n }\n }\n\n public unhook(success: boolean, promiseResult: boolean = true): void | Promise {\n if (!this._active.length) {\n this._handlerFb(this._ident, 'UNHOOK', success);\n } else {\n let handlerResult: boolean | Promise = false;\n let j = this._active.length - 1;\n let fallThrough = false;\n if (this._stack.paused) {\n j = this._stack.loopPosition - 1;\n handlerResult = promiseResult;\n fallThrough = this._stack.fallThrough;\n this._stack.paused = false;\n }\n if (!fallThrough && handlerResult === false) {\n for (; j >= 0; j--) {\n handlerResult = this._active[j].unhook(success);\n if (handlerResult === true) {\n break;\n } else if (handlerResult instanceof Promise) {\n this._stack.paused = true;\n this._stack.loopPosition = j;\n this._stack.fallThrough = false;\n return handlerResult;\n }\n }\n j--;\n }\n // cleanup left over handlers (fallThrough for async)\n for (; j >= 0; j--) {\n handlerResult = this._active[j].unhook(false);\n if (handlerResult instanceof Promise) {\n this._stack.paused = true;\n this._stack.loopPosition = j;\n this._stack.fallThrough = true;\n return handlerResult;\n }\n }\n }\n this._active = EMPTY_HANDLERS;\n this._ident = 0;\n }\n}\n\n// predefine empty params as [0] (ZDM)\nconst EMPTY_PARAMS = new Params();\nEMPTY_PARAMS.addParam(0);\n\n/**\n * Convenient class to create a DCS handler from a single callback function.\n * Note: The payload is currently limited to 50 MB (hardcoded).\n */\nexport class DcsHandler implements IDcsHandler {\n private static _payloadLimit = ParserConstants.PAYLOAD_LIMIT;\n\n private _data = new LimitedStringBuilder(DcsHandler._payloadLimit);\n private _params: IParams = EMPTY_PARAMS;\n private _hitLimit: boolean = false;\n\n constructor(private _handler: (data: string, params: IParams) => boolean | Promise) { }\n\n public hook(params: IParams): void {\n // since we need to preserve params until `unhook`, we have to clone it\n // (only borrowed from parser and spans multiple parser states)\n // perf optimization:\n // clone only, if we have non empty params, otherwise stick with default\n this._params = (params.length > 1 || params.params[0]) ? params.clone() : EMPTY_PARAMS;\n this._data.reset();\n this._hitLimit = false;\n }\n\n public put(data: Uint32Array, start: number, end: number): void {\n if (this._hitLimit) {\n return;\n }\n if (this._data.append(utf32ToString(data, start, end))) {\n this._hitLimit = true;\n }\n }\n\n public unhook(success: boolean): boolean | Promise {\n let ret: boolean | Promise = false;\n if (this._hitLimit) {\n ret = false;\n } else if (success) {\n ret = this._handler(this._data.toString(), this._params);\n if (ret instanceof Promise) {\n // need to hold data and params until `ret` got resolved\n // dont care for errors, data will be freed anyway on next start\n return ret.then(res => {\n this._params = EMPTY_PARAMS;\n this._data.reset();\n this._hitLimit = false;\n return res;\n });\n }\n }\n this._params = EMPTY_PARAMS;\n this._data.reset();\n this._hitLimit = false;\n return ret;\n }\n}\n", "/**\n * Copyright (c) 2025 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { IApcHandler, IHandlerCollection, ApcFallbackHandlerType, IApcParser, ISubParserStackState } from './Types';\nimport { ParserConstants } from './Constants';\nimport { utf32ToString } from '../input/TextDecoder';\nimport { IDisposable } from '../Types';\nimport { LimitedStringBuilder } from '../StringBuilder';\n\nconst EMPTY_HANDLERS: IApcHandler[] = [];\n\n/**\n * APC Parser for handling Application Program Command sequences.\n * APC sequences use the format: ESC _ ESC \\\n *\n * Unlike OSC which uses numeric identifiers (e.g., OSC 1337),\n * APC uses the first character as the identifier (e.g., 'G' for Kitty graphics).\n * The identifier is the character code of the first byte after ESC _.\n */\nexport class ApcParser implements IApcParser {\n private _handlers: IHandlerCollection = Object.create(null);\n private _active = EMPTY_HANDLERS;\n private _ident: number = 0;\n private _handlerFb: ApcFallbackHandlerType = () => { };\n private _stack: ISubParserStackState = {\n paused: false,\n loopPosition: 0,\n fallThrough: false\n };\n\n /**\n * Register an APC handler for a specific identifier.\n * @param ident The character code of the first byte (e.g., 0x47 for 'G')\n * @param handler The handler to register\n */\n public registerHandler(ident: number, handler: IApcHandler): IDisposable {\n this._handlers[ident] ??= [];\n const handlerList = this._handlers[ident];\n handlerList.push(handler);\n return {\n dispose: () => {\n const handlerIndex = handlerList.indexOf(handler);\n if (handlerIndex !== -1) {\n handlerList.splice(handlerIndex, 1);\n }\n }\n };\n }\n\n public clearHandler(ident: number): void {\n if (this._handlers[ident]) delete this._handlers[ident];\n }\n\n public setHandlerFallback(handler: ApcFallbackHandlerType): void {\n this._handlerFb = handler;\n }\n\n public dispose(): void {\n this._handlers = Object.create(null);\n this._handlerFb = () => { };\n this._active = EMPTY_HANDLERS;\n }\n\n public reset(): void {\n // force cleanup handlers\n if (this._active.length) {\n for (let j = this._stack.paused ? this._stack.loopPosition - 1 : this._active.length - 1; j >= 0; --j) {\n this._active[j].end(false);\n }\n }\n this._stack.paused = false;\n this._active = EMPTY_HANDLERS;\n this._ident = 0;\n }\n\n public start(ident: number): void {\n // always reset leftover handlers\n this.reset();\n this._ident = ident;\n this._active = this._handlers[ident] || EMPTY_HANDLERS;\n if (!this._active.length) {\n this._handlerFb(this._ident, 'START');\n } else {\n for (let j = this._active.length - 1; j >= 0; j--) {\n this._active[j].start();\n }\n }\n }\n\n public put(data: Uint32Array, start: number, end: number): void {\n if (!this._active.length) {\n this._handlerFb(this._ident, 'PUT', utf32ToString(data, start, end));\n } else {\n for (let j = this._active.length - 1; j >= 0; j--) {\n this._active[j].put(data, start, end);\n }\n }\n }\n\n /**\n * Indicates end of an APC command.\n * Whether the APC got aborted or finished normally\n * is indicated by `success`.\n */\n public end(success: boolean, promiseResult: boolean = true): void | Promise {\n if (!this._active.length) {\n this._handlerFb(this._ident, 'END', success);\n } else {\n let handlerResult: boolean | Promise = false;\n let j = this._active.length - 1;\n let fallThrough = false;\n if (this._stack.paused) {\n j = this._stack.loopPosition - 1;\n handlerResult = promiseResult;\n fallThrough = this._stack.fallThrough;\n this._stack.paused = false;\n }\n if (!fallThrough && handlerResult === false) {\n for (; j >= 0; j--) {\n handlerResult = this._active[j].end(success);\n if (handlerResult === true) {\n break;\n } else if (handlerResult instanceof Promise) {\n this._stack.paused = true;\n this._stack.loopPosition = j;\n this._stack.fallThrough = false;\n return handlerResult;\n }\n }\n j--;\n }\n // cleanup left over handlers (fallThrough for async)\n for (; j >= 0; j--) {\n handlerResult = this._active[j].end(false);\n if (handlerResult instanceof Promise) {\n this._stack.paused = true;\n this._stack.loopPosition = j;\n this._stack.fallThrough = true;\n return handlerResult;\n }\n }\n }\n this._active = EMPTY_HANDLERS;\n this._ident = 0;\n }\n}\n\n/**\n * Convenient class to allow attaching string based handler functions\n * as APC handlers.\n */\nexport class ApcHandler implements IApcHandler {\n private static _payloadLimit = ParserConstants.PAYLOAD_LIMIT;\n\n private _data = new LimitedStringBuilder(ApcHandler._payloadLimit);\n private _hitLimit: boolean = false;\n\n constructor(private _handler: (data: string) => boolean | Promise) { }\n\n public start(): void {\n this._data.reset();\n this._hitLimit = false;\n }\n\n public put(data: Uint32Array, start: number, end: number): void {\n if (this._hitLimit) {\n return;\n }\n if (this._data.append(utf32ToString(data, start, end))) {\n this._hitLimit = true;\n }\n }\n\n public end(success: boolean): boolean | Promise {\n let ret: boolean | Promise = false;\n if (this._hitLimit) {\n ret = false;\n } else if (success) {\n ret = this._handler(this._data.toString());\n if (ret instanceof Promise) {\n // need to hold data until `ret` got resolved\n // dont care for errors, data will be freed anyway on next start\n return ret.then(res => {\n this._data.reset();\n this._hitLimit = false;\n return res;\n });\n }\n }\n this._data.reset();\n this._hitLimit = false;\n return ret;\n }\n}\n", "/**\n * Copyright (c) 2018 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandlerType, OscFallbackHandlerType, IOscParser, EscHandlerType, IDcsParser, DcsFallbackHandlerType, IFunctionIdentifier, ExecuteFallbackHandlerType, CsiFallbackHandlerType, EscFallbackHandlerType, PrintHandlerType, PrintFallbackHandlerType, ExecuteHandlerType, IParserStackState, ParserStackType, ResumableHandlersType, IApcHandler, IApcParser, ApcFallbackHandlerType } from './Types';\nimport { ParserState, ParserAction } from './Constants';\nimport { Disposable, toDisposable } from '../Lifecycle';\nimport { IDisposable } from '../Types';\nimport { Params } from './Params';\nimport { OscParser } from './OscParser';\nimport { DcsParser } from './DcsParser';\nimport { ApcParser } from './ApcParser';\n\n/**\n * VT commands done by the parser\n */\n// @vt: #Y ESC CSI \"Control Sequence Introducer\" \"ESC [\" \"Start of a CSI sequence.\"\n// @vt: #Y ESC OSC \"Operating System Command\" \"ESC ]\" \"Start of an OSC sequence.\"\n// @vt: #Y ESC DCS \"Device Control String\" \"ESC P\" \"Start of a DCS sequence.\"\n// @vt: #Y ESC ST \"String Terminator\" \"ESC \\\\\" \"Terminator used for string type sequences.\"\n// @vt: #Y ESC PM \"Privacy Message\" \"ESC ^\" \"Start of a privacy message.\"\n// @vt: #Y ESC APC \"Application Program Command\" \"ESC _\" \"Start of an APC sequence.\"\n// @vt: #Y C1 CSI \"Control Sequence Introducer\" \"\\x9B\" \"Start of a CSI sequence.\"\n// @vt: #Y C1 OSC \"Operating System Command\" \"\\x9D\" \"Start of an OSC sequence.\"\n// @vt: #Y C1 DCS \"Device Control String\" \"\\x90\" \"Start of a DCS sequence.\"\n// @vt: #Y C1 ST \"String Terminator\" \"\\x9C\" \"Terminator used for string type sequences.\"\n// @vt: #Y C1 PM \"Privacy Message\" \"\\x9E\" \"Start of a privacy message.\"\n// @vt: #Y C1 APC \"Application Program Command\" \"\\x9F\" \"Start of an APC sequence.\"\n// @vt: #Y C0 NUL \"Null\" \"\\0, \\x00\" \"NUL is ignored.\"\n// @vt: #Y C0 ESC \"Escape\" \"\\e, \\x1B\" \"Start of a sequence. Cancels any other sequence.\"\n\n/**\n * Table values are generated like this:\n * index: currentState << TableValue.INDEX_STATE_SHIFT | charCode\n * value: action << TableValue.TRANSITION_ACTION_SHIFT | nextState\n */\nconst enum TableAccess {\n TRANSITION_ACTION_SHIFT = 8,\n TRANSITION_STATE_MASK = 255,\n INDEX_STATE_SHIFT = 8\n}\n\n/**\n * Transition table for EscapeSequenceParser.\n */\nexport class TransitionTable {\n public table: Uint16Array;\n\n constructor(length: number) {\n this.table = new Uint16Array(length);\n }\n\n /**\n * Set default transition.\n * @param action default action\n * @param next default next state\n */\n public setDefault(action: ParserAction, next: ParserState): void {\n this.table.fill(action << TableAccess.TRANSITION_ACTION_SHIFT | next);\n }\n\n /**\n * Add a transition to the transition table.\n * @param code input character code\n * @param state current parser state\n * @param action parser action to be done\n * @param next next parser state\n */\n public add(code: number, state: ParserState, action: ParserAction, next: ParserState): void {\n this.table[state << TableAccess.INDEX_STATE_SHIFT | code] = action << TableAccess.TRANSITION_ACTION_SHIFT | next;\n }\n\n /**\n * Add transitions for multiple input character codes.\n * @param codes input character code array\n * @param state current parser state\n * @param action parser action to be done\n * @param next next parser state\n */\n public addMany(codes: number[], state: ParserState, action: ParserAction, next: ParserState): void {\n for (let i = 0; i < codes.length; i++) {\n this.table[state << TableAccess.INDEX_STATE_SHIFT | codes[i]] = action << TableAccess.TRANSITION_ACTION_SHIFT | next;\n }\n }\n}\n\n\n// Pseudo-character placeholder for printable non-ascii characters (unicode).\nconst NON_ASCII_PRINTABLE = 0xA0;\n\n\n/**\n * VT500 compatible transition table.\n * Taken from https://vt100.net/emu/dec_ansi_parser.\n */\nexport const VT500_TRANSITION_TABLE = (function (): TransitionTable {\n // table size:\n // (ParserState.STATE_LENGTH - 1) << TableAccess.INDEX_STATE_SHIFT | NON_ASCII_PRINTABLE + 1\n const table: TransitionTable = new TransitionTable(4257);\n\n // range macro for byte\n const BYTE_VALUES = 256;\n const blueprint = Array.apply(null, Array(BYTE_VALUES)).map((unused: any, i: number) => i);\n const r = (start: number, end: number): number[] => blueprint.slice(start, end);\n\n // Default definitions.\n const PRINTABLES = r(0x20, 0x7f); // 0x20 (SP) included, 0x7F (DEL) excluded\n const EXECUTABLES = r(0x00, 0x18);\n EXECUTABLES.push(0x19);\n EXECUTABLES.push.apply(EXECUTABLES, r(0x1c, 0x20));\n\n const states: number[] = r(ParserState.GROUND, ParserState.STATE_LENGTH);\n\n // set default transition\n table.setDefault(ParserAction.ERROR, ParserState.GROUND);\n // printables\n table.addMany(PRINTABLES, ParserState.GROUND, ParserAction.PRINT, ParserState.GROUND);\n // global anywhere rules\n for (const state of states) {\n table.addMany([0x18, 0x1a, 0x99, 0x9a], state, ParserAction.EXECUTE, ParserState.GROUND);\n table.addMany(r(0x80, 0x90), state, ParserAction.EXECUTE, ParserState.GROUND);\n table.addMany(r(0x90, 0x98), state, ParserAction.EXECUTE, ParserState.GROUND);\n table.add(0x9c, state, ParserAction.IGNORE, ParserState.GROUND); // ST as terminator\n table.add(0x1b, state, ParserAction.CLEAR, ParserState.ESCAPE); // ESC\n table.add(0x9d, state, ParserAction.OSC_START, ParserState.OSC_STRING); // OSC\n table.addMany([0x98, 0x9e], state, ParserAction.IGNORE, ParserState.SOS_PM_STRING); // SOS, PM\n table.add(0x9f, state, ParserAction.CLEAR, ParserState.APC_ENTRY); // APC\n table.add(0x9b, state, ParserAction.CLEAR, ParserState.CSI_ENTRY); // CSI\n table.add(0x90, state, ParserAction.CLEAR, ParserState.DCS_ENTRY); // DCS\n }\n // rules for executables and 7f\n table.addMany(EXECUTABLES, ParserState.GROUND, ParserAction.EXECUTE, ParserState.GROUND);\n table.addMany(EXECUTABLES, ParserState.ESCAPE, ParserAction.EXECUTE, ParserState.ESCAPE);\n table.add(0x7f, ParserState.ESCAPE, ParserAction.IGNORE, ParserState.ESCAPE);\n table.addMany(EXECUTABLES, ParserState.OSC_STRING, ParserAction.IGNORE, ParserState.OSC_STRING);\n table.addMany(EXECUTABLES, ParserState.CSI_ENTRY, ParserAction.EXECUTE, ParserState.CSI_ENTRY);\n table.add(0x7f, ParserState.CSI_ENTRY, ParserAction.IGNORE, ParserState.CSI_ENTRY);\n table.addMany(EXECUTABLES, ParserState.CSI_PARAM, ParserAction.EXECUTE, ParserState.CSI_PARAM);\n table.add(0x7f, ParserState.CSI_PARAM, ParserAction.IGNORE, ParserState.CSI_PARAM);\n table.addMany(EXECUTABLES, ParserState.CSI_IGNORE, ParserAction.EXECUTE, ParserState.CSI_IGNORE);\n table.addMany(EXECUTABLES, ParserState.CSI_INTERMEDIATE, ParserAction.EXECUTE, ParserState.CSI_INTERMEDIATE);\n table.add(0x7f, ParserState.CSI_INTERMEDIATE, ParserAction.IGNORE, ParserState.CSI_INTERMEDIATE);\n table.addMany(EXECUTABLES, ParserState.ESCAPE_INTERMEDIATE, ParserAction.EXECUTE, ParserState.ESCAPE_INTERMEDIATE);\n table.add(0x7f, ParserState.ESCAPE_INTERMEDIATE, ParserAction.IGNORE, ParserState.ESCAPE_INTERMEDIATE);\n // osc\n table.add(0x5d, ParserState.ESCAPE, ParserAction.OSC_START, ParserState.OSC_STRING);\n table.addMany(PRINTABLES, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING);\n table.add(0x7f, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING);\n table.addMany([0x9c, 0x1b, 0x18, 0x1a, 0x07], ParserState.OSC_STRING, ParserAction.OSC_END, ParserState.GROUND);\n table.addMany(r(0x1c, 0x20), ParserState.OSC_STRING, ParserAction.IGNORE, ParserState.OSC_STRING);\n // sos/pm\n table.addMany([0x58, 0x5e], ParserState.ESCAPE, ParserAction.IGNORE, ParserState.SOS_PM_STRING);\n table.addMany(PRINTABLES, ParserState.SOS_PM_STRING, ParserAction.IGNORE, ParserState.SOS_PM_STRING);\n table.addMany(EXECUTABLES, ParserState.SOS_PM_STRING, ParserAction.IGNORE, ParserState.SOS_PM_STRING);\n table.add(0x9c, ParserState.SOS_PM_STRING, ParserAction.IGNORE, ParserState.GROUND);\n table.add(0x7f, ParserState.SOS_PM_STRING, ParserAction.IGNORE, ParserState.SOS_PM_STRING);\n // apc\n table.add(0x5f, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.APC_ENTRY);\n table.addMany(EXECUTABLES, ParserState.APC_ENTRY, ParserAction.IGNORE, ParserState.APC_ENTRY);\n table.add(0x7f, ParserState.APC_ENTRY, ParserAction.IGNORE, ParserState.APC_ENTRY);\n table.addMany(r(0x20, 0x30), ParserState.APC_ENTRY, ParserAction.COLLECT, ParserState.APC_INTERMEDIATE);\n table.addMany(r(0x30, 0x7f), ParserState.APC_ENTRY, ParserAction.APC_START, ParserState.APC_PASSTHROUGH);\n table.addMany(r(0x30, 0x7f), ParserState.APC_INTERMEDIATE, ParserAction.APC_START, ParserState.APC_PASSTHROUGH);\n table.addMany(EXECUTABLES, ParserState.APC_INTERMEDIATE, ParserAction.IGNORE, ParserState.APC_INTERMEDIATE);\n table.addMany(r(0x20, 0x30), ParserState.APC_INTERMEDIATE, ParserAction.COLLECT, ParserState.APC_INTERMEDIATE);\n table.add(0x7f, ParserState.APC_INTERMEDIATE, ParserAction.IGNORE, ParserState.APC_INTERMEDIATE);\n table.addMany(PRINTABLES, ParserState.APC_PASSTHROUGH, ParserAction.APC_PUT, ParserState.APC_PASSTHROUGH);\n table.addMany(EXECUTABLES, ParserState.APC_PASSTHROUGH, ParserAction.IGNORE, ParserState.APC_PASSTHROUGH);\n table.addMany(r(0x08, 0x0e), ParserState.APC_PASSTHROUGH, ParserAction.APC_PUT, ParserState.APC_PASSTHROUGH);\n table.add(0x7f, ParserState.APC_PASSTHROUGH, ParserAction.IGNORE, ParserState.APC_PASSTHROUGH);\n table.addMany([0x1b, 0x9c, 0x18, 0x1a], ParserState.APC_PASSTHROUGH, ParserAction.APC_END, ParserState.GROUND);\n // csi entries\n table.add(0x5b, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.CSI_ENTRY);\n table.addMany(r(0x40, 0x7f), ParserState.CSI_ENTRY, ParserAction.CSI_DISPATCH, ParserState.GROUND);\n table.addMany(r(0x30, 0x3c), ParserState.CSI_ENTRY, ParserAction.PARAM, ParserState.CSI_PARAM);\n table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.CSI_ENTRY, ParserAction.COLLECT, ParserState.CSI_PARAM);\n table.addMany(r(0x30, 0x3c), ParserState.CSI_PARAM, ParserAction.PARAM, ParserState.CSI_PARAM);\n table.addMany(r(0x40, 0x7f), ParserState.CSI_PARAM, ParserAction.CSI_DISPATCH, ParserState.GROUND);\n table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.CSI_PARAM, ParserAction.IGNORE, ParserState.CSI_IGNORE);\n table.addMany(r(0x20, 0x40), ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE);\n table.add(0x7f, ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE);\n table.addMany(r(0x40, 0x7f), ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.GROUND);\n table.addMany(r(0x20, 0x30), ParserState.CSI_ENTRY, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE);\n table.addMany(r(0x20, 0x30), ParserState.CSI_INTERMEDIATE, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE);\n table.addMany(r(0x30, 0x40), ParserState.CSI_INTERMEDIATE, ParserAction.IGNORE, ParserState.CSI_IGNORE);\n table.addMany(r(0x40, 0x7f), ParserState.CSI_INTERMEDIATE, ParserAction.CSI_DISPATCH, ParserState.GROUND);\n table.addMany(r(0x20, 0x30), ParserState.CSI_PARAM, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE);\n // esc_intermediate\n table.addMany(r(0x20, 0x30), ParserState.ESCAPE, ParserAction.COLLECT, ParserState.ESCAPE_INTERMEDIATE);\n table.addMany(r(0x20, 0x30), ParserState.ESCAPE_INTERMEDIATE, ParserAction.COLLECT, ParserState.ESCAPE_INTERMEDIATE);\n table.addMany(r(0x30, 0x7f), ParserState.ESCAPE_INTERMEDIATE, ParserAction.ESC_DISPATCH, ParserState.GROUND);\n table.addMany(r(0x30, 0x50), ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND);\n table.addMany(r(0x51, 0x58), ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND);\n table.addMany([0x59, 0x5a, 0x5c], ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND);\n table.addMany(r(0x60, 0x7f), ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND);\n // dcs entry\n table.add(0x50, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.DCS_ENTRY);\n table.addMany(EXECUTABLES, ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY);\n table.add(0x7f, ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY);\n table.addMany(r(0x20, 0x30), ParserState.DCS_ENTRY, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE);\n table.addMany(r(0x30, 0x3c), ParserState.DCS_ENTRY, ParserAction.PARAM, ParserState.DCS_PARAM);\n table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.DCS_ENTRY, ParserAction.COLLECT, ParserState.DCS_PARAM);\n table.addMany(EXECUTABLES, ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE);\n table.addMany(r(0x20, 0x80), ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE);\n table.addMany(EXECUTABLES, ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM);\n table.add(0x7f, ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM);\n table.addMany(r(0x30, 0x3c), ParserState.DCS_PARAM, ParserAction.PARAM, ParserState.DCS_PARAM);\n table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_IGNORE);\n table.addMany(r(0x20, 0x30), ParserState.DCS_PARAM, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE);\n table.addMany(EXECUTABLES, ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE);\n table.add(0x7f, ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE);\n table.addMany(r(0x20, 0x30), ParserState.DCS_INTERMEDIATE, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE);\n table.addMany(r(0x30, 0x40), ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_IGNORE);\n table.addMany(r(0x40, 0x7f), ParserState.DCS_INTERMEDIATE, ParserAction.DCS_HOOK, ParserState.DCS_PASSTHROUGH);\n table.addMany(r(0x40, 0x7f), ParserState.DCS_PARAM, ParserAction.DCS_HOOK, ParserState.DCS_PASSTHROUGH);\n table.addMany(r(0x40, 0x7f), ParserState.DCS_ENTRY, ParserAction.DCS_HOOK, ParserState.DCS_PASSTHROUGH);\n table.addMany(EXECUTABLES, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH);\n table.addMany(PRINTABLES, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH);\n table.add(0x7f, ParserState.DCS_PASSTHROUGH, ParserAction.IGNORE, ParserState.DCS_PASSTHROUGH);\n table.addMany([0x1b, 0x9c, 0x18, 0x1a], ParserState.DCS_PASSTHROUGH, ParserAction.DCS_UNHOOK, ParserState.GROUND);\n // special handling of unicode chars\n table.add(NON_ASCII_PRINTABLE, ParserState.GROUND, ParserAction.PRINT, ParserState.GROUND);\n table.add(NON_ASCII_PRINTABLE, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING);\n table.add(NON_ASCII_PRINTABLE, ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE);\n table.add(NON_ASCII_PRINTABLE, ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE);\n table.add(NON_ASCII_PRINTABLE, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH);\n table.add(NON_ASCII_PRINTABLE, ParserState.APC_PASSTHROUGH, ParserAction.APC_PUT, ParserState.APC_PASSTHROUGH);\n return table;\n})();\n\n\n/**\n * EscapeSequenceParser.\n * This class implements the ANSI/DEC compatible parser described by\n * Paul Williams (https://vt100.net/emu/dec_ansi_parser).\n *\n * To implement custom ANSI compliant escape sequences it is not needed to\n * alter this parser, instead consider registering a custom handler.\n * For non ANSI compliant sequences change the transition table with\n * the optional `transitions` constructor argument and\n * reimplement the `parse` method.\n *\n * This parser is currently hardcoded to operate in ZDM (Zero Default Mode)\n * as suggested by the original parser, thus empty parameters are set to 0.\n * This is not in line with the latest ECMA-48 specification\n * (ZDM was part of the early specs and got completely removed later on).\n *\n * Other than the original parser from vt100.net this parser supports\n * sub parameters in digital parameters separated by colons. Empty sub parameters\n * are set to -1 (no ZDM for sub parameters).\n *\n * About prefix and intermediate bytes:\n * This parser follows the assumptions of the vt100.net parser with these restrictions:\n * - only one prefix byte is allowed as first parameter byte, byte range 0x3c .. 0x3f\n * - max. two intermediates are respected, byte range 0x20 .. 0x2f\n * Note that this is not in line with ECMA-48 which does not limit either of those.\n * Furthermore ECMA-48 allows the prefix byte range at any param byte position. Currently\n * there are no known sequences that follow the broader definition of the specification.\n *\n * TODO: implement error recovery hook via error handler return values\n */\nexport class EscapeSequenceParser extends Disposable implements IEscapeSequenceParser {\n public initialState: number;\n public currentState: number;\n public precedingJoinState: number; // UnicodeJoinProperties\n\n // buffers over several parse calls\n protected _params: Params;\n protected _collect: number;\n\n // handler lookup containers\n protected _printHandler: PrintHandlerType;\n protected _executeHandlers: { [flag: number]: ExecuteHandlerType };\n // fast path for EXE bytes < 0x18\n protected _executeHandlersArr: (ExecuteHandlerType | undefined)[];\n protected _csiHandlers: IHandlerCollection;\n protected _escHandlers: IHandlerCollection;\n protected readonly _oscParser: IOscParser;\n protected readonly _dcsParser: IDcsParser;\n protected readonly _apcParser: IApcParser;\n protected _errorHandler: (state: IParsingState) => IParsingState;\n\n // fallback handlers\n protected _printHandlerFb: PrintFallbackHandlerType;\n protected _executeHandlerFb: ExecuteFallbackHandlerType;\n protected _csiHandlerFb: CsiFallbackHandlerType;\n protected _escHandlerFb: EscFallbackHandlerType;\n protected _errorHandlerFb: (state: IParsingState) => IParsingState;\n\n // parser stack save for async handler support\n protected _parseStack: IParserStackState = {\n state: ParserStackType.NONE,\n handlers: [],\n handlerPos: 0,\n transition: 0,\n chunkPos: 0\n };\n\n constructor(\n protected readonly _transitions: TransitionTable = VT500_TRANSITION_TABLE\n ) {\n super();\n\n this.initialState = ParserState.GROUND;\n this.currentState = this.initialState;\n this._params = new Params(); // defaults to 32 storable params/subparams\n this._params.addParam(0); // ZDM\n this._collect = 0;\n this.precedingJoinState = 0;\n\n // set default fallback handlers and handler lookup containers\n this._printHandlerFb = (data, start, end): void => { };\n this._executeHandlerFb = (code: number): void => { };\n this._csiHandlerFb = (ident: number, params: IParams): void => { };\n this._escHandlerFb = (ident: number): void => { };\n this._errorHandlerFb = (state: IParsingState): IParsingState => state;\n this._printHandler = this._printHandlerFb;\n this._executeHandlers = Object.create(null);\n this._executeHandlersArr = new Array(0x18).fill(undefined);\n this._csiHandlers = Object.create(null);\n this._escHandlers = Object.create(null);\n this._register(toDisposable(() => {\n this._csiHandlers = Object.create(null);\n this._executeHandlers = Object.create(null);\n this._executeHandlersArr = new Array(0x18).fill(undefined);\n this._escHandlers = Object.create(null);\n }));\n this._oscParser = this._register(new OscParser());\n this._dcsParser = this._register(new DcsParser());\n this._apcParser = this._register(new ApcParser());\n this._errorHandler = this._errorHandlerFb;\n\n // swallow 7bit ST (ESC+\\)\n this.registerEscHandler({ final: '\\\\' }, () => true);\n }\n\n protected _identifier(id: IFunctionIdentifier, finalRange: number[] = [0x40, 0x7e]): number {\n let res = 0;\n if (id.prefix) {\n if (id.prefix.length > 1) {\n throw new Error('only one byte as prefix supported');\n }\n res = id.prefix.charCodeAt(0);\n if (res < 0x3c || res > 0x3f) {\n throw new Error('prefix must be in range 0x3c .. 0x3f');\n }\n }\n if (id.intermediates) {\n if (id.intermediates.length > 2) {\n throw new Error('only two bytes as intermediates are supported');\n }\n for (let i = 0; i < id.intermediates.length; ++i) {\n const intermediate = id.intermediates.charCodeAt(i);\n if (0x20 > intermediate || intermediate > 0x2f) {\n throw new Error('intermediate must be in range 0x20 .. 0x2f');\n }\n res <<= 8;\n res |= intermediate;\n }\n }\n if (id.final.length !== 1) {\n throw new Error('final must be a single byte');\n }\n const finalCode = id.final.charCodeAt(0);\n if (finalRange[0] > finalCode || finalCode > finalRange[1]) {\n throw new Error(`final must be in range ${finalRange[0]} .. ${finalRange[1]}`);\n }\n res <<= 8;\n res |= finalCode;\n\n return res;\n }\n\n public identToString(ident: number): string {\n const res: string[] = [];\n while (ident) {\n res.push(String.fromCharCode(ident & 0xFF));\n ident >>= 8;\n }\n return res.reverse().join('');\n }\n\n public setPrintHandler(handler: PrintHandlerType): void {\n this._printHandler = handler;\n }\n public clearPrintHandler(): void {\n this._printHandler = this._printHandlerFb;\n }\n\n public registerEscHandler(id: IFunctionIdentifier, handler: EscHandlerType): IDisposable {\n const ident = this._identifier(id, [0x30, 0x7e]);\n this._escHandlers[ident] ??= [];\n const handlerList = this._escHandlers[ident];\n handlerList.push(handler);\n return {\n dispose: () => {\n const handlerIndex = handlerList.indexOf(handler);\n if (handlerIndex !== -1) {\n handlerList.splice(handlerIndex, 1);\n }\n }\n };\n }\n public clearEscHandler(id: IFunctionIdentifier): void {\n if (this._escHandlers[this._identifier(id, [0x30, 0x7e])]) delete this._escHandlers[this._identifier(id, [0x30, 0x7e])];\n }\n public setEscHandlerFallback(handler: EscFallbackHandlerType): void {\n this._escHandlerFb = handler;\n }\n\n public setExecuteHandler(flag: string, handler: ExecuteHandlerType): void {\n const code = flag.charCodeAt(0);\n this._executeHandlers[code] = handler;\n if (code < 0x18) this._executeHandlersArr[code] = handler;\n }\n public clearExecuteHandler(flag: string): void {\n const code = flag.charCodeAt(0);\n if (this._executeHandlers[code]) delete this._executeHandlers[code];\n if (code < 0x18) this._executeHandlersArr[code] = undefined;\n }\n public setExecuteHandlerFallback(handler: ExecuteFallbackHandlerType): void {\n this._executeHandlerFb = handler;\n }\n\n public registerCsiHandler(id: IFunctionIdentifier, handler: CsiHandlerType): IDisposable {\n const ident = this._identifier(id);\n this._csiHandlers[ident] ??= [];\n const handlerList = this._csiHandlers[ident];\n handlerList.push(handler);\n return {\n dispose: () => {\n const handlerIndex = handlerList.indexOf(handler);\n if (handlerIndex !== -1) {\n handlerList.splice(handlerIndex, 1);\n }\n }\n };\n }\n public clearCsiHandler(id: IFunctionIdentifier): void {\n if (this._csiHandlers[this._identifier(id)]) delete this._csiHandlers[this._identifier(id)];\n }\n public setCsiHandlerFallback(callback: (ident: number, params: IParams) => void): void {\n this._csiHandlerFb = callback;\n }\n\n public registerDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): IDisposable {\n return this._dcsParser.registerHandler(this._identifier(id), handler);\n }\n public clearDcsHandler(id: IFunctionIdentifier): void {\n this._dcsParser.clearHandler(this._identifier(id));\n }\n public setDcsHandlerFallback(handler: DcsFallbackHandlerType): void {\n this._dcsParser.setHandlerFallback(handler);\n }\n\n public registerOscHandler(ident: number, handler: IOscHandler): IDisposable {\n return this._oscParser.registerHandler(ident, handler);\n }\n public clearOscHandler(ident: number): void {\n this._oscParser.clearHandler(ident);\n }\n public setOscHandlerFallback(handler: OscFallbackHandlerType): void {\n this._oscParser.setHandlerFallback(handler);\n }\n\n public registerApcHandler(id: IFunctionIdentifier, handler: IApcHandler): IDisposable {\n id.prefix = undefined; // APC does not support prefix byte\n return this._apcParser.registerHandler(this._identifier(id, [0x30, 0x7e]), handler);\n }\n public clearApcHandler(id: IFunctionIdentifier): void {\n id.prefix = undefined; // APC does not support prefix byte\n this._apcParser.clearHandler(this._identifier(id, [0x30, 0x7e]));\n }\n public setApcHandlerFallback(handler: ApcFallbackHandlerType): void {\n this._apcParser.setHandlerFallback(handler);\n }\n\n public setErrorHandler(callback: (state: IParsingState) => IParsingState): void {\n this._errorHandler = callback;\n }\n public clearErrorHandler(): void {\n this._errorHandler = this._errorHandlerFb;\n }\n\n /**\n * Reset parser to initial values.\n *\n * This can also be used to lift the improper continuation error condition\n * when dealing with async handlers. Use this only as a last resort to silence\n * that error when the terminal has no pending data to be processed. Note that\n * the interrupted async handler might continue its work in the future messing\n * up the terminal state even further.\n */\n public reset(): void {\n this.currentState = this.initialState;\n this._oscParser.reset();\n this._dcsParser.reset();\n this._apcParser.reset();\n this._params.resetZdm();\n this._collect = 0;\n this.precedingJoinState = 0;\n // abort pending continuation from async handler\n // Here the RESET type indicates, that the next parse call will\n // ignore any saved stack, instead continues sync with next codepoint from GROUND\n if (this._parseStack.state !== ParserStackType.NONE) {\n this._parseStack.state = ParserStackType.RESET;\n this._parseStack.handlers = []; // also release handlers ref\n }\n }\n\n /**\n * Async parse support.\n */\n protected _preserveStack(\n state: ParserStackType,\n handlers: ResumableHandlersType,\n handlerPos: number,\n transition: number,\n chunkPos: number\n ): void {\n this._parseStack.state = state;\n this._parseStack.handlers = handlers;\n this._parseStack.handlerPos = handlerPos;\n this._parseStack.transition = transition;\n this._parseStack.chunkPos = chunkPos;\n }\n\n /**\n * Parse UTF32 codepoints in `data` up to `length`.\n *\n * Note: For several actions with high data load the parsing is optimized\n * by using local read ahead loops with hardcoded conditions to\n * avoid costly table lookups. Make sure that any change of table values\n * will be reflected in the loop conditions as well and vice versa.\n * Affected states/actions:\n * - GROUND:PRINT\n * - CSI_PARAM:PARAM\n * - DCS_PARAM:PARAM\n * - OSC_STRING:OSC_PUT\n * - DCS_PASSTHROUGH:DCS_PUT\n *\n * Additionally the following fast paths exist before the table lookup:\n * - EXE bytes < 0x18 in non-payload states (avoids table lookup entirely)\n * - 7-bit CSI sequences without intermediates (ESC [ params final)\n *\n * Note on asynchronous handler support:\n * Any handler returning a promise will be treated as asynchronous.\n * To keep the in-band blocking working for async handlers, `parse` pauses execution,\n * creates a stack save and returns the promise to the caller.\n * For proper continuation of the paused state it is important\n * to await the promise resolving. On resolve the parse must be repeated\n * with the same chunk of data and the resolved value in `promiseResult`\n * until no promise is returned.\n *\n * Important: With only sync handlers defined, parsing is completely synchronous as well.\n * As soon as an async handler is involved, synchronous parsing is not possible anymore.\n *\n * Boilerplate for proper parsing of multiple chunks with async handlers:\n *\n * ```typescript\n * async function parseMultipleChunks(chunks: Uint32Array[]): Promise {\n * for (const chunk of chunks) {\n * let result: void | Promise;\n * let prev: boolean | undefined;\n * while (result = parser.parse(chunk, chunk.length, prev)) {\n * prev = await result;\n * }\n * }\n * // finished parsing all chunks...\n * }\n * ```\n */\n public parse(data: Uint32Array, length: number, promiseResult?: boolean): void | Promise {\n let code: number;\n let transition: number;\n let start = 0;\n let handlerResult: void | boolean | Promise;\n\n // resume from async handler\n if (this._parseStack.state) {\n // allow sync parser reset even in continuation mode\n // Note: can be used to recover parser from improper continuation error below\n if (this._parseStack.state === ParserStackType.RESET) {\n this._parseStack.state = ParserStackType.NONE;\n start = this._parseStack.chunkPos + 1; // continue with next codepoint in GROUND\n } else {\n if (promiseResult === undefined || this._parseStack.state === ParserStackType.FAIL) {\n /**\n * Reject further parsing on improper continuation after pausing. This is a really bad\n * condition with screwed up execution order and prolly messed up terminal state,\n * therefore we exit hard with an exception and reject any further parsing.\n *\n * Note: With `Terminal.write` usage this exception should never occur, as the top level\n * calls are guaranteed to handle async conditions properly. If you ever encounter this\n * exception in your terminal integration it indicates, that you injected data chunks to\n * `InputHandler.parse` or `EscapeSequenceParser.parse` synchronously without waiting for\n * continuation of a running async handler.\n *\n * It is possible to get rid of this error by calling `reset`. But dont rely on that, as\n * the pending async handler still might mess up the terminal later. Instead fix the\n * faulty async handling, so this error will not be thrown anymore.\n */\n this._parseStack.state = ParserStackType.FAIL;\n throw new Error('improper continuation due to previous async handler, giving up parsing');\n }\n\n // we have to resume the old handler loop if:\n // - return value of the promise was `false`\n // - handlers are not exhausted yet\n const handlers = this._parseStack.handlers;\n let handlerPos = this._parseStack.handlerPos - 1;\n switch (this._parseStack.state) {\n case ParserStackType.CSI:\n if (promiseResult === false && handlerPos > -1) {\n for (; handlerPos >= 0; handlerPos--) {\n handlerResult = (handlers as CsiHandlerType[])[handlerPos](this._params);\n if (handlerResult === true) {\n break;\n } else if (handlerResult instanceof Promise) {\n this._parseStack.handlerPos = handlerPos;\n return handlerResult;\n }\n }\n }\n this._parseStack.handlers = [];\n break;\n case ParserStackType.ESC:\n if (promiseResult === false && handlerPos > -1) {\n for (; handlerPos >= 0; handlerPos--) {\n handlerResult = (handlers as EscHandlerType[])[handlerPos]();\n if (handlerResult === true) {\n break;\n } else if (handlerResult instanceof Promise) {\n this._parseStack.handlerPos = handlerPos;\n return handlerResult;\n }\n }\n }\n this._parseStack.handlers = [];\n break;\n case ParserStackType.DCS:\n code = data[this._parseStack.chunkPos];\n handlerResult = this._dcsParser.unhook(code !== 0x18 && code !== 0x1a, promiseResult);\n if (handlerResult) {\n return handlerResult;\n }\n if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE;\n this._params.resetZdm();\n this._collect = 0;\n break;\n case ParserStackType.OSC:\n code = data[this._parseStack.chunkPos];\n handlerResult = this._oscParser.end(code !== 0x18 && code !== 0x1a, promiseResult);\n if (handlerResult) {\n return handlerResult;\n }\n if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE;\n this._params.resetZdm();\n this._collect = 0;\n break;\n case ParserStackType.APC:\n code = data[this._parseStack.chunkPos];\n handlerResult = this._apcParser.end(code !== 0x18 && code !== 0x1a, promiseResult);\n if (handlerResult) {\n return handlerResult;\n }\n if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE;\n this._params.resetZdm();\n this._collect = 0;\n break;\n }\n // cleanup before continuing with the main sync loop\n this._parseStack.state = ParserStackType.NONE;\n start = this._parseStack.chunkPos + 1;\n this.precedingJoinState = 0;\n this.currentState = this._parseStack.transition & TableAccess.TRANSITION_STATE_MASK;\n }\n }\n\n // continue with main sync loop\n\n // process input string\n for (let i = start; i < length; ++i) {\n code = data[i];\n\n // EXE fast-path: common control bytes (0x00-0x17) in non-payload states\n if (code < 0x18 && this.currentState <= ParserState.CSI_IGNORE) {\n (this._executeHandlersArr[code] ?? this._executeHandlerFb)(code);\n this.precedingJoinState = 0;\n continue;\n }\n\n // CSI fast-path: collapse ESC [ into a single entry, parse params+final in a tight loop\n if (code === 0x1b\n && this.currentState < ParserState.OSC_STRING\n && i + 2 < length && data[i + 1] === 0x5b\n ) {\n this._params.resetZdm();\n this._collect = 0;\n let k = i + 2;\n let ch = data[k];\n if (ch >= 0x3c && ch <= 0x3f) {\n this._collect = ch;\n k++;\n }\n let csiDone = false;\n for (; k < length; k++) {\n ch = data[k];\n if (ch >= 0x30 && ch <= 0x39) {\n this._params.addDigit(ch - 48);\n } else if (ch === 0x3b) {\n this._params.addParam(0);\n } else if (ch === 0x3a) {\n this._params.addSubParam(-1);\n } else if (ch >= 0x40 && ch <= 0x7e) {\n const handlers = this._csiHandlers[this._collect << 8 | ch];\n let j = handlers ? handlers.length - 1 : -1;\n for (; j >= 0; j--) {\n handlerResult = handlers[j](this._params);\n if (handlerResult === true) {\n break;\n } else if (handlerResult instanceof Promise) {\n transition = ParserAction.CSI_DISPATCH << TableAccess.TRANSITION_ACTION_SHIFT | ParserState.GROUND;\n this._preserveStack(ParserStackType.CSI, handlers, j, transition, k);\n return handlerResult;\n }\n }\n if (j < 0) {\n this._csiHandlerFb(this._collect << 8 | ch, this._params);\n }\n this.precedingJoinState = 0;\n i = k;\n this.currentState = ParserState.GROUND;\n csiDone = true;\n break;\n } else {\n break;\n }\n }\n if (!csiDone) {\n i = k - 1;\n this.currentState = ParserState.CSI_PARAM;\n }\n continue;\n }\n\n // normal transition & action lookup\n transition = this._transitions.table[\n this.currentState << TableAccess.INDEX_STATE_SHIFT |\n (code < NON_ASCII_PRINTABLE ? code : NON_ASCII_PRINTABLE)\n ];\n switch (transition >> TableAccess.TRANSITION_ACTION_SHIFT) {\n case ParserAction.PRINT:\n // Note: 0x20 (SP) is included, 0x7F (DEL) is excluded\n let c = i;\n const l4 = length - 4;\n while (c < l4\n && data[++c] >= 0x20 && (data[c] <= 0x7e || data[c] >= NON_ASCII_PRINTABLE)\n && data[++c] >= 0x20 && (data[c] <= 0x7e || data[c] >= NON_ASCII_PRINTABLE)\n && data[++c] >= 0x20 && (data[c] <= 0x7e || data[c] >= NON_ASCII_PRINTABLE)\n && data[++c] >= 0x20 && (data[c] <= 0x7e || data[c] >= NON_ASCII_PRINTABLE)\n ) {}\n if (c >= l4) {\n while (c < length && data[c] >= 0x20 && (data[c] <= 0x7e || data[c] >= NON_ASCII_PRINTABLE)) {\n c++;\n }\n }\n this._printHandler(data, i, c);\n i = c - 1;\n break;\n case ParserAction.EXECUTE:\n if (this._executeHandlers[code]) this._executeHandlers[code]();\n else this._executeHandlerFb(code);\n this.precedingJoinState = 0;\n break;\n case ParserAction.IGNORE:\n break;\n case ParserAction.ERROR:\n const inject: IParsingState = this._errorHandler(\n {\n position: i,\n code,\n currentState: this.currentState,\n collect: this._collect,\n params: this._params,\n abort: false\n });\n if (inject.abort) return;\n // inject values: currently not implemented\n break;\n case ParserAction.CSI_DISPATCH:\n // Trigger CSI Handler\n const handlers = this._csiHandlers[this._collect << 8 | code];\n let j = handlers ? handlers.length - 1 : -1;\n for (; j >= 0; j--) {\n // true means success and to stop bubbling\n // a promise indicates an async handler that needs to finish before progressing\n handlerResult = handlers[j](this._params);\n if (handlerResult === true) {\n break;\n } else if (handlerResult instanceof Promise) {\n this._preserveStack(ParserStackType.CSI, handlers, j, transition, i);\n return handlerResult;\n }\n }\n if (j < 0) {\n this._csiHandlerFb(this._collect << 8 | code, this._params);\n }\n this.precedingJoinState = 0;\n break;\n case ParserAction.PARAM:\n // inner loop: digits (0x30 - 0x39) and ; (0x3b) and : (0x3a)\n do {\n switch (code) {\n case 0x3b:\n this._params.addParam(0); // ZDM\n break;\n case 0x3a:\n this._params.addSubParam(-1);\n break;\n default: // 0x30 - 0x39\n this._params.addDigit(code - 48);\n }\n } while (++i < length && (code = data[i]) > 0x2f && code < 0x3c);\n i--;\n break;\n case ParserAction.COLLECT:\n this._collect <<= 8;\n this._collect |= code;\n break;\n case ParserAction.ESC_DISPATCH:\n const handlersEsc = this._escHandlers[this._collect << 8 | code];\n let jj = handlersEsc ? handlersEsc.length - 1 : -1;\n for (; jj >= 0; jj--) {\n // true means success and to stop bubbling\n // a promise indicates an async handler that needs to finish before progressing\n handlerResult = handlersEsc[jj]();\n if (handlerResult === true) {\n break;\n } else if (handlerResult instanceof Promise) {\n this._preserveStack(ParserStackType.ESC, handlersEsc, jj, transition, i);\n return handlerResult;\n }\n }\n if (jj < 0) {\n this._escHandlerFb(this._collect << 8 | code);\n }\n this.precedingJoinState = 0;\n break;\n case ParserAction.CLEAR:\n this._params.resetZdm();\n this._collect = 0;\n break;\n case ParserAction.DCS_HOOK:\n this._dcsParser.hook(this._collect << 8 | code, this._params);\n break;\n case ParserAction.DCS_PUT:\n // inner loop - exit DCS_PUT: 0x18, 0x1a, 0x1b, 0x7f, 0x80 - 0x9f\n // unhook triggered by: 0x1b, 0x9c (success) and 0x18, 0x1a (abort)\n for (let j = i + 1; ; ++j) {\n if (j >= length || (code = data[j]) === 0x18 || code === 0x1a || code === 0x1b || (code > 0x7f && code < NON_ASCII_PRINTABLE)) {\n this._dcsParser.put(data, i, j);\n i = j - 1;\n break;\n }\n }\n break;\n case ParserAction.DCS_UNHOOK:\n handlerResult = this._dcsParser.unhook(code !== 0x18 && code !== 0x1a);\n if (handlerResult) {\n this._preserveStack(ParserStackType.DCS, [], 0, transition, i);\n return handlerResult;\n }\n if (code === 0x1b) transition |= ParserState.ESCAPE;\n this._params.resetZdm();\n this._collect = 0;\n this.precedingJoinState = 0;\n break;\n case ParserAction.OSC_START:\n this._oscParser.start();\n break;\n case ParserAction.OSC_PUT:\n // inner loop: 0x20 (SP) included, 0x7F (DEL) included\n for (let j = i + 1; ; j++) {\n if (j >= length || (code = data[j]) < 0x20 || (code > 0x7f && code < NON_ASCII_PRINTABLE)) {\n this._oscParser.put(data, i, j);\n i = j - 1;\n break;\n }\n }\n break;\n case ParserAction.OSC_END:\n handlerResult = this._oscParser.end(code !== 0x18 && code !== 0x1a);\n if (handlerResult) {\n this._preserveStack(ParserStackType.OSC, [], 0, transition, i);\n return handlerResult;\n }\n if (code === 0x1b) transition |= ParserState.ESCAPE;\n this._params.resetZdm();\n this._collect = 0;\n this.precedingJoinState = 0;\n break;\n case ParserAction.APC_START:\n this._apcParser.start(this._collect << 8 | code);\n break;\n case ParserAction.APC_PUT:\n // inner loop - exit APC_PUT: 0x18, 0x1a, 0x1b, 0x9c\n // allowed: 00/08 .. 00/13, 02/00 .. 07/14 + NON_ASCII_PRINTABLE\n for (let j = i + 1; ; ++j) {\n if (j < length && (\n (data[j] >= 0x20 && data[j] < 0x7f) || (data[j] >= 0x08 && data[j] < 0x0e) || data[j] >= NON_ASCII_PRINTABLE\n )) continue;\n this._apcParser.put(data, i, j);\n i = j - 1;\n break;\n }\n break;\n case ParserAction.APC_END:\n handlerResult = this._apcParser.end(code !== 0x18 && code !== 0x1a);\n if (handlerResult) {\n this._preserveStack(ParserStackType.APC, [], 0, transition, i);\n return handlerResult;\n }\n if (code === 0x1b) transition |= ParserState.ESCAPE;\n this._params.resetZdm();\n this._collect = 0;\n this.precedingJoinState = 0;\n break;\n }\n this.currentState = transition & TableAccess.TRANSITION_STATE_MASK;\n }\n }\n}\n", "/**\n * Copyright (c) 2021 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\n\n// 'rgb:' rule - matching: r/g/b | rr/gg/bb | rrr/ggg/bbb | rrrr/gggg/bbbb (hex digits)\nconst RGB_REX = /^([\\da-f])\\/([\\da-f])\\/([\\da-f])$|^([\\da-f]{2})\\/([\\da-f]{2})\\/([\\da-f]{2})$|^([\\da-f]{3})\\/([\\da-f]{3})\\/([\\da-f]{3})$|^([\\da-f]{4})\\/([\\da-f]{4})\\/([\\da-f]{4})$/;\n// '#...' rule - matching any hex digits\nconst HASH_REX = /^[\\da-f]+$/;\n\n/**\n * Parse color spec to RGB values (8 bit per channel).\n * See `man xparsecolor` for details about certain format specifications.\n *\n * Supported formats:\n * - rgb:// with , , in h | hh | hhh | hhhh\n * - #RGB, #RRGGBB, #RRRGGGBBB, #RRRRGGGGBBBB\n *\n * All other formats like rgbi: or device-independent string specifications\n * with float numbering are not supported.\n */\nexport function parseColor(data: string): [number, number, number] | undefined {\n if (!data) return;\n // also handle uppercases\n let low = data.toLowerCase();\n if (low.startsWith('rgb:')) {\n // 'rgb:' specifier\n low = low.slice(4);\n const m = RGB_REX.exec(low);\n if (m) {\n const base = m[1] ? 15 : m[4] ? 255 : m[7] ? 4095 : 65535;\n return [\n Math.round(parseInt(m[1] || m[4] || m[7] || m[10], 16) / base * 255),\n Math.round(parseInt(m[2] || m[5] || m[8] || m[11], 16) / base * 255),\n Math.round(parseInt(m[3] || m[6] || m[9] || m[12], 16) / base * 255)\n ];\n }\n } else if (low.startsWith('#')) {\n // '#' specifier\n low = low.slice(1);\n if (HASH_REX.exec(low) && [3, 6, 9, 12].includes(low.length)) {\n const adv = low.length / 3;\n const result: [number, number, number] = [0, 0, 0];\n for (let i = 0; i < 3; ++i) {\n const c = parseInt(low.slice(adv * i, adv * i + adv), 16);\n result[i] = adv === 1 ? c << 4 : adv === 2 ? c : adv === 3 ? c >> 4 : c >> 8;\n }\n return result;\n }\n }\n\n // Named colors are currently not supported due to the large addition to the xterm.js bundle size\n // they would add. In order to support named colors, we would need some way of optionally loading\n // additional payloads so startup/download time is not bloated (see #3530).\n}\n\n// pad hex output to requested bit width\nfunction pad(n: number, bits: number): string {\n const s = n.toString(16);\n const s2 = s.length < 2 ? '0' + s : s;\n switch (bits) {\n case 4:\n return s[0];\n case 8:\n return s2;\n case 12:\n return (s2 + s2).slice(0, 3);\n default:\n return s2 + s2;\n }\n}\n\n/**\n * Convert a given color to rgb:../../.. string of `bits` depth.\n */\nexport function toRgbString(color: [number, number, number], bits: number = 16): string {\n const [r, g, b] = color;\n return `rgb:${pad(r, bits)}/${pad(g, bits)}/${pad(b, bits)}`;\n}\n", "/**\n * Copyright (c) 2025 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\n/**\n * The xterm.js version. This is updated by the publish script from package.json.\n */\nexport const XTERM_VERSION = '6.0.0';\n", "/**\n * Copyright (c) 2014 The xterm.js authors. All rights reserved.\n * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)\n * @license MIT\n */\n\nimport { IInputHandler, IDisposable, IWindowOptions, IColorEvent, IParseStack, ColorIndex, ColorRequestType, SpecialColorIndex } from './Types';\nimport { IAttributeData, IBuffer } from './buffer/Types';\nimport { C0, C1 } from './data/EscapeSequences';\nimport { CHARSETS, DEFAULT_CHARSET } from './data/Charsets';\nimport { EscapeSequenceParser } from './parser/EscapeSequenceParser';\nimport { Disposable } from './Lifecycle';\nimport { StringToUtf32, stringFromCodePoint, Utf8ToUtf32 } from './input/TextDecoder';\nimport { BufferLine, DEFAULT_ATTR_DATA } from './buffer/BufferLine';\nimport { IParsingState, IEscapeSequenceParser, IParams, IFunctionIdentifier } from './parser/Types';\nimport { NULL_CELL_CODE, NULL_CELL_WIDTH, Attributes, FgFlags, BgFlags, Content, UnderlineStyle } from './buffer/Constants';\nimport { CellData } from './buffer/CellData';\nimport { AttributeData } from './buffer/AttributeData';\nimport { ICoreService, IBufferService, IOptionsService, ILogService, IMouseStateService, ICharsetService, IUnicodeService, LogLevelEnum, IOscLinkService } from './services/Services';\nimport { UnicodeService } from './services/UnicodeService';\nimport { OscHandler } from './parser/OscParser';\nimport { DcsHandler } from './parser/DcsParser';\nimport { ApcHandler } from './parser/ApcParser';\nimport { parseColor } from './input/XParseColor';\nimport { Emitter } from './Event';\nimport { XTERM_VERSION } from './Version';\n\n/**\n * Map collect to glevel. Used in `selectCharset`.\n */\nconst GLEVEL: { [key: string]: number } = { '(': 0, ')': 1, '*': 2, '+': 3, '-': 1, '.': 2 };\n\n/**\n * Document xterm VT features here that are currently unsupported\n */\n// @vt: #N DCS DECUDK \"User Defined Keys\" \"DCS Ps ; Ps \\| Pt ST\" \"Definitions for user-defined keys.\"\n// @vt: #N DCS XTGETTCAP \"Request Terminfo String\" \"DCS + q Pt ST\" \"Request Terminfo String.\"\n// @vt: #N DCS XTSETTCAP \"Set Terminfo Data\" \"DCS + p Pt ST\" \"Set Terminfo Data.\"\n// @vt: #N OSC 1 \"Set Icon Name\" \"OSC 1 ; Pt BEL\" \"Set icon name.\"\n\n/**\n * Max length of the UTF32 input buffer. Real memory consumption is 4 times higher.\n */\nconst enum Constants {\n MAX_PARSEBUFFER_LENGTH = 131072,\n /** Limit length of title and icon name stacks. */\n STACK_LIMIT = 10,\n // create a warning log if an async handler takes longer than the limit (in ms)\n SLOW_ASYNC_LIMIT = 5000\n}\n\n// map params to window option\nfunction paramToWindowOption(n: number, opts: IWindowOptions): boolean {\n if (n > 24) {\n return opts.setWinLines || false;\n }\n switch (n) {\n case 1: return !!opts.restoreWin;\n case 2: return !!opts.minimizeWin;\n case 3: return !!opts.setWinPosition;\n case 4: return !!opts.setWinSizePixels;\n case 5: return !!opts.raiseWin;\n case 6: return !!opts.lowerWin;\n case 7: return !!opts.refreshWin;\n case 8: return !!opts.setWinSizeChars;\n case 9: return !!opts.maximizeWin;\n case 10: return !!opts.fullscreenWin;\n case 11: return !!opts.getWinState;\n case 13: return !!opts.getWinPosition;\n case 14: return !!opts.getWinSizePixels;\n case 15: return !!opts.getScreenSizePixels;\n case 16: return !!opts.getCellSizePixels;\n case 18: return !!opts.getWinSizeChars;\n case 19: return !!opts.getScreenSizeChars;\n case 20: return !!opts.getIconTitle;\n case 21: return !!opts.getWinTitle;\n case 22: return !!opts.pushTitle;\n case 23: return !!opts.popTitle;\n case 24: return !!opts.setWinLines;\n }\n return false;\n}\n\nexport enum WindowsOptionsReportType {\n GET_WIN_SIZE_PIXELS = 0,\n GET_CELL_SIZE_PIXELS = 1\n}\n\n// Work variables to avoid garbage collection\nlet $temp = 0;\n\n/**\n * The terminal's standard implementation of IInputHandler, this handles all\n * input from the Parser.\n *\n * Refer to http://invisible-island.net/xterm/ctlseqs/ctlseqs.html to understand\n * each function's header comment.\n */\nexport class InputHandler extends Disposable implements IInputHandler {\n private _parseBuffer: Uint32Array = new Uint32Array(4096);\n private _stringDecoder: StringToUtf32 = new StringToUtf32();\n private _utf8Decoder: Utf8ToUtf32 = new Utf8ToUtf32();\n private _windowTitle = '';\n private _iconName = '';\n private _dirtyRowTracker: IDirtyRowTracker;\n protected _windowTitleStack: string[] = [];\n protected _iconNameStack: string[] = [];\n\n private _curAttrData: IAttributeData = DEFAULT_ATTR_DATA.clone();\n public getAttrData(): IAttributeData { return this._curAttrData; }\n private _eraseAttrDataInternal: IAttributeData = DEFAULT_ATTR_DATA.clone();\n\n private _activeBuffer: IBuffer;\n\n private readonly _onRequestBell = this._register(new Emitter());\n public readonly onRequestBell = this._onRequestBell.event;\n private readonly _onRequestRefreshRows = this._register(new Emitter<{ start: number, end: number } | undefined>());\n public readonly onRequestRefreshRows = this._onRequestRefreshRows.event;\n private readonly _onRequestReset = this._register(new Emitter());\n public readonly onRequestReset = this._onRequestReset.event;\n private readonly _onRequestSendFocus = this._register(new Emitter());\n public readonly onRequestSendFocus = this._onRequestSendFocus.event;\n private readonly _onRequestSyncScrollBar = this._register(new Emitter());\n public readonly onRequestSyncScrollBar = this._onRequestSyncScrollBar.event;\n private readonly _onRequestWindowsOptionsReport = this._register(new Emitter());\n public readonly onRequestWindowsOptionsReport = this._onRequestWindowsOptionsReport.event;\n\n private readonly _onA11yChar = this._register(new Emitter());\n public readonly onA11yChar = this._onA11yChar.event;\n private readonly _onA11yTab = this._register(new Emitter());\n public readonly onA11yTab = this._onA11yTab.event;\n private readonly _onCursorMove = this._register(new Emitter());\n public readonly onCursorMove = this._onCursorMove.event;\n private readonly _onLineFeed = this._register(new Emitter());\n public readonly onLineFeed = this._onLineFeed.event;\n private readonly _onScroll = this._register(new Emitter());\n public readonly onScroll = this._onScroll.event;\n private readonly _onTitleChange = this._register(new Emitter());\n public readonly onTitleChange = this._onTitleChange.event;\n private readonly _onColor = this._register(new Emitter());\n public readonly onColor = this._onColor.event;\n private readonly _onRequestColorSchemeQuery = this._register(new Emitter());\n public readonly onRequestColorSchemeQuery = this._onRequestColorSchemeQuery.event;\n\n private _parseStack: IParseStack = {\n paused: false,\n cursorStartX: 0,\n cursorStartY: 0,\n decodedLength: 0,\n position: 0\n };\n\n constructor(\n private readonly _bufferService: IBufferService,\n private readonly _charsetService: ICharsetService,\n private readonly _coreService: ICoreService,\n private readonly _logService: ILogService,\n private readonly _optionsService: IOptionsService,\n private readonly _oscLinkService: IOscLinkService,\n private readonly _mouseStateService: IMouseStateService,\n private readonly _unicodeService: IUnicodeService,\n private readonly _parser: IEscapeSequenceParser = new EscapeSequenceParser()\n ) {\n super();\n this._register(this._parser);\n this._dirtyRowTracker = new DirtyRowTracker(this._bufferService);\n\n // Track properties used in performance critical code manually to avoid using slow getters\n this._activeBuffer = this._bufferService.buffer;\n this._register(this._bufferService.buffers.onBufferActivate(e => this._activeBuffer = e.activeBuffer));\n\n /**\n * custom fallback handlers\n */\n this._parser.setCsiHandlerFallback((ident, params) => {\n this._logService.debug('Unknown CSI code: ', { identifier: this._parser.identToString(ident), params: params.toArray() });\n });\n this._parser.setEscHandlerFallback(ident => {\n this._logService.debug('Unknown ESC code: ', { identifier: this._parser.identToString(ident) });\n });\n this._parser.setExecuteHandlerFallback(code => {\n this._logService.debug('Unknown EXECUTE code: ', { code });\n });\n this._parser.setOscHandlerFallback((identifier, action, data) => {\n this._logService.debug('Unknown OSC code: ', { identifier, action, data });\n });\n this._parser.setDcsHandlerFallback((ident, action, payload) => {\n if (action === 'HOOK') {\n payload = payload.toArray();\n }\n this._logService.debug('Unknown DCS code: ', { identifier: this._parser.identToString(ident), action, payload });\n });\n this._parser.setApcHandlerFallback((ident, action, payload) => {\n this._logService.debug('Unknown APC code: ', { identifier: this._parser.identToString(ident), action, payload });\n });\n\n /**\n * print handler\n */\n this._parser.setPrintHandler((data, start, end) => this.print(data, start, end));\n\n /**\n * CSI handler\n */\n this._parser.registerCsiHandler({ final: '@' }, params => this.insertChars(params));\n this._parser.registerCsiHandler({ intermediates: ' ', final: '@' }, params => this.scrollLeft(params));\n this._parser.registerCsiHandler({ final: 'A' }, params => this.cursorUp(params));\n this._parser.registerCsiHandler({ intermediates: ' ', final: 'A' }, params => this.scrollRight(params));\n this._parser.registerCsiHandler({ final: 'B' }, params => this.cursorDown(params));\n this._parser.registerCsiHandler({ final: 'C' }, params => this.cursorForward(params));\n this._parser.registerCsiHandler({ final: 'D' }, params => this.cursorBackward(params));\n this._parser.registerCsiHandler({ final: 'E' }, params => this.cursorNextLine(params));\n this._parser.registerCsiHandler({ final: 'F' }, params => this.cursorPrecedingLine(params));\n this._parser.registerCsiHandler({ final: 'G' }, params => this.cursorCharAbsolute(params));\n this._parser.registerCsiHandler({ final: 'H' }, params => this.cursorPosition(params));\n this._parser.registerCsiHandler({ final: 'I' }, params => this.cursorForwardTab(params));\n this._parser.registerCsiHandler({ final: 'J' }, params => this.eraseInDisplay(params, false));\n this._parser.registerCsiHandler({ prefix: '?', final: 'J' }, params => this.eraseInDisplay(params, true));\n this._parser.registerCsiHandler({ final: 'K' }, params => this.eraseInLine(params, false));\n this._parser.registerCsiHandler({ prefix: '?', final: 'K' }, params => this.eraseInLine(params, true));\n this._parser.registerCsiHandler({ final: 'L' }, params => this.insertLines(params));\n this._parser.registerCsiHandler({ final: 'M' }, params => this.deleteLines(params));\n this._parser.registerCsiHandler({ final: 'P' }, params => this.deleteChars(params));\n this._parser.registerCsiHandler({ final: 'S' }, params => this.scrollUp(params));\n this._parser.registerCsiHandler({ final: 'T' }, params => this.scrollDown(params));\n this._parser.registerCsiHandler({ final: 'X' }, params => this.eraseChars(params));\n this._parser.registerCsiHandler({ final: 'Z' }, params => this.cursorBackwardTab(params));\n this._parser.registerCsiHandler({ final: '^' }, params => this.scrollDown(params));\n this._parser.registerCsiHandler({ final: '`' }, params => this.charPosAbsolute(params));\n this._parser.registerCsiHandler({ final: 'a' }, params => this.hPositionRelative(params));\n this._parser.registerCsiHandler({ final: 'b' }, params => this.repeatPrecedingCharacter(params));\n this._parser.registerCsiHandler({ final: 'c' }, params => this.sendDeviceAttributesPrimary(params));\n this._parser.registerCsiHandler({ prefix: '>', final: 'c' }, params => this.sendDeviceAttributesSecondary(params));\n this._parser.registerCsiHandler({ final: 'd' }, params => this.linePosAbsolute(params));\n this._parser.registerCsiHandler({ final: 'e' }, params => this.vPositionRelative(params));\n this._parser.registerCsiHandler({ final: 'f' }, params => this.hVPosition(params));\n this._parser.registerCsiHandler({ final: 'g' }, params => this.tabClear(params));\n this._parser.registerCsiHandler({ final: 'h' }, params => this.setMode(params));\n this._parser.registerCsiHandler({ prefix: '?', final: 'h' }, params => this.setModePrivate(params));\n this._parser.registerCsiHandler({ final: 'l' }, params => this.resetMode(params));\n this._parser.registerCsiHandler({ prefix: '?', final: 'l' }, params => this.resetModePrivate(params));\n this._parser.registerCsiHandler({ final: 'm' }, params => this.charAttributes(params));\n this._parser.registerCsiHandler({ final: 'n' }, params => this.deviceStatus(params));\n this._parser.registerCsiHandler({ prefix: '?', final: 'n' }, params => this.deviceStatusPrivate(params));\n this._parser.registerCsiHandler({ intermediates: '!', final: 'p' }, params => this.softReset(params));\n this._parser.registerCsiHandler({ prefix: '>', final: 'q' }, params => this.sendXtVersion(params));\n this._parser.registerCsiHandler({ intermediates: ' ', final: 'q' }, params => this.setCursorStyle(params));\n this._parser.registerCsiHandler({ final: 'r' }, params => this.setScrollRegion(params));\n this._parser.registerCsiHandler({ final: 's' }, params => this.saveCursor(params));\n this._parser.registerCsiHandler({ final: 't' }, params => this.windowOptions(params));\n this._parser.registerCsiHandler({ final: 'u' }, params => this.restoreCursor(params));\n this._parser.registerCsiHandler({ intermediates: '\\'', final: '}' }, params => this.insertColumns(params));\n this._parser.registerCsiHandler({ intermediates: '\\'', final: '~' }, params => this.deleteColumns(params));\n this._parser.registerCsiHandler({ intermediates: '\"', final: 'q' }, params => this.selectProtected(params));\n this._parser.registerCsiHandler({ intermediates: '$', final: 'p' }, params => this.requestMode(params, true));\n this._parser.registerCsiHandler({ prefix: '?', intermediates: '$', final: 'p' }, params => this.requestMode(params, false));\n\n // Kitty keyboard protocol handlers\n this._parser.registerCsiHandler({ prefix: '=', final: 'u' }, params => this.kittyKeyboardSet(params));\n this._parser.registerCsiHandler({ prefix: '?', final: 'u' }, params => this.kittyKeyboardQuery(params));\n this._parser.registerCsiHandler({ prefix: '>', final: 'u' }, params => this.kittyKeyboardPush(params));\n this._parser.registerCsiHandler({ prefix: '<', final: 'u' }, params => this.kittyKeyboardPop(params));\n\n /**\n * execute handler\n */\n this._parser.setExecuteHandler(C0.BEL, () => this.bell());\n this._parser.setExecuteHandler(C0.LF, () => this.lineFeed());\n this._parser.setExecuteHandler(C0.VT, () => this.lineFeed());\n this._parser.setExecuteHandler(C0.FF, () => this.lineFeed());\n this._parser.setExecuteHandler(C0.CR, () => this.carriageReturn());\n this._parser.setExecuteHandler(C0.BS, () => this.backspace());\n this._parser.setExecuteHandler(C0.HT, () => this.tab());\n this._parser.setExecuteHandler(C0.SO, () => this.shiftOut());\n this._parser.setExecuteHandler(C0.SI, () => this.shiftIn());\n // FIXME: What do to with missing? Old code just added those to print.\n\n this._parser.setExecuteHandler(C1.IND, () => this.index());\n this._parser.setExecuteHandler(C1.NEL, () => this.nextLine());\n this._parser.setExecuteHandler(C1.HTS, () => this.tabSet());\n\n /**\n * OSC handler\n */\n // 0 - icon name + title\n this._parser.registerOscHandler(0, new OscHandler(data => { this.setTitle(data); this.setIconName(data); return true; }));\n // 1 - icon name\n this._parser.registerOscHandler(1, new OscHandler(data => this.setIconName(data)));\n // 2 - title\n this._parser.registerOscHandler(2, new OscHandler(data => this.setTitle(data)));\n // 3 - set property X in the form \"prop=value\"\n // 4 - Change Color Number\n this._parser.registerOscHandler(4, new OscHandler(data => this.setOrReportIndexedColor(data)));\n // 5 - Change Special Color Number\n // 6 - Enable/disable Special Color Number c\n // 7 - current directory? (not in xterm spec, see https://gitlab.com/gnachman/iterm2/issues/3939)\n // 8 - create hyperlink (not in xterm spec, see https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda)\n this._parser.registerOscHandler(8, new OscHandler(data => this.setHyperlink(data)));\n // 10 - Change VT100 text foreground color to Pt.\n this._parser.registerOscHandler(10, new OscHandler(data => this.setOrReportFgColor(data)));\n // 11 - Change VT100 text background color to Pt.\n this._parser.registerOscHandler(11, new OscHandler(data => this.setOrReportBgColor(data)));\n // 12 - Change text cursor color to Pt.\n this._parser.registerOscHandler(12, new OscHandler(data => this.setOrReportCursorColor(data)));\n // 13 - Change mouse foreground color to Pt.\n // 14 - Change mouse background color to Pt.\n // 15 - Change Tektronix foreground color to Pt.\n // 16 - Change Tektronix background color to Pt.\n // 17 - Change highlight background color to Pt.\n // 18 - Change Tektronix cursor color to Pt.\n // 19 - Change highlight foreground color to Pt.\n // 46 - Change Log File to Pt.\n // 50 - Set Font to Pt.\n // 51 - reserved for Emacs shell.\n // 52 - Manipulate Selection Data.\n // 104 ; c - Reset Color Number c.\n this._parser.registerOscHandler(104, new OscHandler(data => this.restoreIndexedColor(data)));\n // 105 ; c - Reset Special Color Number c.\n // 106 ; c; f - Enable/disable Special Color Number c.\n // 110 - Reset VT100 text foreground color.\n this._parser.registerOscHandler(110, new OscHandler(data => this.restoreFgColor(data)));\n // 111 - Reset VT100 text background color.\n this._parser.registerOscHandler(111, new OscHandler(data => this.restoreBgColor(data)));\n // 112 - Reset text cursor color.\n this._parser.registerOscHandler(112, new OscHandler(data => this.restoreCursorColor(data)));\n // 113 - Reset mouse foreground color.\n // 114 - Reset mouse background color.\n // 115 - Reset Tektronix foreground color.\n // 116 - Reset Tektronix background color.\n // 117 - Reset highlight color.\n // 118 - Reset Tektronix cursor color.\n // 119 - Reset highlight foreground color.\n\n /**\n * ESC handlers\n */\n this._parser.registerEscHandler({ final: '7' }, () => this.saveCursor());\n this._parser.registerEscHandler({ final: '8' }, () => this.restoreCursor());\n this._parser.registerEscHandler({ final: 'D' }, () => this.index());\n this._parser.registerEscHandler({ final: 'E' }, () => this.nextLine());\n this._parser.registerEscHandler({ final: 'H' }, () => this.tabSet());\n this._parser.registerEscHandler({ final: 'M' }, () => this.reverseIndex());\n this._parser.registerEscHandler({ final: '=' }, () => this.keypadApplicationMode());\n this._parser.registerEscHandler({ final: '>' }, () => this.keypadNumericMode());\n this._parser.registerEscHandler({ final: 'c' }, () => this.fullReset());\n this._parser.registerEscHandler({ final: 'n' }, () => this.setgLevel(2));\n this._parser.registerEscHandler({ final: 'o' }, () => this.setgLevel(3));\n this._parser.registerEscHandler({ final: '|' }, () => this.setgLevel(3));\n this._parser.registerEscHandler({ final: '}' }, () => this.setgLevel(2));\n this._parser.registerEscHandler({ final: '~' }, () => this.setgLevel(1));\n this._parser.registerEscHandler({ intermediates: '%', final: '@' }, () => this.selectDefaultCharset());\n this._parser.registerEscHandler({ intermediates: '%', final: 'G' }, () => this.selectDefaultCharset());\n for (const flag in CHARSETS) {\n this._parser.registerEscHandler({ intermediates: '(', final: flag }, () => this.selectCharset('(' + flag));\n this._parser.registerEscHandler({ intermediates: ')', final: flag }, () => this.selectCharset(')' + flag));\n this._parser.registerEscHandler({ intermediates: '*', final: flag }, () => this.selectCharset('*' + flag));\n this._parser.registerEscHandler({ intermediates: '+', final: flag }, () => this.selectCharset('+' + flag));\n this._parser.registerEscHandler({ intermediates: '-', final: flag }, () => this.selectCharset('-' + flag));\n this._parser.registerEscHandler({ intermediates: '.', final: flag }, () => this.selectCharset('.' + flag));\n this._parser.registerEscHandler({ intermediates: '/', final: flag }, () => this.selectCharset('/' + flag)); // TODO: supported?\n }\n this._parser.registerEscHandler({ intermediates: '#', final: '8' }, () => this.screenAlignmentPattern());\n\n /**\n * error handler\n */\n this._parser.setErrorHandler((state: IParsingState) => {\n this._logService.error('Parsing error: ', state);\n return state;\n });\n\n /**\n * DCS handler\n */\n this._parser.registerDcsHandler({ intermediates: '$', final: 'q' }, new DcsHandler((data, params) => this.requestStatusString(data, params)));\n }\n\n /**\n * Async parse support.\n */\n private _preserveStack(cursorStartX: number, cursorStartY: number, decodedLength: number, position: number): void {\n this._parseStack.paused = true;\n this._parseStack.cursorStartX = cursorStartX;\n this._parseStack.cursorStartY = cursorStartY;\n this._parseStack.decodedLength = decodedLength;\n this._parseStack.position = position;\n }\n\n private _logSlowResolvingAsync(p: Promise): void {\n // log a limited warning about an async handler taking too long\n if (this._logService.logLevel <= LogLevelEnum.WARN) {\n let slowTimeout: ReturnType | undefined;\n const slowPromise = new Promise((_res, rej) => {\n slowTimeout = setTimeout(() => rej('#SLOW_TIMEOUT'), Constants.SLOW_ASYNC_LIMIT);\n });\n Promise.race([p, slowPromise])\n .then(() => {\n if (slowTimeout !== undefined) {\n clearTimeout(slowTimeout);\n }\n }, err => {\n if (slowTimeout !== undefined) {\n clearTimeout(slowTimeout);\n }\n if (err !== '#SLOW_TIMEOUT') {\n throw err;\n }\n console.warn(`async parser handler taking longer than ${Constants.SLOW_ASYNC_LIMIT} ms`);\n });\n }\n }\n\n private _getCurrentLinkId(): number {\n return this._curAttrData.extended.urlId;\n }\n\n /**\n * Parse call with async handler support.\n *\n * Whether the stack state got preserved for the next call, is indicated by the return value:\n * - undefined (void):\n * all handlers were sync, no stack save, continue normally with next chunk\n * - Promise\\:\n * execution stopped at async handler, stack saved, continue with same chunk and the promise\n * resolve value as `promiseResult` until the method returns `undefined`\n *\n * Note: This method should only be called by `Terminal.write` to ensure correct execution order\n * and proper continuation of async parser handlers.\n */\n public parse(data: string | Uint8Array, promiseResult?: boolean): void | Promise {\n let result: void | Promise;\n let cursorStartX = this._activeBuffer.x;\n let cursorStartY = this._activeBuffer.y;\n let start = 0;\n const wasPaused = this._parseStack.paused;\n\n if (wasPaused) {\n // assumption: _parseBuffer never mutates between async calls\n if (result = this._parser.parse(this._parseBuffer, this._parseStack.decodedLength, promiseResult)) {\n this._logSlowResolvingAsync(result);\n return result;\n }\n cursorStartX = this._parseStack.cursorStartX;\n cursorStartY = this._parseStack.cursorStartY;\n this._parseStack.paused = false;\n if (data.length > Constants.MAX_PARSEBUFFER_LENGTH) {\n start = this._parseStack.position + Constants.MAX_PARSEBUFFER_LENGTH;\n }\n }\n\n // Log debug data, the log level gate is to prevent extra work in this hot path\n if (this._logService.logLevel <= LogLevelEnum.DEBUG) {\n this._logService.debug(`parsing data ${typeof data === 'string' ? ` \"${data}\"` : ` \"${Array.prototype.map.call(data, e => String.fromCharCode(e)).join('')}\"`}`);\n }\n if (this._logService.logLevel === LogLevelEnum.TRACE) {\n this._logService.trace(`parsing data (codes)`, typeof data === 'string'\n ? data.split('').map(e => e.charCodeAt(0))\n : data\n );\n }\n\n // resize input buffer if needed\n if (this._parseBuffer.length < data.length) {\n if (this._parseBuffer.length < Constants.MAX_PARSEBUFFER_LENGTH) {\n this._parseBuffer = new Uint32Array(Math.min(data.length, Constants.MAX_PARSEBUFFER_LENGTH));\n }\n }\n\n // Clear the dirty row service so we know which lines changed as a result of parsing\n // Important: do not clear between async calls, otherwise we lost pending update information.\n if (!wasPaused) {\n this._dirtyRowTracker.clearRange();\n }\n\n // process big data in smaller chunks\n if (data.length > Constants.MAX_PARSEBUFFER_LENGTH) {\n for (let i = start; i < data.length; i += Constants.MAX_PARSEBUFFER_LENGTH) {\n const end = i + Constants.MAX_PARSEBUFFER_LENGTH < data.length ? i + Constants.MAX_PARSEBUFFER_LENGTH : data.length;\n const len = (typeof data === 'string')\n ? this._stringDecoder.decode(data.substring(i, end), this._parseBuffer)\n : this._utf8Decoder.decode(data.subarray(i, end), this._parseBuffer);\n if (result = this._parser.parse(this._parseBuffer, len)) {\n this._preserveStack(cursorStartX, cursorStartY, len, i);\n this._logSlowResolvingAsync(result);\n return result;\n }\n }\n } else {\n if (!wasPaused) {\n const len = (typeof data === 'string')\n ? this._stringDecoder.decode(data, this._parseBuffer)\n : this._utf8Decoder.decode(data, this._parseBuffer);\n if (result = this._parser.parse(this._parseBuffer, len)) {\n this._preserveStack(cursorStartX, cursorStartY, len, 0);\n this._logSlowResolvingAsync(result);\n return result;\n }\n }\n }\n\n if (this._activeBuffer.x !== cursorStartX || this._activeBuffer.y !== cursorStartY) {\n this._onCursorMove.fire();\n }\n\n // Refresh any dirty rows accumulated as part of parsing, fire only for rows within the\n // _viewport_ which is relative to ydisp, not relative to ybase.\n const viewportEnd = this._dirtyRowTracker.end + (this._bufferService.buffer.ybase - this._bufferService.buffer.ydisp);\n const viewportStart = this._dirtyRowTracker.start + (this._bufferService.buffer.ybase - this._bufferService.buffer.ydisp);\n if (viewportStart < this._bufferService.rows) {\n this._onRequestRefreshRows.fire({\n start: Math.min(viewportStart, this._bufferService.rows - 1),\n end: Math.min(viewportEnd, this._bufferService.rows - 1)\n });\n }\n }\n\n public print(data: Uint32Array, start: number, end: number): void {\n let code: number;\n let chWidth: number;\n const charset = this._charsetService.charset;\n const screenReaderMode = this._optionsService.rawOptions.screenReaderMode;\n const cols = this._bufferService.cols;\n const wraparoundMode = this._coreService.decPrivateModes.wraparound;\n const insertMode = this._coreService.modes.insertMode;\n const curAttr = this._curAttrData;\n let bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y);\n\n // Defensive check: bufferRow can be undefined if a resize occurred mid-write due to async\n // scheduling gaps in WriteBuffer. See https://github.com/xtermjs/xterm.js/issues/5597\n if (!bufferRow) {\n return;\n }\n\n this._dirtyRowTracker.markDirty(this._activeBuffer.y);\n\n // handle wide chars: reset start_cell-1 if we would overwrite the second cell of a wide char\n if (this._activeBuffer.x && end - start > 0 && bufferRow.getWidth(this._activeBuffer.x - 1) === 2) {\n bufferRow.setCellFromCodepoint(this._activeBuffer.x - 1, 0, 1, curAttr);\n }\n\n let precedingJoinState = this._parser.precedingJoinState;\n for (let pos = start; pos < end; ++pos) {\n // Orca's width-one ASCII invariant excludes ZWJ joins; keep wrapping on the general path.\n if (precedingJoinState === 2 && !charset && !screenReaderMode && !insertMode\n && this._unicodeService.activeVersion === 'orca-11-zwj' && !this._getCurrentLinkId()\n && this._activeBuffer.x < cols && data[pos] >= 0x20 && data[pos] < 0x7f) {\n do {\n bufferRow.setCellFromCodepoint(this._activeBuffer.x++, data[pos], 1, curAttr);\n } while (++pos < end && this._activeBuffer.x < cols && data[pos] >= 0x20 && data[pos] < 0x7f);\n pos--;\n continue;\n }\n code = data[pos];\n\n // Soft hyphen's (U+00AD) behavior is ambiguous and differs across terminals. We opt to treat\n // it as a zero-width hint to text layout engines and simply ignore it.\n if (code === 0xAD) {\n continue;\n }\n\n // get charset replacement character\n // charset is only defined for ASCII, therefore we only\n // search for an replacement char if code < 127\n if (code < 127 && charset) {\n const ch = charset[String.fromCharCode(code)];\n if (ch) {\n code = ch.charCodeAt(0);\n }\n }\n\n const currentInfo = this._unicodeService.charProperties(code, precedingJoinState);\n chWidth = UnicodeService.extractWidth(currentInfo);\n const shouldJoin = UnicodeService.extractShouldJoin(currentInfo);\n const oldWidth = shouldJoin ? UnicodeService.extractWidth(precedingJoinState) : 0;\n precedingJoinState = currentInfo;\n\n if (screenReaderMode) {\n this._onA11yChar.fire(stringFromCodePoint(code));\n }\n const linkId = this._getCurrentLinkId();\n if (linkId) {\n this._oscLinkService.addLineToLink(linkId, this._activeBuffer.ybase + this._activeBuffer.y);\n }\n\n // goto next line if ch would overflow\n // NOTE: To avoid costly width checks here,\n // the terminal does not allow a cols < 2.\n if (this._activeBuffer.x + chWidth - oldWidth > cols) {\n // autowrap - DECAWM\n // automatically wraps to the beginning of the next line\n if (wraparoundMode) {\n const oldRow = bufferRow;\n let oldCol = this._activeBuffer.x - oldWidth;\n this._activeBuffer.x = oldWidth;\n this._activeBuffer.y++;\n if (this._activeBuffer.y === this._activeBuffer.scrollBottom + 1) {\n this._activeBuffer.y--;\n this._bufferService.scroll(this._eraseAttrData(), true);\n } else {\n if (this._activeBuffer.y >= this._bufferService.rows) {\n this._activeBuffer.y = this._bufferService.rows - 1;\n }\n // The line already exists (eg. the initial viewport), mark it as a\n // wrapped line\n this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!.isWrapped = true;\n }\n // row changed, get it again\n bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y);\n if (!bufferRow) {\n return;\n }\n if (oldWidth > 0 && bufferRow instanceof BufferLine) {\n // Combining character widens 1 column to 2.\n // Move old character to next line.\n bufferRow.copyCellsFrom(oldRow as BufferLine,\n oldCol, 0, oldWidth, false);\n }\n // clear left over cells to the right\n while (oldCol < cols) {\n oldRow.setCellFromCodepoint(oldCol++, 0, 1, curAttr);\n }\n } else {\n this._activeBuffer.x = cols - 1;\n if (chWidth === 2) {\n // FIXME: check for xterm behavior\n // What to do here? We got a wide char that does not fit into last cell\n continue;\n }\n }\n }\n\n // insert combining char at last cursor position\n // this._activeBuffer.x should never be 0 for a combining char\n // since they always follow a cell consuming char\n // therefore we can test for this._activeBuffer.x to avoid overflow left\n if (shouldJoin && this._activeBuffer.x) {\n const offset = bufferRow.getWidth(this._activeBuffer.x - 1) ? 1 : 2;\n // if empty cell after fullwidth, need to go 2 cells back\n // it is save to step 2 cells back here\n // since an empty cell is only set by fullwidth chars\n bufferRow.addCodepointToCell(this._activeBuffer.x - offset,\n code, chWidth);\n for (let delta = chWidth - oldWidth; --delta >= 0;) {\n bufferRow.setCellFromCodepoint(this._activeBuffer.x++, 0, 0, curAttr);\n }\n continue;\n }\n\n // insert mode: move characters to right\n if (insertMode) {\n // right shift cells according to the width\n bufferRow.insertCells(this._activeBuffer.x, chWidth - oldWidth, this._activeBuffer.getNullCell(curAttr));\n // test last cell - since the last cell has only room for\n // a halfwidth char any fullwidth shifted there is lost\n // and will be set to empty cell\n if (bufferRow.getWidth(cols - 1) === 2) {\n bufferRow.setCellFromCodepoint(cols - 1, NULL_CELL_CODE, NULL_CELL_WIDTH, curAttr);\n }\n }\n\n // write current char to buffer and advance cursor\n bufferRow.setCellFromCodepoint(this._activeBuffer.x++, code, chWidth, curAttr);\n\n // fullwidth char - also set next cell to placeholder stub and advance cursor\n // for graphemes bigger than fullwidth we can simply loop to zero\n // we already made sure above, that this._activeBuffer.x + chWidth will not overflow right\n if (chWidth > 0) {\n while (--chWidth) {\n // other than a regular empty cell a cell following a wide char has no width\n bufferRow.setCellFromCodepoint(this._activeBuffer.x++, 0, 0, curAttr);\n }\n }\n }\n\n this._parser.precedingJoinState = precedingJoinState;\n\n // handle wide chars: reset cell to the right if it is second cell of a wide char\n if (this._activeBuffer.x < cols && end - start > 0 && bufferRow.getWidth(this._activeBuffer.x) === 0 && !bufferRow.hasContent(this._activeBuffer.x)) {\n bufferRow.setCellFromCodepoint(this._activeBuffer.x, 0, 1, curAttr);\n }\n\n this._dirtyRowTracker.markDirty(this._activeBuffer.y);\n }\n\n /**\n * Forward registerCsiHandler from parser.\n */\n public registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise): IDisposable {\n if (id.final === 't' && !id.prefix && !id.intermediates) {\n // security: always check whether window option is allowed\n return this._parser.registerCsiHandler(id, params => {\n if (!paramToWindowOption(params.params[0], this._optionsService.rawOptions.windowOptions)) {\n return true;\n }\n return callback(params);\n });\n }\n return this._parser.registerCsiHandler(id, callback);\n }\n\n /**\n * Forward registerDcsHandler from parser.\n */\n public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise): IDisposable {\n return this._parser.registerDcsHandler(id, new DcsHandler(callback));\n }\n\n /**\n * Forward registerEscHandler from parser.\n */\n public registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise): IDisposable {\n return this._parser.registerEscHandler(id, callback);\n }\n\n /**\n * Forward registerOscHandler from parser.\n */\n public registerOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable {\n return this._parser.registerOscHandler(ident, new OscHandler(callback));\n }\n\n /**\n * Forward registerApcHandler from parser.\n */\n public registerApcHandler(id: IFunctionIdentifier, callback: (data: string) => boolean | Promise): IDisposable {\n return this._parser.registerApcHandler(id, new ApcHandler(callback));\n }\n\n /**\n * BEL\n * Bell (Ctrl-G).\n *\n * @vt: #Y C0 BEL \"Bell\" \"\\a, \\x07\" \"Ring the bell.\"\n * The behavior of the bell is further customizable with `ITerminalOptions.bellStyle`\n * and `ITerminalOptions.bellSound`.\n */\n public bell(): boolean {\n this._onRequestBell.fire();\n return true;\n }\n\n /**\n * LF\n * Line Feed or New Line (NL). (LF is Ctrl-J).\n *\n * @vt: #Y C0 LF \"Line Feed\" \"\\n, \\x0A\" \"Move the cursor one row down, scrolling if needed.\"\n * Scrolling is restricted to scroll margins and will only happen on the bottom line.\n *\n * @vt: #Y C0 VT \"Vertical Tabulation\" \"\\v, \\x0B\" \"Treated as LF.\"\n * @vt: #Y C0 FF \"Form Feed\" \"\\f, \\x0C\" \"Treated as LF.\"\n */\n public lineFeed(): boolean {\n this._dirtyRowTracker.markDirty(this._activeBuffer.y);\n if (this._optionsService.rawOptions.convertEol) {\n this._activeBuffer.x = 0;\n }\n this._activeBuffer.y++;\n if (this._activeBuffer.y === this._activeBuffer.scrollBottom + 1) {\n this._activeBuffer.y--;\n this._bufferService.scroll(this._eraseAttrData());\n } else if (this._activeBuffer.y >= this._bufferService.rows) {\n this._activeBuffer.y = this._bufferService.rows - 1;\n } else {\n // There was an explicit line feed (not just a carriage return), so clear the wrapped state of\n // the line. This is particularly important on conpty/Windows where revisiting lines to\n // reprint is common, especially on resize. Note that the windowsMode wrapped line heuristics\n // can mess with this so windowsMode should be disabled, which is recommended on Windows build\n // 21376 and above.\n this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!.isWrapped = false;\n }\n // If the end of the line is hit, prevent this action from wrapping around to the next line.\n if (this._activeBuffer.x >= this._bufferService.cols) {\n this._activeBuffer.x--;\n }\n this._dirtyRowTracker.markDirty(this._activeBuffer.y);\n\n this._onLineFeed.fire();\n return true;\n }\n\n /**\n * CR\n * Carriage Return (Ctrl-M).\n *\n * @vt: #Y C0 CR \"Carriage Return\" \"\\r, \\x0D\" \"Move the cursor to the beginning of the row.\"\n */\n public carriageReturn(): boolean {\n this._activeBuffer.x = 0;\n return true;\n }\n\n /**\n * BS\n * Backspace (Ctrl-H).\n *\n * @vt: #Y C0 BS \"Backspace\" \"\\b, \\x08\" \"Move the cursor one position to the left.\"\n * By default it is not possible to move the cursor past the leftmost position.\n * If `reverse wrap-around` (`CSI ? 45 h`) is set, a previous soft line wrap (DECAWM)\n * can be undone with BS within the scroll margins. In that case the cursor will wrap back\n * to the end of the previous row. Note that it is not possible to peek back into the scrollbuffer\n * with the cursor, thus at the home position (top-leftmost cell) this has no effect.\n */\n public backspace(): boolean {\n // reverse wrap-around is disabled\n if (!this._coreService.decPrivateModes.reverseWraparound) {\n this._restrictCursor();\n if (this._activeBuffer.x > 0) {\n this._activeBuffer.x--;\n }\n return true;\n }\n\n // reverse wrap-around is enabled\n // other than for normal operation mode, reverse wrap-around allows the cursor\n // to be at x=cols to be able to address the last cell of a row by BS\n this._restrictCursor(this._bufferService.cols);\n\n if (this._activeBuffer.x > 0) {\n this._activeBuffer.x--;\n } else {\n /**\n * reverse wrap-around handling:\n * Our implementation deviates from xterm on purpose. Details:\n * - only previous soft NLs can be reversed (isWrapped=true)\n * - only works within scrollborders (top/bottom, left/right not yet supported)\n * - cannot peek into scrollbuffer\n * - any cursor movement sequence keeps working as expected\n */\n if (this._activeBuffer.x === 0\n && this._activeBuffer.y > this._activeBuffer.scrollTop\n && this._activeBuffer.y <= this._activeBuffer.scrollBottom\n && this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)?.isWrapped) {\n this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!.isWrapped = false;\n this._activeBuffer.y--;\n this._activeBuffer.x = this._bufferService.cols - 1;\n // find last taken cell - last cell can have 3 different states:\n // - hasContent(true) + hasWidth(1): narrow char - we are done\n // - hasWidth(0): second part of wide char - we are done\n // - hasContent(false) + hasWidth(1): empty cell due to early wrapping wide char, go one\n // cell further back\n const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!;\n if (line.hasWidth(this._activeBuffer.x) && !line.hasContent(this._activeBuffer.x)) {\n this._activeBuffer.x--;\n // We do this only once, since width=1 + hasContent=false currently happens only once\n // before early wrapping of a wide char.\n // This needs to be fixed once we support graphemes taking more than 2 cells.\n }\n }\n }\n this._restrictCursor();\n return true;\n }\n\n /**\n * TAB\n * Horizontal Tab (HT) (Ctrl-I).\n *\n * @vt: #Y C0 HT \"Horizontal Tabulation\" \"\\t, \\x09\" \"Move the cursor to the next character tab stop.\"\n */\n public tab(): boolean {\n if (this._activeBuffer.x >= this._bufferService.cols) {\n return true;\n }\n const originalX = this._activeBuffer.x;\n this._activeBuffer.x = this._activeBuffer.nextStop();\n if (this._optionsService.rawOptions.screenReaderMode) {\n this._onA11yTab.fire(this._activeBuffer.x - originalX);\n }\n return true;\n }\n\n /**\n * SO\n * Shift Out (Ctrl-N) -> Switch to Alternate Character Set. This invokes the\n * G1 character set.\n *\n * @vt: #P[Only limited ISO-2022 charset support.] C0 SO \"Shift Out\" \"\\x0E\" \"Switch to an alternative character set.\"\n */\n public shiftOut(): boolean {\n this._charsetService.setgLevel(1);\n return true;\n }\n\n /**\n * SI\n * Shift In (Ctrl-O) -> Switch to Standard Character Set. This invokes the G0\n * character set (the default).\n *\n * @vt: #Y C0 SI \"Shift In\" \"\\x0F\" \"Return to regular character set after Shift Out.\"\n */\n public shiftIn(): boolean {\n this._charsetService.setgLevel(0);\n return true;\n }\n\n /**\n * Restrict cursor to viewport size / scroll margin (origin mode).\n */\n private _restrictCursor(maxCol: number = this._bufferService.cols - 1): void {\n this._activeBuffer.x = Math.min(maxCol, Math.max(0, this._activeBuffer.x));\n this._activeBuffer.y = this._coreService.decPrivateModes.origin\n ? Math.min(this._activeBuffer.scrollBottom, Math.max(this._activeBuffer.scrollTop, this._activeBuffer.y))\n : Math.min(this._bufferService.rows - 1, Math.max(0, this._activeBuffer.y));\n this._dirtyRowTracker.markDirty(this._activeBuffer.y);\n }\n\n /**\n * Set absolute cursor position.\n */\n private _setCursor(x: number, y: number): void {\n this._dirtyRowTracker.markDirty(this._activeBuffer.y);\n if (this._coreService.decPrivateModes.origin) {\n this._activeBuffer.x = x;\n this._activeBuffer.y = this._activeBuffer.scrollTop + y;\n } else {\n this._activeBuffer.x = x;\n this._activeBuffer.y = y;\n }\n this._restrictCursor();\n this._dirtyRowTracker.markDirty(this._activeBuffer.y);\n }\n\n /**\n * Set relative cursor position.\n */\n private _moveCursor(x: number, y: number): void {\n // for relative changes we have to make sure we are within 0 .. cols/rows - 1\n // before calculating the new position\n this._restrictCursor();\n this._setCursor(this._activeBuffer.x + x, this._activeBuffer.y + y);\n }\n\n /**\n * CSI Ps A\n * Cursor Up Ps Times (default = 1) (CUU).\n *\n * @vt: #Y CSI CUU \"Cursor Up\" \"CSI Ps A\" \"Move cursor `Ps` times up (default=1).\"\n * If the cursor would pass the top scroll margin, it will stop there.\n */\n public cursorUp(params: IParams): boolean {\n // stop at scrollTop\n const diffToTop = this._activeBuffer.y - this._activeBuffer.scrollTop;\n if (diffToTop >= 0) {\n this._moveCursor(0, -Math.min(diffToTop, params.params[0] || 1));\n } else {\n this._moveCursor(0, -(params.params[0] || 1));\n }\n return true;\n }\n\n /**\n * CSI Ps B\n * Cursor Down Ps Times (default = 1) (CUD).\n *\n * @vt: #Y CSI CUD \"Cursor Down\" \"CSI Ps B\" \"Move cursor `Ps` times down (default=1).\"\n * If the cursor would pass the bottom scroll margin, it will stop there.\n */\n public cursorDown(params: IParams): boolean {\n // stop at scrollBottom\n const diffToBottom = this._activeBuffer.scrollBottom - this._activeBuffer.y;\n if (diffToBottom >= 0) {\n this._moveCursor(0, Math.min(diffToBottom, params.params[0] || 1));\n } else {\n this._moveCursor(0, params.params[0] || 1);\n }\n return true;\n }\n\n /**\n * CSI Ps C\n * Cursor Forward Ps Times (default = 1) (CUF).\n *\n * @vt: #Y CSI CUF \"Cursor Forward\" \"CSI Ps C\" \"Move cursor `Ps` times forward (default=1).\"\n */\n public cursorForward(params: IParams): boolean {\n this._moveCursor(params.params[0] || 1, 0);\n return true;\n }\n\n /**\n * CSI Ps D\n * Cursor Backward Ps Times (default = 1) (CUB).\n *\n * @vt: #Y CSI CUB \"Cursor Backward\" \"CSI Ps D\" \"Move cursor `Ps` times backward (default=1).\"\n */\n public cursorBackward(params: IParams): boolean {\n this._moveCursor(-(params.params[0] || 1), 0);\n return true;\n }\n\n /**\n * CSI Ps E\n * Cursor Next Line Ps Times (default = 1) (CNL).\n * Other than cursorDown (CUD) also set the cursor to first column.\n *\n * @vt: #Y CSI CNL \"Cursor Next Line\" \"CSI Ps E\" \"Move cursor `Ps` times down (default=1) and to the first column.\"\n * Same as CUD, additionally places the cursor at the first column.\n */\n public cursorNextLine(params: IParams): boolean {\n this.cursorDown(params);\n this._activeBuffer.x = 0;\n return true;\n }\n\n /**\n * CSI Ps F\n * Cursor Previous Line Ps Times (default = 1) (CPL).\n * Other than cursorUp (CUU) also set the cursor to first column.\n *\n * @vt: #Y CSI CPL \"Cursor Backward\" \"CSI Ps F\" \"Move cursor `Ps` times up (default=1) and to the first column.\"\n * Same as CUU, additionally places the cursor at the first column.\n */\n public cursorPrecedingLine(params: IParams): boolean {\n this.cursorUp(params);\n this._activeBuffer.x = 0;\n return true;\n }\n\n /**\n * CSI Ps G\n * Cursor Character Absolute [column] (default = [row,1]) (CHA).\n *\n * @vt: #Y CSI CHA \"Cursor Horizontal Absolute\" \"CSI Ps G\" \"Move cursor to `Ps`-th column of the active row (default=1).\"\n */\n public cursorCharAbsolute(params: IParams): boolean {\n this._setCursor((params.params[0] || 1) - 1, this._activeBuffer.y);\n return true;\n }\n\n /**\n * CSI Ps ; Ps H\n * Cursor Position [row;column] (default = [1,1]) (CUP).\n *\n * @vt: #Y CSI CUP \"Cursor Position\" \"CSI Ps ; Ps H\" \"Set cursor to position [`Ps`, `Ps`] (default = [1, 1]).\"\n * If ORIGIN mode is set, places the cursor to the absolute position within the scroll margins.\n * If ORIGIN mode is not set, places the cursor to the absolute position within the viewport.\n * Note that the coordinates are 1-based, thus the top left position starts at `1 ; 1`.\n */\n public cursorPosition(params: IParams): boolean {\n this._setCursor(\n // col\n (params.length >= 2) ? (params.params[1] || 1) - 1 : 0,\n // row\n (params.params[0] || 1) - 1\n );\n return true;\n }\n\n /**\n * CSI Pm ` Character Position Absolute\n * [column] (default = [row,1]) (HPA).\n * Currently same functionality as CHA.\n *\n * @vt: #Y CSI HPA \"Horizontal Position Absolute\" \"CSI Ps ` \" \"Same as CHA.\"\n */\n public charPosAbsolute(params: IParams): boolean {\n this._setCursor((params.params[0] || 1) - 1, this._activeBuffer.y);\n return true;\n }\n\n /**\n * CSI Pm a Character Position Relative\n * [columns] (default = [row,col+1]) (HPR)\n *\n * @vt: #Y CSI HPR \"Horizontal Position Relative\" \"CSI Ps a\" \"Same as CUF.\"\n */\n public hPositionRelative(params: IParams): boolean {\n this._moveCursor(params.params[0] || 1, 0);\n return true;\n }\n\n /**\n * CSI Pm d Vertical Position Absolute (VPA)\n * [row] (default = [1,column])\n *\n * @vt: #Y CSI VPA \"Vertical Position Absolute\" \"CSI Ps d\" \"Move cursor to `Ps`-th row (default=1).\"\n */\n public linePosAbsolute(params: IParams): boolean {\n this._setCursor(this._activeBuffer.x, (params.params[0] || 1) - 1);\n return true;\n }\n\n /**\n * CSI Pm e Vertical Position Relative (VPR)\n * [rows] (default = [row+1,column])\n * reuse CSI Ps B ?\n *\n * @vt: #Y CSI VPR \"Vertical Position Relative\" \"CSI Ps e\" \"Move cursor `Ps` times down (default=1).\"\n */\n public vPositionRelative(params: IParams): boolean {\n this._moveCursor(0, params.params[0] || 1);\n return true;\n }\n\n /**\n * CSI Ps ; Ps f\n * Horizontal and Vertical Position [row;column] (default =\n * [1,1]) (HVP).\n * Same as CUP.\n *\n * @vt: #Y CSI HVP \"Horizontal and Vertical Position\" \"CSI Ps ; Ps f\" \"Same as CUP.\"\n */\n public hVPosition(params: IParams): boolean {\n this.cursorPosition(params);\n return true;\n }\n\n /**\n * CSI Ps g Tab Clear (TBC).\n * Ps = 0 -> Clear Current Column (default).\n * Ps = 3 -> Clear All.\n * Potentially:\n * Ps = 2 -> Clear Stops on Line.\n * http://vt100.net/annarbor/aaa-ug/section6.html\n *\n * @vt: #Y CSI TBC \"Tab Clear\" \"CSI Ps g\" \"Clear tab stops at current position (0) or all (3) (default=0).\"\n * Clearing tabstops off the active row (Ps = 2, VT100) is currently not supported.\n */\n public tabClear(params: IParams): boolean {\n const param = params.params[0];\n if (param === 0) {\n delete this._activeBuffer.tabs[this._activeBuffer.x];\n } else if (param === 3) {\n this._activeBuffer.tabs = {};\n }\n return true;\n }\n\n /**\n * CSI Ps I\n * Cursor Forward Tabulation Ps tab stops (default = 1) (CHT).\n *\n * @vt: #Y CSI CHT \"Cursor Horizontal Tabulation\" \"CSI Ps I\" \"Move cursor `Ps` times tabs forward (default=1).\"\n */\n public cursorForwardTab(params: IParams): boolean {\n if (this._activeBuffer.x >= this._bufferService.cols) {\n return true;\n }\n let param = params.params[0] || 1;\n while (param--) {\n this._activeBuffer.x = this._activeBuffer.nextStop();\n }\n return true;\n }\n\n /**\n * CSI Ps Z Cursor Backward Tabulation Ps tab stops (default = 1) (CBT).\n *\n * @vt: #Y CSI CBT \"Cursor Backward Tabulation\" \"CSI Ps Z\" \"Move cursor `Ps` tabs backward (default=1).\"\n */\n public cursorBackwardTab(params: IParams): boolean {\n if (this._activeBuffer.x >= this._bufferService.cols) {\n return true;\n }\n let param = params.params[0] || 1;\n\n while (param--) {\n this._activeBuffer.x = this._activeBuffer.prevStop();\n }\n return true;\n }\n\n /**\n * CSI Ps \" q Select Character Protection Attribute (DECSCA).\n *\n * @vt: #Y CSI DECSCA \"Select Character Protection Attribute\" \"CSI Ps \" q\" \"Whether DECSED and DECSEL can erase (0=default, 2) or not (1).\"\n */\n public selectProtected(params: IParams): boolean {\n const p = params.params[0];\n if (p === 1) this._curAttrData.bg |= BgFlags.PROTECTED;\n if (p === 2 || p === 0) this._curAttrData.bg &= ~BgFlags.PROTECTED;\n return true;\n }\n\n\n /**\n * Helper method to erase cells in a terminal row.\n * The cell gets replaced with the eraseChar of the terminal.\n * @param y The row index relative to the viewport.\n * @param start The start x index of the range to be erased.\n * @param end The end x index of the range to be erased (exclusive).\n * @param clearWrap clear the isWrapped flag\n * @param respectProtect Whether to respect the protection attribute (DECSCA).\n */\n private _eraseInBufferLine(y: number, start: number, end: number, clearWrap: boolean = false, respectProtect: boolean = false): void {\n const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y);\n if (!line) {\n return;\n }\n line.replaceCells(\n start,\n end,\n this._activeBuffer.getNullCell(this._eraseAttrData()),\n respectProtect\n );\n if (clearWrap) {\n line.isWrapped = false;\n }\n }\n\n /**\n * Helper method to reset cells in a terminal row. The cell gets replaced with the eraseChar of\n * the terminal and the isWrapped property is set to false.\n * @param y row index\n */\n private _resetBufferLine(y: number, respectProtect: boolean = false): void {\n const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y);\n if (line) {\n line.fill(this._activeBuffer.getNullCell(this._eraseAttrData()), respectProtect);\n this._bufferService.buffer.clearMarkers(this._activeBuffer.ybase + y);\n line.isWrapped = false;\n }\n }\n\n /**\n * CSI Ps J Erase in Display (ED).\n * Ps = 0 -> Erase Below (default).\n * Ps = 1 -> Erase Above.\n * Ps = 2 -> Erase All.\n * Ps = 3 -> Erase Saved Lines (xterm).\n * CSI ? Ps J\n * Erase in Display (DECSED).\n * Ps = 0 -> Selective Erase Below (default).\n * Ps = 1 -> Selective Erase Above.\n * Ps = 2 -> Selective Erase All.\n *\n * @vt: #Y CSI ED \"Erase In Display\" \"CSI Ps J\" \"Erase various parts of the viewport.\"\n * Supported param values:\n *\n * | Ps | Effect |\n * | -- | ------------------------------------------------------------ |\n * | 0 | Erase from the cursor through the end of the viewport. |\n * | 1 | Erase from the beginning of the viewport through the cursor. |\n * | 2 | Erase complete viewport. |\n * | 3 | Erase scrollback. |\n *\n * @vt: #Y CSI DECSED \"Selective Erase In Display\" \"CSI ? Ps J\" \"Same as ED with respecting protection flag.\"\n */\n public eraseInDisplay(params: IParams, respectProtect: boolean = false): boolean {\n this._restrictCursor(this._bufferService.cols);\n let j;\n switch (params.params[0]) {\n case 0:\n j = this._activeBuffer.y;\n this._dirtyRowTracker.markDirty(j);\n this._eraseInBufferLine(j++, this._activeBuffer.x, this._bufferService.cols, this._activeBuffer.x === 0, respectProtect);\n for (; j < this._bufferService.rows; j++) {\n this._resetBufferLine(j, respectProtect);\n }\n this._dirtyRowTracker.markDirty(j);\n break;\n case 1:\n j = this._activeBuffer.y;\n this._dirtyRowTracker.markDirty(j);\n // Deleted front part of line and everything before. This line will no longer be wrapped.\n this._eraseInBufferLine(j, 0, this._activeBuffer.x + 1, true, respectProtect);\n if (this._activeBuffer.x + 1 >= this._bufferService.cols) {\n // Deleted entire previous line. This next line can no longer be wrapped.\n const nextLine = this._activeBuffer.lines.get(j + 1);\n if (nextLine) {\n nextLine.isWrapped = false;\n }\n }\n while (j--) {\n this._resetBufferLine(j, respectProtect);\n }\n this._dirtyRowTracker.markDirty(0);\n break;\n case 2:\n if (this._optionsService.rawOptions.scrollOnEraseInDisplay) {\n j = this._bufferService.rows;\n this._dirtyRowTracker.markRangeDirty(0, j - 1);\n while (j--) {\n const currentLine = this._activeBuffer.lines.get(this._activeBuffer.ybase + j);\n if (currentLine?.getTrimmedLength()) {\n break;\n }\n }\n for (; j >= 0; j--) {\n this._bufferService.scroll(this._eraseAttrData());\n }\n }\n else {\n j = this._bufferService.rows;\n this._dirtyRowTracker.markDirty(j - 1);\n while (j--) {\n this._resetBufferLine(j, respectProtect);\n }\n this._dirtyRowTracker.markDirty(0);\n }\n break;\n case 3:\n // Clear scrollback (everything not in viewport)\n const scrollBackSize = this._activeBuffer.lines.length - this._bufferService.rows;\n if (scrollBackSize > 0) {\n this._activeBuffer.lines.trimStart(scrollBackSize);\n this._activeBuffer.ybase = Math.max(this._activeBuffer.ybase - scrollBackSize, 0);\n this._activeBuffer.ydisp = Math.max(this._activeBuffer.ydisp - scrollBackSize, 0);\n // isUserScrolling tracks the normal buffer's viewport, so ED3 on the alt\n // screen must not touch it\n if (this._activeBuffer === this._bufferService.buffers.normal) {\n this._bufferService.isUserScrolling = false;\n }\n // Force a scroll event to refresh viewport\n this._onScroll.fire(0);\n }\n break;\n }\n return true;\n }\n\n /**\n * CSI Ps K Erase in Line (EL).\n * Ps = 0 -> Erase to Right (default).\n * Ps = 1 -> Erase to Left.\n * Ps = 2 -> Erase All.\n * CSI ? Ps K\n * Erase in Line (DECSEL).\n * Ps = 0 -> Selective Erase to Right (default).\n * Ps = 1 -> Selective Erase to Left.\n * Ps = 2 -> Selective Erase All.\n *\n * @vt: #Y CSI EL \"Erase In Line\" \"CSI Ps K\" \"Erase various parts of the active row.\"\n * Supported param values:\n *\n * | Ps | Effect |\n * | -- | -------------------------------------------------------- |\n * | 0 | Erase from the cursor through the end of the row. |\n * | 1 | Erase from the beginning of the line through the cursor. |\n * | 2 | Erase complete line. |\n *\n * @vt: #Y CSI DECSEL \"Selective Erase In Line\" \"CSI ? Ps K\" \"Same as EL with respecting protecting flag.\"\n */\n public eraseInLine(params: IParams, respectProtect: boolean = false): boolean {\n this._restrictCursor(this._bufferService.cols);\n switch (params.params[0]) {\n case 0:\n this._eraseInBufferLine(this._activeBuffer.y, this._activeBuffer.x, this._bufferService.cols, this._activeBuffer.x === 0, respectProtect);\n break;\n case 1:\n this._eraseInBufferLine(this._activeBuffer.y, 0, this._activeBuffer.x + 1, false, respectProtect);\n break;\n case 2:\n this._eraseInBufferLine(this._activeBuffer.y, 0, this._bufferService.cols, true, respectProtect);\n break;\n }\n this._dirtyRowTracker.markDirty(this._activeBuffer.y);\n return true;\n }\n\n /**\n * CSI Ps L\n * Insert Ps Line(s) (default = 1) (IL).\n *\n * @vt: #Y CSI IL \"Insert Line\" \"CSI Ps L\" \"Insert `Ps` blank lines at active row (default=1).\"\n * For every inserted line at the scroll top one line at the scroll bottom gets removed.\n * The cursor is set to the first column.\n * IL has no effect if the cursor is outside the scroll margins.\n */\n public insertLines(params: IParams): boolean {\n this._restrictCursor();\n let param = params.params[0] || 1;\n\n if (this._activeBuffer.y > this._activeBuffer.scrollBottom || this._activeBuffer.y < this._activeBuffer.scrollTop) {\n return true;\n }\n\n const row: number = this._activeBuffer.ybase + this._activeBuffer.y;\n\n const scrollBottomRowsOffset = this._bufferService.rows - 1 - this._activeBuffer.scrollBottom;\n const scrollBottomAbsolute = this._bufferService.rows - 1 + this._activeBuffer.ybase - scrollBottomRowsOffset + 1;\n while (param--) {\n // test: echo -e '\\e[44m\\e[1L\\e[0m'\n // blankLine(true) - xterm/linux behavior\n this._activeBuffer.lines.splice(scrollBottomAbsolute - 1, 1);\n this._activeBuffer.lines.splice(row, 0, this._activeBuffer.getBlankLine(this._eraseAttrData()));\n }\n\n this._dirtyRowTracker.markRangeDirty(this._activeBuffer.y, this._activeBuffer.scrollBottom);\n this._activeBuffer.x = 0; // see https://vt100.net/docs/vt220-rm/chapter4.html - vt220 only?\n return true;\n }\n\n /**\n * CSI Ps M\n * Delete Ps Line(s) (default = 1) (DL).\n *\n * @vt: #Y CSI DL \"Delete Line\" \"CSI Ps M\" \"Delete `Ps` lines at active row (default=1).\"\n * For every deleted line at the scroll top one blank line at the scroll bottom gets appended.\n * The cursor is set to the first column.\n * DL has no effect if the cursor is outside the scroll margins.\n */\n public deleteLines(params: IParams): boolean {\n this._restrictCursor();\n let param = params.params[0] || 1;\n\n if (this._activeBuffer.y > this._activeBuffer.scrollBottom || this._activeBuffer.y < this._activeBuffer.scrollTop) {\n return true;\n }\n\n const row: number = this._activeBuffer.ybase + this._activeBuffer.y;\n\n let j: number;\n j = this._bufferService.rows - 1 - this._activeBuffer.scrollBottom;\n j = this._bufferService.rows - 1 + this._activeBuffer.ybase - j;\n while (param--) {\n // test: echo -e '\\e[44m\\e[1M\\e[0m'\n // blankLine(true) - xterm/linux behavior\n this._activeBuffer.lines.splice(row, 1);\n this._activeBuffer.lines.splice(j, 0, this._activeBuffer.getBlankLine(this._eraseAttrData()));\n }\n\n this._dirtyRowTracker.markRangeDirty(this._activeBuffer.y, this._activeBuffer.scrollBottom);\n this._activeBuffer.x = 0; // see https://vt100.net/docs/vt220-rm/chapter4.html - vt220 only?\n return true;\n }\n\n /**\n * CSI Ps @\n * Insert Ps (Blank) Character(s) (default = 1) (ICH).\n *\n * @vt: #Y CSI ICH \"Insert Characters\" \"CSI Ps @\" \"Insert `Ps` (blank) characters (default = 1).\"\n * The ICH sequence inserts `Ps` blank characters. The cursor remains at the beginning of the\n * blank characters. Text between the cursor and right margin moves to the right. Characters moved\n * past the right margin are lost.\n *\n *\n * FIXME: check against xterm - should not work outside of scroll margins (see VT520 manual)\n */\n public insertChars(params: IParams): boolean {\n this._restrictCursor();\n const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y);\n if (line) {\n line.insertCells(\n this._activeBuffer.x,\n params.params[0] || 1,\n this._activeBuffer.getNullCell(this._eraseAttrData())\n );\n this._dirtyRowTracker.markDirty(this._activeBuffer.y);\n }\n return true;\n }\n\n /**\n * CSI Ps P\n * Delete Ps Character(s) (default = 1) (DCH).\n *\n * @vt: #Y CSI DCH \"Delete Character\" \"CSI Ps P\" \"Delete `Ps` characters (default=1).\"\n * As characters are deleted, the remaining characters between the cursor and right margin move to\n * the left. Character attributes move with the characters. The terminal adds blank characters at\n * the right margin.\n *\n *\n * FIXME: check against xterm - should not work outside of scroll margins (see VT520 manual)\n */\n public deleteChars(params: IParams): boolean {\n this._restrictCursor();\n const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y);\n if (line) {\n line.deleteCells(\n this._activeBuffer.x,\n params.params[0] || 1,\n this._activeBuffer.getNullCell(this._eraseAttrData())\n );\n this._dirtyRowTracker.markDirty(this._activeBuffer.y);\n }\n return true;\n }\n\n /**\n * CSI Ps S Scroll up Ps lines (default = 1) (SU).\n *\n * @vt: #Y CSI SU \"Scroll Up\" \"CSI Ps S\" \"Scroll `Ps` lines up (default=1).\"\n *\n *\n * FIXME: scrolled out lines at top = 1 should add to scrollback (xterm)\n */\n public scrollUp(params: IParams): boolean {\n let param = params.params[0] || 1;\n\n while (param--) {\n this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollTop, 1);\n this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollBottom, 0, this._activeBuffer.getBlankLine(this._eraseAttrData()));\n }\n this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);\n return true;\n }\n\n /**\n * CSI Ps T Scroll down Ps lines (default = 1) (SD).\n *\n * @vt: #Y CSI SD \"Scroll Down\" \"CSI Ps T\" \"Scroll `Ps` lines down (default=1).\"\n */\n public scrollDown(params: IParams): boolean {\n let param = params.params[0] || 1;\n\n while (param--) {\n this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollBottom, 1);\n this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollTop, 0, this._activeBuffer.getBlankLine(DEFAULT_ATTR_DATA));\n }\n this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);\n return true;\n }\n\n /**\n * CSI Ps SP @ Scroll left Ps columns (default = 1) (SL) ECMA-48\n *\n * Notation: (Pn)\n * Representation: CSI Pn 02/00 04/00\n * Parameter default value: Pn = 1\n * SL causes the data in the presentation component to be moved by n character positions\n * if the line orientation is horizontal, or by n line positions if the line orientation\n * is vertical, such that the data appear to move to the left; where n equals the value of Pn.\n * The active presentation position is not affected by this control function.\n *\n * Supported:\n * - always left shift (no line orientation setting respected)\n *\n * @vt: #Y CSI SL \"Scroll Left\" \"CSI Ps SP @\" \"Scroll viewport `Ps` times to the left.\"\n * SL moves the content of all lines within the scroll margins `Ps` times to the left.\n * SL has no effect outside of the scroll margins.\n */\n public scrollLeft(params: IParams): boolean {\n if (this._activeBuffer.y > this._activeBuffer.scrollBottom || this._activeBuffer.y < this._activeBuffer.scrollTop) {\n return true;\n }\n const param = params.params[0] || 1;\n for (let y = this._activeBuffer.scrollTop; y <= this._activeBuffer.scrollBottom; ++y) {\n const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!;\n line.deleteCells(0, param, this._activeBuffer.getNullCell(this._eraseAttrData()));\n line.isWrapped = false;\n }\n this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);\n return true;\n }\n\n /**\n * CSI Ps SP A Scroll right Ps columns (default = 1) (SR) ECMA-48\n *\n * Notation: (Pn)\n * Representation: CSI Pn 02/00 04/01\n * Parameter default value: Pn = 1\n * SR causes the data in the presentation component to be moved by n character positions\n * if the line orientation is horizontal, or by n line positions if the line orientation\n * is vertical, such that the data appear to move to the right; where n equals the value of Pn.\n * The active presentation position is not affected by this control function.\n *\n * Supported:\n * - always right shift (no line orientation setting respected)\n *\n * @vt: #Y CSI SR \"Scroll Right\" \"CSI Ps SP A\" \"Scroll viewport `Ps` times to the right.\"\n * SL moves the content of all lines within the scroll margins `Ps` times to the right.\n * Content at the right margin is lost.\n * SL has no effect outside of the scroll margins.\n */\n public scrollRight(params: IParams): boolean {\n if (this._activeBuffer.y > this._activeBuffer.scrollBottom || this._activeBuffer.y < this._activeBuffer.scrollTop) {\n return true;\n }\n const param = params.params[0] || 1;\n for (let y = this._activeBuffer.scrollTop; y <= this._activeBuffer.scrollBottom; ++y) {\n const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!;\n line.insertCells(0, param, this._activeBuffer.getNullCell(this._eraseAttrData()));\n line.isWrapped = false;\n }\n this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);\n return true;\n }\n\n /**\n * CSI Pm ' }\n * Insert Ps Column(s) (default = 1) (DECIC), VT420 and up.\n *\n * @vt: #Y CSI DECIC \"Insert Columns\" \"CSI Ps ' }\" \"Insert `Ps` columns at cursor position.\"\n * DECIC inserts `Ps` times blank columns at the cursor position for all lines with the scroll\n * margins, moving content to the right. Content at the right margin is lost. DECIC has no effect\n * outside the scrolling margins.\n */\n public insertColumns(params: IParams): boolean {\n if (this._activeBuffer.y > this._activeBuffer.scrollBottom || this._activeBuffer.y < this._activeBuffer.scrollTop) {\n return true;\n }\n const param = params.params[0] || 1;\n for (let y = this._activeBuffer.scrollTop; y <= this._activeBuffer.scrollBottom; ++y) {\n const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!;\n line.insertCells(this._activeBuffer.x, param, this._activeBuffer.getNullCell(this._eraseAttrData()));\n line.isWrapped = false;\n }\n this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);\n return true;\n }\n\n /**\n * CSI Pm ' ~\n * Delete Ps Column(s) (default = 1) (DECDC), VT420 and up.\n *\n * @vt: #Y CSI DECDC \"Delete Columns\" \"CSI Ps ' ~\" \"Delete `Ps` columns at cursor position.\"\n * DECDC deletes `Ps` times columns at the cursor position for all lines with the scroll margins,\n * moving content to the left. Blank columns are added at the right margin.\n * DECDC has no effect outside the scrolling margins.\n */\n public deleteColumns(params: IParams): boolean {\n if (this._activeBuffer.y > this._activeBuffer.scrollBottom || this._activeBuffer.y < this._activeBuffer.scrollTop) {\n return true;\n }\n const param = params.params[0] || 1;\n for (let y = this._activeBuffer.scrollTop; y <= this._activeBuffer.scrollBottom; ++y) {\n const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!;\n line.deleteCells(this._activeBuffer.x, param, this._activeBuffer.getNullCell(this._eraseAttrData()));\n line.isWrapped = false;\n }\n this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);\n return true;\n }\n\n /**\n * CSI Ps X\n * Erase Ps Character(s) (default = 1) (ECH).\n *\n * @vt: #Y CSI ECH \"Erase Character\" \"CSI Ps X\" \"Erase `Ps` characters from current cursor position to the right (default=1).\"\n * ED erases `Ps` characters from current cursor position to the right.\n * ED works inside or outside the scrolling margins.\n */\n public eraseChars(params: IParams): boolean {\n this._restrictCursor();\n const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y);\n if (line) {\n line.replaceCells(\n this._activeBuffer.x,\n this._activeBuffer.x + (params.params[0] || 1),\n this._activeBuffer.getNullCell(this._eraseAttrData())\n );\n this._dirtyRowTracker.markDirty(this._activeBuffer.y);\n }\n return true;\n }\n\n /**\n * CSI Ps b Repeat the preceding graphic character Ps times (REP).\n * From ECMA 48 (@see http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-048.pdf)\n * Notation: (Pn)\n * Representation: CSI Pn 06/02\n * Parameter default value: Pn = 1\n * REP is used to indicate that the preceding character in the data stream,\n * if it is a graphic character (represented by one or more bit combinations) including SPACE,\n * is to be repeated n times, where n equals the value of Pn.\n * If the character preceding REP is a control function or part of a control function,\n * the effect of REP is not defined by this Standard.\n *\n * We extend xterm's behavior to allow repeating entire grapheme clusters.\n * This isn't 100% xterm-compatible, but it seems saner and more useful.\n * - text attrs are applied normally\n * - wrap around is respected\n * - any valid sequence resets the carried forward char\n *\n * Note: To get reset on a valid sequence working correctly without much runtime penalty, the\n * preceding codepoint is stored on the parser in `this.print` and reset during `parser.parse`.\n *\n * @vt: #Y CSI REP \"Repeat Preceding Character\" \"CSI Ps b\" \"Repeat preceding character `Ps` times (default=1).\"\n * REP repeats the previous character `Ps` times advancing the cursor, also wrapping if DECAWM is\n * set. REP has no effect if the sequence does not follow a printable ASCII character\n * (NOOP for any other sequence in between or NON ASCII characters).\n */\n public repeatPrecedingCharacter(params: IParams): boolean {\n const joinState = this._parser.precedingJoinState;\n if (!joinState) {\n return true;\n }\n // call print to insert the chars and handle correct wrapping\n const length = params.params[0] || 1;\n const chWidth = UnicodeService.extractWidth(joinState);\n const x = this._activeBuffer.x - chWidth;\n const bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!;\n const text = bufferRow.getString(x);\n const data = new Uint32Array(text.length * length);\n let idata = 0;\n for (let itext = 0; itext < text.length;) {\n const ch = text.codePointAt(itext) || 0;\n data[idata++] = ch;\n itext += ch > 0xffff ? 2 : 1;\n }\n let tlength = idata;\n for (let i = 1; i < length; ++i) {\n data.copyWithin(tlength, 0, idata);\n tlength += idata;\n }\n this.print(data, 0, tlength);\n return true;\n }\n\n /**\n * CSI Ps c Send Device Attributes (Primary DA).\n * Ps = 0 or omitted -> request attributes from terminal. The\n * response depends on the decTerminalID resource setting.\n * -> CSI ? 1 ; 2 c (``VT100 with Advanced Video Option'')\n * -> CSI ? 1 ; 0 c (``VT101 with No Options'')\n * -> CSI ? 6 c (``VT102'')\n * -> CSI ? 6 0 ; 1 ; 2 ; 6 ; 8 ; 9 ; 1 5 ; c (``VT220'')\n * The VT100-style response parameters do not mean anything by\n * themselves. VT220 parameters do, telling the host what fea-\n * tures the terminal supports:\n * Ps = 1 -> 132-columns.\n * Ps = 2 -> Printer.\n * Ps = 6 -> Selective erase.\n * Ps = 8 -> User-defined keys.\n * Ps = 9 -> National replacement character sets.\n * Ps = 1 5 -> Technical characters.\n * Ps = 2 2 -> ANSI color, e.g., VT525.\n * Ps = 2 9 -> ANSI text locator (i.e., DEC Locator mode).\n *\n * @vt: #Y CSI DA1 \"Primary Device Attributes\" \"CSI c\" \"Send primary device attributes.\"\n *\n *\n * TODO: fix and cleanup response\n */\n public sendDeviceAttributesPrimary(params: IParams): boolean {\n if (params.params[0] > 0) {\n return true;\n }\n if (this._is('xterm') || this._is('rxvt-unicode') || this._is('screen')) {\n this._coreService.triggerDataEvent(C0.ESC + '[?1;2c');\n } else if (this._is('linux')) {\n this._coreService.triggerDataEvent(C0.ESC + '[?6c');\n }\n return true;\n }\n\n /**\n * CSI > Ps c\n * Send Device Attributes (Secondary DA).\n * Ps = 0 or omitted -> request the terminal's identification\n * code. The response depends on the decTerminalID resource set-\n * ting. It should apply only to VT220 and up, but xterm extends\n * this to VT100.\n * -> CSI > Pp ; Pv ; Pc c\n * where Pp denotes the terminal type\n * Pp = 0 -> ``VT100''.\n * Pp = 1 -> ``VT220''.\n * and Pv is the firmware version (for xterm, this was originally\n * the XFree86 patch number, starting with 95). In a DEC termi-\n * nal, Pc indicates the ROM cartridge registration number and is\n * always zero.\n * More information:\n * xterm/charproc.c - line 2012, for more information.\n * vim responds with ^[[?0c or ^[[?1c after the terminal's response (?)\n *\n * @vt: #Y CSI DA2 \"Secondary Device Attributes\" \"CSI > c\" \"Send primary device attributes.\"\n *\n *\n * TODO: fix and cleanup response\n */\n public sendDeviceAttributesSecondary(params: IParams): boolean {\n if (params.params[0] > 0) {\n return true;\n }\n // xterm and urxvt\n // seem to spit this\n // out around ~370 times (?).\n if (this._is('xterm')) {\n this._coreService.triggerDataEvent(C0.ESC + '[>0;276;0c');\n } else if (this._is('rxvt-unicode')) {\n this._coreService.triggerDataEvent(C0.ESC + '[>85;95;0c');\n } else if (this._is('linux')) {\n // not supported by linux console.\n // linux console echoes parameters.\n this._coreService.triggerDataEvent(params.params[0] + 'c');\n } else if (this._is('screen')) {\n this._coreService.triggerDataEvent(C0.ESC + '[>83;40003;0c');\n }\n return true;\n }\n\n /**\n * CSI > Ps q\n * Ps = 0 => Report xterm name and version (XTVERSION).\n *\n * The response is a DCS sequence identifying the version: DCS > | text ST\n *\n * @vt: #Y CSI XTVERSION \"Report Xterm Version\" \"CSI > q\" \"Report the terminal name and version.\"\n */\n public sendXtVersion(params: IParams): boolean {\n if (params.params[0] > 0) {\n return true;\n }\n this._coreService.triggerDataEvent(`${C0.ESC}P>|xterm.js(${XTERM_VERSION})${C0.ESC}\\\\`);\n return true;\n }\n\n /**\n * Evaluate if the current terminal is the given argument.\n * @param term The terminal name to evaluate\n */\n private _is(term: string): boolean {\n return (this._optionsService.rawOptions.termName + '').startsWith(term);\n }\n\n /**\n * CSI Pm h Set Mode (SM).\n * Ps = 2 -> Keyboard Action Mode (AM).\n * Ps = 4 -> Insert Mode (IRM).\n * Ps = 1 2 -> Send/receive (SRM).\n * Ps = 2 0 -> Automatic Newline (LNM).\n *\n * @vt: #P[Only IRM is supported.] CSI SM \"Set Mode\" \"CSI Pm h\" \"Set various terminal modes.\"\n * Supported param values by SM:\n *\n * | Param | Action | Support |\n * | ----- | -------------------------------------- | ------- |\n * | 2 | Keyboard Action Mode (KAM). Always on. | #N |\n * | 4 | Insert Mode (IRM). | #Y |\n * | 12 | Send/receive (SRM). Always off. | #N |\n * | 20 | Automatic Newline (LNM). | #Y |\n */\n public setMode(params: IParams): boolean {\n for (let i = 0; i < params.length; i++) {\n switch (params.params[i]) {\n case 4:\n this._coreService.modes.insertMode = true;\n break;\n case 20:\n this._optionsService.options.convertEol = true;\n break;\n }\n }\n return true;\n }\n\n /**\n * CSI ? Pm h\n * DEC Private Mode Set (DECSET).\n * Ps = 1 -> Application Cursor Keys (DECCKM).\n * Ps = 2 -> Designate USASCII for character sets G0-G3\n * (DECANM), and set VT100 mode.\n * Ps = 3 -> 132 Column Mode (DECCOLM).\n * Ps = 4 -> Smooth (Slow) Scroll (DECSCLM).\n * Ps = 5 -> Reverse Video (DECSCNM).\n * Ps = 6 -> Origin Mode (DECOM).\n * Ps = 7 -> Wraparound Mode (DECAWM).\n * Ps = 8 -> Auto-repeat Keys (DECARM).\n * Ps = 9 -> Send Mouse X & Y on button press. See the sec-\n * tion Mouse Tracking.\n * Ps = 1 0 -> Show toolbar (rxvt).\n * Ps = 1 2 -> Start Blinking Cursor (att610).\n * Ps = 1 8 -> Print form feed (DECPFF).\n * Ps = 1 9 -> Set print extent to full screen (DECPEX).\n * Ps = 2 5 -> Show Cursor (DECTCEM).\n * Ps = 3 0 -> Show scrollbar (rxvt).\n * Ps = 3 5 -> Enable font-shifting functions (rxvt).\n * Ps = 3 8 -> Enter Tektronix Mode (DECTEK).\n * Ps = 4 0 -> Allow 80 -> 132 Mode.\n * Ps = 4 1 -> more(1) fix (see curses resource).\n * Ps = 4 2 -> Enable Nation Replacement Character sets (DECN-\n * RCM).\n * Ps = 4 4 -> Turn On Margin Bell.\n * Ps = 4 5 -> Reverse-wraparound Mode.\n * Ps = 4 6 -> Start Logging. This is normally disabled by a\n * compile-time option.\n * Ps = 4 7 -> Use Alternate Screen Buffer. (This may be dis-\n * abled by the titeInhibit resource).\n * Ps = 6 6 -> Application keypad (DECNKM).\n * Ps = 6 7 -> Backarrow key sends backspace (DECBKM).\n * Ps = 1 0 0 0 -> Send Mouse X & Y on button press and\n * release. See the section Mouse Tracking.\n * Ps = 1 0 0 1 -> Use Hilite Mouse Tracking.\n * Ps = 1 0 0 2 -> Use Cell Motion Mouse Tracking.\n * Ps = 1 0 0 3 -> Use All Motion Mouse Tracking.\n * Ps = 1 0 0 4 -> Send FocusIn/FocusOut events.\n * Ps = 1 0 0 5 -> Enable Extended Mouse Mode.\n * Ps = 1 0 1 0 -> Scroll to bottom on tty output (rxvt).\n * Ps = 1 0 1 1 -> Scroll to bottom on key press (rxvt).\n * Ps = 1 0 3 4 -> Interpret \"meta\" key, sets eighth bit.\n * (enables the eightBitInput resource).\n * Ps = 1 0 3 5 -> Enable special modifiers for Alt and Num-\n * Lock keys. (This enables the numLock resource).\n * Ps = 1 0 3 6 -> Send ESC when Meta modifies a key. (This\n * enables the metaSendsEscape resource).\n * Ps = 1 0 3 7 -> Send DEL from the editing-keypad Delete\n * key.\n * Ps = 1 0 3 9 -> Send ESC when Alt modifies a key. (This\n * enables the altSendsEscape resource).\n * Ps = 1 0 4 0 -> Keep selection even if not highlighted.\n * (This enables the keepSelection resource).\n * Ps = 1 0 4 1 -> Use the CLIPBOARD selection. (This enables\n * the selectToClipboard resource).\n * Ps = 1 0 4 2 -> Enable Urgency window manager hint when\n * Control-G is received. (This enables the bellIsUrgent\n * resource).\n * Ps = 1 0 4 3 -> Enable raising of the window when Control-G\n * is received. (enables the popOnBell resource).\n * Ps = 1 0 4 7 -> Use Alternate Screen Buffer. (This may be\n * disabled by the titeInhibit resource).\n * Ps = 1 0 4 8 -> Save cursor as in DECSC. (This may be dis-\n * abled by the titeInhibit resource).\n * Ps = 1 0 4 9 -> Save cursor as in DECSC and use Alternate\n * Screen Buffer, clearing it first. (This may be disabled by\n * the titeInhibit resource). This combines the effects of the 1\n * 0 4 7 and 1 0 4 8 modes. Use this with terminfo-based\n * applications rather than the 4 7 mode.\n * Ps = 1 0 5 0 -> Set terminfo/termcap function-key mode.\n * Ps = 1 0 5 1 -> Set Sun function-key mode.\n * Ps = 1 0 5 2 -> Set HP function-key mode.\n * Ps = 1 0 5 3 -> Set SCO function-key mode.\n * Ps = 1 0 6 0 -> Set legacy keyboard emulation (X11R6).\n * Ps = 1 0 6 1 -> Set VT220 keyboard emulation.\n * Ps = 2 0 0 4 -> Set bracketed paste mode.\n * Modes:\n * http: *vt100.net/docs/vt220-rm/chapter4.html\n *\n * @vt: #P[See below for supported modes.] CSI DECSET \"DEC Private Set Mode\" \"CSI ? Pm h\" \"Set various terminal attributes.\"\n * Supported param values by DECSET:\n *\n * | param | Action | Support |\n * | ----- | ------------------------------------------------------- | --------|\n * | 1 | Application Cursor Keys (DECCKM). | #Y |\n * | 2 | Designate US-ASCII for character sets G0-G3 (DECANM). | #Y |\n * | 3 | 132 Column Mode (DECCOLM). | #Y |\n * | 6 | Origin Mode (DECOM). | #Y |\n * | 7 | Auto-wrap Mode (DECAWM). | #Y |\n * | 8 | Auto-repeat Keys (DECARM). Always on. | #N |\n * | 9 | X10 xterm mouse protocol. | #Y |\n * | 12 | Start Blinking Cursor. | #P[Requires the allowSetCursorBlink quirk option enabled.] |\n * | 25 | Show Cursor (DECTCEM). | #Y |\n * | 45 | Reverse wrap-around. | #Y |\n * | 47 | Use Alternate Screen Buffer. | #Y |\n * | 66 | Application keypad (DECNKM). | #Y |\n * | 1000 | X11 xterm mouse protocol. | #Y |\n * | 1002 | Use Cell Motion Mouse Tracking. | #Y |\n * | 1003 | Use All Motion Mouse Tracking. | #Y |\n * | 1004 | Send FocusIn/FocusOut events | #Y |\n * | 1005 | Enable UTF-8 Mouse Mode. | #N |\n * | 1006 | Enable SGR Mouse Mode. | #Y |\n * | 1015 | Enable urxvt Mouse Mode. | #N |\n * | 1016 | Enable SGR-Pixels Mouse Mode. | #Y |\n * | 1047 | Use Alternate Screen Buffer. | #Y |\n * | 1048 | Save cursor as in DECSC. | #Y |\n * | 1049 | Save cursor and switch to alternate buffer clearing it. | #P[Does not clear the alternate buffer.] |\n * | 2004 | Set bracketed paste mode. | #Y |\n *\n *\n * FIXME: implement DECSCNM, 1049 should clear altbuffer\n */\n public setModePrivate(params: IParams): boolean {\n for (let i = 0; i < params.length; i++) {\n switch (params.params[i]) {\n case 1:\n this._coreService.decPrivateModes.applicationCursorKeys = true;\n break;\n case 2:\n this._charsetService.setgCharset(0, DEFAULT_CHARSET);\n this._charsetService.setgCharset(1, DEFAULT_CHARSET);\n this._charsetService.setgCharset(2, DEFAULT_CHARSET);\n this._charsetService.setgCharset(3, DEFAULT_CHARSET);\n // set VT100 mode here\n break;\n case 3:\n /**\n * DECCOLM - 132 column mode.\n * This is only active if 'SetWinLines' (24) is enabled\n * through `options.windowsOptions`.\n */\n if (this._optionsService.rawOptions.windowOptions.setWinLines) {\n this._bufferService.resize(132, this._bufferService.rows);\n this._onRequestReset.fire();\n }\n break;\n case 6:\n this._coreService.decPrivateModes.origin = true;\n this._setCursor(0, 0);\n break;\n case 7:\n this._coreService.decPrivateModes.wraparound = true;\n break;\n case 12:\n if (this._optionsService.rawOptions.quirks?.allowSetCursorBlink) {\n this._optionsService.options.cursorBlink = true;\n }\n break;\n case 45:\n this._coreService.decPrivateModes.reverseWraparound = true;\n break;\n case 66:\n this._logService.debug('Serial port requested application keypad.');\n this._coreService.decPrivateModes.applicationKeypad = true;\n this._onRequestSyncScrollBar.fire();\n break;\n case 9: // X10 Mouse\n // no release, no motion, no wheel, no modifiers.\n this._mouseStateService.activeProtocol = 'X10';\n break;\n case 1000: // vt200 mouse\n // no motion.\n this._mouseStateService.activeProtocol = 'VT200';\n break;\n case 1002: // button event mouse\n this._mouseStateService.activeProtocol = 'DRAG';\n break;\n case 1003: // any event mouse\n // any event - sends motion events,\n // even if there is no button held down.\n this._mouseStateService.activeProtocol = 'ANY';\n break;\n case 1004: // send focusin/focusout events\n // focusin: ^[[I\n // focusout: ^[[O\n this._coreService.decPrivateModes.sendFocus = true;\n this._onRequestSendFocus.fire();\n break;\n case 1005: // utf8 ext mode mouse - removed in #2507\n this._logService.debug('DECSET 1005 not supported (see #2507)');\n break;\n case 1006: // sgr ext mode mouse\n this._mouseStateService.activeEncoding = 'SGR';\n break;\n case 1015: // urxvt ext mode mouse - removed in #2507\n this._logService.debug('DECSET 1015 not supported (see #2507)');\n break;\n case 1016: // sgr pixels mode mouse\n this._mouseStateService.activeEncoding = 'SGR_PIXELS';\n break;\n case 25: // show cursor\n this._coreService.isCursorHidden = false;\n break;\n case 1048: // alt screen cursor\n this.saveCursor();\n break;\n case 1049: // alt screen buffer cursor\n this.saveCursor();\n // FALL-THROUGH\n case 47: // alt screen buffer\n case 1047: // alt screen buffer\n // Swap kitty keyboard flags: save main, restore alt\n if (this._optionsService.rawOptions.vtExtensions?.kittyKeyboard) {\n const state = this._coreService.kittyKeyboard;\n state.mainFlags = state.flags;\n state.flags = state.altFlags;\n }\n this._bufferService.buffers.activateAltBuffer(this._eraseAttrData());\n this._coreService.isCursorInitialized = true;\n this._onRequestRefreshRows.fire(undefined);\n this._onRequestSyncScrollBar.fire();\n break;\n case 2004: // bracketed paste mode (https://cirw.in/blog/bracketed-paste)\n this._coreService.decPrivateModes.bracketedPasteMode = true;\n break;\n case 2026: // synchronized output (https://github.com/contour-terminal/vt-extensions/blob/master/synchronized-output.md)\n this._coreService.decPrivateModes.synchronizedOutput = true;\n break;\n case 2031: // color scheme updates (https://contour-terminal.org/vt-extensions/color-palette-update-notifications/)\n if (this._optionsService.rawOptions.vtExtensions?.colorSchemeQuery ?? true) {\n this._coreService.decPrivateModes.colorSchemeUpdates = true;\n }\n break;\n case 9001: // win32-input-mode (https://github.com/microsoft/terminal/blob/main/doc/specs/%234999%20-%20Improved%20keyboard%20handling%20in%20Conpty.md)\n if (this._optionsService.rawOptions.vtExtensions?.win32InputMode) {\n this._coreService.decPrivateModes.win32InputMode = true;\n }\n break;\n }\n }\n return true;\n }\n\n\n /**\n * CSI Pm l Reset Mode (RM).\n * Ps = 2 -> Keyboard Action Mode (AM).\n * Ps = 4 -> Replace Mode (IRM).\n * Ps = 1 2 -> Send/receive (SRM).\n * Ps = 2 0 -> Normal Linefeed (LNM).\n *\n * @vt: #P[Only IRM is supported.] CSI RM \"Reset Mode\" \"CSI Pm l\" \"Set various terminal attributes.\"\n * Supported param values by RM:\n *\n * | Param | Action | Support |\n * | ----- | -------------------------------------- | ------- |\n * | 2 | Keyboard Action Mode (KAM). Always on. | #N |\n * | 4 | Replace Mode (IRM). (default) | #Y |\n * | 12 | Send/receive (SRM). Always off. | #N |\n * | 20 | Normal Linefeed (LNM). | #Y |\n *\n *\n * FIXME: why is LNM commented out?\n */\n public resetMode(params: IParams): boolean {\n for (let i = 0; i < params.length; i++) {\n switch (params.params[i]) {\n case 4:\n this._coreService.modes.insertMode = false;\n break;\n case 20:\n this._optionsService.options.convertEol = false;\n break;\n }\n }\n return true;\n }\n\n /**\n * CSI ? Pm l\n * DEC Private Mode Reset (DECRST).\n * Ps = 1 -> Normal Cursor Keys (DECCKM).\n * Ps = 2 -> Designate VT52 mode (DECANM).\n * Ps = 3 -> 80 Column Mode (DECCOLM).\n * Ps = 4 -> Jump (Fast) Scroll (DECSCLM).\n * Ps = 5 -> Normal Video (DECSCNM).\n * Ps = 6 -> Normal Cursor Mode (DECOM).\n * Ps = 7 -> No Wraparound Mode (DECAWM).\n * Ps = 8 -> No Auto-repeat Keys (DECARM).\n * Ps = 9 -> Don't send Mouse X & Y on button press.\n * Ps = 1 0 -> Hide toolbar (rxvt).\n * Ps = 1 2 -> Stop Blinking Cursor (att610).\n * Ps = 1 8 -> Don't print form feed (DECPFF).\n * Ps = 1 9 -> Limit print to scrolling region (DECPEX).\n * Ps = 2 5 -> Hide Cursor (DECTCEM).\n * Ps = 3 0 -> Don't show scrollbar (rxvt).\n * Ps = 3 5 -> Disable font-shifting functions (rxvt).\n * Ps = 4 0 -> Disallow 80 -> 132 Mode.\n * Ps = 4 1 -> No more(1) fix (see curses resource).\n * Ps = 4 2 -> Disable Nation Replacement Character sets (DEC-\n * NRCM).\n * Ps = 4 4 -> Turn Off Margin Bell.\n * Ps = 4 5 -> No Reverse-wraparound Mode.\n * Ps = 4 6 -> Stop Logging. (This is normally disabled by a\n * compile-time option).\n * Ps = 4 7 -> Use Normal Screen Buffer.\n * Ps = 6 6 -> Numeric keypad (DECNKM).\n * Ps = 6 7 -> Backarrow key sends delete (DECBKM).\n * Ps = 1 0 0 0 -> Don't send Mouse X & Y on button press and\n * release. See the section Mouse Tracking.\n * Ps = 1 0 0 1 -> Don't use Hilite Mouse Tracking.\n * Ps = 1 0 0 2 -> Don't use Cell Motion Mouse Tracking.\n * Ps = 1 0 0 3 -> Don't use All Motion Mouse Tracking.\n * Ps = 1 0 0 4 -> Don't send FocusIn/FocusOut events.\n * Ps = 1 0 0 5 -> Disable Extended Mouse Mode.\n * Ps = 1 0 1 0 -> Don't scroll to bottom on tty output\n * (rxvt).\n * Ps = 1 0 1 1 -> Don't scroll to bottom on key press (rxvt).\n * Ps = 1 0 3 4 -> Don't interpret \"meta\" key. (This disables\n * the eightBitInput resource).\n * Ps = 1 0 3 5 -> Disable special modifiers for Alt and Num-\n * Lock keys. (This disables the numLock resource).\n * Ps = 1 0 3 6 -> Don't send ESC when Meta modifies a key.\n * (This disables the metaSendsEscape resource).\n * Ps = 1 0 3 7 -> Send VT220 Remove from the editing-keypad\n * Delete key.\n * Ps = 1 0 3 9 -> Don't send ESC when Alt modifies a key.\n * (This disables the altSendsEscape resource).\n * Ps = 1 0 4 0 -> Do not keep selection when not highlighted.\n * (This disables the keepSelection resource).\n * Ps = 1 0 4 1 -> Use the PRIMARY selection. (This disables\n * the selectToClipboard resource).\n * Ps = 1 0 4 2 -> Disable Urgency window manager hint when\n * Control-G is received. (This disables the bellIsUrgent\n * resource).\n * Ps = 1 0 4 3 -> Disable raising of the window when Control-\n * G is received. (This disables the popOnBell resource).\n * Ps = 1 0 4 7 -> Use Normal Screen Buffer, clearing screen\n * first if in the Alternate Screen. (This may be disabled by\n * the titeInhibit resource).\n * Ps = 1 0 4 8 -> Restore cursor as in DECRC. (This may be\n * disabled by the titeInhibit resource).\n * Ps = 1 0 4 9 -> Use Normal Screen Buffer and restore cursor\n * as in DECRC. (This may be disabled by the titeInhibit\n * resource). This combines the effects of the 1 0 4 7 and 1 0\n * 4 8 modes. Use this with terminfo-based applications rather\n * than the 4 7 mode.\n * Ps = 1 0 5 0 -> Reset terminfo/termcap function-key mode.\n * Ps = 1 0 5 1 -> Reset Sun function-key mode.\n * Ps = 1 0 5 2 -> Reset HP function-key mode.\n * Ps = 1 0 5 3 -> Reset SCO function-key mode.\n * Ps = 1 0 6 0 -> Reset legacy keyboard emulation (X11R6).\n * Ps = 1 0 6 1 -> Reset keyboard emulation to Sun/PC style.\n * Ps = 2 0 0 4 -> Reset bracketed paste mode.\n *\n * @vt: #P[See below for supported modes.] CSI DECRST \"DEC Private Reset Mode\" \"CSI ? Pm l\" \"Reset various terminal attributes.\"\n * Supported param values by DECRST:\n *\n * | param | Action | Support |\n * | ----- | ------------------------------------------------------- | ------- |\n * | 1 | Normal Cursor Keys (DECCKM). | #Y |\n * | 2 | Designate VT52 mode (DECANM). | #N |\n * | 3 | 80 Column Mode (DECCOLM). | #B[Switches to old column width instead of 80.] |\n * | 6 | Normal Cursor Mode (DECOM). | #Y |\n * | 7 | No Wraparound Mode (DECAWM). | #Y |\n * | 8 | No Auto-repeat Keys (DECARM). | #N |\n * | 9 | Don't send Mouse X & Y on button press. | #Y |\n * | 12 | Stop Blinking Cursor. | #P[Requires the allowSetCursorBlink quirk option enabled.] |\n * | 25 | Hide Cursor (DECTCEM). | #Y |\n * | 45 | No reverse wrap-around. | #Y |\n * | 47 | Use Normal Screen Buffer. | #Y |\n * | 66 | Numeric keypad (DECNKM). | #Y |\n * | 1000 | Don't send Mouse reports. | #Y |\n * | 1002 | Don't use Cell Motion Mouse Tracking. | #Y |\n * | 1003 | Don't use All Motion Mouse Tracking. | #Y |\n * | 1004 | Don't send FocusIn/FocusOut events. | #Y |\n * | 1005 | Disable UTF-8 Mouse Mode. | #N |\n * | 1006 | Disable SGR Mouse Mode. | #Y |\n * | 1015 | Disable urxvt Mouse Mode. | #N |\n * | 1016 | Disable SGR-Pixels Mouse Mode. | #Y |\n * | 1047 | Use Normal Screen Buffer (clearing screen if in alt). | #Y |\n * | 1048 | Restore cursor as in DECRC. | #Y |\n * | 1049 | Use Normal Screen Buffer and restore cursor. | #Y |\n * | 2004 | Reset bracketed paste mode. | #Y |\n *\n *\n * FIXME: DECCOLM is currently broken (already fixed in window options PR)\n */\n public resetModePrivate(params: IParams): boolean {\n for (let i = 0; i < params.length; i++) {\n switch (params.params[i]) {\n case 1:\n this._coreService.decPrivateModes.applicationCursorKeys = false;\n break;\n case 3:\n /**\n * DECCOLM - 80 column mode.\n * This is only active if 'SetWinLines' (24) is enabled\n * through `options.windowsOptions`.\n */\n if (this._optionsService.rawOptions.windowOptions.setWinLines) {\n this._bufferService.resize(80, this._bufferService.rows);\n this._onRequestReset.fire();\n }\n break;\n case 6:\n this._coreService.decPrivateModes.origin = false;\n this._setCursor(0, 0);\n break;\n case 7:\n this._coreService.decPrivateModes.wraparound = false;\n break;\n case 12:\n if (this._optionsService.rawOptions.quirks?.allowSetCursorBlink) {\n this._optionsService.options.cursorBlink = false;\n }\n break;\n case 45:\n this._coreService.decPrivateModes.reverseWraparound = false;\n break;\n case 66:\n this._logService.debug('Switching back to normal keypad.');\n this._coreService.decPrivateModes.applicationKeypad = false;\n this._onRequestSyncScrollBar.fire();\n break;\n case 9: // X10 Mouse\n case 1000: // vt200 mouse\n case 1002: // button event mouse\n case 1003: // any event mouse\n this._mouseStateService.activeProtocol = 'NONE';\n break;\n case 1004: // send focusin/focusout events\n this._coreService.decPrivateModes.sendFocus = false;\n break;\n case 1005: // utf8 ext mode mouse - removed in #2507\n this._logService.debug('DECRST 1005 not supported (see #2507)');\n break;\n case 1006: // sgr ext mode mouse\n this._mouseStateService.activeEncoding = 'DEFAULT';\n break;\n case 1015: // urxvt ext mode mouse - removed in #2507\n this._logService.debug('DECRST 1015 not supported (see #2507)');\n break;\n case 1016: // sgr pixels mode mouse\n this._mouseStateService.activeEncoding = 'DEFAULT';\n break;\n case 25: // hide cursor\n this._coreService.isCursorHidden = true;\n break;\n case 1048: // alt screen cursor\n this.restoreCursor();\n break;\n case 1049: // alt screen buffer cursor\n // FALL-THROUGH\n case 47: // normal screen buffer\n case 1047: // normal screen buffer - clearing it first\n // Swap kitty keyboard flags: save alt, restore main\n if (this._optionsService.rawOptions.vtExtensions?.kittyKeyboard) {\n const state = this._coreService.kittyKeyboard;\n state.altFlags = state.flags;\n state.flags = state.mainFlags;\n }\n // Ensure the selection manager has the correct buffer\n this._bufferService.buffers.activateNormalBuffer();\n if (params.params[i] === 1049) {\n this.restoreCursor();\n }\n this._coreService.isCursorInitialized = true;\n this._onRequestRefreshRows.fire(undefined);\n this._onRequestSyncScrollBar.fire();\n break;\n case 2004: // bracketed paste mode (https://cirw.in/blog/bracketed-paste)\n this._coreService.decPrivateModes.bracketedPasteMode = false;\n break;\n case 2026: // synchronized output (https://github.com/contour-terminal/vt-extensions/blob/master/synchronized-output.md)\n this._coreService.decPrivateModes.synchronizedOutput = false;\n this._onRequestRefreshRows.fire(undefined);\n break;\n case 2031: // color scheme updates (https://contour-terminal.org/vt-extensions/color-palette-update-notifications/)\n if (this._optionsService.rawOptions.vtExtensions?.colorSchemeQuery ?? true) {\n this._coreService.decPrivateModes.colorSchemeUpdates = false;\n }\n break;\n case 9001: // win32-input-mode\n if (this._optionsService.rawOptions.vtExtensions?.win32InputMode) {\n this._coreService.decPrivateModes.win32InputMode = false;\n }\n break;\n }\n }\n return true;\n }\n\n /**\n * CSI Ps $ p Request ANSI Mode (DECRQM).\n *\n * Reports CSI Ps; Pm $ y (DECRPM), where Ps is the mode number as in SM/RM,\n * and Pm is the mode value:\n * 0 - not recognized\n * 1 - set\n * 2 - reset\n * 3 - permanently set\n * 4 - permanently reset\n *\n * @vt: #Y CSI DECRQM \"Request Mode\" \"CSI Ps $p\" \"Request mode state.\"\n * Returns a report as `CSI Ps; Pm $ y` (DECRPM), where `Ps` is the mode number as in SM/RM\n * or DECSET/DECRST, and `Pm` is the mode value:\n * - 0: not recognized\n * - 1: set\n * - 2: reset\n * - 3: permanently set\n * - 4: permanently reset\n *\n * For modes not understood xterm.js always returns `notRecognized`. In general this means,\n * that a certain operation mode is not implemented and cannot be used.\n *\n * Modes changing the active terminal buffer (47, 1047, 1049) are not subqueried\n * and only report, whether the alternate buffer is set.\n *\n * Mouse encodings and mouse protocols are handled mutual exclusive,\n * thus only one of each of those can be set at a given time.\n *\n * There is a chance, that some mode reports are not fully in line with xterm.js' behavior,\n * e.g. if the default implementation already exposes a certain behavior. If you find\n * discrepancies in the mode reports, please file a bug.\n */\n public requestMode(params: IParams, ansi: boolean): boolean {\n // return value as in DECRPM\n const enum V {\n NOT_RECOGNIZED = 0,\n SET = 1,\n RESET = 2,\n PERMANENTLY_SET = 3,\n PERMANENTLY_RESET = 4\n }\n\n // access helpers\n const dm = this._coreService.decPrivateModes;\n const { activeProtocol: mouseProtocol, activeEncoding: mouseEncoding } = this._mouseStateService;\n const cs = this._coreService;\n const { buffers, cols } = this._bufferService;\n const { active, alt } = buffers;\n const opts = this._optionsService.rawOptions;\n\n const f = (m: number, v: V): boolean => {\n cs.triggerDataEvent(`${C0.ESC}[${ansi ? '' : '?'}${m};${v}$y`);\n return true;\n };\n const b2v = (value: boolean): V => value ? V.SET : V.RESET;\n\n const p = params.params[0];\n\n if (ansi) {\n if (p === 2) return f(p, V.PERMANENTLY_RESET);\n if (p === 4) return f(p, b2v(cs.modes.insertMode));\n if (p === 12) return f(p, V.PERMANENTLY_SET);\n if (p === 20) return f(p, b2v(opts.convertEol));\n return f(p, V.NOT_RECOGNIZED);\n }\n\n if (p === 1) return f(p, b2v(dm.applicationCursorKeys));\n if (p === 3) return f(p, opts.windowOptions.setWinLines ? (cols === 80 ? V.RESET : cols === 132 ? V.SET : V.NOT_RECOGNIZED) : V.NOT_RECOGNIZED);\n if (p === 6) return f(p, b2v(dm.origin));\n if (p === 7) return f(p, b2v(dm.wraparound));\n if (p === 8) return f(p, V.PERMANENTLY_SET);\n if (p === 9) return f(p, b2v(mouseProtocol === 'X10'));\n if (p === 12) return f(p, b2v(opts.cursorBlink));\n if (p === 25) return f(p, b2v(!cs.isCursorHidden));\n if (p === 45) return f(p, b2v(dm.reverseWraparound));\n if (p === 66) return f(p, b2v(dm.applicationKeypad));\n if (p === 67) return f(p, V.PERMANENTLY_RESET);\n if (p === 1000) return f(p, b2v(mouseProtocol === 'VT200'));\n if (p === 1002) return f(p, b2v(mouseProtocol === 'DRAG'));\n if (p === 1003) return f(p, b2v(mouseProtocol === 'ANY'));\n if (p === 1004) return f(p, b2v(dm.sendFocus));\n if (p === 1005) return f(p, V.PERMANENTLY_RESET);\n if (p === 1006) return f(p, b2v(mouseEncoding === 'SGR'));\n if (p === 1015) return f(p, V.PERMANENTLY_RESET);\n if (p === 1016) return f(p, b2v(mouseEncoding === 'SGR_PIXELS'));\n if (p === 1048) return f(p, V.SET); // xterm always returns SET here\n if (p === 47 || p === 1047 || p === 1049) return f(p, b2v(active === alt));\n if (p === 2004) return f(p, b2v(dm.bracketedPasteMode));\n if (p === 2026) return f(p, b2v(dm.synchronizedOutput));\n if (p === 9001) return this._optionsService.rawOptions.vtExtensions?.win32InputMode ? f(p, b2v(dm.win32InputMode)) : f(p, V.NOT_RECOGNIZED);\n return f(p, V.NOT_RECOGNIZED);\n }\n\n /**\n * Helper to write color information packed with color mode.\n */\n private _updateAttrColor(color: number, mode: number, c1: number, c2: number, c3: number): number {\n if (mode === 2) {\n color |= Attributes.CM_RGB;\n color &= ~Attributes.RGB_MASK;\n color |= AttributeData.fromColorRGB([c1, c2, c3]);\n } else if (mode === 5) {\n color &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);\n color |= Attributes.CM_P256 | (c1 & 0xff);\n }\n return color;\n }\n\n /**\n * Helper to extract and apply color params/subparams.\n * Returns advance for params index.\n */\n private _extractColor(params: IParams, pos: number, attr: IAttributeData): number {\n // normalize params\n // meaning: [target, CM, ign, val, val, val]\n // RGB : [ 38/48, 2, ign, r, g, b]\n // P256 : [ 38/48, 5, ign, v, ign, ign]\n const accu = [0, 0, -1, 0, 0, 0];\n\n // alignment placeholder for non color space sequences\n let cSpace = 0;\n\n // return advance we took in params\n let advance = 0;\n\n do {\n accu[advance + cSpace] = params.params[pos + advance];\n if (params.hasSubParams(pos + advance)) {\n const subparams = params.getSubParams(pos + advance)!;\n let i = 0;\n do {\n if (accu[1] === 5) {\n cSpace = 1;\n }\n accu[advance + i + 1 + cSpace] = subparams[i];\n } while (++i < subparams.length && i + advance + 1 + cSpace < accu.length);\n break;\n }\n // exit early if can decide color mode with semicolons\n if ((accu[1] === 5 && advance + cSpace >= 2)\n || (accu[1] === 2 && advance + cSpace >= 5)) {\n break;\n }\n // offset colorSpace slot for semicolon mode\n if (accu[1]) {\n cSpace = 1;\n }\n } while (++advance + pos < params.length && advance + cSpace < accu.length);\n\n // set default values to 0\n for (let i = 2; i < accu.length; ++i) {\n if (accu[i] === -1) {\n accu[i] = 0;\n }\n }\n\n // apply colors\n switch (accu[0]) {\n case 38:\n attr.fg = this._updateAttrColor(attr.fg, accu[1], accu[3], accu[4], accu[5]);\n break;\n case 48:\n attr.bg = this._updateAttrColor(attr.bg, accu[1], accu[3], accu[4], accu[5]);\n break;\n case 58:\n attr.extended = attr.extended.clone();\n attr.extended.underlineColor = this._updateAttrColor(attr.extended.underlineColor, accu[1], accu[3], accu[4], accu[5]);\n }\n\n return advance;\n }\n\n /**\n * SGR 4 subparams:\n * 4:0 - equal to SGR 24 (turn off all underline)\n * 4:1 - equal to SGR 4 (single underline)\n * 4:2 - equal to SGR 21 (double underline)\n * 4:3 - curly underline\n * 4:4 - dotted underline\n * 4:5 - dashed underline\n */\n private _processUnderline(style: number, attr: IAttributeData): void {\n // treat extended attrs as immutable, thus always clone from old one\n // this is needed since the buffer only holds references to it\n attr.extended = attr.extended.clone();\n\n // default to 1 == single underline\n if (!~style || style > 5) {\n style = 1;\n }\n attr.extended.underlineStyle = style;\n attr.fg |= FgFlags.UNDERLINE;\n\n // 0 deactivates underline\n if (style === 0) {\n attr.fg &= ~FgFlags.UNDERLINE;\n }\n\n // update HAS_EXTENDED in BG\n attr.updateExtended();\n }\n\n private _processSGR0(attr: IAttributeData): void {\n attr.fg = DEFAULT_ATTR_DATA.fg;\n attr.bg = DEFAULT_ATTR_DATA.bg;\n attr.extended = attr.extended.clone();\n // Reset underline style and color. Note that we don't want to reset other\n // fields such as the url id.\n attr.extended.underlineStyle = UnderlineStyle.NONE;\n attr.extended.underlineColor &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);\n attr.updateExtended();\n }\n\n /**\n * CSI Pm m Character Attributes (SGR).\n *\n * @vt: #P[See below for supported attributes.] CSI SGR \"Select Graphic Rendition\" \"CSI Pm m\" \"Set/Reset various text attributes.\"\n * SGR selects one or more character attributes at the same time. Multiple params (up to 32)\n * are applied in order from left to right. The changed attributes are applied to all new\n * characters received. If you move characters in the viewport by scrolling or any other means,\n * then the attributes move with the characters.\n *\n * Supported param values by SGR:\n *\n * | Param | Meaning | Support |\n * | --------- | -------------------------------------------------------- | ------- |\n * | 0 | Normal (default). Resets any other preceding SGR. | #Y |\n * | 1 | Bold. (also see `options.drawBoldTextInBrightColors`) | #Y |\n * | 2 | Faint, decreased intensity. | #Y |\n * | 3 | Italic. | #Y |\n * | 4 | Underlined (see below for style support). | #Y |\n * | 5 | Slowly blinking. | #N |\n * | 6 | Rapidly blinking. | #N |\n * | 7 | Inverse. Flips foreground and background color. | #Y |\n * | 8 | Invisible (hidden). | #Y |\n * | 9 | Crossed-out characters (strikethrough). | #Y |\n * | 21 | Doubly underlined. | #Y |\n * | 22 | Normal (neither bold nor faint). | #Y |\n * | 23 | No italic. | #Y |\n * | 24 | Not underlined. | #Y |\n * | 25 | Steady (not blinking). | #Y |\n * | 27 | Positive (not inverse). | #Y |\n * | 28 | Visible (not hidden). | #Y |\n * | 29 | Not Crossed-out (strikethrough). | #Y |\n * | 30 | Foreground color: Black. | #Y |\n * | 31 | Foreground color: Red. | #Y |\n * | 32 | Foreground color: Green. | #Y |\n * | 33 | Foreground color: Yellow. | #Y |\n * | 34 | Foreground color: Blue. | #Y |\n * | 35 | Foreground color: Magenta. | #Y |\n * | 36 | Foreground color: Cyan. | #Y |\n * | 37 | Foreground color: White. | #Y |\n * | 38 | Foreground color: Extended color. | #P[Support for RGB and indexed colors, see below.] |\n * | 39 | Foreground color: Default (original). | #Y |\n * | 40 | Background color: Black. | #Y |\n * | 41 | Background color: Red. | #Y |\n * | 42 | Background color: Green. | #Y |\n * | 43 | Background color: Yellow. | #Y |\n * | 44 | Background color: Blue. | #Y |\n * | 45 | Background color: Magenta. | #Y |\n * | 46 | Background color: Cyan. | #Y |\n * | 47 | Background color: White. | #Y |\n * | 48 | Background color: Extended color. | #P[Support for RGB and indexed colors, see below.] |\n * | 49 | Background color: Default (original). | #Y |\n * | 53 | Overlined. | #Y |\n * | 55 | Not Overlined. | #Y |\n * | 58 | Underline color: Extended color. | #P[Support for RGB and indexed colors, see below.] |\n * | 221 | Not bold (kitty extension). | #Y |\n * | 222 | Not faint (kitty extension). | #Y |\n * | 90 - 97 | Bright foreground color (analogous to 30 - 37). | #Y |\n * | 100 - 107 | Bright background color (analogous to 40 - 47). | #Y |\n *\n * Underline supports subparams to denote the style in the form `4 : x`:\n *\n * | x | Meaning | Support |\n * | ------ | ------------------------------------------------------------- | ------- |\n * | 0 | No underline. Same as `SGR 24 m`. | #Y |\n * | 1 | Single underline. Same as `SGR 4 m`. | #Y |\n * | 2 | Double underline. | #Y |\n * | 3 | Curly underline. | #Y |\n * | 4 | Dotted underline. | #Y |\n * | 5 | Dashed underline. | #Y |\n * | other | Single underline. Same as `SGR 4 m`. | #Y |\n *\n * Extended colors are supported for foreground (Ps=38), background (Ps=48) and underline (Ps=58)\n * as follows:\n *\n * | Ps + 1 | Meaning | Support |\n * | ------ | ------------------------------------------------------------- | ------- |\n * | 0 | Implementation defined. | #N |\n * | 1 | Transparent. | #N |\n * | 2 | RGB color as `Ps ; 2 ; R ; G ; B` or `Ps : 2 : : R : G : B`. | #Y |\n * | 3 | CMY color. | #N |\n * | 4 | CMYK color. | #N |\n * | 5 | Indexed (256 colors) as `Ps ; 5 ; INDEX` or `Ps : 5 : INDEX`. | #Y |\n */\n public charAttributes(params: IParams): boolean {\n // Optimize a single SGR0.\n if (params.length === 1 && params.params[0] === 0) {\n this._processSGR0(this._curAttrData);\n return true;\n }\n\n const l = params.length;\n let p;\n const attr = this._curAttrData;\n\n for (let i = 0; i < l; i++) {\n p = params.params[i];\n if (p >= 30 && p <= 37) {\n // fg color 8\n attr.fg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);\n attr.fg |= Attributes.CM_P16 | (p - 30);\n } else if (p >= 40 && p <= 47) {\n // bg color 8\n attr.bg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);\n attr.bg |= Attributes.CM_P16 | (p - 40);\n } else if (p >= 90 && p <= 97) {\n // fg color 16\n attr.fg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);\n attr.fg |= Attributes.CM_P16 | (p - 90) | 8;\n } else if (p >= 100 && p <= 107) {\n // bg color 16\n attr.bg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);\n attr.bg |= Attributes.CM_P16 | (p - 100) | 8;\n } else if (p === 0) {\n // default\n this._processSGR0(attr);\n } else if (p === 1) {\n // bold text\n attr.fg |= FgFlags.BOLD;\n } else if (p === 3) {\n // italic text\n attr.bg |= BgFlags.ITALIC;\n } else if (p === 4) {\n // underlined text\n attr.fg |= FgFlags.UNDERLINE;\n this._processUnderline(params.hasSubParams(i) ? params.getSubParams(i)![0] : UnderlineStyle.SINGLE, attr);\n } else if (p === 5) {\n // blink\n attr.fg |= FgFlags.BLINK;\n } else if (p === 7) {\n // inverse and positive\n // test with: echo -e '\\e[31m\\e[42mhello\\e[7mworld\\e[27mhi\\e[m'\n attr.fg |= FgFlags.INVERSE;\n } else if (p === 8) {\n // invisible\n attr.fg |= FgFlags.INVISIBLE;\n } else if (p === 9) {\n // strikethrough\n attr.fg |= FgFlags.STRIKETHROUGH;\n } else if (p === 2) {\n // dimmed text\n attr.bg |= BgFlags.DIM;\n } else if (p === 21) {\n // double underline\n this._processUnderline(UnderlineStyle.DOUBLE, attr);\n } else if (p === 22) {\n // not bold nor faint\n attr.fg &= ~FgFlags.BOLD;\n attr.bg &= ~BgFlags.DIM;\n } else if (p === 23) {\n // not italic\n attr.bg &= ~BgFlags.ITALIC;\n } else if (p === 24) {\n // not underlined\n attr.fg &= ~FgFlags.UNDERLINE;\n this._processUnderline(UnderlineStyle.NONE, attr);\n } else if (p === 25) {\n // not blink\n attr.fg &= ~FgFlags.BLINK;\n } else if (p === 27) {\n // not inverse\n attr.fg &= ~FgFlags.INVERSE;\n } else if (p === 28) {\n // not invisible\n attr.fg &= ~FgFlags.INVISIBLE;\n } else if (p === 29) {\n // not strikethrough\n attr.fg &= ~FgFlags.STRIKETHROUGH;\n } else if (p === 39) {\n // reset fg\n attr.fg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);\n attr.fg |= DEFAULT_ATTR_DATA.fg & Attributes.RGB_MASK;\n } else if (p === 49) {\n // reset bg\n attr.bg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);\n attr.bg |= DEFAULT_ATTR_DATA.bg & Attributes.RGB_MASK;\n } else if (p === 38 || p === 48 || p === 58) {\n // fg color 256 and RGB\n i += this._extractColor(params, i, attr);\n } else if (p === 53) {\n // overline\n attr.bg |= BgFlags.OVERLINE;\n } else if (p === 55) {\n // not overline\n attr.bg &= ~BgFlags.OVERLINE;\n } else if (p === 221 && (this._optionsService.rawOptions.vtExtensions?.kittySgrBoldFaintControl ?? true)) {\n // not bold (kitty extension)\n attr.fg &= ~FgFlags.BOLD;\n } else if (p === 222 && (this._optionsService.rawOptions.vtExtensions?.kittySgrBoldFaintControl ?? true)) {\n // not faint (kitty extension)\n attr.bg &= ~BgFlags.DIM;\n } else if (p === 59) {\n attr.extended = attr.extended.clone();\n attr.extended.underlineColor = -1;\n attr.updateExtended();\n } else {\n this._logService.debug('Unknown SGR attribute: %d.', p);\n }\n }\n return true;\n }\n\n /**\n * CSI Ps n Device Status Report (DSR).\n * Ps = 5 -> Status Report. Result (``OK'') is\n * CSI 0 n\n * Ps = 6 -> Report Cursor Position (CPR) [row;column].\n * Result is\n * CSI r ; c R\n * CSI ? Ps n\n * Device Status Report (DSR, DEC-specific).\n * Ps = 6 -> Report Cursor Position (CPR) [row;column] as CSI\n * ? r ; c R (assumes page is zero).\n * Ps = 1 5 -> Report Printer status as CSI ? 1 0 n (ready).\n * or CSI ? 1 1 n (not ready).\n * Ps = 2 5 -> Report UDK status as CSI ? 2 0 n (unlocked)\n * or CSI ? 2 1 n (locked).\n * Ps = 2 6 -> Report Keyboard status as\n * CSI ? 2 7 ; 1 ; 0 ; 0 n (North American).\n * The last two parameters apply to VT400 & up, and denote key-\n * board ready and LK01 respectively.\n * Ps = 5 3 -> Report Locator status as\n * CSI ? 5 3 n Locator available, if compiled-in, or\n * CSI ? 5 0 n No Locator, if not.\n *\n * @vt: #Y CSI DSR \"Device Status Report\" \"CSI Ps n\" \"Request cursor position (CPR) with `Ps` = 6.\"\n */\n public deviceStatus(params: IParams): boolean {\n switch (params.params[0]) {\n case 5:\n // status report\n this._coreService.triggerDataEvent(`${C0.ESC}[0n`);\n break;\n case 6:\n // cursor position\n const y = this._activeBuffer.y + 1;\n const x = this._activeBuffer.x + 1;\n this._coreService.triggerDataEvent(`${C0.ESC}[${y};${x}R`);\n break;\n }\n return true;\n }\n\n // @vt: #P[Only CPR is supported.] CSI DECDSR \"DEC Device Status Report\" \"CSI ? Ps n\" \"Only CPR is supported (same as DSR).\"\n public deviceStatusPrivate(params: IParams): boolean {\n // modern xterm doesnt seem to\n // respond to any of these except ?6, 6, and 5\n switch (params.params[0]) {\n case 6:\n // cursor position\n const y = this._activeBuffer.y + 1;\n const x = this._activeBuffer.x + 1;\n this._coreService.triggerDataEvent(`${C0.ESC}[?${y};${x}R`);\n break;\n case 15:\n // no printer\n // this.handler(C0.ESC + '[?11n');\n break;\n case 25:\n // dont support user defined keys\n // this.handler(C0.ESC + '[?21n');\n break;\n case 26:\n // north american keyboard\n // this.handler(C0.ESC + '[?27;1;0;0n');\n break;\n case 53:\n // no dec locator/mouse\n // this.handler(C0.ESC + '[?50n');\n break;\n case 996:\n // color scheme query (https://contour-terminal.org/vt-extensions/color-palette-update-notifications/)\n if (this._optionsService.rawOptions.vtExtensions?.colorSchemeQuery ?? true) {\n this._onRequestColorSchemeQuery.fire();\n }\n break;\n }\n return true;\n }\n\n /**\n * CSI ! p Soft terminal reset (DECSTR).\n * http://vt100.net/docs/vt220-rm/table4-10.html\n *\n * @vt: #Y CSI DECSTR \"Soft Terminal Reset\" \"CSI ! p\" \"Reset several terminal attributes to initial state.\"\n * There are two terminal reset sequences - RIS and DECSTR. While RIS performs almost a full\n * terminal bootstrap, DECSTR only resets certain attributes. For most needs DECSTR should be\n * sufficient.\n *\n * The following terminal attributes are reset to default values:\n * - IRM is reset (dafault = false)\n * - scroll margins are reset (default = viewport size)\n * - erase attributes are reset to default\n * - charsets are reset\n * - DECSC data is reset to initial values\n * - DECOM is reset to absolute mode\n *\n *\n * FIXME: there are several more attributes missing (see VT520 manual)\n */\n public softReset(params: IParams): boolean {\n this._coreService.isCursorHidden = false;\n this._onRequestSyncScrollBar.fire();\n this._activeBuffer.scrollTop = 0;\n this._activeBuffer.scrollBottom = this._bufferService.rows - 1;\n this._curAttrData = DEFAULT_ATTR_DATA.clone();\n this._coreService.reset();\n this._charsetService.reset();\n\n // reset DECSC data\n this._activeBuffer.savedX = 0;\n this._activeBuffer.savedY = this._activeBuffer.ybase;\n this._activeBuffer.savedCurAttrData.fg = this._curAttrData.fg;\n this._activeBuffer.savedCurAttrData.bg = this._curAttrData.bg;\n this._activeBuffer.savedCharset = this._charsetService.charset;\n\n // reset DECOM\n this._coreService.decPrivateModes.origin = false;\n return true;\n }\n\n /**\n * CSI Ps SP q Set cursor style (DECSCUSR, VT520).\n * Ps = 0 -> reset to option.\n * Ps = 1 -> blinking block (default).\n * Ps = 2 -> steady block.\n * Ps = 3 -> blinking underline.\n * Ps = 4 -> steady underline.\n * Ps = 5 -> blinking bar (xterm).\n * Ps = 6 -> steady bar (xterm).\n *\n * @vt: #Y CSI DECSCUSR \"Set Cursor Style\" \"CSI Ps SP q\" \"Set cursor style.\"\n * Supported cursor styles:\n * - 0: reset to option\n * - empty, 1: blinking block\n * - 2: steady block\n * - 3: blinking underline\n * - 4: steady underline\n * - 5: blinking bar\n * - 6: steady bar\n */\n public setCursorStyle(params: IParams): boolean {\n const param = params.length === 0 ? 1 : params.params[0];\n if (param === 0) {\n this._coreService.decPrivateModes.cursorStyle = undefined;\n this._coreService.decPrivateModes.cursorBlink = undefined;\n } else {\n switch (param) {\n case 1:\n case 2:\n this._coreService.decPrivateModes.cursorStyle = 'block';\n break;\n case 3:\n case 4:\n this._coreService.decPrivateModes.cursorStyle = 'underline';\n break;\n case 5:\n case 6:\n this._coreService.decPrivateModes.cursorStyle = 'bar';\n break;\n }\n const isBlinking = param % 2 === 1;\n this._coreService.decPrivateModes.cursorBlink = isBlinking;\n }\n return true;\n }\n\n /**\n * CSI Ps ; Ps r\n * Set Scrolling Region [top;bottom] (default = full size of win-\n * dow) (DECSTBM).\n *\n * @vt: #Y CSI DECSTBM \"Set Top and Bottom Margin\" \"CSI Ps ; Ps r\" \"Set top and bottom margins of the viewport [top;bottom] (default = viewport size).\"\n */\n public setScrollRegion(params: IParams): boolean {\n const top = params.params[0] || 1;\n let bottom: number;\n\n if (params.length < 2 || (bottom = params.params[1]) > this._bufferService.rows || bottom === 0) {\n bottom = this._bufferService.rows;\n }\n\n if (bottom > top) {\n this._activeBuffer.scrollTop = top - 1;\n this._activeBuffer.scrollBottom = bottom - 1;\n this._setCursor(0, 0);\n }\n return true;\n }\n\n /**\n * CSI Ps ; Ps ; Ps t - Various window manipulations and reports (xterm)\n *\n * Note: Only those listed below are supported. All others are left to integrators and\n * need special treatment based on the embedding environment.\n *\n * Ps = 1 4 supported\n * Report xterm text area size in pixels.\n * Result is CSI 4 ; height ; width t\n * Ps = 14 ; 2 not implemented\n * Ps = 16 supported\n * Report xterm character cell size in pixels.\n * Result is CSI 6 ; height ; width t\n * Ps = 18 supported\n * Report the size of the text area in characters.\n * Result is CSI 8 ; height ; width t\n * Ps = 20 supported\n * Report xterm window's icon label.\n * Result is OSC L label ST\n * Ps = 21 supported\n * Report xterm window's title.\n * Result is OSC l label ST\n * Ps = 22 ; 0 -> Save xterm icon and window title on stack. supported\n * Ps = 22 ; 1 -> Save xterm icon title on stack. supported\n * Ps = 22 ; 2 -> Save xterm window title on stack. supported\n * Ps = 23 ; 0 -> Restore xterm icon and window title from stack. supported\n * Ps = 23 ; 1 -> Restore xterm icon title from stack. supported\n * Ps = 23 ; 2 -> Restore xterm window title from stack. supported\n * Ps >= 24 not implemented\n */\n public windowOptions(params: IParams): boolean {\n if (!paramToWindowOption(params.params[0], this._optionsService.rawOptions.windowOptions)) {\n return true;\n }\n const second = (params.length > 1) ? params.params[1] : 0;\n switch (params.params[0]) {\n case 14: // GetWinSizePixels, returns CSI 4 ; height ; width t\n if (second !== 2) {\n this._onRequestWindowsOptionsReport.fire(WindowsOptionsReportType.GET_WIN_SIZE_PIXELS);\n }\n break;\n case 16: // GetCellSizePixels, returns CSI 6 ; height ; width t\n this._onRequestWindowsOptionsReport.fire(WindowsOptionsReportType.GET_CELL_SIZE_PIXELS);\n break;\n case 18: // GetWinSizeChars, returns CSI 8 ; height ; width t\n if (this._bufferService) {\n this._coreService.triggerDataEvent(`${C0.ESC}[8;${this._bufferService.rows};${this._bufferService.cols}t`);\n }\n break;\n case 22: // PushTitle\n if (second === 0 || second === 2) {\n this._windowTitleStack.push(this._windowTitle);\n if (this._windowTitleStack.length > Constants.STACK_LIMIT) {\n this._windowTitleStack.shift();\n }\n }\n if (second === 0 || second === 1) {\n this._iconNameStack.push(this._iconName);\n if (this._iconNameStack.length > Constants.STACK_LIMIT) {\n this._iconNameStack.shift();\n }\n }\n break;\n case 23: // PopTitle\n if (second === 0 || second === 2) {\n if (this._windowTitleStack.length) {\n this.setTitle(this._windowTitleStack.pop()!);\n }\n }\n if (second === 0 || second === 1) {\n if (this._iconNameStack.length) {\n this.setIconName(this._iconNameStack.pop()!);\n }\n }\n break;\n }\n return true;\n }\n\n\n /**\n * CSI s\n * ESC 7\n * Save cursor (ANSI.SYS).\n *\n * @vt: #P[TODO...] CSI SCOSC \"Save Cursor\" \"CSI s\" \"Save cursor position, charmap and text attributes.\"\n * @vt: #Y ESC SC \"Save Cursor\" \"ESC 7\" \"Save cursor position, charmap and text attributes.\"\n */\n public saveCursor(params?: IParams): boolean {\n this._activeBuffer.savedX = this._activeBuffer.x;\n this._activeBuffer.savedY = this._activeBuffer.ybase + this._activeBuffer.y;\n this._activeBuffer.savedCurAttrData.fg = this._curAttrData.fg;\n this._activeBuffer.savedCurAttrData.bg = this._curAttrData.bg;\n this._activeBuffer.savedCharset = this._charsetService.charset;\n this._activeBuffer.savedCharsets = this._charsetService.charsets.slice();\n this._activeBuffer.savedGlevel = this._charsetService.glevel;\n this._activeBuffer.savedOriginMode = this._coreService.decPrivateModes.origin;\n this._activeBuffer.savedWraparoundMode = this._coreService.decPrivateModes.wraparound;\n return true;\n }\n\n\n /**\n * CSI u\n * ESC 8\n * Restore cursor (ANSI.SYS).\n *\n * @vt: #P[TODO...] CSI SCORC \"Restore Cursor\" \"CSI u\" \"Restore cursor position, charmap and text attributes.\"\n * @vt: #Y ESC RC \"Restore Cursor\" \"ESC 8\" \"Restore cursor position, charmap and text attributes.\"\n */\n public restoreCursor(params?: IParams): boolean {\n this._activeBuffer.x = this._activeBuffer.savedX || 0;\n this._activeBuffer.y = Math.max(this._activeBuffer.savedY - this._activeBuffer.ybase, 0);\n this._curAttrData.fg = this._activeBuffer.savedCurAttrData.fg;\n this._curAttrData.bg = this._activeBuffer.savedCurAttrData.bg;\n for (let i = 0; i < this._activeBuffer.savedCharsets.length; i++) {\n this._charsetService.setgCharset(i, this._activeBuffer.savedCharsets[i]);\n }\n this._charsetService.setgLevel(this._activeBuffer.savedGlevel);\n this._coreService.decPrivateModes.origin = this._activeBuffer.savedOriginMode;\n this._coreService.decPrivateModes.wraparound = this._activeBuffer.savedWraparoundMode;\n this._restrictCursor();\n return true;\n }\n\n /**\n * OSC 2; ST (set window title)\n * Proxy to set window title.\n *\n * @vt: #P[Icon name is not exposed.] OSC 0 \"Set Windows Title and Icon Name\" \"OSC 0 ; Pt BEL\" \"Set window title and icon name.\"\n * Icon name is not supported. For Window Title see below.\n *\n * @vt: #Y OSC 2 \"Set Windows Title\" \"OSC 2 ; Pt BEL\" \"Set window title.\"\n * xterm.js does not manipulate the title directly, instead exposes changes via the event\n * `Terminal.onTitleChange`.\n */\n public setTitle(data: string): boolean {\n this._windowTitle = data;\n this._onTitleChange.fire(data);\n return true;\n }\n\n /**\n * OSC 1; ST\n * Note: Icon name is not exposed.\n */\n public setIconName(data: string): boolean {\n this._iconName = data;\n return true;\n }\n\n /**\n * OSC 4; ; ST (set ANSI color to )\n *\n * @vt: #Y OSC 4 \"Set ANSI color\" \"OSC 4 ; c ; spec BEL\" \"Change color number `c` to the color specified by `spec`.\"\n * `c` is the color index between 0 and 255. The color format of `spec` is derived from\n * `XParseColor` (see OSC 10 for supported formats). There may be multipe `c ; spec` pairs present\n * in the same instruction. If `spec` contains `?` the terminal returns a sequence with the\n * currently set color.\n */\n public setOrReportIndexedColor(data: string): boolean {\n const event: IColorEvent = [];\n const slots = data.split(';');\n while (slots.length > 1) {\n const idx = slots.shift() as string;\n const spec = slots.shift() as string;\n if (/^\\d+$/.exec(idx)) {\n const index = parseInt(idx, 10);\n if (isValidColorIndex(index)) {\n if (spec === '?') {\n event.push({ type: ColorRequestType.REPORT, index });\n } else {\n const color = parseColor(spec);\n if (color) {\n event.push({ type: ColorRequestType.SET, index, color });\n }\n }\n }\n }\n }\n if (event.length) {\n this._onColor.fire(event);\n }\n return true;\n }\n\n /**\n * OSC 8 ; ; ST - create hyperlink\n * OSC 8 ; ; ST - finish hyperlink\n *\n * Test case:\n *\n * ```sh\n * printf '\\e]8;;http://example.com\\e\\\\This is a link\\e]8;;\\e\\\\\\n'\n * ```\n *\n * @vt: #Y OSC 8 \"Create hyperlink\" \"OSC 8 ; params ; uri BEL\" \"Create a hyperlink to `uri` using `params`.\"\n * `uri` is a hyperlink starting with `http://`, `https://`, `ftp://`, `file://` or `mailto://`. `params` is an\n * optional list of key=value assignments, separated by the : character.\n * Example: `id=xyz123:foo=bar:baz=quux`.\n * Currently only the id key is defined. Cells that share the same ID and URI share hover\n * feedback. Use `OSC 8 ; ; BEL` to finish the current hyperlink.\n */\n public setHyperlink(data: string): boolean {\n // Arg parsing is special cases to support unencoded semi-colons in the URIs (#4944)\n const idx = data.indexOf(';');\n if (idx === -1) {\n // malformed sequence, just return as handled\n return true;\n }\n const id = data.slice(0, idx).trim();\n const uri = data.slice(idx + 1);\n if (uri) {\n return this._createHyperlink(id, uri);\n }\n if (id.trim()) {\n return false;\n }\n return this._finishHyperlink();\n }\n\n private _createHyperlink(params: string, uri: string): boolean {\n // It's legal to open a new hyperlink without explicitly finishing the previous one\n if (this._getCurrentLinkId()) {\n this._finishHyperlink();\n }\n const parsedParams = params.split(':');\n let id: string | undefined;\n const idParamIndex = parsedParams.findIndex(e => e.startsWith('id='));\n if (idParamIndex !== -1) {\n id = parsedParams[idParamIndex].slice(3) || undefined;\n }\n this._curAttrData.extended = this._curAttrData.extended.clone();\n this._curAttrData.extended.urlId = this._oscLinkService.registerLink({ id, uri });\n this._curAttrData.updateExtended();\n return true;\n }\n\n private _finishHyperlink(): boolean {\n this._curAttrData.extended = this._curAttrData.extended.clone();\n this._curAttrData.extended.urlId = 0;\n this._curAttrData.updateExtended();\n return true;\n }\n\n // special colors - OSC 10 | 11 | 12\n private _specialColors = [SpecialColorIndex.FOREGROUND, SpecialColorIndex.BACKGROUND, SpecialColorIndex.CURSOR];\n\n /**\n * Apply colors requests for special colors in OSC 10 | 11 | 12.\n * Since these commands are stacking from multiple parameters,\n * we handle them in a loop with an entry offset to `_specialColors`.\n */\n private _setOrReportSpecialColor(data: string, offset: number): boolean {\n const slots = data.split(';');\n for (let i = 0; i < slots.length; ++i, ++offset) {\n if (offset >= this._specialColors.length) break;\n if (slots[i] === '?') {\n this._onColor.fire([{ type: ColorRequestType.REPORT, index: this._specialColors[offset] }]);\n } else {\n const color = parseColor(slots[i]);\n if (color) {\n this._onColor.fire([{ type: ColorRequestType.SET, index: this._specialColors[offset], color }]);\n }\n }\n }\n return true;\n }\n\n /**\n * OSC 10 ; | ST - set or query default foreground color\n *\n * @vt: #Y OSC 10 \"Set or query default foreground color\" \"OSC 10 ; Pt BEL\" \"Set or query default foreground color.\"\n * To set the color, the following color specification formats are supported:\n * - `rgb://` for `, , ` in `h | hh | hhh | hhhh`, where\n * `h` is a single hexadecimal digit (case insignificant). The different widths scale\n * from 4 bit (`h`) to 16 bit (`hhhh`) and get converted to 8 bit (`hh`).\n * - `#RGB` - 4 bits per channel, expanded to `#R0G0B0`\n * - `#RRGGBB` - 8 bits per channel\n * - `#RRRGGGBBB` - 12 bits per channel, truncated to `#RRGGBB`\n * - `#RRRRGGGGBBBB` - 16 bits per channel, truncated to `#RRGGBB`\n *\n * **Note:** X11 named colors are currently unsupported.\n *\n * If `Pt` contains `?` instead of a color specification, the terminal\n * returns a sequence with the current default foreground color\n * (use that sequence to restore the color after changes).\n *\n * **Note:** Other than xterm, xterm.js does not support OSC 12 - 19.\n * Therefore stacking multiple `Pt` separated by `;` only works for the first two entries.\n */\n public setOrReportFgColor(data: string): boolean {\n return this._setOrReportSpecialColor(data, 0);\n }\n\n /**\n * OSC 11 ; | ST - set or query default background color\n *\n * @vt: #Y OSC 11 \"Set or query default background color\" \"OSC 11 ; Pt BEL\" \"Same as OSC 10, but for default background.\"\n */\n public setOrReportBgColor(data: string): boolean {\n return this._setOrReportSpecialColor(data, 1);\n }\n\n /**\n * OSC 12 ; | ST - set or query default cursor color\n *\n * @vt: #Y OSC 12 \"Set or query default cursor color\" \"OSC 12 ; Pt BEL\" \"Same as OSC 10, but for default cursor color.\"\n */\n public setOrReportCursorColor(data: string): boolean {\n return this._setOrReportSpecialColor(data, 2);\n }\n\n /**\n * OSC 104 ; ST - restore ANSI color \n *\n * @vt: #Y OSC 104 \"Reset ANSI color\" \"OSC 104 ; c BEL\" \"Reset color number `c` to themed color.\"\n * `c` is the color index between 0 and 255. This function restores the default color for `c` as\n * specified by the loaded theme. Any number of `c` parameters may be given.\n * If no parameters are given, the entire indexed color table will be reset.\n */\n public restoreIndexedColor(data: string): boolean {\n if (!data) {\n this._onColor.fire([{ type: ColorRequestType.RESTORE }]);\n return true;\n }\n const event: IColorEvent = [];\n const slots = data.split(';');\n for (let i = 0; i < slots.length; ++i) {\n if (/^\\d+$/.exec(slots[i])) {\n const index = parseInt(slots[i], 10);\n if (isValidColorIndex(index)) {\n event.push({ type: ColorRequestType.RESTORE, index });\n }\n }\n }\n if (event.length) {\n this._onColor.fire(event);\n }\n return true;\n }\n\n /**\n * OSC 110 ST - restore default foreground color\n *\n * @vt: #Y OSC 110 \"Restore default foreground color\" \"OSC 110 BEL\" \"Restore default foreground to themed color.\"\n */\n public restoreFgColor(data: string): boolean {\n this._onColor.fire([{ type: ColorRequestType.RESTORE, index: SpecialColorIndex.FOREGROUND }]);\n return true;\n }\n\n /**\n * OSC 111 ST - restore default background color\n *\n * @vt: #Y OSC 111 \"Restore default background color\" \"OSC 111 BEL\" \"Restore default background to themed color.\"\n */\n public restoreBgColor(data: string): boolean {\n this._onColor.fire([{ type: ColorRequestType.RESTORE, index: SpecialColorIndex.BACKGROUND }]);\n return true;\n }\n\n /**\n * OSC 112 ST - restore default cursor color\n *\n * @vt: #Y OSC 112 \"Restore default cursor color\" \"OSC 112 BEL\" \"Restore default cursor to themed color.\"\n */\n public restoreCursorColor(data: string): boolean {\n this._onColor.fire([{ type: ColorRequestType.RESTORE, index: SpecialColorIndex.CURSOR }]);\n return true;\n }\n\n /**\n * ESC E\n * C1.NEL\n * DEC mnemonic: NEL (https://vt100.net/docs/vt510-rm/NEL)\n * Moves cursor to first position on next line.\n *\n * @vt: #Y C1 NEL \"Next Line\" \"\\x85\" \"Move the cursor to the beginning of the next row.\"\n * @vt: #Y ESC NEL \"Next Line\" \"ESC E\" \"Move the cursor to the beginning of the next row.\"\n */\n public nextLine(): boolean {\n this._activeBuffer.x = 0;\n this.index();\n return true;\n }\n\n /**\n * ESC =\n * DEC mnemonic: DECKPAM (https://vt100.net/docs/vt510-rm/DECKPAM.html)\n * Enables the numeric keypad to send application sequences to the host.\n */\n public keypadApplicationMode(): boolean {\n this._logService.debug('Serial port requested application keypad.');\n this._coreService.decPrivateModes.applicationKeypad = true;\n this._onRequestSyncScrollBar.fire();\n return true;\n }\n\n /**\n * ESC >\n * DEC mnemonic: DECKPNM (https://vt100.net/docs/vt510-rm/DECKPNM.html)\n * Enables the keypad to send numeric characters to the host.\n */\n public keypadNumericMode(): boolean {\n this._logService.debug('Switching back to normal keypad.');\n this._coreService.decPrivateModes.applicationKeypad = false;\n this._onRequestSyncScrollBar.fire();\n return true;\n }\n\n /**\n * ESC % @\n * ESC % G\n * Select default character set. UTF-8 is not supported (string are unicode anyways)\n * therefore ESC % G does the same.\n */\n public selectDefaultCharset(): boolean {\n this._charsetService.setgLevel(0);\n this._charsetService.setgCharset(0, DEFAULT_CHARSET); // US (default)\n return true;\n }\n\n /**\n * ESC ( C\n * Designate G0 Character Set, VT100, ISO 2022.\n * ESC ) C\n * Designate G1 Character Set (ISO 2022, VT100).\n * ESC * C\n * Designate G2 Character Set (ISO 2022, VT220).\n * ESC + C\n * Designate G3 Character Set (ISO 2022, VT220).\n * ESC - C\n * Designate G1 Character Set (VT300).\n * ESC . C\n * Designate G2 Character Set (VT300).\n * ESC / C\n * Designate G3 Character Set (VT300). C = A -> ISO Latin-1 Supplemental. - Supported?\n */\n public selectCharset(collectAndFlag: string): boolean {\n if (collectAndFlag.length !== 2) {\n this.selectDefaultCharset();\n return true;\n }\n if (collectAndFlag[0] === '/') {\n return true; // TODO: Is this supported?\n }\n this._charsetService.setgCharset(GLEVEL[collectAndFlag[0]], CHARSETS[collectAndFlag[1]] ?? DEFAULT_CHARSET);\n return true;\n }\n\n /**\n * ESC D\n * C1.IND\n * DEC mnemonic: IND (https://vt100.net/docs/vt510-rm/IND.html)\n * Moves the cursor down one line in the same column.\n *\n * @vt: #Y C1 IND \"Index\" \"\\x84\" \"Move the cursor one line down scrolling if needed.\"\n * @vt: #Y ESC IND \"Index\" \"ESC D\" \"Move the cursor one line down scrolling if needed.\"\n */\n public index(): boolean {\n this._restrictCursor();\n this._activeBuffer.y++;\n if (this._activeBuffer.y === this._activeBuffer.scrollBottom + 1) {\n this._activeBuffer.y--;\n this._bufferService.scroll(this._eraseAttrData());\n } else if (this._activeBuffer.y >= this._bufferService.rows) {\n this._activeBuffer.y = this._bufferService.rows - 1;\n }\n this._restrictCursor();\n return true;\n }\n\n /**\n * ESC H\n * C1.HTS\n * DEC mnemonic: HTS (https://vt100.net/docs/vt510-rm/HTS.html)\n * Sets a horizontal tab stop at the column position indicated by\n * the value of the active column when the terminal receives an HTS.\n *\n * @vt: #Y C1 HTS \"Horizontal Tabulation Set\" \"\\x88\" \"Places a tab stop at the current cursor position.\"\n * @vt: #Y ESC HTS \"Horizontal Tabulation Set\" \"ESC H\" \"Places a tab stop at the current cursor position.\"\n */\n public tabSet(): boolean {\n this._activeBuffer.tabs[this._activeBuffer.x] = true;\n return true;\n }\n\n /**\n * ESC M\n * C1.RI\n * DEC mnemonic: HTS\n * Moves the cursor up one line in the same column. If the cursor is at the top margin,\n * the page scrolls down.\n *\n * @vt: #Y ESC IR \"Reverse Index\" \"ESC M\" \"Move the cursor one line up scrolling if needed.\"\n */\n public reverseIndex(): boolean {\n this._restrictCursor();\n if (this._activeBuffer.y === this._activeBuffer.scrollTop) {\n // possibly move the code below to term.reverseScroll();\n // test: echo -ne '\\e[1;1H\\e[44m\\eM\\e[0m'\n // blankLine(true) is xterm/linux behavior\n const scrollRegionHeight = this._activeBuffer.scrollBottom - this._activeBuffer.scrollTop;\n this._activeBuffer.lines.shiftElements(this._activeBuffer.ybase + this._activeBuffer.y, scrollRegionHeight, 1);\n this._activeBuffer.lines.set(this._activeBuffer.ybase + this._activeBuffer.y, this._activeBuffer.getBlankLine(this._eraseAttrData()));\n this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);\n } else {\n this._activeBuffer.y--;\n this._restrictCursor(); // quickfix to not run out of bounds\n }\n return true;\n }\n\n /**\n * ESC c\n * DEC mnemonic: RIS (https://vt100.net/docs/vt510-rm/RIS.html)\n * Reset to initial state.\n *\n * @vt: #Y ESC RIS \"Full Reset\" \"ESC c\" \"Reset to initial state.\"\n */\n public fullReset(): boolean {\n this._parser.reset();\n this._onRequestReset.fire();\n return true;\n }\n\n public reset(): void {\n this._curAttrData = DEFAULT_ATTR_DATA.clone();\n this._eraseAttrDataInternal = DEFAULT_ATTR_DATA.clone();\n }\n\n /**\n * back_color_erase feature for xterm.\n */\n private _eraseAttrData(): IAttributeData {\n this._eraseAttrDataInternal.bg &= ~(Attributes.CM_MASK | 0xFFFFFF);\n this._eraseAttrDataInternal.bg |= this._curAttrData.bg & ~0xFC000000;\n return this._eraseAttrDataInternal;\n }\n\n /**\n * ESC n\n * ESC o\n * ESC |\n * ESC }\n * ESC ~\n * DEC mnemonic: LS (https://vt100.net/docs/vt510-rm/LS.html)\n * When you use a locking shift, the character set remains in GL or GR until\n * you use another locking shift. (partly supported)\n */\n public setgLevel(level: number): boolean {\n this._charsetService.setgLevel(level);\n return true;\n }\n\n /**\n * ESC # 8\n * DEC mnemonic: DECALN (https://vt100.net/docs/vt510-rm/DECALN.html)\n * This control function fills the complete screen area with\n * a test pattern (E) used for adjusting screen alignment.\n *\n * @vt: #Y ESC DECALN \"Screen Alignment Pattern\" \"ESC # 8\" \"Fill viewport with a test pattern (E).\"\n */\n public screenAlignmentPattern(): boolean {\n // prepare cell data\n const cell = new CellData();\n cell.content = 1 << Content.WIDTH_SHIFT | 'E'.charCodeAt(0);\n cell.fg = this._curAttrData.fg;\n cell.bg = this._curAttrData.bg;\n\n\n this._setCursor(0, 0);\n for (let yOffset = 0; yOffset < this._bufferService.rows; ++yOffset) {\n const row = this._activeBuffer.ybase + this._activeBuffer.y + yOffset;\n const line = this._activeBuffer.lines.get(row);\n if (line) {\n line.fill(cell);\n line.isWrapped = false;\n }\n }\n this._dirtyRowTracker.markAllDirty();\n this._setCursor(0, 0);\n return true;\n }\n\n\n /**\n * DCS $ q Pt ST\n * DECRQSS (https://vt100.net/docs/vt510-rm/DECRQSS.html)\n * Request Status String (DECRQSS), VT420 and up.\n * Response: DECRPSS (https://vt100.net/docs/vt510-rm/DECRPSS.html)\n *\n * @vt: #P[Limited support, see below.] DCS DECRQSS \"Request Selection or Setting\" \"DCS $ q Pt ST\" \"Request several terminal settings.\"\n * Response is in the form `ESC P 1 $ r Pt ST` for valid requests, where `Pt` contains the\n * corresponding CSI string, `ESC P 0 ST` for invalid requests.\n *\n * Supported requests and responses:\n *\n * | Type | Request | Response (`Pt`) |\n * | -------------------------------- | ----------------- | ----------------------------------------------------- |\n * | Graphic Rendition (SGR) | `DCS $ q m ST` | always reporting `0m` (currently broken) |\n * | Top and Bottom Margins (DECSTBM) | `DCS $ q r ST` | `Ps ; Ps r` |\n * | Cursor Style (DECSCUSR) | `DCS $ q SP q ST` | `Ps SP q` |\n * | Protection Attribute (DECSCA) | `DCS $ q \" q ST` | `Ps \" q` (DECSCA 2 is reported as Ps = 0) |\n * | Conformance Level (DECSCL) | `DCS $ q \" p ST` | always reporting `61 ; 1 \" p` (DECSCL is unsupported) |\n *\n *\n * TODO:\n * - fix SGR report\n * - either check which conformance is better suited or remove the report completely\n * --> we are currently a mixture of all up to VT400 but dont follow anyone strictly\n */\n public requestStatusString(data: string, params: IParams): boolean {\n const f = (s: string): boolean => {\n this._coreService.triggerDataEvent(`${C0.ESC}${s}${C0.ESC}\\\\`);\n return true;\n };\n\n // access helpers\n const b = this._bufferService.buffer;\n const opts = this._optionsService.rawOptions;\n const STYLES: { [key: string]: number } = { 'block': 2, 'underline': 4, 'bar': 6 };\n\n if (data === '\"q') return f(`P1$r${this._curAttrData.isProtected() ? 1 : 0}\"q`);\n if (data === '\"p') return f(`P1$r61;1\"p`);\n if (data === 'r') return f(`P1$r${b.scrollTop + 1};${b.scrollBottom + 1}r`);\n // FIXME: report real SGR settings instead of 0m\n if (data === 'm') return f(`P1$r0m`);\n if (data === ' q') return f(`P1$r${STYLES[opts.cursorStyle] - (opts.cursorBlink ? 1 : 0)} q`);\n return f(`P0$r`);\n }\n\n public markRangeDirty(y1: number, y2: number): void {\n this._dirtyRowTracker.markRangeDirty(y1, y2);\n }\n\n // #region Kitty keyboard\n\n /**\n * CSI = flags ; mode u\n * Set Kitty keyboard protocol flags.\n * mode: 1=set, 2=set-only-specified, 3=reset-only-specified\n *\n * @vt: #Y CSI KKBDSET \"Kitty Keyboard Set\" \"CSI = Ps ; Pm u\" \"Set Kitty keyboard protocol flags.\"\n */\n public kittyKeyboardSet(params: IParams): boolean {\n if (!this._optionsService.rawOptions.vtExtensions?.kittyKeyboard) {\n return true;\n }\n const flags = params.params[0] || 0;\n const mode = params.length > 1 ? (params.params[1] || 1) : 1;\n const state = this._coreService.kittyKeyboard;\n\n switch (mode) {\n case 1: // Set all flags\n state.flags = flags;\n break;\n case 2: // Set only specified flags (OR)\n state.flags |= flags;\n break;\n case 3: // Reset only specified flags (AND NOT)\n state.flags &= ~flags;\n break;\n }\n return true;\n }\n\n /**\n * CSI ? u\n * Query Kitty keyboard protocol flags.\n * Terminal responds with CSI ? flags u\n *\n * @vt: #Y CSI KKBDQUERY \"Kitty Keyboard Query\" \"CSI ? u\" \"Query Kitty keyboard protocol flags.\"\n */\n public kittyKeyboardQuery(params: IParams): boolean {\n if (!this._optionsService.rawOptions.vtExtensions?.kittyKeyboard) {\n return true;\n }\n const flags = this._coreService.kittyKeyboard.flags;\n this._coreService.triggerDataEvent(`${C0.ESC}[?${flags}u`);\n return true;\n }\n\n /**\n * CSI > flags u\n * Push Kitty keyboard flags onto stack and set new flags.\n *\n * @vt: #Y CSI KKBDPUSH \"Kitty Keyboard Push\" \"CSI > Ps u\" \"Push keyboard flags to stack and set new flags.\"\n */\n public kittyKeyboardPush(params: IParams): boolean {\n if (!this._optionsService.rawOptions.vtExtensions?.kittyKeyboard) {\n return true;\n }\n const flags = params.params[0] || 0;\n const state = this._coreService.kittyKeyboard;\n const isAlt = this._bufferService.buffer === this._bufferService.buffers.alt;\n const stack = isAlt ? state.altStack : state.mainStack;\n\n // Evict oldest entry if stack is full (DoS protection, limit of 16)\n if (stack.length >= 16) {\n stack.shift();\n }\n\n // Push current flags onto stack and set new flags\n stack.push(state.flags);\n state.flags = flags;\n return true;\n }\n\n /**\n * CSI < count u\n * Pop Kitty keyboard flags from stack.\n *\n * @vt: #Y CSI KKBDPOP \"Kitty Keyboard Pop\" \"CSI < Ps u\" \"Pop keyboard flags from stack.\"\n */\n public kittyKeyboardPop(params: IParams): boolean {\n if (!this._optionsService.rawOptions.vtExtensions?.kittyKeyboard) {\n return true;\n }\n const count = Math.max(1, params.params[0] || 1);\n const state = this._coreService.kittyKeyboard;\n const isAlt = this._bufferService.buffer === this._bufferService.buffers.alt;\n const stack = isAlt ? state.altStack : state.mainStack;\n\n // Pop specified number of entries from stack\n for (let i = 0; i < count && stack.length > 0; i++) {\n state.flags = stack.pop()!;\n }\n // If stack is empty after popping, reset to 0\n if (stack.length === 0 && count > 0) {\n state.flags = 0;\n }\n return true;\n }\n\n // #endregion\n}\n\nexport interface IDirtyRowTracker {\n readonly start: number;\n readonly end: number;\n\n clearRange(): void;\n markDirty(y: number): void;\n markRangeDirty(y1: number, y2: number): void;\n markAllDirty(): void;\n}\n\nclass DirtyRowTracker implements IDirtyRowTracker {\n public start!: number;\n public end!: number;\n\n constructor(\n @IBufferService private readonly _bufferService: IBufferService\n ) {\n this.clearRange();\n }\n\n public clearRange(): void {\n this.start = this._bufferService.buffer.y;\n this.end = this._bufferService.buffer.y;\n }\n\n public markDirty(y: number): void {\n if (y < this.start) {\n this.start = y;\n } else if (y > this.end) {\n this.end = y;\n }\n }\n\n public markRangeDirty(y1: number, y2: number): void {\n if (y1 > y2) {\n $temp = y1;\n y1 = y2;\n y2 = $temp;\n }\n if (y1 < this.start) {\n this.start = y1;\n }\n if (y2 > this.end) {\n this.end = y2;\n }\n }\n\n public markAllDirty(): void {\n this.markRangeDirty(0, this._bufferService.rows - 1);\n }\n}\n\nexport function isValidColorIndex(value: number): value is ColorIndex {\n return 0 <= value && value < 256;\n}\n", "/**\n * Copyright (c) 2026 The xterm.js authors. All rights reserved.\n * @license MIT\n *\n * Minimal async helpers for xterm.js core.\n */\n\nimport { DisposableStore, IDisposable, toDisposable } from './Lifecycle';\n\nexport function timeout(millis: number): Promise {\n return new Promise(resolve => setTimeout(resolve, millis));\n}\n\n/**\n * Creates a timeout that can be disposed using its returned value.\n * @param handler The timeout handler.\n * @param timeout An optional timeout in milliseconds.\n * @param store An optional {@link DisposableStore} that will have the timeout disposable managed\n * automatically.\n */\nexport function disposableTimeout(handler: () => void, timeout = 0, store?: DisposableStore): IDisposable {\n const timer = setTimeout(() => {\n handler();\n if (store) {\n disposable.dispose();\n }\n }, timeout);\n const disposable = toDisposable(() => {\n clearTimeout(timer);\n });\n store?.add(disposable);\n return disposable;\n}\n\nexport class TimeoutTimer implements IDisposable {\n private _token: any = -1;\n private _isDisposed = false;\n\n public dispose(): void {\n this.cancel();\n this._isDisposed = true;\n }\n\n public cancel(): void {\n if (this._token !== -1) {\n clearTimeout(this._token);\n this._token = -1;\n }\n }\n\n public cancelAndSet(runner: () => void, timeout: number): void {\n if (this._isDisposed) {\n throw new Error('Calling cancelAndSet on a disposed TimeoutTimer');\n }\n this.cancel();\n this._token = setTimeout(() => {\n this._token = -1;\n runner();\n }, timeout);\n }\n\n public setIfNotSet(runner: () => void, timeout: number): void {\n if (this._isDisposed) {\n throw new Error('Calling setIfNotSet on a disposed TimeoutTimer');\n }\n if (this._token !== -1) {\n return;\n }\n this._token = setTimeout(() => {\n this._token = -1;\n runner();\n }, timeout);\n }\n}\n\n/**\n * Schedules a single runner on the microtask queue. Unlike {@link TimeoutTimer}, a scheduled\n * microtask cannot be unqueued; {@link cancel} prevents the runner from executing if it has not\n * run yet.\n */\nexport class MicrotaskTimer implements IDisposable {\n private _isScheduled = false;\n private _isDisposed = false;\n\n public dispose(): void {\n this.cancel();\n this._isDisposed = true;\n }\n\n public cancel(): void {\n this._isScheduled = false;\n }\n\n public set(runner: () => void): void {\n if (this._isDisposed) {\n throw new Error('Calling set on a disposed MicrotaskTimer');\n }\n if (this._isScheduled) {\n return;\n }\n this._isScheduled = true;\n queueMicrotask(() => {\n if (!this._isScheduled) {\n return;\n }\n this._isScheduled = false;\n runner();\n });\n }\n}\n\nexport class IntervalTimer implements IDisposable {\n private _disposable: IDisposable | undefined;\n private _isDisposed = false;\n\n public cancel(): void {\n this._disposable?.dispose();\n this._disposable = undefined;\n }\n\n public cancelAndSet(runner: () => void, interval: number, context: Window | typeof globalThis = globalThis): void {\n if (this._isDisposed) {\n throw new Error('Calling cancelAndSet on a disposed IntervalTimer');\n }\n this.cancel();\n const handle = context.setInterval(() => {\n runner();\n }, interval);\n this._disposable = {\n dispose: () => {\n context.clearInterval(handle as any);\n this._disposable = undefined;\n }\n };\n }\n\n public dispose(): void {\n this.cancel();\n this._isDisposed = true;\n }\n}\n", "\n/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { TimeoutTimer } from '../Async';\nimport { Disposable, toDisposable } from '../Lifecycle';\nimport { Emitter } from '../Event';\n\nconst enum Constants {\n /**\n * Safety watermark to avoid memory exhaustion and browser engine crash on fast data input.\n * Enable flow control to avoid this limit and make sure that your backend correctly\n * propagates this to the underlying pty. (see docs for further instructions)\n * Since this limit is meant as a safety parachute to prevent browser crashs,\n * it is set to a very high number. Typically xterm.js gets unresponsive with\n * a 100 times lower number (>500 kB).\n */\n DISCARD_WATERMARK = 50000000, // ~50 MB\n /**\n * The max number of ms to spend on writes before allowing the renderer to\n * catch up with a 0ms setTimeout. A value of < 33 to keep us close to\n * 30fps, and a value of < 16 to try to run at 60fps. Of course, the real FPS\n * depends on the time it takes for the renderer to draw the frame.\n */\n WRITE_TIMEOUT_MS = 12,\n /**\n * Threshold of max held chunks in the write buffer, that were already processed.\n * This is a tradeoff between extensive write buffer shifts (bad runtime) and high\n * memory consumption by data thats not used anymore.\n */\n WRITE_BUFFER_LENGTH_THRESHOLD = 50\n}\n\nexport class WriteBuffer extends Disposable {\n private _writeBuffer: (string | Uint8Array)[] = [];\n private _callbacks: ((() => void) | undefined)[] = [];\n private _pendingData = 0;\n private _bufferOffset = 0;\n private _isSyncWriting = false;\n private _syncCalls = 0;\n private _didUserInput = false;\n\n private readonly _innerWriteTimer = this._register(new TimeoutTimer());\n private readonly _onWriteParsed = this._register(new Emitter());\n public readonly onWriteParsed = this._onWriteParsed.event;\n\n constructor(private _action: (data: string | Uint8Array, promiseResult?: boolean) => void | Promise) {\n super();\n this._register(toDisposable(() => {\n this._writeBuffer.length = 0;\n this._callbacks.length = 0;\n this._pendingData = 0;\n this._bufferOffset = 0;\n }));\n }\n\n public handleUserInput(): void {\n this._didUserInput = true;\n }\n\n /**\n * Flushes all pending writes synchronously. This is useful when you need to\n * ensure all queued data is processed before performing an operation that\n * depends upon everything being parsed like resize.\n *\n * Note: This is unreliable with async parser handlers as it does not wait for\n * promises to resolve.\n */\n public flushSync(): void {\n if (this._store.isDisposed) {\n return;\n }\n // exit early if another sync write loop is active\n if (this._isSyncWriting) {\n return;\n }\n this._isSyncWriting = true;\n\n // Process all pending chunks synchronously\n let chunk: string | Uint8Array | undefined;\n let didProcess = false;\n while (chunk = this._writeBuffer.shift()) {\n didProcess = true;\n this._action(chunk);\n const cb = this._callbacks.shift();\n if (cb) cb();\n }\n\n // Reset buffer state\n this._pendingData = 0;\n this._bufferOffset = 0x7FFFFFFF;\n this._writeBuffer.length = 0;\n this._callbacks.length = 0;\n\n this._isSyncWriting = false;\n if (didProcess) {\n this._onWriteParsed.fire();\n }\n }\n\n /**\n * @deprecated Unreliable, to be removed soon.\n */\n public writeSync(data: string | Uint8Array, maxSubsequentCalls?: number): void {\n if (this._store.isDisposed) {\n return;\n }\n // stop writeSync recursions with maxSubsequentCalls argument\n // This is dangerous to use as it will lose the current data chunk\n // and return immediately.\n if (maxSubsequentCalls !== undefined && this._syncCalls > maxSubsequentCalls) {\n // comment next line if a whole loop block should only contain x `writeSync` calls\n // (total flat vs. deep nested limit)\n this._syncCalls = 0;\n return;\n }\n // append chunk to buffer\n this._pendingData += data.length;\n this._writeBuffer.push(data);\n this._callbacks.push(undefined);\n\n // increase recursion counter\n this._syncCalls++;\n // exit early if another writeSync loop is active\n if (this._isSyncWriting) {\n return;\n }\n this._isSyncWriting = true;\n\n // force sync processing on pending data chunks to avoid in-band data scrambling\n // does the same as innerWrite but without event loop\n // we have to do it here as single loop steps to not corrupt loop subject\n // by another writeSync call triggered from _action\n let chunk: string | Uint8Array | undefined;\n while (chunk = this._writeBuffer.shift()) {\n this._action(chunk);\n const cb = this._callbacks.shift();\n if (cb) cb();\n }\n // reset to avoid reprocessing of chunks with scheduled innerWrite call\n // stopping scheduled innerWrite by offset > length condition\n this._pendingData = 0;\n this._bufferOffset = 0x7FFFFFFF;\n\n // allow another writeSync to loop\n this._isSyncWriting = false;\n this._syncCalls = 0;\n }\n\n public write(data: string | Uint8Array, callback?: () => void): void {\n if (this._store.isDisposed) {\n return;\n }\n if (this._pendingData > Constants.DISCARD_WATERMARK) {\n throw new Error('write data discarded, use flow control to avoid losing data');\n }\n\n // schedule chunk processing for next event loop run\n if (!this._writeBuffer.length) {\n this._bufferOffset = 0;\n\n // If this is the first write call after the user has done some input,\n // parse it immediately to minimize input latency,\n // otherwise schedule for the next event\n if (this._didUserInput) {\n this._didUserInput = false;\n this._pendingData += data.length;\n this._writeBuffer.push(data);\n this._callbacks.push(callback);\n this._innerWrite();\n return;\n }\n\n this._scheduleInnerWrite();\n }\n\n this._pendingData += data.length;\n this._writeBuffer.push(data);\n this._callbacks.push(callback);\n }\n\n /**\n * Inner write call, that enters the sliced chunk processing by timing.\n *\n * `lastTime` indicates, when the last _innerWrite call had started.\n * It is used to aggregate async handler execution under a timeout constraint\n * effectively lowering the redrawing needs, schematically:\n *\n * macroTask _innerWrite:\n * if (performance.now() - (lastTime | 0) < Constants.WRITE_TIMEOUT_MS):\n * schedule microTask _innerWrite(lastTime)\n * else:\n * schedule macroTask _innerWrite(0)\n *\n * overall execution order on task queues:\n *\n * macrotasks: [...] --> _innerWrite(0) --> [...] --> screenUpdate --> [...]\n * m t: |\n * i a: [...]\n * c s: |\n * r k: while < timeout:\n * o s: _innerWrite(timeout)\n *\n * `promiseResult` depicts the promise resolve value of an async handler.\n * This value gets carried forward through all saved stack states of the\n * paused parser for proper continuation.\n *\n * Note, for pure sync code `lastTime` and `promiseResult` have no meaning.\n */\n private _scheduleInnerWrite(lastTime: number = 0, promiseResult: boolean = true): void {\n if (this._store.isDisposed) {\n return;\n }\n this._innerWriteTimer.cancelAndSet(() => this._innerWrite(lastTime, promiseResult), 0);\n }\n\n protected _innerWrite(lastTime: number = 0, promiseResult: boolean = true): void {\n if (this._store.isDisposed) {\n return;\n }\n const startTime = lastTime || performance.now();\n while (this._writeBuffer.length > this._bufferOffset) {\n const data = this._writeBuffer[this._bufferOffset];\n const result = this._action(data, promiseResult);\n if (result) {\n /**\n * If we get a promise as return value, we re-schedule the continuation\n * as thenable on the promise and exit right away.\n *\n * The exit here means, that we block input processing at the current active chunk,\n * the exact execution position within the chunk is preserved by the saved\n * stack content in InputHandler and EscapeSequenceParser.\n *\n * Resuming happens automatically from that saved stack state.\n * Also the resolved promise value is passed along the callstack to\n * `EscapeSequenceParser.parse` to correctly resume the stopped handler loop.\n *\n * Exceptions on async handlers will be logged to console async, but do not interrupt\n * the input processing (continues with next handler at the current input position).\n */\n\n /**\n * If a promise takes long to resolve, we should schedule continuation behind setTimeout.\n * This might already be too late, if our .then enters really late (executor + prev thens\n * took very long). This cannot be solved here for the handler itself (it is the handlers\n * responsibility to slice hard work), but we can at least schedule a screen update as we\n * gain control.\n */\n const continuation: (r: boolean) => void = (r: boolean) => {\n if (this._store.isDisposed) {\n return;\n }\n if (performance.now() - startTime >= Constants.WRITE_TIMEOUT_MS) {\n this._scheduleInnerWrite(0, r);\n } else {\n this._innerWrite(startTime, r);\n }\n };\n\n /**\n * Optimization considerations:\n * The continuation above favors FPS over throughput by eval'ing `startTime` on resolve.\n * This might schedule too many screen updates with bad throughput drops (in case a slow\n * resolving handler sliced its work properly behind setTimeout calls). We cannot spot\n * this condition here, also the renderer has no way to spot nonsense updates either.\n * FIXME: A proper fix for this would track the FPS at the renderer entry level separately.\n *\n * If favoring of FPS shows bad throughput impact, use the following instead. It favors\n * throughput by eval'ing `startTime` upfront pulling at least one more chunk into the\n * current microtask queue (executed before setTimeout).\n */\n // const continuation: (r: boolean) => void = performance.now() - startTime >=\n // Constants.WRITE_TIMEOUT_MS\n // ? r => setTimeout(() => this._innerWrite(0, r))\n // : r => this._innerWrite(startTime, r);\n\n // Handle exceptions synchronously to current band position, idea:\n // 1. spawn a single microtask which we allow to throw hard\n // 2. spawn a promise immediately resolving to `true`\n // (executed on the same queue, thus properly aligned before continuation happens)\n result.catch(err => {\n queueMicrotask(() => {throw err;});\n return Promise.resolve(false);\n }).then(continuation);\n return;\n }\n\n const cb = this._callbacks[this._bufferOffset];\n if (cb) cb();\n this._bufferOffset++;\n this._pendingData -= data.length;\n\n if (performance.now() - startTime >= Constants.WRITE_TIMEOUT_MS) {\n break;\n }\n }\n if (this._writeBuffer.length > this._bufferOffset) {\n // Allow renderer to catch up before processing the next batch\n // trim already processed chunks if we are above threshold\n if (this._bufferOffset > Constants.WRITE_BUFFER_LENGTH_THRESHOLD) {\n this._writeBuffer = this._writeBuffer.slice(this._bufferOffset);\n this._callbacks = this._callbacks.slice(this._bufferOffset);\n this._bufferOffset = 0;\n }\n this._scheduleInnerWrite();\n } else {\n this._writeBuffer.length = 0;\n this._callbacks.length = 0;\n this._pendingData = 0;\n this._bufferOffset = 0;\n }\n this._onWriteParsed.fire();\n }\n}\n", "/**\n * Copyright (c) 2022 The xterm.js authors. All rights reserved.\n * @license MIT\n */\nimport { IBufferService, IOscLinkService } from './Services';\nimport { IOscLinkData } from '../Types';\nimport { IMarker } from '../buffer/Types';\n\nexport class OscLinkService implements IOscLinkService {\n public serviceBrand: any;\n\n private _nextId = 1;\n\n /**\n * A map of the link key to link entry. This is used to add additional lines to links with ids.\n */\n private _entriesWithId: Map = new Map();\n\n /**\n * A map of the link id to the link entry. The \"link id\" (number) which is the numberic\n * representation of a unique link should not be confused with \"id\" (string) which comes in with\n * `id=` in the OSC link's properties.\n */\n private _dataByLinkId: Map = new Map();\n\n constructor(\n @IBufferService private readonly _bufferService: IBufferService\n ) {\n }\n\n public registerLink(data: IOscLinkData): number {\n const buffer = this._bufferService.buffer;\n\n // Links with no id will only ever be registered a single time\n if (data.id === undefined) {\n const marker = buffer.addMarker(buffer.ybase + buffer.y);\n const entry: IOscLinkEntryNoId = {\n data,\n id: this._nextId++,\n lines: [marker]\n };\n marker.onDispose(() => this._removeMarkerFromLink(entry, marker));\n this._dataByLinkId.set(entry.id, entry);\n return entry.id;\n }\n\n // Add the line to the link if it already exists\n const castData = data as Required;\n const key = this._getEntryIdKey(castData);\n const match = this._entriesWithId.get(key);\n if (match) {\n this.addLineToLink(match.id, buffer.ybase + buffer.y);\n return match.id;\n }\n\n // Create the link\n const marker = buffer.addMarker(buffer.ybase + buffer.y);\n const entry: IOscLinkEntryWithId = {\n id: this._nextId++,\n key: this._getEntryIdKey(castData),\n data: castData,\n lines: [marker]\n };\n marker.onDispose(() => this._removeMarkerFromLink(entry, marker));\n this._entriesWithId.set(entry.key, entry);\n this._dataByLinkId.set(entry.id, entry);\n return entry.id;\n }\n\n public addLineToLink(linkId: number, y: number): void {\n const entry = this._dataByLinkId.get(linkId);\n if (!entry) {\n return;\n }\n if (entry.lines.every(e => e.line !== y)) {\n const marker = this._bufferService.buffer.addMarker(y);\n entry.lines.push(marker);\n marker.onDispose(() => this._removeMarkerFromLink(entry, marker));\n }\n }\n\n public getLinkData(linkId: number): IOscLinkData | undefined {\n return this._dataByLinkId.get(linkId)?.data;\n }\n\n private _getEntryIdKey(linkData: Required): string {\n return `${linkData.id};;${linkData.uri}`;\n }\n\n private _removeMarkerFromLink(entry: IOscLinkEntryNoId | IOscLinkEntryWithId, marker: IMarker): void {\n const index = entry.lines.indexOf(marker);\n if (index === -1) {\n return;\n }\n entry.lines.splice(index, 1);\n if (entry.lines.length === 0) {\n if (entry.data.id !== undefined) {\n this._entriesWithId.delete((entry as IOscLinkEntryWithId).key);\n }\n this._dataByLinkId.delete(entry.id);\n }\n }\n}\n\ninterface IOscLinkEntry {\n data: T;\n id: number;\n lines: IMarker[];\n}\n\ninterface IOscLinkEntryNoId extends IOscLinkEntry {\n}\n\ninterface IOscLinkEntryWithId extends IOscLinkEntry> {\n key: string;\n}\n", "/**\n * Copyright (c) 2014-2020 The xterm.js authors. All rights reserved.\n * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)\n * @license MIT\n *\n * Originally forked from (with the author's permission):\n * Fabrice Bellard's javascript vt100 for jslinux:\n * http://bellard.org/jslinux/\n * Copyright (c) 2011 Fabrice Bellard\n * The original design remains. The terminal itself\n * has been extended to include xterm CSI codes, among\n * other features.\n *\n * Terminal Emulation References:\n * http://vt100.net/\n * http://invisible-island.net/xterm/ctlseqs/ctlseqs.txt\n * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html\n * http://invisible-island.net/vttest/\n * http://www.inwap.com/pdp10/ansicode.txt\n * http://linux.die.net/man/4/console_codes\n * http://linux.die.net/man/7/urxvt\n */\n\nimport { IInstantiationService, IOptionsService, IBufferService, ILogService, ICharsetService, ICoreService, IMouseStateService, IUnicodeService, LogLevelEnum, IOscLinkService } from './services/Services';\nimport { InstantiationService } from './services/InstantiationService';\nimport { LogService } from './services/LogService';\nimport { BufferService, BufferServiceConstants } from './services/BufferService';\nimport { OptionsService } from './services/OptionsService';\nimport { IDisposable, IScrollEvent, ITerminalOptions, IParams } from './Types';\nimport { IAttributeData, IBufferSet } from './buffer/Types';\nimport { CoreService } from './services/CoreService';\nimport { MouseStateService } from './services/MouseStateService';\nimport { UnicodeV6 } from './input/UnicodeV6';\nimport { UnicodeService } from './services/UnicodeService';\nimport { CharsetService } from './services/CharsetService';\nimport { updateWindowsModeWrappedState } from './WindowsMode';\nimport { IFunctionIdentifier } from './parser/Types';\nimport { InputHandler } from './InputHandler';\nimport { WriteBuffer } from './input/WriteBuffer';\nimport { OscLinkService } from './services/OscLinkService';\nimport { Emitter, EventUtils, type IEvent } from './Event';\nimport { Disposable, MutableDisposable, toDisposable } from './Lifecycle';\n\n// Only trigger this warning a single time per session\nlet hasWriteSyncWarnHappened = false;\n\nexport interface ICoreTerminal {\n mouseStateService: IMouseStateService;\n coreService: ICoreService;\n optionsService: IOptionsService;\n unicodeService: IUnicodeService;\n buffers: IBufferSet;\n options: Required;\n registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise): IDisposable;\n registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise): IDisposable;\n registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise): IDisposable;\n registerOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable;\n registerApcHandler(id: IFunctionIdentifier, callback: (data: string) => boolean | Promise): IDisposable;\n}\n\nexport abstract class CoreTerminal extends Disposable implements ICoreTerminal {\n protected readonly _instantiationService: IInstantiationService;\n protected readonly _bufferService: IBufferService;\n protected readonly _logService: ILogService;\n protected readonly _charsetService: ICharsetService;\n protected readonly _oscLinkService: IOscLinkService;\n\n public readonly mouseStateService: IMouseStateService;\n public readonly coreService: ICoreService;\n public readonly unicodeService: IUnicodeService;\n public readonly optionsService: IOptionsService;\n\n protected _inputHandler: InputHandler;\n private _writeBuffer: WriteBuffer;\n private _windowsWrappingHeuristics = this._register(new MutableDisposable());\n\n private readonly _onBinary = this._register(new Emitter());\n public readonly onBinary = this._onBinary.event;\n private readonly _onData = this._register(new Emitter());\n public readonly onData = this._onData.event;\n protected _onLineFeed = this._register(new Emitter());\n public readonly onLineFeed = this._onLineFeed.event;\n protected readonly _onRender = this._register(new Emitter<{ start: number, end: number }>());\n public readonly onRender = this._onRender.event;\n private readonly _onResize = this._register(new Emitter<{ cols: number, rows: number }>());\n public readonly onResize = this._onResize.event;\n protected readonly _onWriteParsed = this._register(new Emitter());\n public readonly onWriteParsed = this._onWriteParsed.event;\n\n /**\n * Internally we track the source of the scroll but this is meaningless outside the library so\n * it's filtered out.\n */\n protected _onScrollApi?: Emitter;\n protected _onScroll = this._register(new Emitter());\n public get onScroll(): IEvent {\n if (!this._onScrollApi) {\n this._onScrollApi = this._register(new Emitter());\n this._onScroll.event(ev => {\n this._onScrollApi?.fire(ev.position);\n });\n }\n return this._onScrollApi.event;\n }\n\n public get cols(): number { return this._bufferService.cols; }\n public get rows(): number { return this._bufferService.rows; }\n public get buffers(): IBufferSet { return this._bufferService.buffers; }\n public get options(): Required { return this.optionsService.options; }\n public set options(options: ITerminalOptions) {\n for (const key in options) {\n this.optionsService.options[key] = options[key];\n }\n }\n\n constructor(\n options: Partial\n ) {\n super();\n\n // Setup and initialize services\n this._instantiationService = new InstantiationService();\n this.optionsService = this._register(new OptionsService(options));\n this._instantiationService.setService(IOptionsService, this.optionsService);\n this._logService = this._register(this._instantiationService.createInstance(LogService));\n this._instantiationService.setService(ILogService, this._logService);\n this._bufferService = this._register(this._instantiationService.createInstance(BufferService));\n this._instantiationService.setService(IBufferService, this._bufferService);\n this.coreService = this._register(this._instantiationService.createInstance(CoreService));\n this._instantiationService.setService(ICoreService, this.coreService);\n this.mouseStateService = this._register(this._instantiationService.createInstance(MouseStateService));\n this._instantiationService.setService(IMouseStateService, this.mouseStateService);\n this.unicodeService = this._register(this._instantiationService.createInstance(UnicodeService));\n this.unicodeService.register(new UnicodeV6());\n this._instantiationService.setService(IUnicodeService, this.unicodeService);\n this._charsetService = this._instantiationService.createInstance(CharsetService);\n this._instantiationService.setService(ICharsetService, this._charsetService);\n this._oscLinkService = this._instantiationService.createInstance(OscLinkService);\n this._instantiationService.setService(IOscLinkService, this._oscLinkService);\n\n\n // Register input handler and handle/forward events\n this._inputHandler = this._register(new InputHandler(this._bufferService, this._charsetService, this.coreService, this._logService, this.optionsService, this._oscLinkService, this.mouseStateService, this.unicodeService));\n this._register(EventUtils.forward(this._inputHandler.onLineFeed, this._onLineFeed));\n\n // Setup listeners\n this._register(EventUtils.forward(this._bufferService.onResize, this._onResize));\n this._register(EventUtils.forward(this.coreService.onData, this._onData));\n this._register(EventUtils.forward(this.coreService.onBinary, this._onBinary));\n this._register(this.coreService.onRequestScrollToBottom(() => this.scrollToBottom(true)));\n this._register(this.coreService.onUserInput(() => this._writeBuffer.handleUserInput()));\n this._register(this.optionsService.onMultipleOptionChange(['windowsPty'], () => this._handleWindowsPtyOptionChange()));\n this._register(this._bufferService.onScroll(() => {\n this._onScroll.fire({ position: this._bufferService.buffer.ydisp });\n this._inputHandler.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom);\n }));\n // Setup WriteBuffer\n this._writeBuffer = this._register(new WriteBuffer((data, promiseResult) => this._inputHandler.parse(data, promiseResult)));\n this._register(EventUtils.forward(this._writeBuffer.onWriteParsed, this._onWriteParsed));\n }\n\n public write(data: string | Uint8Array, callback?: () => void): void {\n this._writeBuffer.write(data, callback);\n }\n\n /**\n * Write data to terminal synchonously.\n *\n * This method is unreliable with async parser handlers, thus should not\n * be used anymore. If you need blocking semantics on data input consider\n * `write` with a callback instead.\n *\n * @deprecated Unreliable, will be removed soon.\n */\n public writeSync(data: string | Uint8Array, maxSubsequentCalls?: number): void {\n if (this._logService.logLevel <= LogLevelEnum.WARN && !hasWriteSyncWarnHappened) {\n this._logService.warn('writeSync is unreliable and will be removed soon.');\n hasWriteSyncWarnHappened = true;\n }\n this._writeBuffer.writeSync(data, maxSubsequentCalls);\n }\n\n public input(data: string, wasUserInput: boolean = true): void {\n this.coreService.triggerDataEvent(data, wasUserInput);\n }\n\n public resize(x: number, y: number): void {\n if (isNaN(x) || isNaN(y)) {\n return;\n }\n\n x = Math.max(x, BufferServiceConstants.MINIMUM_COLS);\n y = Math.max(y, BufferServiceConstants.MINIMUM_ROWS);\n\n // Flush pending writes before resize to avoid race conditions where async\n // writes are processed with incorrect dimensions\n this._writeBuffer.flushSync();\n\n this._bufferService.resize(x, y);\n }\n\n /**\n * Scroll the terminal down 1 row, creating a blank line.\n * @param eraseAttr The attribute data to use the for blank line.\n * @param isWrapped Whether the new line is wrapped from the previous line.\n */\n public scroll(eraseAttr: IAttributeData, isWrapped: boolean = false): void {\n this._bufferService.scroll(eraseAttr, isWrapped);\n }\n\n /**\n * Scroll the display of the terminal\n * @param disp The number of lines to scroll down (negative scroll up).\n * @param suppressScrollEvent Don't emit the scroll event as scrollLines. This is used to avoid\n * unwanted events being handled by the viewport when the event was triggered from the viewport\n * originally.\n */\n public scrollLines(disp: number, suppressScrollEvent?: boolean): void {\n this._bufferService.scrollLines(disp, suppressScrollEvent);\n }\n\n public scrollPages(pageCount: number): void {\n this.scrollLines(pageCount * (this.rows - 1));\n }\n\n public scrollToTop(): void {\n this.scrollLines(-this._bufferService.buffer.ydisp);\n }\n\n public scrollToBottom(disableSmoothScroll?: boolean): void {\n this.scrollLines(this._bufferService.buffer.ybase - this._bufferService.buffer.ydisp);\n }\n\n public scrollToLine(line: number): void {\n const scrollAmount = line - this._bufferService.buffer.ydisp;\n if (scrollAmount !== 0) {\n this.scrollLines(scrollAmount);\n }\n }\n\n /** Add handler for ESC escape sequence. See xterm.d.ts for details. */\n public registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise): IDisposable {\n return this._inputHandler.registerEscHandler(id, callback);\n }\n\n /** Add handler for DCS escape sequence. See xterm.d.ts for details. */\n public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise): IDisposable {\n return this._inputHandler.registerDcsHandler(id, callback);\n }\n\n /** Add handler for CSI escape sequence. See xterm.d.ts for details. */\n public registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise): IDisposable {\n return this._inputHandler.registerCsiHandler(id, callback);\n }\n\n /** Add handler for OSC escape sequence. See xterm.d.ts for details. */\n public registerOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable {\n return this._inputHandler.registerOscHandler(ident, callback);\n }\n\n /** Add handler for APC escape sequence. See xterm.d.ts for details. */\n public registerApcHandler(id: IFunctionIdentifier, callback: (data: string) => boolean | Promise): IDisposable {\n return this._inputHandler.registerApcHandler(id, callback);\n }\n\n protected _setup(): void {\n this._handleWindowsPtyOptionChange();\n }\n\n public reset(): void {\n this._inputHandler.reset();\n this._bufferService.reset();\n this._charsetService.reset();\n this.coreService.reset();\n this.mouseStateService.reset();\n }\n\n\n private _handleWindowsPtyOptionChange(): void {\n let value = false;\n const windowsPty = this.optionsService.rawOptions.windowsPty;\n if (windowsPty && windowsPty.backend !== undefined && windowsPty.buildNumber !== undefined) {\n value = !!(windowsPty.backend === 'conpty' && windowsPty.buildNumber < 21376);\n }\n if (value) {\n this._enableWindowsWrappingHeuristics();\n } else {\n this._windowsWrappingHeuristics.clear();\n }\n }\n\n protected _enableWindowsWrappingHeuristics(): void {\n if (!this._windowsWrappingHeuristics.value) {\n const disposables: IDisposable[] = [];\n disposables.push(this.onLineFeed(updateWindowsModeWrappedState.bind(null, this._bufferService)));\n disposables.push(this.registerCsiHandler({ final: 'H' }, () => {\n updateWindowsModeWrappedState(this._bufferService);\n return false;\n }));\n this._windowsWrappingHeuristics.value = toDisposable(() => {\n for (const d of disposables) {\n d.dispose();\n }\n });\n }\n }\n}\n", "/**\n * Copyright (c) 2014 The xterm.js authors. All rights reserved.\n * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)\n * @license MIT\n *\n * Originally forked from (with the author's permission):\n * Fabrice Bellard's javascript vt100 for jslinux:\n * http://bellard.org/jslinux/\n * Copyright (c) 2011 Fabrice Bellard\n * The original design remains. The terminal itself\n * has been extended to include xterm CSI codes, among\n * other features.\n *\n * Terminal Emulation References:\n * http://vt100.net/\n * http://invisible-island.net/xterm/ctlseqs/ctlseqs.txt\n * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html\n * http://invisible-island.net/vttest/\n * http://www.inwap.com/pdp10/ansicode.txt\n * http://linux.die.net/man/4/console_codes\n * http://linux.die.net/man/7/urxvt\n */\n\nimport { DEFAULT_ATTR_DATA } from '../common/buffer/BufferLine';\nimport { IBuffer, IMarker } from '../common/buffer/Types';\nimport { CoreTerminal } from '../common/CoreTerminal';\nimport { ITerminalOptions } from '../common/Types';\nimport { Emitter, EventUtils } from '../common/Event';\n\nexport class Terminal extends CoreTerminal {\n private readonly _onBell = this._register(new Emitter());\n public readonly onBell = this._onBell.event;\n private readonly _onCursorMove = this._register(new Emitter());\n public readonly onCursorMove = this._onCursorMove.event;\n private readonly _onTitleChange = this._register(new Emitter());\n public readonly onTitleChange = this._onTitleChange.event;\n private readonly _onA11yCharEmitter = this._register(new Emitter());\n public readonly onA11yChar = this._onA11yCharEmitter.event;\n private readonly _onA11yTabEmitter = this._register(new Emitter());\n public readonly onA11yTab = this._onA11yTabEmitter.event;\n\n constructor(\n options: ITerminalOptions = {}\n ) {\n super(options);\n\n this._setup();\n\n // Setup InputHandler listeners\n this._register(this._inputHandler.onRequestBell(() => this.bell()));\n this._register(this._inputHandler.onRequestReset(() => this.reset()));\n this._register(EventUtils.forward(this._inputHandler.onCursorMove, this._onCursorMove));\n this._register(EventUtils.forward(this._inputHandler.onTitleChange, this._onTitleChange));\n this._register(EventUtils.forward(this._inputHandler.onA11yChar, this._onA11yCharEmitter));\n this._register(EventUtils.forward(this._inputHandler.onA11yTab, this._onA11yTabEmitter));\n this._register(EventUtils.forward(EventUtils.map(this._inputHandler.onRequestRefreshRows, e => ({ start: e?.start ?? 0, end: e?.end ?? this.rows - 1 })), this._onRender));\n }\n\n /**\n * Convenience property to active buffer.\n */\n public get buffer(): IBuffer {\n return this.buffers.active;\n }\n\n // TODO: Support paste here?\n\n public get markers(): IMarker[] {\n return this.buffer.markers;\n }\n\n public registerMarker(cursorYOffset: number): IMarker {\n return this.buffer.addMarker(this.buffer.ybase + this.buffer.y + cursorYOffset);\n }\n\n public bell(): void {\n this._onBell.fire();\n }\n\n public input(data: string, wasUserInput: boolean = true): void {\n this.coreService.triggerDataEvent(data, wasUserInput);\n }\n\n /**\n * Resizes the terminal.\n *\n * @param x The number of columns to resize to.\n * @param y The number of rows to resize to.\n */\n public resize(x: number, y: number): void {\n if (x === this.cols && y === this.rows) {\n return;\n }\n\n super.resize(x, y);\n }\n\n /**\n * Clear the entire buffer, making the prompt line the new first line.\n */\n public clear(): void {\n this.buffer.clearAllMarkers();\n this.buffer.lines.set(0, this.buffer.lines.get(this.buffer.ybase + this.buffer.y)!);\n this.buffer.lines.length = 1;\n this.buffer.ydisp = 0;\n this.buffer.ybase = 0;\n this.buffer.y = 0;\n for (let i = 1; i < this.rows; i++) {\n this.buffer.lines.push(this.buffer.getBlankLine(DEFAULT_ATTR_DATA));\n }\n this._onScroll.fire({ position: this.buffer.ydisp });\n }\n\n /**\n * Reset terminal.\n * Note: Calling this directly from JS is synchronous but does not clear\n * input buffers and does not reset the parser, thus the terminal will\n * continue to apply pending input data.\n * If you need in band reset (synchronous with input data) consider\n * using DECSTR (soft reset, CSI ! p) or RIS instead (hard reset, ESC c).\n */\n public reset(): void {\n /**\n * Since _setup handles a full terminal creation, we have to carry forward\n * a few things that should not reset.\n */\n this.options.rows = this.rows;\n this.options.cols = this.cols;\n\n this._setup();\n super.reset();\n }\n}\n", "/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { ITerminalAddon, IDisposable, Terminal } from '@xterm/xterm';\n\nexport interface ILoadedAddon {\n instance: ITerminalAddon;\n dispose: () => void;\n isDisposed: boolean;\n}\n\nexport class AddonManager implements IDisposable {\n protected _addons: ILoadedAddon[] = [];\n\n public dispose(): void {\n for (let i = this._addons.length - 1; i >= 0; i--) {\n this._addons[i].instance.dispose();\n }\n }\n\n public loadAddon(terminal: Terminal, instance: ITerminalAddon): void {\n const loadedAddon: ILoadedAddon = {\n instance,\n dispose: instance.dispose,\n isDisposed: false\n };\n this._addons.push(loadedAddon);\n instance.dispose = () => this._wrappedAddonDispose(loadedAddon);\n instance.activate(terminal as any);\n }\n\n private _wrappedAddonDispose(loadedAddon: ILoadedAddon): void {\n if (loadedAddon.isDisposed) {\n // Do nothing if already disposed\n return;\n }\n let index = -1;\n for (let i = 0; i < this._addons.length; i++) {\n if (this._addons[i] === loadedAddon) {\n index = i;\n break;\n }\n }\n if (index === -1) {\n throw new Error('Could not dispose an addon that has not been loaded');\n }\n loadedAddon.isDisposed = true;\n loadedAddon.dispose.apply(loadedAddon.instance);\n this._addons.splice(index, 1);\n }\n}\n", "/**\n * Copyright (c) 2018 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { BufferNamespaceApi } from '../../common/public/BufferNamespaceApi';\nimport { ParserApi } from '../../common/public/ParserApi';\nimport { UnicodeApi } from '../../common/public/UnicodeApi';\nimport { IBufferNamespace as IBufferNamespaceApi, IMarker, IModes, IParser, ITerminalAddon, ITerminalInitOnlyOptions, IUnicodeHandling, Terminal as ITerminalApi } from '@xterm/headless';\nimport { Terminal as TerminalCore } from '../Terminal';\nimport { AddonManager } from '../../common/public/AddonManager';\nimport { ITerminalOptions } from '../../common/Types';\nimport { Disposable } from '../../common/Lifecycle';\nimport type { IEvent } from '../../common/Event';\n/**\n * The set of options that only have an effect when set in the Terminal constructor.\n */\nconst CONSTRUCTOR_ONLY_OPTIONS = ['cols', 'rows'];\n\nexport class Terminal extends Disposable implements ITerminalApi {\n private _core: TerminalCore;\n private _addonManager: AddonManager;\n private _parser: IParser | undefined;\n private _buffer: BufferNamespaceApi | undefined;\n private _publicOptions: Required;\n\n constructor(options?: ITerminalOptions & ITerminalInitOnlyOptions) {\n super();\n\n this._core = this._register(new TerminalCore(options));\n this._addonManager = this._register(new AddonManager());\n\n this._publicOptions = { ... this._core.options };\n const getter = (propName: string): any => {\n return this._core.options[propName];\n };\n const setter = (propName: string, value: any): void => {\n this._checkReadonlyOptions(propName);\n this._core.options[propName] = value;\n };\n\n for (const propName in this._core.options) {\n const desc = {\n get: getter.bind(this, propName),\n set: setter.bind(this, propName)\n };\n Object.defineProperty(this._publicOptions, propName, desc);\n }\n }\n\n private _checkReadonlyOptions(propName: string): void {\n // Throw an error if any constructor only option is modified\n // from terminal.options\n // Modifications from anywhere else are allowed\n if (CONSTRUCTOR_ONLY_OPTIONS.includes(propName)) {\n throw new Error(`Option \"${propName}\" can only be set in the constructor`);\n }\n }\n\n private _checkProposedApi(): void {\n if (!this._core.optionsService.options.allowProposedApi) {\n throw new Error('You must set the allowProposedApi option to true to use proposed API');\n }\n }\n\n public get onBell(): IEvent { return this._core.onBell; }\n public get onBinary(): IEvent { return this._core.onBinary; }\n public get onCursorMove(): IEvent { return this._core.onCursorMove; }\n public get onData(): IEvent { return this._core.onData; }\n public get onLineFeed(): IEvent { return this._core.onLineFeed; }\n public get onRender(): IEvent<{ start: number, end: number }> { return this._core.onRender; }\n public get onResize(): IEvent<{ cols: number, rows: number }> { return this._core.onResize; }\n public get onScroll(): IEvent { return this._core.onScroll; }\n public get onTitleChange(): IEvent { return this._core.onTitleChange; }\n public get onWriteParsed(): IEvent { return this._core.onWriteParsed; }\n\n public get parser(): IParser {\n this._parser ??= new ParserApi(this._core);\n return this._parser;\n }\n public get unicode(): IUnicodeHandling {\n this._checkProposedApi();\n return new UnicodeApi(this._core);\n }\n public get rows(): number { return this._core.rows; }\n public get cols(): number { return this._core.cols; }\n public get buffer(): IBufferNamespaceApi {\n this._buffer ??= this._register(new BufferNamespaceApi(this._core));\n return this._buffer;\n }\n public get markers(): ReadonlyArray {\n return this._core.markers;\n }\n public get modes(): IModes {\n const m = this._core.coreService.decPrivateModes;\n let mouseTrackingMode: 'none' | 'x10' | 'vt200' | 'drag' | 'any' = 'none';\n switch (this._core.mouseStateService.activeProtocol) {\n case 'X10': mouseTrackingMode = 'x10'; break;\n case 'VT200': mouseTrackingMode = 'vt200'; break;\n case 'DRAG': mouseTrackingMode = 'drag'; break;\n case 'ANY': mouseTrackingMode = 'any'; break;\n }\n return {\n applicationCursorKeysMode: m.applicationCursorKeys,\n applicationKeypadMode: m.applicationKeypad,\n bracketedPasteMode: m.bracketedPasteMode,\n insertMode: this._core.coreService.modes.insertMode,\n mouseTrackingMode: mouseTrackingMode,\n originMode: m.origin,\n reverseWraparoundMode: m.reverseWraparound,\n sendFocusMode: m.sendFocus,\n showCursor: !this._core.coreService.isCursorHidden,\n synchronizedOutputMode: m.synchronizedOutput,\n win32InputMode: m.win32InputMode,\n wraparoundMode: m.wraparound\n };\n }\n public get options(): Required {\n return this._publicOptions;\n }\n public set options(options: ITerminalOptions) {\n for (const propName in options) {\n this._publicOptions[propName] = options[propName];\n }\n }\n public input(data: string, wasUserInput: boolean = true): void {\n this._core.input(data, wasUserInput);\n }\n public resize(columns: number, rows: number): void {\n this._verifyIntegers(columns, rows);\n this._core.resize(columns, rows);\n }\n public registerMarker(cursorYOffset: number = 0): IMarker {\n this._verifyIntegers(cursorYOffset);\n return this._core.registerMarker(cursorYOffset);\n }\n public addMarker(cursorYOffset: number): IMarker {\n return this.registerMarker(cursorYOffset);\n }\n public dispose(): void {\n super.dispose();\n }\n public scrollLines(amount: number): void {\n this._verifyIntegers(amount);\n this._core.scrollLines(amount);\n }\n public scrollPages(pageCount: number): void {\n this._verifyIntegers(pageCount);\n this._core.scrollPages(pageCount);\n }\n public scrollToTop(): void {\n this._core.scrollToTop();\n }\n public scrollToBottom(): void {\n this._core.scrollToBottom();\n }\n public scrollToLine(line: number): void {\n this._verifyIntegers(line);\n this._core.scrollToLine(line);\n }\n public clear(): void {\n this._core.clear();\n }\n public write(data: string | Uint8Array, callback?: () => void): void {\n this._core.write(data, callback);\n }\n public writeln(data: string | Uint8Array, callback?: () => void): void {\n this._core.write(data);\n this._core.write('\\r\\n', callback);\n }\n public reset(): void {\n this._core.reset();\n }\n public loadAddon(addon: ITerminalAddon): void {\n // TODO: This could cause issues if the addon calls renderer apis\n this._addonManager.loadAddon(this as any, addon);\n }\n\n private _verifyIntegers(...values: number[]): void {\n for (const value of values) {\n if (value === Infinity || isNaN(value) || value % 1 !== 0) {\n throw new Error('This API only accepts integers');\n }\n }\n }\n}\n"], ++ "mappings": "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M,EAAG,CAAC,KAAQ,IAAM,EACnD,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EACnD,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EACnD,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EACnD,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EACnD,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EACnD,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EACnD,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EACnD,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EACnD,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EACnD,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EACnD,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EACnD,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EACnD,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EACnD,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EACnD,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EACnD,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EACnD,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EACnD,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EACnD,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EACnD,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EACnD,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EACnD,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EACnD,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EACnD,CAAC,KAAQ,IAAM,EAAG,CAAC,KAAQ,IAAM,EAAG,CAAC,MAAQ,KAAM,EACnD,CAAC,MAAQ,KAAM,EAAG,CAAC,MAAQ,KAAM,EAAG,CAAC,MAAQ,KAAM,EACnD,CAAC,MAAQ,KAAM,EAAG,CAAC,MAAQ,KAAM,EAAG,CAAC,MAAQ,KAAM,EACnD,CAAC,MAAQ,KAAM,EAAG,CAAC,MAAQ,KAAM,EAAG,CAAC,MAAQ,KAAM,CACrD,EACMC,GAAiB,CACrB,CAAC,MAAS,KAAO,EAAG,CAAC,MAAS,KAAO,EAAG,CAAC,MAAS,KAAO,EACzD,CAAC,MAAS,KAAO,EAAG,CAAC,MAAS,KAAO,EAAG,CAAC,OAAS,MAAO,EACzD,CAAC,OAAS,MAAO,EAAG,CAAC,OAAS,MAAO,EAAG,CAAC,OAAS,MAAO,EACzD,CAAC,OAAS,MAAO,EAAG,CAAC,OAAS,MAAO,EAAG,CAAC,OAAS,MAAO,EACzD,CAAC,OAAS,MAAO,CACnB,EAGIC,EAEJ,SAASC,GAASC,EAAaC,EAA2B,CACxD,IAAIC,EAAM,EACNC,EAAMF,EAAK,OAAS,EACpBG,EACJ,GAAIJ,EAAMC,EAAK,CAAC,EAAE,CAAC,GAAKD,EAAMC,EAAKE,CAAG,EAAE,CAAC,EACvC,MAAO,GAET,KAAOA,GAAOD,GAEZ,GADAE,EAAOF,EAAMC,GAAQ,EACjBH,EAAMC,EAAKG,CAAG,EAAE,CAAC,EACnBF,EAAME,EAAM,UACHJ,EAAMC,EAAKG,CAAG,EAAE,CAAC,EAC1BD,EAAMC,EAAM,MAEZ,OAAO,GAGX,MAAO,EACT,CAEO,IAAMC,GAAN,KAAmD,CAGxD,aAAc,CAFd,KAAgB,QAAU,IAIxB,GAAI,CAACP,EAAO,CACVA,EAAQ,IAAI,WAAW,KAAK,EAC5BA,EAAM,KAAK,CAAC,EACZA,EAAM,CAAC,EAAI,EAEXA,EAAM,KAAK,EAAG,EAAG,EAAE,EACnBA,EAAM,KAAK,EAAG,IAAM,GAAI,EAIxBA,EAAM,KAAK,EAAG,KAAQ,IAAM,EAC5BA,EAAM,IAAM,EAAI,EAChBA,EAAM,IAAM,EAAI,EAChBA,EAAM,KAAK,EAAG,MAAQ,KAAM,EAC5BA,EAAM,KAAM,EAAI,EAEhBA,EAAM,KAAK,EAAG,MAAQ,KAAM,EAC5BA,EAAM,KAAK,EAAG,MAAQ,KAAM,EAC5BA,EAAM,KAAK,EAAG,MAAQ,KAAM,EAC5BA,EAAM,KAAK,EAAG,MAAQ,KAAM,EAC5BA,EAAM,KAAK,EAAG,MAAQ,KAAM,EAC5BA,EAAM,KAAK,EAAG,MAAQ,KAAM,EAO5B,QAASQ,EAAI,EAAGA,EAAIV,GAAc,OAAQ,EAAEU,EAC1CR,EAAM,KAAK,EAAGF,GAAcU,CAAC,EAAE,CAAC,EAAGV,GAAcU,CAAC,EAAE,CAAC,EAAI,CAAC,CAE9D,CACF,CAEO,QAAQC,EAA+B,CAC5C,OAAIA,EAAM,GAAW,EACjBA,EAAM,IAAY,EAClBA,EAAM,MAAcT,EAAMS,CAAG,EAC7BR,GAASQ,EAAKV,EAAc,EAAU,EACrCU,GAAO,QAAWA,GAAO,QAAaA,GAAO,QAAWA,GAAO,OAAiB,EAC9E,CACT,CAEO,eAAeC,EAAmBC,EAAyD,CAChG,IAAIC,EAAQ,KAAK,QAAQF,CAAS,EAC9BG,EAAaD,IAAU,GAAKD,IAAc,EAE9C,GAAIE,EAAY,CACd,IAAMC,EAAWC,EAAe,aAAaJ,CAAS,EAClDG,IAAa,EACfD,EAAa,GACJC,EAAWF,IACpBA,EAAQE,EAEZ,CACA,OAAOC,EAAe,oBAAoB,EAAGH,EAAOC,CAAU,CAChE,CACF,ECzIO,IAAMG,GAAN,KAAgD,CAAhD,cAIL,KAAO,OAAiB,EAExB,KAAQ,UAAsC,CAAC,EAE/C,IAAW,UAAqC,CAC9C,OAAO,KAAK,SACd,CAEO,OAAc,CACnB,KAAK,QAAU,OACf,KAAK,UAAY,CAAC,EAClB,KAAK,OAAS,CAChB,CAEO,UAAUC,EAAiB,CAChC,KAAK,OAASA,EACd,KAAK,QAAU,KAAK,UAAUA,CAAC,CACjC,CAEO,YAAYA,EAAWC,EAAqC,CACjE,KAAK,UAAUD,CAAC,EAAIC,EAChB,KAAK,SAAWD,IAClB,KAAK,QAAUC,EAEnB,CACF,EC7BO,SAASC,GAA8BC,EAAqC,CAYjF,IAAMC,EADOD,EAAc,OAAO,MAAM,IAAIA,EAAc,OAAO,MAAQA,EAAc,OAAO,EAAI,CAAC,GAC5E,IAAIA,EAAc,KAAO,CAAC,EAE3CE,EAAWF,EAAc,OAAO,MAAM,IAAIA,EAAc,OAAO,MAAQA,EAAc,OAAO,CAAC,EAC/FE,GAAYD,IACdC,EAAS,UAAaD,EAAS,CAAoB,IAAM,GAAkBA,EAAS,CAAoB,IAAM,GAElH,CCUO,IAAME,GAAN,MAAMC,CAA0B,CAyCrC,YAAmBC,EAAoB,GAAWC,EAA6B,GAAI,CAAhE,eAAAD,EAA+B,wBAAAC,EAChD,GAAIA,EAAqB,IACvB,MAAM,IAAI,MAAM,iDAAiD,EAEnE,KAAK,OAAS,IAAI,WAAWD,CAAS,EACtC,KAAK,OAAS,EACd,KAAK,WAAa,IAAI,WAAWC,CAAkB,EACnD,KAAK,iBAAmB,EACxB,KAAK,cAAgB,IAAI,YAAYD,CAAS,EAC9C,KAAK,cAAgB,GACrB,KAAK,iBAAmB,GACxB,KAAK,YAAc,EACrB,CAnCA,OAAc,UAAUE,EAA6B,CACnD,IAAMC,EAAS,IAAIJ,EACnB,GAAI,CAACG,EAAO,OACV,OAAOC,EAGT,QAASC,EAAK,MAAM,QAAQF,EAAO,CAAC,CAAC,EAAK,EAAI,EAAGE,EAAIF,EAAO,OAAQ,EAAEE,EAAG,CACvE,IAAMC,EAAQH,EAAOE,CAAC,EACtB,GAAI,MAAM,QAAQC,CAAK,EACrB,QAASC,EAAI,EAAGA,EAAID,EAAM,OAAQ,EAAEC,EAClCH,EAAO,YAAYE,EAAMC,CAAC,CAAC,OAG7BH,EAAO,SAASE,CAAK,CAEzB,CACA,OAAOF,CACT,CAuBO,OAAgB,CACrB,IAAMI,EAAY,IAAIR,EAAO,KAAK,UAAW,KAAK,kBAAkB,EACpE,OAAAQ,EAAU,OAAO,IAAI,KAAK,MAAM,EAChCA,EAAU,OAAS,KAAK,OACxBA,EAAU,WAAW,IAAI,KAAK,UAAU,EACxCA,EAAU,iBAAmB,KAAK,iBAClCA,EAAU,cAAc,IAAI,KAAK,aAAa,EAC9CA,EAAU,cAAgB,KAAK,cAC/BA,EAAU,iBAAmB,KAAK,iBAClCA,EAAU,YAAc,KAAK,YACtBA,CACT,CAQO,SAAuB,CAC5B,IAAMC,EAAmB,CAAC,EAC1B,QAASJ,EAAI,EAAGA,EAAI,KAAK,OAAQ,EAAEA,EAAG,CACpCI,EAAI,KAAK,KAAK,OAAOJ,CAAC,CAAC,EACvB,IAAMK,EAAQ,KAAK,cAAcL,CAAC,GAAK,EACjCM,EAAM,KAAK,cAAcN,CAAC,EAAI,IAChCM,EAAMD,EAAQ,GAChBD,EAAI,KAAK,MAAM,UAAU,MAAM,KAAK,KAAK,WAAYC,EAAOC,CAAG,CAAC,CAEpE,CACA,OAAOF,CACT,CAKO,OAAc,CACnB,KAAK,OAAS,EACd,KAAK,iBAAmB,EACxB,KAAK,cAAgB,GACrB,KAAK,iBAAmB,GACxB,KAAK,YAAc,EACrB,CAKO,UAAiB,CACtB,KAAK,OAAS,EACd,KAAK,iBAAmB,EACxB,KAAK,cAAgB,GACrB,KAAK,iBAAmB,GACxB,KAAK,YAAc,GACnB,KAAK,cAAc,CAAC,EAAI,EACxB,KAAK,OAAO,CAAC,EAAI,CACnB,CASO,SAASH,EAAqB,CAEnC,GADA,KAAK,YAAc,GACf,KAAK,QAAU,KAAK,UAAW,CACjC,KAAK,cAAgB,GACrB,MACF,CACA,GAAIA,EAAQ,GACV,MAAM,IAAI,MAAM,qCAAqC,EAEvD,KAAK,cAAc,KAAK,MAAM,EAAI,KAAK,kBAAoB,EAAI,KAAK,iBACpE,KAAK,OAAO,KAAK,QAAQ,EAAIA,EAAQ,WAAsB,WAAsBA,CACnF,CASO,YAAYA,EAAqB,CAEtC,GADA,KAAK,YAAc,GACf,EAAC,KAAK,OAGV,IAAI,KAAK,eAAiB,KAAK,kBAAoB,KAAK,mBAAoB,CAC1E,KAAK,iBAAmB,GACxB,MACF,CACA,GAAIA,EAAQ,GACV,MAAM,IAAI,MAAM,qCAAqC,EAEvD,KAAK,WAAW,KAAK,kBAAkB,EAAIA,EAAQ,WAAsB,WAAsBA,EAC/F,KAAK,cAAc,KAAK,OAAS,CAAC,IACpC,CAKO,aAAaM,EAAsB,CACxC,OAAS,KAAK,cAAcA,CAAG,EAAI,MAAS,KAAK,cAAcA,CAAG,GAAK,GAAK,CAC9E,CAOO,aAAaA,EAAgC,CAClD,IAAMF,EAAQ,KAAK,cAAcE,CAAG,GAAK,EACnCD,EAAM,KAAK,cAAcC,CAAG,EAAI,IACtC,OAAID,EAAMD,EAAQ,EACT,KAAK,WAAW,SAASA,EAAOC,CAAG,EAErC,IACT,CAMO,iBAA+C,CACpD,IAAME,EAAsC,CAAC,EAC7C,QAASR,EAAI,EAAGA,EAAI,KAAK,OAAQ,EAAEA,EAAG,CACpC,IAAMK,EAAQ,KAAK,cAAcL,CAAC,GAAK,EACjCM,EAAM,KAAK,cAAcN,CAAC,EAAI,IAChCM,EAAMD,EAAQ,IAChBG,EAAOR,CAAC,EAAI,KAAK,WAAW,MAAMK,EAAOC,CAAG,EAEhD,CACA,OAAOE,CACT,CAMO,SAASP,EAAqB,CACnC,IAAIQ,EACJ,GAAI,KAAK,eACJ,EAAEA,EAAS,KAAK,YAAc,KAAK,iBAAmB,KAAK,SAC1D,KAAK,aAAe,KAAK,iBAE7B,OAGF,IAAMC,EAAQ,KAAK,YAAc,KAAK,WAAa,KAAK,OAClDC,EAAMD,EAAMD,EAAS,CAAC,EAC5BC,EAAMD,EAAS,CAAC,EAAI,CAACE,EAAM,KAAK,IAAIA,EAAM,GAAKV,EAAO,UAAmB,EAAIA,CAC/E,CACF,EC/OO,IAAMW,GAAN,KAAoB,CAApB,cACL,KAAQ,QAAoB,CAAC,EAC7B,KAAQ,QAAU,EAElB,IAAW,QAAiB,CAC1B,OAAO,KAAK,OACd,CAEO,OAAc,CACnB,KAAK,QAAQ,OAAS,EACtB,KAAK,QAAU,CACjB,CAEO,OAAOC,EAAqB,CACjC,KAAK,QAAQ,KAAKA,CAAK,EACvB,KAAK,SAAWA,EAAM,MACxB,CAEO,UAAmB,CACxB,OAAO,KAAK,QAAQ,KAAK,EAAE,CAC7B,CACF,EAKaC,EAAN,KAA2B,CAGhC,YAA6BC,EAAgB,CAAhB,YAAAA,EAF7B,KAAiB,SAAW,IAAIH,EAEe,CAE/C,IAAW,QAAiB,CAC1B,OAAO,KAAK,SAAS,MACvB,CAEA,IAAW,OAAgB,CACzB,OAAO,KAAK,MACd,CAEO,OAAc,CACnB,KAAK,SAAS,MAAM,CACtB,CAKO,OAAOC,EAAwB,CAEpC,OADA,KAAK,SAAS,OAAOA,CAAK,EACtB,KAAK,SAAS,OAAS,KAAK,QAC9B,KAAK,SAAS,MAAM,EACb,IAEF,EACT,CAEO,UAAmB,CACxB,OAAO,KAAK,SAAS,SAAS,CAChC,CACF,ECvDA,IAAMG,GAAgC,CAAC,EAE1BC,GAAN,KAAsC,CAAtC,cACL,KAAQ,OAAS,EACjB,KAAQ,QAAUD,GAClB,KAAQ,IAAM,GACd,KAAQ,UAA6C,OAAO,OAAO,IAAI,EACvE,KAAQ,WAAqC,IAAM,CAAE,EACrD,KAAQ,OAA+B,CACrC,OAAQ,GACR,aAAc,EACd,YAAa,EACf,EAEO,gBAAgBE,EAAeC,EAAmC,CACvE,KAAK,UAAUD,CAAK,IAAM,CAAC,EAC3B,IAAME,EAAc,KAAK,UAAUF,CAAK,EACxC,OAAAE,EAAY,KAAKD,CAAO,EACjB,CACL,QAAS,IAAM,CACb,IAAME,EAAeD,EAAY,QAAQD,CAAO,EAC5CE,IAAiB,IACnBD,EAAY,OAAOC,EAAc,CAAC,CAEtC,CACF,CACF,CACO,aAAaH,EAAqB,CACnC,KAAK,UAAUA,CAAK,GAAG,OAAO,KAAK,UAAUA,CAAK,CACxD,CACO,mBAAmBC,EAAuC,CAC/D,KAAK,WAAaA,CACpB,CAEO,SAAgB,CACrB,KAAK,UAAY,OAAO,OAAO,IAAI,EACnC,KAAK,WAAa,IAAM,CAAE,EAC1B,KAAK,QAAUH,EACjB,CAEO,OAAc,CAEnB,GAAI,KAAK,SAAW,EAClB,QAASM,EAAI,KAAK,OAAO,OAAS,KAAK,OAAO,aAAe,EAAI,KAAK,QAAQ,OAAS,EAAGA,GAAK,EAAG,EAAEA,EAClG,KAAK,QAAQA,CAAC,EAAE,IAAI,EAAK,EAG7B,KAAK,OAAO,OAAS,GACrB,KAAK,QAAUN,GACf,KAAK,IAAM,GACX,KAAK,OAAS,CAChB,CAEQ,QAAe,CAErB,GADA,KAAK,QAAU,KAAK,UAAU,KAAK,GAAG,GAAKA,GACvC,CAAC,KAAK,QAAQ,OAChB,KAAK,WAAW,KAAK,IAAK,OAAO,MAEjC,SAASM,EAAI,KAAK,QAAQ,OAAS,EAAGA,GAAK,EAAGA,IAC5C,KAAK,QAAQA,CAAC,EAAE,MAAM,CAG5B,CAEQ,KAAKC,EAAmBC,EAAeC,EAAmB,CAChE,GAAI,CAAC,KAAK,QAAQ,OAChB,KAAK,WAAW,KAAK,IAAK,MAAOC,EAAcH,EAAMC,EAAOC,CAAG,CAAC,MAEhE,SAASH,EAAI,KAAK,QAAQ,OAAS,EAAGA,GAAK,EAAGA,IAC5C,KAAK,QAAQA,CAAC,EAAE,IAAIC,EAAMC,EAAOC,CAAG,CAG1C,CAEO,OAAc,CAEnB,KAAK,MAAM,EACX,KAAK,OAAS,CAChB,CASO,IAAIF,EAAmBC,EAAeC,EAAmB,CAC9D,GAAI,KAAK,SAAW,EAGpB,IAAI,KAAK,SAAW,EAClB,KAAOD,EAAQC,GAAK,CAClB,IAAME,EAAOJ,EAAKC,GAAO,EACzB,GAAIG,IAAS,GAAM,CACjB,KAAK,OAAS,EACd,KAAK,OAAO,EACZ,KACF,CACA,GAAIA,EAAO,IAAQ,GAAOA,EAAM,CAC9B,KAAK,OAAS,EACd,MACF,CACI,KAAK,MAAQ,KACf,KAAK,IAAM,GAEb,KAAK,IAAM,KAAK,IAAM,GAAKA,EAAO,EACpC,CAEE,KAAK,SAAW,GAAoBF,EAAMD,EAAQ,GACpD,KAAK,KAAKD,EAAMC,EAAOC,CAAG,EAE9B,CAOO,IAAIG,EAAkBC,EAAyB,GAA+B,CACnF,GAAI,KAAK,SAAW,EAIpB,IAAI,KAAK,SAAW,EAQlB,GAJI,KAAK,SAAW,GAClB,KAAK,OAAO,EAGV,CAAC,KAAK,QAAQ,OAChB,KAAK,WAAW,KAAK,IAAK,MAAOD,CAAO,MACnC,CACL,IAAIE,EAA4C,GAC5CR,EAAI,KAAK,QAAQ,OAAS,EAC1BS,EAAc,GAOlB,GANI,KAAK,OAAO,SACdT,EAAI,KAAK,OAAO,aAAe,EAC/BQ,EAAgBD,EAChBE,EAAc,KAAK,OAAO,YAC1B,KAAK,OAAO,OAAS,IAEnB,CAACA,GAAeD,IAAkB,GAAO,CAC3C,KAAOR,GAAK,IACVQ,EAAgB,KAAK,QAAQR,CAAC,EAAE,IAAIM,CAAO,EACvCE,IAAkB,IAFTR,IAIN,GAAIQ,aAAyB,QAClC,YAAK,OAAO,OAAS,GACrB,KAAK,OAAO,aAAeR,EAC3B,KAAK,OAAO,YAAc,GACnBQ,EAGXR,GACF,CAIA,KAAOA,GAAK,EAAGA,IAEb,GADAQ,EAAgB,KAAK,QAAQR,CAAC,EAAE,IAAI,EAAK,EACrCQ,aAAyB,QAC3B,YAAK,OAAO,OAAS,GACrB,KAAK,OAAO,aAAeR,EAC3B,KAAK,OAAO,YAAc,GACnBQ,CAGb,CAGF,KAAK,QAAUd,GACf,KAAK,IAAM,GACX,KAAK,OAAS,EAChB,CACF,EAMagB,GAAN,MAAMA,EAAkC,CAM7C,YAAoBC,EAAwD,CAAxD,cAAAA,EAHpB,KAAQ,MAAQ,IAAIC,EAAqBF,GAAW,aAAa,EACjE,KAAQ,UAAqB,EAEiD,CAEvE,OAAc,CACnB,KAAK,MAAM,MAAM,EACjB,KAAK,UAAY,EACnB,CAEO,IAAIT,EAAmBC,EAAeC,EAAmB,CAC1D,KAAK,WAGL,KAAK,MAAM,OAAOC,EAAcH,EAAMC,EAAOC,CAAG,CAAC,IACnD,KAAK,UAAY,GAErB,CAEO,IAAIG,EAA8C,CACvD,IAAIO,EAAkC,GACtC,GAAI,KAAK,UACPA,EAAM,WACGP,IACTO,EAAM,KAAK,SAAS,KAAK,MAAM,SAAS,CAAC,EACrCA,aAAe,SAGjB,OAAOA,EAAI,KAAKC,IACd,KAAK,MAAM,MAAM,EACjB,KAAK,UAAY,GACVA,EACR,EAGL,YAAK,MAAM,MAAM,EACjB,KAAK,UAAY,GACVD,CACT,CACF,EA1CaH,GACI,cAAgB,IAD1B,IAAMK,EAANL,GCtLP,IAAMM,GAAgC,CAAC,EAE1BC,GAAN,KAAsC,CAAtC,cACL,KAAQ,UAA6C,OAAO,OAAO,IAAI,EACvE,KAAQ,QAAyBD,GACjC,KAAQ,OAAiB,EACzB,KAAQ,WAAqC,IAAM,CAAE,EACrD,KAAQ,OAA+B,CACrC,OAAQ,GACR,aAAc,EACd,YAAa,EACf,EAEO,SAAgB,CACrB,KAAK,UAAY,OAAO,OAAO,IAAI,EACnC,KAAK,WAAa,IAAM,CAAE,EAC1B,KAAK,QAAUA,EACjB,CAEO,gBAAgBE,EAAeC,EAAmC,CACvE,KAAK,UAAUD,CAAK,IAAM,CAAC,EAC3B,IAAME,EAAc,KAAK,UAAUF,CAAK,EACxC,OAAAE,EAAY,KAAKD,CAAO,EACjB,CACL,QAAS,IAAM,CACb,IAAME,EAAeD,EAAY,QAAQD,CAAO,EAC5CE,IAAiB,IACnBD,EAAY,OAAOC,EAAc,CAAC,CAEtC,CACF,CACF,CAEO,aAAaH,EAAqB,CACnC,KAAK,UAAUA,CAAK,GAAG,OAAO,KAAK,UAAUA,CAAK,CACxD,CAEO,mBAAmBC,EAAuC,CAC/D,KAAK,WAAaA,CACpB,CAEO,OAAc,CAEnB,GAAI,KAAK,QAAQ,OACf,QAASG,EAAI,KAAK,OAAO,OAAS,KAAK,OAAO,aAAe,EAAI,KAAK,QAAQ,OAAS,EAAGA,GAAK,EAAG,EAAEA,EAClG,KAAK,QAAQA,CAAC,EAAE,OAAO,EAAK,EAGhC,KAAK,OAAO,OAAS,GACrB,KAAK,QAAUN,GACf,KAAK,OAAS,CAChB,CAEO,KAAKE,EAAeK,EAAuB,CAKhD,GAHA,KAAK,MAAM,EACX,KAAK,OAASL,EACd,KAAK,QAAU,KAAK,UAAUA,CAAK,GAAKF,GACpC,CAAC,KAAK,QAAQ,OAChB,KAAK,WAAW,KAAK,OAAQ,OAAQO,CAAM,MAE3C,SAASD,EAAI,KAAK,QAAQ,OAAS,EAAGA,GAAK,EAAGA,IAC5C,KAAK,QAAQA,CAAC,EAAE,KAAKC,CAAM,CAGjC,CAEO,IAAIC,EAAmBC,EAAeC,EAAmB,CAC9D,GAAI,CAAC,KAAK,QAAQ,OAChB,KAAK,WAAW,KAAK,OAAQ,MAAOC,EAAcH,EAAMC,EAAOC,CAAG,CAAC,MAEnE,SAASJ,EAAI,KAAK,QAAQ,OAAS,EAAGA,GAAK,EAAGA,IAC5C,KAAK,QAAQA,CAAC,EAAE,IAAIE,EAAMC,EAAOC,CAAG,CAG1C,CAEO,OAAOE,EAAkBC,EAAyB,GAA+B,CACtF,GAAI,CAAC,KAAK,QAAQ,OAChB,KAAK,WAAW,KAAK,OAAQ,SAAUD,CAAO,MACzC,CACL,IAAIE,EAA4C,GAC5CR,EAAI,KAAK,QAAQ,OAAS,EAC1BS,EAAc,GAOlB,GANI,KAAK,OAAO,SACdT,EAAI,KAAK,OAAO,aAAe,EAC/BQ,EAAgBD,EAChBE,EAAc,KAAK,OAAO,YAC1B,KAAK,OAAO,OAAS,IAEnB,CAACA,GAAeD,IAAkB,GAAO,CAC3C,KAAOR,GAAK,IACVQ,EAAgB,KAAK,QAAQR,CAAC,EAAE,OAAOM,CAAO,EAC1CE,IAAkB,IAFTR,IAIN,GAAIQ,aAAyB,QAClC,YAAK,OAAO,OAAS,GACrB,KAAK,OAAO,aAAeR,EAC3B,KAAK,OAAO,YAAc,GACnBQ,EAGXR,GACF,CAEA,KAAOA,GAAK,EAAGA,IAEb,GADAQ,EAAgB,KAAK,QAAQR,CAAC,EAAE,OAAO,EAAK,EACxCQ,aAAyB,QAC3B,YAAK,OAAO,OAAS,GACrB,KAAK,OAAO,aAAeR,EAC3B,KAAK,OAAO,YAAc,GACnBQ,CAGb,CACA,KAAK,QAAUd,GACf,KAAK,OAAS,CAChB,CACF,EAGMgB,GAAe,IAAIC,GACzBD,GAAa,SAAS,CAAC,EAMhB,IAAME,GAAN,MAAMA,EAAkC,CAO7C,YAAoBC,EAAyE,CAAzE,cAAAA,EAJpB,KAAQ,MAAQ,IAAIC,EAAqBF,GAAW,aAAa,EACjE,KAAQ,QAAmBF,GAC3B,KAAQ,UAAqB,EAEkE,CAExF,KAAKT,EAAuB,CAKjC,KAAK,QAAWA,EAAO,OAAS,GAAKA,EAAO,OAAO,CAAC,EAAKA,EAAO,MAAM,EAAIS,GAC1E,KAAK,MAAM,MAAM,EACjB,KAAK,UAAY,EACnB,CAEO,IAAIR,EAAmBC,EAAeC,EAAmB,CAC1D,KAAK,WAGL,KAAK,MAAM,OAAOC,EAAcH,EAAMC,EAAOC,CAAG,CAAC,IACnD,KAAK,UAAY,GAErB,CAEO,OAAOE,EAA8C,CAC1D,IAAIS,EAAkC,GACtC,GAAI,KAAK,UACPA,EAAM,WACGT,IACTS,EAAM,KAAK,SAAS,KAAK,MAAM,SAAS,EAAG,KAAK,OAAO,EACnDA,aAAe,SAGjB,OAAOA,EAAI,KAAKC,IACd,KAAK,QAAUN,GACf,KAAK,MAAM,MAAM,EACjB,KAAK,UAAY,GACVM,EACR,EAGL,YAAK,QAAUN,GACf,KAAK,MAAM,MAAM,EACjB,KAAK,UAAY,GACVK,CACT,CACF,EAlDaH,GACI,cAAgB,IAD1B,IAAMK,GAANL,GCjIP,IAAMM,GAAgC,CAAC,EAU1BC,GAAN,KAAsC,CAAtC,cACL,KAAQ,UAA6C,OAAO,OAAO,IAAI,EACvE,KAAQ,QAAUD,GAClB,KAAQ,OAAiB,EACzB,KAAQ,WAAqC,IAAM,CAAE,EACrD,KAAQ,OAA+B,CACrC,OAAQ,GACR,aAAc,EACd,YAAa,EACf,EAOO,gBAAgBE,EAAeC,EAAmC,CACvE,KAAK,UAAUD,CAAK,IAAM,CAAC,EAC3B,IAAME,EAAc,KAAK,UAAUF,CAAK,EACxC,OAAAE,EAAY,KAAKD,CAAO,EACjB,CACL,QAAS,IAAM,CACb,IAAME,EAAeD,EAAY,QAAQD,CAAO,EAC5CE,IAAiB,IACnBD,EAAY,OAAOC,EAAc,CAAC,CAEtC,CACF,CACF,CAEO,aAAaH,EAAqB,CACnC,KAAK,UAAUA,CAAK,GAAG,OAAO,KAAK,UAAUA,CAAK,CACxD,CAEO,mBAAmBC,EAAuC,CAC/D,KAAK,WAAaA,CACpB,CAEO,SAAgB,CACrB,KAAK,UAAY,OAAO,OAAO,IAAI,EACnC,KAAK,WAAa,IAAM,CAAE,EAC1B,KAAK,QAAUH,EACjB,CAEO,OAAc,CAEnB,GAAI,KAAK,QAAQ,OACf,QAASM,EAAI,KAAK,OAAO,OAAS,KAAK,OAAO,aAAe,EAAI,KAAK,QAAQ,OAAS,EAAGA,GAAK,EAAG,EAAEA,EAClG,KAAK,QAAQA,CAAC,EAAE,IAAI,EAAK,EAG7B,KAAK,OAAO,OAAS,GACrB,KAAK,QAAUN,GACf,KAAK,OAAS,CAChB,CAEO,MAAME,EAAqB,CAKhC,GAHA,KAAK,MAAM,EACX,KAAK,OAASA,EACd,KAAK,QAAU,KAAK,UAAUA,CAAK,GAAKF,GACpC,CAAC,KAAK,QAAQ,OAChB,KAAK,WAAW,KAAK,OAAQ,OAAO,MAEpC,SAASM,EAAI,KAAK,QAAQ,OAAS,EAAGA,GAAK,EAAGA,IAC5C,KAAK,QAAQA,CAAC,EAAE,MAAM,CAG5B,CAEO,IAAIC,EAAmBC,EAAeC,EAAmB,CAC9D,GAAI,CAAC,KAAK,QAAQ,OAChB,KAAK,WAAW,KAAK,OAAQ,MAAOC,EAAcH,EAAMC,EAAOC,CAAG,CAAC,MAEnE,SAASH,EAAI,KAAK,QAAQ,OAAS,EAAGA,GAAK,EAAGA,IAC5C,KAAK,QAAQA,CAAC,EAAE,IAAIC,EAAMC,EAAOC,CAAG,CAG1C,CAOO,IAAIE,EAAkBC,EAAyB,GAA+B,CACnF,GAAI,CAAC,KAAK,QAAQ,OAChB,KAAK,WAAW,KAAK,OAAQ,MAAOD,CAAO,MACtC,CACL,IAAIE,EAA4C,GAC5CP,EAAI,KAAK,QAAQ,OAAS,EAC1BQ,EAAc,GAOlB,GANI,KAAK,OAAO,SACdR,EAAI,KAAK,OAAO,aAAe,EAC/BO,EAAgBD,EAChBE,EAAc,KAAK,OAAO,YAC1B,KAAK,OAAO,OAAS,IAEnB,CAACA,GAAeD,IAAkB,GAAO,CAC3C,KAAOP,GAAK,IACVO,EAAgB,KAAK,QAAQP,CAAC,EAAE,IAAIK,CAAO,EACvCE,IAAkB,IAFTP,IAIN,GAAIO,aAAyB,QAClC,YAAK,OAAO,OAAS,GACrB,KAAK,OAAO,aAAeP,EAC3B,KAAK,OAAO,YAAc,GACnBO,EAGXP,GACF,CAEA,KAAOA,GAAK,EAAGA,IAEb,GADAO,EAAgB,KAAK,QAAQP,CAAC,EAAE,IAAI,EAAK,EACrCO,aAAyB,QAC3B,YAAK,OAAO,OAAS,GACrB,KAAK,OAAO,aAAeP,EAC3B,KAAK,OAAO,YAAc,GACnBO,CAGb,CACA,KAAK,QAAUb,GACf,KAAK,OAAS,CAChB,CACF,EAMae,GAAN,MAAMA,EAAkC,CAM7C,YAAoBC,EAAwD,CAAxD,cAAAA,EAHpB,KAAQ,MAAQ,IAAIC,EAAqBF,GAAW,aAAa,EACjE,KAAQ,UAAqB,EAEiD,CAEvE,OAAc,CACnB,KAAK,MAAM,MAAM,EACjB,KAAK,UAAY,EACnB,CAEO,IAAIR,EAAmBC,EAAeC,EAAmB,CAC1D,KAAK,WAGL,KAAK,MAAM,OAAOC,EAAcH,EAAMC,EAAOC,CAAG,CAAC,IACnD,KAAK,UAAY,GAErB,CAEO,IAAIE,EAA8C,CACvD,IAAIO,EAAkC,GACtC,GAAI,KAAK,UACPA,EAAM,WACGP,IACTO,EAAM,KAAK,SAAS,KAAK,MAAM,SAAS,CAAC,EACrCA,aAAe,SAGjB,OAAOA,EAAI,KAAKC,IACd,KAAK,MAAM,MAAM,EACjB,KAAK,UAAY,GACVA,EACR,EAGL,YAAK,MAAM,MAAM,EACjB,KAAK,UAAY,GACVD,CACT,CACF,EA1CaH,GACI,cAAgB,IAD1B,IAAMK,GAANL,GC3GA,IAAMM,GAAN,KAAsB,CAG3B,YAAYC,EAAgB,CAC1B,KAAK,MAAQ,IAAI,YAAYA,CAAM,CACrC,CAOO,WAAWC,EAAsBC,EAAyB,CAC/D,KAAK,MAAM,KAAKD,GAAU,EAAsCC,CAAI,CACtE,CASO,IAAIC,EAAcC,EAAoBH,EAAsBC,EAAyB,CAC1F,KAAK,MAAME,GAAS,EAAgCD,CAAI,EAAIF,GAAU,EAAsCC,CAC9G,CASO,QAAQG,EAAiBD,EAAoBH,EAAsBC,EAAyB,CACjG,QAASI,EAAI,EAAGA,EAAID,EAAM,OAAQC,IAChC,KAAK,MAAMF,GAAS,EAAgCC,EAAMC,CAAC,CAAC,EAAIL,GAAU,EAAsCC,CAEpH,CACF,EAIMK,EAAsB,IAOfC,IAA0B,UAA6B,CAGlE,IAAMC,EAAyB,IAAIV,GAAgB,IAAI,EAIjDW,EAAY,MAAM,MAAM,KAAM,MADhB,GACiC,CAAC,EAAE,IAAI,CAACC,EAAaL,IAAcA,CAAC,EACnFM,EAAI,CAACC,EAAeC,IAA0BJ,EAAU,MAAMG,EAAOC,CAAG,EAGxEC,EAAaH,EAAE,GAAM,GAAI,EACzBI,EAAcJ,EAAE,EAAM,EAAI,EAChCI,EAAY,KAAK,EAAI,EACrBA,EAAY,KAAK,MAAMA,EAAaJ,EAAE,GAAM,EAAI,CAAC,EAEjD,IAAMK,EAAmBL,MAA8C,EAGvEH,EAAM,cAAiD,EAEvDA,EAAM,QAAQM,OAAsE,EAEpF,QAAWX,KAASa,EAClBR,EAAM,QAAQ,CAAC,GAAM,GAAM,IAAM,GAAI,EAAGL,KAA+C,EACvFK,EAAM,QAAQG,EAAE,IAAM,GAAI,EAAGR,KAA+C,EAC5EK,EAAM,QAAQG,EAAE,IAAM,GAAI,EAAGR,KAA+C,EAC5EK,EAAM,IAAI,IAAML,KAA8C,EAC9DK,EAAM,IAAI,GAAML,MAA6C,EAC7DK,EAAM,IAAI,IAAML,KAAqD,EACrEK,EAAM,QAAQ,CAAC,IAAM,GAAI,EAAGL,KAAqD,EACjFK,EAAM,IAAI,IAAML,OAAgD,EAChEK,EAAM,IAAI,IAAML,MAAgD,EAChEK,EAAM,IAAI,IAAML,MAAgD,EAGlE,OAAAK,EAAM,QAAQO,OAAyE,EACvFP,EAAM,QAAQO,OAAyE,EACvFP,EAAM,IAAI,SAAiE,EAC3EA,EAAM,QAAQO,OAAgF,EAC9FP,EAAM,QAAQO,OAA+E,EAC7FP,EAAM,IAAI,SAAuE,EACjFA,EAAM,QAAQO,OAA+E,EAC7FP,EAAM,IAAI,SAAuE,EACjFA,EAAM,QAAQO,OAAiF,EAC/FP,EAAM,QAAQO,OAA6F,EAC3GP,EAAM,IAAI,SAAqF,EAC/FA,EAAM,QAAQO,OAAmG,EACjHP,EAAM,IAAI,SAA2F,EAErGA,EAAM,IAAI,QAAwE,EAClFA,EAAM,QAAQM,OAAgF,EAC9FN,EAAM,IAAI,SAA0E,EACpFA,EAAM,QAAQ,CAAC,IAAM,GAAM,GAAM,GAAM,CAAI,OAAmE,EAC9GA,EAAM,QAAQG,EAAE,GAAM,EAAI,OAAsE,EAEhGH,EAAM,QAAQ,CAAC,GAAM,EAAI,OAAqE,EAC9FA,EAAM,QAAQM,OAAqF,EACnGN,EAAM,QAAQO,OAAsF,EACpGP,EAAM,IAAI,SAAwE,EAClFA,EAAM,IAAI,SAA+E,EAEzFA,EAAM,IAAI,UAAmE,EAC7EA,EAAM,QAAQO,SAA8E,EAC5FP,EAAM,IAAI,WAAuE,EACjFA,EAAM,QAAQG,EAAE,GAAM,EAAI,SAA4E,EACtGH,EAAM,QAAQG,EAAE,GAAM,GAAI,UAA6E,EACvGH,EAAM,QAAQG,EAAE,GAAM,GAAI,UAAoF,EAC9GH,EAAM,QAAQO,SAA4F,EAC1GP,EAAM,QAAQG,EAAE,GAAM,EAAI,SAAmF,EAC7GH,EAAM,IAAI,WAAqF,EAC/FA,EAAM,QAAQM,UAA0F,EACxGN,EAAM,QAAQO,SAA0F,EACxGP,EAAM,QAAQG,EAAE,EAAM,EAAI,UAAiF,EAC3GH,EAAM,IAAI,WAAmF,EAC7FA,EAAM,QAAQ,CAAC,GAAM,IAAM,GAAM,EAAI,SAAwE,EAE7GA,EAAM,IAAI,SAAmE,EAC7EA,EAAM,QAAQG,EAAE,GAAM,GAAI,OAAuE,EACjGH,EAAM,QAAQG,EAAE,GAAM,EAAI,OAAmE,EAC7FH,EAAM,QAAQ,CAAC,GAAM,GAAM,GAAM,EAAI,OAAqE,EAC1GA,EAAM,QAAQG,EAAE,GAAM,EAAI,OAAmE,EAC7FH,EAAM,QAAQG,EAAE,GAAM,GAAI,OAAuE,EACjGH,EAAM,QAAQ,CAAC,GAAM,GAAM,GAAM,EAAI,OAAqE,EAC1GA,EAAM,QAAQG,EAAE,GAAM,EAAI,OAAsE,EAChGH,EAAM,IAAI,SAAyE,EACnFA,EAAM,QAAQG,EAAE,GAAM,GAAI,OAAkE,EAC5FH,EAAM,QAAQG,EAAE,GAAM,EAAI,OAA4E,EACtGH,EAAM,QAAQG,EAAE,GAAM,EAAI,OAAmF,EAC7GH,EAAM,QAAQG,EAAE,GAAM,EAAI,OAA4E,EACtGH,EAAM,QAAQG,EAAE,GAAM,GAAI,OAA8E,EACxGH,EAAM,QAAQG,EAAE,GAAM,EAAI,OAA4E,EAEtGH,EAAM,QAAQG,EAAE,GAAM,EAAI,OAA4E,EACtGH,EAAM,QAAQG,EAAE,GAAM,EAAI,OAAyF,EACnHH,EAAM,QAAQG,EAAE,GAAM,GAAI,QAAiF,EAC3GH,EAAM,QAAQG,EAAE,GAAM,EAAI,QAAoE,EAC9FH,EAAM,QAAQG,EAAE,GAAM,EAAI,QAAoE,EAC9FH,EAAM,QAAQ,CAAC,GAAM,GAAM,EAAI,QAAoE,EACnGA,EAAM,QAAQG,EAAE,GAAM,GAAI,QAAoE,EAE9FH,EAAM,IAAI,SAAmE,EAC7EA,EAAM,QAAQO,OAA8E,EAC5FP,EAAM,IAAI,SAAuE,EACjFA,EAAM,QAAQG,EAAE,GAAM,EAAI,QAA4E,EACtGH,EAAM,QAAQG,EAAE,GAAM,EAAI,QAAmE,EAC7FH,EAAM,QAAQ,CAAC,GAAM,GAAM,GAAM,EAAI,QAAqE,EAC1GA,EAAM,QAAQO,SAAgF,EAC9FP,EAAM,QAAQG,EAAE,GAAM,GAAI,SAAsE,EAChGH,EAAM,QAAQO,SAA8E,EAC5FP,EAAM,IAAI,WAAuE,EACjFA,EAAM,QAAQG,EAAE,GAAM,EAAI,SAAmE,EAC7FH,EAAM,QAAQ,CAAC,GAAM,GAAM,GAAM,EAAI,SAAqE,EAC1GA,EAAM,QAAQG,EAAE,GAAM,EAAI,SAA4E,EACtGH,EAAM,QAAQO,SAA4F,EAC1GP,EAAM,IAAI,WAAqF,EAC/FA,EAAM,QAAQG,EAAE,GAAM,EAAI,SAAmF,EAC7GH,EAAM,QAAQG,EAAE,GAAM,EAAI,SAA4E,EACtGH,EAAM,QAAQG,EAAE,GAAM,GAAI,UAAmF,EAC7GH,EAAM,QAAQG,EAAE,GAAM,GAAI,UAA4E,EACtGH,EAAM,QAAQG,EAAE,GAAM,GAAI,SAA4E,EACtGH,EAAM,QAAQO,UAA2F,EACzGP,EAAM,QAAQM,UAA0F,EACxGN,EAAM,IAAI,WAAmF,EAC7FA,EAAM,QAAQ,CAAC,GAAM,IAAM,GAAM,EAAI,SAA2E,EAEhHA,EAAM,IAAIF,OAA+E,EACzFE,EAAM,IAAIF,OAAyF,EACnGE,EAAM,IAAIF,OAAwF,EAClGE,EAAM,IAAIF,SAAwF,EAClGE,EAAM,IAAIF,UAAmG,EAC7GE,EAAM,IAAIF,UAAmG,EACtGE,CACT,GAAG,EAiCUS,GAAN,cAAmCC,CAA4C,CAqCpF,YACqBC,EAAgCZ,GACnD,CACA,MAAM,EAFa,kBAAAY,EATrB,KAAU,YAAiC,CACzC,QACA,SAAU,CAAC,EACX,WAAY,EACZ,WAAY,EACZ,SAAU,CACZ,EAOE,KAAK,aAAe,EACpB,KAAK,aAAe,KAAK,aACzB,KAAK,QAAU,IAAIC,GACnB,KAAK,QAAQ,SAAS,CAAC,EACvB,KAAK,SAAW,EAChB,KAAK,mBAAqB,EAG1B,KAAK,gBAAkB,CAACC,EAAMT,EAAOC,IAAc,CAAE,EACrD,KAAK,kBAAqBX,GAAuB,CAAE,EACnD,KAAK,cAAgB,CAACoB,EAAeC,IAA0B,CAAE,EACjE,KAAK,cAAiBD,GAAwB,CAAE,EAChD,KAAK,gBAAmBnB,GAAwCA,EAChE,KAAK,cAAgB,KAAK,gBAC1B,KAAK,iBAAmB,OAAO,OAAO,IAAI,EAC1C,KAAK,oBAAsB,IAAI,MAAM,EAAI,EAAE,KAAK,MAAS,EACzD,KAAK,aAAe,OAAO,OAAO,IAAI,EACtC,KAAK,aAAe,OAAO,OAAO,IAAI,EACtC,KAAK,UAAUqB,EAAa,IAAM,CAChC,KAAK,aAAe,OAAO,OAAO,IAAI,EACtC,KAAK,iBAAmB,OAAO,OAAO,IAAI,EAC1C,KAAK,oBAAsB,IAAI,MAAM,EAAI,EAAE,KAAK,MAAS,EACzD,KAAK,aAAe,OAAO,OAAO,IAAI,CACxC,CAAC,CAAC,EACF,KAAK,WAAa,KAAK,UAAU,IAAIC,EAAW,EAChD,KAAK,WAAa,KAAK,UAAU,IAAIC,EAAW,EAChD,KAAK,WAAa,KAAK,UAAU,IAAIC,EAAW,EAChD,KAAK,cAAgB,KAAK,gBAG1B,KAAK,mBAAmB,CAAE,MAAO,IAAK,EAAG,IAAM,EAAI,CACrD,CAEU,YAAYC,EAAyBC,EAAuB,CAAC,GAAM,GAAI,EAAW,CAC1F,IAAIC,EAAM,EACV,GAAIF,EAAG,OAAQ,CACb,GAAIA,EAAG,OAAO,OAAS,EACrB,MAAM,IAAI,MAAM,mCAAmC,EAGrD,GADAE,EAAMF,EAAG,OAAO,WAAW,CAAC,EACxBE,EAAM,IAAQA,EAAM,GACtB,MAAM,IAAI,MAAM,sCAAsC,CAE1D,CACA,GAAIF,EAAG,cAAe,CACpB,GAAIA,EAAG,cAAc,OAAS,EAC5B,MAAM,IAAI,MAAM,+CAA+C,EAEjE,QAASvB,EAAI,EAAGA,EAAIuB,EAAG,cAAc,OAAQ,EAAEvB,EAAG,CAChD,IAAM0B,EAAeH,EAAG,cAAc,WAAWvB,CAAC,EAClD,GAAI,GAAO0B,GAAgBA,EAAe,GACxC,MAAM,IAAI,MAAM,4CAA4C,EAE9DD,IAAQ,EACRA,GAAOC,CACT,CACF,CACA,GAAIH,EAAG,MAAM,SAAW,EACtB,MAAM,IAAI,MAAM,6BAA6B,EAE/C,IAAMI,EAAYJ,EAAG,MAAM,WAAW,CAAC,EACvC,GAAIC,EAAW,CAAC,EAAIG,GAAaA,EAAYH,EAAW,CAAC,EACvD,MAAM,IAAI,MAAM,0BAA0BA,EAAW,CAAC,CAAC,OAAOA,EAAW,CAAC,CAAC,EAAE,EAE/E,OAAAC,IAAQ,EACRA,GAAOE,EAEAF,CACT,CAEO,cAAcR,EAAuB,CAC1C,IAAMQ,EAAgB,CAAC,EACvB,KAAOR,GACLQ,EAAI,KAAK,OAAO,aAAaR,EAAQ,GAAI,CAAC,EAC1CA,IAAU,EAEZ,OAAOQ,EAAI,QAAQ,EAAE,KAAK,EAAE,CAC9B,CAEO,gBAAgBG,EAAiC,CACtD,KAAK,cAAgBA,CACvB,CACO,mBAA0B,CAC/B,KAAK,cAAgB,KAAK,eAC5B,CAEO,mBAAmBL,EAAyBK,EAAsC,CACvF,IAAMX,EAAQ,KAAK,YAAYM,EAAI,CAAC,GAAM,GAAI,CAAC,EAC/C,KAAK,aAAaN,CAAK,IAAM,CAAC,EAC9B,IAAMY,EAAc,KAAK,aAAaZ,CAAK,EAC3C,OAAAY,EAAY,KAAKD,CAAO,EACjB,CACL,QAAS,IAAM,CACb,IAAME,EAAeD,EAAY,QAAQD,CAAO,EAC5CE,IAAiB,IACnBD,EAAY,OAAOC,EAAc,CAAC,CAEtC,CACF,CACF,CACO,gBAAgBP,EAA+B,CAChD,KAAK,aAAa,KAAK,YAAYA,EAAI,CAAC,GAAM,GAAI,CAAC,CAAC,GAAG,OAAO,KAAK,aAAa,KAAK,YAAYA,EAAI,CAAC,GAAM,GAAI,CAAC,CAAC,CACxH,CACO,sBAAsBK,EAAuC,CAClE,KAAK,cAAgBA,CACvB,CAEO,kBAAkBG,EAAcH,EAAmC,CACxE,IAAM/B,EAAOkC,EAAK,WAAW,CAAC,EAC9B,KAAK,iBAAiBlC,CAAI,EAAI+B,EAC1B/B,EAAO,KAAM,KAAK,oBAAoBA,CAAI,EAAI+B,EACpD,CACO,oBAAoBG,EAAoB,CAC7C,IAAMlC,EAAOkC,EAAK,WAAW,CAAC,EAC1B,KAAK,iBAAiBlC,CAAI,GAAG,OAAO,KAAK,iBAAiBA,CAAI,EAC9DA,EAAO,KAAM,KAAK,oBAAoBA,CAAI,EAAI,OACpD,CACO,0BAA0B+B,EAA2C,CAC1E,KAAK,kBAAoBA,CAC3B,CAEO,mBAAmBL,EAAyBK,EAAsC,CACvF,IAAMX,EAAQ,KAAK,YAAYM,CAAE,EACjC,KAAK,aAAaN,CAAK,IAAM,CAAC,EAC9B,IAAMY,EAAc,KAAK,aAAaZ,CAAK,EAC3C,OAAAY,EAAY,KAAKD,CAAO,EACjB,CACL,QAAS,IAAM,CACb,IAAME,EAAeD,EAAY,QAAQD,CAAO,EAC5CE,IAAiB,IACnBD,EAAY,OAAOC,EAAc,CAAC,CAEtC,CACF,CACF,CACO,gBAAgBP,EAA+B,CAChD,KAAK,aAAa,KAAK,YAAYA,CAAE,CAAC,GAAG,OAAO,KAAK,aAAa,KAAK,YAAYA,CAAE,CAAC,CAC5F,CACO,sBAAsBS,EAA0D,CACrF,KAAK,cAAgBA,CACvB,CAEO,mBAAmBT,EAAyBK,EAAmC,CACpF,OAAO,KAAK,WAAW,gBAAgB,KAAK,YAAYL,CAAE,EAAGK,CAAO,CACtE,CACO,gBAAgBL,EAA+B,CACpD,KAAK,WAAW,aAAa,KAAK,YAAYA,CAAE,CAAC,CACnD,CACO,sBAAsBK,EAAuC,CAClE,KAAK,WAAW,mBAAmBA,CAAO,CAC5C,CAEO,mBAAmBX,EAAeW,EAAmC,CAC1E,OAAO,KAAK,WAAW,gBAAgBX,EAAOW,CAAO,CACvD,CACO,gBAAgBX,EAAqB,CAC1C,KAAK,WAAW,aAAaA,CAAK,CACpC,CACO,sBAAsBW,EAAuC,CAClE,KAAK,WAAW,mBAAmBA,CAAO,CAC5C,CAEO,mBAAmBL,EAAyBK,EAAmC,CACpF,OAAAL,EAAG,OAAS,OACL,KAAK,WAAW,gBAAgB,KAAK,YAAYA,EAAI,CAAC,GAAM,GAAI,CAAC,EAAGK,CAAO,CACpF,CACO,gBAAgBL,EAA+B,CACpDA,EAAG,OAAS,OACZ,KAAK,WAAW,aAAa,KAAK,YAAYA,EAAI,CAAC,GAAM,GAAI,CAAC,CAAC,CACjE,CACO,sBAAsBK,EAAuC,CAClE,KAAK,WAAW,mBAAmBA,CAAO,CAC5C,CAEO,gBAAgBI,EAAyD,CAC9E,KAAK,cAAgBA,CACvB,CACO,mBAA0B,CAC/B,KAAK,cAAgB,KAAK,eAC5B,CAWO,OAAc,CACnB,KAAK,aAAe,KAAK,aACzB,KAAK,WAAW,MAAM,EACtB,KAAK,WAAW,MAAM,EACtB,KAAK,WAAW,MAAM,EACtB,KAAK,QAAQ,SAAS,EACtB,KAAK,SAAW,EAChB,KAAK,mBAAqB,EAItB,KAAK,YAAY,QAAU,IAC7B,KAAK,YAAY,MAAQ,EACzB,KAAK,YAAY,SAAW,CAAC,EAEjC,CAKU,eACRlC,EACAmC,EACAC,EACAC,EACAC,EACM,CACN,KAAK,YAAY,MAAQtC,EACzB,KAAK,YAAY,SAAWmC,EAC5B,KAAK,YAAY,WAAaC,EAC9B,KAAK,YAAY,WAAaC,EAC9B,KAAK,YAAY,SAAWC,CAC9B,CA+CO,MAAMpB,EAAmBtB,EAAgB2C,EAAkD,CAChG,IAAIxC,EACAsC,EACA5B,EAAQ,EACR+B,EAGJ,GAAI,KAAK,YAAY,MAGnB,GAAI,KAAK,YAAY,QAAU,EAC7B,KAAK,YAAY,MAAQ,EACzB/B,EAAQ,KAAK,YAAY,SAAW,MAC/B,CACL,GAAI8B,IAAkB,QAAa,KAAK,YAAY,QAAU,EAgB5D,WAAK,YAAY,MAAQ,EACnB,IAAI,MAAM,wEAAwE,EAM1F,IAAMJ,EAAW,KAAK,YAAY,SAC9BC,EAAa,KAAK,YAAY,WAAa,EAC/C,OAAQ,KAAK,YAAY,MAAO,CAC9B,OACE,GAAIG,IAAkB,IAASH,EAAa,IAC1C,KAAOA,GAAc,IACnBI,EAAiBL,EAA8BC,CAAU,EAAE,KAAK,OAAO,EACnEI,IAAkB,IAFAJ,IAIf,GAAII,aAAyB,QAClC,YAAK,YAAY,WAAaJ,EACvBI,EAIb,KAAK,YAAY,SAAW,CAAC,EAC7B,MACF,OACE,GAAID,IAAkB,IAASH,EAAa,IAC1C,KAAOA,GAAc,IACnBI,EAAiBL,EAA8BC,CAAU,EAAE,EACvDI,IAAkB,IAFAJ,IAIf,GAAII,aAAyB,QAClC,YAAK,YAAY,WAAaJ,EACvBI,EAIb,KAAK,YAAY,SAAW,CAAC,EAC7B,MACF,OAGE,GAFAzC,EAAOmB,EAAK,KAAK,YAAY,QAAQ,EACrCsB,EAAgB,KAAK,WAAW,OAAOzC,IAAS,IAAQA,IAAS,GAAMwC,CAAa,EAChFC,EACF,OAAOA,EAELzC,IAAS,KAAM,KAAK,YAAY,YAAc,GAClD,KAAK,QAAQ,SAAS,EACtB,KAAK,SAAW,EAChB,MACF,OAGE,GAFAA,EAAOmB,EAAK,KAAK,YAAY,QAAQ,EACrCsB,EAAgB,KAAK,WAAW,IAAIzC,IAAS,IAAQA,IAAS,GAAMwC,CAAa,EAC7EC,EACF,OAAOA,EAELzC,IAAS,KAAM,KAAK,YAAY,YAAc,GAClD,KAAK,QAAQ,SAAS,EACtB,KAAK,SAAW,EAChB,MACF,OAGE,GAFAA,EAAOmB,EAAK,KAAK,YAAY,QAAQ,EACrCsB,EAAgB,KAAK,WAAW,IAAIzC,IAAS,IAAQA,IAAS,GAAMwC,CAAa,EAC7EC,EACF,OAAOA,EAELzC,IAAS,KAAM,KAAK,YAAY,YAAc,GAClD,KAAK,QAAQ,SAAS,EACtB,KAAK,SAAW,EAChB,KACJ,CAEA,KAAK,YAAY,MAAQ,EACzBU,EAAQ,KAAK,YAAY,SAAW,EACpC,KAAK,mBAAqB,EAC1B,KAAK,aAAe,KAAK,YAAY,WAAa,GACpD,CAMF,QAASP,EAAIO,EAAOP,EAAIN,EAAQ,EAAEM,EAAG,CAInC,GAHAH,EAAOmB,EAAKhB,CAAC,EAGTH,EAAO,IAAQ,KAAK,cAAgB,EAAwB,EAC7D,KAAK,oBAAoBA,CAAI,GAAK,KAAK,mBAAmBA,CAAI,EAC/D,KAAK,mBAAqB,EAC1B,QACF,CAGA,GAAIA,IAAS,IACR,KAAK,aAAe,GACpBG,EAAI,EAAIN,GAAUsB,EAAKhB,EAAI,CAAC,IAAM,GACrC,CACA,KAAK,QAAQ,SAAS,EACtB,KAAK,SAAW,EAChB,IAAIuC,EAAIvC,EAAI,EACRwC,EAAKxB,EAAKuB,CAAC,EACXC,GAAM,IAAQA,GAAM,KACtB,KAAK,SAAWA,EAChBD,KAEF,IAAIE,EAAU,GACd,KAAOF,EAAI7C,EAAQ6C,IAEjB,GADAC,EAAKxB,EAAKuB,CAAC,EACPC,GAAM,IAAQA,GAAM,GACtB,KAAK,QAAQ,SAASA,EAAK,EAAE,UACpBA,IAAO,GAChB,KAAK,QAAQ,SAAS,CAAC,UACdA,IAAO,GAChB,KAAK,QAAQ,YAAY,EAAE,UAClBA,GAAM,IAAQA,GAAM,IAAM,CACnC,IAAMP,EAAW,KAAK,aAAa,KAAK,UAAY,EAAIO,CAAE,EACtDE,EAAIT,EAAWA,EAAS,OAAS,EAAI,GACzC,KAAOS,GAAK,IACVJ,EAAgBL,EAASS,CAAC,EAAE,KAAK,OAAO,EACpCJ,IAAkB,IAFTI,IAIN,GAAIJ,aAAyB,QAClC,OAAAH,EAAa,KACb,KAAK,iBAAoCF,EAAUS,EAAGP,EAAYI,CAAC,EAC5DD,EAGPI,EAAI,GACN,KAAK,cAAc,KAAK,UAAY,EAAIF,EAAI,KAAK,OAAO,EAE1D,KAAK,mBAAqB,EAC1BxC,EAAIuC,EACJ,KAAK,aAAe,EACpBE,EAAU,GACV,KACF,KACE,OAGCA,IACHzC,EAAIuC,EAAI,EACR,KAAK,aAAe,GAEtB,QACF,CAOA,OAJAJ,EAAa,KAAK,aAAa,MAC7B,KAAK,cAAgB,GACpBtC,EAAOI,EAAsBJ,EAAOI,EACvC,EACQkC,GAAc,EAAqC,CACzD,OAEE,IAAIQ,EAAI3C,EACF4C,EAAKlD,EAAS,EACpB,KAAOiD,EAAIC,GACN5B,EAAK,EAAE2B,CAAC,GAAK,KAAS3B,EAAK2B,CAAC,GAAK,KAAQ3B,EAAK2B,CAAC,GAAK1C,IACpDe,EAAK,EAAE2B,CAAC,GAAK,KAAS3B,EAAK2B,CAAC,GAAK,KAAQ3B,EAAK2B,CAAC,GAAK1C,IACpDe,EAAK,EAAE2B,CAAC,GAAK,KAAS3B,EAAK2B,CAAC,GAAK,KAAQ3B,EAAK2B,CAAC,GAAK1C,IACpDe,EAAK,EAAE2B,CAAC,GAAK,KAAS3B,EAAK2B,CAAC,GAAK,KAAQ3B,EAAK2B,CAAC,GAAK1C,IACvD,CACF,GAAI0C,GAAKC,EACP,KAAOD,EAAIjD,GAAUsB,EAAK2B,CAAC,GAAK,KAAS3B,EAAK2B,CAAC,GAAK,KAAQ3B,EAAK2B,CAAC,GAAK1C,IACrE0C,IAGJ,KAAK,cAAc3B,EAAMhB,EAAG2C,CAAC,EAC7B3C,EAAI2C,EAAI,EACR,MACF,OACM,KAAK,iBAAiB9C,CAAI,EAAG,KAAK,iBAAiBA,CAAI,EAAE,EACxD,KAAK,kBAAkBA,CAAI,EAChC,KAAK,mBAAqB,EAC1B,MACF,OACE,MACF,OAUE,GAT8B,KAAK,cACjC,CACE,SAAUG,EACV,KAAAH,EACA,aAAc,KAAK,aACnB,QAAS,KAAK,SACd,OAAQ,KAAK,QACb,MAAO,EACT,CAAC,EACQ,MAAO,OAElB,MACF,OAEE,IAAMoC,EAAW,KAAK,aAAa,KAAK,UAAY,EAAIpC,CAAI,EACxD6C,EAAIT,EAAWA,EAAS,OAAS,EAAI,GACzC,KAAOS,GAAK,IAGVJ,EAAgBL,EAASS,CAAC,EAAE,KAAK,OAAO,EACpCJ,IAAkB,IAJTI,IAMN,GAAIJ,aAAyB,QAClC,YAAK,iBAAoCL,EAAUS,EAAGP,EAAYnC,CAAC,EAC5DsC,EAGPI,EAAI,GACN,KAAK,cAAc,KAAK,UAAY,EAAI7C,EAAM,KAAK,OAAO,EAE5D,KAAK,mBAAqB,EAC1B,MACF,OAEE,EACE,QAAQA,EAAM,CACZ,IAAK,IACH,KAAK,QAAQ,SAAS,CAAC,EACvB,MACF,IAAK,IACH,KAAK,QAAQ,YAAY,EAAE,EAC3B,MACF,QACE,KAAK,QAAQ,SAASA,EAAO,EAAE,CACnC,OACO,EAAEG,EAAIN,IAAWG,EAAOmB,EAAKhB,CAAC,GAAK,IAAQH,EAAO,IAC3DG,IACA,MACF,OACE,KAAK,WAAa,EAClB,KAAK,UAAYH,EACjB,MACF,QACE,IAAMgD,EAAc,KAAK,aAAa,KAAK,UAAY,EAAIhD,CAAI,EAC3DiD,EAAKD,EAAcA,EAAY,OAAS,EAAI,GAChD,KAAOC,GAAM,IAGXR,EAAgBO,EAAYC,CAAE,EAAE,EAC5BR,IAAkB,IAJRQ,IAMP,GAAIR,aAAyB,QAClC,YAAK,iBAAoCO,EAAaC,EAAIX,EAAYnC,CAAC,EAChEsC,EAGPQ,EAAK,GACP,KAAK,cAAc,KAAK,UAAY,EAAIjD,CAAI,EAE9C,KAAK,mBAAqB,EAC1B,MACF,QACE,KAAK,QAAQ,SAAS,EACtB,KAAK,SAAW,EAChB,MACF,QACE,KAAK,WAAW,KAAK,KAAK,UAAY,EAAIA,EAAM,KAAK,OAAO,EAC5D,MACF,QAGE,QAAS6C,EAAI1C,EAAI,GAAK,EAAE0C,EACtB,GAAIA,GAAKhD,IAAWG,EAAOmB,EAAK0B,CAAC,KAAO,IAAQ7C,IAAS,IAAQA,IAAS,IAASA,EAAO,KAAQA,EAAOI,EAAsB,CAC7H,KAAK,WAAW,IAAIe,EAAMhB,EAAG0C,CAAC,EAC9B1C,EAAI0C,EAAI,EACR,KACF,CAEF,MACF,QAEE,GADAJ,EAAgB,KAAK,WAAW,OAAOzC,IAAS,IAAQA,IAAS,EAAI,EACjEyC,EACF,YAAK,iBAAoC,CAAC,EAAG,EAAGH,EAAYnC,CAAC,EACtDsC,EAELzC,IAAS,KAAMsC,GAAc,GACjC,KAAK,QAAQ,SAAS,EACtB,KAAK,SAAW,EAChB,KAAK,mBAAqB,EAC1B,MACF,OACE,KAAK,WAAW,MAAM,EACtB,MACF,OAEE,QAASO,EAAI1C,EAAI,GAAK0C,IACpB,GAAIA,GAAKhD,IAAWG,EAAOmB,EAAK0B,CAAC,GAAK,IAAS7C,EAAO,KAAQA,EAAOI,EAAsB,CACzF,KAAK,WAAW,IAAIe,EAAMhB,EAAG0C,CAAC,EAC9B1C,EAAI0C,EAAI,EACR,KACF,CAEF,MACF,OAEE,GADAJ,EAAgB,KAAK,WAAW,IAAIzC,IAAS,IAAQA,IAAS,EAAI,EAC9DyC,EACF,YAAK,iBAAoC,CAAC,EAAG,EAAGH,EAAYnC,CAAC,EACtDsC,EAELzC,IAAS,KAAMsC,GAAc,GACjC,KAAK,QAAQ,SAAS,EACtB,KAAK,SAAW,EAChB,KAAK,mBAAqB,EAC1B,MACF,QACE,KAAK,WAAW,MAAM,KAAK,UAAY,EAAItC,CAAI,EAC/C,MACF,QAGE,QAAS6C,EAAI1C,EAAI,GAAK,EAAE0C,EACtB,GAAI,EAAAA,EAAIhD,IACLsB,EAAK0B,CAAC,GAAK,IAAQ1B,EAAK0B,CAAC,EAAI,KAAU1B,EAAK0B,CAAC,GAAK,GAAQ1B,EAAK0B,CAAC,EAAI,IAAS1B,EAAK0B,CAAC,GAAKzC,IAE3F,MAAK,WAAW,IAAIe,EAAMhB,EAAG0C,CAAC,EAC9B1C,EAAI0C,EAAI,EACR,MAEF,MACF,QAEE,GADAJ,EAAgB,KAAK,WAAW,IAAIzC,IAAS,IAAQA,IAAS,EAAI,EAC9DyC,EACF,YAAK,iBAAoC,CAAC,EAAG,EAAGH,EAAYnC,CAAC,EACtDsC,EAELzC,IAAS,KAAMsC,GAAc,GACjC,KAAK,QAAQ,SAAS,EACtB,KAAK,SAAW,EAChB,KAAK,mBAAqB,EAC1B,KACJ,CACA,KAAK,aAAeA,EAAa,GACnC,CACF,CACF,EC95BA,IAAMY,GAAU,qKAEVC,GAAW,aAaV,SAASC,GAAWC,EAAoD,CAC7E,GAAI,CAACA,EAAM,OAEX,IAAIC,EAAMD,EAAK,YAAY,EAC3B,GAAIC,EAAI,WAAW,MAAM,EAAG,CAE1BA,EAAMA,EAAI,MAAM,CAAC,EACjB,IAAMC,EAAIL,GAAQ,KAAKI,CAAG,EAC1B,GAAIC,EAAG,CACL,IAAMC,EAAOD,EAAE,CAAC,EAAI,GAAKA,EAAE,CAAC,EAAI,IAAMA,EAAE,CAAC,EAAI,KAAO,MACpD,MAAO,CACL,KAAK,MAAM,SAASA,EAAE,CAAC,GAAKA,EAAE,CAAC,GAAKA,EAAE,CAAC,GAAKA,EAAE,EAAE,EAAG,EAAE,EAAIC,EAAO,GAAG,EACnE,KAAK,MAAM,SAASD,EAAE,CAAC,GAAKA,EAAE,CAAC,GAAKA,EAAE,CAAC,GAAKA,EAAE,EAAE,EAAG,EAAE,EAAIC,EAAO,GAAG,EACnE,KAAK,MAAM,SAASD,EAAE,CAAC,GAAKA,EAAE,CAAC,GAAKA,EAAE,CAAC,GAAKA,EAAE,EAAE,EAAG,EAAE,EAAIC,EAAO,GAAG,CACrE,CACF,CACF,SAAWF,EAAI,WAAW,GAAG,IAE3BA,EAAMA,EAAI,MAAM,CAAC,EACbH,GAAS,KAAKG,CAAG,GAAK,CAAC,EAAG,EAAG,EAAG,EAAE,EAAE,SAASA,EAAI,MAAM,GAAG,CAC5D,IAAMG,EAAMH,EAAI,OAAS,EACnBI,EAAmC,CAAC,EAAG,EAAG,CAAC,EACjD,QAASC,EAAI,EAAGA,EAAI,EAAG,EAAEA,EAAG,CAC1B,IAAMC,EAAI,SAASN,EAAI,MAAMG,EAAME,EAAGF,EAAME,EAAIF,CAAG,EAAG,EAAE,EACxDC,EAAOC,CAAC,EAAIF,IAAQ,EAAIG,GAAK,EAAIH,IAAQ,EAAIG,EAAIH,IAAQ,EAAIG,GAAK,EAAIA,GAAK,CAC7E,CACA,OAAOF,CACT,CAMJ,CC/CO,IAAMG,GAAgB,QCsB7B,IAAMC,GAAoC,CAAE,IAAK,EAAG,IAAK,EAAG,IAAK,EAAG,IAAK,EAAG,IAAK,EAAG,IAAK,CAAE,EAsB3F,SAASC,GAAoB,EAAWC,EAA+B,CACrE,GAAI,EAAI,GACN,OAAOA,EAAK,aAAe,GAE7B,OAAQ,EAAG,CACT,IAAK,GAAG,MAAO,CAAC,CAACA,EAAK,WACtB,IAAK,GAAG,MAAO,CAAC,CAACA,EAAK,YACtB,IAAK,GAAG,MAAO,CAAC,CAACA,EAAK,eACtB,IAAK,GAAG,MAAO,CAAC,CAACA,EAAK,iBACtB,IAAK,GAAG,MAAO,CAAC,CAACA,EAAK,SACtB,IAAK,GAAG,MAAO,CAAC,CAACA,EAAK,SACtB,IAAK,GAAG,MAAO,CAAC,CAACA,EAAK,WACtB,IAAK,GAAG,MAAO,CAAC,CAACA,EAAK,gBACtB,IAAK,GAAG,MAAO,CAAC,CAACA,EAAK,YACtB,IAAK,IAAI,MAAO,CAAC,CAACA,EAAK,cACvB,IAAK,IAAI,MAAO,CAAC,CAACA,EAAK,YACvB,IAAK,IAAI,MAAO,CAAC,CAACA,EAAK,eACvB,IAAK,IAAI,MAAO,CAAC,CAACA,EAAK,iBACvB,IAAK,IAAI,MAAO,CAAC,CAACA,EAAK,oBACvB,IAAK,IAAI,MAAO,CAAC,CAACA,EAAK,kBACvB,IAAK,IAAI,MAAO,CAAC,CAACA,EAAK,gBACvB,IAAK,IAAI,MAAO,CAAC,CAACA,EAAK,mBACvB,IAAK,IAAI,MAAO,CAAC,CAACA,EAAK,aACvB,IAAK,IAAI,MAAO,CAAC,CAACA,EAAK,YACvB,IAAK,IAAI,MAAO,CAAC,CAACA,EAAK,UACvB,IAAK,IAAI,MAAO,CAAC,CAACA,EAAK,SACvB,IAAK,IAAI,MAAO,CAAC,CAACA,EAAK,WACzB,CACA,MAAO,EACT,CAQA,IAAIC,GAAQ,EASCC,GAAN,cAA2BC,CAAoC,CAsDpE,YACmBC,EACAC,EACAC,EACAC,EACAC,EACAC,EACAC,EACAC,EACAC,EAAiC,IAAIC,GACtD,CACA,MAAM,EAVW,oBAAAT,EACA,qBAAAC,EACA,kBAAAC,EACA,iBAAAC,EACA,qBAAAC,EACA,qBAAAC,EACA,wBAAAC,EACA,qBAAAC,EACA,aAAAC,EA9DnB,KAAQ,aAA4B,IAAI,YAAY,IAAI,EACxD,KAAQ,eAAgC,IAAIE,GAC5C,KAAQ,aAA4B,IAAIC,GACxC,KAAQ,aAAe,GACvB,KAAQ,UAAY,GAEpB,KAAU,kBAA8B,CAAC,EACzC,KAAU,eAA2B,CAAC,EAEtC,KAAQ,aAA+BC,EAAkB,MAAM,EAE/D,KAAQ,uBAAyCA,EAAkB,MAAM,EAIzE,KAAiB,eAAiB,KAAK,UAAU,IAAIC,CAAe,EACpE,KAAgB,cAAgB,KAAK,eAAe,MACpD,KAAiB,sBAAwB,KAAK,UAAU,IAAIA,CAAqD,EACjH,KAAgB,qBAAuB,KAAK,sBAAsB,MAClE,KAAiB,gBAAkB,KAAK,UAAU,IAAIA,CAAe,EACrE,KAAgB,eAAiB,KAAK,gBAAgB,MACtD,KAAiB,oBAAsB,KAAK,UAAU,IAAIA,CAAe,EACzE,KAAgB,mBAAqB,KAAK,oBAAoB,MAC9D,KAAiB,wBAA0B,KAAK,UAAU,IAAIA,CAAe,EAC7E,KAAgB,uBAAyB,KAAK,wBAAwB,MACtE,KAAiB,+BAAiC,KAAK,UAAU,IAAIA,CAAmC,EACxG,KAAgB,8BAAgC,KAAK,+BAA+B,MAEpF,KAAiB,YAAc,KAAK,UAAU,IAAIA,CAAiB,EACnE,KAAgB,WAAa,KAAK,YAAY,MAC9C,KAAiB,WAAa,KAAK,UAAU,IAAIA,CAAiB,EAClE,KAAgB,UAAY,KAAK,WAAW,MAC5C,KAAiB,cAAgB,KAAK,UAAU,IAAIA,CAAe,EACnE,KAAgB,aAAe,KAAK,cAAc,MAClD,KAAiB,YAAc,KAAK,UAAU,IAAIA,CAAe,EACjE,KAAgB,WAAa,KAAK,YAAY,MAC9C,KAAiB,UAAY,KAAK,UAAU,IAAIA,CAAiB,EACjE,KAAgB,SAAW,KAAK,UAAU,MAC1C,KAAiB,eAAiB,KAAK,UAAU,IAAIA,CAAiB,EACtE,KAAgB,cAAgB,KAAK,eAAe,MACpD,KAAiB,SAAW,KAAK,UAAU,IAAIA,CAAsB,EACrE,KAAgB,QAAU,KAAK,SAAS,MACxC,KAAiB,2BAA6B,KAAK,UAAU,IAAIA,CAAe,EAChF,KAAgB,0BAA4B,KAAK,2BAA2B,MAE5E,KAAQ,YAA2B,CACjC,OAAQ,GACR,aAAc,EACd,aAAc,EACd,cAAe,EACf,SAAU,CACZ,EAm8FA,KAAQ,eAAiB,YAAqF,EAr7F5G,KAAK,UAAU,KAAK,OAAO,EAC3B,KAAK,iBAAmB,IAAIC,GAAgB,KAAK,cAAc,EAG/D,KAAK,cAAgB,KAAK,eAAe,OACzC,KAAK,UAAU,KAAK,eAAe,QAAQ,iBAAiBC,GAAK,KAAK,cAAgBA,EAAE,YAAY,CAAC,EAKrG,KAAK,QAAQ,sBAAsB,CAACC,EAAOC,IAAW,CACpD,KAAK,YAAY,MAAM,qBAAsB,CAAE,WAAY,KAAK,QAAQ,cAAcD,CAAK,EAAG,OAAQC,EAAO,QAAQ,CAAE,CAAC,CAC1H,CAAC,EACD,KAAK,QAAQ,sBAAsBD,GAAS,CAC1C,KAAK,YAAY,MAAM,qBAAsB,CAAE,WAAY,KAAK,QAAQ,cAAcA,CAAK,CAAE,CAAC,CAChG,CAAC,EACD,KAAK,QAAQ,0BAA0BE,GAAQ,CAC7C,KAAK,YAAY,MAAM,yBAA0B,CAAE,KAAAA,CAAK,CAAC,CAC3D,CAAC,EACD,KAAK,QAAQ,sBAAsB,CAACC,EAAYC,EAAQC,IAAS,CAC/D,KAAK,YAAY,MAAM,qBAAsB,CAAE,WAAAF,EAAY,OAAAC,EAAQ,KAAAC,CAAK,CAAC,CAC3E,CAAC,EACD,KAAK,QAAQ,sBAAsB,CAACL,EAAOI,EAAQE,IAAY,CACzDF,IAAW,SACbE,EAAUA,EAAQ,QAAQ,GAE5B,KAAK,YAAY,MAAM,qBAAsB,CAAE,WAAY,KAAK,QAAQ,cAAcN,CAAK,EAAG,OAAAI,EAAQ,QAAAE,CAAQ,CAAC,CACjH,CAAC,EACD,KAAK,QAAQ,sBAAsB,CAACN,EAAOI,EAAQE,IAAY,CAC7D,KAAK,YAAY,MAAM,qBAAsB,CAAE,WAAY,KAAK,QAAQ,cAAcN,CAAK,EAAG,OAAAI,EAAQ,QAAAE,CAAQ,CAAC,CACjH,CAAC,EAKD,KAAK,QAAQ,gBAAgB,CAACD,EAAME,EAAOC,IAAQ,KAAK,MAAMH,EAAME,EAAOC,CAAG,CAAC,EAK/E,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAGP,GAAU,KAAK,YAAYA,CAAM,CAAC,EAClF,KAAK,QAAQ,mBAAmB,CAAE,cAAe,IAAK,MAAO,GAAI,EAAGA,GAAU,KAAK,WAAWA,CAAM,CAAC,EACrG,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAGA,GAAU,KAAK,SAASA,CAAM,CAAC,EAC/E,KAAK,QAAQ,mBAAmB,CAAE,cAAe,IAAK,MAAO,GAAI,EAAGA,GAAU,KAAK,YAAYA,CAAM,CAAC,EACtG,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAGA,GAAU,KAAK,WAAWA,CAAM,CAAC,EACjF,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAGA,GAAU,KAAK,cAAcA,CAAM,CAAC,EACpF,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAGA,GAAU,KAAK,eAAeA,CAAM,CAAC,EACrF,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAGA,GAAU,KAAK,eAAeA,CAAM,CAAC,EACrF,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAGA,GAAU,KAAK,oBAAoBA,CAAM,CAAC,EAC1F,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAGA,GAAU,KAAK,mBAAmBA,CAAM,CAAC,EACzF,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAGA,GAAU,KAAK,eAAeA,CAAM,CAAC,EACrF,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAGA,GAAU,KAAK,iBAAiBA,CAAM,CAAC,EACvF,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAGA,GAAU,KAAK,eAAeA,EAAQ,EAAK,CAAC,EAC5F,KAAK,QAAQ,mBAAmB,CAAE,OAAQ,IAAK,MAAO,GAAI,EAAGA,GAAU,KAAK,eAAeA,EAAQ,EAAI,CAAC,EACxG,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAGA,GAAU,KAAK,YAAYA,EAAQ,EAAK,CAAC,EACzF,KAAK,QAAQ,mBAAmB,CAAE,OAAQ,IAAK,MAAO,GAAI,EAAGA,GAAU,KAAK,YAAYA,EAAQ,EAAI,CAAC,EACrG,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAGA,GAAU,KAAK,YAAYA,CAAM,CAAC,EAClF,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAGA,GAAU,KAAK,YAAYA,CAAM,CAAC,EAClF,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAGA,GAAU,KAAK,YAAYA,CAAM,CAAC,EAClF,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAGA,GAAU,KAAK,SAASA,CAAM,CAAC,EAC/E,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAGA,GAAU,KAAK,WAAWA,CAAM,CAAC,EACjF,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAGA,GAAU,KAAK,WAAWA,CAAM,CAAC,EACjF,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAGA,GAAU,KAAK,kBAAkBA,CAAM,CAAC,EACxF,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAGA,GAAU,KAAK,WAAWA,CAAM,CAAC,EACjF,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAGA,GAAU,KAAK,gBAAgBA,CAAM,CAAC,EACtF,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAGA,GAAU,KAAK,kBAAkBA,CAAM,CAAC,EACxF,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAGA,GAAU,KAAK,yBAAyBA,CAAM,CAAC,EAC/F,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAGA,GAAU,KAAK,4BAA4BA,CAAM,CAAC,EAClG,KAAK,QAAQ,mBAAmB,CAAE,OAAQ,IAAK,MAAO,GAAI,EAAGA,GAAU,KAAK,8BAA8BA,CAAM,CAAC,EACjH,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAGA,GAAU,KAAK,gBAAgBA,CAAM,CAAC,EACtF,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAGA,GAAU,KAAK,kBAAkBA,CAAM,CAAC,EACxF,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAGA,GAAU,KAAK,WAAWA,CAAM,CAAC,EACjF,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAGA,GAAU,KAAK,SAASA,CAAM,CAAC,EAC/E,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAGA,GAAU,KAAK,QAAQA,CAAM,CAAC,EAC9E,KAAK,QAAQ,mBAAmB,CAAE,OAAQ,IAAK,MAAO,GAAI,EAAGA,GAAU,KAAK,eAAeA,CAAM,CAAC,EAClG,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAGA,GAAU,KAAK,UAAUA,CAAM,CAAC,EAChF,KAAK,QAAQ,mBAAmB,CAAE,OAAQ,IAAK,MAAO,GAAI,EAAGA,GAAU,KAAK,iBAAiBA,CAAM,CAAC,EACpG,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAGA,GAAU,KAAK,eAAeA,CAAM,CAAC,EACrF,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAGA,GAAU,KAAK,aAAaA,CAAM,CAAC,EACnF,KAAK,QAAQ,mBAAmB,CAAE,OAAQ,IAAK,MAAO,GAAI,EAAGA,GAAU,KAAK,oBAAoBA,CAAM,CAAC,EACvG,KAAK,QAAQ,mBAAmB,CAAE,cAAe,IAAK,MAAO,GAAI,EAAGA,GAAU,KAAK,UAAUA,CAAM,CAAC,EACpG,KAAK,QAAQ,mBAAmB,CAAE,OAAQ,IAAK,MAAO,GAAI,EAAGA,GAAU,KAAK,cAAcA,CAAM,CAAC,EACjG,KAAK,QAAQ,mBAAmB,CAAE,cAAe,IAAK,MAAO,GAAI,EAAGA,GAAU,KAAK,eAAeA,CAAM,CAAC,EACzG,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAGA,GAAU,KAAK,gBAAgBA,CAAM,CAAC,EACtF,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAGA,GAAU,KAAK,WAAWA,CAAM,CAAC,EACjF,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAGA,GAAU,KAAK,cAAcA,CAAM,CAAC,EACpF,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAGA,GAAU,KAAK,cAAcA,CAAM,CAAC,EACpF,KAAK,QAAQ,mBAAmB,CAAE,cAAe,IAAM,MAAO,GAAI,EAAGA,GAAU,KAAK,cAAcA,CAAM,CAAC,EACzG,KAAK,QAAQ,mBAAmB,CAAE,cAAe,IAAM,MAAO,GAAI,EAAGA,GAAU,KAAK,cAAcA,CAAM,CAAC,EACzG,KAAK,QAAQ,mBAAmB,CAAE,cAAe,IAAK,MAAO,GAAI,EAAGA,GAAU,KAAK,gBAAgBA,CAAM,CAAC,EAC1G,KAAK,QAAQ,mBAAmB,CAAE,cAAe,IAAK,MAAO,GAAI,EAAGA,GAAU,KAAK,YAAYA,EAAQ,EAAI,CAAC,EAC5G,KAAK,QAAQ,mBAAmB,CAAE,OAAQ,IAAK,cAAe,IAAK,MAAO,GAAI,EAAGA,GAAU,KAAK,YAAYA,EAAQ,EAAK,CAAC,EAG1H,KAAK,QAAQ,mBAAmB,CAAE,OAAQ,IAAK,MAAO,GAAI,EAAGA,GAAU,KAAK,iBAAiBA,CAAM,CAAC,EACpG,KAAK,QAAQ,mBAAmB,CAAE,OAAQ,IAAK,MAAO,GAAI,EAAGA,GAAU,KAAK,mBAAmBA,CAAM,CAAC,EACtG,KAAK,QAAQ,mBAAmB,CAAE,OAAQ,IAAK,MAAO,GAAI,EAAGA,GAAU,KAAK,kBAAkBA,CAAM,CAAC,EACrG,KAAK,QAAQ,mBAAmB,CAAE,OAAQ,IAAK,MAAO,GAAI,EAAGA,GAAU,KAAK,iBAAiBA,CAAM,CAAC,EAKpG,KAAK,QAAQ,yBAA0B,IAAM,KAAK,KAAK,CAAC,EACxD,KAAK,QAAQ;AAAA,EAAyB,IAAM,KAAK,SAAS,CAAC,EAC3D,KAAK,QAAQ,uBAAyB,IAAM,KAAK,SAAS,CAAC,EAC3D,KAAK,QAAQ,uBAAyB,IAAM,KAAK,SAAS,CAAC,EAC3D,KAAK,QAAQ,uBAAyB,IAAM,KAAK,eAAe,CAAC,EACjE,KAAK,QAAQ,uBAAyB,IAAM,KAAK,UAAU,CAAC,EAC5D,KAAK,QAAQ,sBAAyB,IAAM,KAAK,IAAI,CAAC,EACtD,KAAK,QAAQ,sBAAyB,IAAM,KAAK,SAAS,CAAC,EAC3D,KAAK,QAAQ,sBAAyB,IAAM,KAAK,QAAQ,CAAC,EAG1D,KAAK,QAAQ,yBAA0B,IAAM,KAAK,MAAM,CAAC,EACzD,KAAK,QAAQ,yBAA0B,IAAM,KAAK,SAAS,CAAC,EAC5D,KAAK,QAAQ,yBAA0B,IAAM,KAAK,OAAO,CAAC,EAM1D,KAAK,QAAQ,mBAAmB,EAAG,IAAIQ,EAAWJ,IAAU,KAAK,SAASA,CAAI,EAAG,KAAK,YAAYA,CAAI,EAAU,GAAO,CAAC,EAExH,KAAK,QAAQ,mBAAmB,EAAG,IAAII,EAAWJ,GAAQ,KAAK,YAAYA,CAAI,CAAC,CAAC,EAEjF,KAAK,QAAQ,mBAAmB,EAAG,IAAII,EAAWJ,GAAQ,KAAK,SAASA,CAAI,CAAC,CAAC,EAG9E,KAAK,QAAQ,mBAAmB,EAAG,IAAII,EAAWJ,GAAQ,KAAK,wBAAwBA,CAAI,CAAC,CAAC,EAK7F,KAAK,QAAQ,mBAAmB,EAAG,IAAII,EAAWJ,GAAQ,KAAK,aAAaA,CAAI,CAAC,CAAC,EAElF,KAAK,QAAQ,mBAAmB,GAAI,IAAII,EAAWJ,GAAQ,KAAK,mBAAmBA,CAAI,CAAC,CAAC,EAEzF,KAAK,QAAQ,mBAAmB,GAAI,IAAII,EAAWJ,GAAQ,KAAK,mBAAmBA,CAAI,CAAC,CAAC,EAEzF,KAAK,QAAQ,mBAAmB,GAAI,IAAII,EAAWJ,GAAQ,KAAK,uBAAuBA,CAAI,CAAC,CAAC,EAa7F,KAAK,QAAQ,mBAAmB,IAAK,IAAII,EAAWJ,GAAQ,KAAK,oBAAoBA,CAAI,CAAC,CAAC,EAI3F,KAAK,QAAQ,mBAAmB,IAAK,IAAII,EAAWJ,GAAQ,KAAK,eAAeA,CAAI,CAAC,CAAC,EAEtF,KAAK,QAAQ,mBAAmB,IAAK,IAAII,EAAWJ,GAAQ,KAAK,eAAeA,CAAI,CAAC,CAAC,EAEtF,KAAK,QAAQ,mBAAmB,IAAK,IAAII,EAAWJ,GAAQ,KAAK,mBAAmBA,CAAI,CAAC,CAAC,EAY1F,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAG,IAAM,KAAK,WAAW,CAAC,EACvE,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAG,IAAM,KAAK,cAAc,CAAC,EAC1E,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAG,IAAM,KAAK,MAAM,CAAC,EAClE,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAG,IAAM,KAAK,SAAS,CAAC,EACrE,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAG,IAAM,KAAK,OAAO,CAAC,EACnE,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAG,IAAM,KAAK,aAAa,CAAC,EACzE,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAG,IAAM,KAAK,sBAAsB,CAAC,EAClF,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAG,IAAM,KAAK,kBAAkB,CAAC,EAC9E,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAG,IAAM,KAAK,UAAU,CAAC,EACtE,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAG,IAAM,KAAK,UAAU,CAAC,CAAC,EACvE,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAG,IAAM,KAAK,UAAU,CAAC,CAAC,EACvE,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAG,IAAM,KAAK,UAAU,CAAC,CAAC,EACvE,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAG,IAAM,KAAK,UAAU,CAAC,CAAC,EACvE,KAAK,QAAQ,mBAAmB,CAAE,MAAO,GAAI,EAAG,IAAM,KAAK,UAAU,CAAC,CAAC,EACvE,KAAK,QAAQ,mBAAmB,CAAE,cAAe,IAAK,MAAO,GAAI,EAAG,IAAM,KAAK,qBAAqB,CAAC,EACrG,KAAK,QAAQ,mBAAmB,CAAE,cAAe,IAAK,MAAO,GAAI,EAAG,IAAM,KAAK,qBAAqB,CAAC,EACrG,QAAWK,KAAQC,EACjB,KAAK,QAAQ,mBAAmB,CAAE,cAAe,IAAK,MAAOD,CAAK,EAAG,IAAM,KAAK,cAAc,IAAMA,CAAI,CAAC,EACzG,KAAK,QAAQ,mBAAmB,CAAE,cAAe,IAAK,MAAOA,CAAK,EAAG,IAAM,KAAK,cAAc,IAAMA,CAAI,CAAC,EACzG,KAAK,QAAQ,mBAAmB,CAAE,cAAe,IAAK,MAAOA,CAAK,EAAG,IAAM,KAAK,cAAc,IAAMA,CAAI,CAAC,EACzG,KAAK,QAAQ,mBAAmB,CAAE,cAAe,IAAK,MAAOA,CAAK,EAAG,IAAM,KAAK,cAAc,IAAMA,CAAI,CAAC,EACzG,KAAK,QAAQ,mBAAmB,CAAE,cAAe,IAAK,MAAOA,CAAK,EAAG,IAAM,KAAK,cAAc,IAAMA,CAAI,CAAC,EACzG,KAAK,QAAQ,mBAAmB,CAAE,cAAe,IAAK,MAAOA,CAAK,EAAG,IAAM,KAAK,cAAc,IAAMA,CAAI,CAAC,EACzG,KAAK,QAAQ,mBAAmB,CAAE,cAAe,IAAK,MAAOA,CAAK,EAAG,IAAM,KAAK,cAAc,IAAMA,CAAI,CAAC,EAE3G,KAAK,QAAQ,mBAAmB,CAAE,cAAe,IAAK,MAAO,GAAI,EAAG,IAAM,KAAK,uBAAuB,CAAC,EAKvG,KAAK,QAAQ,gBAAiBE,IAC5B,KAAK,YAAY,MAAM,kBAAmBA,CAAK,EACxCA,EACR,EAKD,KAAK,QAAQ,mBAAmB,CAAE,cAAe,IAAK,MAAO,GAAI,EAAG,IAAIC,GAAW,CAACR,EAAMJ,IAAW,KAAK,oBAAoBI,EAAMJ,CAAM,CAAC,CAAC,CAC9I,CA1QO,aAA8B,CAAE,OAAO,KAAK,YAAc,CA+QzD,eAAea,EAAsBC,EAAsBC,EAAuBC,EAAwB,CAChH,KAAK,YAAY,OAAS,GAC1B,KAAK,YAAY,aAAeH,EAChC,KAAK,YAAY,aAAeC,EAChC,KAAK,YAAY,cAAgBC,EACjC,KAAK,YAAY,SAAWC,CAC9B,CAEQ,uBAAuBC,EAA2B,CAExD,GAAI,KAAK,YAAY,UAAY,EAAmB,CAClD,IAAIC,EACEC,EAAc,IAAI,QAAe,CAACC,EAAMC,IAAQ,CACpDH,EAAc,WAAW,IAAMG,EAAI,eAAe,EAAG,GAA0B,CACjF,CAAC,EACD,QAAQ,KAAK,CAACJ,EAAGE,CAAW,CAAC,EAC1B,KAAK,IAAM,CACND,IAAgB,QAClB,aAAaA,CAAW,CAE5B,EAAGI,GAAO,CAIR,GAHIJ,IAAgB,QAClB,aAAaA,CAAW,EAEtBI,IAAQ,gBACV,MAAMA,EAER,QAAQ,KAAK,iDAA0E,CACzF,CAAC,CACL,CACF,CAEQ,mBAA4B,CAClC,OAAO,KAAK,aAAa,SAAS,KACpC,CAeO,MAAMlB,EAA2BmB,EAAkD,CACxF,IAAIC,EACAX,EAAe,KAAK,cAAc,EAClCC,EAAe,KAAK,cAAc,EAClCR,EAAQ,EACNmB,EAAY,KAAK,YAAY,OAEnC,GAAIA,EAAW,CAEb,GAAID,EAAS,KAAK,QAAQ,MAAM,KAAK,aAAc,KAAK,YAAY,cAAeD,CAAa,EAC9F,YAAK,uBAAuBC,CAAM,EAC3BA,EAETX,EAAe,KAAK,YAAY,aAChCC,EAAe,KAAK,YAAY,aAChC,KAAK,YAAY,OAAS,GACtBV,EAAK,OAAS,SAChBE,EAAQ,KAAK,YAAY,SAAW,OAExC,CA2BA,GAxBI,KAAK,YAAY,UAAY,GAC/B,KAAK,YAAY,MAAM,gBAAgB,OAAOF,GAAS,SAAW,KAAKA,CAAI,IAAM,KAAK,MAAM,UAAU,IAAI,KAAKA,EAAMN,GAAK,OAAO,aAAaA,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,EAAE,EAE7J,KAAK,YAAY,WAAa,GAChC,KAAK,YAAY,MAAM,uBAAwB,OAAOM,GAAS,SAC3DA,EAAK,MAAM,EAAE,EAAE,IAAIN,GAAKA,EAAE,WAAW,CAAC,CAAC,EACvCM,CACJ,EAIE,KAAK,aAAa,OAASA,EAAK,QAC9B,KAAK,aAAa,OAAS,SAC7B,KAAK,aAAe,IAAI,YAAY,KAAK,IAAIA,EAAK,OAAQ,MAAgC,CAAC,GAM1FqB,GACH,KAAK,iBAAiB,WAAW,EAI/BrB,EAAK,OAAS,OAChB,QAASsB,EAAIpB,EAAOoB,EAAItB,EAAK,OAAQsB,GAAK,OAAkC,CAC1E,IAAMnB,EAAMmB,EAAI,OAAmCtB,EAAK,OAASsB,EAAI,OAAmCtB,EAAK,OACvGuB,EAAO,OAAOvB,GAAS,SACzB,KAAK,eAAe,OAAOA,EAAK,UAAUsB,EAAGnB,CAAG,EAAG,KAAK,YAAY,EACpE,KAAK,aAAa,OAAOH,EAAK,SAASsB,EAAGnB,CAAG,EAAG,KAAK,YAAY,EACrE,GAAIiB,EAAS,KAAK,QAAQ,MAAM,KAAK,aAAcG,CAAG,EACpD,YAAK,eAAed,EAAcC,EAAca,EAAKD,CAAC,EACtD,KAAK,uBAAuBF,CAAM,EAC3BA,CAEX,SAEI,CAACC,EAAW,CACd,IAAME,EAAO,OAAOvB,GAAS,SACzB,KAAK,eAAe,OAAOA,EAAM,KAAK,YAAY,EAClD,KAAK,aAAa,OAAOA,EAAM,KAAK,YAAY,EACpD,GAAIoB,EAAS,KAAK,QAAQ,MAAM,KAAK,aAAcG,CAAG,EACpD,YAAK,eAAed,EAAcC,EAAca,EAAK,CAAC,EACtD,KAAK,uBAAuBH,CAAM,EAC3BA,CAEX,EAGE,KAAK,cAAc,IAAMX,GAAgB,KAAK,cAAc,IAAMC,IACpE,KAAK,cAAc,KAAK,EAK1B,IAAMc,EAAc,KAAK,iBAAiB,KAAO,KAAK,eAAe,OAAO,MAAQ,KAAK,eAAe,OAAO,OACzGC,EAAgB,KAAK,iBAAiB,OAAS,KAAK,eAAe,OAAO,MAAQ,KAAK,eAAe,OAAO,OAC/GA,EAAgB,KAAK,eAAe,MACtC,KAAK,sBAAsB,KAAK,CAC9B,MAAO,KAAK,IAAIA,EAAe,KAAK,eAAe,KAAO,CAAC,EAC3D,IAAK,KAAK,IAAID,EAAa,KAAK,eAAe,KAAO,CAAC,CACzD,CAAC,CAEL,CAEO,MAAMxB,EAAmBE,EAAeC,EAAmB,CAChE,IAAIN,EACA6B,EACEC,EAAU,KAAK,gBAAgB,QAC/BC,EAAmB,KAAK,gBAAgB,WAAW,iBACnDC,EAAO,KAAK,eAAe,KAC3BC,EAAiB,KAAK,aAAa,gBAAgB,WACnDC,EAAa,KAAK,aAAa,MAAM,WACrCC,EAAU,KAAK,aACjBC,EAAY,KAAK,cAAc,MAAM,IAAI,KAAK,cAAc,MAAQ,KAAK,cAAc,CAAC,EAI5F,GAAI,CAACA,EACH,OAGF,KAAK,iBAAiB,UAAU,KAAK,cAAc,CAAC,EAGhD,KAAK,cAAc,GAAK9B,EAAMD,EAAQ,GAAK+B,EAAU,SAAS,KAAK,cAAc,EAAI,CAAC,IAAM,GAC9FA,EAAU,qBAAqB,KAAK,cAAc,EAAI,EAAG,EAAG,EAAGD,CAAO,EAGxE,IAAIE,EAAqB,KAAK,QAAQ,mBACtC,QAASC,EAAMjC,EAAOiC,EAAMhC,EAAK,EAAEgC,EAAK,CAEtC,GAAID,IAAuB,GAAK,CAACP,GAAW,CAACC,GAAoB,CAACG,GAC7D,KAAK,gBAAgB,gBAAkB,eAAiB,CAAC,KAAK,kBAAkB,GAChF,KAAK,cAAc,EAAIF,GAAQ7B,EAAKmC,CAAG,GAAK,IAAQnC,EAAKmC,CAAG,EAAI,IAAM,CACzE,GACEF,EAAU,qBAAqB,KAAK,cAAc,IAAKjC,EAAKmC,CAAG,EAAG,EAAGH,CAAO,QACrE,EAAEG,EAAMhC,GAAO,KAAK,cAAc,EAAI0B,GAAQ7B,EAAKmC,CAAG,GAAK,IAAQnC,EAAKmC,CAAG,EAAI,KACxFA,IACA,QACF,CAKA,GAJAtC,EAAOG,EAAKmC,CAAG,EAIXtC,IAAS,IACX,SAMF,GAAIA,EAAO,KAAO8B,EAAS,CACzB,IAAMS,EAAKT,EAAQ,OAAO,aAAa9B,CAAI,CAAC,EACxCuC,IACFvC,EAAOuC,EAAG,WAAW,CAAC,EAE1B,CAEA,IAAMC,EAAc,KAAK,gBAAgB,eAAexC,EAAMqC,CAAkB,EAChFR,EAAUY,EAAe,aAAaD,CAAW,EACjD,IAAME,EAAaD,EAAe,kBAAkBD,CAAW,EACzDG,EAAWD,EAAaD,EAAe,aAAaJ,CAAkB,EAAI,EAChFA,EAAqBG,EAEjBT,GACF,KAAK,YAAY,KAAKa,EAAoB5C,CAAI,CAAC,EAEjD,IAAM6C,EAAS,KAAK,kBAAkB,EAQtC,GAPIA,GACF,KAAK,gBAAgB,cAAcA,EAAQ,KAAK,cAAc,MAAQ,KAAK,cAAc,CAAC,EAMxF,KAAK,cAAc,EAAIhB,EAAUc,EAAWX,GAG9C,GAAIC,EAAgB,CAClB,IAAMa,EAASV,EACXW,EAAS,KAAK,cAAc,EAAIJ,EAgBpC,GAfA,KAAK,cAAc,EAAIA,EACvB,KAAK,cAAc,IACf,KAAK,cAAc,IAAM,KAAK,cAAc,aAAe,GAC7D,KAAK,cAAc,IACnB,KAAK,eAAe,OAAO,KAAK,eAAe,EAAG,EAAI,IAElD,KAAK,cAAc,GAAK,KAAK,eAAe,OAC9C,KAAK,cAAc,EAAI,KAAK,eAAe,KAAO,GAIpD,KAAK,cAAc,MAAM,IAAI,KAAK,cAAc,MAAQ,KAAK,cAAc,CAAC,EAAG,UAAY,IAG7FP,EAAY,KAAK,cAAc,MAAM,IAAI,KAAK,cAAc,MAAQ,KAAK,cAAc,CAAC,EACpF,CAACA,EACH,OASF,IAPIO,EAAW,GAAKP,aAAqBY,GAGvCZ,EAAU,cAAcU,EACtBC,EAAQ,EAAGJ,EAAU,EAAK,EAGvBI,EAASf,GACdc,EAAO,qBAAqBC,IAAU,EAAG,EAAGZ,CAAO,CAEvD,SACE,KAAK,cAAc,EAAIH,EAAO,EAC1BH,IAAY,EAGd,SASN,GAAIa,GAAc,KAAK,cAAc,EAAG,CACtC,IAAMO,EAASb,EAAU,SAAS,KAAK,cAAc,EAAI,CAAC,EAAI,EAAI,EAIlEA,EAAU,mBAAmB,KAAK,cAAc,EAAIa,EAClDjD,EAAM6B,CAAO,EACf,QAASqB,EAAQrB,EAAUc,EAAU,EAAEO,GAAS,GAC9Cd,EAAU,qBAAqB,KAAK,cAAc,IAAK,EAAG,EAAGD,CAAO,EAEtE,QACF,CAoBA,GAjBID,IAEFE,EAAU,YAAY,KAAK,cAAc,EAAGP,EAAUc,EAAU,KAAK,cAAc,YAAYR,CAAO,CAAC,EAInGC,EAAU,SAASJ,EAAO,CAAC,IAAM,GACnCI,EAAU,qBAAqBJ,EAAO,EAAG,EAAgB,EAAiBG,CAAO,GAKrFC,EAAU,qBAAqB,KAAK,cAAc,IAAKpC,EAAM6B,EAASM,CAAO,EAKzEN,EAAU,EACZ,KAAO,EAAEA,GAEPO,EAAU,qBAAqB,KAAK,cAAc,IAAK,EAAG,EAAGD,CAAO,CAG1E,CAEA,KAAK,QAAQ,mBAAqBE,EAG9B,KAAK,cAAc,EAAIL,GAAQ1B,EAAMD,EAAQ,GAAK+B,EAAU,SAAS,KAAK,cAAc,CAAC,IAAM,GAAK,CAACA,EAAU,WAAW,KAAK,cAAc,CAAC,GAChJA,EAAU,qBAAqB,KAAK,cAAc,EAAG,EAAG,EAAGD,CAAO,EAGpE,KAAK,iBAAiB,UAAU,KAAK,cAAc,CAAC,CACtD,CAKO,mBAAmBgB,EAAyBC,EAAwE,CACzH,OAAID,EAAG,QAAU,KAAO,CAACA,EAAG,QAAU,CAACA,EAAG,cAEjC,KAAK,QAAQ,mBAAmBA,EAAIpD,GACpCsD,GAAoBtD,EAAO,OAAO,CAAC,EAAG,KAAK,gBAAgB,WAAW,aAAa,EAGjFqD,EAASrD,CAAM,EAFb,EAGV,EAEI,KAAK,QAAQ,mBAAmBoD,EAAIC,CAAQ,CACrD,CAKO,mBAAmBD,EAAyBC,EAAqF,CACtI,OAAO,KAAK,QAAQ,mBAAmBD,EAAI,IAAIxC,GAAWyC,CAAQ,CAAC,CACrE,CAKO,mBAAmBD,EAAyBC,EAAyD,CAC1G,OAAO,KAAK,QAAQ,mBAAmBD,EAAIC,CAAQ,CACrD,CAKO,mBAAmBtD,EAAesD,EAAqE,CAC5G,OAAO,KAAK,QAAQ,mBAAmBtD,EAAO,IAAIS,EAAW6C,CAAQ,CAAC,CACxE,CAKO,mBAAmBD,EAAyBC,EAAqE,CACtH,OAAO,KAAK,QAAQ,mBAAmBD,EAAI,IAAIG,GAAWF,CAAQ,CAAC,CACrE,CAUO,MAAgB,CACrB,YAAK,eAAe,KAAK,EAClB,EACT,CAYO,UAAoB,CACzB,YAAK,iBAAiB,UAAU,KAAK,cAAc,CAAC,EAChD,KAAK,gBAAgB,WAAW,aAClC,KAAK,cAAc,EAAI,GAEzB,KAAK,cAAc,IACf,KAAK,cAAc,IAAM,KAAK,cAAc,aAAe,GAC7D,KAAK,cAAc,IACnB,KAAK,eAAe,OAAO,KAAK,eAAe,CAAC,GACvC,KAAK,cAAc,GAAK,KAAK,eAAe,KACrD,KAAK,cAAc,EAAI,KAAK,eAAe,KAAO,EAOlD,KAAK,cAAc,MAAM,IAAI,KAAK,cAAc,MAAQ,KAAK,cAAc,CAAC,EAAG,UAAY,GAGzF,KAAK,cAAc,GAAK,KAAK,eAAe,MAC9C,KAAK,cAAc,IAErB,KAAK,iBAAiB,UAAU,KAAK,cAAc,CAAC,EAEpD,KAAK,YAAY,KAAK,EACf,EACT,CAQO,gBAA0B,CAC/B,YAAK,cAAc,EAAI,EAChB,EACT,CAaO,WAAqB,CAE1B,GAAI,CAAC,KAAK,aAAa,gBAAgB,kBACrC,YAAK,gBAAgB,EACjB,KAAK,cAAc,EAAI,GACzB,KAAK,cAAc,IAEd,GAQT,GAFA,KAAK,gBAAgB,KAAK,eAAe,IAAI,EAEzC,KAAK,cAAc,EAAI,EACzB,KAAK,cAAc,YAUf,KAAK,cAAc,IAAM,GACxB,KAAK,cAAc,EAAI,KAAK,cAAc,WAC1C,KAAK,cAAc,GAAK,KAAK,cAAc,cAC3C,KAAK,cAAc,MAAM,IAAI,KAAK,cAAc,MAAQ,KAAK,cAAc,CAAC,GAAG,UAAW,CAC7F,KAAK,cAAc,MAAM,IAAI,KAAK,cAAc,MAAQ,KAAK,cAAc,CAAC,EAAG,UAAY,GAC3F,KAAK,cAAc,IACnB,KAAK,cAAc,EAAI,KAAK,eAAe,KAAO,EAMlD,IAAMG,EAAO,KAAK,cAAc,MAAM,IAAI,KAAK,cAAc,MAAQ,KAAK,cAAc,CAAC,EACrFA,EAAK,SAAS,KAAK,cAAc,CAAC,GAAK,CAACA,EAAK,WAAW,KAAK,cAAc,CAAC,GAC9E,KAAK,cAAc,GAKvB,CAEF,YAAK,gBAAgB,EACd,EACT,CAQO,KAAe,CACpB,GAAI,KAAK,cAAc,GAAK,KAAK,eAAe,KAC9C,MAAO,GAET,IAAMC,EAAY,KAAK,cAAc,EACrC,YAAK,cAAc,EAAI,KAAK,cAAc,SAAS,EAC/C,KAAK,gBAAgB,WAAW,kBAClC,KAAK,WAAW,KAAK,KAAK,cAAc,EAAIA,CAAS,EAEhD,EACT,CASO,UAAoB,CACzB,YAAK,gBAAgB,UAAU,CAAC,EACzB,EACT,CASO,SAAmB,CACxB,YAAK,gBAAgB,UAAU,CAAC,EACzB,EACT,CAKQ,gBAAgBC,EAAiB,KAAK,eAAe,KAAO,EAAS,CAC3E,KAAK,cAAc,EAAI,KAAK,IAAIA,EAAQ,KAAK,IAAI,EAAG,KAAK,cAAc,CAAC,CAAC,EACzE,KAAK,cAAc,EAAI,KAAK,aAAa,gBAAgB,OACrD,KAAK,IAAI,KAAK,cAAc,aAAc,KAAK,IAAI,KAAK,cAAc,UAAW,KAAK,cAAc,CAAC,CAAC,EACtG,KAAK,IAAI,KAAK,eAAe,KAAO,EAAG,KAAK,IAAI,EAAG,KAAK,cAAc,CAAC,CAAC,EAC5E,KAAK,iBAAiB,UAAU,KAAK,cAAc,CAAC,CACtD,CAKQ,WAAWC,EAAWC,EAAiB,CAC7C,KAAK,iBAAiB,UAAU,KAAK,cAAc,CAAC,EAChD,KAAK,aAAa,gBAAgB,QACpC,KAAK,cAAc,EAAID,EACvB,KAAK,cAAc,EAAI,KAAK,cAAc,UAAYC,IAEtD,KAAK,cAAc,EAAID,EACvB,KAAK,cAAc,EAAIC,GAEzB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,UAAU,KAAK,cAAc,CAAC,CACtD,CAKQ,YAAYD,EAAWC,EAAiB,CAG9C,KAAK,gBAAgB,EACrB,KAAK,WAAW,KAAK,cAAc,EAAID,EAAG,KAAK,cAAc,EAAIC,CAAC,CACpE,CASO,SAAS5D,EAA0B,CAExC,IAAM6D,EAAY,KAAK,cAAc,EAAI,KAAK,cAAc,UAC5D,OAAIA,GAAa,EACf,KAAK,YAAY,EAAG,CAAC,KAAK,IAAIA,EAAW7D,EAAO,OAAO,CAAC,GAAK,CAAC,CAAC,EAE/D,KAAK,YAAY,EAAG,EAAEA,EAAO,OAAO,CAAC,GAAK,EAAE,EAEvC,EACT,CASO,WAAWA,EAA0B,CAE1C,IAAM8D,EAAe,KAAK,cAAc,aAAe,KAAK,cAAc,EAC1E,OAAIA,GAAgB,EAClB,KAAK,YAAY,EAAG,KAAK,IAAIA,EAAc9D,EAAO,OAAO,CAAC,GAAK,CAAC,CAAC,EAEjE,KAAK,YAAY,EAAGA,EAAO,OAAO,CAAC,GAAK,CAAC,EAEpC,EACT,CAQO,cAAcA,EAA0B,CAC7C,YAAK,YAAYA,EAAO,OAAO,CAAC,GAAK,EAAG,CAAC,EAClC,EACT,CAQO,eAAeA,EAA0B,CAC9C,YAAK,YAAY,EAAEA,EAAO,OAAO,CAAC,GAAK,GAAI,CAAC,EACrC,EACT,CAUO,eAAeA,EAA0B,CAC9C,YAAK,WAAWA,CAAM,EACtB,KAAK,cAAc,EAAI,EAChB,EACT,CAUO,oBAAoBA,EAA0B,CACnD,YAAK,SAASA,CAAM,EACpB,KAAK,cAAc,EAAI,EAChB,EACT,CAQO,mBAAmBA,EAA0B,CAClD,YAAK,YAAYA,EAAO,OAAO,CAAC,GAAK,GAAK,EAAG,KAAK,cAAc,CAAC,EAC1D,EACT,CAWO,eAAeA,EAA0B,CAC9C,YAAK,WAEFA,EAAO,QAAU,GAAMA,EAAO,OAAO,CAAC,GAAK,GAAK,EAAI,GAEpDA,EAAO,OAAO,CAAC,GAAK,GAAK,CAC5B,EACO,EACT,CASO,gBAAgBA,EAA0B,CAC/C,YAAK,YAAYA,EAAO,OAAO,CAAC,GAAK,GAAK,EAAG,KAAK,cAAc,CAAC,EAC1D,EACT,CAQO,kBAAkBA,EAA0B,CACjD,YAAK,YAAYA,EAAO,OAAO,CAAC,GAAK,EAAG,CAAC,EAClC,EACT,CAQO,gBAAgBA,EAA0B,CAC/C,YAAK,WAAW,KAAK,cAAc,GAAIA,EAAO,OAAO,CAAC,GAAK,GAAK,CAAC,EAC1D,EACT,CASO,kBAAkBA,EAA0B,CACjD,YAAK,YAAY,EAAGA,EAAO,OAAO,CAAC,GAAK,CAAC,EAClC,EACT,CAUO,WAAWA,EAA0B,CAC1C,YAAK,eAAeA,CAAM,EACnB,EACT,CAaO,SAASA,EAA0B,CACxC,IAAM+D,EAAQ/D,EAAO,OAAO,CAAC,EAC7B,OAAI+D,IAAU,EACZ,OAAO,KAAK,cAAc,KAAK,KAAK,cAAc,CAAC,EAC1CA,IAAU,IACnB,KAAK,cAAc,KAAO,CAAC,GAEtB,EACT,CAQO,iBAAiB/D,EAA0B,CAChD,GAAI,KAAK,cAAc,GAAK,KAAK,eAAe,KAC9C,MAAO,GAET,IAAI+D,EAAQ/D,EAAO,OAAO,CAAC,GAAK,EAChC,KAAO+D,KACL,KAAK,cAAc,EAAI,KAAK,cAAc,SAAS,EAErD,MAAO,EACT,CAOO,kBAAkB/D,EAA0B,CACjD,GAAI,KAAK,cAAc,GAAK,KAAK,eAAe,KAC9C,MAAO,GAET,IAAI+D,EAAQ/D,EAAO,OAAO,CAAC,GAAK,EAEhC,KAAO+D,KACL,KAAK,cAAc,EAAI,KAAK,cAAc,SAAS,EAErD,MAAO,EACT,CAOO,gBAAgB/D,EAA0B,CAC/C,IAAMiB,EAAIjB,EAAO,OAAO,CAAC,EACzB,OAAIiB,IAAM,IAAG,KAAK,aAAa,IAAM,YACjCA,IAAM,GAAKA,IAAM,KAAG,KAAK,aAAa,IAAM,YACzC,EACT,CAYQ,mBAAmB2C,EAAWtD,EAAeC,EAAayD,EAAqB,GAAOC,EAA0B,GAAa,CACnI,IAAMT,EAAO,KAAK,cAAc,MAAM,IAAI,KAAK,cAAc,MAAQI,CAAC,EACjEJ,IAGLA,EAAK,aACHlD,EACAC,EACA,KAAK,cAAc,YAAY,KAAK,eAAe,CAAC,EACpD0D,CACF,EACID,IACFR,EAAK,UAAY,IAErB,CAOQ,iBAAiBI,EAAWK,EAA0B,GAAa,CACzE,IAAMT,EAAO,KAAK,cAAc,MAAM,IAAI,KAAK,cAAc,MAAQI,CAAC,EAClEJ,IACFA,EAAK,KAAK,KAAK,cAAc,YAAY,KAAK,eAAe,CAAC,EAAGS,CAAc,EAC/E,KAAK,eAAe,OAAO,aAAa,KAAK,cAAc,MAAQL,CAAC,EACpEJ,EAAK,UAAY,GAErB,CA0BO,eAAexD,EAAiBiE,EAA0B,GAAgB,CAC/E,KAAK,gBAAgB,KAAK,eAAe,IAAI,EAC7C,IAAIC,EACJ,OAAQlE,EAAO,OAAO,CAAC,EAAG,CACxB,IAAK,GAIH,IAHAkE,EAAI,KAAK,cAAc,EACvB,KAAK,iBAAiB,UAAUA,CAAC,EACjC,KAAK,mBAAmBA,IAAK,KAAK,cAAc,EAAG,KAAK,eAAe,KAAM,KAAK,cAAc,IAAM,EAAGD,CAAc,EAChHC,EAAI,KAAK,eAAe,KAAMA,IACnC,KAAK,iBAAiBA,EAAGD,CAAc,EAEzC,KAAK,iBAAiB,UAAUC,CAAC,EACjC,MACF,IAAK,GAKH,GAJAA,EAAI,KAAK,cAAc,EACvB,KAAK,iBAAiB,UAAUA,CAAC,EAEjC,KAAK,mBAAmBA,EAAG,EAAG,KAAK,cAAc,EAAI,EAAG,GAAMD,CAAc,EACxE,KAAK,cAAc,EAAI,GAAK,KAAK,eAAe,KAAM,CAExD,IAAME,EAAW,KAAK,cAAc,MAAM,IAAID,EAAI,CAAC,EAC/CC,IACFA,EAAS,UAAY,GAEzB,CACA,KAAOD,KACL,KAAK,iBAAiBA,EAAGD,CAAc,EAEzC,KAAK,iBAAiB,UAAU,CAAC,EACjC,MACF,IAAK,GACH,GAAI,KAAK,gBAAgB,WAAW,uBAAwB,CAG1D,IAFAC,EAAI,KAAK,eAAe,KACxB,KAAK,iBAAiB,eAAe,EAAGA,EAAI,CAAC,EACtCA,KAED,CADgB,KAAK,cAAc,MAAM,IAAI,KAAK,cAAc,MAAQA,CAAC,GAC5D,iBAAiB,GAAlC,CAIF,KAAOA,GAAK,EAAGA,IACb,KAAK,eAAe,OAAO,KAAK,eAAe,CAAC,CAEpD,KACK,CAGH,IAFAA,EAAI,KAAK,eAAe,KACxB,KAAK,iBAAiB,UAAUA,EAAI,CAAC,EAC9BA,KACL,KAAK,iBAAiBA,EAAGD,CAAc,EAEzC,KAAK,iBAAiB,UAAU,CAAC,CACnC,CACA,MACF,IAAK,GAEH,IAAMG,EAAiB,KAAK,cAAc,MAAM,OAAS,KAAK,eAAe,KACzEA,EAAiB,IACnB,KAAK,cAAc,MAAM,UAAUA,CAAc,EACjD,KAAK,cAAc,MAAQ,KAAK,IAAI,KAAK,cAAc,MAAQA,EAAgB,CAAC,EAChF,KAAK,cAAc,MAAQ,KAAK,IAAI,KAAK,cAAc,MAAQA,EAAgB,CAAC,EAG5E,KAAK,gBAAkB,KAAK,eAAe,QAAQ,SACrD,KAAK,eAAe,gBAAkB,IAGxC,KAAK,UAAU,KAAK,CAAC,GAEvB,KACJ,CACA,MAAO,EACT,CAwBO,YAAYpE,EAAiBiE,EAA0B,GAAgB,CAE5E,OADA,KAAK,gBAAgB,KAAK,eAAe,IAAI,EACrCjE,EAAO,OAAO,CAAC,EAAG,CACxB,IAAK,GACH,KAAK,mBAAmB,KAAK,cAAc,EAAG,KAAK,cAAc,EAAG,KAAK,eAAe,KAAM,KAAK,cAAc,IAAM,EAAGiE,CAAc,EACxI,MACF,IAAK,GACH,KAAK,mBAAmB,KAAK,cAAc,EAAG,EAAG,KAAK,cAAc,EAAI,EAAG,GAAOA,CAAc,EAChG,MACF,IAAK,GACH,KAAK,mBAAmB,KAAK,cAAc,EAAG,EAAG,KAAK,eAAe,KAAM,GAAMA,CAAc,EAC/F,KACJ,CACA,YAAK,iBAAiB,UAAU,KAAK,cAAc,CAAC,EAC7C,EACT,CAWO,YAAYjE,EAA0B,CAC3C,KAAK,gBAAgB,EACrB,IAAI+D,EAAQ/D,EAAO,OAAO,CAAC,GAAK,EAEhC,GAAI,KAAK,cAAc,EAAI,KAAK,cAAc,cAAgB,KAAK,cAAc,EAAI,KAAK,cAAc,UACtG,MAAO,GAGT,IAAMqE,EAAc,KAAK,cAAc,MAAQ,KAAK,cAAc,EAE5DC,EAAyB,KAAK,eAAe,KAAO,EAAI,KAAK,cAAc,aAC3EC,EAAuB,KAAK,eAAe,KAAO,EAAI,KAAK,cAAc,MAAQD,EAAyB,EAChH,KAAOP,KAGL,KAAK,cAAc,MAAM,OAAOQ,EAAuB,EAAG,CAAC,EAC3D,KAAK,cAAc,MAAM,OAAOF,EAAK,EAAG,KAAK,cAAc,aAAa,KAAK,eAAe,CAAC,CAAC,EAGhG,YAAK,iBAAiB,eAAe,KAAK,cAAc,EAAG,KAAK,cAAc,YAAY,EAC1F,KAAK,cAAc,EAAI,EAChB,EACT,CAWO,YAAYrE,EAA0B,CAC3C,KAAK,gBAAgB,EACrB,IAAI+D,EAAQ/D,EAAO,OAAO,CAAC,GAAK,EAEhC,GAAI,KAAK,cAAc,EAAI,KAAK,cAAc,cAAgB,KAAK,cAAc,EAAI,KAAK,cAAc,UACtG,MAAO,GAGT,IAAMqE,EAAc,KAAK,cAAc,MAAQ,KAAK,cAAc,EAE9DH,EAGJ,IAFAA,EAAI,KAAK,eAAe,KAAO,EAAI,KAAK,cAAc,aACtDA,EAAI,KAAK,eAAe,KAAO,EAAI,KAAK,cAAc,MAAQA,EACvDH,KAGL,KAAK,cAAc,MAAM,OAAOM,EAAK,CAAC,EACtC,KAAK,cAAc,MAAM,OAAOH,EAAG,EAAG,KAAK,cAAc,aAAa,KAAK,eAAe,CAAC,CAAC,EAG9F,YAAK,iBAAiB,eAAe,KAAK,cAAc,EAAG,KAAK,cAAc,YAAY,EAC1F,KAAK,cAAc,EAAI,EAChB,EACT,CAcO,YAAYlE,EAA0B,CAC3C,KAAK,gBAAgB,EACrB,IAAMwD,EAAO,KAAK,cAAc,MAAM,IAAI,KAAK,cAAc,MAAQ,KAAK,cAAc,CAAC,EACzF,OAAIA,IACFA,EAAK,YACH,KAAK,cAAc,EACnBxD,EAAO,OAAO,CAAC,GAAK,EACpB,KAAK,cAAc,YAAY,KAAK,eAAe,CAAC,CACtD,EACA,KAAK,iBAAiB,UAAU,KAAK,cAAc,CAAC,GAE/C,EACT,CAcO,YAAYA,EAA0B,CAC3C,KAAK,gBAAgB,EACrB,IAAMwD,EAAO,KAAK,cAAc,MAAM,IAAI,KAAK,cAAc,MAAQ,KAAK,cAAc,CAAC,EACzF,OAAIA,IACFA,EAAK,YACH,KAAK,cAAc,EACnBxD,EAAO,OAAO,CAAC,GAAK,EACpB,KAAK,cAAc,YAAY,KAAK,eAAe,CAAC,CACtD,EACA,KAAK,iBAAiB,UAAU,KAAK,cAAc,CAAC,GAE/C,EACT,CAUO,SAASA,EAA0B,CACxC,IAAI+D,EAAQ/D,EAAO,OAAO,CAAC,GAAK,EAEhC,KAAO+D,KACL,KAAK,cAAc,MAAM,OAAO,KAAK,cAAc,MAAQ,KAAK,cAAc,UAAW,CAAC,EAC1F,KAAK,cAAc,MAAM,OAAO,KAAK,cAAc,MAAQ,KAAK,cAAc,aAAc,EAAG,KAAK,cAAc,aAAa,KAAK,eAAe,CAAC,CAAC,EAEvJ,YAAK,iBAAiB,eAAe,KAAK,cAAc,UAAW,KAAK,cAAc,YAAY,EAC3F,EACT,CAOO,WAAW/D,EAA0B,CAC1C,IAAI+D,EAAQ/D,EAAO,OAAO,CAAC,GAAK,EAEhC,KAAO+D,KACL,KAAK,cAAc,MAAM,OAAO,KAAK,cAAc,MAAQ,KAAK,cAAc,aAAc,CAAC,EAC7F,KAAK,cAAc,MAAM,OAAO,KAAK,cAAc,MAAQ,KAAK,cAAc,UAAW,EAAG,KAAK,cAAc,aAAapE,CAAiB,CAAC,EAEhJ,YAAK,iBAAiB,eAAe,KAAK,cAAc,UAAW,KAAK,cAAc,YAAY,EAC3F,EACT,CAoBO,WAAWK,EAA0B,CAC1C,GAAI,KAAK,cAAc,EAAI,KAAK,cAAc,cAAgB,KAAK,cAAc,EAAI,KAAK,cAAc,UACtG,MAAO,GAET,IAAM+D,EAAQ/D,EAAO,OAAO,CAAC,GAAK,EAClC,QAAS4D,EAAI,KAAK,cAAc,UAAWA,GAAK,KAAK,cAAc,aAAc,EAAEA,EAAG,CACpF,IAAMJ,EAAO,KAAK,cAAc,MAAM,IAAI,KAAK,cAAc,MAAQI,CAAC,EACtEJ,EAAK,YAAY,EAAGO,EAAO,KAAK,cAAc,YAAY,KAAK,eAAe,CAAC,CAAC,EAChFP,EAAK,UAAY,EACnB,CACA,YAAK,iBAAiB,eAAe,KAAK,cAAc,UAAW,KAAK,cAAc,YAAY,EAC3F,EACT,CAqBO,YAAYxD,EAA0B,CAC3C,GAAI,KAAK,cAAc,EAAI,KAAK,cAAc,cAAgB,KAAK,cAAc,EAAI,KAAK,cAAc,UACtG,MAAO,GAET,IAAM+D,EAAQ/D,EAAO,OAAO,CAAC,GAAK,EAClC,QAAS4D,EAAI,KAAK,cAAc,UAAWA,GAAK,KAAK,cAAc,aAAc,EAAEA,EAAG,CACpF,IAAMJ,EAAO,KAAK,cAAc,MAAM,IAAI,KAAK,cAAc,MAAQI,CAAC,EACtEJ,EAAK,YAAY,EAAGO,EAAO,KAAK,cAAc,YAAY,KAAK,eAAe,CAAC,CAAC,EAChFP,EAAK,UAAY,EACnB,CACA,YAAK,iBAAiB,eAAe,KAAK,cAAc,UAAW,KAAK,cAAc,YAAY,EAC3F,EACT,CAWO,cAAcxD,EAA0B,CAC7C,GAAI,KAAK,cAAc,EAAI,KAAK,cAAc,cAAgB,KAAK,cAAc,EAAI,KAAK,cAAc,UACtG,MAAO,GAET,IAAM+D,EAAQ/D,EAAO,OAAO,CAAC,GAAK,EAClC,QAAS4D,EAAI,KAAK,cAAc,UAAWA,GAAK,KAAK,cAAc,aAAc,EAAEA,EAAG,CACpF,IAAMJ,EAAO,KAAK,cAAc,MAAM,IAAI,KAAK,cAAc,MAAQI,CAAC,EACtEJ,EAAK,YAAY,KAAK,cAAc,EAAGO,EAAO,KAAK,cAAc,YAAY,KAAK,eAAe,CAAC,CAAC,EACnGP,EAAK,UAAY,EACnB,CACA,YAAK,iBAAiB,eAAe,KAAK,cAAc,UAAW,KAAK,cAAc,YAAY,EAC3F,EACT,CAWO,cAAcxD,EAA0B,CAC7C,GAAI,KAAK,cAAc,EAAI,KAAK,cAAc,cAAgB,KAAK,cAAc,EAAI,KAAK,cAAc,UACtG,MAAO,GAET,IAAM+D,EAAQ/D,EAAO,OAAO,CAAC,GAAK,EAClC,QAAS4D,EAAI,KAAK,cAAc,UAAWA,GAAK,KAAK,cAAc,aAAc,EAAEA,EAAG,CACpF,IAAMJ,EAAO,KAAK,cAAc,MAAM,IAAI,KAAK,cAAc,MAAQI,CAAC,EACtEJ,EAAK,YAAY,KAAK,cAAc,EAAGO,EAAO,KAAK,cAAc,YAAY,KAAK,eAAe,CAAC,CAAC,EACnGP,EAAK,UAAY,EACnB,CACA,YAAK,iBAAiB,eAAe,KAAK,cAAc,UAAW,KAAK,cAAc,YAAY,EAC3F,EACT,CAUO,WAAWxD,EAA0B,CAC1C,KAAK,gBAAgB,EACrB,IAAMwD,EAAO,KAAK,cAAc,MAAM,IAAI,KAAK,cAAc,MAAQ,KAAK,cAAc,CAAC,EACzF,OAAIA,IACFA,EAAK,aACH,KAAK,cAAc,EACnB,KAAK,cAAc,GAAKxD,EAAO,OAAO,CAAC,GAAK,GAC5C,KAAK,cAAc,YAAY,KAAK,eAAe,CAAC,CACtD,EACA,KAAK,iBAAiB,UAAU,KAAK,cAAc,CAAC,GAE/C,EACT,CA4BO,yBAAyBA,EAA0B,CACxD,IAAMwE,EAAY,KAAK,QAAQ,mBAC/B,GAAI,CAACA,EACH,MAAO,GAGT,IAAMC,EAASzE,EAAO,OAAO,CAAC,GAAK,EAC7B8B,EAAUY,EAAe,aAAa8B,CAAS,EAC/Cb,EAAI,KAAK,cAAc,EAAI7B,EAE3B4C,EADY,KAAK,cAAc,MAAM,IAAI,KAAK,cAAc,MAAQ,KAAK,cAAc,CAAC,EACvE,UAAUf,CAAC,EAC5BvD,EAAO,IAAI,YAAYsE,EAAK,OAASD,CAAM,EAC7CE,EAAQ,EACZ,QAASC,EAAQ,EAAGA,EAAQF,EAAK,QAAS,CACxC,IAAMlC,EAAKkC,EAAK,YAAYE,CAAK,GAAK,EACtCxE,EAAKuE,GAAO,EAAInC,EAChBoC,GAASpC,EAAK,MAAS,EAAI,CAC7B,CACA,IAAIqC,EAAUF,EACd,QAASjD,EAAI,EAAGA,EAAI+C,EAAQ,EAAE/C,EAC5BtB,EAAK,WAAWyE,EAAS,EAAGF,CAAK,EACjCE,GAAWF,EAEb,YAAK,MAAMvE,EAAM,EAAGyE,CAAO,EACpB,EACT,CA2BO,4BAA4B7E,EAA0B,CAC3D,OAAIA,EAAO,OAAO,CAAC,EAAI,IAGnB,KAAK,IAAI,OAAO,GAAK,KAAK,IAAI,cAAc,GAAK,KAAK,IAAI,QAAQ,EACpE,KAAK,aAAa,iBAAiB,YAAiB,EAC3C,KAAK,IAAI,OAAO,GACzB,KAAK,aAAa,iBAAiB,UAAe,GAE7C,EACT,CA0BO,8BAA8BA,EAA0B,CAC7D,OAAIA,EAAO,OAAO,CAAC,EAAI,IAMnB,KAAK,IAAI,OAAO,EAClB,KAAK,aAAa,iBAAiB,gBAAqB,EAC/C,KAAK,IAAI,cAAc,EAChC,KAAK,aAAa,iBAAiB,gBAAqB,EAC/C,KAAK,IAAI,OAAO,EAGzB,KAAK,aAAa,iBAAiBA,EAAO,OAAO,CAAC,EAAI,GAAG,EAChD,KAAK,IAAI,QAAQ,GAC1B,KAAK,aAAa,iBAAiB,mBAAwB,GAEtD,EACT,CAUO,cAAcA,EAA0B,CAC7C,OAAIA,EAAO,OAAO,CAAC,EAAI,GAGvB,KAAK,aAAa,iBAAiB,mBAAwB8E,EAAa,SAAc,EAC/E,EACT,CAMQ,IAAIC,EAAuB,CACjC,OAAQ,KAAK,gBAAgB,WAAW,SAAW,IAAI,WAAWA,CAAI,CACxE,CAmBO,QAAQ/E,EAA0B,CACvC,QAAS0B,EAAI,EAAGA,EAAI1B,EAAO,OAAQ0B,IACjC,OAAQ1B,EAAO,OAAO0B,CAAC,EAAG,CACxB,IAAK,GACH,KAAK,aAAa,MAAM,WAAa,GACrC,MACF,IAAK,IACH,KAAK,gBAAgB,QAAQ,WAAa,GAC1C,KACJ,CAEF,MAAO,EACT,CAoHO,eAAe1B,EAA0B,CAC9C,QAAS0B,EAAI,EAAGA,EAAI1B,EAAO,OAAQ0B,IACjC,OAAQ1B,EAAO,OAAO0B,CAAC,EAAG,CACxB,IAAK,GACH,KAAK,aAAa,gBAAgB,sBAAwB,GAC1D,MACF,IAAK,GACH,KAAK,gBAAgB,YAAY,EAAGsD,CAAe,EACnD,KAAK,gBAAgB,YAAY,EAAGA,CAAe,EACnD,KAAK,gBAAgB,YAAY,EAAGA,CAAe,EACnD,KAAK,gBAAgB,YAAY,EAAGA,CAAe,EAEnD,MACF,IAAK,GAMC,KAAK,gBAAgB,WAAW,cAAc,cAChD,KAAK,eAAe,OAAO,IAAK,KAAK,eAAe,IAAI,EACxD,KAAK,gBAAgB,KAAK,GAE5B,MACF,IAAK,GACH,KAAK,aAAa,gBAAgB,OAAS,GAC3C,KAAK,WAAW,EAAG,CAAC,EACpB,MACF,IAAK,GACH,KAAK,aAAa,gBAAgB,WAAa,GAC/C,MACF,IAAK,IACC,KAAK,gBAAgB,WAAW,QAAQ,sBAC1C,KAAK,gBAAgB,QAAQ,YAAc,IAE7C,MACF,IAAK,IACH,KAAK,aAAa,gBAAgB,kBAAoB,GACtD,MACF,IAAK,IACH,KAAK,YAAY,MAAM,2CAA2C,EAClE,KAAK,aAAa,gBAAgB,kBAAoB,GACtD,KAAK,wBAAwB,KAAK,EAClC,MACF,IAAK,GAEH,KAAK,mBAAmB,eAAiB,MACzC,MACF,IAAK,KAEH,KAAK,mBAAmB,eAAiB,QACzC,MACF,IAAK,MACH,KAAK,mBAAmB,eAAiB,OACzC,MACF,IAAK,MAGH,KAAK,mBAAmB,eAAiB,MACzC,MACF,IAAK,MAGH,KAAK,aAAa,gBAAgB,UAAY,GAC9C,KAAK,oBAAoB,KAAK,EAC9B,MACF,IAAK,MACH,KAAK,YAAY,MAAM,uCAAuC,EAC9D,MACF,IAAK,MACH,KAAK,mBAAmB,eAAiB,MACzC,MACF,IAAK,MACH,KAAK,YAAY,MAAM,uCAAuC,EAC9D,MACF,IAAK,MACH,KAAK,mBAAmB,eAAiB,aACzC,MACF,IAAK,IACH,KAAK,aAAa,eAAiB,GACnC,MACF,IAAK,MACH,KAAK,WAAW,EAChB,MACF,IAAK,MACH,KAAK,WAAW,EAElB,IAAK,IACL,IAAK,MAEH,GAAI,KAAK,gBAAgB,WAAW,cAAc,cAAe,CAC/D,IAAMrE,EAAQ,KAAK,aAAa,cAChCA,EAAM,UAAYA,EAAM,MACxBA,EAAM,MAAQA,EAAM,QACtB,CACA,KAAK,eAAe,QAAQ,kBAAkB,KAAK,eAAe,CAAC,EACnE,KAAK,aAAa,oBAAsB,GACxC,KAAK,sBAAsB,KAAK,MAAS,EACzC,KAAK,wBAAwB,KAAK,EAClC,MACF,IAAK,MACH,KAAK,aAAa,gBAAgB,mBAAqB,GACvD,MACF,IAAK,MACH,KAAK,aAAa,gBAAgB,mBAAqB,GACvD,MACF,IAAK,OACC,KAAK,gBAAgB,WAAW,cAAc,kBAAoB,MACpE,KAAK,aAAa,gBAAgB,mBAAqB,IAEzD,MACF,IAAK,MACC,KAAK,gBAAgB,WAAW,cAAc,iBAChD,KAAK,aAAa,gBAAgB,eAAiB,IAErD,KACJ,CAEF,MAAO,EACT,CAuBO,UAAUX,EAA0B,CACzC,QAAS0B,EAAI,EAAGA,EAAI1B,EAAO,OAAQ0B,IACjC,OAAQ1B,EAAO,OAAO0B,CAAC,EAAG,CACxB,IAAK,GACH,KAAK,aAAa,MAAM,WAAa,GACrC,MACF,IAAK,IACH,KAAK,gBAAgB,QAAQ,WAAa,GAC1C,KACJ,CAEF,MAAO,EACT,CAgHO,iBAAiB1B,EAA0B,CAChD,QAAS0B,EAAI,EAAGA,EAAI1B,EAAO,OAAQ0B,IACjC,OAAQ1B,EAAO,OAAO0B,CAAC,EAAG,CACxB,IAAK,GACH,KAAK,aAAa,gBAAgB,sBAAwB,GAC1D,MACF,IAAK,GAMC,KAAK,gBAAgB,WAAW,cAAc,cAChD,KAAK,eAAe,OAAO,GAAI,KAAK,eAAe,IAAI,EACvD,KAAK,gBAAgB,KAAK,GAE5B,MACF,IAAK,GACH,KAAK,aAAa,gBAAgB,OAAS,GAC3C,KAAK,WAAW,EAAG,CAAC,EACpB,MACF,IAAK,GACH,KAAK,aAAa,gBAAgB,WAAa,GAC/C,MACF,IAAK,IACC,KAAK,gBAAgB,WAAW,QAAQ,sBAC1C,KAAK,gBAAgB,QAAQ,YAAc,IAE7C,MACF,IAAK,IACH,KAAK,aAAa,gBAAgB,kBAAoB,GACtD,MACF,IAAK,IACH,KAAK,YAAY,MAAM,kCAAkC,EACzD,KAAK,aAAa,gBAAgB,kBAAoB,GACtD,KAAK,wBAAwB,KAAK,EAClC,MACF,IAAK,GACL,IAAK,KACL,IAAK,MACL,IAAK,MACH,KAAK,mBAAmB,eAAiB,OACzC,MACF,IAAK,MACH,KAAK,aAAa,gBAAgB,UAAY,GAC9C,MACF,IAAK,MACH,KAAK,YAAY,MAAM,uCAAuC,EAC9D,MACF,IAAK,MACH,KAAK,mBAAmB,eAAiB,UACzC,MACF,IAAK,MACH,KAAK,YAAY,MAAM,uCAAuC,EAC9D,MACF,IAAK,MACH,KAAK,mBAAmB,eAAiB,UACzC,MACF,IAAK,IACH,KAAK,aAAa,eAAiB,GACnC,MACF,IAAK,MACH,KAAK,cAAc,EACnB,MACF,IAAK,MAEL,IAAK,IACL,IAAK,MAEH,GAAI,KAAK,gBAAgB,WAAW,cAAc,cAAe,CAC/D,IAAMf,EAAQ,KAAK,aAAa,cAChCA,EAAM,SAAWA,EAAM,MACvBA,EAAM,MAAQA,EAAM,SACtB,CAEA,KAAK,eAAe,QAAQ,qBAAqB,EAC7CX,EAAO,OAAO0B,CAAC,IAAM,MACvB,KAAK,cAAc,EAErB,KAAK,aAAa,oBAAsB,GACxC,KAAK,sBAAsB,KAAK,MAAS,EACzC,KAAK,wBAAwB,KAAK,EAClC,MACF,IAAK,MACH,KAAK,aAAa,gBAAgB,mBAAqB,GACvD,MACF,IAAK,MACH,KAAK,aAAa,gBAAgB,mBAAqB,GACvD,KAAK,sBAAsB,KAAK,MAAS,EACzC,MACF,IAAK,OACC,KAAK,gBAAgB,WAAW,cAAc,kBAAoB,MACpE,KAAK,aAAa,gBAAgB,mBAAqB,IAEzD,MACF,IAAK,MACC,KAAK,gBAAgB,WAAW,cAAc,iBAChD,KAAK,aAAa,gBAAgB,eAAiB,IAErD,KACJ,CAEF,MAAO,EACT,CAmCO,YAAY1B,EAAiBiF,EAAwB,CAE1D,IAAWC,OACTA,IAAA,eAAiB,GAAjB,iBACAA,IAAA,IAAM,GAAN,MACAA,IAAA,MAAQ,GAAR,QACAA,IAAA,gBAAkB,GAAlB,kBACAA,IAAA,kBAAoB,GAApB,sBALSA,IAAA,IASX,IAAMC,EAAK,KAAK,aAAa,gBACvB,CAAE,eAAgBC,EAAe,eAAgBC,CAAc,EAAI,KAAK,mBACxEC,EAAK,KAAK,aACV,CAAE,QAAAC,EAAS,KAAAtD,CAAK,EAAI,KAAK,eACzB,CAAE,OAAAuD,EAAQ,IAAAC,CAAI,EAAIF,EAClBG,EAAO,KAAK,gBAAgB,WAE5BC,EAAI,CAACC,EAAWC,KACpBP,EAAG,iBAAiB,QAAaL,EAAO,GAAK,GAAG,GAAGW,CAAC,IAAIC,CAAC,IAAI,EACtD,IAEHC,EAAOC,GAAsBA,EAAQ,EAAQ,EAE7C9E,EAAIjB,EAAO,OAAO,CAAC,EAEzB,OAAIiF,EACEhE,IAAM,EAAU0E,EAAE1E,EAAG,CAAmB,EACxCA,IAAM,EAAU0E,EAAE1E,EAAG6E,EAAIR,EAAG,MAAM,UAAU,CAAC,EAC7CrE,IAAM,GAAW0E,EAAE1E,EAAG,CAAiB,EACvCA,IAAM,GAAW0E,EAAE1E,EAAG6E,EAAIJ,EAAK,UAAU,CAAC,EACvCC,EAAE1E,EAAG,CAAgB,EAG1BA,IAAM,EAAU0E,EAAE1E,EAAG6E,EAAIX,EAAG,qBAAqB,CAAC,EAClDlE,IAAM,EAAU0E,EAAE1E,EAAGyE,EAAK,cAAc,YAAezD,IAAS,GAAK,EAAUA,IAAS,IAAM,EAAQ,EAAoB,CAAgB,EAC1IhB,IAAM,EAAU0E,EAAE1E,EAAG6E,EAAIX,EAAG,MAAM,CAAC,EACnClE,IAAM,EAAU0E,EAAE1E,EAAG6E,EAAIX,EAAG,UAAU,CAAC,EACvClE,IAAM,EAAU0E,EAAE1E,EAAG,CAAiB,EACtCA,IAAM,EAAU0E,EAAE1E,EAAG6E,EAAIV,IAAkB,KAAK,CAAC,EACjDnE,IAAM,GAAW0E,EAAE1E,EAAG6E,EAAIJ,EAAK,WAAW,CAAC,EAC3CzE,IAAM,GAAW0E,EAAE1E,EAAG6E,EAAI,CAACR,EAAG,cAAc,CAAC,EAC7CrE,IAAM,GAAW0E,EAAE1E,EAAG6E,EAAIX,EAAG,iBAAiB,CAAC,EAC/ClE,IAAM,GAAW0E,EAAE1E,EAAG6E,EAAIX,EAAG,iBAAiB,CAAC,EAC/ClE,IAAM,GAAW0E,EAAE1E,EAAG,CAAmB,EACzCA,IAAM,IAAa0E,EAAE1E,EAAG6E,EAAIV,IAAkB,OAAO,CAAC,EACtDnE,IAAM,KAAa0E,EAAE1E,EAAG6E,EAAIV,IAAkB,MAAM,CAAC,EACrDnE,IAAM,KAAa0E,EAAE1E,EAAG6E,EAAIV,IAAkB,KAAK,CAAC,EACpDnE,IAAM,KAAa0E,EAAE1E,EAAG6E,EAAIX,EAAG,SAAS,CAAC,EACzClE,IAAM,KAAa0E,EAAE1E,EAAG,CAAmB,EAC3CA,IAAM,KAAa0E,EAAE1E,EAAG6E,EAAIT,IAAkB,KAAK,CAAC,EACpDpE,IAAM,KAAa0E,EAAE1E,EAAG,CAAmB,EAC3CA,IAAM,KAAa0E,EAAE1E,EAAG6E,EAAIT,IAAkB,YAAY,CAAC,EAC3DpE,IAAM,KAAa0E,EAAE1E,EAAG,CAAK,EAC7BA,IAAM,IAAMA,IAAM,MAAQA,IAAM,KAAa0E,EAAE1E,EAAG6E,EAAIN,IAAWC,CAAG,CAAC,EACrExE,IAAM,KAAa0E,EAAE1E,EAAG6E,EAAIX,EAAG,kBAAkB,CAAC,EAClDlE,IAAM,KAAa0E,EAAE1E,EAAG6E,EAAIX,EAAG,kBAAkB,CAAC,EAClDlE,IAAM,MAAa,KAAK,gBAAgB,WAAW,cAAc,eAAiB0E,EAAE1E,EAAG6E,EAAIX,EAAG,cAAc,CAAC,EAC1GQ,EAAE1E,EAAG,CAAgB,CAC9B,CAKQ,iBAAiB+E,EAAeC,EAAcC,EAAYC,EAAYC,EAAoB,CAChG,OAAIH,IAAS,GACXD,GAAS,SACTA,GAAS,UACTA,GAASK,EAAc,aAAa,CAACH,EAAIC,EAAIC,CAAE,CAAC,GACvCH,IAAS,IAClBD,GAAS,UACTA,GAAS,SAAsBE,EAAK,KAE/BF,CACT,CAMQ,cAAchG,EAAiBuC,EAAa+D,EAA8B,CAKhF,IAAMC,EAAO,CAAC,EAAG,EAAG,GAAI,EAAG,EAAG,CAAC,EAG3BC,EAAS,EAGTC,EAAU,EAEd,EAAG,CAED,GADAF,EAAKE,EAAUD,CAAM,EAAIxG,EAAO,OAAOuC,EAAMkE,CAAO,EAChDzG,EAAO,aAAauC,EAAMkE,CAAO,EAAG,CACtC,IAAMC,EAAY1G,EAAO,aAAauC,EAAMkE,CAAO,EAC/C/E,EAAI,EACR,GACM6E,EAAK,CAAC,IAAM,IACdC,EAAS,GAEXD,EAAKE,EAAU/E,EAAI,EAAI8E,CAAM,EAAIE,EAAUhF,CAAC,QACrC,EAAEA,EAAIgF,EAAU,QAAUhF,EAAI+E,EAAU,EAAID,EAASD,EAAK,QACnE,KACF,CAEA,GAAKA,EAAK,CAAC,IAAM,GAAKE,EAAUD,GAAU,GACpCD,EAAK,CAAC,IAAM,GAAKE,EAAUD,GAAU,EACzC,MAGED,EAAK,CAAC,IACRC,EAAS,EAEb,OAAS,EAAEC,EAAUlE,EAAMvC,EAAO,QAAUyG,EAAUD,EAASD,EAAK,QAGpE,QAAS7E,EAAI,EAAGA,EAAI6E,EAAK,OAAQ,EAAE7E,EAC7B6E,EAAK7E,CAAC,IAAM,KACd6E,EAAK7E,CAAC,EAAI,GAKd,OAAQ6E,EAAK,CAAC,EAAG,CACf,IAAK,IACHD,EAAK,GAAK,KAAK,iBAAiBA,EAAK,GAAIC,EAAK,CAAC,EAAGA,EAAK,CAAC,EAAGA,EAAK,CAAC,EAAGA,EAAK,CAAC,CAAC,EAC3E,MACF,IAAK,IACHD,EAAK,GAAK,KAAK,iBAAiBA,EAAK,GAAIC,EAAK,CAAC,EAAGA,EAAK,CAAC,EAAGA,EAAK,CAAC,EAAGA,EAAK,CAAC,CAAC,EAC3E,MACF,IAAK,IACHD,EAAK,SAAWA,EAAK,SAAS,MAAM,EACpCA,EAAK,SAAS,eAAiB,KAAK,iBAAiBA,EAAK,SAAS,eAAgBC,EAAK,CAAC,EAAGA,EAAK,CAAC,EAAGA,EAAK,CAAC,EAAGA,EAAK,CAAC,CAAC,CACzH,CAEA,OAAOE,CACT,CAWQ,kBAAkBE,EAAeL,EAA4B,CAGnEA,EAAK,SAAWA,EAAK,SAAS,MAAM,GAGhC,CAAC,CAACK,GAASA,EAAQ,KACrBA,EAAQ,GAEVL,EAAK,SAAS,eAAiBK,EAC/BL,EAAK,IAAM,UAGPK,IAAU,IACZL,EAAK,IAAM,YAIbA,EAAK,eAAe,CACtB,CAEQ,aAAaA,EAA4B,CAC/CA,EAAK,GAAK3G,EAAkB,GAC5B2G,EAAK,GAAK3G,EAAkB,GAC5B2G,EAAK,SAAWA,EAAK,SAAS,MAAM,EAGpCA,EAAK,SAAS,eAAiB,EAC/BA,EAAK,SAAS,gBAAkB,UAChCA,EAAK,eAAe,CACtB,CAqFO,eAAetG,EAA0B,CAE9C,GAAIA,EAAO,SAAW,GAAKA,EAAO,OAAO,CAAC,IAAM,EAC9C,YAAK,aAAa,KAAK,YAAY,EAC5B,GAGT,IAAM4G,EAAI5G,EAAO,OACbiB,EACEqF,EAAO,KAAK,aAElB,QAAS5E,EAAI,EAAGA,EAAIkF,EAAGlF,IACrBT,EAAIjB,EAAO,OAAO0B,CAAC,EACfT,GAAK,IAAMA,GAAK,IAElBqF,EAAK,IAAM,UACXA,EAAK,IAAM,SAAqBrF,EAAI,IAC3BA,GAAK,IAAMA,GAAK,IAEzBqF,EAAK,IAAM,UACXA,EAAK,IAAM,SAAqBrF,EAAI,IAC3BA,GAAK,IAAMA,GAAK,IAEzBqF,EAAK,IAAM,UACXA,EAAK,IAAM,SAAqBrF,EAAI,GAAM,GACjCA,GAAK,KAAOA,GAAK,KAE1BqF,EAAK,IAAM,UACXA,EAAK,IAAM,SAAqBrF,EAAI,IAAO,GAClCA,IAAM,EAEf,KAAK,aAAaqF,CAAI,EACbrF,IAAM,EAEfqF,EAAK,IAAM,UACFrF,IAAM,EAEfqF,EAAK,IAAM,SACFrF,IAAM,GAEfqF,EAAK,IAAM,UACX,KAAK,kBAAkBtG,EAAO,aAAa0B,CAAC,EAAI1B,EAAO,aAAa0B,CAAC,EAAG,CAAC,IAA2B4E,CAAI,GAC/FrF,IAAM,EAEfqF,EAAK,IAAM,UACFrF,IAAM,EAGfqF,EAAK,IAAM,SACFrF,IAAM,EAEfqF,EAAK,IAAM,WACFrF,IAAM,EAEfqF,EAAK,IAAM,WACFrF,IAAM,EAEfqF,EAAK,IAAM,UACFrF,IAAM,GAEf,KAAK,oBAAyCqF,CAAI,EACzCrF,IAAM,IAEfqF,EAAK,IAAM,WACXA,EAAK,IAAM,YACFrF,IAAM,GAEfqF,EAAK,IAAM,UACFrF,IAAM,IAEfqF,EAAK,IAAM,WACX,KAAK,oBAAuCA,CAAI,GACvCrF,IAAM,GAEfqF,EAAK,IAAM,WACFrF,IAAM,GAEfqF,EAAK,IAAM,UACFrF,IAAM,GAEfqF,EAAK,IAAM,YACFrF,IAAM,GAEfqF,EAAK,IAAM,WACFrF,IAAM,IAEfqF,EAAK,IAAM,UACXA,EAAK,IAAM3G,EAAkB,GAAK,UACzBsB,IAAM,IAEfqF,EAAK,IAAM,UACXA,EAAK,IAAM3G,EAAkB,GAAK,UACzBsB,IAAM,IAAMA,IAAM,IAAMA,IAAM,GAEvCS,GAAK,KAAK,cAAc1B,EAAQ0B,EAAG4E,CAAI,EAC9BrF,IAAM,GAEfqF,EAAK,IAAM,WACFrF,IAAM,GAEfqF,EAAK,IAAM,YACFrF,IAAM,MAAQ,KAAK,gBAAgB,WAAW,cAAc,0BAA4B,IAEjGqF,EAAK,IAAM,WACFrF,IAAM,MAAQ,KAAK,gBAAgB,WAAW,cAAc,0BAA4B,IAEjGqF,EAAK,IAAM,WACFrF,IAAM,IACfqF,EAAK,SAAWA,EAAK,SAAS,MAAM,EACpCA,EAAK,SAAS,eAAiB,GAC/BA,EAAK,eAAe,GAEpB,KAAK,YAAY,MAAM,6BAA8BrF,CAAC,EAG1D,MAAO,EACT,CA2BO,aAAajB,EAA0B,CAC5C,OAAQA,EAAO,OAAO,CAAC,EAAG,CACxB,IAAK,GAEH,KAAK,aAAa,0BAA+B,EACjD,MACF,IAAK,GAEH,IAAM4D,EAAI,KAAK,cAAc,EAAI,EAC3BD,EAAI,KAAK,cAAc,EAAI,EACjC,KAAK,aAAa,iBAAiB,QAAaC,CAAC,IAAID,CAAC,GAAG,EACzD,KACJ,CACA,MAAO,EACT,CAGO,oBAAoB3D,EAA0B,CAGnD,OAAQA,EAAO,OAAO,CAAC,EAAG,CACxB,IAAK,GAEH,IAAM4D,EAAI,KAAK,cAAc,EAAI,EAC3BD,EAAI,KAAK,cAAc,EAAI,EACjC,KAAK,aAAa,iBAAiB,SAAcC,CAAC,IAAID,CAAC,GAAG,EAC1D,MACF,IAAK,IAGH,MACF,IAAK,IAGH,MACF,IAAK,IAGH,MACF,IAAK,IAGH,MACF,IAAK,MAEC,KAAK,gBAAgB,WAAW,cAAc,kBAAoB,KACpE,KAAK,2BAA2B,KAAK,EAEvC,KACJ,CACA,MAAO,EACT,CAsBO,UAAU3D,EAA0B,CACzC,YAAK,aAAa,eAAiB,GACnC,KAAK,wBAAwB,KAAK,EAClC,KAAK,cAAc,UAAY,EAC/B,KAAK,cAAc,aAAe,KAAK,eAAe,KAAO,EAC7D,KAAK,aAAeL,EAAkB,MAAM,EAC5C,KAAK,aAAa,MAAM,EACxB,KAAK,gBAAgB,MAAM,EAG3B,KAAK,cAAc,OAAS,EAC5B,KAAK,cAAc,OAAS,KAAK,cAAc,MAC/C,KAAK,cAAc,iBAAiB,GAAK,KAAK,aAAa,GAC3D,KAAK,cAAc,iBAAiB,GAAK,KAAK,aAAa,GAC3D,KAAK,cAAc,aAAe,KAAK,gBAAgB,QAGvD,KAAK,aAAa,gBAAgB,OAAS,GACpC,EACT,CAsBO,eAAeK,EAA0B,CAC9C,IAAM+D,EAAQ/D,EAAO,SAAW,EAAI,EAAIA,EAAO,OAAO,CAAC,EACvD,GAAI+D,IAAU,EACZ,KAAK,aAAa,gBAAgB,YAAc,OAChD,KAAK,aAAa,gBAAgB,YAAc,WAC3C,CACL,OAAQA,EAAO,CACb,IAAK,GACL,IAAK,GACH,KAAK,aAAa,gBAAgB,YAAc,QAChD,MACF,IAAK,GACL,IAAK,GACH,KAAK,aAAa,gBAAgB,YAAc,YAChD,MACF,IAAK,GACL,IAAK,GACH,KAAK,aAAa,gBAAgB,YAAc,MAChD,KACJ,CACA,IAAM8C,EAAa9C,EAAQ,IAAM,EACjC,KAAK,aAAa,gBAAgB,YAAc8C,CAClD,CACA,MAAO,EACT,CASO,gBAAgB7G,EAA0B,CAC/C,IAAM8G,EAAM9G,EAAO,OAAO,CAAC,GAAK,EAC5B+G,EAEJ,OAAI/G,EAAO,OAAS,IAAM+G,EAAS/G,EAAO,OAAO,CAAC,GAAK,KAAK,eAAe,MAAQ+G,IAAW,KAC5FA,EAAS,KAAK,eAAe,MAG3BA,EAASD,IACX,KAAK,cAAc,UAAYA,EAAM,EACrC,KAAK,cAAc,aAAeC,EAAS,EAC3C,KAAK,WAAW,EAAG,CAAC,GAEf,EACT,CAgCO,cAAc/G,EAA0B,CAC7C,GAAI,CAACsD,GAAoBtD,EAAO,OAAO,CAAC,EAAG,KAAK,gBAAgB,WAAW,aAAa,EACtF,MAAO,GAET,IAAMgH,EAAUhH,EAAO,OAAS,EAAKA,EAAO,OAAO,CAAC,EAAI,EACxD,OAAQA,EAAO,OAAO,CAAC,EAAG,CACxB,IAAK,IACCgH,IAAW,GACb,KAAK,+BAA+B,KAAK,CAA4C,EAEvF,MACF,IAAK,IACH,KAAK,+BAA+B,KAAK,CAA6C,EACtF,MACF,IAAK,IACC,KAAK,gBACP,KAAK,aAAa,iBAAiB,UAAe,KAAK,eAAe,IAAI,IAAI,KAAK,eAAe,IAAI,GAAG,EAE3G,MACF,IAAK,KACCA,IAAW,GAAKA,IAAW,KAC7B,KAAK,kBAAkB,KAAK,KAAK,YAAY,EACzC,KAAK,kBAAkB,OAAS,IAClC,KAAK,kBAAkB,MAAM,IAG7BA,IAAW,GAAKA,IAAW,KAC7B,KAAK,eAAe,KAAK,KAAK,SAAS,EACnC,KAAK,eAAe,OAAS,IAC/B,KAAK,eAAe,MAAM,GAG9B,MACF,IAAK,KACCA,IAAW,GAAKA,IAAW,IACzB,KAAK,kBAAkB,QACzB,KAAK,SAAS,KAAK,kBAAkB,IAAI,CAAE,GAG3CA,IAAW,GAAKA,IAAW,IACzB,KAAK,eAAe,QACtB,KAAK,YAAY,KAAK,eAAe,IAAI,CAAE,EAG/C,KACJ,CACA,MAAO,EACT,CAWO,WAAWhH,EAA2B,CAC3C,YAAK,cAAc,OAAS,KAAK,cAAc,EAC/C,KAAK,cAAc,OAAS,KAAK,cAAc,MAAQ,KAAK,cAAc,EAC1E,KAAK,cAAc,iBAAiB,GAAK,KAAK,aAAa,GAC3D,KAAK,cAAc,iBAAiB,GAAK,KAAK,aAAa,GAC3D,KAAK,cAAc,aAAe,KAAK,gBAAgB,QACvD,KAAK,cAAc,cAAgB,KAAK,gBAAgB,SAAS,MAAM,EACvE,KAAK,cAAc,YAAc,KAAK,gBAAgB,OACtD,KAAK,cAAc,gBAAkB,KAAK,aAAa,gBAAgB,OACvE,KAAK,cAAc,oBAAsB,KAAK,aAAa,gBAAgB,WACpE,EACT,CAWO,cAAcA,EAA2B,CAC9C,KAAK,cAAc,EAAI,KAAK,cAAc,QAAU,EACpD,KAAK,cAAc,EAAI,KAAK,IAAI,KAAK,cAAc,OAAS,KAAK,cAAc,MAAO,CAAC,EACvF,KAAK,aAAa,GAAK,KAAK,cAAc,iBAAiB,GAC3D,KAAK,aAAa,GAAK,KAAK,cAAc,iBAAiB,GAC3D,QAAS0B,EAAI,EAAGA,EAAI,KAAK,cAAc,cAAc,OAAQA,IAC3D,KAAK,gBAAgB,YAAYA,EAAG,KAAK,cAAc,cAAcA,CAAC,CAAC,EAEzE,YAAK,gBAAgB,UAAU,KAAK,cAAc,WAAW,EAC7D,KAAK,aAAa,gBAAgB,OAAS,KAAK,cAAc,gBAC9D,KAAK,aAAa,gBAAgB,WAAa,KAAK,cAAc,oBAClE,KAAK,gBAAgB,EACd,EACT,CAaO,SAAStB,EAAuB,CACrC,YAAK,aAAeA,EACpB,KAAK,eAAe,KAAKA,CAAI,EACtB,EACT,CAMO,YAAYA,EAAuB,CACxC,YAAK,UAAYA,EACV,EACT,CAWO,wBAAwBA,EAAuB,CACpD,IAAM6G,EAAqB,CAAC,EACtBC,EAAQ9G,EAAK,MAAM,GAAG,EAC5B,KAAO8G,EAAM,OAAS,GAAG,CACvB,IAAMC,EAAMD,EAAM,MAAM,EAClBE,EAAOF,EAAM,MAAM,EACzB,GAAI,QAAQ,KAAKC,CAAG,EAAG,CACrB,IAAME,EAAQ,SAASF,EAAK,EAAE,EAC9B,GAAIG,GAAkBD,CAAK,EACzB,GAAID,IAAS,IACXH,EAAM,KAAK,CAAE,OAA+B,MAAAI,CAAM,CAAC,MAC9C,CACL,IAAMrB,EAAQuB,GAAWH,CAAI,EACzBpB,GACFiB,EAAM,KAAK,CAAE,OAA4B,MAAAI,EAAO,MAAArB,CAAM,CAAC,CAE3D,CAEJ,CACF,CACA,OAAIiB,EAAM,QACR,KAAK,SAAS,KAAKA,CAAK,EAEnB,EACT,CAmBO,aAAa7G,EAAuB,CAEzC,IAAM+G,EAAM/G,EAAK,QAAQ,GAAG,EAC5B,GAAI+G,IAAQ,GAEV,MAAO,GAET,IAAM/D,EAAKhD,EAAK,MAAM,EAAG+G,CAAG,EAAE,KAAK,EAC7BK,EAAMpH,EAAK,MAAM+G,EAAM,CAAC,EAC9B,OAAIK,EACK,KAAK,iBAAiBpE,EAAIoE,CAAG,EAElCpE,EAAG,KAAK,EACH,GAEF,KAAK,iBAAiB,CAC/B,CAEQ,iBAAiBpD,EAAgBwH,EAAsB,CAEzD,KAAK,kBAAkB,GACzB,KAAK,iBAAiB,EAExB,IAAMC,EAAezH,EAAO,MAAM,GAAG,EACjCoD,EACEsE,EAAeD,EAAa,UAAU3H,GAAKA,EAAE,WAAW,KAAK,CAAC,EACpE,OAAI4H,IAAiB,KACnBtE,EAAKqE,EAAaC,CAAY,EAAE,MAAM,CAAC,GAAK,QAE9C,KAAK,aAAa,SAAW,KAAK,aAAa,SAAS,MAAM,EAC9D,KAAK,aAAa,SAAS,MAAQ,KAAK,gBAAgB,aAAa,CAAE,GAAAtE,EAAI,IAAAoE,CAAI,CAAC,EAChF,KAAK,aAAa,eAAe,EAC1B,EACT,CAEQ,kBAA4B,CAClC,YAAK,aAAa,SAAW,KAAK,aAAa,SAAS,MAAM,EAC9D,KAAK,aAAa,SAAS,MAAQ,EACnC,KAAK,aAAa,eAAe,EAC1B,EACT,CAUQ,yBAAyBpH,EAAc8C,EAAyB,CACtE,IAAMgE,EAAQ9G,EAAK,MAAM,GAAG,EAC5B,QAASsB,EAAI,EAAGA,EAAIwF,EAAM,QACpB,EAAAhE,GAAU,KAAK,eAAe,QADF,EAAExB,EAAG,EAAEwB,EAEvC,GAAIgE,EAAMxF,CAAC,IAAM,IACf,KAAK,SAAS,KAAK,CAAC,CAAE,OAA+B,MAAO,KAAK,eAAewB,CAAM,CAAE,CAAC,CAAC,MACrF,CACL,IAAM8C,EAAQuB,GAAWL,EAAMxF,CAAC,CAAC,EAC7BsE,GACF,KAAK,SAAS,KAAK,CAAC,CAAE,OAA4B,MAAO,KAAK,eAAe9C,CAAM,EAAG,MAAA8C,CAAM,CAAC,CAAC,CAElG,CAEF,MAAO,EACT,CAwBO,mBAAmB5F,EAAuB,CAC/C,OAAO,KAAK,yBAAyBA,EAAM,CAAC,CAC9C,CAOO,mBAAmBA,EAAuB,CAC/C,OAAO,KAAK,yBAAyBA,EAAM,CAAC,CAC9C,CAOO,uBAAuBA,EAAuB,CACnD,OAAO,KAAK,yBAAyBA,EAAM,CAAC,CAC9C,CAUO,oBAAoBA,EAAuB,CAChD,GAAI,CAACA,EACH,YAAK,SAAS,KAAK,CAAC,CAAE,MAA+B,CAAC,CAAC,EAChD,GAET,IAAM6G,EAAqB,CAAC,EACtBC,EAAQ9G,EAAK,MAAM,GAAG,EAC5B,QAASsB,EAAI,EAAGA,EAAIwF,EAAM,OAAQ,EAAExF,EAClC,GAAI,QAAQ,KAAKwF,EAAMxF,CAAC,CAAC,EAAG,CAC1B,IAAM2F,EAAQ,SAASH,EAAMxF,CAAC,EAAG,EAAE,EAC/B4F,GAAkBD,CAAK,GACzBJ,EAAM,KAAK,CAAE,OAAgC,MAAAI,CAAM,CAAC,CAExD,CAEF,OAAIJ,EAAM,QACR,KAAK,SAAS,KAAKA,CAAK,EAEnB,EACT,CAOO,eAAe7G,EAAuB,CAC3C,YAAK,SAAS,KAAK,CAAC,CAAE,OAAgC,SAAoC,CAAC,CAAC,EACrF,EACT,CAOO,eAAeA,EAAuB,CAC3C,YAAK,SAAS,KAAK,CAAC,CAAE,OAAgC,SAAoC,CAAC,CAAC,EACrF,EACT,CAOO,mBAAmBA,EAAuB,CAC/C,YAAK,SAAS,KAAK,CAAC,CAAE,OAAgC,SAAgC,CAAC,CAAC,EACjF,EACT,CAWO,UAAoB,CACzB,YAAK,cAAc,EAAI,EACvB,KAAK,MAAM,EACJ,EACT,CAOO,uBAAiC,CACtC,YAAK,YAAY,MAAM,2CAA2C,EAClE,KAAK,aAAa,gBAAgB,kBAAoB,GACtD,KAAK,wBAAwB,KAAK,EAC3B,EACT,CAOO,mBAA6B,CAClC,YAAK,YAAY,MAAM,kCAAkC,EACzD,KAAK,aAAa,gBAAgB,kBAAoB,GACtD,KAAK,wBAAwB,KAAK,EAC3B,EACT,CAQO,sBAAgC,CACrC,YAAK,gBAAgB,UAAU,CAAC,EAChC,KAAK,gBAAgB,YAAY,EAAG4E,CAAe,EAC5C,EACT,CAkBO,cAAc2C,EAAiC,CACpD,OAAIA,EAAe,SAAW,GAC5B,KAAK,qBAAqB,EACnB,KAELA,EAAe,CAAC,IAAM,KAG1B,KAAK,gBAAgB,YAAYC,GAAOD,EAAe,CAAC,CAAC,EAAGjH,EAASiH,EAAe,CAAC,CAAC,GAAK3C,CAAe,EACnG,GACT,CAWO,OAAiB,CACtB,YAAK,gBAAgB,EACrB,KAAK,cAAc,IACf,KAAK,cAAc,IAAM,KAAK,cAAc,aAAe,GAC7D,KAAK,cAAc,IACnB,KAAK,eAAe,OAAO,KAAK,eAAe,CAAC,GACvC,KAAK,cAAc,GAAK,KAAK,eAAe,OACrD,KAAK,cAAc,EAAI,KAAK,eAAe,KAAO,GAEpD,KAAK,gBAAgB,EACd,EACT,CAYO,QAAkB,CACvB,YAAK,cAAc,KAAK,KAAK,cAAc,CAAC,EAAI,GACzC,EACT,CAWO,cAAwB,CAE7B,GADA,KAAK,gBAAgB,EACjB,KAAK,cAAc,IAAM,KAAK,cAAc,UAAW,CAIzD,IAAM6C,EAAqB,KAAK,cAAc,aAAe,KAAK,cAAc,UAChF,KAAK,cAAc,MAAM,cAAc,KAAK,cAAc,MAAQ,KAAK,cAAc,EAAGA,EAAoB,CAAC,EAC7G,KAAK,cAAc,MAAM,IAAI,KAAK,cAAc,MAAQ,KAAK,cAAc,EAAG,KAAK,cAAc,aAAa,KAAK,eAAe,CAAC,CAAC,EACpI,KAAK,iBAAiB,eAAe,KAAK,cAAc,UAAW,KAAK,cAAc,YAAY,CACpG,MACE,KAAK,cAAc,IACnB,KAAK,gBAAgB,EAEvB,MAAO,EACT,CASO,WAAqB,CAC1B,YAAK,QAAQ,MAAM,EACnB,KAAK,gBAAgB,KAAK,EACnB,EACT,CAEO,OAAc,CACnB,KAAK,aAAelI,EAAkB,MAAM,EAC5C,KAAK,uBAAyBA,EAAkB,MAAM,CACxD,CAKQ,gBAAiC,CACvC,YAAK,uBAAuB,IAAM,UAClC,KAAK,uBAAuB,IAAM,KAAK,aAAa,GAAK,SAClD,KAAK,sBACd,CAYO,UAAUmI,EAAwB,CACvC,YAAK,gBAAgB,UAAUA,CAAK,EAC7B,EACT,CAUO,wBAAkC,CAEvC,IAAMC,EAAO,IAAIC,EACjBD,EAAK,QAAU,GAAK,GAAsB,GAC1CA,EAAK,GAAK,KAAK,aAAa,GAC5BA,EAAK,GAAK,KAAK,aAAa,GAG5B,KAAK,WAAW,EAAG,CAAC,EACpB,QAASE,EAAU,EAAGA,EAAU,KAAK,eAAe,KAAM,EAAEA,EAAS,CACnE,IAAM5D,EAAM,KAAK,cAAc,MAAQ,KAAK,cAAc,EAAI4D,EACxDzE,EAAO,KAAK,cAAc,MAAM,IAAIa,CAAG,EACzCb,IACFA,EAAK,KAAKuE,CAAI,EACdvE,EAAK,UAAY,GAErB,CACA,YAAK,iBAAiB,aAAa,EACnC,KAAK,WAAW,EAAG,CAAC,EACb,EACT,CA6BO,oBAAoBpD,EAAcJ,EAA0B,CACjE,IAAM2F,EAAKuC,IACT,KAAK,aAAa,iBAAiB,OAAYA,CAAC,QAAa,EACtD,IAIHC,EAAI,KAAK,eAAe,OACxBzC,EAAO,KAAK,gBAAgB,WAC5B0C,EAAoC,CAAE,MAAS,EAAG,UAAa,EAAG,IAAO,CAAE,EAEjF,OAA0BzC,EAAtBvF,IAAS,KAAe,OAAO,KAAK,aAAa,YAAY,EAAI,EAAI,CAAC,KACtEA,IAAS,KAAe,aACxBA,IAAS,IAAc,OAAO+H,EAAE,UAAY,CAAC,IAAIA,EAAE,aAAe,CAAC,IAEnE/H,IAAS,IAAc,SACvBA,IAAS,KAAe,OAAOgI,EAAO1C,EAAK,WAAW,GAAKA,EAAK,YAAc,EAAI,EAAE,KAC/E,MANqE,CAOhF,CAEO,eAAe2C,EAAYC,EAAkB,CAClD,KAAK,iBAAiB,eAAeD,EAAIC,CAAE,CAC7C,CAWO,iBAAiBtI,EAA0B,CAChD,GAAI,CAAC,KAAK,gBAAgB,WAAW,cAAc,cACjD,MAAO,GAET,IAAMuI,EAAQvI,EAAO,OAAO,CAAC,GAAK,EAC5BiG,EAAOjG,EAAO,OAAS,GAAKA,EAAO,OAAO,CAAC,GAAK,EAChDW,EAAQ,KAAK,aAAa,cAEhC,OAAQsF,EAAM,CACZ,IAAK,GACHtF,EAAM,MAAQ4H,EACd,MACF,IAAK,GACH5H,EAAM,OAAS4H,EACf,MACF,IAAK,GACH5H,EAAM,OAAS,CAAC4H,EAChB,KACJ,CACA,MAAO,EACT,CASO,mBAAmBvI,EAA0B,CAClD,GAAI,CAAC,KAAK,gBAAgB,WAAW,cAAc,cACjD,MAAO,GAET,IAAMuI,EAAQ,KAAK,aAAa,cAAc,MAC9C,YAAK,aAAa,iBAAiB,SAAcA,CAAK,GAAG,EAClD,EACT,CAQO,kBAAkBvI,EAA0B,CACjD,GAAI,CAAC,KAAK,gBAAgB,WAAW,cAAc,cACjD,MAAO,GAET,IAAMuI,EAAQvI,EAAO,OAAO,CAAC,GAAK,EAC5BW,EAAQ,KAAK,aAAa,cAE1B6H,EADQ,KAAK,eAAe,SAAW,KAAK,eAAe,QAAQ,IACnD7H,EAAM,SAAWA,EAAM,UAG7C,OAAI6H,EAAM,QAAU,IAClBA,EAAM,MAAM,EAIdA,EAAM,KAAK7H,EAAM,KAAK,EACtBA,EAAM,MAAQ4H,EACP,EACT,CAQO,iBAAiBvI,EAA0B,CAChD,GAAI,CAAC,KAAK,gBAAgB,WAAW,cAAc,cACjD,MAAO,GAET,IAAMyI,EAAQ,KAAK,IAAI,EAAGzI,EAAO,OAAO,CAAC,GAAK,CAAC,EACzCW,EAAQ,KAAK,aAAa,cAE1B6H,EADQ,KAAK,eAAe,SAAW,KAAK,eAAe,QAAQ,IACnD7H,EAAM,SAAWA,EAAM,UAG7C,QAASe,EAAI,EAAGA,EAAI+G,GAASD,EAAM,OAAS,EAAG9G,IAC7Cf,EAAM,MAAQ6H,EAAM,IAAI,EAG1B,OAAIA,EAAM,SAAW,GAAKC,EAAQ,IAChC9H,EAAM,MAAQ,GAET,EACT,CAGF,EAYMd,GAAN,KAAkD,CAIhD,YACmCd,EACjC,CADiC,oBAAAA,EAEjC,KAAK,WAAW,CAClB,CAEO,YAAmB,CACxB,KAAK,MAAQ,KAAK,eAAe,OAAO,EACxC,KAAK,IAAM,KAAK,eAAe,OAAO,CACxC,CAEO,UAAU6E,EAAiB,CAC5BA,EAAI,KAAK,MACX,KAAK,MAAQA,EACJA,EAAI,KAAK,MAClB,KAAK,IAAMA,EAEf,CAEO,eAAeyE,EAAYC,EAAkB,CAC9CD,EAAKC,IACP1J,GAAQyJ,EACRA,EAAKC,EACLA,EAAK1J,IAEHyJ,EAAK,KAAK,QACZ,KAAK,MAAQA,GAEXC,EAAK,KAAK,MACZ,KAAK,IAAMA,EAEf,CAEO,cAAqB,CAC1B,KAAK,eAAe,EAAG,KAAK,eAAe,KAAO,CAAC,CACrD,CACF,EAxCMzI,GAAN6I,EAAA,CAKKC,EAAA,EAAAC,IALC/I,IA0CC,SAASyH,GAAkBvB,EAAoC,CACpE,MAAO,IAAKA,GAASA,EAAQ,GAC/B,CC1lHO,IAAM8C,GAAN,KAA0C,CAA1C,cACL,KAAQ,OAAc,GACtB,KAAQ,YAAc,GAEf,SAAgB,CACrB,KAAK,OAAO,EACZ,KAAK,YAAc,EACrB,CAEO,QAAe,CAChB,KAAK,SAAW,KAClB,aAAa,KAAK,MAAM,EACxB,KAAK,OAAS,GAElB,CAEO,aAAaC,EAAoBC,EAAuB,CAC7D,GAAI,KAAK,YACP,MAAM,IAAI,MAAM,iDAAiD,EAEnE,KAAK,OAAO,EACZ,KAAK,OAAS,WAAW,IAAM,CAC7B,KAAK,OAAS,GACdD,EAAO,CACT,EAAGC,CAAO,CACZ,CAEO,YAAYD,EAAoBC,EAAuB,CAC5D,GAAI,KAAK,YACP,MAAM,IAAI,MAAM,gDAAgD,EAE9D,KAAK,SAAW,KAGpB,KAAK,OAAS,WAAW,IAAM,CAC7B,KAAK,OAAS,GACdD,EAAO,CACT,EAAGC,CAAO,EACZ,CACF,ECtCO,IAAMC,GAAN,cAA0BC,CAAW,CAa1C,YAAoBC,EAA0F,CAC5G,MAAM,EADY,aAAAA,EAZpB,KAAQ,aAAwC,CAAC,EACjD,KAAQ,WAA2C,CAAC,EACpD,KAAQ,aAAe,EACvB,KAAQ,cAAgB,EACxB,KAAQ,eAAiB,GACzB,KAAQ,WAAa,EACrB,KAAQ,cAAgB,GAExB,KAAiB,iBAAmB,KAAK,UAAU,IAAIC,EAAc,EACrE,KAAiB,eAAiB,KAAK,UAAU,IAAIC,CAAe,EACpE,KAAgB,cAAgB,KAAK,eAAe,MAIlD,KAAK,UAAUC,EAAa,IAAM,CAChC,KAAK,aAAa,OAAS,EAC3B,KAAK,WAAW,OAAS,EACzB,KAAK,aAAe,EACpB,KAAK,cAAgB,CACvB,CAAC,CAAC,CACJ,CAEO,iBAAwB,CAC7B,KAAK,cAAgB,EACvB,CAUO,WAAkB,CAKvB,GAJI,KAAK,OAAO,YAIZ,KAAK,eACP,OAEF,KAAK,eAAiB,GAGtB,IAAIC,EACAC,EAAa,GACjB,KAAOD,EAAQ,KAAK,aAAa,MAAM,GAAG,CACxCC,EAAa,GACb,KAAK,QAAQD,CAAK,EAClB,IAAME,EAAK,KAAK,WAAW,MAAM,EAC7BA,GAAIA,EAAG,CACb,CAGA,KAAK,aAAe,EACpB,KAAK,cAAgB,WACrB,KAAK,aAAa,OAAS,EAC3B,KAAK,WAAW,OAAS,EAEzB,KAAK,eAAiB,GAClBD,GACF,KAAK,eAAe,KAAK,CAE7B,CAKO,UAAUE,EAA2BC,EAAmC,CAC7E,GAAI,KAAK,OAAO,WACd,OAKF,GAAIA,IAAuB,QAAa,KAAK,WAAaA,EAAoB,CAG5E,KAAK,WAAa,EAClB,MACF,CASA,GAPA,KAAK,cAAgBD,EAAK,OAC1B,KAAK,aAAa,KAAKA,CAAI,EAC3B,KAAK,WAAW,KAAK,MAAS,EAG9B,KAAK,aAED,KAAK,eACP,OAEF,KAAK,eAAiB,GAMtB,IAAIH,EACJ,KAAOA,EAAQ,KAAK,aAAa,MAAM,GAAG,CACxC,KAAK,QAAQA,CAAK,EAClB,IAAME,EAAK,KAAK,WAAW,MAAM,EAC7BA,GAAIA,EAAG,CACb,CAGA,KAAK,aAAe,EACpB,KAAK,cAAgB,WAGrB,KAAK,eAAiB,GACtB,KAAK,WAAa,CACpB,CAEO,MAAMC,EAA2BE,EAA6B,CACnE,GAAI,MAAK,OAAO,WAGhB,IAAI,KAAK,aAAe,IACtB,MAAM,IAAI,MAAM,6DAA6D,EAI/E,GAAI,CAAC,KAAK,aAAa,OAAQ,CAM7B,GALA,KAAK,cAAgB,EAKjB,KAAK,cAAe,CACtB,KAAK,cAAgB,GACrB,KAAK,cAAgBF,EAAK,OAC1B,KAAK,aAAa,KAAKA,CAAI,EAC3B,KAAK,WAAW,KAAKE,CAAQ,EAC7B,KAAK,YAAY,EACjB,MACF,CAEA,KAAK,oBAAoB,CAC3B,CAEA,KAAK,cAAgBF,EAAK,OAC1B,KAAK,aAAa,KAAKA,CAAI,EAC3B,KAAK,WAAW,KAAKE,CAAQ,EAC/B,CA8BQ,oBAAoBC,EAAmB,EAAGC,EAAyB,GAAY,CACjF,KAAK,OAAO,YAGhB,KAAK,iBAAiB,aAAa,IAAM,KAAK,YAAYD,EAAUC,CAAa,EAAG,CAAC,CACvF,CAEU,YAAYD,EAAmB,EAAGC,EAAyB,GAAY,CAC/E,GAAI,KAAK,OAAO,WACd,OAEF,IAAMC,EAAYF,GAAY,YAAY,IAAI,EAC9C,KAAO,KAAK,aAAa,OAAS,KAAK,eAAe,CACpD,IAAMH,EAAO,KAAK,aAAa,KAAK,aAAa,EAC3CM,EAAS,KAAK,QAAQN,EAAMI,CAAa,EAC/C,GAAIE,EAAQ,CAwBV,IAAMC,EAAsCC,GAAe,CACrD,KAAK,OAAO,aAGZ,YAAY,IAAI,EAAIH,GAAa,GACnC,KAAK,oBAAoB,EAAGG,CAAC,EAE7B,KAAK,YAAYH,EAAWG,CAAC,EAEjC,EAuBAF,EAAO,MAAMG,IACX,eAAe,IAAM,CAAC,MAAMA,CAAI,CAAC,EAC1B,QAAQ,QAAQ,EAAK,EAC7B,EAAE,KAAKF,CAAY,EACpB,MACF,CAEA,IAAMR,EAAK,KAAK,WAAW,KAAK,aAAa,EAK7C,GAJIA,GAAIA,EAAG,EACX,KAAK,gBACL,KAAK,cAAgBC,EAAK,OAEtB,YAAY,IAAI,EAAIK,GAAa,GACnC,KAEJ,CACI,KAAK,aAAa,OAAS,KAAK,eAG9B,KAAK,cAAgB,KACvB,KAAK,aAAe,KAAK,aAAa,MAAM,KAAK,aAAa,EAC9D,KAAK,WAAa,KAAK,WAAW,MAAM,KAAK,aAAa,EAC1D,KAAK,cAAgB,GAEvB,KAAK,oBAAoB,IAEzB,KAAK,aAAa,OAAS,EAC3B,KAAK,WAAW,OAAS,EACzB,KAAK,aAAe,EACpB,KAAK,cAAgB,GAEvB,KAAK,eAAe,KAAK,CAC3B,CACF,ECnTO,IAAMK,GAAN,KAAgD,CAiBrD,YACmCC,EACjC,CADiC,oBAAAA,EAfnC,KAAQ,QAAU,EAKlB,KAAQ,eAAmD,IAAI,IAO/D,KAAQ,cAAsE,IAAI,GAKlF,CAEO,aAAaC,EAA4B,CAC9C,IAAMC,EAAS,KAAK,eAAe,OAGnC,GAAID,EAAK,KAAO,OAAW,CACzB,IAAME,EAASD,EAAO,UAAUA,EAAO,MAAQA,EAAO,CAAC,EACjDE,EAA2B,CAC/B,KAAAH,EACA,GAAI,KAAK,UACT,MAAO,CAACE,CAAM,CAChB,EACA,OAAAA,EAAO,UAAU,IAAM,KAAK,sBAAsBC,EAAOD,CAAM,CAAC,EAChE,KAAK,cAAc,IAAIC,EAAM,GAAIA,CAAK,EAC/BA,EAAM,EACf,CAGA,IAAMC,EAAWJ,EACXK,EAAM,KAAK,eAAeD,CAAQ,EAClCE,EAAQ,KAAK,eAAe,IAAID,CAAG,EACzC,GAAIC,EACF,YAAK,cAAcA,EAAM,GAAIL,EAAO,MAAQA,EAAO,CAAC,EAC7CK,EAAM,GAIf,IAAMJ,EAASD,EAAO,UAAUA,EAAO,MAAQA,EAAO,CAAC,EACjDE,EAA6B,CACjC,GAAI,KAAK,UACT,IAAK,KAAK,eAAeC,CAAQ,EACjC,KAAMA,EACN,MAAO,CAACF,CAAM,CAChB,EACA,OAAAA,EAAO,UAAU,IAAM,KAAK,sBAAsBC,EAAOD,CAAM,CAAC,EAChE,KAAK,eAAe,IAAIC,EAAM,IAAKA,CAAK,EACxC,KAAK,cAAc,IAAIA,EAAM,GAAIA,CAAK,EAC/BA,EAAM,EACf,CAEO,cAAcI,EAAgBC,EAAiB,CACpD,IAAML,EAAQ,KAAK,cAAc,IAAII,CAAM,EAC3C,GAAKJ,GAGDA,EAAM,MAAM,MAAMM,GAAKA,EAAE,OAASD,CAAC,EAAG,CACxC,IAAMN,EAAS,KAAK,eAAe,OAAO,UAAUM,CAAC,EACrDL,EAAM,MAAM,KAAKD,CAAM,EACvBA,EAAO,UAAU,IAAM,KAAK,sBAAsBC,EAAOD,CAAM,CAAC,CAClE,CACF,CAEO,YAAYK,EAA0C,CAC3D,OAAO,KAAK,cAAc,IAAIA,CAAM,GAAG,IACzC,CAEQ,eAAeG,EAA0C,CAC/D,MAAO,GAAGA,EAAS,EAAE,KAAKA,EAAS,GAAG,EACxC,CAEQ,sBAAsBP,EAAgDD,EAAuB,CACnG,IAAMS,EAAQR,EAAM,MAAM,QAAQD,CAAM,EACpCS,IAAU,KAGdR,EAAM,MAAM,OAAOQ,EAAO,CAAC,EACvBR,EAAM,MAAM,SAAW,IACrBA,EAAM,KAAK,KAAO,QACpB,KAAK,eAAe,OAAQA,EAA8B,GAAG,EAE/D,KAAK,cAAc,OAAOA,EAAM,EAAE,GAEtC,CACF,EA9FaL,GAANc,EAAA,CAkBFC,EAAA,EAAAC,IAlBQhB,ICoCb,IAAIiB,GAA2B,GAgBTC,GAAf,cAAoCC,CAAoC,CAuD7E,YACEC,EACA,CACA,MAAM,EA5CR,KAAQ,2BAA6B,KAAK,UAAU,IAAIC,CAAmB,EAE3E,KAAiB,UAAY,KAAK,UAAU,IAAIC,CAAiB,EACjE,KAAgB,SAAW,KAAK,UAAU,MAC1C,KAAiB,QAAU,KAAK,UAAU,IAAIA,CAAiB,EAC/D,KAAgB,OAAS,KAAK,QAAQ,MACtC,KAAU,YAAc,KAAK,UAAU,IAAIA,CAAe,EAC1D,KAAgB,WAAa,KAAK,YAAY,MAC9C,KAAmB,UAAY,KAAK,UAAU,IAAIA,CAAyC,EAC3F,KAAgB,SAAW,KAAK,UAAU,MAC1C,KAAiB,UAAY,KAAK,UAAU,IAAIA,CAAyC,EACzF,KAAgB,SAAW,KAAK,UAAU,MAC1C,KAAmB,eAAiB,KAAK,UAAU,IAAIA,CAAe,EACtE,KAAgB,cAAgB,KAAK,eAAe,MAOpD,KAAU,UAAY,KAAK,UAAU,IAAIA,CAAuB,EA2B9D,KAAK,sBAAwB,IAAIC,GACjC,KAAK,eAAiB,KAAK,UAAU,IAAIC,GAAeJ,CAAO,CAAC,EAChE,KAAK,sBAAsB,WAAWK,EAAiB,KAAK,cAAc,EAC1E,KAAK,YAAc,KAAK,UAAU,KAAK,sBAAsB,eAAeC,CAAU,CAAC,EACvF,KAAK,sBAAsB,WAAWC,EAAa,KAAK,WAAW,EACnE,KAAK,eAAiB,KAAK,UAAU,KAAK,sBAAsB,eAAeC,CAAa,CAAC,EAC7F,KAAK,sBAAsB,WAAWC,EAAgB,KAAK,cAAc,EACzE,KAAK,YAAc,KAAK,UAAU,KAAK,sBAAsB,eAAeC,EAAW,CAAC,EACxF,KAAK,sBAAsB,WAAWC,GAAc,KAAK,WAAW,EACpE,KAAK,kBAAoB,KAAK,UAAU,KAAK,sBAAsB,eAAeC,EAAiB,CAAC,EACpG,KAAK,sBAAsB,WAAWC,GAAoB,KAAK,iBAAiB,EAChF,KAAK,eAAiB,KAAK,UAAU,KAAK,sBAAsB,eAAeC,CAAc,CAAC,EAC9F,KAAK,eAAe,SAAS,IAAIC,EAAW,EAC5C,KAAK,sBAAsB,WAAWC,GAAiB,KAAK,cAAc,EAC1E,KAAK,gBAAkB,KAAK,sBAAsB,eAAeC,EAAc,EAC/E,KAAK,sBAAsB,WAAWC,GAAiB,KAAK,eAAe,EAC3E,KAAK,gBAAkB,KAAK,sBAAsB,eAAeC,EAAc,EAC/E,KAAK,sBAAsB,WAAWC,GAAiB,KAAK,eAAe,EAI3E,KAAK,cAAgB,KAAK,UAAU,IAAIC,GAAa,KAAK,eAAgB,KAAK,gBAAiB,KAAK,YAAa,KAAK,YAAa,KAAK,eAAgB,KAAK,gBAAiB,KAAK,kBAAmB,KAAK,cAAc,CAAC,EAC3N,KAAK,UAAUC,EAAW,QAAQ,KAAK,cAAc,WAAY,KAAK,WAAW,CAAC,EAGlF,KAAK,UAAUA,EAAW,QAAQ,KAAK,eAAe,SAAU,KAAK,SAAS,CAAC,EAC/E,KAAK,UAAUA,EAAW,QAAQ,KAAK,YAAY,OAAQ,KAAK,OAAO,CAAC,EACxE,KAAK,UAAUA,EAAW,QAAQ,KAAK,YAAY,SAAU,KAAK,SAAS,CAAC,EAC5E,KAAK,UAAU,KAAK,YAAY,wBAAwB,IAAM,KAAK,eAAe,EAAI,CAAC,CAAC,EACxF,KAAK,UAAU,KAAK,YAAY,YAAY,IAAO,KAAK,aAAa,gBAAgB,CAAC,CAAC,EACvF,KAAK,UAAU,KAAK,eAAe,uBAAuB,CAAC,YAAY,EAAG,IAAM,KAAK,8BAA8B,CAAC,CAAC,EACrH,KAAK,UAAU,KAAK,eAAe,SAAS,IAAM,CAChD,KAAK,UAAU,KAAK,CAAE,SAAU,KAAK,eAAe,OAAO,KAAM,CAAC,EAClE,KAAK,cAAc,eAAe,KAAK,eAAe,OAAO,UAAW,KAAK,eAAe,OAAO,YAAY,CACjH,CAAC,CAAC,EAEF,KAAK,aAAe,KAAK,UAAU,IAAIC,GAAY,CAACC,EAAMC,IAAkB,KAAK,cAAc,MAAMD,EAAMC,CAAa,CAAC,CAAC,EAC1H,KAAK,UAAUH,EAAW,QAAQ,KAAK,aAAa,cAAe,KAAK,cAAc,CAAC,CACzF,CAhEA,IAAW,UAA2B,CACpC,OAAK,KAAK,eACR,KAAK,aAAe,KAAK,UAAU,IAAIpB,CAAiB,EACxD,KAAK,UAAU,MAAMwB,GAAM,CACzB,KAAK,cAAc,KAAKA,EAAG,QAAQ,CACrC,CAAC,GAEI,KAAK,aAAa,KAC3B,CAEA,IAAW,MAAe,CAAE,OAAO,KAAK,eAAe,IAAM,CAC7D,IAAW,MAAe,CAAE,OAAO,KAAK,eAAe,IAAM,CAC7D,IAAW,SAAsB,CAAE,OAAO,KAAK,eAAe,OAAS,CACvE,IAAW,SAAsC,CAAE,OAAO,KAAK,eAAe,OAAS,CACvF,IAAW,QAAQ1B,EAA2B,CAC5C,QAAW2B,KAAO3B,EAChB,KAAK,eAAe,QAAQ2B,CAAG,EAAI3B,EAAQ2B,CAAG,CAElD,CAgDO,MAAMH,EAA2BI,EAA6B,CACnE,KAAK,aAAa,MAAMJ,EAAMI,CAAQ,CACxC,CAWO,UAAUJ,EAA2BK,EAAmC,CACzE,KAAK,YAAY,UAAY,GAAqB,CAAChC,KACrD,KAAK,YAAY,KAAK,mDAAmD,EACzEA,GAA2B,IAE7B,KAAK,aAAa,UAAU2B,EAAMK,CAAkB,CACtD,CAEO,MAAML,EAAcM,EAAwB,GAAY,CAC7D,KAAK,YAAY,iBAAiBN,EAAMM,CAAY,CACtD,CAEO,OAAOC,EAAWC,EAAiB,CACpC,MAAMD,CAAC,GAAK,MAAMC,CAAC,IAIvBD,EAAI,KAAK,IAAIA,GAAsC,EACnDC,EAAI,KAAK,IAAIA,GAAsC,EAInD,KAAK,aAAa,UAAU,EAE5B,KAAK,eAAe,OAAOD,EAAGC,CAAC,EACjC,CAOO,OAAOC,EAA2BC,EAAqB,GAAa,CACzE,KAAK,eAAe,OAAOD,EAAWC,CAAS,CACjD,CASO,YAAYC,EAAcC,EAAqC,CACpE,KAAK,eAAe,YAAYD,EAAMC,CAAmB,CAC3D,CAEO,YAAYC,EAAyB,CAC1C,KAAK,YAAYA,GAAa,KAAK,KAAO,EAAE,CAC9C,CAEO,aAAoB,CACzB,KAAK,YAAY,CAAC,KAAK,eAAe,OAAO,KAAK,CACpD,CAEO,eAAeC,EAAqC,CACzD,KAAK,YAAY,KAAK,eAAe,OAAO,MAAQ,KAAK,eAAe,OAAO,KAAK,CACtF,CAEO,aAAaC,EAAoB,CACtC,IAAMC,EAAeD,EAAO,KAAK,eAAe,OAAO,MACnDC,IAAiB,GACnB,KAAK,YAAYA,CAAY,CAEjC,CAGO,mBAAmBC,EAAyBb,EAAyD,CAC1G,OAAO,KAAK,cAAc,mBAAmBa,EAAIb,CAAQ,CAC3D,CAGO,mBAAmBa,EAAyBb,EAAqF,CACtI,OAAO,KAAK,cAAc,mBAAmBa,EAAIb,CAAQ,CAC3D,CAGO,mBAAmBa,EAAyBb,EAAwE,CACzH,OAAO,KAAK,cAAc,mBAAmBa,EAAIb,CAAQ,CAC3D,CAGO,mBAAmBc,EAAed,EAAqE,CAC5G,OAAO,KAAK,cAAc,mBAAmBc,EAAOd,CAAQ,CAC9D,CAGO,mBAAmBa,EAAyBb,EAAqE,CACtH,OAAO,KAAK,cAAc,mBAAmBa,EAAIb,CAAQ,CAC3D,CAEU,QAAe,CACvB,KAAK,8BAA8B,CACrC,CAEO,OAAc,CACnB,KAAK,cAAc,MAAM,EACzB,KAAK,eAAe,MAAM,EAC1B,KAAK,gBAAgB,MAAM,EAC3B,KAAK,YAAY,MAAM,EACvB,KAAK,kBAAkB,MAAM,CAC/B,CAGQ,+BAAsC,CAC5C,IAAIe,EAAQ,GACNC,EAAa,KAAK,eAAe,WAAW,WAC9CA,GAAcA,EAAW,UAAY,QAAaA,EAAW,cAAgB,SAC/ED,EAAWC,EAAW,UAAY,UAAYA,EAAW,YAAc,OAErED,EACF,KAAK,iCAAiC,EAEtC,KAAK,2BAA2B,MAAM,CAE1C,CAEU,kCAAyC,CACjD,GAAI,CAAC,KAAK,2BAA2B,MAAO,CAC1C,IAAME,EAA6B,CAAC,EACpCA,EAAY,KAAK,KAAK,WAAWC,GAA8B,KAAK,KAAM,KAAK,cAAc,CAAC,CAAC,EAC/FD,EAAY,KAAK,KAAK,mBAAmB,CAAE,MAAO,GAAI,EAAG,KACvDC,GAA8B,KAAK,cAAc,EAC1C,GACR,CAAC,EACF,KAAK,2BAA2B,MAAQC,EAAa,IAAM,CACzD,QAAWC,KAAKH,EACdG,EAAE,QAAQ,CAEd,CAAC,CACH,CACF,CACF,ECrRO,IAAMC,GAAN,cAAuBC,EAAa,CAYzC,YACEC,EAA4B,CAAC,EAC7B,CACA,MAAMA,CAAO,EAdf,KAAiB,QAAU,KAAK,UAAU,IAAIC,CAAe,EAC7D,KAAgB,OAAS,KAAK,QAAQ,MACtC,KAAiB,cAAgB,KAAK,UAAU,IAAIA,CAAe,EACnE,KAAgB,aAAe,KAAK,cAAc,MAClD,KAAiB,eAAiB,KAAK,UAAU,IAAIA,CAAiB,EACtE,KAAgB,cAAgB,KAAK,eAAe,MACpD,KAAiB,mBAAqB,KAAK,UAAU,IAAIA,CAAiB,EAC1E,KAAgB,WAAa,KAAK,mBAAmB,MACrD,KAAiB,kBAAoB,KAAK,UAAU,IAAIA,CAAiB,EACzE,KAAgB,UAAY,KAAK,kBAAkB,MAOjD,KAAK,OAAO,EAGZ,KAAK,UAAU,KAAK,cAAc,cAAc,IAAM,KAAK,KAAK,CAAC,CAAC,EAClE,KAAK,UAAU,KAAK,cAAc,eAAe,IAAM,KAAK,MAAM,CAAC,CAAC,EACpE,KAAK,UAAUC,EAAW,QAAQ,KAAK,cAAc,aAAc,KAAK,aAAa,CAAC,EACtF,KAAK,UAAUA,EAAW,QAAQ,KAAK,cAAc,cAAe,KAAK,cAAc,CAAC,EACxF,KAAK,UAAUA,EAAW,QAAQ,KAAK,cAAc,WAAY,KAAK,kBAAkB,CAAC,EACzF,KAAK,UAAUA,EAAW,QAAQ,KAAK,cAAc,UAAW,KAAK,iBAAiB,CAAC,EACvF,KAAK,UAAUA,EAAW,QAAQA,EAAW,IAAI,KAAK,cAAc,qBAAsBC,IAAM,CAAE,MAAOA,GAAG,OAAS,EAAG,IAAKA,GAAG,KAAO,KAAK,KAAO,CAAE,EAAE,EAAG,KAAK,SAAS,CAAC,CAC3K,CAKA,IAAW,QAAkB,CAC3B,OAAO,KAAK,QAAQ,MACtB,CAIA,IAAW,SAAqB,CAC9B,OAAO,KAAK,OAAO,OACrB,CAEO,eAAeC,EAAgC,CACpD,OAAO,KAAK,OAAO,UAAU,KAAK,OAAO,MAAQ,KAAK,OAAO,EAAIA,CAAa,CAChF,CAEO,MAAa,CAClB,KAAK,QAAQ,KAAK,CACpB,CAEO,MAAMC,EAAcC,EAAwB,GAAY,CAC7D,KAAK,YAAY,iBAAiBD,EAAMC,CAAY,CACtD,CAQO,OAAOC,EAAWC,EAAiB,CACpCD,IAAM,KAAK,MAAQC,IAAM,KAAK,MAIlC,MAAM,OAAOD,EAAGC,CAAC,CACnB,CAKO,OAAc,CACnB,KAAK,OAAO,gBAAgB,EAC5B,KAAK,OAAO,MAAM,IAAI,EAAG,KAAK,OAAO,MAAM,IAAI,KAAK,OAAO,MAAQ,KAAK,OAAO,CAAC,CAAE,EAClF,KAAK,OAAO,MAAM,OAAS,EAC3B,KAAK,OAAO,MAAQ,EACpB,KAAK,OAAO,MAAQ,EACpB,KAAK,OAAO,EAAI,EAChB,QAASC,EAAI,EAAGA,EAAI,KAAK,KAAMA,IAC7B,KAAK,OAAO,MAAM,KAAK,KAAK,OAAO,aAAaC,CAAiB,CAAC,EAEpE,KAAK,UAAU,KAAK,CAAE,SAAU,KAAK,OAAO,KAAM,CAAC,CACrD,CAUO,OAAc,CAKnB,KAAK,QAAQ,KAAO,KAAK,KACzB,KAAK,QAAQ,KAAO,KAAK,KAEzB,KAAK,OAAO,EACZ,MAAM,MAAM,CACd,CACF,ECvHO,IAAMC,GAAN,KAA0C,CAA1C,cACL,KAAU,QAA0B,CAAC,EAE9B,SAAgB,CACrB,QAAS,EAAI,KAAK,QAAQ,OAAS,EAAG,GAAK,EAAG,IAC5C,KAAK,QAAQ,CAAC,EAAE,SAAS,QAAQ,CAErC,CAEO,UAAUC,EAAoBC,EAAgC,CACnE,IAAMC,EAA4B,CAChC,SAAAD,EACA,QAASA,EAAS,QAClB,WAAY,EACd,EACA,KAAK,QAAQ,KAAKC,CAAW,EAC7BD,EAAS,QAAU,IAAM,KAAK,qBAAqBC,CAAW,EAC9DD,EAAS,SAASD,CAAe,CACnC,CAEQ,qBAAqBE,EAAiC,CAC5D,GAAIA,EAAY,WAEd,OAEF,IAAIC,EAAQ,GACZ,QAASC,EAAI,EAAGA,EAAI,KAAK,QAAQ,OAAQA,IACvC,GAAI,KAAK,QAAQA,CAAC,IAAMF,EAAa,CACnCC,EAAQC,EACR,KACF,CAEF,GAAID,IAAU,GACZ,MAAM,IAAI,MAAM,qDAAqD,EAEvED,EAAY,WAAa,GACzBA,EAAY,QAAQ,MAAMA,EAAY,QAAQ,EAC9C,KAAK,QAAQ,OAAOC,EAAO,CAAC,CAC9B,CACF,ECnCA,IAAME,GAA2B,CAAC,OAAQ,MAAM,EAEnCC,GAAN,cAAuBC,CAAmC,CAO/D,YAAYC,EAAuD,CACjE,MAAM,EAEN,KAAK,MAAQ,KAAK,UAAU,IAAIF,GAAaE,CAAO,CAAC,EACrD,KAAK,cAAgB,KAAK,UAAU,IAAIC,EAAc,EAEtD,KAAK,eAAiB,CAAE,GAAI,KAAK,MAAM,OAAQ,EAC/C,IAAMC,EAAUC,GACP,KAAK,MAAM,QAAQA,CAAQ,EAE9BC,EAAS,CAACD,EAAkBE,IAAqB,CACrD,KAAK,sBAAsBF,CAAQ,EACnC,KAAK,MAAM,QAAQA,CAAQ,EAAIE,CACjC,EAEA,QAAWF,KAAY,KAAK,MAAM,QAAS,CACzC,IAAMG,EAAO,CACX,IAAKJ,EAAO,KAAK,KAAMC,CAAQ,EAC/B,IAAKC,EAAO,KAAK,KAAMD,CAAQ,CACjC,EACA,OAAO,eAAe,KAAK,eAAgBA,EAAUG,CAAI,CAC3D,CACF,CAEQ,sBAAsBH,EAAwB,CAIpD,GAAIN,GAAyB,SAASM,CAAQ,EAC5C,MAAM,IAAI,MAAM,WAAWA,CAAQ,sCAAsC,CAE7E,CAEQ,mBAA0B,CAChC,GAAI,CAAC,KAAK,MAAM,eAAe,QAAQ,iBACrC,MAAM,IAAI,MAAM,sEAAsE,CAE1F,CAEA,IAAW,QAAuB,CAAE,OAAO,KAAK,MAAM,MAAQ,CAC9D,IAAW,UAA2B,CAAE,OAAO,KAAK,MAAM,QAAU,CACpE,IAAW,cAA6B,CAAE,OAAO,KAAK,MAAM,YAAc,CAC1E,IAAW,QAAyB,CAAE,OAAO,KAAK,MAAM,MAAQ,CAChE,IAAW,YAA2B,CAAE,OAAO,KAAK,MAAM,UAAY,CACtE,IAAW,UAAmD,CAAE,OAAO,KAAK,MAAM,QAAU,CAC5F,IAAW,UAAmD,CAAE,OAAO,KAAK,MAAM,QAAU,CAC5F,IAAW,UAA2B,CAAE,OAAO,KAAK,MAAM,QAAU,CACpE,IAAW,eAAgC,CAAE,OAAO,KAAK,MAAM,aAAe,CAC9E,IAAW,eAA8B,CAAE,OAAO,KAAK,MAAM,aAAe,CAE5E,IAAW,QAAkB,CAC3B,YAAK,UAAY,IAAII,GAAU,KAAK,KAAK,EAClC,KAAK,OACd,CACA,IAAW,SAA4B,CACrC,YAAK,kBAAkB,EAChB,IAAIC,GAAW,KAAK,KAAK,CAClC,CACA,IAAW,MAAe,CAAE,OAAO,KAAK,MAAM,IAAM,CACpD,IAAW,MAAe,CAAE,OAAO,KAAK,MAAM,IAAM,CACpD,IAAW,QAA8B,CACvC,YAAK,UAAY,KAAK,UAAU,IAAIC,GAAmB,KAAK,KAAK,CAAC,EAC3D,KAAK,OACd,CACA,IAAW,SAAkC,CAC3C,OAAO,KAAK,MAAM,OACpB,CACA,IAAW,OAAgB,CACzB,IAAMC,EAAI,KAAK,MAAM,YAAY,gBAC7BC,EAA+D,OACnE,OAAQ,KAAK,MAAM,kBAAkB,eAAgB,CACnD,IAAK,MAAOA,EAAoB,MAAO,MACvC,IAAK,QAASA,EAAoB,QAAS,MAC3C,IAAK,OAAQA,EAAoB,OAAQ,MACzC,IAAK,MAAOA,EAAoB,MAAO,KACzC,CACA,MAAO,CACL,0BAA2BD,EAAE,sBAC7B,sBAAuBA,EAAE,kBACzB,mBAAoBA,EAAE,mBACtB,WAAY,KAAK,MAAM,YAAY,MAAM,WACzC,kBAAmBC,EACnB,WAAYD,EAAE,OACd,sBAAuBA,EAAE,kBACzB,cAAeA,EAAE,UACjB,WAAY,CAAC,KAAK,MAAM,YAAY,eACpC,uBAAwBA,EAAE,mBAC1B,eAAgBA,EAAE,eAClB,eAAgBA,EAAE,UACpB,CACF,CACA,IAAW,SAAsC,CAC/C,OAAO,KAAK,cACd,CACA,IAAW,QAAQV,EAA2B,CAC5C,QAAWG,KAAYH,EACrB,KAAK,eAAeG,CAAQ,EAAIH,EAAQG,CAAQ,CAEpD,CACO,MAAMS,EAAcC,EAAwB,GAAY,CAC7D,KAAK,MAAM,MAAMD,EAAMC,CAAY,CACrC,CACO,OAAOC,EAAiBC,EAAoB,CACjD,KAAK,gBAAgBD,EAASC,CAAI,EAClC,KAAK,MAAM,OAAOD,EAASC,CAAI,CACjC,CACO,eAAeC,EAAwB,EAAY,CACxD,YAAK,gBAAgBA,CAAa,EAC3B,KAAK,MAAM,eAAeA,CAAa,CAChD,CACO,UAAUA,EAAgC,CAC/C,OAAO,KAAK,eAAeA,CAAa,CAC1C,CACO,SAAgB,CACrB,MAAM,QAAQ,CAChB,CACO,YAAYC,EAAsB,CACvC,KAAK,gBAAgBA,CAAM,EAC3B,KAAK,MAAM,YAAYA,CAAM,CAC/B,CACO,YAAYC,EAAyB,CAC1C,KAAK,gBAAgBA,CAAS,EAC9B,KAAK,MAAM,YAAYA,CAAS,CAClC,CACO,aAAoB,CACzB,KAAK,MAAM,YAAY,CACzB,CACO,gBAAuB,CAC5B,KAAK,MAAM,eAAe,CAC5B,CACO,aAAaC,EAAoB,CACtC,KAAK,gBAAgBA,CAAI,EACzB,KAAK,MAAM,aAAaA,CAAI,CAC9B,CACO,OAAc,CACnB,KAAK,MAAM,MAAM,CACnB,CACO,MAAMP,EAA2BQ,EAA6B,CACnE,KAAK,MAAM,MAAMR,EAAMQ,CAAQ,CACjC,CACO,QAAQR,EAA2BQ,EAA6B,CACrE,KAAK,MAAM,MAAMR,CAAI,EACrB,KAAK,MAAM,MAAM;AAAA,EAAQQ,CAAQ,CACnC,CACO,OAAc,CACnB,KAAK,MAAM,MAAM,CACnB,CACO,UAAUC,EAA6B,CAE5C,KAAK,cAAc,UAAU,KAAaA,CAAK,CACjD,CAEQ,mBAAmBC,EAAwB,CACjD,QAAWjB,KAASiB,EAClB,GAAIjB,IAAU,KAAY,MAAMA,CAAK,GAAKA,EAAQ,IAAM,EACtD,MAAM,IAAI,MAAM,gCAAgC,CAGtD,CACF", + "names": ["stringFromCodePoint", "codePoint", "utf32ToString", "data", "start", "end", "result", "i", "codepoint", "StringToUtf32", "input", "target", "length", "size", "startPos", "second", "code", "Utf8ToUtf32", "byte1", "byte2", "byte3", "byte4", "discardInterim", "cp", "pos", "tmp", "type", "missing", "fourStop", "AttributeData", "_AttributeData", "ExtendedAttrs", "value", "newObj", "_ExtendedAttrs", "ext", "urlId", "val", "CellData", "_CellData", "AttributeData", "ExtendedAttrs", "value", "obj", "stringFromCodePoint", "combined", "code", "second", "other", "thisDefault", "otherDefault", "BufferLineApiView", "_line", "x", "cell", "CellData", "trimRight", "startColumn", "endColumn", "BufferApiView", "_buffer", "type", "buffer", "y", "line", "BufferLineApiView", "CellData", "toDisposable", "fn", "dispose", "arg", "d", "DisposableStore", "o", "d", "Disposable", "MutableDisposable", "value", "Emitter", "listener", "thisArgs", "disposables", "toDisposable", "entry", "result", "idx", "event", "listeners", "i", "len", "EventUtils", "forward", "from", "to", "e", "map", "any", "events", "store", "DisposableStore", "runAndSubscribe", "handler", "initial", "BufferNamespaceApi", "Disposable", "_core", "Emitter", "BufferApiView", "ParserApi", "_core", "id", "callback", "params", "data", "handler", "ident", "UnicodeApi", "_core", "provider", "version", "DEFAULT_ATTR_DATA", "AttributeData", "$startIndex", "$workCell", "CellData", "$extended", "BufferLine", "_BufferLine", "cols", "fillCellData", "isWrapped", "cell", "i", "index", "content", "cp", "stringFromCodePoint", "value", "codePoint", "width", "attrs", "$idx", "pos", "n", "start", "end", "respectProtect", "uint32Cells", "data", "keys", "key", "extKeys", "line", "blank", "newLine", "src", "srcCol", "destCol", "length", "applyInReverse", "srcData", "trimRight", "startCol", "endCol", "outColumns", "isCanonical", "cellContents", "chars", "result", "srcStart", "serviceRegistry", "getServiceDependencies", "ctor", "createDecorator", "id", "decorator", "target", "key", "index", "storeServiceDependency", "IBufferService", "createDecorator", "IMouseStateService", "ICoreService", "ICharsetService", "IInstantiationService", "ILogService", "createDecorator", "IOptionsService", "IOscLinkService", "IUnicodeService", "IDecorationService", "ServiceCollection", "entries", "id", "service", "instance", "result", "callback", "key", "value", "InstantiationService", "IInstantiationService", "ctor", "args", "serviceDependencies", "getServiceDependencies", "a", "b", "serviceArgs", "dependency", "firstServiceArgPos", "optionsKeyToLogLevel", "LOG_PREFIX", "LogService", "Disposable", "_optionsService", "optionalParams", "i", "type", "message", "__decorateClass", "__decorateParam", "IOptionsService", "CircularList", "Disposable", "_maxLength", "Emitter", "newMaxLength", "newArray", "i", "newLength", "index", "value", "start", "deleteCount", "items", "countToTrim", "count", "offset", "expandListBy", "TaskQueue", "logService", "task", "deadline", "taskDuration", "longestTask", "lastDeadlineRemaining", "deadlineRemaining", "PriorityTaskQueue", "callback", "identifier", "duration", "end", "IdleTaskQueueInternal", "IdleTaskQueue", "reflowLargerGetLinesToRemove", "lines", "oldCols", "newCols", "bufferAbsoluteY", "nullCell", "reflowCursorLine", "toRemove", "y", "i", "nextLine", "wrappedLines", "destLineIndex", "destCol", "getWrappedLineTrimmedLength", "srcLineIndex", "srcCol", "srcTrimmedTineLength", "srcRemainingCells", "destRemainingCells", "cellsToCopy", "countToRemove", "reflowLargerCreateNewLayout", "layout", "nextToRemoveIndex", "nextToRemoveStart", "countRemovedSoFar", "reflowLargerApplyNewLayout", "newLayout", "newLayoutLines", "reflowSmallerGetNewLineLengths", "newLineLengths", "cellsNeeded", "srcLine", "cellsAvailable", "oldTrimmedLength", "endsWithWide", "lineLength", "cols", "endsInNull", "followingLineStartsWithWide", "_Marker", "line", "Emitter", "dispose", "disposable", "Marker", "CHARSETS", "DEFAULT_CHARSET", "MAX_BUFFER_SIZE", "Buffer", "Disposable", "_hasScrollback", "_optionsService", "_bufferService", "_logService", "DEFAULT_ATTR_DATA", "DEFAULT_CHARSET", "CellData", "CircularList", "IdleTaskQueue", "toDisposable", "attr", "ExtendedAttrs", "isWrapped", "BufferLine", "relativeY", "rows", "correctBufferLength", "fillAttr", "i", "newCols", "newRows", "nullCell", "dirtyMemoryLines", "newMaxLength", "addToY", "y", "amountToTrim", "maxY", "normalRun", "counted", "windowsPty", "reflowCursorLine", "toRemove", "reflowLargerGetLinesToRemove", "newLayoutResult", "reflowLargerCreateNewLayout", "reflowLargerApplyNewLayout", "countRemoved", "viewportAdjustments", "toInsert", "countToInsert", "nextLine", "wrappedLines", "absoluteY", "lastLineLength", "destLineLengths", "reflowSmallerGetNewLineLengths", "linesToAdd", "trimmedLines", "newLines", "newLine", "destLineIndex", "destCol", "srcLineIndex", "srcCol", "cellsToCopy", "wrappedLinesIndex", "getWrappedLineTrimmedLength", "insertEvents", "originalLines", "originalLinesLength", "originalLineIndex", "nextToInsertIndex", "nextToInsert", "countInsertedSoFar", "nextI", "insertCountEmitted", "lineIndex", "trimRight", "startCol", "endCol", "line", "first", "last", "x", "marker", "Marker", "amount", "event", "BufferSet", "Disposable", "_optionsService", "_bufferService", "_logService", "MutableDisposable", "Emitter", "Buffer", "fillAttr", "newCols", "newRows", "i", "BufferService", "Disposable", "optionsService", "logService", "Emitter", "BufferSet", "e", "cols", "rows", "colsChanged", "rowsChanged", "eraseAttr", "isWrapped", "buffer", "newLine", "topRow", "bottomRow", "willBufferBeTrimmed", "scrollRegionHeight", "disp", "suppressScrollEvent", "oldYdisp", "__decorateClass", "__decorateParam", "IOptionsService", "ILogService", "isNode", "userAgent", "platform", "isFirefox", "isChrome", "isLegacyEdge", "isSafari", "isMac", "platform", "isWindows", "isLinux", "isChromeOS", "userAgent", "DEFAULT_OPTIONS", "isMac", "FONT_WEIGHT_OPTIONS", "OptionsService", "Disposable", "options", "Emitter", "defaultOptions", "key", "newValue", "e", "toDisposable", "listener", "eventKey", "keys", "getter", "propName", "setter", "value", "desc", "isCursorStyle", "DEFAULT_MODES", "DEFAULT_DEC_PRIVATE_MODES", "DEFAULT_KITTY_KEYBOARD_STATE", "CoreService", "Disposable", "_bufferService", "_logService", "_optionsService", "Emitter", "data", "wasUserInput", "buffer", "e", "__decorateClass", "__decorateParam", "IBufferService", "ILogService", "IOptionsService", "DEFAULT_PROTOCOLS", "e", "eventCode", "e", "isSGR", "code", "S", "DEFAULT_ENCODINGS", "params", "final", "MouseStateService", "Disposable", "Emitter", "name", "DEFAULT_PROTOCOLS", "protocol", "encoding", "customWheelEventHandler", "ev", "UnicodeService", "_UnicodeService", "Emitter", "value", "state", "width", "shouldJoin", "version", "provider", "num", "s", "result", "precedingInfo", "length", "i", "code", "second", "currentInfo", "chWidth", "codepoint", "preceding", "BMP_COMBINING", "HIGH_COMBINING", "table", "bisearch", "ucs", "data", "min", "max", "mid", "UnicodeV6", "r", "num", "codepoint", "preceding", "width", "shouldJoin", "oldWidth", "UnicodeService", "CharsetService", "g", "charset", "updateWindowsModeWrappedState", "bufferService", "lastChar", "nextLine", "Params", "_Params", "maxLength", "maxSubParamsLength", "values", "params", "i", "value", "k", "newParams", "res", "start", "end", "idx", "result", "length", "store", "cur", "StringBuilder", "chunk", "LimitedStringBuilder", "_limit", "EMPTY_HANDLERS", "OscParser", "ident", "handler", "handlerList", "handlerIndex", "j", "data", "start", "end", "utf32ToString", "code", "success", "promiseResult", "handlerResult", "fallThrough", "_OscHandler", "_handler", "LimitedStringBuilder", "ret", "res", "OscHandler", "EMPTY_HANDLERS", "DcsParser", "ident", "handler", "handlerList", "handlerIndex", "j", "params", "data", "start", "end", "utf32ToString", "success", "promiseResult", "handlerResult", "fallThrough", "EMPTY_PARAMS", "Params", "_DcsHandler", "_handler", "LimitedStringBuilder", "ret", "res", "DcsHandler", "EMPTY_HANDLERS", "ApcParser", "ident", "handler", "handlerList", "handlerIndex", "j", "data", "start", "end", "utf32ToString", "success", "promiseResult", "handlerResult", "fallThrough", "_ApcHandler", "_handler", "LimitedStringBuilder", "ret", "res", "ApcHandler", "TransitionTable", "length", "action", "next", "code", "state", "codes", "i", "NON_ASCII_PRINTABLE", "VT500_TRANSITION_TABLE", "table", "blueprint", "unused", "r", "start", "end", "PRINTABLES", "EXECUTABLES", "states", "EscapeSequenceParser", "Disposable", "_transitions", "Params", "data", "ident", "params", "toDisposable", "OscParser", "DcsParser", "ApcParser", "id", "finalRange", "res", "intermediate", "finalCode", "handler", "handlerList", "handlerIndex", "flag", "callback", "handlers", "handlerPos", "transition", "chunkPos", "promiseResult", "handlerResult", "k", "ch", "csiDone", "j", "c", "l4", "handlersEsc", "jj", "RGB_REX", "HASH_REX", "parseColor", "data", "low", "m", "base", "adv", "result", "i", "c", "XTERM_VERSION", "GLEVEL", "paramToWindowOption", "opts", "$temp", "InputHandler", "Disposable", "_bufferService", "_charsetService", "_coreService", "_logService", "_optionsService", "_oscLinkService", "_mouseStateService", "_unicodeService", "_parser", "EscapeSequenceParser", "StringToUtf32", "Utf8ToUtf32", "DEFAULT_ATTR_DATA", "Emitter", "DirtyRowTracker", "e", "ident", "params", "code", "identifier", "action", "data", "payload", "start", "end", "OscHandler", "flag", "CHARSETS", "state", "DcsHandler", "cursorStartX", "cursorStartY", "decodedLength", "position", "p", "slowTimeout", "slowPromise", "_res", "rej", "err", "promiseResult", "result", "wasPaused", "i", "len", "viewportEnd", "viewportStart", "chWidth", "charset", "screenReaderMode", "cols", "wraparoundMode", "insertMode", "curAttr", "bufferRow", "precedingJoinState", "pos", "ch", "currentInfo", "UnicodeService", "shouldJoin", "oldWidth", "stringFromCodePoint", "linkId", "oldRow", "oldCol", "BufferLine", "offset", "delta", "id", "callback", "paramToWindowOption", "ApcHandler", "line", "originalX", "maxCol", "x", "y", "diffToTop", "diffToBottom", "param", "clearWrap", "respectProtect", "j", "nextLine", "scrollBackSize", "row", "scrollBottomRowsOffset", "scrollBottomAbsolute", "joinState", "length", "text", "idata", "itext", "tlength", "XTERM_VERSION", "term", "DEFAULT_CHARSET", "ansi", "V", "dm", "mouseProtocol", "mouseEncoding", "cs", "buffers", "active", "alt", "opts", "f", "m", "v", "b2v", "value", "color", "mode", "c1", "c2", "c3", "AttributeData", "attr", "accu", "cSpace", "advance", "subparams", "style", "l", "isBlinking", "top", "bottom", "second", "event", "slots", "idx", "spec", "index", "isValidColorIndex", "parseColor", "uri", "parsedParams", "idParamIndex", "collectAndFlag", "GLEVEL", "scrollRegionHeight", "level", "cell", "CellData", "yOffset", "s", "b", "STYLES", "y1", "y2", "flags", "stack", "count", "__decorateClass", "__decorateParam", "IBufferService", "TimeoutTimer", "runner", "timeout", "WriteBuffer", "Disposable", "_action", "TimeoutTimer", "Emitter", "toDisposable", "chunk", "didProcess", "cb", "data", "maxSubsequentCalls", "callback", "lastTime", "promiseResult", "startTime", "result", "continuation", "r", "err", "OscLinkService", "_bufferService", "data", "buffer", "marker", "entry", "castData", "key", "match", "linkId", "y", "e", "linkData", "index", "__decorateClass", "__decorateParam", "IBufferService", "hasWriteSyncWarnHappened", "CoreTerminal", "Disposable", "options", "MutableDisposable", "Emitter", "InstantiationService", "OptionsService", "IOptionsService", "LogService", "ILogService", "BufferService", "IBufferService", "CoreService", "ICoreService", "MouseStateService", "IMouseStateService", "UnicodeService", "UnicodeV6", "IUnicodeService", "CharsetService", "ICharsetService", "OscLinkService", "IOscLinkService", "InputHandler", "EventUtils", "WriteBuffer", "data", "promiseResult", "ev", "key", "callback", "maxSubsequentCalls", "wasUserInput", "x", "y", "eraseAttr", "isWrapped", "disp", "suppressScrollEvent", "pageCount", "disableSmoothScroll", "line", "scrollAmount", "id", "ident", "value", "windowsPty", "disposables", "updateWindowsModeWrappedState", "toDisposable", "d", "Terminal", "CoreTerminal", "options", "Emitter", "EventUtils", "e", "cursorYOffset", "data", "wasUserInput", "x", "y", "i", "DEFAULT_ATTR_DATA", "AddonManager", "terminal", "instance", "loadedAddon", "index", "i", "CONSTRUCTOR_ONLY_OPTIONS", "Terminal", "Disposable", "options", "AddonManager", "getter", "propName", "setter", "value", "desc", "ParserApi", "UnicodeApi", "BufferNamespaceApi", "m", "mouseTrackingMode", "data", "wasUserInput", "columns", "rows", "cursorYOffset", "amount", "pageCount", "line", "callback", "addon", "values"] + } diff --git a/config/patches/node-pty@1.1.0.patch b/config/patches/node-pty@1.1.0.patch deleted file mode 100644 index 40b240f3296c..000000000000 --- a/config/patches/node-pty@1.1.0.patch +++ /dev/null @@ -1,1442 +0,0 @@ -diff --git a/binding.gyp b/binding.gyp -index 5f63978b07ab50aaf7523219a2170ec737a6b5db..bbd9e06136e8922f40b5779e35d4fc835f1479ab 100644 ---- a/binding.gyp -+++ b/binding.gyp -@@ -1,13 +1,18 @@ - { - 'target_defaults': { - 'dependencies': [ -- " - #include - #include -+#include - #include -+#include - #include - - #include -@@ -47,6 +49,25 @@ - #include - #endif - -+/* Orca: glibc 2.32-2.34 relocated pthread_sigmask/openpty/forkpty into libc -+ * under new symbol versions, so building on a newer glibc produces references -+ * (GLIBC_2.32/2.34) absent on Ubuntu 20.04 (glibc 2.31) and the app fails to -+ * launch. Pin these to the pre-merge version glibc still ships as a compat -+ * alias; the binding.gyp ldflags force libutil/libpthread into DT_NEEDED so -+ * those aliases are actually loaded on the target. */ -+#if defined(__linux__) -+# if defined(__x86_64__) -+# define ORCA_GLIBC_COMPAT_VERSION "GLIBC_2.2.5" -+# elif defined(__aarch64__) -+# define ORCA_GLIBC_COMPAT_VERSION "GLIBC_2.17" -+# endif -+# ifdef ORCA_GLIBC_COMPAT_VERSION -+__asm__(".symver openpty,openpty@" ORCA_GLIBC_COMPAT_VERSION); -+__asm__(".symver forkpty,forkpty@" ORCA_GLIBC_COMPAT_VERSION); -+__asm__(".symver pthread_sigmask,pthread_sigmask@" ORCA_GLIBC_COMPAT_VERSION); -+# endif -+#endif -+ - /* Some platforms name VWERASE and VDISCARD differently */ - #if !defined(VWERASE) && defined(VWERSE) - #define VWERASE VWERSE -@@ -228,6 +249,9 @@ Napi::Value PtyGetProc(const Napi::CallbackInfo& info); - static int - pty_nonblock(int); - -+static int -+pty_cloexec(int); -+ - #if defined(__APPLE__) - static char * - pty_getproc(int); -@@ -237,13 +261,23 @@ pty_getproc(int, char *); - #endif - - #if defined(__APPLE__) || defined(__OpenBSD__) -+struct pty_spawn_error { -+ const char* step; -+ int errnum; -+ std::string detail_name; -+ std::string detail_value; -+}; -+ -+static std::string -+pty_format_spawn_error(const pty_spawn_error&); -+ - static void - pty_posix_spawn(char** argv, char** env, - const struct termios *termp, - const struct winsize *winp, - int* master, - pid_t* pid, -- int* err); -+ pty_spawn_error* err); - #endif - - struct DelBuf { -@@ -367,14 +401,18 @@ Napi::Value PtyFork(const Napi::CallbackInfo& info) { - argv[i + 3] = strdup(arg.c_str()); - } - -- int err = -1; -- pty_posix_spawn(argv, env, term, &winp, &master, &pid, &err); -- if (err != 0) { -- throw Napi::Error::New(napiEnv, "posix_spawnp failed."); -+ pty_spawn_error spawn_error = { NULL, 0, "", "" }; -+ pty_posix_spawn(argv, env, term, &winp, &master, &pid, &spawn_error); -+ if (spawn_error.errnum != 0) { -+ std::string spawn_message = pty_format_spawn_error(spawn_error); -+ throw Napi::Error::New(napiEnv, spawn_message); - } - if (pty_nonblock(master) == -1) { - throw Napi::Error::New(napiEnv, "Could not set master fd to nonblocking."); - } -+ if (pty_cloexec(master) == -1) { -+ throw Napi::Error::New(napiEnv, "Could not set master fd to close-on-exec."); -+ } - #else - int argc = argv_.Length(); - int argl = argc + 2; -@@ -445,6 +483,9 @@ Napi::Value PtyFork(const Napi::CallbackInfo& info) { - if (pty_nonblock(master) == -1) { - throw Napi::Error::New(napiEnv, "Could not set master fd to nonblocking."); - } -+ if (pty_cloexec(master) == -1) { -+ throw Napi::Error::New(napiEnv, "Could not set master fd to close-on-exec."); -+ } - } - #endif - -@@ -586,6 +627,23 @@ pty_nonblock(int fd) { - return fcntl(fd, F_SETFL, flags | O_NONBLOCK); - } - -+/** -+ * Orca: close-on-exec FD -+ * -+ * forkpty()/posix_openpt() have no atomic O_CLOEXEC, so a master left without -+ * FD_CLOEXEC is inherited by every later child of this process -- including -+ * later pty children -- which keeps its /dev/pts device and buffers alive long -+ * after its own session ends (#8362). -+ */ -+ -+static int -+pty_cloexec(int fd) { -+ int flags = fcntl(fd, F_GETFD); -+ if (flags == -1) return -1; -+ if (flags & FD_CLOEXEC) return 0; -+ return fcntl(fd, F_SETFD, flags | FD_CLOEXEC); -+} -+ - /** - * pty_getproc - * Taken from tmux. -@@ -684,15 +742,73 @@ pty_getproc(int fd, char *tty) { - #endif - - #if defined(__APPLE__) -+static const char* -+pty_errno_name(int errnum) { -+ switch (errnum) { -+ case E2BIG: return "E2BIG"; -+ case EACCES: return "EACCES"; -+ case EAGAIN: return "EAGAIN"; -+ case EMFILE: return "EMFILE"; -+ case ENFILE: return "ENFILE"; -+ case ENOENT: return "ENOENT"; -+ case ENOMEM: return "ENOMEM"; -+ default: return "errno"; -+ } -+} -+ -+static void -+pty_set_spawn_error(pty_spawn_error* err, -+ const char* step, -+ int errnum, -+ const char* detail_name = NULL, -+ const char* detail_value = NULL) { -+ err->step = step; -+ err->errnum = errnum; -+ err->detail_name = detail_name ? detail_name : ""; -+ err->detail_value = detail_value ? detail_value : ""; -+} -+ -+static std::string -+pty_format_spawn_error(const pty_spawn_error& err) { -+ char errno_buf[64]; -+ snprintf(errno_buf, sizeof(errno_buf), "%d", err.errnum); -+ -+ std::string message = "node-pty: "; -+ message += err.step ? err.step : "unknown"; -+ message += " failed: "; -+ message += pty_errno_name(err.errnum); -+ message += " (errno "; -+ message += errno_buf; -+ message += ", "; -+ message += strerror(err.errnum); -+ message += ")"; -+ -+ if (!err.detail_name.empty()) { -+ message += " - "; -+ message += err.detail_name; -+ message += "='"; -+ message += err.detail_value; -+ message += "'"; -+ } -+ -+ return message; -+} -+ - static void - pty_posix_spawn(char** argv, char** env, - const struct termios *termp, - const struct winsize *winp, - int* master, - pid_t* pid, -- int* err) { -- int low_fds[3]; -+ pty_spawn_error* err) { -+ int low_fds[3] = {-1, -1, -1}; - size_t count = 0; -+ int res = -1; -+ int slave = -1; -+ posix_spawn_file_actions_t acts; -+ bool acts_initialized = false; -+ posix_spawnattr_t attrs; -+ bool attrs_initialized = false; - - for (; count < 3; count++) { - low_fds[count] = posix_openpt(O_RDWR); -@@ -706,80 +822,118 @@ pty_posix_spawn(char** argv, char** env, - POSIX_SPAWN_SETSID; - *master = posix_openpt(O_RDWR); - if (*master == -1) { -- return; -+ pty_set_spawn_error(err, "posix_openpt", errno); -+ goto done; - } - -- int res = grantpt(*master) || unlockpt(*master); -+ res = grantpt(*master); - if (res == -1) { -- return; -+ pty_set_spawn_error(err, "grantpt", errno); -+ goto done; -+ } -+ -+ res = unlockpt(*master); -+ if (res == -1) { -+ pty_set_spawn_error(err, "unlockpt", errno); -+ goto done; - } - - // Use TIOCPTYGNAME instead of ptsname() to avoid threading problems. -- int slave; - char slave_pty_name[128]; - res = ioctl(*master, TIOCPTYGNAME, slave_pty_name); - if (res == -1) { -- return; -+ pty_set_spawn_error(err, "ioctl_TIOCPTYGNAME", errno); -+ goto done; - } - - slave = open(slave_pty_name, O_RDWR | O_NOCTTY); - if (slave == -1) { -- return; -+ pty_set_spawn_error(err, "open_slave", errno, "slave", slave_pty_name); -+ goto done; - } - - if (termp) { - res = tcsetattr(slave, TCSANOW, termp); - if (res == -1) { -- return; -+ pty_set_spawn_error(err, "tcsetattr", errno, "slave", slave_pty_name); -+ goto done; - }; - } - - if (winp) { - res = ioctl(slave, TIOCSWINSZ, winp); - if (res == -1) { -- return; -+ pty_set_spawn_error(err, "ioctl_TIOCSWINSZ", errno, "slave", slave_pty_name); -+ goto done; - } - } - -- posix_spawn_file_actions_t acts; -- posix_spawn_file_actions_init(&acts); -+ res = posix_spawn_file_actions_init(&acts); -+ if (res != 0) { -+ pty_set_spawn_error(err, "posix_spawn_file_actions_init", res); -+ goto done; -+ } -+ acts_initialized = true; - posix_spawn_file_actions_adddup2(&acts, slave, STDIN_FILENO); - posix_spawn_file_actions_adddup2(&acts, slave, STDOUT_FILENO); - posix_spawn_file_actions_adddup2(&acts, slave, STDERR_FILENO); - posix_spawn_file_actions_addclose(&acts, slave); - posix_spawn_file_actions_addclose(&acts, *master); - -- posix_spawnattr_t attrs; -- posix_spawnattr_init(&attrs); -- *err = posix_spawnattr_setflags(&attrs, flags); -- if (*err != 0) { -+ res = posix_spawnattr_init(&attrs); -+ if (res != 0) { -+ pty_set_spawn_error(err, "posix_spawnattr_init", res); -+ goto done; -+ } -+ attrs_initialized = true; -+ res = posix_spawnattr_setflags(&attrs, flags); -+ if (res != 0) { -+ pty_set_spawn_error(err, "posix_spawnattr_setflags", res); - goto done; - } - - sigset_t signal_set; - /* Reset all signal the child to their default behavior */ - sigfillset(&signal_set); -- *err = posix_spawnattr_setsigdefault(&attrs, &signal_set); -- if (*err != 0) { -+ res = posix_spawnattr_setsigdefault(&attrs, &signal_set); -+ if (res != 0) { -+ pty_set_spawn_error(err, "posix_spawnattr_setsigdefault", res); - goto done; - } - - /* Reset the signal mask for all signals */ - sigemptyset(&signal_set); -- *err = posix_spawnattr_setsigmask(&attrs, &signal_set); -- if (*err != 0) { -+ res = posix_spawnattr_setsigmask(&attrs, &signal_set); -+ if (res != 0) { -+ pty_set_spawn_error(err, "posix_spawnattr_setsigmask", res); - goto done; - } - - do -- *err = posix_spawn(pid, argv[0], &acts, &attrs, argv, env); -- while (*err == EINTR); -+ res = posix_spawn(pid, argv[0], &acts, &attrs, argv, env); -+ while (res == EINTR); -+ if (res != 0) { -+ pty_set_spawn_error(err, "posix_spawn", res, "helper", argv[0]); -+ } - done: -- posix_spawn_file_actions_destroy(&acts); -- posix_spawnattr_destroy(&attrs); -+ if (acts_initialized) { -+ posix_spawn_file_actions_destroy(&acts); -+ } -+ if (attrs_initialized) { -+ posix_spawnattr_destroy(&attrs); -+ } -+ if (slave != -1) { -+ close(slave); -+ } -+ if (err->errnum != 0 && *master != -1) { -+ close(*master); -+ *master = -1; -+ } - -- for (; count > 0; count--) { -- close(low_fds[count]); -+ for (size_t i = 0; i <= count && i < 3; i++) { -+ if (low_fds[i] != -1) { -+ close(low_fds[i]); -+ } - } - } - #endif -diff --git a/src/unixTerminal.ts b/src/unixTerminal.ts -index 98733dc0cd752b554bd94e45904ca341ad141bba..3dd5ad9b3124dbd5ba7f76679b26650dad503ec6 100644 ---- a/src/unixTerminal.ts -+++ b/src/unixTerminal.ts -@@ -306,6 +306,14 @@ interface IWriteTask { - offset: number; - } - -+/** -+ * Orca: upstream retries EAGAIN with `setImmediate`, which re-attempts within microseconds -+ * and pins a core on the daemon thread when a reader stops draining. 1ms keeps ~97% of that -+ * CPU saving; longer delays cost 2-3x delivery time to a reader that drains in bursts (an -+ * agent). Re-measure both before changing. -+ */ -+const EAGAIN_RETRY_DELAY_MS = 1; -+ - /** - * A custom write stream that writes directly to a file descriptor with proper - * handling of backpressure and errors. This avoids some event loop exhaustion -@@ -314,20 +322,28 @@ interface IWriteTask { - class CustomWriteStream implements IDisposable { - - private readonly _writeQueue: IWriteTask[] = []; -- private _writeImmediate: NodeJS.Immediate | undefined; -+ private _writeImmediate: NodeJS.Timeout | undefined; - - constructor( -- private readonly _fd: number, -+ private _fd: number, - private readonly _encoding: BufferEncoding - ) { - } - - dispose(): void { -- clearImmediate(this._writeImmediate); -+ clearTimeout(this._writeImmediate); - this._writeImmediate = undefined; -+ // Orca: retire this stream's own copy of the master fd and drop what has -+ // not shipped, so nothing queued here reaches a reused descriptor. -+ this._fd = -1; -+ this._writeQueue.length = 0; - } - - write(data: string | Buffer): void { -+ if (this._fd < 0) { -+ return; -+ } -+ - // Writes are put in a queue and processed asynchronously in order to handle - // backpressure from the kernel buffer. - const buffer = typeof data === 'string' -@@ -345,7 +361,8 @@ class CustomWriteStream implements IDisposable { - private _processWriteQueue(): void { - this._writeImmediate = undefined; - -- if (this._writeQueue.length === 0) { -+ // Orca: an in-flight fs.write can re-enter here after dispose(). -+ if (this._fd < 0 || this._writeQueue.length === 0) { - return; - } - -@@ -357,9 +374,9 @@ class CustomWriteStream implements IDisposable { - fs.write(this._fd, task.buffer, task.offset, (err, written) => { - if (err) { - if ('code' in err && err.code === 'EAGAIN') { -- // `setImmediate` is used to yield to the event loop and re-attempt -- // the write later. -- this._writeImmediate = setImmediate(() => this._processWriteQueue()); -+ // Paced, not `setImmediate`: a stalled reader keeps this branch EAGAIN-ing, and an -+ // immediate re-attempt turns the retry into a busy-loop on the daemon thread. -+ this._writeImmediate = setTimeout(() => this._processWriteQueue(), EAGAIN_RETRY_DELAY_MS); - } else { - // Stop processing immediately on unexpected error and log - this._writeQueue.length = 0; -diff --git a/src/win/conpty.cc b/src/win/conpty.cc -index 7b286d3d64..5239ba4e40 100644 ---- a/src/win/conpty.cc -+++ b/src/win/conpty.cc -@@ -18,6 +18,7 @@ - #include - #include - #include -+#include - #include - #include - #include -@@ -44,15 +45,37 @@ struct pty_baton { - HANDLE hOut; - HPCON hpc; - -- HANDLE hShell; -+ HANDLE hShell = nullptr; -+ // Orca: the shell's pid, captured at spawn. The ownership guard compares -+ // against this rather than calling GetProcessId(hShell), because the exit -+ // watcher closes hShell on another thread -- reading it there is an -+ // invalid-handle operation, and under strict handle checks that is fatal. -+ DWORD shellPid = 0; -+ -+ // Orca: job object owning this pty's whole process tree. Null when the OS -+ // refused to create or assign one (an outer job without breakaway rights), -+ // in which case callers fall back to their pre-job behaviour. -+ HANDLE hJob = nullptr; -+ bool allowJobBreakaway = true; -+ -+ // Orca: teardown needs BOTH the shell's death and an explicit kill() before -+ // the baton can be freed, so each side records that it has run. Whichever -+ // arrives second frees it. Freeing on the shell's death alone -- what this -+ // file did before -- destroyed the only record of `hpc` while -+ // ClosePseudoConsole was still owed, which is why a self-exiting shell -+ // leaked its pseudoconsole and the console host it reaps (#18601 / F24). -+ bool shellExited = false; -+ bool consoleClosed = false; - - pty_baton(int _id, HANDLE _hIn, HANDLE _hOut, HPCON _hpc) : id(_id), hIn(_hIn), hOut(_hOut), hpc(_hpc) {}; - }; - - static std::vector> ptyHandles; -+// Orca: every table access shares the exit watcher's lock; handles can be recycled. -+static std::mutex ptyJobMutex; - static volatile LONG ptyCounter; - --static pty_baton* get_pty_baton(int id) { -+static pty_baton* get_pty_baton_locked(int id) { - auto it = std::find_if(ptyHandles.begin(), ptyHandles.end(), [id](const auto& ptyHandle) { - return ptyHandle->id == id; - }); -@@ -62,7 +85,7 @@ static pty_baton* get_pty_baton(int id) { - return nullptr; - } - --static bool remove_pty_baton(int id) { -+static bool remove_pty_baton_locked(int id) { - auto it = std::remove_if(ptyHandles.begin(), ptyHandles.end(), [id](const auto& ptyHandle) { - return ptyHandle->id == id; - }); -@@ -102,8 +125,31 @@ void SetupExitCallback(Napi::Env env, Napi::Function cb, pty_baton* baton) { - // Get process exit code. - GetExitCodeProcess(baton->hShell, (LPDWORD)(&exit_event->exit_code)); - // Clean up handles -- CloseHandle(baton->hShell); -- assert(remove_pty_baton(baton->id)); -+ // Orca: release the job once the shell is gone. Without kill-on-close this -+ // only frees the handle -- anything the user backgrounded is orphaned, as -+ // it was before this patch. -+ { -+ std::lock_guard guard(ptyJobMutex); -+ CloseHandle(baton->hShell); -+ baton->hShell = nullptr; -+ if (baton->hJob != nullptr) { -+ CloseHandle(baton->hJob); -+ baton->hJob = nullptr; -+ } -+ // Why inside the lock: erasing frees the baton the job accessors hold a -+ // pointer to. Note remove_pty_baton must not be an assert() argument -- -+ // NDEBUG would compile the call away and leak every baton. -+ baton->shellExited = true; -+ if (baton->consoleClosed) { -+ const bool removed = remove_pty_baton_locked(baton->id); -+ assert(removed); -+ (void)removed; -+ } -+ // Else PtyKill has not run yet and still owns hpc. It frees the baton. -+ } -+ // Why the lock ends here: BlockingCall below waits on the JS thread, and the -+ // JS thread can be waiting on ptyJobMutex inside PtyTerminateJob. Holding -+ // the lock across it deadlocks. Do not widen this scope. - - auto status = tsfn.BlockingCall(exit_event, callback); // In main thread - switch (status) { -@@ -242,6 +288,20 @@ HRESULT CreateNamedPipesAndPseudoConsole(const Napi::CallbackInfo& info, - return HRESULT_FROM_WIN32(GetLastError()); - } - -+// Cygwin and MSYS request breakaway for every child whenever the job allows it, -+// so their shells need one that does not. The runtime DLL on the exe's search -+// path is the signal; Git for Windows ships bash.exe in bin\ beside usr\bin\. -+static bool usesCygwinRuntime(const std::wstring& shellpath) { -+ const size_t separator = shellpath.find_last_of(L"\\/"); -+ if (separator == std::wstring::npos) return false; -+ const std::wstring directory = shellpath.substr(0, separator + 1); -+ for (const wchar_t* dll : {L"msys-2.0.dll", L"cygwin1.dll"}) { -+ if (path_util::file_exists(directory + dll) || -+ path_util::file_exists(directory + L"..\\usr\\bin\\" + dll)) return true; -+ } -+ return false; -+} -+ - static Napi::Value PtyStartProcess(const Napi::CallbackInfo& info) { - Napi::Env env(info.Env()); - Napi::HandleScope scope(env); -@@ -301,8 +361,10 @@ static Napi::Value PtyStartProcess(const Napi::CallbackInfo& info) { - // We were able to instantiate a conpty - const int ptyId = InterlockedIncrement(&ptyCounter); - marshal.Set("pty", Napi::Number::New(env, ptyId)); -- ptyHandles.emplace_back( -- std::make_unique(ptyId, hIn, hOut, hpc)); -+ auto baton = std::make_unique(ptyId, hIn, hOut, hpc); -+ baton->allowJobBreakaway = !usesCygwinRuntime(shellpath); -+ std::lock_guard guard(ptyJobMutex); -+ ptyHandles.emplace_back(std::move(baton)); - } else { - throw Napi::Error::New(env, "Cannot launch conpty"); - } -@@ -350,11 +412,15 @@ static Napi::Value PtyConnect(const Napi::CallbackInfo& info) { - const bool useConptyDll = info[4].As().Value(); - Napi::Function exitCallback = info[5].As(); - -- // Fetch pty handle from ID and start process -- pty_baton* handle = get_pty_baton(id); -- if (!handle) { -- throw Napi::Error::New(env, "Invalid pty handle"); -+ pty_baton* handle; -+ { -+ std::lock_guard guard(ptyJobMutex); -+ handle = get_pty_baton_locked(id); -+ if (!handle || handle->consoleClosed) { -+ throw Napi::Error::New(env, "Invalid pty handle"); -+ } - } -+ // No watcher exists for this baton until SetupExitCallback below; pipe waits stay unlocked. - - // Prepare command line - std::unique_ptr mutableCommandline = std::make_unique(cmdline.length() + 1); -@@ -409,6 +475,15 @@ static Napi::Value PtyConnect(const Napi::CallbackInfo& info) { - throw errorWithCode(info, "UpdateProcThreadAttribute failed"); - } - -+ // Orca: resolve the DLL BEFORE creating anything. It throws when conpty.dll -+ // is missing -- a real state, and one this branch hit during development -- -+ // and every throw between CreateProcessW and SetupExitCallback leaks the job, -+ // process and thread handles AND leaves an untracked shell tree running, -+ // once per attempt. Validating first means the only throw after creation is -+ // the resume failure, which cleans up after itself. -+ HANDLE hLibrary = LoadConptyDll(info, useConptyDll); -+ bool fLoadedDll = hLibrary != nullptr; -+ - PROCESS_INFORMATION piClient{}; - fSuccess = !!CreateProcessW( - nullptr, -@@ -416,7 +491,10 @@ static Napi::Value PtyConnect(const Napi::CallbackInfo& info) { - nullptr, // lpProcessAttributes - nullptr, // lpThreadAttributes - false, // bInheritHandles VERY IMPORTANT that this is false -- EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT, // dwCreationFlags -+ // Orca: CREATE_SUSPENDED so the shell is inside its job before it can -+ // spawn anything. Assigning after the fact leaves a window in which a -+ // fast child escapes the job and outlives the pane. -+ EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT | CREATE_SUSPENDED, // dwCreationFlags - envArg, // lpEnvironment - mutableCwd.get(), // lpCurrentDirectory - &siEx.StartupInfo, // lpStartupInfo -@@ -426,8 +504,48 @@ static Napi::Value PtyConnect(const Napi::CallbackInfo& info) { - throw errorWithCode(info, "Cannot create process"); - } - -- HANDLE hLibrary = LoadConptyDll(info, useConptyDll); -- bool fLoadedDll = hLibrary != nullptr; -+ // Orca: own the tree with a handle instead of inferring it later from a -+ // parent-pid walk. A pid walk cannot survive pid reuse and cannot see a -+ // descendant that reparented, which is why detached agent children outlived -+ // their pane and held the worktree directory open. -+ // -+ // Deliberately WITHOUT JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE. Measured on -+ // Windows 11: with that flag, closing the handle when the shell exits also -+ // kills whatever the user left running, so typing `exit` in a pane reaped a -+ // `start /b` server that used to survive. This job exists to make an -+ // EXPLICIT teardown exact, not to redefine what a clean exit means. -+ HANDLE hJob = CreateJobObjectW(nullptr, nullptr); -+ if (hJob != nullptr) { -+ // Native shells retain explicit breakaway for installers and updaters. -+ // Cygwin/MSYS shells take it automatically for ordinary children whenever -+ // this flag is present, so they get strict per-PTY membership instead. -+ // Explicit breakaway requests inside such a pane are consequently denied; -+ // ordinary backgrounding and clean shell exit remain supported. -+ JOBOBJECT_EXTENDED_LIMIT_INFORMATION jobLimits{}; -+ jobLimits.BasicLimitInformation.LimitFlags = -+ handle->allowJobBreakaway ? JOB_OBJECT_LIMIT_BREAKAWAY_OK : 0; -+ if (!SetInformationJobObject(hJob, JobObjectExtendedLimitInformation, &jobLimits, sizeof(jobLimits)) || -+ !AssignProcessToJobObject(hJob, piClient.hProcess)) { -+ // Why tolerate failure: an outer job without JOB_OBJECT_LIMIT_BREAKAWAY_OK -+ // (some EDR and container hosts) refuses the assignment. The pty must -+ // still start; ownership just degrades to the older best-effort path. -+ CloseHandle(hJob); -+ hJob = nullptr; -+ } -+ } -+ // Safe to run now: either it is in the job, or we accepted that it is not. -+ if (ResumeThread(piClient.hThread) == static_cast(-1)) { -+ // Why fatal: a shell left suspended produces a pane that never prints and -+ // never exits, which is far harder to diagnose than a failed spawn. -+ if (hJob != nullptr) { -+ CloseHandle(hJob); -+ } -+ TerminateProcess(piClient.hProcess, 1); -+ CloseHandle(piClient.hProcess); -+ CloseHandle(piClient.hThread); -+ throw errorWithCode(info, "Cannot resume process"); -+ } -+ - if (useConptyDll && fLoadedDll) - { - PFNRELEASEPSEUDOCONSOLE const pfnReleasePseudoConsole = (PFNRELEASEPSEUDOCONSOLE)GetProcAddress( -@@ -438,8 +556,12 @@ static Napi::Value PtyConnect(const Napi::CallbackInfo& info) { - } - } - -- // Update handle -- handle->hShell = piClient.hProcess; -+ { -+ std::lock_guard guard(ptyJobMutex); -+ handle->hShell = piClient.hProcess; -+ handle->shellPid = piClient.dwProcessId; -+ handle->hJob = hJob; -+ } - - // Close the thread handle to avoid resource leak - CloseHandle(piClient.hThread); -@@ -472,9 +594,10 @@ static Napi::Value PtyResize(const Napi::CallbackInfo& info) { - SHORT rows = static_cast(info[2].As().Uint32Value()); - const bool useConptyDll = info[3].As().Value(); - -- const pty_baton* handle = get_pty_baton(id); -+ std::lock_guard guard(ptyJobMutex); -+ const pty_baton* handle = get_pty_baton_locked(id); - -- if (handle != nullptr) { -+ if (handle != nullptr && !handle->consoleClosed) { - HANDLE hLibrary = LoadConptyDll(info, useConptyDll); - bool fLoadedDll = hLibrary != nullptr; - if (fLoadedDll) -@@ -513,9 +636,10 @@ static Napi::Value PtyClear(const Napi::CallbackInfo& info) { - return env.Undefined(); - } - -- const pty_baton* handle = get_pty_baton(id); -+ std::lock_guard guard(ptyJobMutex); -+ const pty_baton* handle = get_pty_baton_locked(id); - -- if (handle != nullptr) { -+ if (handle != nullptr && !handle->consoleClosed) { - HANDLE hLibrary = LoadConptyDll(info, useConptyDll); - bool fLoadedDll = hLibrary != nullptr; - if (fLoadedDll) -@@ -544,29 +668,215 @@ static Napi::Value PtyKill(const Napi::CallbackInfo& info) { - int id = info[0].As().Int32Value(); - const bool useConptyDll = info[1].As().Value(); - -- const pty_baton* handle = get_pty_baton(id); -+ // Orca: resolve the DLL BEFORE touching any baton state, for the same reason -+ // PtyConnect does it before creating anything. LoadConptyDll throws when -+ // conpty.dll is missing, and a throw after consoleClosed was set would strand -+ // the pseudoconsole permanently: the retry would find the work already -+ // claimed and do nothing. Only the useConptyDll path can throw here; the -+ // other returns kernel32. -+ HANDLE hLibrary = LoadConptyDll(info, useConptyDll); -+ PFNCLOSEPSEUDOCONSOLE pfnClosePseudoConsole = nullptr; -+ if (hLibrary != nullptr) { -+ pfnClosePseudoConsole = (PFNCLOSEPSEUDOCONSOLE)GetProcAddress( -+ (HMODULE)hLibrary, -+ useConptyDll ? "ConptyClosePseudoConsole" : "ClosePseudoConsole"); -+ } - -- if (handle != nullptr) { -- HANDLE hLibrary = LoadConptyDll(info, useConptyDll); -- bool fLoadedDll = hLibrary != nullptr; -- if (fLoadedDll) -- { -- PFNCLOSEPSEUDOCONSOLE const pfnClosePseudoConsole = (PFNCLOSEPSEUDOCONSOLE)GetProcAddress( -- (HMODULE)hLibrary, -- useConptyDll ? "ConptyClosePseudoConsole" : "ClosePseudoConsole"); -- if (pfnClosePseudoConsole) -- { -- pfnClosePseudoConsole(handle->hpc); -+ // Orca: the baton now outlives the shell, so this runs on a self-exited pty -+ // too -- that is the whole point. Take what we need under the lock: the -+ // watcher thread nulls hShell the moment the shell dies, and TerminateProcess -+ // on a handle it just closed is an invalid-handle operation. Duplicating -+ // rather than reordering keeps upstream's close-then-terminate sequence. -+ HPCON hpc = nullptr; -+ HANDLE hShellDup = nullptr; -+ bool owed = false; -+ { -+ std::lock_guard guard(ptyJobMutex); -+ pty_baton* handle = get_pty_baton_locked(id); -+ // Why the consoleClosed check: a second kill() would otherwise close the -+ // same pseudoconsole twice. Upstream relied on the baton being gone. -+ if (handle != nullptr && !handle->consoleClosed) { -+ hpc = handle->hpc; -+ owed = true; -+ handle->consoleClosed = true; -+ // Null hShell means a self-exited pty, where there is nothing to kill. -+ if (useConptyDll && handle->hShell != nullptr) { -+ if (!DuplicateHandle(GetCurrentProcess(), handle->hShell, GetCurrentProcess(), -+ &hShellDup, 0, FALSE, DUPLICATE_SAME_ACCESS)) { -+ // Why terminate here instead of skipping: a failed duplication leaves -+ // hShellDup null, which is indistinguishable from the self-exit case, -+ // and skipping would leave the shell RUNNING after its pane closed -- -+ // a worse outcome than the leak this all exists to fix. hShell is -+ // valid under this lock and TerminateProcess does not block, so the -+ // only cost is that this rare path kills before the console closes. -+ hShellDup = nullptr; -+ TerminateProcess(handle->hShell, 1); -+ } - } -+ if (handle->shellExited) { -+ const bool removed = remove_pty_baton_locked(id); -+ assert(removed); -+ (void)removed; -+ } -+ // Else the shell is still running and the watcher frees the baton. -+ } -+ } -+ -+ // Why outside the lock: ClosePseudoConsole blocks until the conout side has -+ // drained, and the watcher must be able to take the lock while it does. -+ if (owed) { -+ if (pfnClosePseudoConsole) -+ { -+ pfnClosePseudoConsole(hpc); - } -- if (useConptyDll) { -- TerminateProcess(handle->hShell, 1); -+ if (hShellDup != nullptr) { -+ TerminateProcess(hShellDup, 1); -+ CloseHandle(hShellDup); - } - } - - return env.Undefined(); - } - -+/** -+ * Orca: confirm a baton really is the pty the caller means. -+ * -+ * The winpty backend mints its own `pty` ids from a separate counter, and the -+ * JS layer stores both in the same field -- so a winpty terminal's id can -+ * collide with a live ConPTY baton here and terminate an unrelated pane's whole -+ * process tree. Matching the shell pid makes the id unforgeable. -+ */ -+static bool ownsShell(const pty_baton* handle, DWORD expectedShellPid) { -+ return handle != nullptr && handle->hJob != nullptr && expectedShellPid != 0 && -+ handle->shellPid == expectedShellPid; -+} -+ -+/** -+ * Orca: kill this pty's entire tree in one syscall. -+ * -+ * Replaces "scrape the process table, walk parent pids, hope none were -+ * recycled, then taskkill /T /F". Returns false when no job was assigned so -+ * the caller knows to fall back rather than assume the tree is gone. -+ */ -+static Napi::Value PtyTerminateJob(const Napi::CallbackInfo& info) { -+ Napi::Env env(info.Env()); -+ Napi::HandleScope scope(env); -+ -+ if (info.Length() != 2 || !info[0].IsNumber() || !info[1].IsNumber()) { -+ throw Napi::Error::New(env, "Usage: pty.terminateJob(id, shellPid)"); -+ } -+ -+ // Held across the lookup AND the Win32 call: the watcher thread can otherwise -+ // close these handles and free the baton in between. -+ std::lock_guard guard(ptyJobMutex); -+ const pty_baton* handle = get_pty_baton_locked(info[0].As().Int32Value()); -+ if (!ownsShell(handle, info[1].As().Uint32Value())) { -+ return Napi::Boolean::New(env, false); -+ } -+ return Napi::Boolean::New(env, !!TerminateJobObject(handle->hJob, 1)); -+} -+ -+/** -+ * Orca: the pids still alive in this pty's tree, straight from the kernel. -+ * -+ * Descendant liveness for a tree that is still tracked, including children that -+ * detached from the console. Once the shell exits the watcher nulls hJob, which -+ * ownsShell rejects, so this returns null rather than an empty list -- null -+ * means "no answer", never "they died". (The baton itself now outlives the -+ * shell, until kill() runs; hJob is what makes the answer null.) Also returns -+ * null when no job was assigned. -+ * -+ * Does not include the ConPTY console host: CreatePseudoConsole spawns it -+ * before this job exists, so it is not a member and ClosePseudoConsole is what -+ * reaps it. -+ */ -+static Napi::Value PtyListJobProcessIds(const Napi::CallbackInfo& info) { -+ Napi::Env env(info.Env()); -+ Napi::HandleScope scope(env); -+ -+ if (info.Length() != 2 || !info[0].IsNumber() || !info[1].IsNumber()) { -+ throw Napi::Error::New(env, "Usage: pty.listJobProcessIds(id, shellPid)"); -+ } -+ -+ // Held across the lookup AND the Win32 call: the watcher thread can otherwise -+ // close these handles and free the baton in between. -+ std::lock_guard guard(ptyJobMutex); -+ const pty_baton* handle = get_pty_baton_locked(info[0].As().Int32Value()); -+ if (!ownsShell(handle, info[1].As().Uint32Value())) { -+ return env.Null(); -+ } -+ -+ // Grow until the buffer holds every pid: the count can change between calls, -+ // and a truncated list would read as "these children are gone". -+ DWORD capacity = 64; -+ for (int attempt = 0; attempt < 8; attempt++) { -+ const size_t bytes = sizeof(JOBOBJECT_BASIC_PROCESS_ID_LIST) + sizeof(ULONG_PTR) * capacity; -+ std::vector buffer(bytes, 0); -+ auto* list = reinterpret_cast(buffer.data()); -+ if (QueryInformationJobObject(handle->hJob, JobObjectBasicProcessIdList, list, static_cast(bytes), nullptr)) { -+ auto pids = Napi::Array::New(env, list->NumberOfProcessIdsInList); -+ for (DWORD i = 0; i < list->NumberOfProcessIdsInList; i++) { -+ pids.Set(i, Napi::Number::New(env, static_cast(list->ProcessIdList[i]))); -+ } -+ return pids; -+ } -+ if (GetLastError() != ERROR_MORE_DATA) { -+ return env.Null(); -+ } -+ capacity *= 4; -+ } -+ return env.Null(); -+} -+ -+/** -+ * Orca: put THIS process in a kill-on-close job, so its whole descendant tree -+ * dies with it. -+ * -+ * Why here and not per-pty: a per-pty job cannot carry KILL_ON_JOB_CLOSE, -+ * because its handle is released when the shell exits and that would reap -+ * whatever the user had backgrounded. This job's handle is released only when -+ * the process itself dies, so it reaps a crashed host without changing what a -+ * clean shell exit means. Children inherit job membership, so every pty the -+ * caller later spawns is covered without further work, and the per-pty jobs -+ * simply nest inside this one. -+ * -+ * The handle is deliberately never closed: it must outlive every caller, and -+ * process teardown is what releases it. -+ */ -+static Napi::Value PtyAssignCurrentProcessToJob(const Napi::CallbackInfo& info) { -+ Napi::Env env(info.Env()); -+ Napi::HandleScope scope(env); -+ -+ // Why locked: two callers racing here would each create a job, put the -+ // process in both, and leak the first handle -- and since the handle is what -+ // keeps a kill-on-close job alive, a leaked one is never released. A worker -+ // thread with its own N-API env shares these statics, so "only JS calls it" -+ // is not a guarantee. -+ static HANDLE hHostJob = nullptr; -+ std::lock_guard guard(ptyJobMutex); -+ if (hHostJob != nullptr) { -+ return Napi::Boolean::New(env, true); -+ } -+ -+ HANDLE job = CreateJobObjectW(nullptr, nullptr); -+ if (job == nullptr) { -+ return Napi::Boolean::New(env, false); -+ } -+ JOBOBJECT_EXTENDED_LIMIT_INFORMATION limits{}; -+ // BREAKAWAY_OK for the same reason as the per-pty job: without it a child -+ // asking for CREATE_BREAKAWAY_FROM_JOB is refused outright. -+ limits.BasicLimitInformation.LimitFlags = -+ JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE | JOB_OBJECT_LIMIT_BREAKAWAY_OK; -+ if (!SetInformationJobObject(job, JobObjectExtendedLimitInformation, &limits, sizeof(limits)) || -+ !AssignProcessToJobObject(job, GetCurrentProcess())) { -+ // An outer job that forbids nesting refuses this; the caller degrades. -+ CloseHandle(job); -+ return Napi::Boolean::New(env, false); -+ } -+ hHostJob = job; -+ return Napi::Boolean::New(env, true); -+} -+ - /** - * Init - */ -@@ -577,6 +887,9 @@ Napi::Object init(Napi::Env env, Napi::Object exports) { - exports.Set("resize", Napi::Function::New(env, PtyResize)); - exports.Set("clear", Napi::Function::New(env, PtyClear)); - exports.Set("kill", Napi::Function::New(env, PtyKill)); -+ exports.Set("terminateJob", Napi::Function::New(env, PtyTerminateJob)); -+ exports.Set("listJobProcessIds", Napi::Function::New(env, PtyListJobProcessIds)); -+ exports.Set("assignCurrentProcessToJob", Napi::Function::New(env, PtyAssignCurrentProcessToJob)); - return exports; - }; - -diff --git a/lib/windowsPtyAgent.js b/lib/windowsPtyAgent.js -index a358ffb177357e177661033c1b092f9c9d0e5f5a..26c2a4c58799ce649f5113131e4c52f7ed2d87ad 100644 ---- a/lib/windowsPtyAgent.js -+++ b/lib/windowsPtyAgent.js -@@ -136,6 +136,9 @@ var WindowsPtyAgent = /** @class */ (function () { - if (this._useConpty) { - if (!this._useConptyDll) { - this._inSocket.readable = false; -+ // The non-DLL path previously only flipped `readable`, leaving the -+ // conin PipeWrap alive until the host exited (#947). -+ this._inSocket.destroy(); - this._outSocket.readable = false; - this._getConsoleProcessList().then(function (consoleProcessList) { - consoleProcessList.forEach(function (pid) { -@@ -154,9 +157,10 @@ var WindowsPtyAgent = /** @class */ (function () { - // Close the input write handle to signal the end of session. - this._inSocket.destroy(); - this._ptyNative.kill(this._pty, this._useConptyDll); -- this._outSocket.on('data', function () { -- _this._conoutSocketWorker.dispose(); -- }); -+ // Orca: dispose unconditionally, as the non-DLL branch above does. -+ // Waiting for another 'data' event leaks the conout worker on every -+ // self-exiting shell, because no more data ever arrives (F24). -+ this._conoutSocketWorker.dispose(); - } - } - else { -diff --git a/lib/windowsTerminal.js b/lib/windowsTerminal.js -index 3c38f89..e20b3e6 100644 ---- a/lib/windowsTerminal.js -+++ b/lib/windowsTerminal.js -@@ -50,6 +50,27 @@ var WindowsTerminal = /** @class */ (function (_super) { - // Create new termal. - _this._agent = new windowsPtyAgent_1.WindowsPtyAgent(file, args, parsedEnv, cwd, _this._cols, _this._rows, false, opt.useConpty, opt.useConptyDll, opt.conptyInheritCursor); - _this._socket = _this._agent.outSocket; -+ // Attach before readiness so a broken ConPTY output pipe cannot be unhandled. -+ _this._socket.on('error', function (err) { -+ var code = err && err.code; -+ // PTY output can report EPIPE before `_close()` wins the race. -+ _this._close(); -+ if (code === 'EPIPE' || code === 'ERR_STREAM_PUSH_AFTER_EOF' || code === 'ERR_STREAM_DESTROYED') { -+ return; -+ } -+ // EIO, happens when someone closes our child process: the only process -+ // in the terminal. -+ // node < 0.6.14: errno 5 -+ // node >= 0.6.14: read EIO -+ if (typeof code === 'string') { -+ if (~code.indexOf('errno 5') || ~code.indexOf('EIO')) -+ return; -+ } -+ // Throw anything else. -+ if (_this.listeners('error').length < 2) { -+ throw err; -+ } -+ }); - // Not available until `ready` event emitted. - _this._pid = _this._agent.innerPid; - _this._fd = _this._agent.fd; -@@ -76,23 +99,6 @@ var WindowsTerminal = /** @class */ (function (_super) { - _this._deferreds = []; - } - }); -- // Shutdown if `error` event is emitted. -- _this._socket.on('error', function (err) { -- // Close terminal session. -- _this._close(); -- // EIO, happens when someone closes our child process: the only process -- // in the terminal. -- // node < 0.6.14: errno 5 -- // node >= 0.6.14: read EIO -- if (err.code) { -- if (~err.code.indexOf('errno 5') || ~err.code.indexOf('EIO')) -- return; -- } -- // Throw anything else. -- if (_this.listeners('error').length < 2) { -- throw err; -- } -- }); - // Cleanup after the socket is closed. - _this._socket.on('close', function () { - _this.emit('exit', _this._agent.exitCode); -@@ -103,6 +109,20 @@ var WindowsTerminal = /** @class */ (function (_super) { - _this._name = name; - _this._readable = true; - _this._writable = true; -+ // A ConPTY input-pipe error must retire only this terminal. Without a listener, Node promotes -+ // errors such as write EAGAIN to uncaughtException and kills every PTY in the daemon. -+ _this._agent.inSocket.on('error', function () { -+ if (!_this._writable) { -+ return; -+ } -+ _this._close(); -+ try { -+ _this._agent.kill(); -+ } -+ catch (_a) { -+ // The failing pipe may have raced process exit; the terminal is already unwritable. -+ } -+ }); - _this._forwardEvents(); - return _this; - } -@@ -196,4 +216,4 @@ var WindowsTerminal = /** @class */ (function (_super) { - return WindowsTerminal; - }(terminal_1.Terminal)); - exports.WindowsTerminal = WindowsTerminal; --//# sourceMappingURL=windowsTerminal.js.map -\ No newline at end of file -+//# sourceMappingURL=windowsTerminal.js.map -diff --git a/src/windowsPtyAgent.ts b/src/windowsPtyAgent.ts -index d7054449516f0c9a62af351c2caa17331206d530..0c28a32e2e1db2b3f208ddde8443cd4e67bb1ad6 100644 ---- a/src/windowsPtyAgent.ts -+++ b/src/windowsPtyAgent.ts -@@ -143,6 +143,9 @@ export class WindowsPtyAgent { - if (this._useConpty) { - if (!this._useConptyDll) { - this._inSocket.readable = false; -+ // The non-DLL path previously only flipped `readable`, leaving the -+ // conin PipeWrap alive until the host exited (#947). -+ this._inSocket.destroy(); - this._outSocket.readable = false; - this._getConsoleProcessList().then(consoleProcessList => { - consoleProcessList.forEach((pid: number) => { -@@ -159,9 +162,10 @@ export class WindowsPtyAgent { - // Close the input write handle to signal the end of session. - this._inSocket.destroy(); - (this._ptyNative as IConptyNative).kill(this._pty, this._useConptyDll); -- this._outSocket.on('data', () => { -- this._conoutSocketWorker.dispose(); -- }); -+ // Orca: dispose unconditionally, as the non-DLL branch above does. -+ // Waiting for another 'data' event leaks the conout worker on every -+ // self-exiting shell, because no more data ever arrives (F24). -+ this._conoutSocketWorker.dispose(); - } - } else { - // Because pty.kill closes the handle, it will kill most processes by itself. -diff --git a/src/windowsTerminal.ts b/src/windowsTerminal.ts -index 13f6c6d..eda63c8 100644 ---- a/src/windowsTerminal.ts -+++ b/src/windowsTerminal.ts -@@ -51,6 +51,30 @@ export class WindowsTerminal extends Terminal { - this._agent = new WindowsPtyAgent(file, args, parsedEnv, cwd, this._cols, this._rows, false, opt.useConpty, opt.useConptyDll, opt.conptyInheritCursor); - this._socket = this._agent.outSocket; -- -+ -+ // Attach before readiness so a broken ConPTY output pipe cannot be unhandled. -+ this._socket.on('error', err => { -+ const code = (err).code; -+ -+ // PTY output can report EPIPE before `_close()` wins the race. -+ this._close(); -+ if (code === 'EPIPE' || code === 'ERR_STREAM_PUSH_AFTER_EOF' || code === 'ERR_STREAM_DESTROYED') { -+ return; -+ } -+ -+ // EIO, happens when someone closes our child process: the only process -+ // in the terminal. -+ // node < 0.6.14: errno 5 -+ // node >= 0.6.14: read EIO -+ if (typeof code === 'string') { -+ if (~code.indexOf('errno 5') || ~code.indexOf('EIO')) return; -+ } -+ -+ // Throw anything else. -+ if (this.listeners('error').length < 2) { -+ throw err; -+ } -+ }); -+ - // Not available until `ready` event emitted. - this._pid = this._agent.innerPid; - this._fd = this._agent.fd; -@@ -82,25 +108,6 @@ export class WindowsTerminal extends Terminal { - } - }); -- -+ -- // Shutdown if `error` event is emitted. -- this._socket.on('error', err => { -- // Close terminal session. -- this._close(); -- -- // EIO, happens when someone closes our child process: the only process -- // in the terminal. -- // node < 0.6.14: errno 5 -- // node >= 0.6.14: read EIO -- if ((err).code) { -- if (~(err).code.indexOf('errno 5') || ~(err).code.indexOf('EIO')) return; -- } -- -- // Throw anything else. -- if (this.listeners('error').length < 2) { -- throw err; -- } -- }); -- - // Cleanup after the socket is closed. - this._socket.on('close', () => { - this.emit('exit', this._agent.exitCode); -@@ -114,6 +121,19 @@ export class WindowsTerminal extends Terminal { -- -+ - this._readable = true; - this._writable = true; -+ // A ConPTY input-pipe error must retire only this terminal. Without a listener, Node promotes -+ // errors such as write EAGAIN to uncaughtException and kills every PTY in the daemon. -+ this._agent.inSocket.on('error', () => { -+ if (!this._writable) { -+ return; -+ } -+ this._close(); -+ try { -+ this._agent.kill(); -+ } catch { -+ // The failing pipe may have raced process exit; the terminal is already unwritable. -+ } -+ }); -- -+ - this._forwardEvents(); - } diff --git a/config/patches/xterm-src/@xterm__headless@6.1.0-beta.302.src.patch b/config/patches/xterm-src/@xterm__headless@6.1.0-beta.302.src.patch new file mode 100644 index 000000000000..898aeec0c5ab --- /dev/null +++ b/config/patches/xterm-src/@xterm__headless@6.1.0-beta.302.src.patch @@ -0,0 +1,21 @@ +diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts +index 995fe7670a10f11fb9b0b42332484b038ea6c989..133db3d372e91517ca42b9558adba3b97f1b9fb5 100644 +--- a/src/common/InputHandler.ts ++++ b/src/common/InputHandler.ts +@@ -540,6 +540,16 @@ export class InputHandler extends Disposable implements IInputHandler { + + let precedingJoinState = this._parser.precedingJoinState; + for (let pos = start; pos < end; ++pos) { ++ // Orca's width-one ASCII invariant excludes ZWJ joins; keep wrapping on the general path. ++ if (precedingJoinState === 2 && !charset && !screenReaderMode && !insertMode ++ && this._unicodeService.activeVersion === 'orca-11-zwj' && !this._getCurrentLinkId() ++ && this._activeBuffer.x < cols && data[pos] >= 0x20 && data[pos] < 0x7f) { ++ do { ++ bufferRow.setCellFromCodepoint(this._activeBuffer.x++, data[pos], 1, curAttr); ++ } while (++pos < end && this._activeBuffer.x < cols && data[pos] >= 0x20 && data[pos] < 0x7f); ++ pos--; ++ continue; ++ } + code = data[pos]; + + // Soft hyphen's (U+00AD) behavior is ambiguous and differs across terminals. We opt to treat diff --git a/config/patches/xterm-upstream.json b/config/patches/xterm-upstream.json index 729be67b1061..6bf0a9bf6f9f 100644 --- a/config/patches/xterm-upstream.json +++ b/config/patches/xterm-upstream.json @@ -136,6 +136,29 @@ "args": ["run", "esbuild-package"] } ] + }, + { + "name": "@xterm/headless", + "version": "6.1.0-beta.302", + "packageDir": ".", + "outputDir": "headless", + "sourcePatch": "config/patches/xterm-src/@xterm__headless@6.1.0-beta.302.src.patch", + "patch": "config/patches/@xterm__headless@6.1.0-beta.302.patch", + "generatedPaths": ["lib-headless/"], + "build": [ + { + "cwd": ".", + "command": "npm", + "args": ["run", "build"] + }, + { + "cwd": ".", + "command": "npm", + "args": ["run", "package-headless"] + } + ], + "$note": "Headless publishes source only in maps and leaves Version.ts at the upstream value; pristine reproduction verifies both bundles and maps.", + "sourceMaps": ["lib-headless/xterm-headless.js.map", "lib-headless/xterm-headless.mjs.map"] } ], "forbiddenBuildScripts": { diff --git a/config/relay-assets/node-pty-1.1.0-console-list-agent-patch.cjs b/config/relay-assets/node-pty-1.1.0-console-list-agent-patch.cjs deleted file mode 100644 index 051ea13cc464..000000000000 --- a/config/relay-assets/node-pty-1.1.0-console-list-agent-patch.cjs +++ /dev/null @@ -1,71 +0,0 @@ -const { createHash } = require('node:crypto') -const { readFileSync, renameSync, rmSync, writeFileSync } = require('node:fs') -const { join, resolve } = require('node:path') - -const EXPECTED_NODE_PTY_VERSION = '1.1.0' -const ORIGINAL_SOURCE_SHA256 = '0d010879bb6680a0253d44363183d53e631f42972594eb6dcb1fb842c8c85e52' -const PATCHED_SOURCE_SHA256 = '84df20cfe711a88d2bef35078615c58a6ce14f39348a4aef40e852b854dcd857' -const ORIGINAL_BODY = 'var consoleProcessList = getConsoleProcessList(shellPid);' -const PATCHED_BODY = `var consoleProcessList; -try { - consoleProcessList = getConsoleProcessList(shellPid); -} -catch (_a) { - // Why: AttachConsole can fail without a Win32 console; use node-pty's timeout fallback immediately. - consoleProcessList = [shellPid]; -}` - -function inspectNodePtyConsoleListAgent(relayDir = process.cwd()) { - const nodePtyDir = resolve(relayDir, 'node_modules', 'node-pty') - const packageJsonPath = join(nodePtyDir, 'package.json') - const agentPath = join(nodePtyDir, 'lib', 'conpty_console_list_agent.js') - const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) - if (packageJson.version !== EXPECTED_NODE_PTY_VERSION) { - throw new Error( - `Refusing to patch node-pty ${packageJson.version}; expected ${EXPECTED_NODE_PTY_VERSION}` - ) - } - const source = readFileSync(agentPath, 'utf8') - return { agentPath, source } -} - -function assertPatchedNodePtyConsoleListAgent(relayDir = process.cwd()) { - const inspected = inspectNodePtyConsoleListAgent(relayDir) - if (sourceSha256(inspected.source) !== PATCHED_SOURCE_SHA256) { - throw new Error('node-pty ConPTY console-list fallback is not installed') - } -} - -function patchNodePtyConsoleListAgent(relayDir = process.cwd()) { - const inspected = inspectNodePtyConsoleListAgent(relayDir) - const sourceHash = sourceSha256(inspected.source) - if (sourceHash === PATCHED_SOURCE_SHA256) { - return - } - if (sourceHash !== ORIGINAL_SOURCE_SHA256) { - throw new Error('Refusing to patch unexpected node-pty console-list agent source') - } - const patchedSource = inspected.source.replace(ORIGINAL_BODY, PATCHED_BODY) - const temporaryPath = `${inspected.agentPath}.orca-patch-${process.pid}` - // Why: a terminated remote install must leave either known source version recoverable on reconnect. - try { - writeFileSync(temporaryPath, patchedSource) - renameSync(temporaryPath, inspected.agentPath) - } finally { - rmSync(temporaryPath, { force: true }) - } - assertPatchedNodePtyConsoleListAgent(relayDir) -} - -function sourceSha256(source) { - return createHash('sha256').update(source).digest('hex') -} - -if (require.main === module) { - patchNodePtyConsoleListAgent() -} - -module.exports = { - assertPatchedNodePtyConsoleListAgent, - patchNodePtyConsoleListAgent -} diff --git a/config/relay-assets/node-pty-1.1.0-master-cloexec-patch.cjs b/config/relay-assets/node-pty-1.1.0-master-cloexec-patch.cjs deleted file mode 100644 index 40bc0350d86d..000000000000 --- a/config/relay-assets/node-pty-1.1.0-master-cloexec-patch.cjs +++ /dev/null @@ -1,448 +0,0 @@ -/** - * Relay-side pty fd-leak patch for node-pty 1.1.0 (#17915). - * - * The app gets this through pnpm `patchedDependencies`; the relay installs stock - * node-pty from npm onto the host, where no pnpm patch reaches. Stock 1.1.0 leaks - * a pty fd on both Unix relay platforms, by two unrelated bugs on two code paths. - * - * Linux takes forkpty(), which has no atomic O_CLOEXEC, so every later child of - * the relay -- pty children, git helpers, probes, agent CLIs -- inherits each live - * master and keeps its /dev/pts device alive for the life of the relay (#8362). - * - * macOS takes pty_posix_spawn(), which opens up to three throwaway ptys to push - * the real master off fds 0-2 and then never closes them: the cleanup loop is - * `for (; count > 0; count--)`, but in any running process the first posix_openpt() - * already returns >= 2, so the loop breaks with count == 0 and its body never runs - * -- and where it does run it closes low_fds[count], never low_fds[0]. Measured on - * darwin-arm64: one orphaned /dev/ptmx fd per terminal, never returned. - * - * macOS does not inherit the master into spawned children today, but not because it - * is marked: FD_CLOEXEC is not set on it (`lsof +fg` shows R,W,NB, no CX). What - * closes it is POSIX_SPAWN_CLOEXEC_DEFAULT in pty_posix_spawn's spawn flags, an - * Apple-only flag that closes every fd in the child. That is one option away from - * gone -- setting uid/gid drops libuv back to fork()/exec(), which honors nothing - * but FD_CLOEXEC -- so the master is marked on the Apple path too, exactly as the - * app's pnpm patch marks it. Windows has no fds and is excluded. - * - * The compile it buys differs by platform. Linux relays already run node-gyp at - * install time (1.1.0 ships no linux prebuild), so this is a second compile on a - * path that already compiles. macOS runs the shipped darwin prebuild and has no - * build/ at all, so this is its first compile -- the price of the only fix there - * is, since the bug is in the source that prebuild was built from. - * - * Non-fatal by construction: the working build is moved aside before anything is - * touched and moved back on any failure, and a failed attempt drops a skip marker - * so the compile is attempted at most once per relay directory. - */ - -const { spawnSync } = require('node:child_process') -const { createHash } = require('node:crypto') -const { - existsSync, - mkdirSync, - readFileSync, - renameSync, - rmSync, - writeFileSync -} = require('node:fs') -const { dirname, join, resolve } = require('node:path') - -const EXPECTED_NODE_PTY_VERSION = '1.1.0' -const ORIGINAL_SOURCE_SHA256 = '5e1005d6bdcfbe97b486ee415419fe7adae99035047f07340fbad36419e0bae6' -const PATCHED_SOURCE_SHA256 = '3e6bc1a688aae187d231687130cfc0a11781c672f5f616d73183d471ee8ee65c' - -const STATUS_PREFIX = 'ORCA-NPTY-CLOEXEC:' -const SKIP_MARKER_FILENAME = '.node-pty-cloexec-skip' -const BACKUP_DIRNAME = '.orca-cloexec-prepatch-release' -// Under the caller's 240s SSH command timeout, so the rollback below still runs. -const REBUILD_TIMEOUT_MS = 200000 -const VERIFY_TIMEOUT_MS = 15000 - -const FORWARD_DECLARATION = [ - 'static int\npty_nonblock(int);\n', - 'static int\npty_nonblock(int);\n\nstatic int\npty_cloexec(int);\n' -] - -const DEFINITION = [ - `static int -pty_nonblock(int fd) { - int flags = fcntl(fd, F_GETFL, 0); - if (flags == -1) return -1; - return fcntl(fd, F_SETFL, flags | O_NONBLOCK); -} -`, - `static int -pty_nonblock(int fd) { - int flags = fcntl(fd, F_GETFL, 0); - if (flags == -1) return -1; - return fcntl(fd, F_SETFL, flags | O_NONBLOCK); -} - -/** - * Orca: close-on-exec FD - * - * forkpty()/posix_openpt() have no atomic O_CLOEXEC, so a master left without - * FD_CLOEXEC is inherited by every later child of this process -- including - * later pty children -- which keeps its /dev/pts device and buffers alive long - * after its own session ends (#8362). - */ - -static int -pty_cloexec(int fd) { - int flags = fcntl(fd, F_GETFD); - if (flags == -1) return -1; - if (flags & FD_CLOEXEC) return 0; - return fcntl(fd, F_SETFD, flags | FD_CLOEXEC); -} -` -] - -const FORKPTY_CALL_SITE = [ - ` default: - if (pty_nonblock(master) == -1) { - throw Napi::Error::New(napiEnv, "Could not set master fd to nonblocking."); - } - } -`, - ` default: - if (pty_nonblock(master) == -1) { - throw Napi::Error::New(napiEnv, "Could not set master fd to nonblocking."); - } - if (pty_cloexec(master) == -1) { - throw Napi::Error::New(napiEnv, "Could not set master fd to close-on-exec."); - } - } -` -] - -// Apple never reaches FORKPTY_CALL_SITE: `default:` sits in the `#else` arm of PtyFork's -// `#if defined(__APPLE__)`, so before this pair the asset patched nothing macOS executes. -const POSIX_SPAWN_CALL_SITE = [ - ` if (pty_nonblock(master) == -1) { - throw Napi::Error::New(napiEnv, "Could not set master fd to nonblocking."); - } -#else -`, - ` if (pty_nonblock(master) == -1) { - throw Napi::Error::New(napiEnv, "Could not set master fd to nonblocking."); - } - if (pty_cloexec(master) == -1) { - throw Napi::Error::New(napiEnv, "Could not set master fd to close-on-exec."); - } -#else -` -] - -// The throwaway ptys pty_posix_spawn opens to keep the real master off fds 0-2. Byte-identical to -// the app's pnpm patch, so both trees compile the same cleanup. -const LOW_FDS_DECLARATION = [ - ` int low_fds[3]; - size_t count = 0; -`, - ` int low_fds[3] = {-1, -1, -1}; - size_t count = 0; -` -] - -const LOW_FDS_CLEANUP = [ - ` for (; count > 0; count--) { - close(low_fds[count]); - } -`, - ` for (size_t i = 0; i <= count && i < 3; i++) { - if (low_fds[i] != -1) { - close(low_fds[i]); - } - } -` -] - -const REPLACEMENTS = [ - FORWARD_DECLARATION, - DEFINITION, - POSIX_SPAWN_CALL_SITE, - FORKPTY_CALL_SITE, - LOW_FDS_DECLARATION, - LOW_FDS_CLEANUP -] - -function sourceSha256(source) { - return createHash('sha256').update(source).digest('hex') -} - -function nodePtyDir(relayDir) { - return resolve(relayDir, 'node_modules', 'node-pty') -} - -function inspectNodePtyUnixSource(relayDir) { - const ptyDir = nodePtyDir(relayDir) - const sourcePath = join(ptyDir, 'src', 'unix', 'pty.cc') - const version = JSON.parse(readFileSync(join(ptyDir, 'package.json'), 'utf8')).version - if (version !== EXPECTED_NODE_PTY_VERSION) { - throw new Error(`Refusing to patch node-pty ${version}; expected ${EXPECTED_NODE_PTY_VERSION}`) - } - return { ptyDir, sourcePath, source: readFileSync(sourcePath, 'utf8') } -} - -function writeSourceAtomically(sourcePath, contents) { - const temporaryPath = `${sourcePath}.orca-patch-${process.pid}` - // Why: a terminated install must leave one of the two known source versions on disk. - try { - writeFileSync(temporaryPath, contents) - renameSync(temporaryPath, sourcePath) - } finally { - rmSync(temporaryPath, { force: true }) - } -} - -function rewriteSource(source, reverse) { - let rewritten = source - for (const [original, patched] of REPLACEMENTS) { - const from = reverse ? patched : original - const to = reverse ? original : patched - if (rewritten.split(from).length - 1 !== 1) { - throw new Error('Refusing to rewrite unexpected node-pty pty.cc source') - } - rewritten = rewritten.replace(from, to) - } - return rewritten -} - -/** True when the patch was applied, false when it was already installed. */ -function patchNodePtyMasterCloexecSource(relayDir = process.cwd()) { - const inspected = inspectNodePtyUnixSource(relayDir) - const hash = sourceSha256(inspected.source) - if (hash === PATCHED_SOURCE_SHA256) { - return false - } - if (hash !== ORIGINAL_SOURCE_SHA256) { - throw new Error('Refusing to patch unexpected node-pty pty.cc source') - } - writeSourceAtomically(inspected.sourcePath, rewriteSource(inspected.source, false)) - assertPatchedNodePtyMasterCloexecSource(relayDir) - return true -} - -function assertPatchedNodePtyMasterCloexecSource(relayDir = process.cwd()) { - const inspected = inspectNodePtyUnixSource(relayDir) - if (sourceSha256(inspected.source) !== PATCHED_SOURCE_SHA256) { - throw new Error('node-pty pty master close-on-exec patch is not installed') - } -} - -function revertNodePtyMasterCloexecSource(relayDir = process.cwd()) { - const inspected = inspectNodePtyUnixSource(relayDir) - if (sourceSha256(inspected.source) === ORIGINAL_SOURCE_SHA256) { - return false - } - writeSourceAtomically(inspected.sourcePath, rewriteSource(inspected.source, true)) - return true -} - -function rebuildNodePty(relayDir) { - const result = spawnSync('npm', ['rebuild', '--ignore-scripts=false', 'node-pty'], { - cwd: relayDir, - encoding: 'utf8', - timeout: REBUILD_TIMEOUT_MS, - windowsHide: true - }) - if (result.error) { - throw new Error(`npm rebuild node-pty failed: ${result.error.message}`) - } - if (result.status !== 0) { - const tail = `${result.stdout || ''}${result.stderr || ''}`.trim().slice(-300) - throw new Error(`npm rebuild node-pty exited ${result.status ?? result.signal}: ${tail}`) - } -} - -// Why a child, for both scripts below: a bad build can abort the process on require, which would -// strand the moved-aside working build. Why each ends in a reachability check: a host that cannot -// show its fds says nothing, and an unobservable flag is not evidence the rebuild was wrong. -// -// Linux's leak is inheritance, so the observation is a later plain child's /proc/self/fd. -const VERIFY_INHERITANCE_SCRIPT = ` -const pty = require(process.argv[1]); -const term = pty.spawn('/bin/sh', ['-c', 'exit 0'], { - name: 'xterm-256color', cols: 80, rows: 24, cwd: process.cwd(), env: process.env -}); -const probe = require('node:child_process').spawnSync('/bin/sh', ['-c', 'ls -l /proc/self/fd'], { encoding: 'utf8' }); -try { term.kill() } catch {} -const listing = probe.stdout || ''; -if (probe.status !== 0 || !listing.includes('->')) { console.log('UNVERIFIED'); process.exit(0) } -console.log(listing.includes('ptmx') ? 'LEAKED' : 'ISOLATED'); -process.exit(0); -` - -// Apple's leak is self-held, not inherited, so the observation is this process's own fd table: -// N live ptys must account for exactly N /dev/ptmx rows. A stock build shows 2N -- the master plus -// the throwaway pty_posix_spawn opened and never closed. lsof, not /proc, because macOS has no -// /proc; a host without lsof cannot say, which is 'unverified', not a failed patch. -const VERIFY_SELF_FDS_SCRIPT = ` -const pty = require(process.argv[1]); -const terms = []; -for (let i = 0; i < 3; i++) { - terms.push(pty.spawn('/bin/sh', ['-c', 'sleep 30'], { - name: 'xterm-256color', cols: 80, rows: 24, cwd: process.cwd(), env: process.env - })); -} -const probe = require('node:child_process').spawnSync('/bin/sh', ['-c', 'lsof -p ' + process.pid], { encoding: 'utf8', maxBuffer: 1 << 24 }); -for (const term of terms) { try { term.kill() } catch {} } -const rows = (probe.stdout || '').split('\\n').filter((line) => line.includes('/dev/ptmx')); -if (probe.status !== 0 || rows.length < terms.length) { console.log('UNVERIFIED'); process.exit(0) } -console.log(rows.length > terms.length ? 'LEAKED' : 'ISOLATED'); -process.exit(0); -` - -const LEAK_MESSAGE = { - darwin: 'rebuilt node-pty still leaks a throwaway pty fd per spawn', - linux: 'rebuilt node-pty still leaks the pty master into later children' -} - -/** 'isolated' when the platform's leak is gone, 'unverified' when the host cannot show it. */ -function verifyNoPtyFdLeak(relayDir, platform) { - const script = platform === 'darwin' ? VERIFY_SELF_FDS_SCRIPT : VERIFY_INHERITANCE_SCRIPT - const result = spawnSync(process.execPath, ['-e', script, nodePtyDir(relayDir)], { - cwd: relayDir, - encoding: 'utf8', - timeout: VERIFY_TIMEOUT_MS, - windowsHide: true - }) - const output = `${result.stdout || ''}` - if (result.status !== 0 || result.error) { - const tail = `${output}${result.stderr || ''}`.trim().slice(-300) - throw new Error( - `rebuilt node-pty did not load: ${tail || result.error?.message || result.signal}` - ) - } - if (output.includes('LEAKED')) { - throw new Error(LEAK_MESSAGE[platform] || LEAK_MESSAGE.linux) - } - return output.includes('ISOLATED') ? 'isolated' : 'unverified' -} - -/** - * What gets moved aside before the compile, and where the compile writes. - * - * Linux ships no prebuild, so `build/Release` is both the working build and the compile's output, - * and moving it aside only arms the rollback. macOS runs `prebuilds/darwin-` and has no - * `build/` at all, so the compile writes a new `build/Release` -- which node-pty's loader checks - * ahead of `prebuilds`. Moving `prebuilds` aside does double duty there: it arms the rollback and - * it is what makes node-pty's install script fall through from "prebuild found" to `node-gyp - * rebuild`. Deliberately not `npm_config_build_from_source`, which deletes the prebuilds outright - * and would leave nothing to roll back to. - */ -function buildLayout(relayDir, platform, arch) { - const ptyDir = nodePtyDir(relayDir) - const compiledDir = join(ptyDir, 'build', 'Release') - if (platform === 'darwin') { - const prebuildsDir = join(ptyDir, 'prebuilds') - return { - compiledDir, - movedDir: prebuildsDir, - workingBuildPath: join(prebuildsDir, `darwin-${arch}`, 'pty.node'), - missingStatus: 'skipped:no-prebuild' - } - } - return { - compiledDir, - movedDir: compiledDir, - workingBuildPath: join(compiledDir, 'pty.node'), - missingStatus: 'skipped:no-compiled-build' - } -} - -function rollback(relayDir, layout, backupDir) { - rmSync(layout.compiledDir, { recursive: true, force: true }) - try { - revertNodePtyMasterCloexecSource(relayDir) - } catch { - // The build that is about to be restored predates the patch either way. - } - if (existsSync(backupDir)) { - mkdirSync(dirname(layout.movedDir), { recursive: true }) - renameSync(backupDir, layout.movedDir) - } -} - -/** - * Patch and rebuild the host's node-pty, or leave it exactly as found. - * Never throws: the caller is on the connect path and a leaky relay beats no relay. - */ -function applyNodePtyMasterCloexecPatch(relayDir = process.cwd(), options = {}) { - const platform = options.platform || process.platform - const arch = options.arch || process.arch - const rebuild = options.rebuild || rebuildNodePty - const verify = options.verify || verifyNoPtyFdLeak - if (platform !== 'linux' && platform !== 'darwin') { - return 'skipped:unsupported-platform' - } - const skipMarkerPath = join(relayDir, SKIP_MARKER_FILENAME) - if (existsSync(skipMarkerPath)) { - return 'skipped:earlier-attempt-failed' - } - const layout = buildLayout(relayDir, platform, arch) - const backupDir = join(nodePtyDir(relayDir), BACKUP_DIRNAME) - // A backup stranded by a connection that died mid-rebuild is stale by definition: - // whatever repaired node-pty since built from the source now on disk. - rmSync(backupDir, { recursive: true, force: true }) - - let inspected - try { - inspected = inspectNodePtyUnixSource(relayDir) - } catch (err) { - return `skipped:${err.message}` - } - const hash = sourceSha256(inspected.source) - if (hash === PATCHED_SOURCE_SHA256) { - return 'already-patched' - } - if (hash !== ORIGINAL_SOURCE_SHA256) { - return 'skipped:unexpected-source' - } - // Nothing to fall back on means the host runs neither a compile nor the prebuild - // this platform expects; rebuilding could only take away the artifact the probe - // just proved loadable. - if (!existsSync(layout.workingBuildPath)) { - return layout.missingStatus - } - - try { - renameSync(layout.movedDir, backupDir) - } catch (err) { - return `skipped:${err.message}` - } - try { - patchNodePtyMasterCloexecSource(relayDir) - rebuild(relayDir) - const verdict = verify(relayDir, platform) - // Discarded, not restored: a tree that gets published must hold no unpatched binary the - // loader could still fall back to. A later repair recompiles from the patched source. - rmSync(backupDir, { recursive: true, force: true }) - return verdict === 'isolated' ? 'patched' : 'patched-unverified' - } catch (err) { - rollback(relayDir, layout, backupDir) - // Bounded on purpose: one compile attempt per relay directory, never a retry loop. - try { - writeFileSync(skipMarkerPath, `${new Date().toISOString()} ${err.message}\n`) - } catch { - // A relay dir we cannot write to will fail the cheap checks above next time anyway. - } - return `failed:${err.message}` - } -} - -if (require.main === module) { - console.log(`${STATUS_PREFIX}${applyNodePtyMasterCloexecPatch()}`) -} - -module.exports = { - EXPECTED_NODE_PTY_VERSION, - ORIGINAL_SOURCE_SHA256, - PATCHED_SOURCE_SHA256, - SKIP_MARKER_FILENAME, - STATUS_PREFIX, - applyNodePtyMasterCloexecPatch, - assertPatchedNodePtyMasterCloexecSource, - patchNodePtyMasterCloexecSource, - revertNodePtyMasterCloexecSource -} diff --git a/config/relay-assets/node-pty-1.1.0-windows-pty-teardown-patch.cjs b/config/relay-assets/node-pty-1.1.0-windows-pty-teardown-patch.cjs deleted file mode 100644 index 367f9a10dcd5..000000000000 --- a/config/relay-assets/node-pty-1.1.0-windows-pty-teardown-patch.cjs +++ /dev/null @@ -1,203 +0,0 @@ -const { createHash } = require('node:crypto') -const { readFileSync, renameSync, rmSync, writeFileSync } = require('node:fs') -const { join, resolve } = require('node:path') - -/** - * Release the ConPTY teardown handles a relay's npm-installed node-pty never releases. - * - * Two files, and the ORDER of one of the edits is the whole fix. - * - * `windowsPtyAgent.js` -- `kill()` flips `readable` on both sockets and destroys neither. - * `_cleanUpProcess` destroys `_outSocket`, so the conout handle comes back; nothing ever destroys - * `_inSocket`, and it wraps a real Windows named-pipe handle from `fs.openSync(term.conin, 'w')`. - * Every terminal leaks one File handle for the life of the host process. - * - * The obvious fix -- and the placement `config/patches/node-pty@1.1.0.patch` uses -- releases it at - * the TOP of the branch, before `_getConsoleProcessList()` forks and before the native kill. That is - * measurably worse than leaving the leak alone: teardown aborts partway, the forked console-list - * agent is never reaped, and both pipe handles stay alive instead of one. This asset releases it at - * the END of the branch instead, after the fork and the kill have already happened. - * - * Measured on a Windows SSH host, 20 spawn/kill cycles, handles bucketed by NT object type - * (identical numbers standalone and through a real relay). Every row is the NON-DLL branch, which - * is the branch a relay runs -- see the divergence note below for why that matters: - * - * published node-pty File +1/terminal, Process flat - * desktop patch placement File +2/terminal, Process +1/terminal <-- 3x WORSE - * released last (here) File flat, Process flat - * - * `windowsTerminal.js` carries the desktop's error-listener hunks verbatim. The conin listener is - * what keeps a pipe error retiring one terminal instead of the host -- its own comment names the - * failure mode: "Without a listener, Node promotes errors such as write EAGAIN to uncaughtException". - * It is not what fixes the leak (adding it changed nothing on its own), but it is the guard that - * makes destroying conin safe at all. - * - * Why this ships as a relay asset rather than only in config/patches/node-pty@1.1.0.patch: pnpm - * patches do not cross the SSH boundary -- a relay host runs the tree `npm install` put there. - * - * DELIBERATE DIVERGENCE FROM THE DESKTOP, AND WHY IT IS NOT A DESKTOP-TERMINAL BUG: the two hosts - * do not run the same branch of `kill()`. node-pty defaults `_useConptyDll` to false - * (`windowsPtyAgent.js`). Every desktop site that opens a terminal pane sets it true -- - * `local-pty-utils.ts` (two) and `native-pty-spawn.ts` -- as does the `windows-conpty-warmup.ts` - * warm-up, so all of those take the `else` branch, where UPSTREAM ALREADY destroys the input - * socket. The relay passes no such option (`src/relay/pty-handler.ts`), so it takes the - * `!useConptyDll` branch -- the one this asset and the desktop patch both edit. - * - * THE DESKTOP IS NOT ENTIRELY OFF THAT BRANCH. Two desktop sites omit the option and so run it - * too: the hidden rate-limit probes in `src/main/rate-limits/claude-pty.ts` and - * `codex-pty-rate-limit-probe.ts`. Both recur -- their fetchers poll -- and both tear down through - * `kill()`, so this hunk is live on the desktop, just never for a pane a user can see. Do not - * restate this as "the desktop never executes that branch": that sentence stood here for two - * revisions and is false. - * - * What the numbers above therefore do NOT cover: they were measured on relay-style spawn/kill - * cycles. Whether the early placement costs the same +2 File / +1 Process across a probe's - * lifecycle is UNMEASURED -- plausible, not established, and worth measuring before anyone quotes - * a desktop figure. What IS settled is the claim this comment replaced: that the desktop patch made - * every Windows user worse off ON EVERY TERMINAL. Terminals take the DLL branch, and the harness - * that produced that claim defaulted into the branch it was not trying to measure. - * - * The divergence is therefore about which branch each host runs for the workload that matters, not - * about a regression in the terminals users open. The test still pins it, because a future "sync - * the patches" would put the early placement onto the relay's branch, where it does cost +2 File - * and +1 Process per terminal. - * - * If you extend this enumeration, grep for `node-pty` rather than for a static import: those two - * probes were missed three times because they use `await import('node-pty')`. - * - * THE SELF-EXIT LEAK: FIXED FOR THE DESKTOP BY #18635, STILL LIVE ON A RELAY. A terminal that exits - * on its own is also torn down through `kill()` -- both hosts call `destroy()` on natural exit and - * `WindowsTerminal.destroy()` is `kill()` -- but the shell is already gone by then, so the ordering - * this asset relies on does not hold. Measured over 20 self-exit cycles on the NON-DLL branch: - * published +3 File/+1 Process per terminal, desktop patch placement +2/+1, this tree +2/+1. This - * asset does not close it. - * - * #18635 does, in `config/patches/node-pty@1.1.0.patch`: the baton outlives the shell so `PtyKill` - * still reaches `ClosePseudoConsole`, plus an unconditional conout dispose on the DLL branch. That - * fix does not reach a Windows relay, and no hunk in THIS file can carry it, because it is mostly - * NATIVE (`src/win/conpty.cc`) and this asset only rewrites `lib/*.js`. Three delivery paths exist - * and none currently covers Windows: - * - * - the pnpm patch does not cross the SSH boundary -- the remote `npm install` yields upstream's - * unpatched node-pty; - * - the former orcad node-pty prebuild matrix had no win32 entry; - * - a relay asset CAN patch native source and rebuild on the host -- that is exactly what - * `node-pty-1.1.0-master-cloexec-patch.cjs` does -- but it returns - * `skipped:unsupported-platform` for anything but linux/darwin. Extending it to win32 means - * requiring an MSVC toolchain on the relay host, a far heavier precondition than on Linux, - * where node-gyp already runs at install time. - * - * So a Windows SSH relay still leaks a pseudoconsole per self-exiting terminal, and closing it is a - * DELIVERY problem, not another hunk here. Do not read #18635's flat self-exit relay numbers as - * covering deployed relays: they were measured against a locally rebuilt binary, so they describe - * the relay CODE PATH on a patched tree, not the tree a relay host actually installs. - */ - -const EXPECTED_NODE_PTY_VERSION = '1.1.0' - -/** Each entry is one published file, its patched form, and the edits between them. */ -const PATCH_TARGETS = [ - { - relativePath: ['lib', 'windowsPtyAgent.js'], - originalSha256: '8636d16b38266112204061a22b135734177c242837982fd3a4055be726efa64a', - patchedSha256: '1e23ef480569e73706e3ab4f5482c7e553c76f51414ae8e7b0bdcc2fd75f7280', - replacements: [ - [ - ' this._ptyNative.kill(this._pty, this._useConptyDll);\n this._conoutSocketWorker.dispose();\n', - ' this._ptyNative.kill(this._pty, this._useConptyDll);\n this._conoutSocketWorker.dispose();\n // Orca: released AFTER the console-list fork and the native kill, not before them.\n // Destroying conin first aborts teardown partway -- measured on a Windows SSH relay\n // as +2 File and +1 Process handles per terminal, against +1 File unpatched.\n this._inSocket.destroy();\n' - ] - ] - }, - { - relativePath: ['lib', 'windowsTerminal.js'], - originalSha256: 'c3a65716f53fed0135a8a633373d5f9c2ab092544d651f27ef0a67096dd3bcd9', - patchedSha256: '8247ecd69be8b18257050fb026b290024612c5ffc6d492ff1d46f81e613be2cf', - replacements: [ - [ - ' _this._agent = new windowsPtyAgent_1.WindowsPtyAgent(file, args, parsedEnv, cwd, _this._cols, _this._rows, false, opt.useConpty, opt.useConptyDll, opt.conptyInheritCursor);\n _this._socket = _this._agent.outSocket;\n // Not available until `ready` event emitted.\n _this._pid = _this._agent.innerPid;', - " _this._agent = new windowsPtyAgent_1.WindowsPtyAgent(file, args, parsedEnv, cwd, _this._cols, _this._rows, false, opt.useConpty, opt.useConptyDll, opt.conptyInheritCursor);\n _this._socket = _this._agent.outSocket;\n // Attach before readiness so a broken ConPTY output pipe cannot be unhandled.\n _this._socket.on('error', function (err) {\n var code = err && err.code;\n // PTY output can report EPIPE before `_close()` wins the race.\n _this._close();\n if (code === 'EPIPE' || code === 'ERR_STREAM_PUSH_AFTER_EOF' || code === 'ERR_STREAM_DESTROYED') {\n return;\n }\n // EIO, happens when someone closes our child process: the only process\n // in the terminal.\n // node < 0.6.14: errno 5\n // node >= 0.6.14: read EIO\n if (typeof code === 'string') {\n if (~code.indexOf('errno 5') || ~code.indexOf('EIO'))\n return;\n }\n // Throw anything else.\n if (_this.listeners('error').length < 2) {\n throw err;\n }\n });\n // Not available until `ready` event emitted.\n _this._pid = _this._agent.innerPid;" - ], - [ - " }\n });\n // Shutdown if `error` event is emitted.\n _this._socket.on('error', function (err) {\n // Close terminal session.\n _this._close();\n // EIO, happens when someone closes our child process: the only process\n // in the terminal.\n // node < 0.6.14: errno 5\n // node >= 0.6.14: read EIO\n if (err.code) {\n if (~err.code.indexOf('errno 5') || ~err.code.indexOf('EIO'))\n return;\n }\n // Throw anything else.\n if (_this.listeners('error').length < 2) {\n throw err;\n }\n });\n // Cleanup after the socket is closed.\n _this._socket.on('close', function () {", - " }\n });\n // Cleanup after the socket is closed.\n _this._socket.on('close', function () {" - ], - [ - ' _this._readable = true;\n _this._writable = true;\n _this._forwardEvents();\n return _this;', - " _this._readable = true;\n _this._writable = true;\n // A ConPTY input-pipe error must retire only this terminal. Without a listener, Node promotes\n // errors such as write EAGAIN to uncaughtException and kills every PTY in the daemon.\n _this._agent.inSocket.on('error', function () {\n if (!_this._writable) {\n return;\n }\n _this._close();\n try {\n _this._agent.kill();\n }\n catch (_a) {\n // The failing pipe may have raced process exit; the terminal is already unwritable.\n }\n });\n _this._forwardEvents();\n return _this;" - ], - [ - 'exports.WindowsTerminal = WindowsTerminal;\n//# sourceMappingURL=windowsTerminal.js.map', - 'exports.WindowsTerminal = WindowsTerminal;\n//# sourceMappingURL=windowsTerminal.js.map\n' - ] - ] - } -] - -function inspectTarget(relayDir, target) { - const nodePtyDir = resolve(relayDir, 'node_modules', 'node-pty') - const packageJson = JSON.parse(readFileSync(join(nodePtyDir, 'package.json'), 'utf8')) - if (packageJson.version !== EXPECTED_NODE_PTY_VERSION) { - throw new Error( - `Refusing to patch node-pty ${packageJson.version}; expected ${EXPECTED_NODE_PTY_VERSION}` - ) - } - const filePath = join(nodePtyDir, ...target.relativePath) - return { filePath, source: readFileSync(filePath, 'utf8') } -} - -function assertPatchedNodePtyWindowsTeardown(relayDir = process.cwd()) { - for (const target of PATCH_TARGETS) { - const inspected = inspectTarget(relayDir, target) - if (sourceSha256(inspected.source) !== target.patchedSha256) { - throw new Error( - `node-pty ConPTY teardown release is not installed in ${target.relativePath.join('/')}` - ) - } - } -} - -function patchNodePtyWindowsTeardown(relayDir = process.cwd()) { - for (const target of PATCH_TARGETS) { - const inspected = inspectTarget(relayDir, target) - const sourceHash = sourceSha256(inspected.source) - if (sourceHash === target.patchedSha256) { - continue - } - if (sourceHash !== target.originalSha256) { - throw new Error( - `Refusing to patch unexpected node-pty source in ${target.relativePath.join('/')}` - ) - } - let patchedSource = inspected.source - for (const [from, to] of target.replacements) { - // Why the count check: an anchor that matched twice would patch the wrong site silently, and - // the hash below would then reject a tree this script had already rewritten. - if (patchedSource.split(from).length - 1 !== 1) { - throw new Error(`Refusing to patch ${target.relativePath.join('/')}; anchor is not unique`) - } - patchedSource = patchedSource.replace(from, to) - } - const temporaryPath = `${inspected.filePath}.orca-patch-${process.pid}` - // Why: a terminated remote install must leave either known source version recoverable on reconnect. - try { - writeFileSync(temporaryPath, patchedSource) - renameSync(temporaryPath, inspected.filePath) - } finally { - rmSync(temporaryPath, { force: true }) - } - } - assertPatchedNodePtyWindowsTeardown(relayDir) -} - -function sourceSha256(source) { - return createHash('sha256').update(source).digest('hex') -} - -if (require.main === module) { - patchNodePtyWindowsTeardown() -} - -module.exports = { - assertPatchedNodePtyWindowsTeardown, - patchNodePtyWindowsTeardown -} diff --git a/config/reliability-gates.jsonc b/config/reliability-gates.jsonc index 19228a3ae482..7c6f0e3aa7b0 100644 --- a/config/reliability-gates.jsonc +++ b/config/reliability-gates.jsonc @@ -2489,7 +2489,7 @@ "providers": ["local", "local-daemon", "ssh-daemon", "ssh-relay", "paired-runtime"], "coveredPlatforms": ["macos", "linux"], "coveredProviders": ["local", "local-daemon", "ssh-relay", "paired-runtime"], - "coverageNotes": "Real macOS node-pty integration covers zsh 5.9 .zshenv/.zprofile and bash 3.2 profile replacement shells, including exec -a names, same-shell and child-process silent reads, a blocking zle-line-init hook, non-shell readline replacements, and a non-shell image renamed zsh. A real Fish PTY proves marker stripping is byte-exact and does not render bracketed-paste control text. Ubuntu 20.04/Bash 5.0 and Ubuntu 24.04/Bash 5.2 containers cover scalar and array PROMPT_COMMAND composition, including real zoxide on 24.04. Deterministic owner contracts cover local, daemon, and SSH relay identity/readiness composition plus provider-owned delivery; explicit terminal-paste remains renderer-owned. Live isolated journeys cover a macOS headed desktop owner with paired web client, headless orca serve with paired web client, and a Docker Linux OpenSSH/Bash target with one readiness-time reconnect. Linux desktop, WSL/Windows, live zsh-over-SSH, and physical remote hosts remain gaps.", + "coverageNotes": "Real macOS node-pty integration covers zsh 5.9 .zshenv/.zprofile and bash 3.2 profile replacement shells, including exec -a names, same-shell and child-process silent reads, a blocking zle-line-init hook, non-shell readline replacements, and a non-shell image renamed zsh. A real Fish PTY proves marker stripping is byte-exact and does not render bracketed-paste control text. Ubuntu 20.04/Bash 5.0 and Ubuntu 24.04/Bash 5.2 containers cover scalar and array PROMPT_COMMAND composition, including real zoxide on 24.04. Deterministic owner contracts cover local, daemon, and SSH relay identity/readiness composition plus provider-owned delivery; explicit terminal-paste remains renderer-owned. Live isolated journeys cover a macOS headed desktop owner with paired web client, headless orca serve with paired web client, and a Docker Linux OpenSSH/Bash target with one readiness-time reconnect. Linux desktop, WSL/Windows, live zsh-over-SSH, and physical remote hosts remain gaps. Current executable owners are daemon and SSH relay; the in-process provider tests were retired. Historical node-pty and live-app evidence is retained as historical evidence, not relabeled as Bun qualification.", "motivatingLinks": [ "https://github.com/stablyai/orca/issues/13767", "https://linear.app/stably/issue/STA-4067", @@ -2500,17 +2500,22 @@ "https://github.com/stablyai/orca/pull/14451" ], "invariant": "Shell startup scanning preserves every non-Orca output byte in order across arbitrary PTY chunk boundaries. Bash scalar and supported array PROMPT_COMMAND values preserve user bytes, hook order, and foreground exit status while emitting exactly one OSC 133 A/C/D lifecycle without syntax errors. Before user startup files run, the wrapper publishes its shell PID. If a later exec discards the ready marker, proof-based recovery releases queued startup input only after the launched shell's actual executable image with that exact PID owns the PTY foreground, emits its post-hook line-editor enable sequence, and exposes zle/readline termios; startup reads, line-init hooks, child foreground processes, and non-shell readline replacements cannot trigger that proof. The existing bounded compatibility timeout may enqueue input without proof, but must not publish readiness, lose or duplicate that input across reconnect, or execute it before the replacement shell can accept it.", - "oracle": "Split Fish output immediately after the ready marker and after the following ESC introducer; local and daemon owners must emit the exact ESC [ ? 2004 h bytes and headless xterm must not render literal control text. Run the real generated wrappers under node-pty with disposable zsh, bash, and Fish startup files. Exercise Bash scalar and array PROMPT_COMMAND values with empty, repeated, escaped, whitespace, control-byte, odd-backslash, status-mutating, DEBUG-trap, zoxide, and RHEL-style hooks; require exact A,C,D:0,A,C,D:1,A,C ordering and byte preservation. Plain and exec -a replacement shells must receive one queued command at their first completed line editor without waiting for the 15-second timeout. The identical oracle must time out without the startup identity and prompt fallback. Same-shell reads, child-process reads, a blocking zle-line-init hook, sqlite3, and sqlite3 renamed zsh must not trigger prompt fallback. Relay owner coverage must use the same lossless scanner composition and release through the same prompt contract.", + "oracle": "Split Fish output immediately after the ready marker and after the following ESC introducer; daemon owners must emit the exact ESC [ ? 2004 h bytes and headless xterm must not render literal control text. Run the real generated wrappers under the pinned bundled Bun runtime with disposable zsh, bash, and Fish startup files. Exercise Bash scalar and array PROMPT_COMMAND values with empty, repeated, escaped, whitespace, control-byte, odd-backslash, status-mutating, DEBUG-trap, zoxide, and RHEL-style hooks; require exact A,C,D:0,A,C,D:1,A,C ordering and byte preservation. Plain and exec -a replacement shells must receive one queued command at their first completed line editor without waiting for the 15-second timeout. The identical oracle must time out without the startup identity and prompt fallback. Same-shell reads, child-process reads, a blocking zle-line-init hook, sqlite3, and sqlite3 renamed zsh must not trigger prompt fallback. Relay owner coverage must use the same lossless scanner composition and release through the same prompt contract.", "commands": [ - "node config/scripts/run-bun-profile-tests.mjs src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec.test.ts src/main/daemon/bash-prompt-command-composition.test.ts src/main/line-editor-ready-output-scanner.test.ts src/main/shell-startup-identity-scanner.test.ts src/main/shell-startup-output-scanner.test.ts src/main/shell-prompt-readiness-probe.test.ts src/shared/pty-slave-line-discipline-echo.test.ts src/shared/shell-process-readiness.test.ts src/main/daemon/session.test.ts src/main/daemon/shell-ready.test.ts src/main/providers/local-pty-shell-ready-marker-scan.test.ts src/main/providers/local-pty-shell-ready-startup-command.test.ts src/main/providers/local-pty-shell-ready-wrapper-generation.test.ts src/main/providers/local-pty-provider-shell-readiness.test.ts src/relay/pty-shell-launch.test.ts src/relay/pty-handler-startup-command-delivery.test.ts", + "node config/scripts/run-bun-profile-tests.mjs src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec.test.ts src/main/daemon/bash-prompt-command-composition.test.ts src/main/line-editor-ready-output-scanner.test.ts src/main/shell-startup-identity-scanner.test.ts src/main/shell-startup-output-scanner.test.ts src/main/shell-prompt-readiness-probe.test.ts src/shared/pty-slave-line-discipline-echo.test.ts src/shared/shell-process-readiness.test.ts src/main/daemon/session.test.ts src/main/daemon/shell-ready.test.ts src/main/providers/local-pty-shell-ready-marker-scan.test.ts src/main/daemon/daemon-pty-startup-delivery.test.ts src/main/providers/local-pty-shell-ready-wrapper-generation.test.ts src/main/daemon/shell-ready.test.ts src/relay/pty-shell-launch.test.ts src/relay/pty-handler-startup-command-delivery.test.ts", // Historical runner retained for the dated evidence records below. - "pnpm exec vitest run --config config/vitest.config.ts src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec.test.ts src/main/daemon/bash-prompt-command-composition.test.ts src/main/line-editor-ready-output-scanner.test.ts src/main/shell-startup-identity-scanner.test.ts src/main/shell-startup-output-scanner.test.ts src/main/shell-prompt-readiness-probe.test.ts src/shared/pty-slave-line-discipline-echo.test.ts src/shared/shell-process-readiness.test.ts src/main/daemon/session.test.ts src/main/daemon/shell-ready.test.ts src/main/providers/local-pty-shell-ready-marker-scan.test.ts src/main/providers/local-pty-shell-ready-startup-command.test.ts src/main/providers/local-pty-shell-ready-wrapper-generation.test.ts src/main/providers/local-pty-provider-shell-readiness.test.ts src/relay/pty-shell-launch.test.ts src/relay/pty-handler-startup-command-delivery.test.ts", - "node config/scripts/run-bun-profile-tests.mjs src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec.test.ts src/main/line-editor-ready-output-scanner.test.ts src/main/shell-startup-identity-scanner.test.ts src/main/shell-startup-output-scanner.test.ts src/main/shell-prompt-readiness-probe.test.ts src/shared/pty-slave-line-discipline-echo.test.ts src/shared/shell-process-readiness.test.ts src/main/daemon/session.test.ts src/main/daemon/shell-ready.test.ts src/main/providers/local-pty-shell-ready-marker-scan.test.ts src/main/providers/local-pty-shell-ready-startup-command.test.ts src/main/providers/local-pty-shell-ready-wrapper-generation.test.ts src/main/providers/local-pty-provider-shell-readiness.test.ts src/relay/pty-shell-launch.test.ts src/relay/pty-handler-startup-command-delivery.test.ts", + "pnpm exec vitest run --config config/vitest.config.ts src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec.test.ts src/main/daemon/bash-prompt-command-composition.test.ts src/main/line-editor-ready-output-scanner.test.ts src/main/shell-startup-identity-scanner.test.ts src/main/shell-startup-output-scanner.test.ts src/main/shell-prompt-readiness-probe.test.ts src/shared/pty-slave-line-discipline-echo.test.ts src/shared/shell-process-readiness.test.ts src/main/daemon/session.test.ts src/main/daemon/shell-ready.test.ts src/main/providers/local-pty-shell-ready-marker-scan.test.ts src/main/daemon/daemon-pty-startup-delivery.test.ts src/main/providers/local-pty-shell-ready-wrapper-generation.test.ts src/main/daemon/shell-ready.test.ts src/relay/pty-shell-launch.test.ts src/relay/pty-handler-startup-command-delivery.test.ts", + "node config/scripts/run-bun-profile-tests.mjs src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec.test.ts src/main/line-editor-ready-output-scanner.test.ts src/main/shell-startup-identity-scanner.test.ts src/main/shell-startup-output-scanner.test.ts src/main/shell-prompt-readiness-probe.test.ts src/shared/pty-slave-line-discipline-echo.test.ts src/shared/shell-process-readiness.test.ts src/main/daemon/session.test.ts src/main/daemon/shell-ready.test.ts src/main/providers/local-pty-shell-ready-marker-scan.test.ts src/main/daemon/daemon-pty-startup-delivery.test.ts src/main/providers/local-pty-shell-ready-wrapper-generation.test.ts src/main/daemon/shell-ready.test.ts src/relay/pty-shell-launch.test.ts src/relay/pty-handler-startup-command-delivery.test.ts", // Historical runner retained for the dated evidence records below. - "pnpm exec vitest run --config config/vitest.config.ts src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec.test.ts src/main/line-editor-ready-output-scanner.test.ts src/main/shell-startup-identity-scanner.test.ts src/main/shell-startup-output-scanner.test.ts src/main/shell-prompt-readiness-probe.test.ts src/shared/pty-slave-line-discipline-echo.test.ts src/shared/shell-process-readiness.test.ts src/main/daemon/session.test.ts src/main/daemon/shell-ready.test.ts src/main/providers/local-pty-shell-ready-marker-scan.test.ts src/main/providers/local-pty-shell-ready-startup-command.test.ts src/main/providers/local-pty-shell-ready-wrapper-generation.test.ts src/main/providers/local-pty-provider-shell-readiness.test.ts src/relay/pty-shell-launch.test.ts src/relay/pty-handler-startup-command-delivery.test.ts", + "pnpm exec vitest run --config config/vitest.config.ts src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec.test.ts src/main/line-editor-ready-output-scanner.test.ts src/main/shell-startup-identity-scanner.test.ts src/main/shell-startup-output-scanner.test.ts src/main/shell-prompt-readiness-probe.test.ts src/shared/pty-slave-line-discipline-echo.test.ts src/shared/shell-process-readiness.test.ts src/main/daemon/session.test.ts src/main/daemon/shell-ready.test.ts src/main/providers/local-pty-shell-ready-marker-scan.test.ts src/main/daemon/daemon-pty-startup-delivery.test.ts src/main/providers/local-pty-shell-ready-wrapper-generation.test.ts src/main/daemon/shell-ready.test.ts src/relay/pty-shell-launch.test.ts src/relay/pty-handler-startup-command-delivery.test.ts", "pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 src/main/daemon/shell-ready.test.ts src/main/shell-startup-output-scanner.test.ts", "ORCA_E2E_SSH_DOCKER=1 SKIP_BUILD=1 pnpm exec playwright test tests/e2e/ssh-startup-exec-readiness.spec.ts --config tests/playwright.config.ts --project electron-headful --workers=1", - "ORCA_E2E_WEB_CLIENT=1 SKIP_BUILD=1 pnpm exec playwright test tests/e2e/paired-startup-exec-readiness.spec.ts --config tests/playwright.config.ts --project electron-headless --project electron-headful --workers=1" + "ORCA_E2E_WEB_CLIENT=1 SKIP_BUILD=1 pnpm exec playwright test tests/e2e/paired-startup-exec-readiness.spec.ts --config tests/playwright.config.ts --project electron-headless --project electron-headful --workers=1", + "node config/scripts/run-bun-profile-tests.mjs src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec.test.ts src/main/daemon/bash-prompt-command-composition.test.ts src/main/line-editor-ready-output-scanner.test.ts src/main/shell-startup-identity-scanner.test.ts src/main/shell-startup-output-scanner.test.ts src/main/shell-prompt-readiness-probe.test.ts src/shared/pty-slave-line-discipline-echo.test.ts src/shared/shell-process-readiness.test.ts src/main/daemon/session.test.ts src/main/daemon/shell-ready.test.ts src/main/providers/local-pty-shell-ready-marker-scan.test.ts src/main/daemon/daemon-pty-startup-delivery.test.ts src/main/providers/local-pty-shell-ready-wrapper-generation.test.ts src/relay/pty-shell-launch.test.ts src/relay/pty-handler-startup-command-delivery.test.ts tests/e2e/ssh-startup-exec-readiness.spec.ts tests/e2e/paired-startup-exec-readiness.spec.ts", + // Historical runner retained for the dated evidence records below. + "pnpm exec vitest run --config config/vitest.config.ts src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec.test.ts src/main/daemon/bash-prompt-command-composition.test.ts src/main/line-editor-ready-output-scanner.test.ts src/main/shell-startup-identity-scanner.test.ts src/main/shell-startup-output-scanner.test.ts src/main/shell-prompt-readiness-probe.test.ts src/shared/pty-slave-line-discipline-echo.test.ts src/shared/shell-process-readiness.test.ts src/main/daemon/session.test.ts src/main/daemon/shell-ready.test.ts src/main/providers/local-pty-shell-ready-marker-scan.test.ts src/main/daemon/daemon-pty-startup-delivery.test.ts src/main/providers/local-pty-shell-ready-wrapper-generation.test.ts src/relay/pty-shell-launch.test.ts src/relay/pty-handler-startup-command-delivery.test.ts tests/e2e/ssh-startup-exec-readiness.spec.ts tests/e2e/paired-startup-exec-readiness.spec.ts", + "pnpm exec vitest run --config config/vitest.config.ts src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec.test.ts src/main/line-editor-ready-output-scanner.test.ts src/main/shell-startup-identity-scanner.test.ts src/main/shell-startup-output-scanner.test.ts src/main/shell-prompt-readiness-probe.test.ts src/shared/pty-slave-line-discipline-echo.test.ts src/shared/shell-process-readiness.test.ts src/main/daemon/session.test.ts src/main/daemon/shell-ready.test.ts src/main/providers/local-pty-shell-ready-marker-scan.test.ts src/main/daemon/daemon-pty-startup-delivery.test.ts src/main/providers/local-pty-shell-ready-wrapper-generation.test.ts src/main/daemon/shell-ready.test.ts src/relay/pty-shell-launch.test.ts src/relay/pty-handler-startup-command-delivery.test.ts", + "pnpm exec vitest run --config config/vitest.config.ts src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec.test.ts src/main/daemon/bash-prompt-command-composition.test.ts src/main/line-editor-ready-output-scanner.test.ts src/main/shell-startup-identity-scanner.test.ts src/main/shell-startup-output-scanner.test.ts src/main/shell-prompt-readiness-probe.test.ts src/shared/pty-slave-line-discipline-echo.test.ts src/shared/shell-process-readiness.test.ts src/main/daemon/session.test.ts src/main/daemon/shell-ready.test.ts src/main/providers/local-pty-shell-ready-marker-scan.test.ts src/main/daemon/daemon-pty-startup-delivery.test.ts src/main/providers/local-pty-shell-ready-wrapper-generation.test.ts src/main/daemon/shell-ready.test.ts src/relay/pty-shell-launch.test.ts src/relay/pty-handler-startup-command-delivery.test.ts" ], "testFiles": [ "src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec.test.ts", @@ -2524,9 +2529,8 @@ "src/main/daemon/session.test.ts", "src/main/daemon/shell-ready.test.ts", "src/main/providers/local-pty-shell-ready-marker-scan.test.ts", - "src/main/providers/local-pty-shell-ready-startup-command.test.ts", + "src/main/daemon/daemon-pty-startup-delivery.test.ts", "src/main/providers/local-pty-shell-ready-wrapper-generation.test.ts", - "src/main/providers/local-pty-provider-shell-readiness.test.ts", "src/relay/pty-shell-launch.test.ts", "src/relay/pty-handler-startup-command-delivery.test.ts", "tests/e2e/ssh-startup-exec-readiness.spec.ts", @@ -2559,8 +2563,10 @@ ] }, { - "file": "src/main/providers/local-pty-provider-shell-readiness.test.ts", - "assertions": ["local PTY output is byte-exact for both Fish chunk splits"] + "file": "src/main/daemon/session.test.ts", + "assertions": [ + "preserves Fish bracketed-paste output split after the ready marker and after the ESC introducer" + ] }, { "file": "src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec.test.ts", @@ -2611,7 +2617,7 @@ "date": "2026-08-12", "runner": "local", "platform": "macos", - "command": "pnpm exec vitest run --config config/vitest.config.ts src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec.test.ts src/main/line-editor-ready-output-scanner.test.ts src/main/shell-startup-identity-scanner.test.ts src/main/shell-startup-output-scanner.test.ts src/main/shell-prompt-readiness-probe.test.ts src/shared/pty-slave-line-discipline-echo.test.ts src/shared/shell-process-readiness.test.ts src/main/daemon/session.test.ts src/main/daemon/shell-ready.test.ts src/main/providers/local-pty-shell-ready-marker-scan.test.ts src/main/providers/local-pty-shell-ready-startup-command.test.ts src/main/providers/local-pty-shell-ready-wrapper-generation.test.ts src/main/providers/local-pty-provider-shell-readiness.test.ts src/relay/pty-shell-launch.test.ts src/relay/pty-handler-startup-command-delivery.test.ts", + "command": "pnpm exec vitest run --config config/vitest.config.ts src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec.test.ts src/main/line-editor-ready-output-scanner.test.ts src/main/shell-startup-identity-scanner.test.ts src/main/shell-startup-output-scanner.test.ts src/main/shell-prompt-readiness-probe.test.ts src/shared/pty-slave-line-discipline-echo.test.ts src/shared/shell-process-readiness.test.ts src/main/daemon/session.test.ts src/main/daemon/shell-ready.test.ts src/main/providers/local-pty-shell-ready-marker-scan.test.ts src/main/daemon/daemon-pty-startup-delivery.test.ts src/main/providers/local-pty-shell-ready-wrapper-generation.test.ts src/main/daemon/shell-ready.test.ts src/relay/pty-shell-launch.test.ts src/relay/pty-handler-startup-command-delivery.test.ts", "result": "passed", "durationSeconds": 6, "summary": "473 tests passed with one local Fish skip, including every-boundary scanner composition, real exec, silent-read, line-init-hook, renamed-image, disabled-bracketed-paste, and relay-owner oracles." @@ -2620,7 +2626,7 @@ "date": "2026-08-15", "runner": "local", "platform": "macos", - "command": "pnpm exec vitest run --config config/vitest.config.ts src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec.test.ts src/main/daemon/bash-prompt-command-composition.test.ts src/main/line-editor-ready-output-scanner.test.ts src/main/shell-startup-identity-scanner.test.ts src/main/shell-startup-output-scanner.test.ts src/main/shell-prompt-readiness-probe.test.ts src/shared/pty-slave-line-discipline-echo.test.ts src/shared/shell-process-readiness.test.ts src/main/daemon/session.test.ts src/main/daemon/shell-ready.test.ts src/main/providers/local-pty-shell-ready-marker-scan.test.ts src/main/providers/local-pty-shell-ready-startup-command.test.ts src/main/providers/local-pty-shell-ready-wrapper-generation.test.ts src/main/providers/local-pty-provider-shell-readiness.test.ts src/relay/pty-shell-launch.test.ts src/relay/pty-handler-startup-command-delivery.test.ts", + "command": "pnpm exec vitest run --config config/vitest.config.ts src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec.test.ts src/main/daemon/bash-prompt-command-composition.test.ts src/main/line-editor-ready-output-scanner.test.ts src/main/shell-startup-identity-scanner.test.ts src/main/shell-startup-output-scanner.test.ts src/main/shell-prompt-readiness-probe.test.ts src/shared/pty-slave-line-discipline-echo.test.ts src/shared/shell-process-readiness.test.ts src/main/daemon/session.test.ts src/main/daemon/shell-ready.test.ts src/main/providers/local-pty-shell-ready-marker-scan.test.ts src/main/daemon/daemon-pty-startup-delivery.test.ts src/main/providers/local-pty-shell-ready-wrapper-generation.test.ts src/main/daemon/shell-ready.test.ts src/relay/pty-shell-launch.test.ts src/relay/pty-handler-startup-command-delivery.test.ts", "result": "passed", "durationSeconds": 6, "summary": "233 tests passed with one local Fish skip, including exact Bash PROMPT_COMMAND byte/status/lifecycle composition across daemon, local, and relay owners." @@ -8238,12 +8244,13 @@ "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orca-runtime.test.ts", "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal/initial-terminal.test.ts src/renderer/src/lib/worktree-activation-default-tabs.test.ts src/renderer/src/store/slices/terminals-explicit-empty-hydration.test.ts", "pnpm dlx node@24 ./node_modules/vitest/vitest.mjs run --config config/vitest.config.ts src/main/agent-hooks/server-pane-authority.test.ts src/main/ipc/agent-hooks.test.ts src/main/ipc/agent-pane-authority-ownership.test.ts src/main/ipc/pty-management.test.ts src/main/persistence-initial-load.test.ts src/main/persistence-pane-identity-migration.test.ts src/main/persistence-pty-binding-reconciliation.test.ts src/renderer/src/store/slices/agent-pane-authority.test.ts src/renderer/src/store/slices/terminal-pane-detach-agent-identity.test.ts src/renderer/src/store/slices/terminal-tab-retirement.test.ts src/renderer/src/store/slices/terminal-tab-retirement-store.test.ts tests/e2e/completed-worker-retirement-resume.unit.test.ts src/renderer/src/components/shared/kill-all-terminal-surfaces.test.ts", - "pnpm exec vitest run --config config/vitest.config.ts src/main/pty-descendant-termination.test.ts src/main/daemon/session.test.ts src/main/daemon/terminal-host.test.ts src/main/providers/local-pty-provider-shutdown.test.ts src/main/runtime/worktree-teardown.test.ts", + "pnpm exec vitest run --config config/vitest.config.ts src/main/pty-descendant-termination.test.ts src/main/daemon/session.test.ts src/main/daemon/terminal-host-concurrent-create.test.ts src/main/daemon/terminal-session-teardown.test.ts src/main/runtime/worktree-teardown.test.ts", "pnpm run test:e2e -- tests/e2e/terminal-tab-close-restart-persistence.spec.ts --workers=1", "pnpm run test:e2e -- tests/e2e/terminal-parked-close-retirement.spec.ts --workers=1", "pnpm run test:e2e -- tests/e2e/agent-descendant-process-kill.spec.ts --workers=1", "pnpm run test:e2e -- tests/e2e/paired-cli-terminal-graph-sync-tab-retention.spec.ts --workers=1", - "pnpm run test:e2e -- tests/e2e/headless-serve-cli-terminal-retention-parity.spec.ts --workers=1" + "pnpm run test:e2e -- tests/e2e/headless-serve-cli-terminal-retention-parity.spec.ts --workers=1", + "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orca-runtime-terminal-close-continuity.test.ts src/main/runtime/orca-runtime.test.ts src/renderer/src/components/terminal/initial-terminal.test.ts src/renderer/src/lib/worktree-activation-default-tabs.test.ts src/renderer/src/store/slices/terminals-explicit-empty-hydration.test.ts src/main/agent-hooks/server-pane-authority.test.ts src/main/ipc/agent-hooks.test.ts src/main/ipc/agent-pane-authority-ownership.test.ts src/main/ipc/pty-management.test.ts src/main/persistence-initial-load.test.ts src/main/persistence-pane-identity-migration.test.ts src/main/persistence-pty-binding-reconciliation.test.ts src/main/pty-descendant-termination.test.ts src/main/daemon/session.test.ts src/main/daemon/terminal-host-concurrent-create.test.ts src/main/runtime/worktree-teardown.test.ts src/renderer/src/store/slices/agent-pane-authority.test.ts src/renderer/src/store/slices/terminal-pane-detach-agent-identity.test.ts src/renderer/src/store/slices/terminal-tab-retirement.test.ts src/renderer/src/store/slices/terminal-tab-retirement-store.test.ts tests/e2e/completed-worker-retirement-resume.unit.test.ts src/renderer/src/components/shared/kill-all-terminal-surfaces.test.ts tests/e2e/terminal-tab-close-restart-persistence.spec.ts tests/e2e/terminal-parked-close-retirement.spec.ts tests/e2e/agent-descendant-process-kill.spec.ts tests/e2e/paired-cli-terminal-graph-sync-tab-retention.spec.ts tests/e2e/headless-serve-cli-terminal-retention-parity.spec.ts" ], "testFiles": [ "src/main/runtime/orca-runtime-terminal-close-continuity.test.ts", @@ -8260,8 +8267,7 @@ "src/main/persistence-pty-binding-reconciliation.test.ts", "src/main/pty-descendant-termination.test.ts", "src/main/daemon/session.test.ts", - "src/main/daemon/terminal-host.test.ts", - "src/main/providers/local-pty-provider-shutdown.test.ts", + "src/main/daemon/terminal-host-concurrent-create.test.ts", "src/main/runtime/worktree-teardown.test.ts", "src/renderer/src/store/slices/agent-pane-authority.test.ts", "src/renderer/src/store/slices/terminal-pane-detach-agent-identity.test.ts", @@ -8370,7 +8376,7 @@ ] }, { - "file": "src/main/daemon/terminal-host.test.ts", + "file": "src/main/daemon/terminal-host-concurrent-create.test.ts", "assertions": [ "agent immediate kill rejects reattach while descendant capture is pending", "a naturally exited session id remains reserved until capture finishes without force-killing its retired PID", @@ -8755,7 +8761,9 @@ "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/pty-spawn-cwd-fallback.test.ts src/main/ipc/pty-wsl-cwd-validation.test.ts", "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/pty-transport-connect-spawn.test.ts", "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/pty-connection-runtime-owner-spawn-routing.test.ts", - "pnpm exec vitest run --config config/vitest.config.ts src/main/pty/omp-shell-wrapper.node-pty.test.ts src/main/pty/omp-shell-wrapper-alias-safety.test.ts src/main/shell-wrapper-generated-file-snapshot.test.ts" + "pnpm exec vitest run --config config/vitest.config.ts src/main/pty/omp-shell-wrapper.bun.test.ts src/main/pty/omp-shell-wrapper-alias-safety.test.ts src/main/shell-wrapper-generated-file-snapshot.test.ts", + "pnpm exec vitest run --config config/vitest.config.ts src/shared/terminal-startup-cwd.test.ts src/main/ipc/pty-spawn-cwd-fallback.test.ts src/main/ipc/pty-wsl-cwd-validation.test.ts src/renderer/src/components/terminal-pane/pty-transport-connect-spawn.test.ts src/renderer/src/components/terminal-pane/pty-connection-runtime-owner-spawn-routing.test.ts src/main/pty/omp-shell-wrapper.bun.test.ts src/main/pty/omp-shell-wrapper-alias-safety.test.ts src/main/shell-wrapper-generated-file-snapshot.test.ts", + "pnpm exec vitest run --config config/vitest.config.ts src/main/pty/omp-shell-wrapper.bun.test.ts src/main/pty/omp-shell-wrapper-alias-safety.test.ts src/main/shell-wrapper-generated-file-snapshot.test.ts" ], "testFiles": [ "src/shared/terminal-startup-cwd.test.ts", @@ -8763,7 +8771,7 @@ "src/main/ipc/pty-wsl-cwd-validation.test.ts", "src/renderer/src/components/terminal-pane/pty-transport-connect-spawn.test.ts", "src/renderer/src/components/terminal-pane/pty-connection-runtime-owner-spawn-routing.test.ts", - "src/main/pty/omp-shell-wrapper.node-pty.test.ts", + "src/main/pty/omp-shell-wrapper.bun.test.ts", "src/main/pty/omp-shell-wrapper-alias-safety.test.ts", "src/main/shell-wrapper-generated-file-snapshot.test.ts" ], @@ -8809,7 +8817,7 @@ ] }, { - "file": "src/main/pty/omp-shell-wrapper.node-pty.test.ts", + "file": "src/main/pty/omp-shell-wrapper.bun.test.ts", "assertions": [ "real Bash and Zsh PTYs rebind OMP to a recreated cwd inode without changing the parent shell", "an unset PWD with a usable cwd stays unset and does not remap OMP to ORCA_WORKTREE_PATH", @@ -8869,7 +8877,7 @@ "date": "2026-08-29", "runner": "local", "platform": "macos", - "command": "pnpm exec vitest run --config config/vitest.config.ts src/main/pty/omp-shell-wrapper.node-pty.test.ts src/main/pty/omp-shell-wrapper-alias-safety.test.ts src/main/shell-wrapper-generated-file-snapshot.test.ts", + "command": "pnpm exec vitest run --config config/vitest.config.ts src/main/pty/omp-shell-wrapper.bun.test.ts src/main/pty/omp-shell-wrapper-alias-safety.test.ts src/main/shell-wrapper-generated-file-snapshot.test.ts", "result": "passed", "durationSeconds": 3, "summary": "3 test files passed; real Bash and Zsh PTYs covered usable physical cwd authority, stale-inode recovery with set and unset PWD, extension-enabled and extension-skipping argv, parent-shell isolation, exit status, missing-path visibility, alias safety, and generated local/daemon/relay parity." @@ -9350,14 +9358,15 @@ "commands": [ "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/lib/resume-sleeping-agent-session.test.ts", "pnpm exec vitest run --config config/vitest.config.ts tests/e2e/completed-worker-retirement-resume.unit.test.ts --reporter=verbose", - "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/lib/resume-sleeping-agent-session.test.ts src/main/providers/local-pty-provider-spawn-session.test.ts src/main/daemon/terminal-host.test.ts src/main/daemon/daemon-pty-adapter.test.ts src/main/ipc/pty-pane-materialization-race.test.ts src/main/ipc/pty-persisted-incarnation-repair.test.ts src/main/ipc/pty-pane-reservation-settlement.test.ts src/main/runtime/orca-runtime.test.ts src/renderer/src/lib/pane-manager/pane-fit.test.ts src/renderer/src/components/terminal-pane/pty-connection-deferred-reattach-live-output.test.ts src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-host-session-launch.test.ts", - "pnpm exec electron-vite build --mode e2e && SKIP_BUILD=1 pnpm exec playwright test tests/e2e/live-background-terminal-mount-authority.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1" + "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/lib/resume-sleeping-agent-session.test.ts src/main/ipc/pty-daemon-spawn-session-identity.test.ts src/main/daemon/terminal-host-concurrent-create.test.ts src/main/daemon/daemon-pty-adapter.test.ts src/main/ipc/pty-pane-materialization-race.test.ts src/main/ipc/pty-persisted-incarnation-repair.test.ts src/main/ipc/pty-pane-reservation-settlement.test.ts src/main/runtime/orca-runtime.test.ts src/renderer/src/lib/pane-manager/pane-fit.test.ts src/renderer/src/components/terminal-pane/pty-connection-deferred-reattach-live-output.test.ts src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-host-session-launch.test.ts", + "pnpm exec electron-vite build --mode e2e && SKIP_BUILD=1 pnpm exec playwright test tests/e2e/live-background-terminal-mount-authority.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1", + "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/lib/resume-sleeping-agent-session.test.ts tests/e2e/completed-worker-retirement-resume.unit.test.ts src/main/daemon/daemon-pty-adapter-session-adoption.test.ts src/main/daemon/terminal-host-concurrent-create.test.ts src/main/daemon/daemon-pty-adapter.test.ts src/main/ipc/pty-pane-materialization-race.test.ts src/main/ipc/pty-persisted-incarnation-repair.test.ts src/main/ipc/pty-pane-reservation-settlement.test.ts src/main/runtime/orca-runtime.test.ts src/renderer/src/lib/pane-manager/pane-fit.test.ts src/renderer/src/components/terminal-pane/pty-connection-deferred-reattach-live-output.test.ts src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-host-session-launch.test.ts tests/e2e/live-background-terminal-mount-authority.spec.ts" ], "testFiles": [ "src/renderer/src/lib/resume-sleeping-agent-session.test.ts", "tests/e2e/completed-worker-retirement-resume.unit.test.ts", - "src/main/providers/local-pty-provider-spawn-session.test.ts", - "src/main/daemon/terminal-host.test.ts", + "src/main/daemon/daemon-pty-adapter-session-adoption.test.ts", + "src/main/daemon/terminal-host-concurrent-create.test.ts", "src/main/daemon/daemon-pty-adapter.test.ts", "src/main/ipc/pty-pane-materialization-race.test.ts", "src/main/ipc/pty-persisted-incarnation-repair.test.ts", @@ -9518,14 +9527,15 @@ "ORCA_E2E_WEB_CLIENT=1 SKIP_BUILD=1 pnpm exec playwright test tests/e2e/remote-agent-session-focus-authority.spec.ts --config tests/playwright.config.ts --project electron-headful --workers=1", "Manual headed paired-server journey: isolated Orca desktop host + separate paired web client + real Codex process + 20-second WebSocket fault + reconnect + explicit stop", "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/terminal-orphan-owner.test.ts src/main/runtime/terminal-orphan-topology.test.ts src/renderer/src/runtime/web-session-terminal-orphan-recovery.test.ts src/renderer/src/runtime/web-session-terminal-orphan-mixed-version.test.ts src/renderer/src/runtime/web-session-tabs-sync.test.ts --maxWorkers=1", - "pnpm exec vitest run --config config/vitest.config.ts src/main/providers/pty-process-inspection.test.ts src/main/daemon/terminal-host.test.ts src/main/daemon/daemon-pty-router.test.ts src/main/daemon/degraded-daemon-pty-provider.test.ts src/relay/pty-handler-spawn-admission.test.ts src/main/daemon/daemon-pty-adapter.test.ts src/main/runtime/orca-runtime.test.ts tests/e2e/remote-agent-completion-authority.unit.test.ts src/renderer/src/runtime/runtime-terminal-inspection.test.ts src/renderer/src/components/terminal-pane/agent-completion-coordinator-process-cadence.test.ts src/renderer/src/components/terminal-pane/agent-completion-coordinator-pending-title-inspection.test.ts", - "pnpm exec vitest run --config config/vitest.config.ts tests/e2e/remote-agent-completion-authority.unit.test.ts src/main/providers/pty-process-inspection.test.ts src/main/daemon/terminal-host.test.ts src/main/daemon/daemon-pty-router.test.ts src/main/daemon/degraded-daemon-pty-provider.test.ts src/relay/pty-handler-spawn-admission.test.ts src/main/daemon/daemon-pty-adapter.test.ts src/renderer/src/runtime/runtime-terminal-inspection.test.ts src/renderer/src/components/terminal-pane/agent-completion-coordinator-process-cadence.test.ts src/renderer/src/components/terminal-pane/agent-completion-coordinator-pending-title-inspection.test.ts src/renderer/src/components/terminal-pane/pty-connection-task-complete-dispatch.test.ts src/renderer/src/lib/codex-session-restart.test.ts" + "pnpm exec vitest run --config config/vitest.config.ts src/main/providers/pty-process-inspection.test.ts src/main/daemon/terminal-host-concurrent-create.test.ts src/main/daemon/daemon-init-live-session-preservation.test.ts src/main/daemon/daemon-pty-router.test.ts src/relay/pty-handler-spawn-admission.test.ts src/main/daemon/daemon-pty-adapter.test.ts src/main/runtime/orca-runtime.test.ts tests/e2e/remote-agent-completion-authority.unit.test.ts src/renderer/src/runtime/runtime-terminal-inspection.test.ts src/renderer/src/components/terminal-pane/agent-completion-coordinator-process-cadence.test.ts src/renderer/src/components/terminal-pane/agent-completion-coordinator-pending-title-inspection.test.ts", + "pnpm exec vitest run --config config/vitest.config.ts tests/e2e/remote-agent-completion-authority.unit.test.ts src/main/providers/pty-process-inspection.test.ts src/main/daemon/terminal-host-concurrent-create.test.ts src/main/daemon/daemon-init-live-session-preservation.test.ts src/main/daemon/daemon-pty-router.test.ts src/relay/pty-handler-spawn-admission.test.ts src/main/daemon/daemon-pty-adapter.test.ts src/renderer/src/runtime/runtime-terminal-inspection.test.ts src/renderer/src/components/terminal-pane/agent-completion-coordinator-process-cadence.test.ts src/renderer/src/components/terminal-pane/agent-completion-coordinator-pending-title-inspection.test.ts src/renderer/src/components/terminal-pane/pty-connection-task-complete-dispatch.test.ts src/renderer/src/lib/codex-session-restart.test.ts", + "pnpm exec vitest run --config config/vitest.config.ts src/main/providers/pty-process-inspection.test.ts src/main/daemon/terminal-host-concurrent-create.test.ts src/main/daemon/daemon-init-live-session-preservation.test.ts src/relay/pty-handler-spawn-admission.test.ts src/main/runtime/orca-runtime.test.ts tests/e2e/remote-agent-completion-authority.unit.test.ts src/renderer/src/runtime/runtime-terminal-inspection.test.ts src/renderer/src/components/terminal-pane/agent-completion-coordinator-process-cadence.test.ts src/renderer/src/components/terminal-pane/agent-completion-coordinator-pending-title-inspection.test.ts src/renderer/src/components/terminal-pane/pty-connection-task-complete-dispatch.test.ts src/renderer/src/lib/codex-session-restart.test.ts src/shared/claimed-agent-pty-owner.test.ts src/main/daemon/daemon-pty-adapter.test.ts src/main/providers/ssh-pty-provider-agent-session-create-operation.test.ts src/main/runtime/orca-runtime-agent-session-operation.test.ts src/main/runtime/remote-agent-session-host-authority.integration.test.ts src/main/runtime/rpc/methods/agent-session.test.ts src/main/runtime/orca-runtime-terminal-retirement.test.ts src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-host-session-launch.test.ts src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-create-handoff.test.ts src/renderer/src/runtime/remote-runtime-session-tabs-inflight.test.ts src/renderer/src/runtime/web-runtime-session.test.ts src/renderer/src/runtime/web-session-tabs-sync.test.ts src/renderer/src/runtime/web-session-intent-owner.test.ts src/renderer/src/runtime/remote-server-parity.test.ts tests/e2e/remote-agent-session-focus-authority.spec.ts config/scripts/remote-agent-session-authority-repro.mjs config/scripts/remote-agent-session-process-cleanup.mjs tests/e2e/remote-terminal-tab-retirement.unit.test.ts src/main/runtime/terminal-orphan-owner.test.ts src/main/runtime/terminal-orphan-topology.test.ts src/renderer/src/runtime/web-session-terminal-orphan-recovery.test.ts src/renderer/src/runtime/web-session-terminal-orphan-mixed-version.test.ts", + "pnpm exec vitest run --config config/vitest.config.ts tests/e2e/remote-agent-completion-authority.unit.test.ts src/main/providers/pty-process-inspection.test.ts src/main/daemon/terminal-host.test.ts src/main/daemon/daemon-pty-router.test.ts src/main/providers/unavailable-pty-provider.test.ts src/main/daemon/daemon-fresh-spawn-admission.test.ts src/relay/pty-handler-spawn-admission.test.ts src/main/daemon/daemon-pty-adapter.test.ts src/renderer/src/runtime/runtime-terminal-inspection.test.ts src/renderer/src/components/terminal-pane/agent-completion-coordinator-process-cadence.test.ts src/renderer/src/components/terminal-pane/agent-completion-coordinator-pending-title-inspection.test.ts src/renderer/src/components/terminal-pane/pty-connection-task-complete-dispatch.test.ts src/renderer/src/lib/codex-session-restart.test.ts" ], "testFiles": [ "src/main/providers/pty-process-inspection.test.ts", - "src/main/daemon/terminal-host.test.ts", - "src/main/daemon/daemon-pty-router.test.ts", - "src/main/daemon/degraded-daemon-pty-provider.test.ts", + "src/main/daemon/terminal-host-concurrent-create.test.ts", + "src/main/daemon/daemon-init-live-session-preservation.test.ts", "src/relay/pty-handler-spawn-admission.test.ts", "src/main/runtime/orca-runtime.test.ts", "tests/e2e/remote-agent-completion-authority.unit.test.ts", @@ -9552,7 +9562,6 @@ "config/scripts/remote-agent-session-authority-repro.mjs", "config/scripts/remote-agent-session-process-cleanup.mjs", "tests/e2e/remote-terminal-tab-retirement.unit.test.ts", - "src/main/runtime/orca-runtime.test.ts", "src/main/runtime/terminal-orphan-owner.test.ts", "src/main/runtime/terminal-orphan-topology.test.ts", "src/renderer/src/runtime/web-session-terminal-orphan-recovery.test.ts", @@ -9572,16 +9581,14 @@ ] }, { - "file": "src/main/daemon/daemon-pty-router.test.ts", + "file": "src/main/daemon/daemon-init-live-session-preservation.test.ts", "assertions": [ "completion inspection rejects an unmapped session instead of borrowing the current daemon" ] }, { - "file": "src/main/daemon/degraded-daemon-pty-provider.test.ts", - "assertions": [ - "completion inspection rejects an unmapped session instead of borrowing the local fallback" - ] + "file": "src/main/daemon/daemon-init-live-session-preservation.test.ts", + "assertions": ["rejects completion inspection when no daemon owns the session"] }, { "file": "src/relay/pty-handler-spawn-admission.test.ts", @@ -9768,7 +9775,7 @@ "date": "2026-07-23", "runner": "local", "platform": "macos", - "command": "pnpm exec vitest run --config config/vitest.config.ts tests/e2e/remote-agent-completion-authority.unit.test.ts src/main/providers/pty-process-inspection.test.ts src/main/daemon/terminal-host.test.ts src/main/daemon/daemon-pty-router.test.ts src/main/daemon/degraded-daemon-pty-provider.test.ts src/relay/pty-handler-spawn-admission.test.ts src/main/daemon/daemon-pty-adapter.test.ts src/renderer/src/runtime/runtime-terminal-inspection.test.ts src/renderer/src/components/terminal-pane/agent-completion-coordinator-process-cadence.test.ts src/renderer/src/components/terminal-pane/agent-completion-coordinator-pending-title-inspection.test.ts src/renderer/src/components/terminal-pane/pty-connection-task-complete-dispatch.test.ts src/renderer/src/lib/codex-session-restart.test.ts", + "command": "pnpm exec vitest run --config config/vitest.config.ts tests/e2e/remote-agent-completion-authority.unit.test.ts src/main/providers/pty-process-inspection.test.ts src/main/daemon/terminal-host.test.ts src/main/daemon/daemon-pty-router.test.ts src/main/providers/unavailable-pty-provider.test.ts src/main/daemon/daemon-fresh-spawn-admission.test.ts src/relay/pty-handler-spawn-admission.test.ts src/main/daemon/daemon-pty-adapter.test.ts src/renderer/src/runtime/runtime-terminal-inspection.test.ts src/renderer/src/components/terminal-pane/agent-completion-coordinator-process-cadence.test.ts src/renderer/src/components/terminal-pane/agent-completion-coordinator-pending-title-inspection.test.ts src/renderer/src/components/terminal-pane/pty-connection-task-complete-dispatch.test.ts src/renderer/src/lib/codex-session-restart.test.ts", "result": "passed", "durationSeconds": 11.96, "summary": "Eleven focused files and 870 tests passed on the current-main candidate. The cross-boundary harness fails with the implementation reverted by dispatching process-exit from unavailable remote evidence. Direct SSH uses strict main-process inspection, daemon and relay inspection reject missing or unmapped sessions, the terminal lifecycle suite uses the atomic inspection boundary, and one stale pane cannot suppress restart notices for a separately confirmed Codex pane." @@ -10939,7 +10946,7 @@ "providers": ["local", "daemon", "wsl"], "coveredPlatforms": ["windows"], "coveredProviders": ["daemon"], - "coverageNotes": "Issue #8048 now has deterministic wrapper and cold-restore re-anchor tests plus a Windows PR-CI harness that drives the built daemon through 25 real ConPTY workspace-close races while an unrelated witness PTY stays alive. A Windows-only patched-node-pty test injects EAGAIN on one ConPTY input pipe and requires only that PTY to close while an unrelated PTY remains writable; a daemon-level classifier test keeps the native exception backstop narrow. Keyboard reset, CJK repaint, WSL, and full visible Electron coverage remain gaps.", + "coverageNotes": "Issue #8048 now has deterministic wrapper and cold-restore re-anchor tests plus a Windows PR-CI harness that drives the built daemon through 25 real ConPTY workspace-close races while an unrelated witness PTY stays alive. A Windows-only Bun test closes a real native terminal between the adapter check and write, requires only that PTY to exit while an unrelated PTY remains writable, and checks late writes plus final-output drain ordering; a daemon-level classifier test keeps the native exception backstop narrow. Keyboard reset, CJK repaint, WSL, and full visible Electron coverage remain gaps.", "motivatingLinks": [ "https://github.com/stablyai/orca/pull/6541", "https://github.com/stablyai/orca/pull/6858", @@ -10951,16 +10958,16 @@ "https://github.com/stablyai/orca/issues/17027" ], "invariant": "Windows local and daemon terminals must spawn with the intended shell, survive overlapping graceful/forced workspace teardown without affecting unrelated PTYs, contain an asynchronous ConPTY input-pipe failure to the affected terminal without killing the daemon, retain recovered scrollback across the fresh daemon's first checkpoint, accept normal Enter/Backspace/Arrow input after agent or TUI exit, render cursor/CJK/wide-glyph redraws without stale cells, and converge to nonzero applied size.", - "oracle": "The issue #8048 slice asserts one node-pty ConPTY close for a graceful-then-force sequence, atomically seeds recovered history before fresh shell output and re-anchoring, preserves recovery after seed failure plus adapter restart, and runs 25 built-daemon close races while checking victim session/PID reaping, a stable daemon PID, and a live witness PTY. The EAGAIN slice emits an error from one real patched node-pty Windows input socket, requires its terminal to become unwritable and run the normal per-PTY kill path, then writes through an unrelated PTY without an uncaught exception. A broader Windows live gate still needs shell input, resize, cursor, and CJK/wide-glyph pixel evidence.", + "oracle": "The issue #8048 slice asserts one node-pty ConPTY close for a graceful-then-force sequence, atomically seeds recovered history before fresh shell output and re-anchoring, preserves recovery after seed failure plus adapter restart, and runs 25 built-daemon close races while checking victim session/PID reaping, a stable daemon PID, and a live witness PTY. The native I/O slice closes a real Bun terminal at the native write boundary, requires the actual closed-terminal exception to retire that PTY, then writes through an unrelated PTY without an uncaught exception. Separate cases check late input after observed exit and capture final output at the natural-exit boundary. This does not induce kernel EAGAIN, which Bun handles through its internal input buffer. A broader Windows live gate still needs shell input, resize, cursor, and CJK/wide-glyph pixel evidence.", "commands": [ - "pnpm vitest run src/main/daemon/pty-subprocess.test.ts src/main/daemon/daemon-pty-adapter.test.ts src/main/daemon/node-pty-windows-input-error.win32.test.ts src/main/daemon/daemon-native-pty-exception.test.ts", + "pnpm vitest run src/main/daemon/pty-subprocess.test.ts src/main/daemon/daemon-pty-adapter.test.ts src/main/daemon/bun-pty-windows-io-failure.win32.test.ts src/main/daemon/daemon-native-pty-exception.test.ts", "pnpm build:electron-vite && node config/scripts/windows-daemon-workspace-close-repro.mjs", "node config/scripts/windows-daemon-workspace-close-repro.mjs" ], "testFiles": [ "src/main/daemon/pty-subprocess.test.ts", "src/main/daemon/daemon-pty-adapter.test.ts", - "src/main/daemon/node-pty-windows-input-error.win32.test.ts", + "src/main/daemon/bun-pty-windows-io-failure.win32.test.ts", "src/main/daemon/daemon-native-pty-exception.test.ts", "config/scripts/windows-daemon-workspace-close-repro.mjs" ], @@ -10979,9 +10986,9 @@ ] }, { - "file": "src/main/daemon/node-pty-windows-input-error.win32.test.ts", + "file": "src/main/daemon/bun-pty-windows-io-failure.win32.test.ts", "assertions": [ - "an EAGAIN event retires only the affected patched node-pty terminal while a witness remains writable" + "a real native closed-terminal exception retires only the affected Bun PTY while a witness remains writable; late input is safe and final output arrives before the exit event" ] }, { @@ -12554,24 +12561,23 @@ }, { "id": "terminal-provider.daemon-startup-degraded-contract", - "title": "Daemon startup reconcile and degraded fallback preserve provider identity", + "title": "Daemon startup and unavailable-service recovery preserve provider identity", "maturity": "experimental", "protection": "partial", "owner": "terminal-provider", "layer": "provider-contract", "surfaces": [ "daemon startup", - "degraded daemon", - "fallback PTY", + "unavailable daemon", "provider ownership", "startup restore", "stable-pane reopen" ], "platforms": ["macos", "linux", "windows"], - "providers": ["daemon", "local"], + "providers": ["daemon"], "coveredPlatforms": ["macos"], - "coveredProviders": ["daemon", "local"], - "coverageNotes": "Deterministic provider contracts cover complete, incomplete, conflicting, refused, and identity-invalidated owner inventories; degraded fallback/current/legacy routing; repeated stale-binding classification; and fresh-session reattach without local fallback during unresolved ownership. Main IPC contracts cover exact persisted-binding retirement and one fresh replacement after confirmed absence. Live Linux, Windows, WSL, SSH, paired-runtime, and packaged-upgrade journeys remain uncollected.", + "coveredProviders": ["daemon"], + "coverageNotes": "Current deterministic contracts cover daemon owner inventories, current/legacy routing, incomplete-owner refusal, exact reattach and fresh-spawn admission. The in-process fallback provider has been removed: unavailable service startup remains retryable and cannot create a second PTY owner. Historical evidence below describes the former provider topology; it does not qualify the new topology. Live Linux, Windows, WSL, SSH, paired-runtime and packaged-upgrade journeys remain uncollected for this gate.", "motivatingLinks": [ "https://github.com/stablyai/orca/pull/6830", "https://github.com/stablyai/orca/pull/6866", @@ -12579,16 +12585,18 @@ "https://github.com/stablyai/orca/pull/12776" ], "invariant": "Daemon startup reconciliation must preserve valid live daemon sessions and reap only true orphans. A persisted binding stays fail-closed while any possible owner is incomplete, conflicting, refusing attach, or identity-invalidated, but complete authoritative absence from every configured provider must retire that exact stale binding and let retry or reopen converge without attaching it through local fallback or duplicating a PTY.", - "oracle": "Hold exact fallback, current-daemon, and legacy-daemon inventories behind controlled promises. No attach may settle or dispatch until every provider answers. Complete zero-candidate inventories must report SessionNotFound; incomplete, duplicate, refused, and identity-invalidated attempts must remain owner-unverified. In degraded mode, repeated stale attempts scan each eligible provider once per attempt and dispatch no attach, then one explicit fresh spawn and exact reattach use only the recorded fresh route. Main IPC must compare-and-swap retire only the matching persisted PTY/incarnation, emit one synthetic exit, and create one different fresh PTY.", + "oracle": "Hold current-daemon and legacy-daemon inventories behind controlled promises. Complete authoritative absence reports SessionNotFound; incomplete, duplicate, refused and identity-invalidated attempts remain owner-unverified. Route exact reattach and foreground inspection to the owning daemon. Refuse fresh PTYs when a preserved daemon cannot spawn, then coalesce explicit startup retries without stopping that service. Main IPC compare-and-swap retires only the matching persisted PTY/incarnation.", "commands": [ - "pnpm exec vitest run --config config/vitest.config.ts src/main/daemon/daemon-session-owner-resolution.test.ts src/main/daemon/daemon-pty-router.test.ts src/main/daemon/degraded-daemon-pty-provider.test.ts src/main/ipc/pty-dead-owner-respawn.test.ts src/main/ipc/pty-persisted-incarnation-repair.test.ts --reporter=dot" + "pnpm exec vitest run --config config/vitest.config.ts src/main/daemon/daemon-session-owner-resolution.test.ts src/main/daemon/daemon-init-live-session-preservation.test.ts src/main/daemon/daemon-pty-router.test.ts src/main/ipc/pty-dead-owner-respawn.test.ts src/main/ipc/pty-persisted-incarnation-repair.test.ts --reporter=dot", + "pnpm exec vitest run --config config/vitest.config.ts src/main/daemon/daemon-session-owner-resolution.test.ts src/main/daemon/daemon-init-live-session-preservation.test.ts src/main/ipc/pty-dead-owner-respawn.test.ts src/main/ipc/pty-persisted-incarnation-repair.test.ts src/main/daemon/daemon-init-provider-installation.test.ts", + "pnpm exec vitest run --config config/vitest.config.ts src/main/daemon/daemon-session-owner-resolution.test.ts src/main/daemon/daemon-pty-router.test.ts src/main/providers/unavailable-pty-provider.test.ts src/main/daemon/daemon-fresh-spawn-admission.test.ts src/main/ipc/pty-dead-owner-respawn.test.ts src/main/ipc/pty-persisted-incarnation-repair.test.ts --reporter=dot" ], "testFiles": [ "src/main/daemon/daemon-session-owner-resolution.test.ts", - "src/main/daemon/daemon-pty-router.test.ts", - "src/main/daemon/degraded-daemon-pty-provider.test.ts", + "src/main/daemon/daemon-init-live-session-preservation.test.ts", "src/main/ipc/pty-dead-owner-respawn.test.ts", - "src/main/ipc/pty-persisted-incarnation-repair.test.ts" + "src/main/ipc/pty-persisted-incarnation-repair.test.ts", + "src/main/daemon/daemon-init-provider-installation.test.ts" ], "assertionRefs": [ { @@ -12599,18 +12607,18 @@ ] }, { - "file": "src/main/daemon/daemon-pty-router.test.ts", + "file": "src/main/daemon/daemon-init-live-session-preservation.test.ts", "assertions": [ "complete current and legacy daemon inventories prove liveness absence with one list call per adapter", "one unavailable daemon inventory keeps the same missing PTY liveness unknown" ] }, { - "file": "src/main/daemon/degraded-daemon-pty-provider.test.ts", + "file": "src/main/daemon/daemon-init-live-session-preservation.test.ts", "assertions": [ - "repeated complete absence never dispatches the stale id to fallback or either daemon", - "one explicit fresh fallback PTY reattaches by its exact PTY and incarnation without another inventory", - "a mapped fallback-owned PTY reports live without borrowing fallback authority for unknown ids or scanning daemon inventories" + "does not report absence while any possible daemon owner is unavailable", + "reports absence after every possible daemon owner returns a complete inventory", + "routes fresh foreground confirmation to the session-owning daemon" ] }, { @@ -12629,7 +12637,7 @@ "date": "2026-08-06", "runner": "local", "platform": "macos", - "command": "pnpm exec vitest run --config config/vitest.config.ts src/main/daemon/daemon-session-owner-resolution.test.ts src/main/daemon/daemon-pty-router.test.ts src/main/daemon/degraded-daemon-pty-provider.test.ts src/main/ipc/pty-dead-owner-respawn.test.ts src/main/ipc/pty-persisted-incarnation-repair.test.ts --reporter=dot", + "command": "pnpm exec vitest run --config config/vitest.config.ts src/main/daemon/daemon-session-owner-resolution.test.ts src/main/daemon/daemon-pty-router.test.ts src/main/providers/unavailable-pty-provider.test.ts src/main/daemon/daemon-fresh-spawn-admission.test.ts src/main/ipc/pty-dead-owner-respawn.test.ts src/main/ipc/pty-persisted-incarnation-repair.test.ts --reporter=dot", "result": "passed", "durationSeconds": 5.44, "summary": "Four files passed 535 owner-resolution, router, degraded-provider, and IPC lifecycle tests. With only the resolver fix disabled, the unchanged four-file gate failed four stale-absence, identity-retry, and router-liveness assertions; restoring it passed 535 of 535." @@ -14347,21 +14355,22 @@ "invariant": "Shift+Enter key recognition follows the client OS, while emitted bytes follow the active application and PTY host: active Kitty keyboard protocol authorizes CSI-u on every host; otherwise Windows hosts use Esc+CR except when allowlisted Droid ownership or fresh routing-trusted Droid process evidence authorizes CSI-u, and non-Windows hosts use Esc+CR. The active SSH connection, remote-runtime PTY owner, or live local session outranks later worktree ownership changes, and missing host-platform metadata falls back to the client. Every new command and PTY replacement revokes stale agent routing until current evidence settles; split or reused panes cannot inherit sibling or prior-PTY authority; unrelated keys perform no host, agent, protocol, or ConPTY lookup.", "oracle": "Renderer tests assert exact bytes across client/PTY-host combinations, including Kitty keyboard protocol active and inactive on Windows and non-Windows hosts, and prove SSH identity, the environment encoded in an active remote-runtime PTY id, and live local-session identity outrank current worktree ownership, while unavailable platform metadata falls back to the client. A live linux-arm64 SSH PTY independently captured Esc+CR with KKP inactive and CSI-u with KKP active. Fresh Windows process evidence authorizes Droid bytes only after a recognized global candidate is intersected with the exact ConPTY console process list; detached descendants, helper failure/timeout/root-only fallback, stale PTY exit/rebind results, typed text, and hook/OSC output fail closed. A new OSC 133;C immediately publishes a routing-neutral generation, including during D-to-C races. In cmd.exe/Git Bash/custom shells without OSC 133, accepted inferred commands start the same fresh generation; accepted submit/interrupt, title exit, focus, and visibility revoke trusted Droid bytes while one bounded confirmation runs. Detach preserves the live PTY's source shell override so WSL/native classification cannot change with the current default. Ordinary fast shell commands use cached/no-scan paths; routing-relevant confirmation bypasses cached process snapshots and keeps the bounded three-read ladder. Daemon protocol v21 persists only an allowlisted launchAgent; warm reattach uses it as a display/confirmation hint and restores routing only after current process proof. The Windows Electron test focuses the real xterm textarea and records exact renderer-to-main PTY writes.", "commands": [ - "pnpm exec vitest run --config config/vitest.config.ts src/main/daemon/daemon-foreground-confirmation-protocol.test.ts src/main/daemon/daemon-pty-adapter.test.ts src/main/daemon/daemon-pty-router.test.ts src/main/daemon/daemon-server.test.ts src/main/daemon/degraded-daemon-pty-provider.test.ts src/main/daemon/pty-subprocess-foreground-scan-cadence.test.ts src/main/daemon/pty-subprocess.test.ts src/main/ipc/pty-runtime-kill-and-exit.test.ts src/main/providers/agent-foreground-process.test.ts src/main/providers/local-pty-provider-foreground-process.test.ts src/main/daemon/pty-subprocess-foreground-degraded-scan.test.ts src/main/providers/windows-console-attached-processes.test.ts src/main/providers/windows-pty-job-membership.test.ts src/renderer/src/components/terminal-pane/pane-foreground-agent-tracker.test.ts src/renderer/src/components/terminal-pane/pty-connection-command-finished-cleanup.test.ts src/renderer/src/components/terminal-pane/pty-connection-foreground-agent-routing.test.ts src/renderer/src/components/terminal-pane/pty-connection-foreground-agent-sampling.test.ts src/renderer/src/components/terminal-pane/pty-transport-reattach-admission.test.ts src/renderer/src/components/terminal-pane/terminal-pane-tab-detach.test.ts src/renderer/src/components/terminal-pane/terminal-shortcut-policy.test.ts src/renderer/src/components/terminal-pane/terminal-windows-shift-enter.test.ts src/renderer/src/store/slices/store-active-worktree-terminal-creation.test.ts src/renderer/src/store/slices/terminal-pane-detach-agent-identity.test.ts src/shared/agent-process-recognition.test.ts src/shared/process-table-snapshot.test.ts tests/e2e/terminal-foreground-confirmation.unit.test.ts", + "pnpm exec vitest run --config config/vitest.config.ts src/main/daemon/daemon-foreground-confirmation-protocol.test.ts src/main/daemon/daemon-pty-adapter.test.ts src/main/daemon/daemon-init-live-session-preservation.test.ts src/main/daemon/daemon-server.test.ts src/main/daemon/daemon-pty-router.test.ts src/main/daemon/pty-subprocess-foreground-scan-cadence.test.ts src/main/daemon/pty-subprocess.test.ts src/main/ipc/pty-runtime-kill-and-exit.test.ts src/main/providers/agent-foreground-process.test.ts src/main/daemon/pty-subprocess-spawn-file-foreground.test.ts src/main/daemon/pty-subprocess-foreground-degraded-scan.test.ts src/main/providers/windows-console-attached-processes.test.ts src/main/providers/windows-pty-job-membership.test.ts src/renderer/src/components/terminal-pane/pane-foreground-agent-tracker.test.ts src/renderer/src/components/terminal-pane/pty-connection-command-finished-cleanup.test.ts src/renderer/src/components/terminal-pane/pty-connection-foreground-agent-routing.test.ts src/renderer/src/components/terminal-pane/pty-connection-foreground-agent-sampling.test.ts src/renderer/src/components/terminal-pane/pty-transport-reattach-admission.test.ts src/renderer/src/components/terminal-pane/terminal-pane-tab-detach.test.ts src/renderer/src/components/terminal-pane/terminal-shortcut-policy.test.ts src/renderer/src/components/terminal-pane/terminal-windows-shift-enter.test.ts src/renderer/src/store/slices/store-active-worktree-terminal-creation.test.ts src/renderer/src/store/slices/terminal-pane-detach-agent-identity.test.ts src/shared/agent-process-recognition.test.ts src/shared/process-table-snapshot.test.ts tests/e2e/terminal-foreground-confirmation.unit.test.ts", "pnpm run test:e2e -- tests/e2e/terminal-shortcuts.spec.ts", - "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/keyboard-handlers.test.ts src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-pane-handle-resolution.test.ts src/renderer/src/components/terminal-pane/terminal-input-host-platform.test.ts" + "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/keyboard-handlers.test.ts src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-pane-handle-resolution.test.ts src/renderer/src/components/terminal-pane/terminal-input-host-platform.test.ts", + "pnpm exec vitest run --config config/vitest.config.ts src/main/daemon/daemon-foreground-confirmation-protocol.test.ts src/main/daemon/daemon-pty-adapter.test.ts src/main/daemon/daemon-init-live-session-preservation.test.ts src/main/daemon/daemon-server.test.ts src/main/daemon/pty-subprocess-foreground-scan-cadence.test.ts src/main/daemon/pty-subprocess.test.ts src/main/ipc/pty-runtime-kill-and-exit.test.ts src/main/providers/agent-foreground-process.test.ts src/main/daemon/terminal-host-process-inspection.test.ts src/main/daemon/pty-subprocess-foreground-degraded-scan.test.ts src/main/providers/windows-console-attached-processes.test.ts src/main/providers/windows-pty-job-membership.test.ts src/renderer/src/components/terminal-pane/pane-foreground-agent-tracker.test.ts src/renderer/src/components/terminal-pane/keyboard-handlers.test.ts src/renderer/src/components/terminal-pane/pty-connection-command-finished-cleanup.test.ts src/renderer/src/components/terminal-pane/pty-connection-foreground-agent-routing.test.ts src/renderer/src/components/terminal-pane/pty-connection-foreground-agent-sampling.test.ts src/renderer/src/components/terminal-pane/pty-transport-reattach-admission.test.ts src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-pane-handle-resolution.test.ts src/renderer/src/components/terminal-pane/terminal-pane-tab-detach.test.ts src/renderer/src/components/terminal-pane/terminal-input-host-platform.test.ts src/renderer/src/components/terminal-pane/terminal-shortcut-policy.test.ts src/renderer/src/components/terminal-pane/terminal-windows-shift-enter.test.ts src/renderer/src/store/slices/store-active-worktree-terminal-creation.test.ts src/renderer/src/store/slices/terminal-pane-detach-agent-identity.test.ts src/shared/agent-process-recognition.test.ts src/shared/process-table-snapshot.test.ts tests/e2e/terminal-foreground-confirmation.unit.test.ts tests/e2e/terminal-shortcuts.spec.ts", + "pnpm exec vitest run --config config/vitest.config.ts src/main/daemon/daemon-foreground-confirmation-protocol.test.ts src/main/daemon/daemon-pty-adapter.test.ts src/main/daemon/daemon-pty-router.test.ts src/main/daemon/daemon-server.test.ts src/main/providers/unavailable-pty-provider.test.ts src/main/daemon/daemon-fresh-spawn-admission.test.ts src/main/daemon/pty-subprocess-foreground-scan-cadence.test.ts src/main/daemon/pty-subprocess.test.ts src/main/ipc/pty-runtime-kill-and-exit.test.ts src/main/providers/agent-foreground-process.test.ts src/main/daemon/terminal-host-process-inspection.test.ts src/main/daemon/pty-subprocess-foreground-degraded-scan.test.ts src/main/providers/windows-console-attached-processes.test.ts src/main/providers/windows-pty-job-membership.test.ts src/renderer/src/components/terminal-pane/pane-foreground-agent-tracker.test.ts src/renderer/src/components/terminal-pane/pty-connection-command-finished-cleanup.test.ts src/renderer/src/components/terminal-pane/pty-connection-foreground-agent-routing.test.ts src/renderer/src/components/terminal-pane/pty-connection-foreground-agent-sampling.test.ts src/renderer/src/components/terminal-pane/pty-transport-reattach-admission.test.ts src/renderer/src/components/terminal-pane/terminal-pane-tab-detach.test.ts src/renderer/src/components/terminal-pane/terminal-shortcut-policy.test.ts src/renderer/src/components/terminal-pane/terminal-windows-shift-enter.test.ts src/renderer/src/store/slices/store-active-worktree-terminal-creation.test.ts src/renderer/src/store/slices/terminal-pane-detach-agent-identity.test.ts src/shared/agent-process-recognition.test.ts src/shared/process-table-snapshot.test.ts tests/e2e/terminal-foreground-confirmation.unit.test.ts" ], "testFiles": [ "src/main/daemon/daemon-foreground-confirmation-protocol.test.ts", "src/main/daemon/daemon-pty-adapter.test.ts", - "src/main/daemon/daemon-pty-router.test.ts", + "src/main/daemon/daemon-init-live-session-preservation.test.ts", "src/main/daemon/daemon-server.test.ts", - "src/main/daemon/degraded-daemon-pty-provider.test.ts", "src/main/daemon/pty-subprocess-foreground-scan-cadence.test.ts", "src/main/daemon/pty-subprocess.test.ts", "src/main/ipc/pty-runtime-kill-and-exit.test.ts", "src/main/providers/agent-foreground-process.test.ts", - "src/main/providers/local-pty-provider-foreground-process.test.ts", + "src/main/daemon/terminal-host-process-inspection.test.ts", "src/main/daemon/pty-subprocess-foreground-degraded-scan.test.ts", "src/main/providers/windows-console-attached-processes.test.ts", "src/main/providers/windows-pty-job-membership.test.ts", @@ -14428,10 +14437,8 @@ ] }, { - "file": "src/main/providers/local-pty-provider-foreground-process.test.ts", - "assertions": [ - "a delayed foreground confirmation is discarded after the owning PTY exits" - ] + "file": "src/main/daemon/terminal-host-process-inspection.test.ts", + "assertions": ["returns unverifiable when the expected incarnation is stale"] }, { "file": "src/main/daemon/daemon-pty-adapter.test.ts", @@ -14577,7 +14584,7 @@ "date": "2026-08-25", "runner": "local", "platform": "macos", - "command": "pnpm exec vitest run --config config/vitest.config.ts src/main/daemon/daemon-foreground-confirmation-protocol.test.ts src/main/daemon/daemon-pty-adapter.test.ts src/main/daemon/daemon-pty-router.test.ts src/main/daemon/daemon-server.test.ts src/main/daemon/degraded-daemon-pty-provider.test.ts src/main/daemon/pty-subprocess-foreground-scan-cadence.test.ts src/main/daemon/pty-subprocess.test.ts src/main/ipc/pty-runtime-kill-and-exit.test.ts src/main/providers/agent-foreground-process.test.ts src/main/providers/local-pty-provider-foreground-process.test.ts src/main/daemon/pty-subprocess-foreground-degraded-scan.test.ts src/main/providers/windows-console-attached-processes.test.ts src/main/providers/windows-pty-job-membership.test.ts src/renderer/src/components/terminal-pane/pane-foreground-agent-tracker.test.ts src/renderer/src/components/terminal-pane/pty-connection-command-finished-cleanup.test.ts src/renderer/src/components/terminal-pane/pty-connection-foreground-agent-routing.test.ts src/renderer/src/components/terminal-pane/pty-connection-foreground-agent-sampling.test.ts src/renderer/src/components/terminal-pane/pty-transport-reattach-admission.test.ts src/renderer/src/components/terminal-pane/terminal-pane-tab-detach.test.ts src/renderer/src/components/terminal-pane/terminal-shortcut-policy.test.ts src/renderer/src/components/terminal-pane/terminal-windows-shift-enter.test.ts src/renderer/src/store/slices/store-active-worktree-terminal-creation.test.ts src/renderer/src/store/slices/terminal-pane-detach-agent-identity.test.ts src/shared/agent-process-recognition.test.ts src/shared/process-table-snapshot.test.ts tests/e2e/terminal-foreground-confirmation.unit.test.ts", + "command": "pnpm exec vitest run --config config/vitest.config.ts src/main/daemon/daemon-foreground-confirmation-protocol.test.ts src/main/daemon/daemon-pty-adapter.test.ts src/main/daemon/daemon-pty-router.test.ts src/main/daemon/daemon-server.test.ts src/main/providers/unavailable-pty-provider.test.ts src/main/daemon/daemon-fresh-spawn-admission.test.ts src/main/daemon/pty-subprocess-foreground-scan-cadence.test.ts src/main/daemon/pty-subprocess.test.ts src/main/ipc/pty-runtime-kill-and-exit.test.ts src/main/providers/agent-foreground-process.test.ts src/main/daemon/terminal-host-process-inspection.test.ts src/main/daemon/pty-subprocess-foreground-degraded-scan.test.ts src/main/providers/windows-console-attached-processes.test.ts src/main/providers/windows-pty-job-membership.test.ts src/renderer/src/components/terminal-pane/pane-foreground-agent-tracker.test.ts src/renderer/src/components/terminal-pane/pty-connection-command-finished-cleanup.test.ts src/renderer/src/components/terminal-pane/pty-connection-foreground-agent-routing.test.ts src/renderer/src/components/terminal-pane/pty-connection-foreground-agent-sampling.test.ts src/renderer/src/components/terminal-pane/pty-transport-reattach-admission.test.ts src/renderer/src/components/terminal-pane/terminal-pane-tab-detach.test.ts src/renderer/src/components/terminal-pane/terminal-shortcut-policy.test.ts src/renderer/src/components/terminal-pane/terminal-windows-shift-enter.test.ts src/renderer/src/store/slices/store-active-worktree-terminal-creation.test.ts src/renderer/src/store/slices/terminal-pane-detach-agent-identity.test.ts src/shared/agent-process-recognition.test.ts src/shared/process-table-snapshot.test.ts tests/e2e/terminal-foreground-confirmation.unit.test.ts", "result": "passed", "durationSeconds": 3.88, "summary": "Twenty-six focused test files passed (489 tests) on the PR head, covering fresh-scan ordering/deduplication, exact ConPTY membership and detached-child rejection, stale PTY guards, daemon v21 launch ownership with current-process confirmation, no-OSC command/exit recovery, preserved detach shell classification, unavailable evidence, routing-neutral command generations, exact shortcut bytes, and the composed daemon-plus-tracker contract; the Windows Electron byte test remains platform-gated and is not counted as local macOS evidence." @@ -14611,7 +14618,8 @@ "If both Windows process-enumeration backends remain unavailable through the bounded ladder, routing fails closed to the legacy Esc+CR path; no live Droid artifact covers that degraded host configuration.", "Protocol v21 restarts older daemons because the minimal persisted launchAgent is a new contract; sessions preserved by pre-v21 daemons cannot supply warm-reattach launch ownership and fail closed until current process confirmation or restart.", "No live daemon, WSL, remote-runtime, paired-web, or restore/replay byte artifact has been collected; SSH is covered only by a macOS-client-to-Linux-host relay run.", - "A running PTY keeps its spawn-time ORCA_PANE_KEY after pane-to-tab detach. This change transfers launch/process routing evidence and retires the stale hook snapshot, but does not redesign future hook reattribution across tab IDs." + "A running PTY keeps its spawn-time ORCA_PANE_KEY after pane-to-tab detach. This change transfers launch/process routing evidence and retires the stale hook snapshot, but does not redesign future hook reattribution across tab IDs.", + "The retired in-process delayed-confirmation exit test is replaced by daemon stale-incarnation refusal coverage; an equivalent delayed daemon confirmation/exit interleaving is not claimed by that replacement." ], "demotionRule": "Keep non-blocking or demote to protection none if the Windows byte test silently skips, flakes without an actionable artifact, routes bytes using tab/title/terminal-output heuristics, or adds any ordinary-key lookup, IPC, polling, scan, or lifecycle leak." }, @@ -14639,7 +14647,9 @@ "oracle": "A quarter-circle Claude task title authorizes without a provider lookup only on the exact PTY incarnation carrying verified managed-Claude launch identity; the same title in a bare pane, with an unverified launch hint, or after PTY incarnation replacement is refused while current Claude activity remains a working signal. Fresh explicit state plus ordinary PowerShell plus confirmed recognized agent is sendable on the same PTY. Confirmed shell/non-agent, unavailable confirmation, PTY exit, handle rebind, or a callback PTY mismatch returns a refusal or not-writable result and writes zero bytes.", "commands": [ "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/quarter-circle-title-send-authorization.test.ts", - "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/rpc/terminal-send.test.ts src/main/ipc/pty-runtime-kill-and-exit.test.ts src/main/providers/agent-foreground-process.test.ts src/main/providers/local-pty-provider-foreground-process.test.ts src/main/daemon/pty-subprocess-foreground-degraded-scan.test.ts src/main/providers/windows-console-attached-processes.test.ts src/main/providers/windows-pty-job-membership.test.ts src/main/daemon/daemon-foreground-confirmation-protocol.test.ts src/main/daemon/pty-subprocess.test.ts src/renderer/src/lib/active-agent-note-send-explicit-target.test.ts src/renderer/src/components/browser-pane/annotate/BrowserAnnotationSendMenuContent.test.tsx" + "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/rpc/terminal-send.test.ts src/main/ipc/pty-runtime-kill-and-exit.test.ts src/main/providers/agent-foreground-process.test.ts src/main/daemon/pty-subprocess-spawn-file-foreground.test.ts src/main/daemon/pty-subprocess-foreground-degraded-scan.test.ts src/main/providers/windows-console-attached-processes.test.ts src/main/providers/windows-pty-job-membership.test.ts src/main/daemon/daemon-foreground-confirmation-protocol.test.ts src/main/daemon/pty-subprocess.test.ts src/renderer/src/lib/active-agent-note-send-explicit-target.test.ts src/renderer/src/components/browser-pane/annotate/BrowserAnnotationSendMenuContent.test.tsx", + "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/quarter-circle-title-send-authorization.test.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/rpc/terminal-send.test.ts src/main/ipc/pty-runtime-kill-and-exit.test.ts src/main/providers/agent-foreground-process.test.ts src/main/daemon/terminal-host-process-inspection.test.ts src/main/daemon/pty-subprocess-foreground-degraded-scan.test.ts src/main/providers/windows-console-attached-processes.test.ts src/main/providers/windows-pty-job-membership.test.ts src/main/daemon/daemon-foreground-confirmation-protocol.test.ts src/main/daemon/pty-subprocess.test.ts src/renderer/src/lib/active-agent-note-send-explicit-target.test.ts src/renderer/src/components/browser-pane/annotate/BrowserAnnotationSendMenuContent.test.tsx", + "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/rpc/terminal-send.test.ts src/main/ipc/pty-runtime-kill-and-exit.test.ts src/main/providers/agent-foreground-process.test.ts src/main/daemon/terminal-host-process-inspection.test.ts src/main/daemon/pty-subprocess-foreground-degraded-scan.test.ts src/main/providers/windows-console-attached-processes.test.ts src/main/providers/windows-pty-job-membership.test.ts src/main/daemon/daemon-foreground-confirmation-protocol.test.ts src/main/daemon/pty-subprocess.test.ts src/renderer/src/lib/active-agent-note-send-explicit-target.test.ts src/renderer/src/components/browser-pane/annotate/BrowserAnnotationSendMenuContent.test.tsx" ], "testFiles": [ "src/main/runtime/quarter-circle-title-send-authorization.test.ts", @@ -14647,7 +14657,7 @@ "src/main/runtime/rpc/terminal-send.test.ts", "src/main/ipc/pty-runtime-kill-and-exit.test.ts", "src/main/providers/agent-foreground-process.test.ts", - "src/main/providers/local-pty-provider-foreground-process.test.ts", + "src/main/daemon/terminal-host-process-inspection.test.ts", "src/main/daemon/pty-subprocess-foreground-degraded-scan.test.ts", "src/main/providers/windows-console-attached-processes.test.ts", "src/main/providers/windows-pty-job-membership.test.ts", @@ -14697,8 +14707,8 @@ ] }, { - "file": "src/main/providers/local-pty-provider-foreground-process.test.ts", - "assertions": ["fresh confirmation is discarded when its owning local PTY exits"] + "file": "src/main/daemon/terminal-host-process-inspection.test.ts", + "assertions": ["returns unverifiable when the expected incarnation is stale"] }, { "file": "src/main/providers/windows-console-attached-processes.test.ts", @@ -14745,7 +14755,7 @@ "date": "2026-08-25", "runner": "local", "platform": "macos", - "command": "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/rpc/terminal-send.test.ts src/main/ipc/pty-runtime-kill-and-exit.test.ts src/main/providers/agent-foreground-process.test.ts src/main/providers/local-pty-provider-foreground-process.test.ts src/main/daemon/pty-subprocess-foreground-degraded-scan.test.ts src/main/providers/windows-console-attached-processes.test.ts src/main/providers/windows-pty-job-membership.test.ts src/main/daemon/daemon-foreground-confirmation-protocol.test.ts src/main/daemon/pty-subprocess.test.ts src/renderer/src/lib/active-agent-note-send-explicit-target.test.ts src/renderer/src/components/browser-pane/annotate/BrowserAnnotationSendMenuContent.test.tsx", + "command": "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/rpc/terminal-send.test.ts src/main/ipc/pty-runtime-kill-and-exit.test.ts src/main/providers/agent-foreground-process.test.ts src/main/daemon/terminal-host-process-inspection.test.ts src/main/daemon/pty-subprocess-foreground-degraded-scan.test.ts src/main/providers/windows-console-attached-processes.test.ts src/main/providers/windows-pty-job-membership.test.ts src/main/daemon/daemon-foreground-confirmation-protocol.test.ts src/main/daemon/pty-subprocess.test.ts src/renderer/src/lib/active-agent-note-send-explicit-target.test.ts src/renderer/src/components/browser-pane/annotate/BrowserAnnotationSendMenuContent.test.tsx", "result": "passed", "durationSeconds": 13.74, "summary": "Twelve focused test files passed (1351 tests, 1 skipped), covering runtime confirmation and PTY revalidation, guarded RPC zero-write behavior, PTY controller routing, local/daemon fresh scans, exact ConPTY membership, and unchanged renderer note routing." @@ -14776,7 +14786,8 @@ "Physical Windows validation is unavailable on the current macOS host; exact ConPTY membership is covered deterministically.", "Electron annotation golden-path, repeat-send, adjacent-menu evidence, and screenshots are left to coordinator validation.", "SSH, WSL, legacy daemon, and remote-runtime providers without confirmation remain intentionally fail-closed on an ordinary-shell conflict; no live artifacts cover those degraded paths.", - "No live Linux PTY, paired-web, mobile/relay, restore/replay, or multi-window artifact is attached; those surfaces receive no renderer, persistence, or protocol change." + "No live Linux PTY, paired-web, mobile/relay, restore/replay, or multi-window artifact is attached; those surfaces receive no renderer, persistence, or protocol change.", + "The retired in-process delayed-confirmation exit test is replaced by daemon stale-incarnation refusal coverage; an equivalent delayed daemon confirmation/exit interleaving is not claimed by that replacement." ], "demotionRule": "Keep non-blocking or demote to protection none if a generic busy title authorizes without exact title, verified-launch, or foreground identity; provider confirmation becomes unconditional; exact-PTY mismatch can write bytes; unsupported providers fail open; or the focused gate flakes without an actionable product or harness defect." }, @@ -15130,9 +15141,11 @@ "oracle": "Seed status, dispatch, and worker_done rows across direct-handle and canonical Run recipients in an isolated DB. Compare pointer count, RPC and built-CLI check output, direct SQLite rows, unread/peek/all/type filters, concurrent pollers, fixed Delivery IDs, explicit acknowledgment, restart, filtered check --wait, and coordinator remint. Route a 125-row old-handle backlog, inject a commit without notification, and require startup repair. Exercise duplicate Run/Dispatch owners, stale panes, 50-row pages, cancellation, lifecycle fencing, and absent PTYs. Drop a federation ACK, reconnect/restart v1/v2 peers, and require stable import plus no duplicate read-row wake. Hold a healthy SSH write past five seconds but below the 60-second settlement deadline, then distinguish the three settlement outcomes end to end: only a proven refusal releases the reservation and drains a delivery parked behind the watermark; a dropped in-flight settlement must surface as unverifiable with bytes handed to the transport, preserve the durable write-attempted reservation, and emit no duplicate pointer after restart; a settled write that throws mid-pointer is unverifiable, not a refusal; and an Enter whose settlement is lost stays at enter-attempted so restart emits no second Enter. Install the production PTY controller and verify that it routes settled writes through the owning provider and refuses before any byte when the routed provider cannot settle. Census every production PTY provider class and reject a settlement synthesized from the fire-and-forget write.", "commands": [ "pnpm run build:cli && pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orchestration-message-delivery-identity.test.ts --reporter=dot --testTimeout=5000", - "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orchestration-mailbox-routing-races.test.ts src/main/runtime/orchestration-mailbox-notification-consistency.test.ts src/main/runtime/orchestration-mailbox-detached-routing.test.ts src/main/runtime/orchestration-mailbox-transport-settlement.test.ts src/main/ipc/pty-controller-ownership-routing.test.ts src/main/runtime/orchestration/run-coordinator-handle-migration.test.ts src/main/runtime/orchestration/orchestration-run-delivery-db.test.ts src/main/runtime/orchestration/orchestration-worker-dispatch-db.test.ts src/main/runtime/orchestration/formatter.test.ts src/main/providers/ssh-pty-provider.test.ts src/main/providers/ssh-pty-write.test.ts src/main/providers/settled-pty-writer-census.test.ts src/main/runtime/orchestration/mailbox-pointer-stage.test.ts src/main/daemon/client.test.ts src/main/daemon/daemon-pty-router.test.ts src/main/daemon/degraded-daemon-pty-provider.test.ts", + "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orchestration-mailbox-routing-races.test.ts src/main/runtime/orchestration-mailbox-notification-consistency.test.ts src/main/runtime/orchestration-mailbox-detached-routing.test.ts src/main/runtime/orchestration-mailbox-transport-settlement.test.ts src/main/ipc/pty-controller-ownership-routing.test.ts src/main/runtime/orchestration/run-coordinator-handle-migration.test.ts src/main/runtime/orchestration/orchestration-run-delivery-db.test.ts src/main/runtime/orchestration/orchestration-worker-dispatch-db.test.ts src/main/runtime/orchestration/formatter.test.ts src/main/providers/ssh-pty-provider.test.ts src/main/providers/ssh-pty-write.test.ts src/main/providers/settled-pty-writer-census.test.ts src/main/runtime/orchestration/mailbox-pointer-stage.test.ts src/main/daemon/client.test.ts src/main/daemon/daemon-init-live-session-preservation.test.ts src/main/daemon/daemon-pty-router.test.ts", "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/terminal-send-stale-leaf-liveness.test.ts src/main/runtime/rpc/methods/orchestration/runs/runs.test.ts src/main/runtime/rpc/methods/orchestration/messaging/send.test.ts src/main/runtime/rpc/methods/orchestration/messaging/check.test.ts", - "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orchestration/federation-sync.test.ts src/main/runtime/rpc/methods/orchestration/federation/federation.test.ts src/main/runtime/rpc/methods/orchestration/federation/federation-lifecycle-settlement.test.ts --reporter=dot" + "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orchestration/federation-sync.test.ts src/main/runtime/rpc/methods/orchestration/federation/federation.test.ts src/main/runtime/rpc/methods/orchestration/federation/federation-lifecycle-settlement.test.ts --reporter=dot", + "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orchestration-message-delivery-identity.test.ts src/main/runtime/orchestration-mailbox-notification-consistency.test.ts src/main/runtime/orchestration-mailbox-detached-routing.test.ts src/main/runtime/orchestration-mailbox-routing-races.test.ts src/main/runtime/orchestration-mailbox-transport-settlement.test.ts src/main/ipc/pty-controller-ownership-routing.test.ts src/main/runtime/orchestration/run-coordinator-handle-migration.test.ts src/main/runtime/orchestration/orchestration-run-delivery-db.test.ts src/main/runtime/orchestration/orchestration-worker-dispatch-db.test.ts src/main/runtime/orchestration/formatter.test.ts src/main/providers/ssh-pty-provider.test.ts src/main/providers/ssh-pty-write.test.ts src/main/providers/settled-pty-writer-census.test.ts src/main/runtime/orchestration/mailbox-pointer-stage.test.ts src/main/daemon/client.test.ts src/main/daemon/daemon-init-live-session-preservation.test.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/terminal-send-stale-leaf-liveness.test.ts src/main/runtime/rpc/methods/orchestration/runs/runs.test.ts src/main/runtime/rpc/methods/orchestration/messaging/send.test.ts src/main/runtime/rpc/methods/orchestration/messaging/check.test.ts src/main/runtime/orchestration/federation-sync.test.ts src/main/runtime/rpc/methods/orchestration/federation/federation.test.ts src/main/runtime/rpc/methods/orchestration/federation/federation-lifecycle-settlement.test.ts", + "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orchestration-mailbox-routing-races.test.ts src/main/runtime/orchestration-mailbox-notification-consistency.test.ts src/main/runtime/orchestration-mailbox-detached-routing.test.ts src/main/runtime/orchestration-mailbox-transport-settlement.test.ts src/main/ipc/pty-controller-ownership-routing.test.ts src/main/runtime/orchestration/run-coordinator-handle-migration.test.ts src/main/runtime/orchestration/orchestration-run-delivery-db.test.ts src/main/runtime/orchestration/orchestration-worker-dispatch-db.test.ts src/main/runtime/orchestration/formatter.test.ts src/main/providers/ssh-pty-provider.test.ts src/main/providers/ssh-pty-write.test.ts src/main/providers/settled-pty-writer-census.test.ts src/main/runtime/orchestration/mailbox-pointer-stage.test.ts src/main/daemon/client.test.ts src/main/daemon/daemon-pty-router.test.ts src/main/providers/unavailable-pty-provider.test.ts src/main/daemon/daemon-fresh-spawn-admission.test.ts" ], "testFiles": [ "src/main/runtime/orchestration-message-delivery-identity.test.ts", @@ -15150,8 +15163,7 @@ "src/main/providers/settled-pty-writer-census.test.ts", "src/main/runtime/orchestration/mailbox-pointer-stage.test.ts", "src/main/daemon/client.test.ts", - "src/main/daemon/daemon-pty-router.test.ts", - "src/main/daemon/degraded-daemon-pty-provider.test.ts", + "src/main/daemon/daemon-init-live-session-preservation.test.ts", "src/main/runtime/orca-runtime.test.ts", "src/main/runtime/terminal-send-stale-leaf-liveness.test.ts", "src/main/runtime/rpc/methods/orchestration/runs/runs.test.ts", @@ -15295,7 +15307,7 @@ "date": "2026-09-05", "runner": "local", "platform": "macos", - "command": "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orchestration-mailbox-routing-races.test.ts src/main/runtime/orchestration-mailbox-notification-consistency.test.ts src/main/runtime/orchestration-mailbox-detached-routing.test.ts src/main/runtime/orchestration-mailbox-transport-settlement.test.ts src/main/ipc/pty-controller-ownership-routing.test.ts src/main/runtime/orchestration/run-coordinator-handle-migration.test.ts src/main/runtime/orchestration/orchestration-run-delivery-db.test.ts src/main/runtime/orchestration/orchestration-worker-dispatch-db.test.ts src/main/runtime/orchestration/formatter.test.ts src/main/providers/ssh-pty-provider.test.ts src/main/providers/ssh-pty-write.test.ts src/main/providers/settled-pty-writer-census.test.ts src/main/runtime/orchestration/mailbox-pointer-stage.test.ts src/main/daemon/client.test.ts src/main/daemon/daemon-pty-router.test.ts src/main/daemon/degraded-daemon-pty-provider.test.ts", + "command": "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orchestration-mailbox-routing-races.test.ts src/main/runtime/orchestration-mailbox-notification-consistency.test.ts src/main/runtime/orchestration-mailbox-detached-routing.test.ts src/main/runtime/orchestration-mailbox-transport-settlement.test.ts src/main/ipc/pty-controller-ownership-routing.test.ts src/main/runtime/orchestration/run-coordinator-handle-migration.test.ts src/main/runtime/orchestration/orchestration-run-delivery-db.test.ts src/main/runtime/orchestration/orchestration-worker-dispatch-db.test.ts src/main/runtime/orchestration/formatter.test.ts src/main/providers/ssh-pty-provider.test.ts src/main/providers/ssh-pty-write.test.ts src/main/providers/settled-pty-writer-census.test.ts src/main/runtime/orchestration/mailbox-pointer-stage.test.ts src/main/daemon/client.test.ts src/main/daemon/daemon-pty-router.test.ts src/main/providers/unavailable-pty-provider.test.ts src/main/daemon/daemon-fresh-spawn-admission.test.ts", "result": "passed", "durationSeconds": 4.73, "summary": "267 tests passed after the pointer-write path moved to the three-valued WriteSettlement union. New coverage: a dropped in-flight SSH settlement reaches the stager as unverifiable with bytes handed to the transport, a settled write that throws mid-pointer preserves the write-attempted reservation, an Enter whose settlement is lost stays at enter-attempted with no second Enter after restart, a refusal releases the reservation and drains a delivery parked behind its watermark, the production controller refuses before any byte when the routed provider cannot settle, and a census pins the five production IPtyProvider classes and rejects a settlement synthesized from the fire-and-forget write. Each new assertion was verified red against the pre-fix shape." @@ -15304,7 +15316,7 @@ "date": "2026-08-13", "runner": "local", "platform": "macos", - "command": "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orchestration-mailbox-routing-races.test.ts src/main/runtime/orchestration-mailbox-notification-consistency.test.ts src/main/runtime/orchestration-mailbox-detached-routing.test.ts src/main/runtime/orchestration-mailbox-transport-settlement.test.ts src/main/ipc/pty-controller-ownership-routing.test.ts src/main/runtime/orchestration/run-coordinator-handle-migration.test.ts src/main/runtime/orchestration/orchestration-run-delivery-db.test.ts src/main/runtime/orchestration/orchestration-worker-dispatch-db.test.ts src/main/runtime/orchestration/formatter.test.ts src/main/providers/ssh-pty-provider.test.ts src/main/providers/ssh-pty-write.test.ts src/main/providers/settled-pty-writer-census.test.ts src/main/runtime/orchestration/mailbox-pointer-stage.test.ts src/main/daemon/client.test.ts src/main/daemon/daemon-pty-router.test.ts src/main/daemon/degraded-daemon-pty-provider.test.ts", + "command": "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orchestration-mailbox-routing-races.test.ts src/main/runtime/orchestration-mailbox-notification-consistency.test.ts src/main/runtime/orchestration-mailbox-detached-routing.test.ts src/main/runtime/orchestration-mailbox-transport-settlement.test.ts src/main/ipc/pty-controller-ownership-routing.test.ts src/main/runtime/orchestration/run-coordinator-handle-migration.test.ts src/main/runtime/orchestration/orchestration-run-delivery-db.test.ts src/main/runtime/orchestration/orchestration-worker-dispatch-db.test.ts src/main/runtime/orchestration/formatter.test.ts src/main/providers/ssh-pty-provider.test.ts src/main/providers/ssh-pty-write.test.ts src/main/providers/settled-pty-writer-census.test.ts src/main/runtime/orchestration/mailbox-pointer-stage.test.ts src/main/daemon/client.test.ts src/main/daemon/daemon-pty-router.test.ts src/main/providers/unavailable-pty-provider.test.ts src/main/daemon/daemon-fresh-spawn-admission.test.ts", "result": "passed", "durationSeconds": 8.22, "summary": "245 tests passed across mailbox identity, durable coordinator-handle migration, insertion-time canonicalization, duplicate-free 51-row ownership branch caps, unrestricted reservation merging, direct and Dispatch pointer suppression, persisted reconciliation, 50-row paging and filtered waits, cross-PTY serialization, lifecycle fencing, bounded daemon and SSH transport settlement, outstanding Deliveries, reminted Dispatch ownership, acknowledgment, cancellation, and bounded pane lookup." @@ -18057,12 +18069,12 @@ "pnpm exec vitest run --config config/vitest.config.ts src/relay/relay-watcher-pending-setup-waiters.test.ts --reporter=dot", "pnpm exec vitest run --config config/vitest.config.ts src/relay/relay-watcher-parent-removal.test.ts --reporter=dot", "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/parcel-watcher-unsubscribe-timeout.test.ts --reporter=dot", - "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/filesystem-watcher.test.ts src/main/ipc/filesystem-watcher-native-capacity.test.ts src/main/ipc/parcel-watcher-child-registry.test.ts src/main/ipc/parcel-watcher-child-launch.test.ts src/main/ipc/parcel-watcher-process.test.ts src/main/ipc/parcel-watcher-supervisor-capacity-wait.test.ts src/main/ipc/parcel-watcher-supervisor-capacity.test.ts src/main/ipc/parcel-watcher-disconnect-termination.test.ts src/main/ipc/runtime-watcher-pending-assignment.test.ts src/main/ipc/runtime-watcher-process-pool.test.ts src/main/ipc/parcel-watcher-process-entry.test.ts src/main/ipc/parcel-watcher-event-delivery.test.ts src/main/ipc/filesystem-watcher-local-unsubscribe.test.ts src/main/ipc/filesystem-watcher-wsl.test.ts src/main/ipc/worktrees-delete-pty-teardown.test.ts src/main/ipc/worktrees-orphan-directory-cleanup.test.ts src/main/ipc/worktrees-remove-preflight.test.ts src/main/ipc/worktrees-remove-archive-hooks.test.ts src/main/ipc/watcher-removal-gate.test.ts src/main/runtime/file-watcher-host.test.ts src/main/runtime/orca-runtime-files-watch.test.ts src/main/runtime/worktree-teardown.test.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/rpc/methods/files.test.ts src/main/runtime/rpc/methods/files-watch-cancellation.test.ts src/main/providers/ssh-filesystem-provider-watch-waiters.test.ts src/main/providers/ssh-filesystem-provider.test.ts src/renderer/src/runtime/runtime-file-client.test.ts src/renderer/src/web/web-runtime-client.test.ts src/shared/cross-platform-path.test.ts src/shared/physical-exit-tracker.test.ts src/shared/promise-settlement-waiters.test.ts src/main/ipc/pty-spawn-cwd-fallback.test.ts src/main/ipc/pty-listener-teardown-and-orphans.test.ts src/main/providers/local-pty-provider-shutdown.test.ts src/main/providers/local-pty-provider-session-inventory.test.ts src/main/daemon/daemon-pty-adapter.test.ts src/main/daemon/pty-subprocess.test.ts src/main/daemon/session.test.ts src/main/daemon/terminal-host.test.ts src/main/daemon/terminal-host-session-reaping-leak.test.ts src/main/providers/ssh-pty-provider.test.ts src/relay/relay-watcher-setup-wait.test.ts src/relay/pty-handler-dispose-lifecycle.test.ts src/relay/pty-handler-shutdown-signals.test.ts src/relay/pty-handler-spawn-admission.test.ts src/relay/pty-handler-attach-replay.test.ts src/relay/pty-handler-revive.test.ts --reporter=dot", - "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/filesystem-watcher.test.ts src/main/ipc/filesystem-watcher-native-capacity.test.ts src/main/ipc/parcel-watcher-child-registry.test.ts src/main/ipc/parcel-watcher-child-launch.test.ts src/main/ipc/parcel-watcher-process.test.ts src/main/ipc/parcel-watcher-supervisor-capacity.test.ts src/main/ipc/parcel-watcher-disconnect-termination.test.ts src/main/ipc/runtime-watcher-process-pool.test.ts src/main/ipc/parcel-watcher-process-entry.test.ts src/main/ipc/parcel-watcher-event-delivery.test.ts src/main/ipc/filesystem-watcher-local-unsubscribe.test.ts src/main/ipc/filesystem-watcher-wsl.test.ts src/main/ipc/worktrees-delete-pty-teardown.test.ts src/main/ipc/worktrees-orphan-directory-cleanup.test.ts src/main/ipc/worktrees-remove-preflight.test.ts src/main/ipc/worktrees-remove-archive-hooks.test.ts src/main/ipc/watcher-removal-gate.test.ts src/main/runtime/file-watcher-host.test.ts src/main/runtime/orca-runtime-files-watch.test.ts src/main/runtime/worktree-teardown.test.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/rpc/methods/files.test.ts src/main/runtime/rpc/methods/files-watch-cancellation.test.ts src/main/providers/ssh-filesystem-provider.test.ts src/renderer/src/runtime/runtime-file-client.test.ts src/renderer/src/web/web-runtime-client.test.ts src/shared/cross-platform-path.test.ts src/shared/physical-exit-tracker.test.ts src/main/ipc/pty-spawn-cwd-fallback.test.ts src/main/ipc/pty-listener-teardown-and-orphans.test.ts src/main/providers/local-pty-provider-shutdown.test.ts src/main/providers/local-pty-provider-session-inventory.test.ts src/main/daemon/daemon-pty-adapter.test.ts src/main/daemon/pty-subprocess.test.ts src/main/daemon/session.test.ts src/main/daemon/terminal-host.test.ts src/main/daemon/terminal-host-session-reaping-leak.test.ts src/main/providers/ssh-pty-provider.test.ts src/relay/pty-handler-dispose-lifecycle.test.ts src/relay/pty-handler-shutdown-signals.test.ts src/relay/pty-handler-spawn-admission.test.ts src/relay/pty-handler-attach-replay.test.ts src/relay/pty-handler-revive.test.ts --reporter=dot", - "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/filesystem-watcher.test.ts src/main/ipc/parcel-watcher-process.test.ts src/main/ipc/parcel-watcher-disconnect-termination.test.ts src/main/ipc/runtime-watcher-process-pool.test.ts src/main/ipc/parcel-watcher-process-entry.test.ts src/main/ipc/parcel-watcher-event-delivery.test.ts src/main/ipc/filesystem-watcher-local-unsubscribe.test.ts src/main/ipc/filesystem-watcher-wsl.test.ts src/main/ipc/worktrees-delete-pty-teardown.test.ts src/main/ipc/worktrees-orphan-directory-cleanup.test.ts src/main/ipc/worktrees-remove-preflight.test.ts src/main/ipc/worktrees-remove-archive-hooks.test.ts src/main/ipc/watcher-removal-gate.test.ts src/main/runtime/file-watcher-host.test.ts src/main/runtime/orca-runtime-files-watch.test.ts src/main/runtime/worktree-teardown.test.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/rpc/methods/files.test.ts src/main/providers/ssh-filesystem-provider.test.ts src/renderer/src/runtime/runtime-file-client.test.ts src/renderer/src/web/web-runtime-client.test.ts src/shared/cross-platform-path.test.ts src/main/ipc/pty-spawn-cwd-fallback.test.ts src/main/ipc/pty-listener-teardown-and-orphans.test.ts src/main/providers/local-pty-provider-shutdown.test.ts src/main/providers/local-pty-provider-session-inventory.test.ts src/main/daemon/pty-subprocess.test.ts src/main/daemon/session.test.ts src/main/daemon/terminal-host.test.ts src/main/daemon/terminal-host-session-reaping-leak.test.ts src/main/providers/ssh-pty-provider.test.ts src/relay/pty-handler-dispose-lifecycle.test.ts src/relay/pty-handler-shutdown-signals.test.ts src/relay/pty-handler-spawn-admission.test.ts src/relay/pty-handler-attach-replay.test.ts src/relay/pty-handler-revive.test.ts --reporter=dot", - "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/parcel-watcher-process.test.ts src/main/ipc/parcel-watcher-disconnect-termination.test.ts src/main/ipc/runtime-watcher-process-pool.test.ts src/main/ipc/parcel-watcher-process-entry.test.ts src/main/ipc/parcel-watcher-event-delivery.test.ts src/main/ipc/filesystem-watcher-local-unsubscribe.test.ts src/main/ipc/filesystem-watcher-wsl.test.ts src/main/ipc/worktrees-delete-pty-teardown.test.ts src/main/ipc/worktrees-orphan-directory-cleanup.test.ts src/main/ipc/worktrees-remove-preflight.test.ts src/main/ipc/worktrees-remove-archive-hooks.test.ts src/main/ipc/watcher-removal-gate.test.ts src/main/runtime/file-watcher-host.test.ts src/main/runtime/orca-runtime-files-watch.test.ts src/main/runtime/worktree-teardown.test.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/rpc/methods/files.test.ts src/main/providers/ssh-filesystem-provider.test.ts src/renderer/src/runtime/runtime-file-client.test.ts src/renderer/src/web/web-runtime-client.test.ts src/shared/cross-platform-path.test.ts src/main/ipc/pty-spawn-cwd-fallback.test.ts src/main/ipc/pty-listener-teardown-and-orphans.test.ts src/main/providers/local-pty-provider-shutdown.test.ts src/main/providers/local-pty-provider-session-inventory.test.ts src/main/daemon/pty-subprocess.test.ts src/main/daemon/session.test.ts src/main/daemon/terminal-host.test.ts src/main/daemon/terminal-host-session-reaping-leak.test.ts src/main/providers/ssh-pty-provider.test.ts src/relay/pty-handler-dispose-lifecycle.test.ts src/relay/pty-handler-shutdown-signals.test.ts src/relay/pty-handler-spawn-admission.test.ts src/relay/pty-handler-attach-replay.test.ts src/relay/pty-handler-revive.test.ts", - "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/parcel-watcher-process.test.ts src/main/ipc/parcel-watcher-disconnect-termination.test.ts src/main/ipc/runtime-watcher-process-pool.test.ts src/main/ipc/parcel-watcher-process-entry.test.ts src/main/ipc/parcel-watcher-event-delivery.test.ts src/main/ipc/filesystem-watcher-local-unsubscribe.test.ts src/main/ipc/filesystem-watcher-wsl.test.ts src/main/ipc/worktrees-delete-pty-teardown.test.ts src/main/ipc/worktrees-orphan-directory-cleanup.test.ts src/main/ipc/worktrees-remove-preflight.test.ts src/main/ipc/worktrees-remove-archive-hooks.test.ts src/main/ipc/watcher-removal-gate.test.ts src/main/runtime/file-watcher-host.test.ts src/main/runtime/orca-runtime-files-watch.test.ts src/main/runtime/worktree-teardown.test.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/rpc/methods/files.test.ts src/main/providers/ssh-filesystem-provider.test.ts src/renderer/src/runtime/runtime-file-client.test.ts src/renderer/src/web/web-runtime-client.test.ts src/shared/cross-platform-path.test.ts src/main/ipc/pty-spawn-cwd-fallback.test.ts src/main/ipc/pty-listener-teardown-and-orphans.test.ts src/main/providers/local-pty-provider-shutdown.test.ts src/main/providers/local-pty-provider-session-inventory.test.ts src/main/daemon/session.test.ts src/main/daemon/terminal-host.test.ts src/main/daemon/terminal-host-session-reaping-leak.test.ts src/main/providers/ssh-pty-provider.test.ts src/relay/pty-handler-dispose-lifecycle.test.ts src/relay/pty-handler-shutdown-signals.test.ts src/relay/pty-handler-spawn-admission.test.ts src/relay/pty-handler-attach-replay.test.ts src/relay/pty-handler-revive.test.ts", - "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/parcel-watcher-process.test.ts src/main/ipc/parcel-watcher-disconnect-termination.test.ts src/main/ipc/runtime-watcher-process-pool.test.ts src/main/ipc/parcel-watcher-process-entry.test.ts src/main/ipc/parcel-watcher-event-delivery.test.ts src/main/ipc/filesystem-watcher-local-unsubscribe.test.ts src/main/ipc/filesystem-watcher-wsl.test.ts src/main/ipc/worktrees-delete-pty-teardown.test.ts src/main/ipc/worktrees-orphan-directory-cleanup.test.ts src/main/ipc/worktrees-remove-preflight.test.ts src/main/ipc/worktrees-remove-archive-hooks.test.ts src/main/ipc/watcher-removal-gate.test.ts src/main/runtime/file-watcher-host.test.ts src/main/runtime/orca-runtime-files-watch.test.ts src/main/runtime/worktree-teardown.test.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/rpc/methods/files.test.ts src/main/providers/ssh-filesystem-provider.test.ts src/renderer/src/runtime/runtime-file-client.test.ts src/renderer/src/web/web-runtime-client.test.ts src/shared/cross-platform-path.test.ts src/main/ipc/pty-spawn-cwd-fallback.test.ts src/main/ipc/pty-listener-teardown-and-orphans.test.ts src/main/daemon/terminal-host.test.ts src/main/daemon/terminal-host-session-reaping-leak.test.ts src/main/providers/ssh-pty-provider.test.ts src/relay/pty-handler-dispose-lifecycle.test.ts src/relay/pty-handler-shutdown-signals.test.ts src/relay/pty-handler-spawn-admission.test.ts src/relay/pty-handler-attach-replay.test.ts src/relay/pty-handler-revive.test.ts", + "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/filesystem-watcher.test.ts src/main/ipc/filesystem-watcher-native-capacity.test.ts src/main/ipc/parcel-watcher-child-registry.test.ts src/main/ipc/parcel-watcher-child-launch.test.ts src/main/ipc/parcel-watcher-process.test.ts src/main/ipc/parcel-watcher-supervisor-capacity-wait.test.ts src/main/ipc/parcel-watcher-supervisor-capacity.test.ts src/main/ipc/parcel-watcher-disconnect-termination.test.ts src/main/ipc/runtime-watcher-pending-assignment.test.ts src/main/ipc/runtime-watcher-process-pool.test.ts src/main/ipc/parcel-watcher-process-entry.test.ts src/main/ipc/parcel-watcher-event-delivery.test.ts src/main/ipc/filesystem-watcher-local-unsubscribe.test.ts src/main/ipc/filesystem-watcher-wsl.test.ts src/main/ipc/worktrees-delete-pty-teardown.test.ts src/main/ipc/worktrees-orphan-directory-cleanup.test.ts src/main/ipc/worktrees-remove-preflight.test.ts src/main/ipc/worktrees-remove-archive-hooks.test.ts src/main/ipc/watcher-removal-gate.test.ts src/main/runtime/file-watcher-host.test.ts src/main/runtime/orca-runtime-files-watch.test.ts src/main/runtime/worktree-teardown.test.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/rpc/methods/files.test.ts src/main/runtime/rpc/methods/files-watch-cancellation.test.ts src/main/providers/ssh-filesystem-provider-watch-waiters.test.ts src/main/providers/ssh-filesystem-provider.test.ts src/renderer/src/runtime/runtime-file-client.test.ts src/renderer/src/web/web-runtime-client.test.ts src/shared/cross-platform-path.test.ts src/shared/physical-exit-tracker.test.ts src/shared/promise-settlement-waiters.test.ts src/main/ipc/pty-spawn-cwd-fallback.test.ts src/main/ipc/pty-listener-teardown-and-orphans.test.ts src/main/daemon/terminal-session-teardown.test.ts src/main/daemon/daemon-pty-router.test.ts src/main/daemon/daemon-pty-adapter.test.ts src/main/daemon/pty-subprocess.test.ts src/main/daemon/session.test.ts src/main/daemon/terminal-host-concurrent-create.test.ts src/main/daemon/terminal-host-session-reaping-leak.test.ts src/main/providers/ssh-pty-provider.test.ts src/relay/relay-watcher-setup-wait.test.ts src/relay/pty-handler-dispose-lifecycle.test.ts src/relay/pty-handler-shutdown-signals.test.ts src/relay/pty-handler-spawn-admission.test.ts src/relay/pty-handler-attach-replay.test.ts src/relay/pty-handler-revive.test.ts --reporter=dot", + "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/filesystem-watcher.test.ts src/main/ipc/filesystem-watcher-native-capacity.test.ts src/main/ipc/parcel-watcher-child-registry.test.ts src/main/ipc/parcel-watcher-child-launch.test.ts src/main/ipc/parcel-watcher-process.test.ts src/main/ipc/parcel-watcher-supervisor-capacity.test.ts src/main/ipc/parcel-watcher-disconnect-termination.test.ts src/main/ipc/runtime-watcher-process-pool.test.ts src/main/ipc/parcel-watcher-process-entry.test.ts src/main/ipc/parcel-watcher-event-delivery.test.ts src/main/ipc/filesystem-watcher-local-unsubscribe.test.ts src/main/ipc/filesystem-watcher-wsl.test.ts src/main/ipc/worktrees-delete-pty-teardown.test.ts src/main/ipc/worktrees-orphan-directory-cleanup.test.ts src/main/ipc/worktrees-remove-preflight.test.ts src/main/ipc/worktrees-remove-archive-hooks.test.ts src/main/ipc/watcher-removal-gate.test.ts src/main/runtime/file-watcher-host.test.ts src/main/runtime/orca-runtime-files-watch.test.ts src/main/runtime/worktree-teardown.test.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/rpc/methods/files.test.ts src/main/runtime/rpc/methods/files-watch-cancellation.test.ts src/main/providers/ssh-filesystem-provider.test.ts src/renderer/src/runtime/runtime-file-client.test.ts src/renderer/src/web/web-runtime-client.test.ts src/shared/cross-platform-path.test.ts src/shared/physical-exit-tracker.test.ts src/main/ipc/pty-spawn-cwd-fallback.test.ts src/main/ipc/pty-listener-teardown-and-orphans.test.ts src/main/daemon/terminal-session-teardown.test.ts src/main/daemon/daemon-pty-router.test.ts src/main/daemon/daemon-pty-adapter.test.ts src/main/daemon/pty-subprocess.test.ts src/main/daemon/session.test.ts src/main/daemon/terminal-host-concurrent-create.test.ts src/main/daemon/terminal-host-session-reaping-leak.test.ts src/main/providers/ssh-pty-provider.test.ts src/relay/pty-handler-dispose-lifecycle.test.ts src/relay/pty-handler-shutdown-signals.test.ts src/relay/pty-handler-spawn-admission.test.ts src/relay/pty-handler-attach-replay.test.ts src/relay/pty-handler-revive.test.ts --reporter=dot", + "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/filesystem-watcher.test.ts src/main/ipc/parcel-watcher-process.test.ts src/main/ipc/parcel-watcher-disconnect-termination.test.ts src/main/ipc/runtime-watcher-process-pool.test.ts src/main/ipc/parcel-watcher-process-entry.test.ts src/main/ipc/parcel-watcher-event-delivery.test.ts src/main/ipc/filesystem-watcher-local-unsubscribe.test.ts src/main/ipc/filesystem-watcher-wsl.test.ts src/main/ipc/worktrees-delete-pty-teardown.test.ts src/main/ipc/worktrees-orphan-directory-cleanup.test.ts src/main/ipc/worktrees-remove-preflight.test.ts src/main/ipc/worktrees-remove-archive-hooks.test.ts src/main/ipc/watcher-removal-gate.test.ts src/main/runtime/file-watcher-host.test.ts src/main/runtime/orca-runtime-files-watch.test.ts src/main/runtime/worktree-teardown.test.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/rpc/methods/files.test.ts src/main/providers/ssh-filesystem-provider.test.ts src/renderer/src/runtime/runtime-file-client.test.ts src/renderer/src/web/web-runtime-client.test.ts src/shared/cross-platform-path.test.ts src/main/ipc/pty-spawn-cwd-fallback.test.ts src/main/ipc/pty-listener-teardown-and-orphans.test.ts src/main/daemon/terminal-session-teardown.test.ts src/main/daemon/daemon-pty-router.test.ts src/main/daemon/pty-subprocess.test.ts src/main/daemon/session.test.ts src/main/daemon/terminal-host-concurrent-create.test.ts src/main/daemon/terminal-host-session-reaping-leak.test.ts src/main/providers/ssh-pty-provider.test.ts src/relay/pty-handler-dispose-lifecycle.test.ts src/relay/pty-handler-shutdown-signals.test.ts src/relay/pty-handler-spawn-admission.test.ts src/relay/pty-handler-attach-replay.test.ts src/relay/pty-handler-revive.test.ts --reporter=dot", + "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/parcel-watcher-process.test.ts src/main/ipc/parcel-watcher-disconnect-termination.test.ts src/main/ipc/runtime-watcher-process-pool.test.ts src/main/ipc/parcel-watcher-process-entry.test.ts src/main/ipc/parcel-watcher-event-delivery.test.ts src/main/ipc/filesystem-watcher-local-unsubscribe.test.ts src/main/ipc/filesystem-watcher-wsl.test.ts src/main/ipc/worktrees-delete-pty-teardown.test.ts src/main/ipc/worktrees-orphan-directory-cleanup.test.ts src/main/ipc/worktrees-remove-preflight.test.ts src/main/ipc/worktrees-remove-archive-hooks.test.ts src/main/ipc/watcher-removal-gate.test.ts src/main/runtime/file-watcher-host.test.ts src/main/runtime/orca-runtime-files-watch.test.ts src/main/runtime/worktree-teardown.test.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/rpc/methods/files.test.ts src/main/providers/ssh-filesystem-provider.test.ts src/renderer/src/runtime/runtime-file-client.test.ts src/renderer/src/web/web-runtime-client.test.ts src/shared/cross-platform-path.test.ts src/main/ipc/pty-spawn-cwd-fallback.test.ts src/main/ipc/pty-listener-teardown-and-orphans.test.ts src/main/daemon/terminal-session-teardown.test.ts src/main/daemon/daemon-pty-router.test.ts src/main/daemon/pty-subprocess.test.ts src/main/daemon/session.test.ts src/main/daemon/terminal-host-concurrent-create.test.ts src/main/daemon/terminal-host-session-reaping-leak.test.ts src/main/providers/ssh-pty-provider.test.ts src/relay/pty-handler-dispose-lifecycle.test.ts src/relay/pty-handler-shutdown-signals.test.ts src/relay/pty-handler-spawn-admission.test.ts src/relay/pty-handler-attach-replay.test.ts src/relay/pty-handler-revive.test.ts", + "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/parcel-watcher-process.test.ts src/main/ipc/parcel-watcher-disconnect-termination.test.ts src/main/ipc/runtime-watcher-process-pool.test.ts src/main/ipc/parcel-watcher-process-entry.test.ts src/main/ipc/parcel-watcher-event-delivery.test.ts src/main/ipc/filesystem-watcher-local-unsubscribe.test.ts src/main/ipc/filesystem-watcher-wsl.test.ts src/main/ipc/worktrees-delete-pty-teardown.test.ts src/main/ipc/worktrees-orphan-directory-cleanup.test.ts src/main/ipc/worktrees-remove-preflight.test.ts src/main/ipc/worktrees-remove-archive-hooks.test.ts src/main/ipc/watcher-removal-gate.test.ts src/main/runtime/file-watcher-host.test.ts src/main/runtime/orca-runtime-files-watch.test.ts src/main/runtime/worktree-teardown.test.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/rpc/methods/files.test.ts src/main/providers/ssh-filesystem-provider.test.ts src/renderer/src/runtime/runtime-file-client.test.ts src/renderer/src/web/web-runtime-client.test.ts src/shared/cross-platform-path.test.ts src/main/ipc/pty-spawn-cwd-fallback.test.ts src/main/ipc/pty-listener-teardown-and-orphans.test.ts src/main/daemon/terminal-session-teardown.test.ts src/main/daemon/daemon-pty-router.test.ts src/main/daemon/session.test.ts src/main/daemon/terminal-host-concurrent-create.test.ts src/main/daemon/terminal-host-session-reaping-leak.test.ts src/main/providers/ssh-pty-provider.test.ts src/relay/pty-handler-dispose-lifecycle.test.ts src/relay/pty-handler-shutdown-signals.test.ts src/relay/pty-handler-spawn-admission.test.ts src/relay/pty-handler-attach-replay.test.ts src/relay/pty-handler-revive.test.ts", + "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/parcel-watcher-process.test.ts src/main/ipc/parcel-watcher-disconnect-termination.test.ts src/main/ipc/runtime-watcher-process-pool.test.ts src/main/ipc/parcel-watcher-process-entry.test.ts src/main/ipc/parcel-watcher-event-delivery.test.ts src/main/ipc/filesystem-watcher-local-unsubscribe.test.ts src/main/ipc/filesystem-watcher-wsl.test.ts src/main/ipc/worktrees-delete-pty-teardown.test.ts src/main/ipc/worktrees-orphan-directory-cleanup.test.ts src/main/ipc/worktrees-remove-preflight.test.ts src/main/ipc/worktrees-remove-archive-hooks.test.ts src/main/ipc/watcher-removal-gate.test.ts src/main/runtime/file-watcher-host.test.ts src/main/runtime/orca-runtime-files-watch.test.ts src/main/runtime/worktree-teardown.test.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/rpc/methods/files.test.ts src/main/providers/ssh-filesystem-provider.test.ts src/renderer/src/runtime/runtime-file-client.test.ts src/renderer/src/web/web-runtime-client.test.ts src/shared/cross-platform-path.test.ts src/main/ipc/pty-spawn-cwd-fallback.test.ts src/main/ipc/pty-listener-teardown-and-orphans.test.ts src/main/daemon/terminal-host-concurrent-create.test.ts src/main/daemon/terminal-host-session-reaping-leak.test.ts src/main/providers/ssh-pty-provider.test.ts src/relay/pty-handler-dispose-lifecycle.test.ts src/relay/pty-handler-shutdown-signals.test.ts src/relay/pty-handler-spawn-admission.test.ts src/relay/pty-handler-attach-replay.test.ts src/relay/pty-handler-revive.test.ts", "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/parcel-watcher-process.test.ts src/main/ipc/parcel-watcher-disconnect-termination.test.ts src/main/ipc/runtime-watcher-process-pool.test.ts src/main/ipc/parcel-watcher-process-entry.test.ts src/main/ipc/parcel-watcher-event-delivery.test.ts src/main/ipc/filesystem-watcher-local-unsubscribe.test.ts src/main/ipc/filesystem-watcher-wsl.test.ts src/main/ipc/worktrees-delete-pty-teardown.test.ts src/main/ipc/worktrees-orphan-directory-cleanup.test.ts src/main/ipc/worktrees-remove-preflight.test.ts src/main/ipc/worktrees-remove-archive-hooks.test.ts src/main/ipc/watcher-removal-gate.test.ts src/main/runtime/file-watcher-host.test.ts src/main/runtime/orca-runtime-files-watch.test.ts src/main/runtime/worktree-teardown.test.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/rpc/methods/files.test.ts src/main/providers/ssh-filesystem-provider.test.ts src/renderer/src/runtime/runtime-file-client.test.ts src/renderer/src/web/web-runtime-client.test.ts src/shared/cross-platform-path.test.ts", "pnpm run build:electron-vite", "node config/scripts/runtime-file-watcher-resource-probe.mjs", @@ -18071,7 +18083,13 @@ "pnpm exec vitest run --config config/vitest.config.ts src/relay/relay-watcher-setup-wait.test.ts src/relay/relay-filesystem-watch-registry.test.ts src/relay/fs-handler.test.ts src/relay/git-handler.test.ts src/relay/subprocess.test.ts src/main/ssh/ssh-relay-versioned-install.test.ts config/scripts/package-electron-runtime-contract.test.mjs", "pnpm exec vitest run --config config/vitest.config.ts src/relay/relay-filesystem-watch-registry.test.ts src/relay/fs-handler.test.ts src/relay/git-handler.test.ts src/relay/subprocess.test.ts src/main/ssh/ssh-relay-versioned-install.test.ts config/scripts/package-electron-runtime-contract.test.mjs", "pnpm run build:relay", - "node config/scripts/relay-watcher-fault-harness.mjs" + "node config/scripts/relay-watcher-fault-harness.mjs", + "pnpm exec vitest run --config config/vitest.config.ts src/main/daemon/daemon-pty-adapter.test.ts src/main/ipc/filesystem-watcher.test.ts src/main/ipc/filesystem-watcher-native-capacity.test.ts src/main/ipc/parcel-watcher-child-registry.test.ts src/main/ipc/parcel-watcher-child-launch.test.ts src/main/ipc/parcel-watcher-process.test.ts src/main/ipc/parcel-watcher-unsubscribe-timeout.test.ts src/main/ipc/parcel-watcher-supervisor-capacity-wait.test.ts src/main/ipc/parcel-watcher-supervisor-capacity.test.ts src/main/ipc/parcel-watcher-disconnect-termination.test.ts src/main/ipc/runtime-watcher-pending-assignment.test.ts src/main/ipc/runtime-watcher-process-pool.test.ts src/main/ipc/parcel-watcher-process-entry.test.ts src/main/ipc/parcel-watcher-event-delivery.test.ts src/main/ipc/filesystem-watcher-local-unsubscribe.test.ts src/main/ipc/filesystem-watcher-wsl.test.ts src/main/ipc/worktrees-delete-pty-teardown.test.ts src/main/ipc/worktrees-orphan-directory-cleanup.test.ts src/main/ipc/worktrees-remove-preflight.test.ts src/main/ipc/worktrees-remove-archive-hooks.test.ts src/main/ipc/watcher-removal-gate.test.ts src/main/ipc/pty-spawn-cwd-fallback.test.ts src/main/ipc/pty-listener-teardown-and-orphans.test.ts src/main/daemon/terminal-host-concurrent-create.test.ts src/main/daemon/pty-subprocess.test.ts src/main/daemon/session.test.ts src/main/daemon/terminal-host-session-reaping-leak.test.ts src/main/runtime/file-watcher-host.test.ts src/main/runtime/orca-runtime-files-watch.test.ts src/main/runtime/worktree-teardown.test.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/rpc/methods/files.test.ts src/main/runtime/rpc/methods/files-watch-cancellation.test.ts src/main/runtime/rpc/methods/files-watch-cleanup.test.ts src/main/providers/ssh-filesystem-provider.test.ts src/main/providers/ssh-filesystem-provider-watch-waiters.test.ts src/main/providers/ssh-pty-provider.test.ts src/renderer/src/runtime/runtime-file-client.test.ts src/renderer/src/web/web-runtime-client.test.ts src/shared/cross-platform-path.test.ts src/shared/physical-exit-tracker.test.ts src/shared/promise-settlement-waiters.test.ts config/scripts/runtime-file-watcher-fault-harness.mjs config/scripts/runtime-file-watcher-resource-probe.mjs src/relay/relay-filesystem-watch-registry.test.ts src/relay/relay-watcher-pending-setup-waiters.test.ts src/relay/relay-watcher-parent-removal.test.ts src/relay/relay-watcher-setup-wait.test.ts src/relay/fs-handler.test.ts src/relay/pty-handler-dispose-lifecycle.test.ts src/relay/pty-handler-shutdown-signals.test.ts src/relay/pty-handler-spawn-admission.test.ts src/relay/pty-handler-attach-replay.test.ts src/relay/pty-handler-revive.test.ts src/relay/git-handler.test.ts src/relay/subprocess.test.ts src/main/ssh/ssh-relay-versioned-install.test.ts config/scripts/package-electron-runtime-contract.test.mjs config/scripts/relay-watcher-fault-harness.mjs", + "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/filesystem-watcher.test.ts src/main/ipc/filesystem-watcher-native-capacity.test.ts src/main/ipc/parcel-watcher-child-registry.test.ts src/main/ipc/parcel-watcher-child-launch.test.ts src/main/ipc/parcel-watcher-process.test.ts src/main/ipc/parcel-watcher-supervisor-capacity-wait.test.ts src/main/ipc/parcel-watcher-supervisor-capacity.test.ts src/main/ipc/parcel-watcher-disconnect-termination.test.ts src/main/ipc/runtime-watcher-pending-assignment.test.ts src/main/ipc/runtime-watcher-process-pool.test.ts src/main/ipc/parcel-watcher-process-entry.test.ts src/main/ipc/parcel-watcher-event-delivery.test.ts src/main/ipc/filesystem-watcher-local-unsubscribe.test.ts src/main/ipc/filesystem-watcher-wsl.test.ts src/main/ipc/worktrees-delete-pty-teardown.test.ts src/main/ipc/worktrees-orphan-directory-cleanup.test.ts src/main/ipc/worktrees-remove-preflight.test.ts src/main/ipc/worktrees-remove-archive-hooks.test.ts src/main/ipc/watcher-removal-gate.test.ts src/main/runtime/file-watcher-host.test.ts src/main/runtime/orca-runtime-files-watch.test.ts src/main/runtime/worktree-teardown.test.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/rpc/methods/files.test.ts src/main/runtime/rpc/methods/files-watch-cancellation.test.ts src/main/providers/ssh-filesystem-provider-watch-waiters.test.ts src/main/providers/ssh-filesystem-provider.test.ts src/renderer/src/runtime/runtime-file-client.test.ts src/renderer/src/web/web-runtime-client.test.ts src/shared/cross-platform-path.test.ts src/shared/physical-exit-tracker.test.ts src/shared/promise-settlement-waiters.test.ts src/main/ipc/pty-spawn-cwd-fallback.test.ts src/main/ipc/pty-listener-teardown-and-orphans.test.ts src/main/daemon/terminal-descendant-shutdown.test.ts src/main/daemon/daemon-pty-adapter-inventory-respawn.test.ts src/main/daemon/daemon-pty-adapter.test.ts src/main/daemon/pty-subprocess.test.ts src/main/daemon/session.test.ts src/main/daemon/terminal-host.test.ts src/main/daemon/terminal-host-session-reaping-leak.test.ts src/main/providers/ssh-pty-provider.test.ts src/relay/relay-watcher-setup-wait.test.ts src/relay/pty-handler-dispose-lifecycle.test.ts src/relay/pty-handler-shutdown-signals.test.ts src/relay/pty-handler-spawn-admission.test.ts src/relay/pty-handler-attach-replay.test.ts src/relay/pty-handler-revive.test.ts --reporter=dot", + "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/filesystem-watcher.test.ts src/main/ipc/filesystem-watcher-native-capacity.test.ts src/main/ipc/parcel-watcher-child-registry.test.ts src/main/ipc/parcel-watcher-child-launch.test.ts src/main/ipc/parcel-watcher-process.test.ts src/main/ipc/parcel-watcher-supervisor-capacity.test.ts src/main/ipc/parcel-watcher-disconnect-termination.test.ts src/main/ipc/runtime-watcher-process-pool.test.ts src/main/ipc/parcel-watcher-process-entry.test.ts src/main/ipc/parcel-watcher-event-delivery.test.ts src/main/ipc/filesystem-watcher-local-unsubscribe.test.ts src/main/ipc/filesystem-watcher-wsl.test.ts src/main/ipc/worktrees-delete-pty-teardown.test.ts src/main/ipc/worktrees-orphan-directory-cleanup.test.ts src/main/ipc/worktrees-remove-preflight.test.ts src/main/ipc/worktrees-remove-archive-hooks.test.ts src/main/ipc/watcher-removal-gate.test.ts src/main/runtime/file-watcher-host.test.ts src/main/runtime/orca-runtime-files-watch.test.ts src/main/runtime/worktree-teardown.test.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/rpc/methods/files.test.ts src/main/runtime/rpc/methods/files-watch-cancellation.test.ts src/main/providers/ssh-filesystem-provider.test.ts src/renderer/src/runtime/runtime-file-client.test.ts src/renderer/src/web/web-runtime-client.test.ts src/shared/cross-platform-path.test.ts src/shared/physical-exit-tracker.test.ts src/main/ipc/pty-spawn-cwd-fallback.test.ts src/main/ipc/pty-listener-teardown-and-orphans.test.ts src/main/daemon/terminal-descendant-shutdown.test.ts src/main/daemon/daemon-pty-adapter-inventory-respawn.test.ts src/main/daemon/daemon-pty-adapter.test.ts src/main/daemon/pty-subprocess.test.ts src/main/daemon/session.test.ts src/main/daemon/terminal-host.test.ts src/main/daemon/terminal-host-session-reaping-leak.test.ts src/main/providers/ssh-pty-provider.test.ts src/relay/pty-handler-dispose-lifecycle.test.ts src/relay/pty-handler-shutdown-signals.test.ts src/relay/pty-handler-spawn-admission.test.ts src/relay/pty-handler-attach-replay.test.ts src/relay/pty-handler-revive.test.ts --reporter=dot", + "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/filesystem-watcher.test.ts src/main/ipc/parcel-watcher-process.test.ts src/main/ipc/parcel-watcher-disconnect-termination.test.ts src/main/ipc/runtime-watcher-process-pool.test.ts src/main/ipc/parcel-watcher-process-entry.test.ts src/main/ipc/parcel-watcher-event-delivery.test.ts src/main/ipc/filesystem-watcher-local-unsubscribe.test.ts src/main/ipc/filesystem-watcher-wsl.test.ts src/main/ipc/worktrees-delete-pty-teardown.test.ts src/main/ipc/worktrees-orphan-directory-cleanup.test.ts src/main/ipc/worktrees-remove-preflight.test.ts src/main/ipc/worktrees-remove-archive-hooks.test.ts src/main/ipc/watcher-removal-gate.test.ts src/main/runtime/file-watcher-host.test.ts src/main/runtime/orca-runtime-files-watch.test.ts src/main/runtime/worktree-teardown.test.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/rpc/methods/files.test.ts src/main/providers/ssh-filesystem-provider.test.ts src/renderer/src/runtime/runtime-file-client.test.ts src/renderer/src/web/web-runtime-client.test.ts src/shared/cross-platform-path.test.ts src/main/ipc/pty-spawn-cwd-fallback.test.ts src/main/ipc/pty-listener-teardown-and-orphans.test.ts src/main/daemon/terminal-descendant-shutdown.test.ts src/main/daemon/daemon-pty-adapter-inventory-respawn.test.ts src/main/daemon/pty-subprocess.test.ts src/main/daemon/session.test.ts src/main/daemon/terminal-host.test.ts src/main/daemon/terminal-host-session-reaping-leak.test.ts src/main/providers/ssh-pty-provider.test.ts src/relay/pty-handler-dispose-lifecycle.test.ts src/relay/pty-handler-shutdown-signals.test.ts src/relay/pty-handler-spawn-admission.test.ts src/relay/pty-handler-attach-replay.test.ts src/relay/pty-handler-revive.test.ts --reporter=dot", + "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/parcel-watcher-process.test.ts src/main/ipc/parcel-watcher-disconnect-termination.test.ts src/main/ipc/runtime-watcher-process-pool.test.ts src/main/ipc/parcel-watcher-process-entry.test.ts src/main/ipc/parcel-watcher-event-delivery.test.ts src/main/ipc/filesystem-watcher-local-unsubscribe.test.ts src/main/ipc/filesystem-watcher-wsl.test.ts src/main/ipc/worktrees-delete-pty-teardown.test.ts src/main/ipc/worktrees-orphan-directory-cleanup.test.ts src/main/ipc/worktrees-remove-preflight.test.ts src/main/ipc/worktrees-remove-archive-hooks.test.ts src/main/ipc/watcher-removal-gate.test.ts src/main/runtime/file-watcher-host.test.ts src/main/runtime/orca-runtime-files-watch.test.ts src/main/runtime/worktree-teardown.test.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/rpc/methods/files.test.ts src/main/providers/ssh-filesystem-provider.test.ts src/renderer/src/runtime/runtime-file-client.test.ts src/renderer/src/web/web-runtime-client.test.ts src/shared/cross-platform-path.test.ts src/main/ipc/pty-spawn-cwd-fallback.test.ts src/main/ipc/pty-listener-teardown-and-orphans.test.ts src/main/daemon/terminal-descendant-shutdown.test.ts src/main/daemon/daemon-pty-adapter-inventory-respawn.test.ts src/main/daemon/session.test.ts src/main/daemon/terminal-host.test.ts src/main/daemon/terminal-host-session-reaping-leak.test.ts src/main/providers/ssh-pty-provider.test.ts src/relay/pty-handler-dispose-lifecycle.test.ts src/relay/pty-handler-shutdown-signals.test.ts src/relay/pty-handler-spawn-admission.test.ts src/relay/pty-handler-attach-replay.test.ts src/relay/pty-handler-revive.test.ts", + "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/parcel-watcher-process.test.ts src/main/ipc/parcel-watcher-disconnect-termination.test.ts src/main/ipc/runtime-watcher-process-pool.test.ts src/main/ipc/parcel-watcher-process-entry.test.ts src/main/ipc/parcel-watcher-event-delivery.test.ts src/main/ipc/filesystem-watcher-local-unsubscribe.test.ts src/main/ipc/filesystem-watcher-wsl.test.ts src/main/ipc/worktrees-delete-pty-teardown.test.ts src/main/ipc/worktrees-orphan-directory-cleanup.test.ts src/main/ipc/worktrees-remove-preflight.test.ts src/main/ipc/worktrees-remove-archive-hooks.test.ts src/main/ipc/watcher-removal-gate.test.ts src/main/runtime/file-watcher-host.test.ts src/main/runtime/orca-runtime-files-watch.test.ts src/main/runtime/worktree-teardown.test.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/rpc/methods/files.test.ts src/main/providers/ssh-filesystem-provider.test.ts src/renderer/src/runtime/runtime-file-client.test.ts src/renderer/src/web/web-runtime-client.test.ts src/shared/cross-platform-path.test.ts src/main/ipc/pty-spawn-cwd-fallback.test.ts src/main/ipc/pty-listener-teardown-and-orphans.test.ts src/main/daemon/terminal-host.test.ts src/main/daemon/terminal-host-session-reaping-leak.test.ts src/main/providers/ssh-pty-provider.test.ts src/relay/pty-handler-dispose-lifecycle.test.ts src/relay/pty-handler-shutdown-signals.test.ts src/relay/pty-handler-spawn-admission.test.ts src/relay/pty-handler-attach-replay.test.ts src/relay/pty-handler-revive.test.ts" ], "testFiles": [ "src/main/daemon/daemon-pty-adapter.test.ts", @@ -18097,11 +18115,9 @@ "src/main/ipc/watcher-removal-gate.test.ts", "src/main/ipc/pty-spawn-cwd-fallback.test.ts", "src/main/ipc/pty-listener-teardown-and-orphans.test.ts", - "src/main/providers/local-pty-provider-shutdown.test.ts", - "src/main/providers/local-pty-provider-session-inventory.test.ts", + "src/main/daemon/terminal-host-concurrent-create.test.ts", "src/main/daemon/pty-subprocess.test.ts", "src/main/daemon/session.test.ts", - "src/main/daemon/terminal-host.test.ts", "src/main/daemon/terminal-host-session-reaping-leak.test.ts", "src/main/runtime/file-watcher-host.test.ts", "src/main/runtime/orca-runtime-files-watch.test.ts", @@ -18330,20 +18346,17 @@ ] }, { - "file": "src/main/providers/local-pty-provider-session-inventory.test.ts", - "assertions": [ - "local process enumeration retains the spawn cwd for destructive ownership fallback" - ] + "file": "src/main/daemon/daemon-pty-adapter.test.ts", + "assertions": ["returns the cwd passed at spawn time"] }, { - "file": "src/main/providers/local-pty-provider-shutdown.test.ts", + "file": "src/main/daemon/terminal-host-concurrent-create.test.ts", "assertions": [ - "local shutdown remains pending and owned until node-pty reports physical exit", - "destructive cleanup escalates an existing graceful shutdown without losing its shared exit proof", - "POSIX graceful shutdown escalates to one bounded SIGKILL when the child ignores SIGTERM", - "Windows graceful-to-destructive cleanup treats the first ConPTY kill as final and never double-closes the handle", - "orphan cleanup racing immediate shutdown shares one kill and remains owned until the queued physical exit", - "timeout and kill failure preserve ownership for a later successful retry" + "escalates an already-graceful termination and joins its physical exit", + "retains an immediate-kill session when physical exit times out", + "coalesces duplicate immediate kill while descendant capture is pending", + "keeps a naturally-exited id reserved until teardown finishes without re-killing its pid", + "upgrades a pending graceful agent teardown when immediate kill arrives" ] }, { @@ -18360,7 +18373,7 @@ ] }, { - "file": "src/main/daemon/terminal-host.test.ts", + "file": "src/main/daemon/terminal-host-concurrent-create.test.ts", "assertions": [ "immediate daemon kill remains pending and listed until the subprocess exit callback", "an unconfirmed immediate kill rejects at the bounded deadline, retains ownership, and blocks same-id replacement" @@ -18594,7 +18607,7 @@ "date": "2026-07-13", "runner": "local", "platform": "macos", - "command": "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/filesystem-watcher.test.ts src/main/ipc/filesystem-watcher-native-capacity.test.ts src/main/ipc/parcel-watcher-child-registry.test.ts src/main/ipc/parcel-watcher-child-launch.test.ts src/main/ipc/parcel-watcher-process.test.ts src/main/ipc/parcel-watcher-supervisor-capacity-wait.test.ts src/main/ipc/parcel-watcher-supervisor-capacity.test.ts src/main/ipc/parcel-watcher-disconnect-termination.test.ts src/main/ipc/runtime-watcher-pending-assignment.test.ts src/main/ipc/runtime-watcher-process-pool.test.ts src/main/ipc/parcel-watcher-process-entry.test.ts src/main/ipc/parcel-watcher-event-delivery.test.ts src/main/ipc/filesystem-watcher-local-unsubscribe.test.ts src/main/ipc/filesystem-watcher-wsl.test.ts src/main/ipc/worktrees-delete-pty-teardown.test.ts src/main/ipc/worktrees-orphan-directory-cleanup.test.ts src/main/ipc/worktrees-remove-preflight.test.ts src/main/ipc/worktrees-remove-archive-hooks.test.ts src/main/ipc/watcher-removal-gate.test.ts src/main/runtime/file-watcher-host.test.ts src/main/runtime/orca-runtime-files-watch.test.ts src/main/runtime/worktree-teardown.test.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/rpc/methods/files.test.ts src/main/runtime/rpc/methods/files-watch-cancellation.test.ts src/main/providers/ssh-filesystem-provider-watch-waiters.test.ts src/main/providers/ssh-filesystem-provider.test.ts src/renderer/src/runtime/runtime-file-client.test.ts src/renderer/src/web/web-runtime-client.test.ts src/shared/cross-platform-path.test.ts src/shared/physical-exit-tracker.test.ts src/shared/promise-settlement-waiters.test.ts src/main/ipc/pty-spawn-cwd-fallback.test.ts src/main/ipc/pty-listener-teardown-and-orphans.test.ts src/main/providers/local-pty-provider-shutdown.test.ts src/main/providers/local-pty-provider-session-inventory.test.ts src/main/daemon/daemon-pty-adapter.test.ts src/main/daemon/pty-subprocess.test.ts src/main/daemon/session.test.ts src/main/daemon/terminal-host.test.ts src/main/daemon/terminal-host-session-reaping-leak.test.ts src/main/providers/ssh-pty-provider.test.ts src/relay/relay-watcher-setup-wait.test.ts src/relay/pty-handler-dispose-lifecycle.test.ts src/relay/pty-handler-shutdown-signals.test.ts src/relay/pty-handler-spawn-admission.test.ts src/relay/pty-handler-attach-replay.test.ts src/relay/pty-handler-revive.test.ts --reporter=dot", + "command": "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/filesystem-watcher.test.ts src/main/ipc/filesystem-watcher-native-capacity.test.ts src/main/ipc/parcel-watcher-child-registry.test.ts src/main/ipc/parcel-watcher-child-launch.test.ts src/main/ipc/parcel-watcher-process.test.ts src/main/ipc/parcel-watcher-supervisor-capacity-wait.test.ts src/main/ipc/parcel-watcher-supervisor-capacity.test.ts src/main/ipc/parcel-watcher-disconnect-termination.test.ts src/main/ipc/runtime-watcher-pending-assignment.test.ts src/main/ipc/runtime-watcher-process-pool.test.ts src/main/ipc/parcel-watcher-process-entry.test.ts src/main/ipc/parcel-watcher-event-delivery.test.ts src/main/ipc/filesystem-watcher-local-unsubscribe.test.ts src/main/ipc/filesystem-watcher-wsl.test.ts src/main/ipc/worktrees-delete-pty-teardown.test.ts src/main/ipc/worktrees-orphan-directory-cleanup.test.ts src/main/ipc/worktrees-remove-preflight.test.ts src/main/ipc/worktrees-remove-archive-hooks.test.ts src/main/ipc/watcher-removal-gate.test.ts src/main/runtime/file-watcher-host.test.ts src/main/runtime/orca-runtime-files-watch.test.ts src/main/runtime/worktree-teardown.test.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/rpc/methods/files.test.ts src/main/runtime/rpc/methods/files-watch-cancellation.test.ts src/main/providers/ssh-filesystem-provider-watch-waiters.test.ts src/main/providers/ssh-filesystem-provider.test.ts src/renderer/src/runtime/runtime-file-client.test.ts src/renderer/src/web/web-runtime-client.test.ts src/shared/cross-platform-path.test.ts src/shared/physical-exit-tracker.test.ts src/shared/promise-settlement-waiters.test.ts src/main/ipc/pty-spawn-cwd-fallback.test.ts src/main/ipc/pty-listener-teardown-and-orphans.test.ts src/main/daemon/terminal-descendant-shutdown.test.ts src/main/daemon/daemon-pty-adapter-inventory-respawn.test.ts src/main/daemon/daemon-pty-adapter.test.ts src/main/daemon/pty-subprocess.test.ts src/main/daemon/session.test.ts src/main/daemon/terminal-host.test.ts src/main/daemon/terminal-host-session-reaping-leak.test.ts src/main/providers/ssh-pty-provider.test.ts src/relay/relay-watcher-setup-wait.test.ts src/relay/pty-handler-dispose-lifecycle.test.ts src/relay/pty-handler-shutdown-signals.test.ts src/relay/pty-handler-spawn-admission.test.ts src/relay/pty-handler-attach-replay.test.ts src/relay/pty-handler-revive.test.ts --reporter=dot", "result": "passed", "durationSeconds": 11.98, "summary": "Thirty-nine watcher, cancellation, desktop/serve, paired-web, WSL, SSH/relay, removal, and PTY physical-exit files passed with 2,008 tests. Capacity, quarantine, runtime-root, relay, and SSH setup layers each prove that 10,000 aborted callers detach while one anchor remains and zero waiters remain after settlement." @@ -18603,7 +18616,7 @@ "date": "2026-07-13", "runner": "local", "platform": "macos", - "command": "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/filesystem-watcher.test.ts src/main/ipc/filesystem-watcher-native-capacity.test.ts src/main/ipc/parcel-watcher-child-registry.test.ts src/main/ipc/parcel-watcher-child-launch.test.ts src/main/ipc/parcel-watcher-process.test.ts src/main/ipc/parcel-watcher-supervisor-capacity.test.ts src/main/ipc/parcel-watcher-disconnect-termination.test.ts src/main/ipc/runtime-watcher-process-pool.test.ts src/main/ipc/parcel-watcher-process-entry.test.ts src/main/ipc/parcel-watcher-event-delivery.test.ts src/main/ipc/filesystem-watcher-local-unsubscribe.test.ts src/main/ipc/filesystem-watcher-wsl.test.ts src/main/ipc/worktrees-delete-pty-teardown.test.ts src/main/ipc/worktrees-orphan-directory-cleanup.test.ts src/main/ipc/worktrees-remove-preflight.test.ts src/main/ipc/worktrees-remove-archive-hooks.test.ts src/main/ipc/watcher-removal-gate.test.ts src/main/runtime/file-watcher-host.test.ts src/main/runtime/orca-runtime-files-watch.test.ts src/main/runtime/worktree-teardown.test.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/rpc/methods/files.test.ts src/main/runtime/rpc/methods/files-watch-cancellation.test.ts src/main/providers/ssh-filesystem-provider.test.ts src/renderer/src/runtime/runtime-file-client.test.ts src/renderer/src/web/web-runtime-client.test.ts src/shared/cross-platform-path.test.ts src/shared/physical-exit-tracker.test.ts src/main/ipc/pty-spawn-cwd-fallback.test.ts src/main/ipc/pty-listener-teardown-and-orphans.test.ts src/main/providers/local-pty-provider-shutdown.test.ts src/main/providers/local-pty-provider-session-inventory.test.ts src/main/daemon/daemon-pty-adapter.test.ts src/main/daemon/pty-subprocess.test.ts src/main/daemon/session.test.ts src/main/daemon/terminal-host.test.ts src/main/daemon/terminal-host-session-reaping-leak.test.ts src/main/providers/ssh-pty-provider.test.ts src/relay/pty-handler-dispose-lifecycle.test.ts src/relay/pty-handler-shutdown-signals.test.ts src/relay/pty-handler-spawn-admission.test.ts src/relay/pty-handler-attach-replay.test.ts src/relay/pty-handler-revive.test.ts --reporter=dot", + "command": "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/filesystem-watcher.test.ts src/main/ipc/filesystem-watcher-native-capacity.test.ts src/main/ipc/parcel-watcher-child-registry.test.ts src/main/ipc/parcel-watcher-child-launch.test.ts src/main/ipc/parcel-watcher-process.test.ts src/main/ipc/parcel-watcher-supervisor-capacity.test.ts src/main/ipc/parcel-watcher-disconnect-termination.test.ts src/main/ipc/runtime-watcher-process-pool.test.ts src/main/ipc/parcel-watcher-process-entry.test.ts src/main/ipc/parcel-watcher-event-delivery.test.ts src/main/ipc/filesystem-watcher-local-unsubscribe.test.ts src/main/ipc/filesystem-watcher-wsl.test.ts src/main/ipc/worktrees-delete-pty-teardown.test.ts src/main/ipc/worktrees-orphan-directory-cleanup.test.ts src/main/ipc/worktrees-remove-preflight.test.ts src/main/ipc/worktrees-remove-archive-hooks.test.ts src/main/ipc/watcher-removal-gate.test.ts src/main/runtime/file-watcher-host.test.ts src/main/runtime/orca-runtime-files-watch.test.ts src/main/runtime/worktree-teardown.test.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/rpc/methods/files.test.ts src/main/runtime/rpc/methods/files-watch-cancellation.test.ts src/main/providers/ssh-filesystem-provider.test.ts src/renderer/src/runtime/runtime-file-client.test.ts src/renderer/src/web/web-runtime-client.test.ts src/shared/cross-platform-path.test.ts src/shared/physical-exit-tracker.test.ts src/main/ipc/pty-spawn-cwd-fallback.test.ts src/main/ipc/pty-listener-teardown-and-orphans.test.ts src/main/daemon/terminal-descendant-shutdown.test.ts src/main/daemon/daemon-pty-adapter-inventory-respawn.test.ts src/main/daemon/daemon-pty-adapter.test.ts src/main/daemon/pty-subprocess.test.ts src/main/daemon/session.test.ts src/main/daemon/terminal-host.test.ts src/main/daemon/terminal-host-session-reaping-leak.test.ts src/main/providers/ssh-pty-provider.test.ts src/relay/pty-handler-dispose-lifecycle.test.ts src/relay/pty-handler-shutdown-signals.test.ts src/relay/pty-handler-spawn-admission.test.ts src/relay/pty-handler-attach-replay.test.ts src/relay/pty-handler-revive.test.ts --reporter=dot", "result": "passed", "durationSeconds": 11.59, "summary": "Thirty-four watcher, cancellation, desktop/serve, paired-web, WSL, removal, PTY physical-exit, provider, renderer, and path files passed with 1,999 tests; paired-web cancellation now owns a server token before ready and aborts pending setup without a late native install." @@ -18621,7 +18634,7 @@ "date": "2026-07-13", "runner": "local", "platform": "macos", - "command": "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/filesystem-watcher.test.ts src/main/ipc/parcel-watcher-process.test.ts src/main/ipc/parcel-watcher-disconnect-termination.test.ts src/main/ipc/runtime-watcher-process-pool.test.ts src/main/ipc/parcel-watcher-process-entry.test.ts src/main/ipc/parcel-watcher-event-delivery.test.ts src/main/ipc/filesystem-watcher-local-unsubscribe.test.ts src/main/ipc/filesystem-watcher-wsl.test.ts src/main/ipc/worktrees-delete-pty-teardown.test.ts src/main/ipc/worktrees-orphan-directory-cleanup.test.ts src/main/ipc/worktrees-remove-preflight.test.ts src/main/ipc/worktrees-remove-archive-hooks.test.ts src/main/ipc/watcher-removal-gate.test.ts src/main/runtime/file-watcher-host.test.ts src/main/runtime/orca-runtime-files-watch.test.ts src/main/runtime/worktree-teardown.test.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/rpc/methods/files.test.ts src/main/providers/ssh-filesystem-provider.test.ts src/renderer/src/runtime/runtime-file-client.test.ts src/renderer/src/web/web-runtime-client.test.ts src/shared/cross-platform-path.test.ts src/main/ipc/pty-spawn-cwd-fallback.test.ts src/main/ipc/pty-listener-teardown-and-orphans.test.ts src/main/providers/local-pty-provider-shutdown.test.ts src/main/providers/local-pty-provider-session-inventory.test.ts src/main/daemon/pty-subprocess.test.ts src/main/daemon/session.test.ts src/main/daemon/terminal-host.test.ts src/main/daemon/terminal-host-session-reaping-leak.test.ts src/main/providers/ssh-pty-provider.test.ts src/relay/pty-handler-dispose-lifecycle.test.ts src/relay/pty-handler-shutdown-signals.test.ts src/relay/pty-handler-spawn-admission.test.ts src/relay/pty-handler-attach-replay.test.ts src/relay/pty-handler-revive.test.ts --reporter=dot", + "command": "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/filesystem-watcher.test.ts src/main/ipc/parcel-watcher-process.test.ts src/main/ipc/parcel-watcher-disconnect-termination.test.ts src/main/ipc/runtime-watcher-process-pool.test.ts src/main/ipc/parcel-watcher-process-entry.test.ts src/main/ipc/parcel-watcher-event-delivery.test.ts src/main/ipc/filesystem-watcher-local-unsubscribe.test.ts src/main/ipc/filesystem-watcher-wsl.test.ts src/main/ipc/worktrees-delete-pty-teardown.test.ts src/main/ipc/worktrees-orphan-directory-cleanup.test.ts src/main/ipc/worktrees-remove-preflight.test.ts src/main/ipc/worktrees-remove-archive-hooks.test.ts src/main/ipc/watcher-removal-gate.test.ts src/main/runtime/file-watcher-host.test.ts src/main/runtime/orca-runtime-files-watch.test.ts src/main/runtime/worktree-teardown.test.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/rpc/methods/files.test.ts src/main/providers/ssh-filesystem-provider.test.ts src/renderer/src/runtime/runtime-file-client.test.ts src/renderer/src/web/web-runtime-client.test.ts src/shared/cross-platform-path.test.ts src/main/ipc/pty-spawn-cwd-fallback.test.ts src/main/ipc/pty-listener-teardown-and-orphans.test.ts src/main/daemon/terminal-descendant-shutdown.test.ts src/main/daemon/daemon-pty-adapter-inventory-respawn.test.ts src/main/daemon/pty-subprocess.test.ts src/main/daemon/session.test.ts src/main/daemon/terminal-host.test.ts src/main/daemon/terminal-host-session-reaping-leak.test.ts src/main/providers/ssh-pty-provider.test.ts src/relay/pty-handler-dispose-lifecycle.test.ts src/relay/pty-handler-shutdown-signals.test.ts src/relay/pty-handler-spawn-admission.test.ts src/relay/pty-handler-attach-replay.test.ts src/relay/pty-handler-revive.test.ts --reporter=dot", "result": "passed", "durationSeconds": 10.01, "summary": "Twenty-seven watcher, WSL, desktop/serve removal, local/daemon/SSH PTY physical-exit, provider ownership, RPC, renderer, relay PTY admission, and cross-platform path files passed with 1,886 tests. The suite includes nested removal fencing, renderer/runtime cwd-only admission, legacy provider rows, rejected kill retries, transient WSL capacity, and synchronous-startup reservation cleanup." @@ -18693,7 +18706,7 @@ "date": "2026-07-13", "runner": "local", "platform": "macos", - "command": "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/parcel-watcher-process.test.ts src/main/ipc/parcel-watcher-disconnect-termination.test.ts src/main/ipc/runtime-watcher-process-pool.test.ts src/main/ipc/parcel-watcher-process-entry.test.ts src/main/ipc/parcel-watcher-event-delivery.test.ts src/main/ipc/filesystem-watcher-local-unsubscribe.test.ts src/main/ipc/filesystem-watcher-wsl.test.ts src/main/ipc/worktrees-delete-pty-teardown.test.ts src/main/ipc/worktrees-orphan-directory-cleanup.test.ts src/main/ipc/worktrees-remove-preflight.test.ts src/main/ipc/worktrees-remove-archive-hooks.test.ts src/main/ipc/watcher-removal-gate.test.ts src/main/runtime/file-watcher-host.test.ts src/main/runtime/orca-runtime-files-watch.test.ts src/main/runtime/worktree-teardown.test.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/rpc/methods/files.test.ts src/main/providers/ssh-filesystem-provider.test.ts src/renderer/src/runtime/runtime-file-client.test.ts src/renderer/src/web/web-runtime-client.test.ts src/shared/cross-platform-path.test.ts src/main/ipc/pty-spawn-cwd-fallback.test.ts src/main/ipc/pty-listener-teardown-and-orphans.test.ts src/main/providers/local-pty-provider-shutdown.test.ts src/main/providers/local-pty-provider-session-inventory.test.ts src/main/daemon/session.test.ts src/main/daemon/terminal-host.test.ts src/main/daemon/terminal-host-session-reaping-leak.test.ts src/main/providers/ssh-pty-provider.test.ts src/relay/pty-handler-dispose-lifecycle.test.ts src/relay/pty-handler-shutdown-signals.test.ts src/relay/pty-handler-spawn-admission.test.ts src/relay/pty-handler-attach-replay.test.ts src/relay/pty-handler-revive.test.ts", + "command": "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/parcel-watcher-process.test.ts src/main/ipc/parcel-watcher-disconnect-termination.test.ts src/main/ipc/runtime-watcher-process-pool.test.ts src/main/ipc/parcel-watcher-process-entry.test.ts src/main/ipc/parcel-watcher-event-delivery.test.ts src/main/ipc/filesystem-watcher-local-unsubscribe.test.ts src/main/ipc/filesystem-watcher-wsl.test.ts src/main/ipc/worktrees-delete-pty-teardown.test.ts src/main/ipc/worktrees-orphan-directory-cleanup.test.ts src/main/ipc/worktrees-remove-preflight.test.ts src/main/ipc/worktrees-remove-archive-hooks.test.ts src/main/ipc/watcher-removal-gate.test.ts src/main/runtime/file-watcher-host.test.ts src/main/runtime/orca-runtime-files-watch.test.ts src/main/runtime/worktree-teardown.test.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/rpc/methods/files.test.ts src/main/providers/ssh-filesystem-provider.test.ts src/renderer/src/runtime/runtime-file-client.test.ts src/renderer/src/web/web-runtime-client.test.ts src/shared/cross-platform-path.test.ts src/main/ipc/pty-spawn-cwd-fallback.test.ts src/main/ipc/pty-listener-teardown-and-orphans.test.ts src/main/daemon/terminal-descendant-shutdown.test.ts src/main/daemon/daemon-pty-adapter-inventory-respawn.test.ts src/main/daemon/session.test.ts src/main/daemon/terminal-host.test.ts src/main/daemon/terminal-host-session-reaping-leak.test.ts src/main/providers/ssh-pty-provider.test.ts src/relay/pty-handler-dispose-lifecycle.test.ts src/relay/pty-handler-shutdown-signals.test.ts src/relay/pty-handler-spawn-admission.test.ts src/relay/pty-handler-attach-replay.test.ts src/relay/pty-handler-revive.test.ts", "result": "passed", "durationSeconds": 9.12, "summary": "Twenty-five watcher, WSL, desktop/serve removal, local/daemon/SSH PTY physical-exit, provider ownership, RPC, renderer, relay PTY admission, and cross-platform path files passed with 1,762 tests. The suite covers operations joining in-flight termination, kill failures retaining ownership, bounded timeouts, atomic relay spawn/revive caps, and PTY-removal admission." @@ -19779,7 +19792,7 @@ "runner": "local", "platform": "macos", "result": "failed", - "command": "ORCA_SKILL_UPLOAD_RELAY_ENTRY=<012e9f410c-fix-reverted-relay.js;sha256=ca03bf47d93828ec30473391b542d945df887b7881cce3df17e95d90226e34fb> pnpm exec vitest run --config config/vitest.config.ts src/relay/skill-upload-multi-relay.integration.test.ts --pool=forks --maxWorkers=1 --no-file-parallelism --reporter=verbose", + "command": "ORCA_SKILL_UPLOAD_RELAY_ENTRY= pnpm exec vitest run --config config/vitest.config.ts src/relay/skill-upload-multi-relay.integration.test.ts --pool=forks --maxWorkers=1 --no-file-parallelism --reporter=verbose", "durationSeconds": 0.4, "summary": "Expected red: zero PID/UUID owner directories and one archive were observed where two each were required; relay A's exact first five bytes were missing." } diff --git a/config/scripts/__fixtures__/node-pty-1.1.0-unix-pty.cc b/config/scripts/__fixtures__/node-pty-1.1.0-unix-pty.cc deleted file mode 100644 index 7b4b9e1f990f..000000000000 --- a/config/scripts/__fixtures__/node-pty-1.1.0-unix-pty.cc +++ /dev/null @@ -1,799 +0,0 @@ -/** - * Copyright (c) 2012-2015, Christopher Jeffrey (MIT License) - * Copyright (c) 2017, Daniel Imms (MIT License) - * - * pty.cc: - * This file is responsible for starting processes - * with pseudo-terminal file descriptors. - * - * See: - * man pty - * man tty_ioctl - * man termios - * man forkpty - */ - -/** - * Includes - */ - -#define NODE_ADDON_API_DISABLE_DEPRECATED -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -/* forkpty */ -/* http://www.gnu.org/software/gnulib/manual/html_node/forkpty.html */ -#if defined(__linux__) -#include -#elif defined(__APPLE__) -#include -#elif defined(__FreeBSD__) -#include -#include -#elif defined(__OpenBSD__) -#include -#include -#endif - -/* Some platforms name VWERASE and VDISCARD differently */ -#if !defined(VWERASE) && defined(VWERSE) -#define VWERASE VWERSE -#endif -#if !defined(VDISCARD) && defined(VDISCRD) -#define VDISCARD VDISCRD -#endif - -/* for pty_getproc */ -#if defined(__linux__) -#include -#include -#elif defined(__APPLE__) -#include -#include -#include -#include -#include -#include -#include -#endif - -/* NSIG - macro for highest signal + 1, should be defined */ -#ifndef NSIG -#define NSIG 32 -#endif - -/* macOS 10.14 back does not define this constant */ -#ifndef POSIX_SPAWN_SETSID - #define POSIX_SPAWN_SETSID 1024 -#endif - -/* environ for execvpe */ -/* node/src/node_child_process.cc */ -#if !defined(__APPLE__) -extern char **environ; -#endif - -#if defined(__APPLE__) -extern "C" { -// Changes the current thread's directory to a path or directory file -// descriptor. libpthread only exposes a syscall wrapper starting in -// macOS 10.12, but the system call dates back to macOS 10.5. On older OSes, -// the syscall is issued directly. -int pthread_chdir_np(const char* dir) API_AVAILABLE(macosx(10.12)); -int pthread_fchdir_np(int fd) API_AVAILABLE(macosx(10.12)); -} - -#define HANDLE_EINTR(x) ({ \ - int eintr_wrapper_counter = 0; \ - decltype(x) eintr_wrapper_result; \ - do { \ - eintr_wrapper_result = (x); \ - } while (eintr_wrapper_result == -1 && errno == EINTR && \ - eintr_wrapper_counter++ < 100); \ - eintr_wrapper_result; \ -}) -#endif - -struct ExitEvent { - int exit_code = 0, signal_code = 0; -}; - -void SetupExitCallback(Napi::Env env, Napi::Function cb, pid_t pid) { - std::thread *th = new std::thread; - // Don't use Napi::AsyncWorker which is limited by UV_THREADPOOL_SIZE. - auto tsfn = Napi::ThreadSafeFunction::New( - env, - cb, // JavaScript function called asynchronously - "SetupExitCallback_resource", // Name - 0, // Unlimited queue - 1, // Only one thread will use this initially - [th](Napi::Env) { // Finalizer used to clean threads up - th->join(); - delete th; - }); - *th = std::thread([tsfn = std::move(tsfn), pid] { - auto callback = [](Napi::Env env, Napi::Function cb, ExitEvent *exit_event) { - cb.Call({Napi::Number::New(env, exit_event->exit_code), - Napi::Number::New(env, exit_event->signal_code)}); - delete exit_event; - }; - - int ret; - int stat_loc; -#if defined(__APPLE__) - // Based on - // https://source.chromium.org/chromium/chromium/src/+/main:base/process/kill_mac.cc;l=35-69? - int kq = HANDLE_EINTR(kqueue()); - struct kevent change = {0}; - EV_SET(&change, pid, EVFILT_PROC, EV_ADD, NOTE_EXIT, 0, NULL); - ret = HANDLE_EINTR(kevent(kq, &change, 1, NULL, 0, NULL)); - if (ret == -1) { - if (errno == ESRCH) { - // At this point, one of the following has occurred: - // 1. The process has died but has not yet been reaped. - // 2. The process has died and has already been reaped. - // 3. The process is in the process of dying. It's no longer - // kqueueable, but it may not be waitable yet either. Mark calls - // this case the "zombie death race". - ret = HANDLE_EINTR(waitpid(pid, &stat_loc, WNOHANG)); - if (ret == 0) { - ret = kill(pid, SIGKILL); - if (ret != -1) { - HANDLE_EINTR(waitpid(pid, &stat_loc, 0)); - } - } - } - } else { - struct kevent event = {0}; - ret = HANDLE_EINTR(kevent(kq, NULL, 0, &event, 1, NULL)); - if (ret == 1) { - if ((event.fflags & NOTE_EXIT) && - (event.ident == static_cast(pid))) { - // The process is dead or dying. This won't block for long, if at - // all. - HANDLE_EINTR(waitpid(pid, &stat_loc, 0)); - } - } - } -#else - while (true) { - errno = 0; - if ((ret = waitpid(pid, &stat_loc, 0)) != pid) { - if (ret == -1 && errno == EINTR) { - continue; - } - if (ret == -1 && errno == ECHILD) { - // XXX node v0.8.x seems to have this problem. - // waitpid is already handled elsewhere. - ; - } else { - assert(false); - } - } - break; - } -#endif - ExitEvent *exit_event = new ExitEvent; - if (WIFEXITED(stat_loc)) { - exit_event->exit_code = WEXITSTATUS(stat_loc); // errno? - } - if (WIFSIGNALED(stat_loc)) { - exit_event->signal_code = WTERMSIG(stat_loc); - } - auto status = tsfn.BlockingCall(exit_event, callback); // In main thread - switch (status) { - case napi_closing: - break; - - case napi_queue_full: - Napi::Error::Fatal("SetupExitCallback", "Queue was full"); - - case napi_ok: - if (tsfn.Release() != napi_ok) { - Napi::Error::Fatal("SetupExitCallback", "ThreadSafeFunction.Release() failed"); - } - break; - - default: - Napi::Error::Fatal("SetupExitCallback", "ThreadSafeFunction.BlockingCall() failed"); - } - }); -} - -/** - * Methods - */ - -Napi::Value PtyFork(const Napi::CallbackInfo& info); -Napi::Value PtyOpen(const Napi::CallbackInfo& info); -Napi::Value PtyResize(const Napi::CallbackInfo& info); -Napi::Value PtyGetProc(const Napi::CallbackInfo& info); - -/** - * Functions - */ - -static int -pty_nonblock(int); - -#if defined(__APPLE__) -static char * -pty_getproc(int); -#else -static char * -pty_getproc(int, char *); -#endif - -#if defined(__APPLE__) || defined(__OpenBSD__) -static void -pty_posix_spawn(char** argv, char** env, - const struct termios *termp, - const struct winsize *winp, - int* master, - pid_t* pid, - int* err); -#endif - -struct DelBuf { - int len; - DelBuf(int len) : len(len) {} - void operator()(char **p) { - if (p == nullptr) - return; - for (int i = 0; i < len; i++) - free(p[i]); - delete[] p; - } -}; - -Napi::Value PtyFork(const Napi::CallbackInfo& info) { - Napi::Env napiEnv(info.Env()); - Napi::HandleScope scope(napiEnv); - - if (info.Length() != 11 || - !info[0].IsString() || - !info[1].IsArray() || - !info[2].IsArray() || - !info[3].IsString() || - !info[4].IsNumber() || - !info[5].IsNumber() || - !info[6].IsNumber() || - !info[7].IsNumber() || - !info[8].IsBoolean() || - !info[9].IsString() || - !info[10].IsFunction()) { - throw Napi::Error::New(napiEnv, "Usage: pty.fork(file, args, env, cwd, cols, rows, uid, gid, utf8, helperPath, onexit)"); - } - - // file - std::string file = info[0].As(); - - // args - Napi::Array argv_ = info[1].As(); - - // env - Napi::Array env_ = info[2].As(); - int envc = env_.Length(); - std::unique_ptr env_unique_ptr(new char *[envc + 1], DelBuf(envc + 1)); - char **env = env_unique_ptr.get(); - env[envc] = NULL; - for (int i = 0; i < envc; i++) { - std::string pair = env_.Get(i).As(); - env[i] = strdup(pair.c_str()); - } - - // cwd - std::string cwd_ = info[3].As(); - - // size - struct winsize winp; - winp.ws_col = info[4].As().Int32Value(); - winp.ws_row = info[5].As().Int32Value(); - winp.ws_xpixel = 0; - winp.ws_ypixel = 0; - -#if !defined(__APPLE__) - // uid / gid - int uid = info[6].As().Int32Value(); - int gid = info[7].As().Int32Value(); -#endif - - // termios - struct termios t = termios(); - struct termios *term = &t; - term->c_iflag = ICRNL | IXON | IXANY | IMAXBEL | BRKINT; - if (info[8].As().Value()) { -#if defined(IUTF8) - term->c_iflag |= IUTF8; -#endif - } - term->c_oflag = OPOST | ONLCR; - term->c_cflag = CREAD | CS8 | HUPCL; - term->c_lflag = ICANON | ISIG | IEXTEN | ECHO | ECHOE | ECHOK | ECHOKE | ECHOCTL; - - term->c_cc[VEOF] = 4; - term->c_cc[VEOL] = -1; - term->c_cc[VEOL2] = -1; - term->c_cc[VERASE] = 0x7f; - term->c_cc[VWERASE] = 23; - term->c_cc[VKILL] = 21; - term->c_cc[VREPRINT] = 18; - term->c_cc[VINTR] = 3; - term->c_cc[VQUIT] = 0x1c; - term->c_cc[VSUSP] = 26; - term->c_cc[VSTART] = 17; - term->c_cc[VSTOP] = 19; - term->c_cc[VLNEXT] = 22; - term->c_cc[VDISCARD] = 15; - term->c_cc[VMIN] = 1; - term->c_cc[VTIME] = 0; - - #if (__APPLE__) - term->c_cc[VDSUSP] = 25; - term->c_cc[VSTATUS] = 20; - #endif - - cfsetispeed(term, B38400); - cfsetospeed(term, B38400); - - // helperPath - std::string helper_path = info[9].As(); - - pid_t pid; - int master; -#if defined(__APPLE__) - int argc = argv_.Length(); - int argl = argc + 4; - std::unique_ptr argv_unique_ptr(new char *[argl], DelBuf(argl)); - char **argv = argv_unique_ptr.get(); - argv[0] = strdup(helper_path.c_str()); - argv[1] = strdup(cwd_.c_str()); - argv[2] = strdup(file.c_str()); - argv[argl - 1] = NULL; - for (int i = 0; i < argc; i++) { - std::string arg = argv_.Get(i).As(); - argv[i + 3] = strdup(arg.c_str()); - } - - int err = -1; - pty_posix_spawn(argv, env, term, &winp, &master, &pid, &err); - if (err != 0) { - throw Napi::Error::New(napiEnv, "posix_spawnp failed."); - } - if (pty_nonblock(master) == -1) { - throw Napi::Error::New(napiEnv, "Could not set master fd to nonblocking."); - } -#else - int argc = argv_.Length(); - int argl = argc + 2; - std::unique_ptr argv_unique_ptr(new char *[argl], DelBuf(argl)); - char** argv = argv_unique_ptr.get(); - argv[0] = strdup(file.c_str()); - argv[argl - 1] = NULL; - for (int i = 0; i < argc; i++) { - std::string arg = argv_.Get(i).As(); - argv[i + 1] = strdup(arg.c_str()); - } - - sigset_t newmask, oldmask; - struct sigaction sig_action; - // temporarily block all signals - // this is needed due to a race condition in openpty - // and to avoid running signal handlers in the child - // before exec* happened - sigfillset(&newmask); - pthread_sigmask(SIG_SETMASK, &newmask, &oldmask); - - pid = forkpty(&master, nullptr, static_cast(term), static_cast(&winp)); - - if (!pid) { - // remove all signal handler from child - sig_action.sa_handler = SIG_DFL; - sig_action.sa_flags = 0; - sigemptyset(&sig_action.sa_mask); - for (int i = 0 ; i < NSIG ; i++) { // NSIG is a macro for all signals + 1 - sigaction(i, &sig_action, NULL); - } - } - - // reenable signals - pthread_sigmask(SIG_SETMASK, &oldmask, NULL); - - switch (pid) { - case -1: - throw Napi::Error::New(napiEnv, "forkpty(3) failed."); - case 0: - if (strlen(cwd_.c_str())) { - if (chdir(cwd_.c_str()) == -1) { - perror("chdir(2) failed."); - _exit(1); - } - } - - if (uid != -1 && gid != -1) { - if (setgid(gid) == -1) { - perror("setgid(2) failed."); - _exit(1); - } - if (setuid(uid) == -1) { - perror("setuid(2) failed."); - _exit(1); - } - } - - { - char **old = environ; - environ = env; - execvp(argv[0], argv); - environ = old; - perror("execvp(3) failed."); - _exit(1); - } - default: - if (pty_nonblock(master) == -1) { - throw Napi::Error::New(napiEnv, "Could not set master fd to nonblocking."); - } - } -#endif - - Napi::Object obj = Napi::Object::New(napiEnv); - obj.Set("fd", Napi::Number::New(napiEnv, master)); - obj.Set("pid", Napi::Number::New(napiEnv, pid)); - obj.Set("pty", Napi::String::New(napiEnv, ptsname(master))); - - // Set up process exit callback. - Napi::Function cb = info[10].As(); - SetupExitCallback(napiEnv, cb, pid); - return obj; -} - -Napi::Value PtyOpen(const Napi::CallbackInfo& info) { - Napi::Env env(info.Env()); - Napi::HandleScope scope(env); - - if (info.Length() != 2 || - !info[0].IsNumber() || - !info[1].IsNumber()) { - throw Napi::Error::New(env, "Usage: pty.open(cols, rows)"); - } - - // size - struct winsize winp; - winp.ws_col = info[0].As().Int32Value(); - winp.ws_row = info[1].As().Int32Value(); - winp.ws_xpixel = 0; - winp.ws_ypixel = 0; - - // pty - int master, slave; - int ret = openpty(&master, &slave, nullptr, NULL, static_cast(&winp)); - - if (ret == -1) { - throw Napi::Error::New(env, "openpty(3) failed."); - } - - if (pty_nonblock(master) == -1) { - throw Napi::Error::New(env, "Could not set master fd to nonblocking."); - } - - if (pty_nonblock(slave) == -1) { - throw Napi::Error::New(env, "Could not set slave fd to nonblocking."); - } - - Napi::Object obj = Napi::Object::New(env); - obj.Set("master", Napi::Number::New(env, master)); - obj.Set("slave", Napi::Number::New(env, slave)); - obj.Set("pty", Napi::String::New(env, ptsname(master))); - - return obj; -} - -Napi::Value PtyResize(const Napi::CallbackInfo& info) { - Napi::Env env(info.Env()); - Napi::HandleScope scope(env); - - if (info.Length() != 3 || - !info[0].IsNumber() || - !info[1].IsNumber() || - !info[2].IsNumber()) { - throw Napi::Error::New(env, "Usage: pty.resize(fd, cols, rows)"); - } - - int fd = info[0].As().Int32Value(); - - struct winsize winp; - winp.ws_col = info[1].As().Int32Value(); - winp.ws_row = info[2].As().Int32Value(); - winp.ws_xpixel = 0; - winp.ws_ypixel = 0; - - if (ioctl(fd, TIOCSWINSZ, &winp) == -1) { - switch (errno) { - case EBADF: - throw Napi::Error::New(env, "ioctl(2) failed, EBADF"); - case EFAULT: - throw Napi::Error::New(env, "ioctl(2) failed, EFAULT"); - case EINVAL: - throw Napi::Error::New(env, "ioctl(2) failed, EINVAL"); - case ENOTTY: - throw Napi::Error::New(env, "ioctl(2) failed, ENOTTY"); - } - throw Napi::Error::New(env, "ioctl(2) failed"); - } - - return env.Undefined(); -} - -/** - * Foreground Process Name - */ -Napi::Value PtyGetProc(const Napi::CallbackInfo& info) { - Napi::Env env(info.Env()); - Napi::HandleScope scope(env); - -#if defined(__APPLE__) - if (info.Length() != 1 || - !info[0].IsNumber()) { - throw Napi::Error::New(env, "Usage: pty.process(pid)"); - } - - int fd = info[0].As().Int32Value(); - char *name = pty_getproc(fd); -#else - if (info.Length() != 2 || - !info[0].IsNumber() || - !info[1].IsString()) { - throw Napi::Error::New(env, "Usage: pty.process(fd, tty)"); - } - - int fd = info[0].As().Int32Value(); - - std::string tty_ = info[1].As(); - char *tty = strdup(tty_.c_str()); - char *name = pty_getproc(fd, tty); - free(tty); -#endif - - if (name == NULL) { - return env.Undefined(); - } - - Napi::String name_ = Napi::String::New(env, name); - free(name); - return name_; -} - -/** - * Nonblocking FD - */ - -static int -pty_nonblock(int fd) { - int flags = fcntl(fd, F_GETFL, 0); - if (flags == -1) return -1; - return fcntl(fd, F_SETFL, flags | O_NONBLOCK); -} - -/** - * pty_getproc - * Taken from tmux. - */ - -// Taken from: tmux (http://tmux.sourceforge.net/) -// Copyright (c) 2009 Nicholas Marriott -// Copyright (c) 2009 Joshua Elsasser -// Copyright (c) 2009 Todd Carson -// -// Permission to use, copy, modify, and distribute this software for any -// purpose with or without fee is hereby granted, provided that the above -// copyright notice and this permission notice appear in all copies. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -// WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER -// IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING -// OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - -#if defined(__linux__) - -static char * -pty_getproc(int fd, char *tty) { - FILE *f; - char *path, *buf; - size_t len; - int ch; - pid_t pgrp; - int r; - - if ((pgrp = tcgetpgrp(fd)) == -1) { - return NULL; - } - - r = asprintf(&path, "/proc/%lld/cmdline", (long long)pgrp); - if (r == -1 || path == NULL) return NULL; - - if ((f = fopen(path, "r")) == NULL) { - free(path); - return NULL; - } - - free(path); - - len = 0; - buf = NULL; - while ((ch = fgetc(f)) != EOF) { - if (ch == '\0') break; - buf = (char *)realloc(buf, len + 2); - if (buf == NULL) return NULL; - buf[len++] = ch; - } - - if (buf != NULL) { - buf[len] = '\0'; - } - - fclose(f); - return buf; -} - -#elif defined(__APPLE__) - -static char * -pty_getproc(int fd) { - int mib[4] = { CTL_KERN, KERN_PROC, KERN_PROC_PID, 0 }; - size_t size; - struct kinfo_proc kp; - - if ((mib[3] = tcgetpgrp(fd)) == -1) { - return NULL; - } - - size = sizeof kp; - if (sysctl(mib, 4, &kp, &size, NULL, 0) == -1) { - return NULL; - } - - if (size != (sizeof kp) || *kp.kp_proc.p_comm == '\0') { - return NULL; - } - - return strdup(kp.kp_proc.p_comm); -} - -#else - -static char * -pty_getproc(int fd, char *tty) { - return NULL; -} - -#endif - -#if defined(__APPLE__) -static void -pty_posix_spawn(char** argv, char** env, - const struct termios *termp, - const struct winsize *winp, - int* master, - pid_t* pid, - int* err) { - int low_fds[3]; - size_t count = 0; - - for (; count < 3; count++) { - low_fds[count] = posix_openpt(O_RDWR); - if (low_fds[count] >= STDERR_FILENO) - break; - } - - int flags = POSIX_SPAWN_CLOEXEC_DEFAULT | - POSIX_SPAWN_SETSIGDEF | - POSIX_SPAWN_SETSIGMASK | - POSIX_SPAWN_SETSID; - *master = posix_openpt(O_RDWR); - if (*master == -1) { - return; - } - - int res = grantpt(*master) || unlockpt(*master); - if (res == -1) { - return; - } - - // Use TIOCPTYGNAME instead of ptsname() to avoid threading problems. - int slave; - char slave_pty_name[128]; - res = ioctl(*master, TIOCPTYGNAME, slave_pty_name); - if (res == -1) { - return; - } - - slave = open(slave_pty_name, O_RDWR | O_NOCTTY); - if (slave == -1) { - return; - } - - if (termp) { - res = tcsetattr(slave, TCSANOW, termp); - if (res == -1) { - return; - }; - } - - if (winp) { - res = ioctl(slave, TIOCSWINSZ, winp); - if (res == -1) { - return; - } - } - - posix_spawn_file_actions_t acts; - posix_spawn_file_actions_init(&acts); - posix_spawn_file_actions_adddup2(&acts, slave, STDIN_FILENO); - posix_spawn_file_actions_adddup2(&acts, slave, STDOUT_FILENO); - posix_spawn_file_actions_adddup2(&acts, slave, STDERR_FILENO); - posix_spawn_file_actions_addclose(&acts, slave); - posix_spawn_file_actions_addclose(&acts, *master); - - posix_spawnattr_t attrs; - posix_spawnattr_init(&attrs); - *err = posix_spawnattr_setflags(&attrs, flags); - if (*err != 0) { - goto done; - } - - sigset_t signal_set; - /* Reset all signal the child to their default behavior */ - sigfillset(&signal_set); - *err = posix_spawnattr_setsigdefault(&attrs, &signal_set); - if (*err != 0) { - goto done; - } - - /* Reset the signal mask for all signals */ - sigemptyset(&signal_set); - *err = posix_spawnattr_setsigmask(&attrs, &signal_set); - if (*err != 0) { - goto done; - } - - do - *err = posix_spawn(pid, argv[0], &acts, &attrs, argv, env); - while (*err == EINTR); -done: - posix_spawn_file_actions_destroy(&acts); - posix_spawnattr_destroy(&attrs); - - for (; count > 0; count--) { - close(low_fds[count]); - } -} -#endif - -/** - * Init - */ - -Napi::Object init(Napi::Env env, Napi::Object exports) { - exports.Set("fork", Napi::Function::New(env, PtyFork)); - exports.Set("open", Napi::Function::New(env, PtyOpen)); - exports.Set("resize", Napi::Function::New(env, PtyResize)); - exports.Set("process", Napi::Function::New(env, PtyGetProc)); - return exports; -} - -NODE_API_MODULE(NODE_GYP_MODULE_NAME, init) diff --git a/config/scripts/appimage-startup-cleanup.test.mjs b/config/scripts/appimage-startup-cleanup.test.mjs new file mode 100644 index 000000000000..22f0325ddd92 --- /dev/null +++ b/config/scripts/appimage-startup-cleanup.test.mjs @@ -0,0 +1,108 @@ +import { mkdtempSync, mkdirSync, readFileSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { describe, expect, it } from 'vitest' +import { spawnProcess, runProcess } from '../../src/shared/child-process/run-process' + +const oracle = readFileSync( + 'config/docker/headless-serve-shutdown/run-appimage-desktop-startup-case.sh', + 'utf8' +) +const functions = oracle.slice(oracle.indexOf('read_start_ticks()'), oracle.indexOf('dump_logs()')) + +function isRunning(pid) { + try { + const stat = readFileSync(`/proc/${pid}/stat`, 'utf8') + return stat.slice(stat.lastIndexOf(') ') + 2)[0] !== 'Z' + } catch { + return false + } +} + +async function waitFor(predicate) { + const deadline = Date.now() + 5000 + while (!predicate()) { + if (Date.now() >= deadline) { + throw new Error('Fixture readiness timed out') + } + await new Promise((resolve) => setTimeout(resolve, 20)) + } +} + +describe.skipIf(process.platform !== 'linux')('AppImage startup process cleanup', () => { + it('drains detached profile writers born during shutdown without touching a neighboring HOME', async () => { + const directory = mkdtempSync(join(tmpdir(), 'orca-startup-cleanup-test-')) + const profileHome = join(directory, 'home') + mkdirSync(profileHome) + const successorPidFile = join(directory, 'successor.pid') + const readyFile = join(directory, 'ready') + const writerCode = ` + const fs = require('node:fs'); + process.title = 'orca writer (x)'; + setInterval(() => fs.writeFileSync(process.env.HOME + '/state', 'alive'), 5); + process.on('SIGTERM', () => { + const child = require('node:child_process').spawn(process.execPath, ['-e', + "setInterval(() => require('node:fs').writeFileSync(process.env.HOME + '/late-state', 'alive'), 5)" + ], { detached: true, stdio: 'ignore', env: process.env }); + fs.writeFileSync(${JSON.stringify(successorPidFile)}, String(child.pid)); + child.unref(); + process.exit(0); + }); + fs.writeFileSync(${JSON.stringify(readyFile)}, 'ready'); + ` + const writer = spawnProcess({ + program: process.execPath, + args: ['-e', writerCode], + detached: true, + env: { ...process.env, HOME: profileHome }, + stdio: 'ignore' + }) + const canary = spawnProcess({ + program: process.execPath, + args: ['-e', 'setInterval(() => {}, 1000)'], + detached: true, + env: { ...process.env, HOME: `${profileHome}-unrelated` }, + stdio: 'ignore' + }) + try { + await waitFor(() => { + try { + return readFileSync(readyFile, 'utf8') === 'ready' + } catch { + return false + } + }) + const result = await runProcess({ + program: 'bash', + args: [ + '-c', + ` + set -euo pipefail + state_dir=$1 + tree_pids=() + declare -A tree_start_ticks=() + ${functions} + collect_state_processes TERM + wait_for_owned_exit 5 TERM + `, + 'cleanup-fixture', + directory + ], + timeoutMs: 10_000 + }) + expect(result.code, result.stderr).toBe(0) + expect(result.timedOut).toBe(false) + expect(isRunning(writer.pid)).toBe(false) + expect(isRunning(Number(readFileSync(successorPidFile, 'utf8')))).toBe(false) + expect(isRunning(canary.pid)).toBe(true) + } finally { + writer.kill('SIGKILL') + canary.kill('SIGKILL') + try { + process.kill(Number(readFileSync(successorPidFile, 'utf8')), 'SIGKILL') + } catch {} + await waitFor(() => !isRunning(writer.pid) && !isRunning(canary.pid)) + rmSync(directory, { recursive: true, force: true }) + } + }) +}) diff --git a/config/scripts/build-cli-runtime.mjs b/config/scripts/build-cli-runtime.mjs index 6949b782e9a2..d3510aaec533 100644 --- a/config/scripts/build-cli-runtime.mjs +++ b/config/scripts/build-cli-runtime.mjs @@ -2,6 +2,7 @@ import { copyFileSync, mkdirSync, writeFileSync } from 'node:fs' import { join, resolve } from 'node:path' import { pathToFileURL } from 'node:url' import { materializeRuntime } from './build-orcad-bun.mjs' +import { materializeWindowsConpty } from './build-windows-conpty.mjs' import { ORCAD_BUN_RELEASE_ASSETS, ORCAD_BUN_VERSION } from '../../src/shared/orcad-bun-runtime.ts' import { cliRuntimeTarget, cliRuntimeFilename } from '../bundled-cli-runtime.cjs' @@ -12,6 +13,9 @@ export async function buildCliRuntime(platform, arch, projectDir = root) { const directory = join(projectDir, 'out', 'cli-runtime', `${platform}-${arch}`) mkdirSync(directory, { recursive: true }) await materializeRuntime(target, join(directory, cliRuntimeFilename(platform))) + if (platform === 'win32') { + await materializeWindowsConpty(arch, join(directory, 'conpty')) + } copyFileSync(join(root, 'resources/licenses/bun/LICENSE.md'), join(directory, 'LICENSE.md')) writeFileSync( join(directory, 'runtime.json'), diff --git a/config/scripts/build-cli-runtime.test.mjs b/config/scripts/build-cli-runtime.test.mjs new file mode 100644 index 000000000000..ab70ba4c6421 --- /dev/null +++ b/config/scripts/build-cli-runtime.test.mjs @@ -0,0 +1,44 @@ +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { join } from 'node:path' +import { tmpdir } from 'node:os' +import { afterEach, describe, expect, it, vi } from 'vitest' +vi.mock('./build-orcad-bun.mjs', () => ({ materializeRuntime: vi.fn() })) +vi.mock('./build-windows-conpty.mjs', () => ({ materializeWindowsConpty: vi.fn() })) +import { materializeRuntime } from './build-orcad-bun.mjs' +import { materializeWindowsConpty } from './build-windows-conpty.mjs' +import { buildCliRuntime } from './build-cli-runtime.mjs' +const roots = [] +afterEach(() => { + roots.splice(0).forEach((root) => rmSync(root, { recursive: true, force: true })) + vi.resetAllMocks() +}) +describe('CLI runtime provider packaging', () => { + it.each(['x64', 'arm64'])( + 'adds the pinned %s Windows provider beside the runtime', + async (arch) => { + const root = mkdtempSync(join(tmpdir(), 'cli-build-')) + roots.push(root) + vi.mocked(materializeRuntime).mockImplementation(async (_target, filename) => + writeFileSync(filename, 'runtime') + ) + vi.mocked(materializeWindowsConpty).mockImplementation(async (_arch, directory) => + mkdirSync(directory) + ) + await buildCliRuntime('win32', arch, root) + const directory = join(root, 'out', 'cli-runtime', `win32-${arch}`) + expect(materializeWindowsConpty).toHaveBeenCalledExactlyOnceWith( + arch, + join(directory, 'conpty') + ) + expect(JSON.parse(readFileSync(join(directory, 'runtime.json'), 'utf8')).target).toBe( + `win32-${arch}` + ) + } + ) + it.each(['darwin', 'linux'])('does not provision ConPTY for %s', async (platform) => { + const root = mkdtempSync(join(tmpdir(), 'cli-build-')) + roots.push(root) + await buildCliRuntime(platform, 'arm64', root) + expect(materializeWindowsConpty).not.toHaveBeenCalled() + }) +}) diff --git a/config/scripts/build-orcad-bun.mjs b/config/scripts/build-orcad-bun.mjs index fc46d788292d..b7f2ecd67df0 100644 --- a/config/scripts/build-orcad-bun.mjs +++ b/config/scripts/build-orcad-bun.mjs @@ -18,6 +18,7 @@ import { orcadBunRuntimeFilename } from '../../src/shared/orcad-artifacts.ts' import { ORCAD_BUN_RELEASE_ASSETS, ORCAD_BUN_VERSION, + orcadBunVersionProbeArgs, orcadBunReleaseUrl } from '../../src/shared/orcad-bun-runtime.ts' import { runProcessSync } from './script-child-process.mjs' @@ -73,7 +74,11 @@ export function findBunExecutable(rootDir, target) { } function verifyRuntime(path) { - const result = runProcessSync({ program: path, args: ['--version'] }) + const result = runProcessSync({ + program: path, + args: [...orcadBunVersionProbeArgs()], + timeoutMs: 10_000 + }) if (result.code !== 0 || result.stdout.trim() !== ORCAD_BUN_VERSION) { throw new Error( `Expected Bun ${ORCAD_BUN_VERSION} at ${path}, got ${result.stdout.trim() || result.stderr.trim()}` diff --git a/config/scripts/build-orcad.mjs b/config/scripts/build-orcad.mjs index 41127c28ed20..23f8df5e0402 100644 --- a/config/scripts/build-orcad.mjs +++ b/config/scripts/build-orcad.mjs @@ -1,13 +1,12 @@ #!/usr/bin/env node -// Ship Bun with orcad; keep module loading compatible with legacy Node launchers. +// Ship orcad with its pinned Bun runtime. import { fork, spawnSync } from 'node:child_process' import { build } from 'esbuild' import { buildOrcadEntry, buildOrcadLauncher, ORCAD_BUN_TARGET, - externalNativeAddons, - ORCAD_EXTERNAL_MODULES, + buildOrcadChildEntry, ORCAD_CHILD_ENTRY_POINTS } from './orcad-entry-build.mjs' import { createRequire } from 'node:module' @@ -26,6 +25,7 @@ import { basename, dirname, join, resolve } from 'node:path' import process from 'node:process' import { smokeProfileStateWorkers } from './profile-state-worker-smoke.mjs' import { materializeWatcherPackage } from './orcad-watcher-package.mjs' +import { materializeWindowsConpty } from './build-windows-conpty.mjs' import { parcelWatcherWrapperSource } from './parcel-watcher-bundle.mjs' import { stageOrcadWindowsProcessTree } from './orcad-windows-process-tree.mjs' import { @@ -38,7 +38,7 @@ import { ORCAD_RIPGREP_ARTIFACTS } from '../../src/shared/orcad-artifacts.ts' import { computeOrcadFullVersion } from './orcad-artifact-version.mjs' -import { ORCAD_BUN_VERSION } from '../../src/shared/orcad-bun-runtime.ts' +import { ORCAD_BUN_VERSION, orcadBunVersionProbeArgs } from '../../src/shared/orcad-bun-runtime.ts' import { orcadAgentBrowserNativeName } from '../../src/shared/orcad-agent-browser-name.ts' const ROOT = join(import.meta.dirname, '..', '..') @@ -103,7 +103,10 @@ if (!bunRuntimeSource) { throw new Error('ORCAD_BUN_RUNTIME_PATH is required; run `pnpm build:orcad`') } if (targetIsCurrent) { - const version = spawnSync(bunRuntimeSource, ['--version'], { encoding: 'utf8' }) + const version = spawnSync(bunRuntimeSource, orcadBunVersionProbeArgs(), { + encoding: 'utf8', + timeout: 10_000 + }) if (version.status !== 0 || version.stdout.trim() !== ORCAD_BUN_VERSION) { throw new Error( `ORCAD_BUN_RUNTIME_PATH must be Bun ${ORCAD_BUN_VERSION}; got ${version.stdout.trim() || version.stderr.trim()}` @@ -118,6 +121,9 @@ if (!targetIsWindows) { } await stageParcelWatcher(BUILD_TARGET) stageOrcadWindowsProcessTree(ROOT, OUT_DIR, BUILD_TARGET) +if (targetIsWindows) { + await materializeWindowsConpty(targetArch, join(OUT_DIR, 'conpty')) +} const emojiDatasetOutput = join(OUT_DIR, ORCAD_EMOJI_SHORTCODE_DATASET) mkdirSync(dirname(emojiDatasetOutput), { recursive: true }) copyFileSync( @@ -148,37 +154,14 @@ cpSync(join(ROOT, 'resources', 'licenses', 'ripgrep'), join(OUT_DIR, 'ripgrep', recursive: true }) -/** Why one call per child and not one `outdir` build: esbuild mirrors each entry's source - * directory under `outdir`, and both children must land flat beside orcad.js — that is where - * their runtime resolvers look for them. */ -function buildForkedChild(entryPoint, outfile) { - return build({ - entryPoints: [entryPoint], - bundle: true, - platform: 'node', - target: ORCAD_BUN_TARGET, - format: 'cjs', - outfile, - external: ORCAD_EXTERNAL_MODULES, - plugins: [externalNativeAddons], - metafile: true, - minify: true, - sourcemap: false, - define: { - 'process.env.NODE_ENV': '"production"' - }, - logLevel: 'error' - }) -} - const childResults = await Promise.all([ - buildForkedChild(WATCHER_ENTRY, WATCHER_OUT_FILE), - buildForkedChild(DAEMON_ENTRY, DAEMON_OUT_FILE), - buildForkedChild(PTY_GATE_ENTRY, PTY_GATE_OUT_FILE), + buildOrcadChildEntry(WATCHER_ENTRY, WATCHER_OUT_FILE), + buildOrcadChildEntry(DAEMON_ENTRY, DAEMON_OUT_FILE), + buildOrcadChildEntry(PTY_GATE_ENTRY, PTY_GATE_OUT_FILE), ...Object.entries(ORCAD_CHILD_ENTRY_POINTS) .filter(([role]) => !['watcher', 'daemon', 'ptyGate'].includes(role)) .map(([, entry]) => - buildForkedChild(join(ROOT, entry), join(OUT_DIR, `${basename(entry, '.ts')}.js`)) + buildOrcadChildEntry(join(ROOT, entry), join(OUT_DIR, `${basename(entry, '.ts')}.js`)) ) ]) @@ -213,26 +196,19 @@ const metafiles = [ result.metafile, ...childResults.map((child) => child.metafile) ] -const electronImporters = collectImporters( - metafiles, - (specifier) => specifier === 'electron' || specifier.startsWith('electron/') -) -const sqliteImporters = collectImporters(metafiles, (specifier) => specifier === 'node:sqlite') - const graphErrors = [] -if (electronImporters.size > 0) { - graphErrors.push( - `${electronImporters.size} module(s) in the bundle import electron:\n${[...electronImporters] - .map((file) => ` - ${file}`) - .join('\n')}` - ) -} -if (sqliteImporters.size > 0) { - graphErrors.push( - `${sqliteImporters.size} module(s) in the bundle import node:sqlite:\n${[...sqliteImporters] - .map((file) => ` - ${file}`) - .join('\n')}` +for (const forbidden of ['electron', 'node:sqlite', 'node-pty']) { + const importers = collectImporters( + metafiles, + (specifier) => specifier === forbidden || specifier.startsWith(`${forbidden}/`) ) + if (importers.size > 0) { + graphErrors.push( + `${importers.size} module(s) in the bundle import ${forbidden}:\n${[...importers] + .map((file) => ` - ${file}`) + .join('\n')}` + ) + } } if (graphErrors.length > 0) { @@ -296,28 +272,27 @@ if (graphErrors.length > 0) { } } -try { - if (targetIsCurrent) { - await smokeProfileStateWorkers(OUT_DIR, { runtimePath: bunRuntimeOutput }) - } -} catch (error) { - console.error('[build-orcad] profile state worker check failed:', error) - process.exitCode = 1 -} - -// Why a content hash and not ORCAD_VERSION alone: the remote install directory is keyed on -// this string, so two different builds carrying one version would share a directory — and an -// already-`.install-complete` dir is never re-uploaded. The deploy would silently run stale -// bytes while reporting the new version. +// Remote install directories are keyed by content, including every shipped companion. if (process.exitCode !== 1) { const fullVersion = computeOrcadFullVersion(OUT_DIR, { target: BUILD_TARGET, agentBrowserFilename: AGENT_BROWSER_NAME }) - writeFileSync(join(OUT_DIR, ORCAD_VERSION_FILENAME), fullVersion) - console.log( - `[build-orcad] ok — ${fullVersion}, ${(output.bytes / 1024 / 1024).toFixed(2)} MB, ${Object.keys(output.inputs).length} modules, zero electron and node:sqlite imports, Bun ${ORCAD_BUN_VERSION} included.` - ) + const versionPath = join(OUT_DIR, ORCAD_VERSION_FILENAME) + writeFileSync(versionPath, fullVersion) + try { + // Windows preflight verifies the installed version before launching its native child. + if (targetIsCurrent) { + await smokeProfileStateWorkers(OUT_DIR, { runtimePath: bunRuntimeOutput }) + } + console.log( + `[build-orcad] ok — ${fullVersion}, ${(output.bytes / 1024 / 1024).toFixed(2)} MB, ${Object.keys(output.inputs).length} modules, zero electron and node:sqlite imports, Bun ${ORCAD_BUN_VERSION} included.` + ) + } catch (error) { + rmSync(versionPath, { force: true }) + console.error('[build-orcad] profile state worker check failed:', error) + process.exitCode = 1 + } } // Verify the shipped native watcher actually subscribes under the bundled runtime. diff --git a/config/scripts/build-terminal-daemon.mjs b/config/scripts/build-terminal-daemon.mjs new file mode 100644 index 000000000000..d3a6c780c8ef --- /dev/null +++ b/config/scripts/build-terminal-daemon.mjs @@ -0,0 +1,21 @@ +import { mkdirSync } from 'node:fs' +import { join, resolve } from 'node:path' +import { pathToFileURL } from 'node:url' +import { buildOrcadChildEntry, ORCAD_CHILD_ENTRY_POINTS } from './orcad-entry-build.mjs' + +const root = resolve(import.meta.dirname, '../..') + +export async function buildTerminalDaemon(outputDir = join(root, 'out', 'terminal-daemon')) { + mkdirSync(outputDir, { recursive: true }) + return Promise.all([ + buildOrcadChildEntry(ORCAD_CHILD_ENTRY_POINTS.daemon, join(outputDir, 'daemon-entry.js')), + buildOrcadChildEntry( + ORCAD_CHILD_ENTRY_POINTS.ptyGate, + join(outputDir, 'windows-bun-pty-gate-entry.js') + ) + ]) +} + +if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) { + await buildTerminalDaemon() +} diff --git a/config/scripts/build-windows-conpty.mjs b/config/scripts/build-windows-conpty.mjs new file mode 100644 index 000000000000..1ddaac49e9fb --- /dev/null +++ b/config/scripts/build-windows-conpty.mjs @@ -0,0 +1,148 @@ +#!/usr/bin/env node +import { createHash } from 'node:crypto' +import { + copyFileSync, + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + renameSync, + rmSync, + writeFileSync +} from 'node:fs' +import { tmpdir } from 'node:os' +import { join, resolve } from 'node:path' +import { pathToFileURL } from 'node:url' +import { + WINDOWS_CONPTY_ARCHIVE, + WINDOWS_CONPTY_FILES, + WINDOWS_CONPTY_VERSION +} from '../../src/shared/windows-conpty-release.ts' +import { getZipExtractorCommand } from './zip-extractor-command.mjs' +import { runProcessSync } from './script-child-process.mjs' + +const root = resolve(import.meta.dirname, '../..') +const licensePath = join(root, 'config', 'licenses', 'windows-conpty-LICENSE.txt') +const maximumArchiveBytes = 16 * 1024 * 1024 +const sha256 = (path) => createHash('sha256').update(readFileSync(path)).digest('hex') + +export async function downloadConptyArchive(destination, fetcher = fetch) { + const response = await fetcher(WINDOWS_CONPTY_ARCHIVE.url, { + redirect: 'follow', + signal: AbortSignal.timeout(120_000) + }) + if (!response.ok || !response.body) { + await response.body?.cancel() + throw new Error(`ConPTY download failed: HTTP ${response.status}`) + } + const chunks = [] + let length = 0 + for await (const chunk of response.body) { + length += chunk.byteLength + if (length > maximumArchiveBytes) { + throw new Error('ConPTY archive exceeds download size limit') + } + chunks.push(chunk) + } + writeFileSync(destination, Buffer.concat(chunks)) +} + +export function verifyConptyDirectory(directory, arch) { + const files = WINDOWS_CONPTY_FILES[arch] + if (!files) { + throw new Error(`Unsupported ConPTY architecture: ${arch}`) + } + for (const [filename, expected] of Object.entries(files)) { + if (sha256(join(directory, filename)) !== expected) { + throw new Error(`ConPTY checksum mismatch: ${arch}/${filename}`) + } + } +} + +/** Pins both files independently; no node-pty installation or native rebuild is needed. */ +export async function materializeWindowsConpty(arch, outputDir, options = {}) { + if (!Object.hasOwn(WINDOWS_CONPTY_FILES, arch)) { + throw new Error(`Unsupported ConPTY architecture: ${arch}`) + } + const cacheDir = options.cacheDir ?? join(root, 'out', '.windows-conpty', WINDOWS_CONPTY_VERSION) + mkdirSync(cacheDir, { recursive: true }) + const archive = join(cacheDir, 'conpty.nupkg') + if (existsSync(archive) && sha256(archive) !== WINDOWS_CONPTY_ARCHIVE.sha256) { + rmSync(archive, { force: true }) + } + const temporary = mkdtempSync(join(tmpdir(), 'orca-conpty-')) + try { + if (!existsSync(archive)) { + const downloading = join(temporary, 'conpty.nupkg') + await downloadConptyArchive(downloading, options.fetcher) + if (sha256(downloading) !== WINDOWS_CONPTY_ARCHIVE.sha256) { + throw new Error('ConPTY archive checksum mismatch') + } + // Publish on the cache filesystem, including when TEMP is on another volume. + const staging = mkdtempSync(join(cacheDir, '.download-')) + try { + copyFileSync(downloading, join(staging, 'conpty.nupkg')) + renameSync(join(staging, 'conpty.nupkg'), archive) + } finally { + rmSync(staging, { recursive: true, force: true }) + } + } + const extracted = join(temporary, 'extracted') + mkdirSync(extracted) + const command = getZipExtractorCommand(archive, extracted) + const result = runProcessSync({ program: command.file, args: command.args, timeoutMs: 120_000 }) + if (result.code !== 0) { + throw new Error(`ConPTY extraction failed: ${result.stderr || result.stdout}`) + } + const selected = join(temporary, 'selected') + mkdirSync(selected) + copyFileSync( + join(extracted, 'runtimes', `win-${arch}`, 'native', 'conpty.dll'), + join(selected, 'conpty.dll') + ) + copyFileSync( + join(extracted, 'build', 'native', 'runtimes', arch, 'OpenConsole.exe'), + join(selected, 'OpenConsole.exe') + ) + verifyConptyDirectory(selected, arch) + copyFileSync(licensePath, join(selected, 'LICENSE.txt')) + copyFileSync( + join(extracted, 'Microsoft.Windows.Console.ConPTY.nuspec'), + join(selected, 'Microsoft.Windows.Console.ConPTY.nuspec') + ) + writeFileSync( + join(selected, 'conpty.json'), + `${JSON.stringify( + { + version: WINDOWS_CONPTY_VERSION, + arch, + source: WINDOWS_CONPTY_ARCHIVE.url, + archiveSha256: WINDOWS_CONPTY_ARCHIVE.sha256, + files: WINDOWS_CONPTY_FILES[arch] + }, + null, + 2 + )}\n` + ) + mkdirSync(outputDir, { recursive: true }) + for (const filename of [ + ...Object.keys(WINDOWS_CONPTY_FILES[arch]), + 'LICENSE.txt', + 'Microsoft.Windows.Console.ConPTY.nuspec', + 'conpty.json' + ]) { + copyFileSync(join(selected, filename), join(outputDir, filename)) + } + verifyConptyDirectory(outputDir, arch) + return { directory: outputDir, library: join(outputDir, 'conpty.dll') } + } finally { + rmSync(temporary, { recursive: true, force: true }) + } +} + +if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) { + const arch = process.argv[2] ?? process.arch + const output = resolve(process.argv[3] ?? join(root, 'out', 'windows-conpty', arch)) + await materializeWindowsConpty(arch, output) + console.log(`ConPTY ${WINDOWS_CONPTY_VERSION} ${arch}: ${output}`) +} diff --git a/config/scripts/build-windows-conpty.test.mjs b/config/scripts/build-windows-conpty.test.mjs new file mode 100644 index 000000000000..7d50fa136455 --- /dev/null +++ b/config/scripts/build-windows-conpty.test.mjs @@ -0,0 +1,147 @@ +import { createHash } from 'node:crypto' +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +const fixture = vi.hoisted(() => ({ files: {}, archiveHash: '', archive: 'qualified archive' })) +vi.mock('../../src/shared/windows-conpty-release.ts', () => ({ + WINDOWS_CONPTY_VERSION: 'test-version', + WINDOWS_CONPTY_ARCHIVE: { + url: 'https://example.invalid/conpty.nupkg', + get sha256() { + return fixture.archiveHash + } + }, + WINDOWS_CONPTY_FILES: fixture.files +})) +vi.mock('./zip-extractor-command.mjs', () => ({ + getZipExtractorCommand: (archive, destination) => ({ + file: 'extract', + args: [archive, destination] + }) +})) +vi.mock('./script-child-process.mjs', () => ({ runProcessSync: vi.fn() })) +import { runProcessSync } from './script-child-process.mjs' +import { + downloadConptyArchive, + materializeWindowsConpty, + verifyConptyDirectory +} from './build-windows-conpty.mjs' + +const digest = (bytes) => createHash('sha256').update(bytes).digest('hex') +let root +let cacheDir +let output +let fetcher +beforeEach(() => { + root = mkdtempSync(join(tmpdir(), 'conpty-build-test-')) + cacheDir = join(root, 'cache') + output = join(root, 'output') + fixture.archiveHash = digest(fixture.archive) + for (const arch of ['x64', 'arm64']) { + fixture.files[arch] = { + 'conpty.dll': digest(`${arch} DLL`), + 'OpenConsole.exe': digest(`${arch} EXE`) + } + } + fetcher = vi.fn(async () => new Response(fixture.archive)) + vi.mocked(runProcessSync).mockImplementation(({ args }) => { + const dir = args[1] + for (const arch of ['x64', 'arm64']) { + const dllDir = join(dir, 'runtimes', `win-${arch}`, 'native') + const exeDir = join(dir, 'build', 'native', 'runtimes', arch) + mkdirSync(dllDir, { recursive: true }) + mkdirSync(exeDir, { recursive: true }) + writeFileSync(join(dllDir, 'conpty.dll'), `${arch} DLL`) + writeFileSync(join(exeDir, 'OpenConsole.exe'), `${arch} EXE`) + } + writeFileSync(join(dir, 'Microsoft.Windows.Console.ConPTY.nuspec'), 'MIT package metadata') + return { code: 0, stdout: '', stderr: '' } + }) +}) +afterEach(() => { + rmSync(root, { recursive: true, force: true }) + vi.clearAllMocks() +}) + +describe('independent ConPTY payload builder', () => { + it.each(['x64', 'arm64'])( + 'publishes verified %s payload and preserves licensing', + async (arch) => { + await materializeWindowsConpty(arch, output, { cacheDir, fetcher }) + verifyConptyDirectory(output, arch) + expect(readFileSync(join(output, 'LICENSE.txt'), 'utf8')).toContain('MIT License') + expect( + readFileSync(join(output, 'Microsoft.Windows.Console.ConPTY.nuspec'), 'utf8') + ).toContain('MIT') + expect(JSON.parse(readFileSync(join(output, 'conpty.json'), 'utf8'))).toMatchObject({ + arch, + archiveSha256: fixture.archiveHash, + files: fixture.files[arch] + }) + expect(fetcher).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ signal: expect.any(AbortSignal) }) + ) + } + ) + + it('reuses a verified cache without network access', async () => { + await materializeWindowsConpty('x64', output, { cacheDir, fetcher }) + await materializeWindowsConpty('arm64', output, { cacheDir, fetcher }) + expect(fetcher).toHaveBeenCalledTimes(1) + }) + + it('replaces a damaged cache instead of poisoning subsequent builds', async () => { + mkdirSync(cacheDir) + writeFileSync(join(cacheDir, 'conpty.nupkg'), 'damaged') + await materializeWindowsConpty('x64', output, { cacheDir, fetcher }) + expect(readFileSync(join(cacheDir, 'conpty.nupkg'), 'utf8')).toBe(fixture.archive) + }) + + it('rejects a download hash mismatch before extraction or publication', async () => { + fetcher.mockResolvedValue(new Response('wrong archive')) + await expect(materializeWindowsConpty('x64', output, { cacheDir, fetcher })).rejects.toThrow( + 'archive checksum mismatch' + ) + expect(runProcessSync).not.toHaveBeenCalled() + expect(existsSync(output)).toBe(false) + expect(existsSync(join(cacheDir, 'conpty.nupkg'))).toBe(false) + }) + + it('rejects a failed extraction before publishing files', async () => { + vi.mocked(runProcessSync).mockReturnValue({ code: 1, stderr: 'extract failed' }) + await expect(materializeWindowsConpty('x64', output, { cacheDir, fetcher })).rejects.toThrow( + 'extraction failed' + ) + expect(existsSync(output)).toBe(false) + }) + + it('rejects mismatched architecture bytes before publication', async () => { + fixture.files.arm64['conpty.dll'] = digest('foreign DLL') + await expect(materializeWindowsConpty('arm64', output, { cacheDir, fetcher })).rejects.toThrow( + 'checksum mismatch: arm64/conpty.dll' + ) + expect(existsSync(output)).toBe(false) + }) + + it('rejects unsupported architectures before downloading', async () => { + await expect(materializeWindowsConpty('x86', output, { cacheDir, fetcher })).rejects.toThrow( + 'Unsupported' + ) + expect(fetcher).not.toHaveBeenCalled() + }) + + it('bounds download memory and does not retain oversized archives', async () => { + fetcher.mockResolvedValue(new Response(new Uint8Array(16 * 1024 * 1024 + 1))) + const archive = join(root, 'oversize') + await expect(downloadConptyArchive(archive, fetcher)).rejects.toThrow('size limit') + expect(existsSync(archive)).toBe(false) + }) + + it('reports HTTP errors without caching the error body', async () => { + fetcher.mockResolvedValue(new Response('missing', { status: 404 })) + await expect(downloadConptyArchive(join(root, 'download'), fetcher)).rejects.toThrow('HTTP 404') + }) +}) diff --git a/config/scripts/bun-profile-change-scope.test.mjs b/config/scripts/bun-profile-change-scope.test.mjs index 1ce91b24cd6d..3fc436c32cf4 100644 --- a/config/scripts/bun-profile-change-scope.test.mjs +++ b/config/scripts/bun-profile-change-scope.test.mjs @@ -1,4 +1,4 @@ -import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { join } from 'node:path' import { tmpdir } from 'node:os' import { afterEach, beforeAll, describe, expect, it } from 'vitest' @@ -78,7 +78,7 @@ it.each([ '.npmrc', 'tsconfig.json', 'config/tsconfig.node.json', - 'config/patches/node-pty@1.1.0.patch', + 'config/patches/@vscode__windows-process-tree@0.8.0.patch', 'native/windows-registry/src/addon.cc', '.github/actions/install-node-dependencies/action.yml', '.github/workflows/bun-profile-tests.yml', @@ -124,12 +124,22 @@ describe('the actual Bun build and profile-test dependency graph', () => { 'config/scripts/bun-cpu-baseline-smoke.mjs', 'config/scripts/runtime-serve-terminal-smoke.mjs', 'config/scripts/vitest-host-ports-setup.ts', - 'tests/e2e/daemon-running-work-probe.unit.test.ts' + 'tests/e2e/daemon-running-work-probe.unit.test.ts', + 'src/shared/terminal-unicode-provider.ts', + 'src/shared/terminal-unicode-provider.test.ts', + 'src/main/daemon/headless-emulator-fidelity.fuzz.test.ts' ])('retains the full matrix for a real runtime, worker or test input: %s', async (file) => { expect(inputs.has(file)).toBe(true) expect((await classifyBunProfileChanges([file], async () => inputs)).shouldRun).toBe(true) }) + it('keeps every explicit artifact test selector on disk', () => { + const missing = bunProfileTestPaths({ artifact: true }) + .filter((path) => /\.test\.(?:ts|mjs)$/.test(path)) + .filter((path) => !existsSync(path)) + expect(missing).toEqual([]) + }) + it('retains all selected tests and uses the same selectors as the Bun runner', () => { const tests = discoverBunProfileTests() expect(tests.length).toBeGreaterThan(80) diff --git a/config/scripts/bun-profile-test-paths.mjs b/config/scripts/bun-profile-test-paths.mjs index ccdedef3ce6f..13b17f89b060 100644 --- a/config/scripts/bun-profile-test-paths.mjs +++ b/config/scripts/bun-profile-test-paths.mjs @@ -7,16 +7,25 @@ export function bunProfileTestPaths({ artifact = false } = {}) { 'src/main/sqlite', 'src/main/orcad/orcad-entry.test.ts', 'src/main/orcad/orcad-push-startup.test.ts', + 'src/shared/terminal-unicode-provider.test.ts', + 'src/main/daemon/headless-emulator-unicode-width.test.ts', + 'src/main/daemon/headless-emulator-fidelity.fuzz.test.ts', ...(artifact ? [ 'src/main/daemon/pty-subprocess/bun-pty-process.integration.test.ts', + 'src/main/pty/posix-pty-process-groups.integration.test.ts', 'src/main/daemon/pty-subprocess/bun-pty-job-control.integration.test.ts', 'src/main/daemon/pty-subprocess/bun-pty-process-suspension.test.ts', 'src/main/daemon/pty-subprocess-spawn-file-foreground.test.ts', 'src/main/daemon/pty-subprocess/spawn-file-foreground-rejected-agents.test.ts', 'tests/e2e/daemon-running-work-probe.unit.test.ts', 'src/main/daemon/pty-subprocess/windows-bun-pty-gate.integration.test.ts', - 'src/main/providers/local-pty-bun-artifact.integration.test.ts', + 'src/main/windows/windows-pty-job.win32.test.ts', + 'src/main/daemon/bun-pty-windows-io-failure.win32.test.ts', + 'src/main/windows/windows-msys-job.win32.test.ts', + 'src/main/providers/windows-conpty-wide-char-duplication.bun.test.ts', + 'src/main/providers/pty-repaint-wide-char-buffer.bun.test.ts', + 'src/main/daemon/daemon-bun-pty-artifact.integration.test.ts', 'src/main/providers/agent-foreground-process-git-bash.win32.test.ts', 'src/main/orcad/orcad-bun-launcher.integration.test.ts', 'config/scripts/zip-extractor-command.test.mjs', diff --git a/config/scripts/bundled-cli-runtime-fixture.mjs b/config/scripts/bundled-cli-runtime-fixture.mjs index 783e0c8d6768..848199aa86c4 100644 --- a/config/scripts/bundled-cli-runtime-fixture.mjs +++ b/config/scripts/bundled-cli-runtime-fixture.mjs @@ -1,17 +1,26 @@ +import { createRequire } from 'node:module' +import { afterEach, beforeEach } from 'vitest' import { ORCAD_BUN_VERSION } from '../../src/shared/orcad-bun-runtime.ts' import { createHash } from 'node:crypto' import { copyFile, mkdir, writeFile } from 'node:fs/promises' import { join } from 'node:path' import { cliRuntimeTarget, cliRuntimeFilename } from '../bundled-cli-runtime.cjs' -export async function writeBundledCliRuntimeFixture(directory, platform, arch) { - const bytes = 'runtime fixture' +export async function writeBundledCliRuntimeFixture( + directory, + platform, + arch, + bytes = 'runtime fixture' +) { await mkdir(directory, { recursive: true }) - await writeFile(join(directory, cliRuntimeFilename(platform)), bytes) + await writeFile(join(directory, cliRuntimeFilename(platform)), bytes, { mode: 0o755 }) await copyFile( join(import.meta.dirname, '../../resources/licenses/bun/LICENSE.md'), join(directory, 'LICENSE.md') ) + if (platform === 'win32') { + await writeBundledConptyFixture(join(directory, 'conpty'), arch) + } await writeFile( join(directory, 'runtime.json'), JSON.stringify({ @@ -21,3 +30,53 @@ export async function writeBundledCliRuntimeFixture(directory, platform, arch) { }) ) } + +// CJS packaging verification reads native require's module cache, outside Vitest's ESM mocks. +const require = createRequire(import.meta.url) +const release = require('../../src/shared/windows-conpty-release.ts') +const originalFiles = structuredClone(release.WINDOWS_CONPTY_FILES) +const originalNuspecHash = release.WINDOWS_CONPTY_ARCHIVE.nuspecSha256 +const fixtureArchitectures = new Set() +function mockFixtureHashes(arch) { + for (const filename of Object.keys(release.WINDOWS_CONPTY_FILES[arch])) { + release.WINDOWS_CONPTY_FILES[arch][filename] = createHash('sha256') + .update(`${arch}/${filename} fixture`) + .digest('hex') + } + release.WINDOWS_CONPTY_ARCHIVE.nuspecSha256 = createHash('sha256') + .update('ConPTY MIT package fixture') + .digest('hex') +} +beforeEach(() => fixtureArchitectures.forEach(mockFixtureHashes)) +afterEach(() => { + for (const arch of Object.keys(originalFiles)) { + Object.assign(release.WINDOWS_CONPTY_FILES[arch], originalFiles[arch]) + } + release.WINDOWS_CONPTY_ARCHIVE.nuspecSha256 = originalNuspecHash +}) + +export async function writeBundledConptyFixture(directory, arch) { + fixtureArchitectures.add(arch) + mockFixtureHashes(arch) + await mkdir(directory, { recursive: true }) + for (const filename of Object.keys(release.WINDOWS_CONPTY_FILES[arch])) { + const bytes = `${arch}/${filename} fixture` + await writeFile(join(directory, filename), bytes) + } + const nuspec = 'ConPTY MIT package fixture' + await writeFile(join(directory, 'Microsoft.Windows.Console.ConPTY.nuspec'), nuspec) + await copyFile( + join(import.meta.dirname, '../licenses/windows-conpty-LICENSE.txt'), + join(directory, 'LICENSE.txt') + ) + await writeFile( + join(directory, 'conpty.json'), + JSON.stringify({ + version: release.WINDOWS_CONPTY_VERSION, + arch, + source: release.WINDOWS_CONPTY_ARCHIVE.url, + archiveSha256: release.WINDOWS_CONPTY_ARCHIVE.sha256, + files: release.WINDOWS_CONPTY_FILES[arch] + }) + ) +} diff --git a/config/scripts/bundled-cli-runtime.test.mjs b/config/scripts/bundled-cli-runtime.test.mjs index b5987e6deb80..f1d63a890bc1 100644 --- a/config/scripts/bundled-cli-runtime.test.mjs +++ b/config/scripts/bundled-cli-runtime.test.mjs @@ -1,3 +1,4 @@ +import { writeBundledConptyFixture } from './bundled-cli-runtime-fixture.mjs' import { ORCAD_BUN_VERSION } from '../../src/shared/orcad-bun-runtime.ts' import { createHash } from 'node:crypto' import { @@ -16,6 +17,7 @@ import { cliRuntimeTarget, cliRuntimeFilename, cliRuntimeExtraResource, + verifyWindowsConptyDirectory, assertBundledCliRuntimeBuilt } from '../bundled-cli-runtime.cjs' @@ -24,7 +26,7 @@ afterEach(() => roots.splice(0).forEach((root) => rmSync(root, { recursive: true it.each(['darwin', 'linux', 'win32'])( 'verifies the exact %s architecture and executable bytes', - (platform) => { + async (platform) => { const root = mkdtempSync(join(tmpdir(), 'orca-cli-runtime-')) roots.push(root) const directory = join(root, 'out/cli-runtime', `${platform}-arm64`) @@ -43,6 +45,9 @@ it.each(['darwin', 'linux', 'win32'])( sha256: createHash('sha256').update(bytes).digest('hex') }) ) + if (platform === 'win32') { + await writeBundledConptyFixture(join(directory, 'conpty'), 'arm64') + } expect(() => assertBundledCliRuntimeBuilt(platform, 3, root)).not.toThrow() expect(() => assertBundledCliRuntimeBuilt(platform, 1, root)).toThrow('build-cli-runtime.mjs') const manifestPath = join(directory, 'runtime.json') @@ -64,9 +69,29 @@ it('packages only the selected platform/architecture and rejects unsupported tar expect(cliRuntimeExtraResource('win32')).toEqual({ from: 'out/cli-runtime/win32-${arch}', to: 'cli-runtime', - filter: ['bun-runtime.exe', 'runtime.json', 'LICENSE.md'] + filter: ['bun-runtime.exe', 'runtime.json', 'LICENSE.md', 'conpty/**'] }) expect(cliRuntimeTarget('linux', 'x64')).toBe('linux-x64-glibc') expect(() => cliRuntimeTarget('linux', 'ia32')).toThrow('Unsupported') expect(() => cliRuntimeTarget('freebsd', 'x64')).toThrow('Unsupported') }) + +it.each([ + 'conpty.dll', + 'OpenConsole.exe', + 'LICENSE.txt', + 'Microsoft.Windows.Console.ConPTY.nuspec' +])('rejects missing and corrupted provider %s independently of the manifest', async (filename) => { + const root = mkdtempSync(join(tmpdir(), 'orca-conpty-package-')) + roots.push(root) + await writeBundledConptyFixture(root, 'x64') + expect(() => verifyWindowsConptyDirectory(root, 'x64')).not.toThrow() + expect(() => verifyWindowsConptyDirectory(root, 'arm64')).toThrow('manifest') + unlinkSync(join(root, filename)) + expect(() => verifyWindowsConptyDirectory(root, 'x64')).toThrow() + writeFileSync(join(root, filename), 'corrupt') + const manifest = JSON.parse(readFileSync(join(root, 'conpty.json'), 'utf8')) + manifest.files[filename] = createHash('sha256').update('corrupt').digest('hex') + writeFileSync(join(root, 'conpty.json'), JSON.stringify(manifest)) + expect(() => verifyWindowsConptyDirectory(root, 'x64')).toThrow() +}) diff --git a/config/scripts/capture-agent-pty-transcript.mjs b/config/scripts/capture-agent-pty-transcript.mjs index a60d0adbdd5b..3586af51320a 100644 --- a/config/scripts/capture-agent-pty-transcript.mjs +++ b/config/scripts/capture-agent-pty-transcript.mjs @@ -19,7 +19,6 @@ import { const REPO_ROOT = resolve(import.meta.dirname, '..', '..') const FIXTURE_DIR = join(REPO_ROOT, 'src', 'main', 'runtime', '__fixtures__') -const STOP_KEY = 0x1d // Ctrl-], consumed by the recorder and never forwarded to the agent. const NAME_RE = /^[a-z0-9][a-z0-9-]*$/ const USAGE = `Capture a raw agent PTY transcript into src/main/runtime/__fixtures__/. @@ -121,10 +120,6 @@ function runScan(files, redact) { } function resolveSpawn(command) { - // node-pty cannot run a .cmd/.bat shim directly on Windows; those need cmd.exe. - if (process.platform === 'win32' && /\.(cmd|bat)$/i.test(command[0])) { - return { file: 'cmd.exe', args: ['/c', `"${command[0]}"`, ...command.slice(1)] } - } return { file: command[0], args: command.slice(1) } } @@ -137,87 +132,33 @@ async function runCapture(options, command) { const outPath = options.out ? resolve(options.out) : join(FIXTURE_DIR, `${name}.txt`) mkdirSync(dirname(outPath), { recursive: true }) - const pty = await import('node-pty').catch((error) => { - console.error( - `node-pty failed to load. Build it for plain node first: - node config/scripts/ensure-native-runtime.mjs --runtime=node -${String(error)}` - ) - return null - }) - if (pty === null) { - return 2 - } - + const { spawnTranscriptPty } = await import('./pty-transcript-bun-runtime.mjs') + const { recordPtyTranscript } = await import('./pty-transcript-recording.mjs') const cols = options.cols ?? process.stdout.columns ?? 120 const rows = options.rows ?? process.stdout.rows ?? 40 const { file, args } = resolveSpawn(command) - const term = pty.spawn(file, args, { - name: 'xterm-256color', - cols, - rows, - cwd: process.cwd(), - env: { ...process.env, TERM: 'xterm-256color' }, - encoding: null - }) - - const sink = createWriteStream(outPath) - let recording = true - term.onData((chunk) => { - const bytes = typeof chunk === 'string' ? Buffer.from(chunk, 'utf8') : chunk - // Why recording stops before the kill: an agent repaints an idle frame on its way out, so - // a transcript that keeps writing through shutdown ends on that frame instead of on the - // state you stopped to capture. A mid-turn or dialog capture cannot survive that. - if (recording) { - sink.write(bytes) - } - process.stdout.write(bytes) - }) - - const wasRaw = process.stdin.isTTY === true && process.stdin.isRaw === true - if (process.stdin.isTTY) { - process.stdin.setRawMode(true) - } - process.stdin.resume() - let stopping = false - const stop = () => { - if (stopping) { - return - } - stopping = true - recording = false - try { - term.kill() - } catch { - // The agent may have exited on its own; the transcript is already on disk. - } - } - process.stdin.on('data', (chunk) => { - if (chunk.includes(STOP_KEY)) { - stop() - return - } - term.write(chunk.toString('binary')) - }) - // Why scripted input: a dialog capture has to be driven, and CI (or an agent) has no TTY to - // type into. The keystrokes ride the same PTY a human's would, so the capture is unchanged. - const sendTimers = options.sends.map((send) => setTimeout(() => term.write(send.text), send.atMs)) - const durationTimer = options.duration === null ? null : setTimeout(stop, options.duration * 1000) - - const exitCode = await new Promise((resolveExit) => { - term.onExit(({ exitCode: code }) => resolveExit(code ?? 0)) + const env = Object.fromEntries( + Object.entries(process.env).filter(([, value]) => value !== undefined) + ) + const exitCode = await recordPtyTranscript({ + spawn: (onBytes) => + spawnTranscriptPty( + { + file, + args, + cols, + rows, + cwd: process.cwd(), + env: { ...env, TERM: 'xterm-256color' }, + windowsJobKillOnClose: true + }, + onBytes + ), + sink: createWriteStream(outPath), + stdin: process.stdin, + stdout: process.stdout, + options }) - for (const timer of sendTimers) { - clearTimeout(timer) - } - if (durationTimer !== null) { - clearTimeout(durationTimer) - } - if (process.stdin.isTTY) { - process.stdin.setRawMode(wasRaw) - } - process.stdin.pause() - await new Promise((done) => sink.end(done)) writeMeta(outPath, { command, cols, rows, note: options.note ?? null, exitCode }) const findings = scanTranscriptForSecrets(readFileSync(outPath, 'utf8')) @@ -265,7 +206,9 @@ async function main() { console.error(USAGE) return 2 } - return runCapture(options, command) + const { relaunchTranscriptWithBun } = await import('./pty-transcript-bun-runtime.mjs') + const relaunched = await relaunchTranscriptWithBun(REPO_ROOT, process.argv.slice(2)) + return relaunched ?? runCapture(options, command) } if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { diff --git a/config/scripts/check-runtime-electron-ratchet.mjs b/config/scripts/check-runtime-electron-ratchet.mjs index 7c165bfc02b7..9489878390c7 100644 --- a/config/scripts/check-runtime-electron-ratchet.mjs +++ b/config/scripts/check-runtime-electron-ratchet.mjs @@ -45,7 +45,6 @@ const ENTRY_POINTS = [ // relay build already does (config/scripts/build-relay.mjs). const EXTERNAL = [ 'electron', - 'node-pty', '@parcel/watcher', 'better-sqlite3', 'keytar', diff --git a/config/scripts/ci-shard-timings.json b/config/scripts/ci-shard-timings.json index e7e3e279f366..40cc08547597 100644 --- a/config/scripts/ci-shard-timings.json +++ b/config/scripts/ci-shard-timings.json @@ -63,7 +63,6 @@ "config/scripts/electron-builder-speech-config.test.mjs": 270, "config/scripts/electron-runtime-floor.test.ts": 36, "config/scripts/electron-vite-output-contract.test.ts": 847, - "config/scripts/ensure-native-runtime-job-ownership.test.mjs": 59, "config/scripts/ensure-native-runtime.test.mjs": 679, "config/scripts/ephemeral-vm-runtime-store-cross-version.test.ts": 37, "config/scripts/generate-bundled-skill-guides.test.mjs": 625, @@ -164,9 +163,6 @@ "config/scripts/mobile-web-source-line-endings.test.mjs": 646, "config/scripts/mobile-web-terminal-engine-closure.test.mjs": 73, "config/scripts/node-old-space-limit.test.mjs": 49, - "config/scripts/node-pty-console-list-agent-patch.test.mjs": 81, - "config/scripts/node-pty-master-cloexec-patch.test.mjs": 117, - "config/scripts/node-pty-windows-pty-teardown-patch.test.mjs": 81, "config/scripts/orca-cli-skill-guidance.test.mjs": 46, "config/scripts/orca-dev-bin.test.mjs": 115, "config/scripts/orca-linear-skill-guidance.test.mjs": 76, @@ -180,7 +176,6 @@ "config/scripts/package-electron-runtime-contract.test.mjs": 260, "config/scripts/packaged-browser-lane-contract.test.mjs": 70, "config/scripts/packaged-hang-watchdog-worker-contract.test.mjs": 107, - "config/scripts/packaged-node-pty-prebuild-prune.test.mjs": 85, "config/scripts/packaged-source-map-prune.test.mjs": 93, "config/scripts/patched-dependencies-frozen-install.test.mjs": 699, "config/scripts/plain-node-entry-guard.test.ts": 700, @@ -197,7 +192,6 @@ "config/scripts/pty-transcript-secret-scan.test.mjs": 82, "config/scripts/publish-complete-draft-releases.test.mjs": 190, "config/scripts/quadratic-buffer-concat-plugin.test.mjs": 2300, - "config/scripts/rebuild-native-deps-node-pty.test.mjs": 3978, "config/scripts/rebuild-native-deps-windows-process-tree.test.mjs": 34, "config/scripts/rebuild-native-deps.test.mjs": 1038, "config/scripts/reclaim-dev-electron-bundles.test.ts": 296, @@ -217,7 +211,6 @@ "config/scripts/resolve-7za-path.test.mjs": 1528, "config/scripts/run-codex-real-account-validation.test.ts": 3022, "config/scripts/run-internal-dev-setup.test.mjs": 65, - "config/scripts/run-linux-packaged-node-pty-floor-smoke.test.mjs": 32, "config/scripts/run-terminal-scale-perf-report-gate.test.mjs": 54, "config/scripts/run-typecheck-projects-in-parallel.test.mjs": 48, "config/scripts/script-entry-detection.test.mjs": 93, @@ -249,7 +242,6 @@ "config/scripts/verify-packaged-browser-participation.test.mjs": 71, "config/scripts/verify-packaged-daemon-entry.test.mjs": 192, "config/scripts/verify-packaged-mobile-web-bundle.test.mjs": 507, - "config/scripts/verify-packaged-node-pty-job-ownership.test.mjs": 94, "config/scripts/verify-packaged-plugin-resources.test.mjs": 91, "config/scripts/verify-release-required-assets.test.mjs": 60, "config/scripts/verify-skills-cli-runtime.test.mjs": 366, @@ -1375,7 +1367,7 @@ "src/main/crash-reporting/self-initiated-tree-kill-log.test.ts": 222, "src/main/crash-reporting/suppressed-process-gone-breadcrumb.test.ts": 54, "src/main/cursor/hook-service.test.ts": 289, - "src/main/daemon/agent-startup-prompt-latency.node-pty.test.ts": 2437, + "src/main/daemon/agent-startup-prompt-latency.bun.test.ts": 2437, "src/main/daemon/bash-prompt-command-composition.test.ts": 4252, "src/main/daemon/client.test.ts": 403, "src/main/daemon/cold-restore-payload-cache.test.ts": 54, @@ -1474,7 +1466,6 @@ "src/main/daemon/daemon-tcc-attribution-main-thread.test.ts": 76, "src/main/daemon/daemon-tcc-attribution.test.ts": 71, "src/main/daemon/daemon-transport-attachment-release.test.ts": 714, - "src/main/daemon/degraded-daemon-pty-provider.test.ts": 127, "src/main/daemon/hangul-cell-width-agreement.test.ts": 82, "src/main/daemon/headless-emulator-fidelity.fuzz.test.ts": 18351, "src/main/daemon/headless-emulator-restored-osc-links.test.ts": 154, @@ -1494,7 +1485,6 @@ "src/main/daemon/macos-login-session-death-watch.test.ts": 104, "src/main/daemon/ndjson.test.ts": 206, "src/main/daemon/node-pty-error-hints.test.ts": 54, - "src/main/daemon/node-pty-windows-input-error.win32.test.ts": 46, "src/main/daemon/osc7-file-uri.test.ts": 59, "src/main/daemon/osc7-uri-extraction.test.ts": 32, "src/main/daemon/post-ready-flush-gate.test.ts": 41, @@ -2936,25 +2926,8 @@ "src/main/providers/agent-foreground-process-wsl-shell-proof.test.ts": 95, "src/main/providers/agent-foreground-process.test.ts": 232, "src/main/providers/execution-host-provider-dispatch.test.ts": 54, - "src/main/providers/local-pty-child-process-verdict.test.ts": 311, - "src/main/providers/local-pty-default-shell.test.ts": 148, - "src/main/providers/local-pty-foreground-inspection-cheap-tier.test.ts": 189, - "src/main/providers/local-pty-pi-owner-env.test.ts": 92, - "src/main/providers/local-pty-provider-foreground-process.test.ts": 311, - "src/main/providers/local-pty-provider-io-events.test.ts": 291, - "src/main/providers/local-pty-provider-session-inventory.test.ts": 278, - "src/main/providers/local-pty-provider-shell-readiness.test.ts": 296, - "src/main/providers/local-pty-provider-shutdown.test.ts": 547, - "src/main/providers/local-pty-provider-spawn-cwd-safety.test.ts": 295, - "src/main/providers/local-pty-provider-spawn-env.test.ts": 363, - "src/main/providers/local-pty-provider-spawn-session.test.ts": 293, - "src/main/providers/local-pty-provider-windows-shell-launch.test.ts": 325, "src/main/providers/local-pty-shell-ready-marker-scan.test.ts": 70, - "src/main/providers/local-pty-shell-ready-startup-command.test.ts": 50, "src/main/providers/local-pty-shell-ready-wrapper-generation.test.ts": 652, - "src/main/providers/local-pty-shell-startup-command.node-pty.test.ts": 260, - "src/main/providers/local-pty-utils-windows-fallback.test.ts": 51, - "src/main/providers/local-pty-utils.test.ts": 67, "src/main/providers/macos-login-session-pty-probe.test.ts": 66, "src/main/providers/macos-tcc-login-shell.test.ts": 133, "src/main/providers/posix-pane-foreground-fingerprint.test.ts": 51, @@ -2963,7 +2936,7 @@ "src/main/providers/pty-default-cwd.test.ts": 63, "src/main/providers/pty-process-inspection.test.ts": 32, "src/main/providers/pty-process-list-admission.test.ts": 140, - "src/main/providers/pty-repaint-wide-char-buffer.node-pty.test.ts": 1987, + "src/main/providers/pty-repaint-wide-char-buffer.bun.test.ts": 1987, "src/main/providers/settled-pty-writer-census.test.ts": 805, "src/main/providers/ssh-filesystem-dispatch.test.ts": 44, "src/main/providers/ssh-filesystem-doc-preview.test.ts": 62, @@ -3009,7 +2982,7 @@ "src/main/providers/terminal-path-existence-batch.integration.test.ts": 183, "src/main/providers/windows-agent-foreground-process-scan-volume.test.ts": 95, "src/main/providers/windows-cached-agent-revalidation.test.ts": 57, - "src/main/providers/windows-conpty-wide-char-duplication.node-pty.test.ts": 72, + "src/main/providers/windows-conpty-wide-char-duplication.bun.test.ts": 72, "src/main/providers/windows-console-attached-processes.test.ts": 66, "src/main/providers/windows-foreground-process-inspection-cost.test.ts": 70, "src/main/providers/windows-foreground-process-rows.test.ts": 61, @@ -3034,8 +3007,6 @@ "src/main/pty/conda-activation-env.test.ts": 50, "src/main/pty/legacy-terminal-shim-dir.test.ts": 487, "src/main/pty/legacy-terminal-windows-tombstone.test.ts": 79, - "src/main/pty/node-pty-master-fd-retirement.test.ts": 5186, - "src/main/pty/node-pty-self-exit-pseudoconsole-close.test.ts": 50, "src/main/pty/omp-sqlite-overlay.test.ts": 67, "src/main/pty/overlay-mirror.test.ts": 71, "src/main/pty/pi-process-owner-env.test.ts": 1019, @@ -4241,7 +4212,6 @@ "src/main/ssh/ssh-relay-node-headers.test.ts": 930, "src/main/ssh/ssh-relay-node-pty-repair.test.ts": 124, "src/main/ssh/ssh-relay-node-pty-spawn-repair.test.ts": 194, - "src/main/ssh/ssh-relay-offline-node-headers.docker.test.ts": 234, "src/main/ssh/ssh-relay-orphan-abandon-paths.test.ts": 2548, "src/main/ssh/ssh-relay-pty-master-cloexec-install.test.ts": 2094, "src/main/ssh/ssh-relay-reset.test.ts": 188, @@ -9223,7 +9193,6 @@ "src/shared/node-bounded-json-stringify.test.ts": 59, "src/shared/node-file-content-equality.test.ts": 58, "src/shared/node-markdown-document-discovery.test.ts": 48, - "src/shared/node-pty-spawn-helper.test.ts": 41, "src/shared/node-readable-text.test.ts": 69, "src/shared/node-source-copy-content-equality.test.ts": 54, "src/shared/notebook-venv-location.test.ts": 30, @@ -9682,8 +9651,6 @@ "tests/tools/relay-bench/relay-bench-state-file.test.mjs": 66, "tests/tools/relay-bench/relay-phone-connect-bench.test.mjs": 106, "tests/tools/win-update-e2e/app-driver.test.mjs": 753, - "tests/tools/windows-pty-native-capability-smoke/packaged-node-pty-capability-oracle.test.mjs": 61, - "tests/tools/windows-pty-native-capability-smoke/packaged-node-pty-capability-probe.test.mjs": 108, "tests/tools/windows-pty-native-capability-smoke/run.test.mjs": 53 } }, diff --git a/config/scripts/computer-e2e-workflow.test.mjs b/config/scripts/computer-e2e-workflow.test.mjs index fc605bac5bdc..df1845dc740e 100644 --- a/config/scripts/computer-e2e-workflow.test.mjs +++ b/config/scripts/computer-e2e-workflow.test.mjs @@ -78,6 +78,25 @@ describe('computer-use e2e workflow', () => { expect(triggerPaths).not.toContain('src/shared/runtime-types.ts') }) + it('prepares pinned Bun and ConPTY before native Windows shell fixtures', () => { + const workflow = parse( + readFileSync(join(projectDir, '.github/workflows/computer-e2e.yml'), 'utf8') + ) + const steps = workflow.jobs['native-smoke'].steps + const preparation = steps.findIndex((step) => + step.run?.includes('await buildCliRuntime(process.platform, process.arch)') + ) + const fixtures = steps.findIndex((step) => + step.run?.includes('windows-shell-preflight-runtime.windows.test.ts') + ) + expect(preparation).toBeGreaterThan(-1) + expect(steps[preparation].if).toBe("runner.os == 'Windows'") + expect(preparation).toBeLessThan(fixtures) + const gateBuild = steps.findIndex((step) => step.run === 'pnpm run build:terminal-daemon') + expect(gateBuild).toBeGreaterThan(-1) + expect(gateBuild).toBeLessThan(fixtures) + }) + it('runs focused computer-use regression tests in the PR native-smoke job', () => { const workflow = parse( readFileSync(join(projectDir, '.github/workflows/computer-e2e.yml'), 'utf8') diff --git a/config/scripts/daemon-boot-smoke.mjs b/config/scripts/daemon-boot-smoke.mjs index d9796f3a9623..abd799e89ff9 100644 --- a/config/scripts/daemon-boot-smoke.mjs +++ b/config/scripts/daemon-boot-smoke.mjs @@ -1,21 +1,5 @@ -/** - * Boots the BUILT terminal daemon (out/main/daemon-entry.js) under plain Node — - * the exact way production forks it (ELECTRON_RUN_AS_NODE = a plain-Node - * process) — and asserts it starts, serves a real PTY, and stops. - * - * Why this exists: native-smoke CI (and packaging) went green while - * v1.4.129-rc.1 shipped a daemon that exited code 1 at module load because an - * electron `require` leaked into its bundle graph. Nothing executed the built - * entry under plain Node, so the outage was invisible until an adopted old - * daemon died in the field. This runs on every PR that touches the daemon. - * - * Hard assertions (fail the job): - * - the daemon signals `{ type: 'ready' }` over IPC within the timeout, and - * - it terminates when asked (no hang / zombie). - * Best-effort (logged skip, never fails): an end-to-end `ptySpawnHealth` RPC, - * because node-pty spawn can be flaky on constrained CI runners. - */ -import { fork } from 'node:child_process' +/** Boots the shipped standalone daemon with its verified Bun runtime and a real PTY. */ +import { daemonSmokeEntry, spawnDaemonSmoke } from './daemon-smoke-launch.mjs' import { connect } from 'node:net' import { randomUUID } from 'node:crypto' import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync } from 'node:fs' @@ -23,7 +7,7 @@ import { tmpdir } from 'node:os' import { join, resolve } from 'node:path' const projectDir = resolve(import.meta.dirname, '../..') -const entryPath = join(projectDir, 'out', 'main', 'daemon-entry.js') +const entryPath = daemonSmokeEntry(projectDir) const READY_TIMEOUT_MS = 30_000 const PTY_HEALTH_TIMEOUT_MS = 10_000 @@ -132,21 +116,14 @@ function runDaemonRpc(socketPath, tokenPath, protocolVersion, request, timeoutMs }) } -// Best-effort: constrained CI runners can make node-pty spawn flaky. async function runPtySpawnHealthCheck(socketPath, tokenPath, protocolVersion) { - try { - await runDaemonRpc( - socketPath, - tokenPath, - protocolVersion, - { id: 'health-1', type: 'ptySpawnHealth' }, - PTY_HEALTH_TIMEOUT_MS - ) - return true - } catch (error) { - log(`PTY spawn health check skipped (best-effort): ${error.message}`) - return false - } + await runDaemonRpc( + socketPath, + tokenPath, + protocolVersion, + { id: 'health-1', type: 'ptySpawnHealth' }, + PTY_HEALTH_TIMEOUT_MS + ) } async function main() { @@ -157,31 +134,21 @@ async function main() { const launchNonce = randomUUID() const protocolVersion = readProtocolVersion() - log(`forking ${entryPath} under plain Node (${process.execPath})`) - const child = fork( + log(`launching ${entryPath} under verified bundled Bun`) + const child = spawnDaemonSmoke(projectDir, userDataDir, [ + '--socket', + socketPath, + '--token', + tokenPath, + '--pid-record', + pidPath, + '--launch-nonce', + launchNonce, + '--entry-path', entryPath, - [ - '--socket', - socketPath, - '--token', - tokenPath, - '--pid-record', - pidPath, - '--launch-nonce', - launchNonce, - '--entry-path', - entryPath, - '--app-version', - 'daemon-boot-smoke' - ], - { - // Plain Node: no ELECTRON_RUN_AS_NODE. process.execPath is already node in - // CI, and this is exactly the runtime where a leaked `require("electron")` - // throws MODULE_NOT_FOUND — the failure this smoke exists to catch. - stdio: ['ignore', 'ignore', 'pipe', 'ipc'], - env: { ...process.env, ORCA_USER_DATA_PATH: userDataDir } - } - ) + '--app-version', + 'daemon-boot-smoke' + ]) let stderr = '' child.stderr?.on('data', (chunk) => { @@ -252,10 +219,8 @@ async function main() { stderr += '[boot-smoke] stderr released at readiness, mirroring production\n' child.disconnect() - const ptyHealthy = await runPtySpawnHealthCheck(socketPath, tokenPath, protocolVersion) - if (ptyHealthy) { - log('ptySpawnHealth OK — daemon spawned a real PTY end-to-end') - } + await runPtySpawnHealthCheck(socketPath, tokenPath, protocolVersion) + log('ptySpawnHealth OK — daemon spawned a real PTY end-to-end') await new Promise((resolveExit, rejectExit) => { const timer = setTimeout(() => { @@ -298,7 +263,7 @@ async function main() { throw new Error(`daemon leaked private bind names: ${leaked.join(', ')}`) } - log('PASS: daemon booted, served, and shut down under plain Node') + log('PASS: daemon booted, served, and shut down under bundled Bun') } finally { cleanup() } diff --git a/config/scripts/daemon-endpoint-handover-smoke.mjs b/config/scripts/daemon-endpoint-handover-smoke.mjs index 729b332c67f0..9d459e740bc8 100644 --- a/config/scripts/daemon-endpoint-handover-smoke.mjs +++ b/config/scripts/daemon-endpoint-handover-smoke.mjs @@ -9,7 +9,7 @@ * * Usage: node config/scripts/daemon-endpoint-handover-smoke.mjs */ -import { fork } from 'node:child_process' +import { daemonSmokeEntry, spawnDaemonSmoke } from './daemon-smoke-launch.mjs' import { connect } from 'node:net' import { randomUUID } from 'node:crypto' import { existsSync, mkdtempSync, readFileSync, rmSync, statSync } from 'node:fs' @@ -17,7 +17,7 @@ import { tmpdir } from 'node:os' import { join, resolve } from 'node:path' const repoRoot = resolve(import.meta.dirname, '..', '..') -const entryPath = join(repoRoot, 'out', 'main', 'daemon-entry.js') +const entryPath = daemonSmokeEntry(repoRoot) // Why read it from source: the launcher keys adoption of a live incumbent on this exact code, // and hardcoding it here would let the two drift silently — which is the failure the assertion @@ -45,27 +45,20 @@ function readProtocolVersion() { function bootDaemon(tag, dir, socketPath) { const tokenPath = join(dir, `${tag}.token`) const pidPath = join(dir, `${tag}.pid`) - const child = fork( + const child = spawnDaemonSmoke(repoRoot, dir, [ + '--socket', + socketPath, + '--token', + tokenPath, + '--pid-record', + pidPath, + '--launch-nonce', + randomUUID(), + '--entry-path', entryPath, - [ - '--socket', - socketPath, - '--token', - tokenPath, - '--pid-record', - pidPath, - '--launch-nonce', - randomUUID(), - '--entry-path', - entryPath, - '--app-version', - 'endpoint-handover-smoke' - ], - { - stdio: ['ignore', 'ignore', 'pipe', 'ipc'], - env: { ...process.env, ORCA_USER_DATA_PATH: dir } - } - ) + '--app-version', + 'endpoint-handover-smoke' + ]) let stderr = '' child.stderr?.on('data', (chunk) => { stderr += chunk.toString('utf8') diff --git a/config/scripts/daemon-smoke-launch.mjs b/config/scripts/daemon-smoke-launch.mjs new file mode 100644 index 000000000000..bef6c5ddf8df --- /dev/null +++ b/config/scripts/daemon-smoke-launch.mjs @@ -0,0 +1,54 @@ +import { bunOwnedRuntimeArgs } from '../../src/shared/bun-owned-runtime-args.ts' +import { spawn } from 'node:child_process' +import { existsSync } from 'node:fs' +import { join } from 'node:path' +import { cliRuntimeFilename, verifyCliRuntimeDirectory } from '../bundled-cli-runtime.cjs' + +export function daemonSmokeEntry(projectDir) { + return join(projectDir, 'out', 'terminal-daemon', 'daemon-entry.js') +} + +export function daemonSmokeLaunchOptions(projectDir, userDataDir, env = process.env) { + const runtimeDir = join(projectDir, 'out', 'cli-runtime', `${process.platform}-${process.arch}`) + verifyCliRuntimeDirectory(runtimeDir, process.platform, process.arch) + const entryPath = daemonSmokeEntry(projectDir) + if (!existsSync(entryPath)) { + throw new Error(`Missing ${entryPath}; run pnpm build:terminal-daemon first`) + } + const daemonEnv = { ...env, ORCA_BACKGROUND_LAUNCH: '1', ORCA_USER_DATA_PATH: userDataDir } + for (const key of [ + 'ELECTRON_RUN_AS_NODE', + 'NODE_OPTIONS', + 'NODE_PATH', + 'BUN_OPTIONS', + 'BUN_INSPECT', + 'BUN_INSPECT_BRK', + 'BUN_INSPECT_WAIT', + 'BUN_CONPTY_LIBRARY' + ]) { + delete daemonEnv[key] + } + if (process.platform === 'win32') { + daemonEnv.BUN_CONPTY_LIBRARY = join(runtimeDir, 'conpty', 'conpty.dll') + } + return { + program: join(runtimeDir, cliRuntimeFilename(process.platform)), + entryPath, + options: { + cwd: userDataDir, + detached: true, + windowsHide: true, + stdio: ['ignore', 'ignore', 'pipe', 'ipc'], + env: daemonEnv + } + } +} + +export function spawnDaemonSmoke(projectDir, userDataDir, args) { + const launch = daemonSmokeLaunchOptions(projectDir, userDataDir) + return spawn( + launch.program, + [...bunOwnedRuntimeArgs(), launch.entryPath, ...args], + launch.options + ) +} diff --git a/config/scripts/daemon-smoke-launch.test.mjs b/config/scripts/daemon-smoke-launch.test.mjs new file mode 100644 index 000000000000..eaef8fdb88d8 --- /dev/null +++ b/config/scripts/daemon-smoke-launch.test.mjs @@ -0,0 +1,102 @@ +import { test } from 'vitest' +import assert from 'node:assert/strict' +import { createHash } from 'node:crypto' +import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { daemonSmokeEntry, daemonSmokeLaunchOptions } from './daemon-smoke-launch.mjs' +import { ORCAD_BUN_VERSION } from '../../src/shared/orcad-bun-runtime.ts' + +function fixture() { + const root = mkdtempSync(join(tmpdir(), 'daemon-smoke-launch-')) + const runtime = join(root, 'out', 'cli-runtime', `${process.platform}-${process.arch}`) + mkdirSync(runtime, { recursive: true }) + mkdirSync(join(root, 'out', 'terminal-daemon')) + writeFileSync(daemonSmokeEntry(root), '// daemon fixture') + const binary = join(runtime, 'bun-runtime') + writeFileSync(binary, 'runtime fixture') + writeFileSync( + join(runtime, 'runtime.json'), + JSON.stringify({ + target: `${process.platform}-${process.arch}${process.platform === 'linux' ? '-glibc' : ''}`, + version: ORCAD_BUN_VERSION, + sha256: createHash('sha256').update('runtime fixture').digest('hex') + }) + ) + writeFileSync( + join(runtime, 'LICENSE.md'), + readFileSync(new URL('../../resources/licenses/bun/LICENSE.md', import.meta.url)) + ) + return { root, binary } +} + +test.skipIf(process.platform === 'win32')( + 'launch options use standalone Bun and reject altered runtime bytes', + () => { + const { root, binary } = fixture() + try { + const launch = daemonSmokeLaunchOptions(root, root, { + NODE_OPTIONS: 'injected', + NODE_PATH: 'foreign', + BUN_OPTIONS: 'foreign', + BUN_INSPECT: 'foreign', + BUN_INSPECT_BRK: 'foreign', + BUN_INSPECT_WAIT: 'foreign', + ELECTRON_RUN_AS_NODE: '1', + BUN_CONPTY_LIBRARY: 'foreign' + }) + assert.equal(launch.program, binary) + assert.equal(launch.entryPath, join(root, 'out', 'terminal-daemon', 'daemon-entry.js')) + assert.equal(launch.options.cwd, root) + assert.equal(launch.options.env.ORCA_BACKGROUND_LAUNCH, '1') + for (const key of [ + 'NODE_OPTIONS', + 'NODE_PATH', + 'BUN_OPTIONS', + 'BUN_INSPECT', + 'BUN_INSPECT_BRK', + 'BUN_INSPECT_WAIT', + 'ELECTRON_RUN_AS_NODE', + 'BUN_CONPTY_LIBRARY' + ]) { + assert.equal(launch.options.env[key], undefined) + } + assert.deepEqual(launch.options.stdio, ['ignore', 'ignore', 'pipe', 'ipc']) + writeFileSync(binary, 'tampered') + assert.throws(() => daemonSmokeLaunchOptions(root, root), /checksum mismatch/) + } finally { + rmSync(root, { recursive: true, force: true }) + } + } +) + +test('every lifecycle smoke uses the standalone verified launch path', () => { + for (const name of [ + 'daemon-boot-smoke', + 'daemon-endpoint-handover-smoke', + 'windows-daemon-workspace-close-repro' + ]) { + const source = readFileSync(new URL(`./${name}.mjs`, import.meta.url), 'utf8') + assert.match(source, /spawnDaemonSmoke\(/) + assert.doesNotMatch(source, /\bfork\(/) + assert.doesNotMatch(source, /'out', 'main'/) + } + const boot = readFileSync(new URL('./daemon-boot-smoke.mjs', import.meta.url), 'utf8') + assert.doesNotMatch(boot, /best-effort|health check skipped/i) + const workflow = readFileSync( + new URL('../../.github/workflows/computer-e2e.yml', import.meta.url), + 'utf8' + ) + assert.ok( + workflow.indexOf('run: pnpm run build:terminal-daemon') < + workflow.indexOf('name: Daemon boot smoke') + ) +}) + +test('Windows launch fixes the provider selector and suppresses inherited runtime injection', () => { + const source = readFileSync(new URL('./daemon-smoke-launch.mjs', import.meta.url), 'utf8') + assert.match(source, /verifyCliRuntimeDirectory\(runtimeDir, process.platform, process.arch\)/) + assert.match(source, /daemonEnv.BUN_CONPTY_LIBRARY = join\(runtimeDir, 'conpty', 'conpty.dll'\)/) + assert.match(source, /\[\.\.\.bunOwnedRuntimeArgs\(\), launch.entryPath, \.\.\.args\]/) + assert.match(source, /windowsHide: true/) +}) diff --git a/config/scripts/electron-builder-config.test.mjs b/config/scripts/electron-builder-config.test.mjs index 9d38dc6de15d..531405b3a614 100644 --- a/config/scripts/electron-builder-config.test.mjs +++ b/config/scripts/electron-builder-config.test.mjs @@ -1,6 +1,6 @@ import { writeBundledCliRuntimeFixture } from './bundled-cli-runtime-fixture.mjs' import { existsSync } from 'node:fs' -import { chmod, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' import { createRequire } from 'node:module' import { tmpdir } from 'node:os' import { join } from 'node:path' @@ -17,6 +17,34 @@ const FpmTarget = require('app-builder-lib/out/targets/FpmTarget').default const electronBuilderNativeRebuild = require('./electron-builder-native-rebuild.cjs') describe('electron-builder config', () => { + it('preserves the pinned Bun vendor signature without excluding other application code', () => { + const ignored = (path) => + electronBuilderConfig.mac.signIgnore.some((pattern) => path.match(pattern)) + expect(ignored('/Orca.app/Contents/Resources/cli-runtime/bun-runtime')).toBe(true) + expect(ignored('/Orca.app/Contents/MacOS/Orca')).toBe(false) + expect(ignored('/Orca.app/Contents/Resources/terminal-daemon/daemon-entry.js')).toBe(false) + expect(ignored('/Orca.app/Contents/Resources/cli-runtime/bun-runtime-extra')).toBe(false) + }) + + it('refuses a macOS package if signing changed the pinned runtime bytes', async () => { + const root = await mkdtemp(join(tmpdir(), 'orca-signed-runtime-')) + const directory = join(root, 'Orca.app', 'Contents', 'Resources', 'cli-runtime') + const context = { + appOutDir: root, + electronPlatformName: 'darwin', + arch: 3, + packager: { appInfo: { productFilename: 'Orca' } } + } + try { + await writeBundledCliRuntimeFixture(directory, 'darwin', 'arm64') + expect(() => electronBuilderConfig.afterSign(context)).not.toThrow() + await writeFile(join(directory, 'bun-runtime'), 'changed by signing') + expect(() => electronBuilderConfig.afterSign(context)).toThrow('checksum mismatch') + } finally { + await rm(root, { recursive: true, force: true }) + } + }) + it('keeps the packaged app identity aligned with local-build validation', () => { expect(electronBuilderConfig.appId).toBe( require('../../src/shared/local-build-compatibility-contract.json').appId @@ -26,6 +54,7 @@ describe('electron-builder config', () => { it('excludes repo-only source trees from app.asar', () => { expect(electronBuilderConfig.files).toEqual( expect.arrayContaining([ + '!.build{,/**/*}', '!src{,/**/*}', '!config{,/**/*}', '!docs{,/**/*}', @@ -454,6 +483,10 @@ describe('arch-aware packaging guard', () => { scratch = await mkdtemp(join(tmpdir(), 'orca-electron-builder-guard-')) bundleDir = join(scratch, 'mobile-web') await writeMobileWebBundleFixtureTree({ outDir: bundleDir }) + const daemonDirectory = join(scratch, 'out', 'terminal-daemon') + await mkdir(daemonDirectory, { recursive: true }) + await writeFile(join(daemonDirectory, 'daemon-entry.js'), '') + await writeFile(join(daemonDirectory, 'windows-bun-pty-gate-entry.js'), '') for (const platform of ['darwin', 'linux', 'win32']) { for (const arch of ['x64', 'arm64']) { const directory = join(scratch, 'out', 'cli-runtime', `${platform}-${arch}`) @@ -509,3 +542,20 @@ describe('arch-aware packaging guard', () => { } }) }) + +describe('shared Bun terminal daemon packaging', () => { + it('packages the standalone entries with one shared CLI runtime on every platform', () => { + for (const platform of ['mac', 'linux', 'win']) { + const resources = electronBuilderConfig[platform].extraResources + expect(resources.filter((resource) => resource.to === 'terminal-daemon')).toEqual([ + { + from: 'out/terminal-daemon', + to: 'terminal-daemon', + filter: ['daemon-entry.js', 'windows-bun-pty-gate-entry.js'] + } + ]) + expect(resources.filter((resource) => resource.to === 'cli-runtime')).toHaveLength(1) + expect(resources.some((resource) => resource.to === 'orcad-template')).toBe(false) + } + }) +}) diff --git a/config/scripts/electron-builder-runtime-resources.test.mjs b/config/scripts/electron-builder-runtime-resources.test.mjs index 8a82b6938cbc..b7a181dcb5ef 100644 --- a/config/scripts/electron-builder-runtime-resources.test.mjs +++ b/config/scripts/electron-builder-runtime-resources.test.mjs @@ -15,7 +15,6 @@ const { findAsarEntry, isPackagedExternalSpecifier, packageNameFromSpecifier, - prunePackagedNodePty, prunePackagedParcelWatcher, prunePackagedSherpaOnnx, prunePackagedRuntimeTypeAndSourceMapArtifacts, @@ -189,80 +188,13 @@ describe('packaged runtime resources', () => { expect(findAsarEntry(['/out/main/index.js'], 'out/main/index.js')).toBe('/out/main/index.js') }) - it('prunes non-target node-pty architecture outputs from packaged runtime resources', async () => { - const resourcesDir = await mkdtemp(join(tmpdir(), 'orca-node-pty-prune-')) - try { - const nodePtyDir = join(resourcesDir, 'node_modules', 'node-pty') - const prebuildsDir = join(nodePtyDir, 'prebuilds') - const binDir = join(nodePtyDir, 'bin') - await mkdir(join(prebuildsDir, 'darwin-arm64'), { recursive: true }) - await mkdir(join(prebuildsDir, 'darwin-x64'), { recursive: true }) - await mkdir(join(prebuildsDir, 'linux-x64'), { recursive: true }) - await mkdir(join(prebuildsDir, 'win32-x64'), { recursive: true }) - await mkdir(join(binDir, 'darwin-arm64-148'), { recursive: true }) - await mkdir(join(binDir, 'darwin-x64-148'), { recursive: true }) - await mkdir(join(nodePtyDir, 'third_party', 'conpty'), { - recursive: true - }) - await mkdir(join(nodePtyDir, 'deps', 'winpty'), { recursive: true }) - - prunePackagedNodePty(resourcesDir, 'darwin', 3) - - await expect(readdir(prebuildsDir)).resolves.toEqual(['darwin-arm64']) - await expect(readdir(binDir)).resolves.toEqual(['darwin-arm64-148']) - await expect(readdir(join(nodePtyDir, 'third_party'))).resolves.toEqual([]) - await expect(readdir(join(nodePtyDir, 'deps'))).resolves.toEqual([]) - expect(() => prunePackagedNodePty(resourcesDir, 'darwin', 4)).toThrow( - 'Unsupported packaged runtime architecture: 4' - ) - } finally { - await removeTree(resourcesDir) - } - }) - - it('copies the Windows node-pty ConPTY runtime beside the rebuilt addon', async () => { - for (const [arch, electronArch] of [ - ['x64', 1], - ['arm64', 3] - ]) { - const resourcesDir = await mkdtemp(join(tmpdir(), `orca-node-pty-conpty-${arch}-`)) - try { - const nodePtyDir = join(resourcesDir, 'node_modules', 'node-pty') - const releaseDir = join(nodePtyDir, 'build', 'Release') - const conptyRoot = join(nodePtyDir, 'third_party', 'conpty', '0.1.0') - await mkdir(releaseDir, { recursive: true }) - await writeFile(join(releaseDir, 'conpty.node'), 'native addon placeholder', 'utf8') - for (const sourceArch of ['x64', 'arm64']) { - const sourceDir = join(conptyRoot, `win10-${sourceArch}`) - await mkdir(sourceDir, { recursive: true }) - await writeFile(join(sourceDir, 'conpty.dll'), `dll payload ${sourceArch}`, 'utf8') - await writeFile( - join(sourceDir, 'OpenConsole.exe'), - `console payload ${sourceArch}`, - 'utf8' - ) - } - - prunePackagedNodePty(resourcesDir, 'win32', electronArch) - - await expect(readFile(join(releaseDir, 'conpty', 'conpty.dll'), 'utf8')).resolves.toBe( - `dll payload ${arch}` - ) - await expect(readFile(join(releaseDir, 'conpty', 'OpenConsole.exe'), 'utf8')).resolves.toBe( - `console payload ${arch}` - ) - } finally { - await removeTree(resourcesDir) - } - } - }) - it('includes external main dependencies in the packaged runtime closure', () => { // Why: the main process imports '@parcel/watcher' for filesystem change // events; if it is absent from the packaged closure the serve host silently // stops propagating file changes to clients (regression guard for #4851). const packaged = createPackagedRuntimeNodeModuleResources() const packagedTargets = packaged.map((resource) => resource.to) + expect(packagedTargets).not.toContain(join('node_modules', 'node-pty')) expect(packagedTargets).toContain(join('node_modules', '@parcel', 'watcher')) expect( packagedTargets.some((target) => @@ -398,6 +330,24 @@ describe('packaged runtime resources', () => { } }) + it.each(['x64', 'arm64'])('rejects a missing packaged %s Bun ConPTY provider', async (arch) => { + const root = await mkdtemp(join(tmpdir(), 'orca-packaged-conpty-')) + try { + const runtimeDir = join(root, 'resources', 'cli-runtime') + await writeBundledCliRuntimeFixture(runtimeDir, 'win32', arch) + await removeTree(join(runtimeDir, 'conpty')) + await expect( + electronBuilderConfig.afterPack({ + appOutDir: root, + electronPlatformName: 'win32', + arch: arch === 'x64' ? 1 : 3 + }) + ).rejects.toThrow(/conpty/) + } finally { + await removeTree(root) + } + }) + it.skipIf(process.platform === 'win32')( 'prunes non-target native packages before the Linux glibc gate', async () => { @@ -431,6 +381,8 @@ describe('packaged runtime resources', () => { process.arch === 'x64' ? { electronArch: 3, machine: 0xb7, nonTarget: 'x64' } : { electronArch: 1, machine: 0x3e, nonTarget: 'arm64' } + await mkdir(join(resourcesDir, 'terminal-daemon'), { recursive: true }) + await writeFile(join(resourcesDir, 'terminal-daemon', 'daemon-entry.js'), '') await writeBundledCliRuntimeFixture( join(resourcesDir, 'cli-runtime'), 'linux', @@ -513,13 +465,24 @@ describe('packaged runtime resources', () => { 'utf8' ) await writeFile(join(unpackedCliDir, 'cli-bin.js'), "require('./index.js')\n", 'utf8') - await writeBundledCliRuntimeFixture(join(resourcesDir, 'cli-runtime'), 'linux', 'x64') + await mkdir(join(resourcesDir, 'terminal-daemon'), { recursive: true }) + await writeFile( + join(resourcesDir, 'terminal-daemon', 'daemon-entry.js'), + 'console.error("Usage: daemon-entry "); process.exit(1)\n' + ) + const quotedNode = `'${process.execPath.replaceAll("'", "'\\''")}'` + await writeBundledCliRuntimeFixture( + join(resourcesDir, 'cli-runtime'), + 'linux', + process.arch, + `#!/bin/sh\nexec ${quotedNode} "$@"\n` + ) await writeFile(launcherPath, '#!/usr/bin/env bash\n', { encoding: 'utf8', mode: 0o644 }) await electronBuilderConfig.afterPack({ appOutDir: join(root, 'linux-unpacked'), electronPlatformName: 'linux', - arch: 1, + arch: process.arch === 'arm64' ? 3 : 1, packager: { appInfo: { version: '9.9.9' } } }) diff --git a/config/scripts/electron-vite-output-contract.test.ts b/config/scripts/electron-vite-output-contract.test.ts index 958e461ffd94..e9d620b65d36 100644 --- a/config/scripts/electron-vite-output-contract.test.ts +++ b/config/scripts/electron-vite-output-contract.test.ts @@ -139,7 +139,7 @@ describe('Electron Vite output contract', () => { throw new Error('Expected main-process external predicate') } - expect(external('node-pty', undefined, false)).toBe(true) + expect(external('node-pty', undefined, false)).toBe(false) expect(external('@parcel/watcher', undefined, false)).toBe(true) expect(external('electron', undefined, false)).toBe(true) expect(external('node:fs', undefined, false)).toBe(true) diff --git a/config/scripts/ensure-native-runtime-job-ownership.test.mjs b/config/scripts/ensure-native-runtime-job-ownership.test.mjs deleted file mode 100644 index 960c87e362b5..000000000000 --- a/config/scripts/ensure-native-runtime-job-ownership.test.mjs +++ /dev/null @@ -1,299 +0,0 @@ -import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs' -import { createRequire } from 'node:module' -import { tmpdir } from 'node:os' -import { dirname, join, resolve } from 'node:path' -import { describe, expect, it, vi } from 'vitest' -import { peImage } from './windows-pe-image-fixture.mjs' - -const require = createRequire(import.meta.url) -const { - CYGWIN_BREAKAWAY_MARKER, - CYGWIN_BREAKAWAY_MARKER_TEXT, - assertNodePtyJobOwnership, - assertRebuiltConptyDeniesMsysBreakaway, - nodePtyAddonPath -} = require('./node-pty-job-ownership.cjs') -const NODE_PTY_PATCH = readFileSync( - new URL('../patches/node-pty@1.1.0.patch', import.meta.url), - 'utf8' -) - -const JOB_EXPORTS = { - listJobProcessIds: () => [], - terminateJob: () => true, - assignCurrentProcessToJob: () => true -} - -const fixtureDir = mkdtempSync(join(tmpdir(), 'node-pty-job-ownership-')) - -/** A stand-in addon; only the wide literal the gate reads has to be real. */ -function writeAddon(name, { cygwinBreakawayDenied }) { - const path = join(fixtureDir, name) - writeFileSync( - path, - Buffer.concat([ - Buffer.from('MZ fake addon '), - cygwinBreakawayDenied ? CYGWIN_BREAKAWAY_MARKER : Buffer.alloc(0) - ]) - ) - return path -} - -const CURRENT_ADDON = writeAddon('current.node', { cygwinBreakawayDenied: true }) -const PRE_MSYS_ADDON = writeAddon('pre-msys.node', { cygwinBreakawayDenied: false }) - -const PATCHED = { dir: 'build/Release/', module: JOB_EXPORTS } -const PREBUILD = { - dir: 'prebuilds/win32-x64/', - module: { - startProcess: () => {}, - connect: () => {}, - resize: () => {}, - clear: () => {}, - kill: () => {} - } -} - -const onWindows = (native, addonPath) => ({ - platform: 'win32', - nativeName: 'conpty', - native, - addonPath -}) - -describe('assertNodePtyJobOwnership', () => { - it('keeps node-addon-api project paths absolute during Windows source builds', () => { - expect(NODE_PTY_PATCH).toContain( - `+ " { - expect(NODE_PTY_PATCH).toContain("- 'target_name': 'conpty_console_list'") - expect(NODE_PTY_PATCH).toContain("- 'target_name': 'pty'") - }) - - // The gate's whole case rests on this literal, and nothing else ties the - // constant to the C++ that compiles it in. Drift either way has to fail HERE: - // otherwise it fails every correctly rebuilt addon, and no rebuild can fix it. - it('sniffs for a literal the patch really adds to conpty.cc', () => { - const conptyHunk = NODE_PTY_PATCH.split(/^diff --git /m).find((section) => - section.startsWith('a/src/win/conpty.cc ') - ) - expect(conptyHunk, 'the patch no longer touches src/win/conpty.cc').toBeDefined() - const addedCode = conptyHunk - .split('\n') - .filter((line) => line.startsWith('+') && !/^\+\s*(\/\/|\*)/.test(line)) - expect( - addedCode.some((line) => line.includes(`L"${CYGWIN_BREAKAWAY_MARKER_TEXT}"`)), - `No added conpty.cc line carries L"${CYGWIN_BREAKAWAY_MARKER_TEXT}". Either the patch ` + - 'stopped adding it or CYGWIN_BREAKAWAY_MARKER_TEXT drifted; until they agree the gate ' + - 'rejects every correctly rebuilt addon.' - ).toBe(true) - }) - - // MSVC compiles L"" to UTF-16LE; reading the addon as anything else finds nothing. - it('looks for that literal in the encoding the compiler stores it in', () => { - expect(CYGWIN_BREAKAWAY_MARKER.toString('utf16le')).toBe(CYGWIN_BREAKAWAY_MARKER_TEXT) - expect(CYGWIN_BREAKAWAY_MARKER.length).toBe(CYGWIN_BREAKAWAY_MARKER_TEXT.length * 2) - }) - - it('rejects the prebuild that shipped without the job exports', () => { - expect(() => assertNodePtyJobOwnership(onWindows(PREBUILD, CURRENT_ADDON))).toThrow( - /listJobProcessIds, terminateJob, assignCurrentProcessToJob/ - ) - }) - - it('names where the bad native came from, so the fix is obvious', () => { - expect(() => assertNodePtyJobOwnership(onWindows(PREBUILD, CURRENT_ADDON))).toThrow( - /prebuilds\/win32-x64/ - ) - }) - - it('accepts a source build carrying the patch', () => { - expect(() => assertNodePtyJobOwnership(onWindows(PATCHED, CURRENT_ADDON))).not.toThrow() - }) - - // The reason this gate reads the binary at all: every export above predates - // the Cygwin/MSYS breakaway denial, so a build that leaks every Git Bash - // child out of its pane's job satisfies all of them. - it('rejects a source build that predates the Cygwin/MSYS breakaway denial', () => { - expect(() => assertNodePtyJobOwnership(onWindows(PATCHED, PRE_MSYS_ADDON))).toThrow( - /predates the Cygwin\/MSYS job-breakaway denial/ - ) - }) - - it('tells that build apart by path, and says to rebuild', () => { - expect(() => assertNodePtyJobOwnership(onWindows(PATCHED, PRE_MSYS_ADDON))).toThrow( - /pre-msys\.node[\s\S]*Rebuild node-pty from source/ - ) - }) - - it.each([ - ['no path at all', undefined], - ['a path that is not there', join(fixtureDir, 'absent.node')] - ])('refuses rather than skip when the addon cannot be read: %s', (_case, addonPath) => { - expect(() => assertNodePtyJobOwnership(onWindows(PATCHED, addonPath))).toThrow( - /Cannot read node-pty's conpty native/ - ) - }) - - // Passing the conpty name and no readable addon: on win32 every remaining - // branch throws, so only the platform gate can keep these quiet. The MSYS - // breakaway denial is a Windows concern and must cost other hosts nothing. - it.each([ - ['non-Windows hosts', { platform: 'darwin', nativeName: 'conpty' }], - ['non-Windows hosts building for one', { platform: 'linux', nativeName: 'conpty' }], - ['the pre-ConPTY winpty backend', { platform: 'win32', nativeName: 'pty' }] - ])('stays out of the way on %s', (_case, spec) => { - expect(() => assertNodePtyJobOwnership({ ...spec, native: PREBUILD })).not.toThrow() - }) - - it('would have thrown on Windows for the very same input', () => { - expect(() => - assertNodePtyJobOwnership({ platform: 'win32', nativeName: 'conpty', native: PREBUILD }) - ).toThrow() - }) -}) - -describe('nodePtyAddonPath', () => { - // Built from segments rather than a POSIX string: on Windows `resolve` returns - // a drive letter and backslashes, so a literal only ever passed off Windows. - it('resolves the addon against node-pty lib, which is the only base callers share', () => { - expect( - nodePtyAddonPath( - resolve('/app/node_modules/node-pty/lib/utils.js'), - { dir: '../build/Release/' }, - 'conpty' - ) - ).toBe(join(resolve('/app/node_modules/node-pty'), 'build', 'Release', 'conpty.node')) - }) - - it('handles the bundled layout, where the addon sits beside lib', () => { - expect( - nodePtyAddonPath( - resolve('/app/resources/node-pty/lib/utils.js'), - { dir: './build/Release/' }, - 'conpty' - ) - ).toBe(join(resolve('/app/resources/node-pty/lib'), 'build', 'Release', 'conpty.node')) - }) -}) - -describe('assertRebuiltConptyDeniesMsysBreakaway', () => { - const rebuiltInto = (files) => { - const nodePtyDir = join(mkdtempSync(join(fixtureDir, 'rebuild-')), 'node-pty') - for (const [relativePath, options] of Object.entries(files)) { - const { arch = 'x64', cygwinBreakawayDenied = true } = options - const addonPath = join(nodePtyDir, ...relativePath.split('/')) - mkdirSync(dirname(addonPath), { recursive: true }) - writeFileSync( - addonPath, - Buffer.concat([ - peImage({ arch }), - cygwinBreakawayDenied ? CYGWIN_BREAKAWAY_MARKER : Buffer.alloc(0) - ]) - ) - } - return nodePtyDir - } - - it('accepts the addon a good same-host rebuild leaves behind', () => { - const nodePtyDir = rebuiltInto({ 'build/Release/conpty.node': {} }) - expect(() => - assertRebuiltConptyDeniesMsysBreakaway({ nodePtyDir, rebuildArch: 'x64', crossHost: false }) - ).not.toThrow() - }) - - it('rejects one that predates the denial, wherever the rebuild ran', () => { - const nodePtyDir = rebuiltInto({ - 'build/Release/conpty.node': { cygwinBreakawayDenied: false } - }) - expect(() => - assertRebuiltConptyDeniesMsysBreakaway({ nodePtyDir, rebuildArch: 'x64', crossHost: true }) - ).toThrow(/predates the Cygwin\/MSYS job-breakaway denial/) - }) - - // On the host that will run this install, no addon means loadNativeModule - // falls through to the published prebuild -- the binary that leaks every MSYS - // pane child. That is a broken build, not an absence to shrug at. - it('refuses a same-host rebuild that reported success and produced nothing', () => { - const nodePtyDir = rebuiltInto({ 'package.json': {} }) - expect(() => - assertRebuiltConptyDeniesMsysBreakaway({ nodePtyDir, rebuildArch: 'x64', crossHost: false }) - ).toThrow(/the rebuild reported success/) - }) - - it('names both the addon it wanted and the prebuild that would load instead', () => { - const nodePtyDir = rebuiltInto({ 'package.json': {} }) - expect(() => - assertRebuiltConptyDeniesMsysBreakaway({ nodePtyDir, rebuildArch: 'arm64', crossHost: false }) - ).toThrow(/build[\\/]Release[\s\S]*prebuilds[\\/]win32-arm64/) - }) - - // A rebuild that ignored --arch leaves a binary the target cannot load, so - // node-pty falls back to the prebuild. Saying so here is two steps closer to - // the command that fixes it than saying so at packaging time. - it('rejects an addon of an architecture this rebuild did not target', () => { - const nodePtyDir = rebuiltInto({ 'build/Release/conpty.node': { arch: 'x64' } }) - expect(() => - assertRebuiltConptyDeniesMsysBreakaway({ nodePtyDir, rebuildArch: 'arm64', crossHost: true }) - ).toThrow(/machine 0x8664, but this rebuild targets win32-arm64/) - }) - - // "node-gyp ignored --arch" is a guess when the file is not a PE at all: that - // is a truncated or quarantined artifact, and saying otherwise sends the - // reader to the wrong command. - it('does not blame --arch for a file that is not a PE image', () => { - const nodePtyDir = join(mkdtempSync(join(fixtureDir, 'rebuild-')), 'node-pty') - mkdirSync(join(nodePtyDir, 'build', 'Release'), { recursive: true }) - writeFileSync(join(nodePtyDir, 'build', 'Release', 'conpty.node'), Buffer.alloc(0x200)) - expect(() => - assertRebuiltConptyDeniesMsysBreakaway({ nodePtyDir, rebuildArch: 'x64', crossHost: false }) - ).toThrow(/is not a PE image[\s\S]*truncated or quarantined/) - }) - - it('still names the consequence for that file, which is the prebuild', () => { - const nodePtyDir = join(mkdtempSync(join(fixtureDir, 'rebuild-')), 'node-pty') - mkdirSync(join(nodePtyDir, 'build', 'Release'), { recursive: true }) - writeFileSync(join(nodePtyDir, 'build', 'Release', 'conpty.node'), Buffer.alloc(0x200)) - expect(() => - assertRebuiltConptyDeniesMsysBreakaway({ nodePtyDir, rebuildArch: 'x64', crossHost: false }) - ).toThrow(/fall back to the published prebuild/) - }) - - it('accepts one a cross-arch rebuild really did emit for the target', () => { - const nodePtyDir = rebuiltInto({ 'build/Release/conpty.node': { arch: 'arm64' } }) - expect(() => - assertRebuiltConptyDeniesMsysBreakaway({ nodePtyDir, rebuildArch: 'arm64', crossHost: true }) - ).not.toThrow() - }) - - // PE_MACHINE covers what Orca ships; an arch it does not know is not one this - // can judge, and guessing would fail a rebuild that was fine. - it('does not judge an architecture it has no machine value for', () => { - const nodePtyDir = rebuiltInto({ 'build/Release/conpty.node': { arch: 'x64' } }) - expect(() => - assertRebuiltConptyDeniesMsysBreakaway({ nodePtyDir, rebuildArch: 'ia32', crossHost: true }) - ).not.toThrow() - }) - - it.each([ - ['a cross-host rebuild need not leave a win32 addon here', { crossHost: true }, true], - ['no node-pty on this disk is not a bad build', { crossHost: false }, false] - ])('warns instead: %s', (_case, verdict, nodePtyInstalled) => { - const nodePtyDir = nodePtyInstalled - ? rebuiltInto({ 'package.json': {} }) - : join(fixtureDir, 'no-node-pty-here') - const warn = vi.fn() - expect(() => - assertRebuiltConptyDeniesMsysBreakaway({ - nodePtyDir, - rebuildArch: 'x64', - ...verdict, - warn - }) - ).not.toThrow() - expect(warn).toHaveBeenCalledWith(expect.stringContaining('could not check the MSYS')) - }) -}) diff --git a/config/scripts/ensure-native-runtime.mjs b/config/scripts/ensure-native-runtime.mjs index c93b6a19d621..1df250651b31 100644 --- a/config/scripts/ensure-native-runtime.mjs +++ b/config/scripts/ensure-native-runtime.mjs @@ -3,7 +3,6 @@ import { spawnSync } from 'node:child_process' import { createRequire } from 'node:module' import { existsSync, readFileSync, realpathSync } from 'node:fs' -import { release } from 'node:os' import { basename, dirname, resolve } from 'node:path' import { ensureWindowsProcessTreeCommandLinePatch, @@ -13,19 +12,13 @@ import { } from './windows-process-tree-gyp-rebuild.mjs' const require = createRequire(import.meta.url) -const { assertNodePtyJobOwnership, nodePtyAddonPath } = require('./node-pty-job-ownership.cjs') const { assertWindowsProcessTreeCreationTime } = require('./windows-process-tree-creation-time.cjs') const scriptPath = import.meta.filename const projectDir = resolve(import.meta.dirname, '../..') const runtime = readRuntimeArg() -const NATIVE_MODULES = [ - 'node-pty', - ...(process.platform === 'win32' - ? ['@orca/windows-registry', '@vscode/windows-process-tree'] - : []) -] -const NODE_PTY_CONPTY_RUNTIME_FILES = ['conpty.dll', 'OpenConsole.exe'] +const NATIVE_MODULES = + process.platform === 'win32' ? ['@orca/windows-registry', '@vscode/windows-process-tree'] : [] const CHILD_CHECK_FLAG = '--check-only' if (process.argv.includes(CHILD_CHECK_FLAG)) { @@ -64,23 +57,7 @@ function readRuntimeArg() { function ensureNodeRuntime() { const initial = runNodeCheck() - const patchedNodePtyRebuildReason = getPatchedNodePtyRebuildReason() - if (initial.ok && !patchedNodePtyRebuildReason) { - return - } - - if (patchedNodePtyRebuildReason) { - console.warn(`[native-runtime] ${patchedNodePtyRebuildReason}`) - if (!initial.ok) { - printCheckError(initial) - } - const failedModules = initial.failures.map((failure) => failure.moduleName) - const rebuildModules = [ - 'node-pty', - ...failedModules.filter((moduleName) => moduleName !== 'node-pty') - ] - rebuildNodeRuntimeModules(rebuildModules) - verifyNodeRuntimeAfterRebuild() + if (initial.ok) { return } @@ -106,22 +83,13 @@ function verifyNodeRuntimeAfterRebuild() { function ensureElectronRuntime() { const initial = runElectronCheck() - const patchedNodePtyRebuildReason = getPatchedNodePtyRebuildReason() - if (initial.ok && !patchedNodePtyRebuildReason) { + if (initial.ok) { return } - - if (patchedNodePtyRebuildReason) { - console.warn(`[native-runtime] ${patchedNodePtyRebuildReason}`) - if (!initial.ok) { - printCheckError(initial) - } - } else { - console.warn( - `[native-runtime] ${formatRuntimeLabel('electron')} cannot load native modules; rebuilding native deps for Electron.` - ) - printCheckError(initial) - } + console.warn( + `[native-runtime] ${formatRuntimeLabel('electron')} cannot load native modules; rebuilding native deps for Electron.` + ) + printCheckError(initial) runNodeScript(['config/scripts/rebuild-native-deps.mjs']) const final = runElectronCheck() @@ -281,109 +249,10 @@ function loadNativeModule(moduleName) { registry.getRegistryKey(registry.HK.CU, 'Environment') return } - if (moduleName === 'node-pty') { - loadNodePtyNativeModule() - return - } require(moduleName) } -function loadNodePtyNativeModule() { - require('node-pty') - - const { loadNativeModule } = require('node-pty/lib/utils') - const nativeName = getNodePtyNativeModuleName() - // Why: node-pty's Windows JS wrapper defers conpty.node/pty.node until a - // terminal is created, so require('node-pty') alone can miss ABI mismatches. - const native = loadNativeModule(nativeName) - assertNodePtyWindowsConptyRuntime(native?.dir) - assertNodePtyJobOwnership({ - nativeName, - native, - addonPath: nodePtyAddonPath(require.resolve('node-pty/lib/utils'), native, nativeName) - }) - if (requiresPatchedNodePtySourceBuild() && !isNodePtyReleaseBuildDir(native?.dir)) { - throw new Error( - `node-pty resolved to ${native.dir}; expected build/Release so Orca's node-pty patch is active` - ) - } -} - -function assertNodePtyWindowsConptyRuntime(nativeDir) { - if (process.platform !== 'win32' || !isNodePtyReleaseBuildDir(nativeDir)) { - return - } - const runtimeDir = resolve(projectDir, 'node_modules', 'node-pty', 'build', 'Release', 'conpty') - const missingFile = NODE_PTY_CONPTY_RUNTIME_FILES.find( - (filename) => !existsSync(resolve(runtimeDir, filename)) - ) - if (missingFile) { - throw new Error(`node-pty ConPTY runtime file is missing: ${resolve(runtimeDir, missingFile)}`) - } -} - -function getNodePtyNativeModuleName() { - if (process.platform !== 'win32') { - return 'pty' - } - - return getWindowsBuildNumber() >= 18309 ? 'conpty' : 'pty' -} - -function getPatchedNodePtyRebuildReason() { - if (!requiresPatchedNodePtySourceBuild()) { - return null - } - - // Why: a loadable upstream node-pty prebuild is not enough; Orca's Unix and - // Windows patches only land in the source-built build/Release artifacts. - const nodePtyDir = resolve(projectDir, 'node_modules', 'node-pty') - const artifactPaths = patchedNodePtyArtifactPaths(nodePtyDir) - const missingArtifact = artifactPaths.find((artifactPath) => !existsSync(artifactPath)) - - if (!missingArtifact) { - return null - } - - return 'Patched node-pty build artifacts are missing; rebuilding native deps.' -} - -function patchedNodePtyArtifactPaths(nodePtyDir) { - if (process.platform === 'win32') { - const releaseDir = resolve(nodePtyDir, 'build', 'Release') - return [ - resolve(releaseDir, 'conpty.node'), - ...NODE_PTY_CONPTY_RUNTIME_FILES.map((filename) => resolve(releaseDir, 'conpty', filename)) - ] - } - - const artifactPaths = [resolve(nodePtyDir, 'build', 'Release', 'pty.node')] - // Why: node-pty only builds spawn-helper on macOS; Linux builds only pty.node. - if (process.platform === 'darwin') { - artifactPaths.push(resolve(nodePtyDir, 'build', 'Release', 'spawn-helper')) - } - return artifactPaths -} - -function requiresPatchedNodePtySourceBuild() { - const nodePtyPatchPath = resolve(projectDir, 'config', 'patches', 'node-pty@1.1.0.patch') - if (!existsSync(nodePtyPatchPath)) { - return false - } - - return existsSync(resolve(projectDir, 'node_modules', 'node-pty')) -} - -function isNodePtyReleaseBuildDir(nativeDir) { - return typeof nativeDir === 'string' && nativeDir.replace(/\\/g, '/').includes('build/Release/') -} - -function getWindowsBuildNumber() { - const match = /(\d+)\.(\d+)\.(\d+)/g.exec(release()) - return match && match.length === 4 ? Number.parseInt(match[3], 10) : 0 -} - function rebuildNodeRuntimeModules(moduleNames) { for (const moduleName of moduleNames) { let moduleDir = dirname(require.resolve(`${moduleName}/package.json`)) @@ -401,9 +270,6 @@ function rebuildNodeRuntimeModules(moduleNames) { } console.warn(`[native-runtime] Rebuilding ${moduleName} with node-gyp.`) runPnpm(['exec', 'node-gyp', 'rebuild'], { cwd: moduleDir }) - if (moduleName === 'node-pty' && process.platform === 'win32') { - runNodeScript([resolve(moduleDir, 'scripts', 'post-install.js')]) - } } } diff --git a/config/scripts/ensure-native-runtime.test.mjs b/config/scripts/ensure-native-runtime.test.mjs index 47fe435edccb..98e68a7232a2 100644 --- a/config/scripts/ensure-native-runtime.test.mjs +++ b/config/scripts/ensure-native-runtime.test.mjs @@ -1,127 +1,38 @@ import { spawnSync } from 'node:child_process' -import { - chmodSync, - copyFileSync, - mkdirSync, - mkdtempSync, - readFileSync, - rmSync, - writeFileSync -} from 'node:fs' -import { tmpdir } from 'node:os' +import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { delimiter, join } from 'node:path' import { fileURLToPath } from 'node:url' import { describe, expect, it } from 'vitest' -import { copyScriptWithLocalModules } from './script-module-dependencies.mjs' - -const sourceScriptPath = fileURLToPath(new URL('./ensure-native-runtime.mjs', import.meta.url)) -// The import walk sees `from './x.mjs'` only, so the createRequire'd CJS -// siblings have to be named. Without them the temp project cannot even load. -const REQUIRED_CJS_SIBLINGS = [ - 'node-pty-job-ownership.cjs', - 'windows-process-tree-creation-time.cjs' -] +import { + mkTempProject, + writeFakeElectronRebuild, + writeFakeUsableElectronPackage, + writeFakeWindowsProcessTree +} from './rebuild-native-deps-test-fixtures.mjs' + +function stageEnsureScript(projectDir) { + const path = join(projectDir, 'config', 'scripts', 'ensure-native-runtime.mjs') + copyFileSync(fileURLToPath(new URL('./ensure-native-runtime.mjs', import.meta.url)), path) + return path +} describe('ensure-native-runtime', () => { - it('rechecks Node native modules in fresh child processes after rebuilding', () => { - const projectDir = mkTempProject() - - try { - const scriptPath = join(projectDir, 'config', 'scripts', 'ensure-native-runtime.mjs') - const logPath = join(projectDir, 'native-runtime.log') - const markerPath = join(projectDir, 'rebuilt.marker') - const binDir = join(projectDir, 'bin') - writeFakeNativeModules(projectDir) - writeNodePtyPatchFile(projectDir) - writeFakePnpm(binDir) - - const result = spawnSync(process.execPath, [scriptPath, '--runtime=node'], { - cwd: projectDir, - encoding: 'utf8', - env: envWithPrependedPath(binDir, { - ORCA_NATIVE_TEST_LOG: logPath, - ORCA_NATIVE_TEST_MARKER: markerPath - }) - }) - - expect(result.status, result.stderr).toBe(0) - const log = readFileSync(logPath, 'utf8') - expect(log).toContain('pnpm exec node-gyp rebuild\n') - expect(log).toContain(join('node_modules', 'node-pty')) - if (process.platform === 'linux') { - expect(log).toMatch(/^cxxflags=(?:.*\s)?-std=gnu\+\+2a$/m) - } - expect(log.split('\n').filter((line) => line.startsWith('node-pty child '))).toEqual([ - expect.stringMatching(/^node-pty child (?:conpty|pty) marker=false$/), - expect.stringMatching(/^node-pty child (?:conpty|pty) marker=true$/) - ]) - } finally { - rmSync(projectDir, { recursive: true, force: true }) - } - }) - - it.skipIf(process.platform !== 'win32')( - 'rebuilds other failed Windows addons with patched node-pty', - () => { - const projectDir = mkTempProject() - - try { - const scriptPath = join(projectDir, 'config', 'scripts', 'ensure-native-runtime.mjs') - const logPath = join(projectDir, 'native-runtime.log') - const markerPath = join(projectDir, 'rebuilt.marker') - const binDir = join(projectDir, 'bin') - writeFakeNativeModules(projectDir, { windowsRegistryRequiresMarker: true }) - writeNodePtyPatchFile(projectDir) - writeFakePnpm(binDir) - - const result = spawnSync(process.execPath, [scriptPath, '--runtime=node'], { - cwd: projectDir, - encoding: 'utf8', - env: envWithPrependedPath(binDir, { - ORCA_NATIVE_TEST_LOG: logPath, - ORCA_NATIVE_TEST_MARKER: markerPath - }) - }) - - expect(result.status, result.stderr).toBe(0) - const log = readFileSync(logPath, 'utf8') - expect(log.match(/pnpm exec node-gyp rebuild\n/g)).toHaveLength(2) - expect(log).toContain(join('node_modules', 'node-pty')) - expect(log).toContain(join('node_modules', '@orca', 'windows-registry')) - } finally { - rmSync(projectDir, { recursive: true, force: true }) - } - } - ) - it.skipIf(process.platform === 'win32')( - 'rebuilds patched node-pty artifacts even when the Node load check passes', + 'accepts Node without any installed native package', () => { const projectDir = mkTempProject() - try { - const scriptPath = join(projectDir, 'config', 'scripts', 'ensure-native-runtime.mjs') - const logPath = join(projectDir, 'native-runtime.log') - const markerPath = join(projectDir, 'rebuilt.marker') - const binDir = join(projectDir, 'bin') - writeLoadableNativeModules(projectDir) - writeNodePtyPatchFile(projectDir) - writeFakePnpm(binDir) - - const result = spawnSync(process.execPath, [scriptPath, '--runtime=node'], { - cwd: projectDir, - encoding: 'utf8', - env: envWithPrependedPath(binDir, { - ORCA_NATIVE_TEST_LOG: logPath, - ORCA_NATIVE_TEST_MARKER: markerPath - }) - }) - - expect(result.status, result.stderr).toBe(0) - expect(result.stderr).toContain( - 'Patched node-pty build artifacts are missing; rebuilding native deps.' + const result = spawnSync( + process.execPath, + [stageEnsureScript(projectDir), '--runtime=node'], + { + cwd: projectDir, + encoding: 'utf8' + } ) - expect(readFileSync(logPath, 'utf8')).toContain('pnpm exec node-gyp rebuild\n') + expect(result.status, result.stderr).toBe(0) + expect(result.stderr).not.toContain('Rebuilding') + expect(existsSync(join(projectDir, 'node_modules'))).toBe(false) } finally { rmSync(projectDir, { recursive: true, force: true }) } @@ -129,242 +40,101 @@ describe('ensure-native-runtime', () => { ) it.skipIf(process.platform === 'win32')( - 'rebuilds when patched artifacts exist but node-pty resolves to prebuilds', + 'repairs Electron metadata even without rebuildable addons', () => { const projectDir = mkTempProject() - try { - const scriptPath = join(projectDir, 'config', 'scripts', 'ensure-native-runtime.mjs') - const logPath = join(projectDir, 'native-runtime.log') - const markerPath = join(projectDir, 'rebuilt.marker') - const binDir = join(projectDir, 'bin') - writeLoadableNativeModules(projectDir) - writeNodePtyPatchFile(projectDir) - writePatchedNodePtyBuildArtifacts(projectDir) - writeFakePnpm(binDir) - - const result = spawnSync(process.execPath, [scriptPath, '--runtime=node'], { - cwd: projectDir, - encoding: 'utf8', - env: envWithPrependedPath(binDir, { - ORCA_NATIVE_TEST_LOG: logPath, - ORCA_NATIVE_TEST_MARKER: markerPath - }) - }) - + const logPath = join(projectDir, 'rebuild.log') + writeFakeUsableElectronPackage(projectDir) + writeFakeElectronRebuild(projectDir, { logPathEnv: 'ORCA_REBUILD_TEST_LOG' }) + rmSync(join(projectDir, 'node_modules/electron/path.txt')) + const result = spawnSync( + process.execPath, + [stageEnsureScript(projectDir), '--runtime=electron'], + { + cwd: projectDir, + encoding: 'utf8', + env: { + ...process.env, + ELECTRON_INSTALL_PLATFORM: 'linux', + ORCA_REBUILD_TEST_LOG: logPath + } + } + ) expect(result.status, result.stderr).toBe(0) - expect(result.stderr).toContain("expected build/Release so Orca's node-pty patch is active") - expect(readFileSync(logPath, 'utf8')).toContain('pnpm exec node-gyp rebuild\n') + expect(readFileSync(join(projectDir, 'node_modules/electron/path.txt'), 'utf8')).toBe( + 'electron' + ) + expect(existsSync(logPath)).toBe(false) } finally { rmSync(projectDir, { recursive: true, force: true }) } } ) - it.skipIf(process.platform === 'win32')( - 'keeps the fast path when the platform-specific patched artifacts exist', + it.skipIf(process.platform !== 'win32')( + 'rechecks failed Windows addons in fresh Node children', () => { const projectDir = mkTempProject() - try { - const scriptPath = join(projectDir, 'config', 'scripts', 'ensure-native-runtime.mjs') - const logPath = join(projectDir, 'native-runtime.log') + const logPath = join(projectDir, 'native.log') const markerPath = join(projectDir, 'rebuilt.marker') + const registryDir = join(projectDir, 'node_modules', '@orca', 'windows-registry') + mkdirSync(registryDir, { recursive: true }) + writeFileSync( + join(registryDir, 'package.json'), + '{"name":"@orca/windows-registry","main":"index.js"}' + ) + writeFileSync( + join(registryDir, 'index.js'), + ` +const { appendFileSync, existsSync } = require('node:fs'); +exports.HK = { CU: 0x80000001 }; +exports.getRegistryKey = () => { + const ready = existsSync(process.env.ORCA_NATIVE_TEST_MARKER); + appendFileSync(process.env.ORCA_NATIVE_TEST_LOG, JSON.stringify({ ready, pid: process.pid }) + '\\n'); + if (!ready) throw new Error('ABI mismatch sentinel'); +};` + ) + writeFakeWindowsProcessTree(projectDir) const binDir = join(projectDir, 'bin') - writeLoadableNativeModules(projectDir, { nativeDir: '../build/Release/' }) - writeNodePtyPatchFile(projectDir) - writePatchedNodePtyBuildArtifacts(projectDir) - writeFakePnpm(binDir) - - const result = spawnSync(process.execPath, [scriptPath, '--runtime=node'], { - cwd: projectDir, - encoding: 'utf8', - env: envWithPrependedPath(binDir, { - ORCA_NATIVE_TEST_LOG: logPath, - ORCA_NATIVE_TEST_MARKER: markerPath - }) - }) - + mkdirSync(binDir) + writeFileSync( + join(binDir, 'rebuild.cjs'), + "require('node:fs').writeFileSync(process.env.ORCA_NATIVE_TEST_MARKER, process.cwd())" + ) + writeFileSync( + join(binDir, 'pnpm.cmd'), + `@echo off\r\n"${process.execPath}" "%~dp0rebuild.cjs"\r\n` + ) + const pathKey = + Object.keys(process.env).find((key) => key.toLowerCase() === 'path') ?? 'Path' + const result = spawnSync( + process.execPath, + [stageEnsureScript(projectDir), '--runtime=node'], + { + cwd: projectDir, + encoding: 'utf8', + env: { + ...process.env, + [pathKey]: `${binDir}${delimiter}${process.env[pathKey] ?? ''}`, + ORCA_NATIVE_TEST_LOG: logPath, + ORCA_NATIVE_TEST_MARKER: markerPath + } + } + ) expect(result.status, result.stderr).toBe(0) - expect(result.stderr).not.toContain('Patched node-pty build artifacts are missing') - expect(readFileSync(logPath, 'utf8')).not.toContain('pnpm exec node-gyp rebuild') + const probes = readFileSync(logPath, 'utf8') + .trim() + .split('\n') + .map((line) => JSON.parse(line)) + expect(probes.map((row) => row.ready)).toEqual([false, true]) + expect(probes[0].pid).not.toBe(probes[1].pid) + expect(readFileSync(markerPath, 'utf8')).toBe(registryDir) + expect(result.stderr).not.toContain('Rebuilding @vscode/windows-process-tree') } finally { rmSync(projectDir, { recursive: true, force: true }) } } ) }) - -function mkTempProject() { - const projectDir = mkdtempSync(join(tmpdir(), 'orca-native-runtime-')) - // Walked, not listed: the script imports windows-process-tree-gyp-rebuild.mjs, and a fixture - // missing it fails every case with a module-resolution error instead of the defect under test. - copyScriptWithLocalModules(sourceScriptPath, join(projectDir, 'config', 'scripts')) - for (const name of REQUIRED_CJS_SIBLINGS) { - copyFileSync( - fileURLToPath(new URL(`./${name}`, import.meta.url)), - join(projectDir, 'config', 'scripts', name) - ) - } - return projectDir -} - -function envWithPrependedPath(binDir, extraEnv) { - const pathKey = - process.platform === 'win32' - ? (Object.keys(process.env).find((key) => key.toLowerCase() === 'path') ?? 'Path') - : 'PATH' - return { - ...process.env, - ...extraEnv, - [pathKey]: `${binDir}${delimiter}${process.env[pathKey] ?? ''}` - } -} - -function writeFakeNativeModules(projectDir, { windowsRegistryRequiresMarker = false } = {}) { - const nodePtyDir = join(projectDir, 'node_modules', 'node-pty') - mkdirSync(join(nodePtyDir, 'lib'), { recursive: true }) - writeFileSync( - join(nodePtyDir, 'package.json'), - '{"name":"node-pty","version":"1.1.0","main":"index.js"}\n' - ) - mkdirSync(join(nodePtyDir, 'scripts'), { recursive: true }) - writeFileSync(join(nodePtyDir, 'scripts', 'post-install.js'), '') - - writeFileSync(join(nodePtyDir, 'index.js'), 'module.exports = {}\n') - writeFileSync( - join(nodePtyDir, 'lib', 'utils.js'), - ` -const { appendFileSync, existsSync } = require('node:fs') - -exports.loadNativeModule = function loadNativeModule(nativeName) { - const markerExists = existsSync(process.env.ORCA_NATIVE_TEST_MARKER) - appendFileSync( - process.env.ORCA_NATIVE_TEST_LOG, - \`node-pty \${process.argv.includes('--check-only') ? 'child' : 'parent'} \${nativeName} marker=\${markerExists}\\n\` - ) - if (!markerExists) { - throw new Error('ABI mismatch sentinel') - } - return { - dir: '../build/Release/', - module: { - listJobProcessIds() {}, - terminateJob() {}, - assignCurrentProcessToJob() {} - } - } -} -` - ) - writeFakeWindowsRegistry(projectDir, { requiresMarker: windowsRegistryRequiresMarker }) -} - -function writeLoadableNativeModules(projectDir, { nativeDir = null } = {}) { - const nodePtyDir = join(projectDir, 'node_modules', 'node-pty') - mkdirSync(join(nodePtyDir, 'lib'), { recursive: true }) - writeFileSync( - join(nodePtyDir, 'package.json'), - '{"name":"node-pty","version":"1.1.0","main":"index.js"}\n' - ) - mkdirSync(join(nodePtyDir, 'scripts'), { recursive: true }) - writeFileSync(join(nodePtyDir, 'scripts', 'post-install.js'), '') - - writeFileSync(join(nodePtyDir, 'index.js'), 'module.exports = {}\n') - writeFileSync( - join(nodePtyDir, 'lib', 'utils.js'), - ` -const { appendFileSync, existsSync } = require('node:fs') - -exports.loadNativeModule = function loadNativeModule(nativeName) { - const rebuilt = existsSync(process.env.ORCA_NATIVE_TEST_MARKER) - const dir = ${JSON.stringify(nativeDir)} ?? - (rebuilt ? '../build/Release/' : '../prebuilds/' + process.platform + '-' + process.arch + '/') - appendFileSync(process.env.ORCA_NATIVE_TEST_LOG, \`node-pty load \${nativeName} dir=\${dir}\\n\`) - return { - dir, - module: { - listJobProcessIds: () => [], - terminateJob: () => true, - assignCurrentProcessToJob: () => true - } - } -} -` - ) - writeFakeWindowsRegistry(projectDir) -} - -function writeFakeWindowsRegistry(projectDir, { requiresMarker = false } = {}) { - if (process.platform !== 'win32') { - return - } - const registryDir = join(projectDir, 'node_modules', '@orca', 'windows-registry') - mkdirSync(registryDir, { recursive: true }) - writeFileSync( - join(registryDir, 'package.json'), - '{"name":"@orca/windows-registry","version":"1.0.0","main":"index.js"}\n' - ) - const markerGate = requiresMarker - ? `if (!require('node:fs').existsSync(process.env.ORCA_NATIVE_TEST_MARKER)) { throw new Error('registry ABI mismatch sentinel') }` - : '' - writeFileSync( - join(registryDir, 'index.js'), - `exports.HK = { CU: 0x80000001 }; exports.getRegistryKey = () => { ${markerGate}; return {} }\n` - ) - const processTreeDir = join(projectDir, 'node_modules', '@vscode', 'windows-process-tree') - mkdirSync(processTreeDir, { recursive: true }) - writeFileSync(join(processTreeDir, 'index.js'), 'module.exports = {}\n') -} - -function writeNodePtyPatchFile(projectDir) { - mkdirSync(join(projectDir, 'config', 'patches'), { recursive: true }) - writeFileSync(join(projectDir, 'config', 'patches', 'node-pty@1.1.0.patch'), 'patch marker\n') -} - -function writePatchedNodePtyBuildArtifacts(projectDir) { - const buildDir = join(projectDir, 'node_modules', 'node-pty', 'build', 'Release') - mkdirSync(buildDir, { recursive: true }) - if (process.platform === 'win32') { - writeFileSync(join(buildDir, 'conpty.node'), '') - mkdirSync(join(buildDir, 'conpty'), { recursive: true }) - writeFileSync(join(buildDir, 'conpty', 'conpty.dll'), '') - writeFileSync(join(buildDir, 'conpty', 'OpenConsole.exe'), '') - return - } - writeFileSync(join(buildDir, 'pty.node'), '') - if (process.platform === 'darwin') { - writeFileSync(join(buildDir, 'spawn-helper'), '') - } -} - -function writeFakePnpm(binDir) { - mkdirSync(binDir, { recursive: true }) - const shimPath = join(binDir, 'pnpm-shim.cjs') - writeFileSync( - shimPath, - ` -const { appendFileSync, writeFileSync } = require('node:fs') - -appendFileSync(process.env.ORCA_NATIVE_TEST_LOG, \`pnpm \${process.argv.slice(2).join(' ')}\\n\`) -appendFileSync(process.env.ORCA_NATIVE_TEST_LOG, \`cwd=\${process.cwd()}\\n\`) -appendFileSync( - process.env.ORCA_NATIVE_TEST_LOG, - \`npm_config_build_from_source=\${process.env.npm_config_build_from_source || ''}\\n\` -) -appendFileSync( - process.env.ORCA_NATIVE_TEST_LOG, - \`cxxflags=\${process.env.CXXFLAGS || ''}\\n\` -) -writeFileSync(process.env.ORCA_NATIVE_TEST_MARKER, 'rebuilt') -` - ) - - const posixPnpmPath = join(binDir, 'pnpm') - writeFileSync(posixPnpmPath, `#!/usr/bin/env node\nrequire(${JSON.stringify(shimPath)})\n`) - chmodSync(posixPnpmPath, 0o755) - writeFileSync( - join(binDir, 'pnpm.cmd'), - `@echo off\r\n"${process.execPath}" "%~dp0\\pnpm-shim.cjs" %*\r\n` - ) -} diff --git a/config/scripts/finalize-signed-cli-runtime.mjs b/config/scripts/finalize-signed-cli-runtime.mjs new file mode 100644 index 000000000000..a6db409ae42c --- /dev/null +++ b/config/scripts/finalize-signed-cli-runtime.mjs @@ -0,0 +1,128 @@ +import { createHash } from 'node:crypto' +import { readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs' +import { join, resolve } from 'node:path' +import { pathToFileURL } from 'node:url' +import { ORCAD_BUN_RELEASE_ASSETS, ORCAD_BUN_VERSION } from '../../src/shared/orcad-bun-runtime.ts' +import { verifyCliRuntimeDirectory } from '../bundled-cli-runtime.cjs' + +const digest = (bytes) => createHash('sha256').update(bytes).digest('hex') + +function peSigningFields(bytes) { + if (bytes.length < 64 || bytes.toString('ascii', 0, 2) !== 'MZ') { + throw new Error('Runtime signing requires a complete PE image') + } + const pe = bytes.readUInt32LE(60) + const optional = pe + 24 + if (optional + 2 > bytes.length || bytes.toString('ascii', pe, pe + 4) !== 'PE\0\0') { + throw new Error('Invalid runtime PE header') + } + const magic = bytes.readUInt16LE(optional) + if (magic !== 0x20b && magic !== 0x10b) { + throw new Error('Unsupported runtime PE optional header') + } + const directories = optional + (magic === 0x20b ? 112 : 96) + const security = directories + 4 * 8 + if ( + security + 8 > bytes.length || + bytes.readUInt16LE(pe + 20) < security + 8 - optional || + bytes.readUInt32LE(directories - 4) < 5 + ) { + throw new Error('Missing runtime PE certificate directory') + } + return { + checksum: optional + 64, + security, + offset: bytes.readUInt32LE(security), + size: bytes.readUInt32LE(security + 4) + } +} + +/** Signing may append a certificate and edit its directory/checksum, never executable bytes. */ +export function assertAuthenticodeOnlyChange(original, signed) { + const before = peSigningFields(original) + const after = peSigningFields(signed) + if (before.offset !== 0 || before.size !== 0) { + throw new Error('Refusing to replace an existing vendor runtime signature') + } + const alignedLength = Math.ceil(original.length / 8) * 8 + if ( + after.checksum !== before.checksum || + after.security !== before.security || + after.offset !== alignedLength || + after.size < 8 || + after.offset + after.size !== signed.length + ) { + throw new Error('Signed runtime has unexpected certificate layout') + } + for (let offset = after.offset; offset < signed.length;) { + if (offset + 8 > signed.length) { + throw new Error('Truncated runtime certificate header') + } + const length = signed.readUInt32LE(offset) + const paddedLength = Math.ceil(length / 8) * 8 + if ( + length < 8 || + offset + paddedLength > signed.length || + signed.readUInt16LE(offset + 4) !== 0x200 || + signed.readUInt16LE(offset + 6) !== 2 + ) { + throw new Error('Malformed runtime certificate record') + } + offset += paddedLength + } + if (signed.subarray(original.length, alignedLength).some((byte) => byte !== 0)) { + throw new Error('Signed runtime has nonzero alignment padding') + } + const image = Buffer.from(signed.subarray(0, original.length)) + original.copy(image, before.checksum, before.checksum, before.checksum + 4) + original.copy(image, before.security, before.security, before.security + 8) + if (!image.equals(original)) { + throw new Error('Signing changed runtime executable bytes') + } +} + +export function finalizeSignedCliRuntime(appDirectory, signingStage) { + const relative = join('resources', 'cli-runtime') + const directory = join(appDirectory, relative) + const manifestPath = join(directory, 'runtime.json') + const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) + if (!/^win32-(x64|arm64)$/.test(manifest.target) || manifest.version !== ORCAD_BUN_VERSION) { + throw new Error('Invalid Windows runtime signing manifest') + } + const expected = ORCAD_BUN_RELEASE_ASSETS[manifest.target].executableSha256 + const signed = readFileSync(join(directory, 'bun-runtime.exe')) + const signedHash = digest(signed) + if (signedHash !== manifest.sha256) { + const original = readFileSync(join(signingStage, relative, 'bun-runtime.exe')) + if (digest(original) !== expected || manifest.sha256 !== expected) { + throw new Error('Pre-sign runtime does not match the pinned release') + } + assertAuthenticodeOnlyChange(original, signed) + const temporary = `${manifestPath}.signed-${process.pid}` + try { + writeFileSync( + temporary, + `${JSON.stringify({ ...manifest, sha256: signedHash, unsignedSha256: expected })}\n`, + { flag: 'wx' } + ) + renameSync(temporary, manifestPath) + } finally { + rmSync(temporary, { force: true }) + } + } else if (signedHash !== expected) { + // Re-running is safe only while the retained original still proves the signed payload. + const original = readFileSync(join(signingStage, relative, 'bun-runtime.exe')) + if (digest(original) !== expected || manifest.unsignedSha256 !== expected) { + throw new Error('Signed runtime provenance is missing') + } + assertAuthenticodeOnlyChange(original, signed) + } + verifyCliRuntimeDirectory(directory, 'win32', manifest.target.slice('win32-'.length)) +} + +if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) { + if (!process.argv[2] || !process.argv[3]) { + throw new Error('Usage: finalize-signed-cli-runtime.mjs ') + } + finalizeSignedCliRuntime(resolve(process.argv[2]), resolve(process.argv[3])) +} diff --git a/config/scripts/finalize-signed-cli-runtime.test.mjs b/config/scripts/finalize-signed-cli-runtime.test.mjs new file mode 100644 index 000000000000..ad2fa71289a2 --- /dev/null +++ b/config/scripts/finalize-signed-cli-runtime.test.mjs @@ -0,0 +1,141 @@ +import { createHash } from 'node:crypto' +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, expect, it, vi } from 'vitest' +import { writeBundledCliRuntimeFixture } from './bundled-cli-runtime-fixture.mjs' +const pins = vi.hoisted(() => ({ hash: '' })) +vi.mock('../../src/shared/orcad-bun-runtime.ts', async (importOriginal) => { + const original = await importOriginal() + return { + ...original, + ORCAD_BUN_RELEASE_ASSETS: { + 'win32-x64': { + get executableSha256() { + return pins.hash + } + } + } + } +}) +import { + assertAuthenticodeOnlyChange, + finalizeSignedCliRuntime +} from './finalize-signed-cli-runtime.mjs' +import { verifyCliRuntimeDirectory } from '../bundled-cli-runtime.cjs' +const roots = [] +afterEach(() => roots.splice(0).forEach((root) => rmSync(root, { recursive: true, force: true }))) +const hash = (bytes) => createHash('sha256').update(bytes).digest('hex') +function images(magic = 0x20b) { + const original = Buffer.alloc(515) + original.write('MZ') + original.writeUInt32LE(64, 60) + original.write('PE\0\0', 64) + original.writeUInt16LE(240, 84) + original.writeUInt16LE(magic, 88) + const directory = 88 + (magic === 0x20b ? 112 : 96) + original.writeUInt32LE(16, directory - 4) + original[510] = 42 + const signed = Buffer.concat([original, Buffer.alloc(5), Buffer.alloc(16, 7)]) + signed.writeUInt32LE(16, 520) + signed.writeUInt16LE(0x200, 524) + signed.writeUInt16LE(2, 526) + signed.writeUInt32LE(123, 88 + 64) + signed.writeUInt32LE(520, directory + 32) + signed.writeUInt32LE(16, directory + 36) + return { original, signed, security: directory + 32 } +} + +it.each([0x20b, 0x10b])( + 'allows only certificate append/checksum edits for PE magic %s', + (magic) => { + const { original, signed } = images(magic) + expect(() => assertAuthenticodeOnlyChange(original, signed)).not.toThrow() + } +) +it.each([ + 'code', + 'padding', + 'tail', + 'directory', + 'vendor-signature', + 'certificate-length', + 'certificate-type' +])('rejects unexpected signing change: %s', (kind) => { + let { original, signed, security } = images() + if (kind === 'certificate-length') { + signed.writeUInt32LE(1000, 520) + } + if (kind === 'certificate-type') { + signed.writeUInt16LE(1, 526) + } + if (kind === 'code') { + signed[510]++ + } + if (kind === 'padding') { + signed[517] = 1 + } + if (kind === 'tail') { + signed = Buffer.concat([signed, Buffer.from('extra')]) + } + if (kind === 'directory') { + signed.writeUInt32LE(512, security) + } + if (kind === 'vendor-signature') { + original.writeUInt32LE(512, security) + } + expect(() => assertAuthenticodeOnlyChange(original, signed)).toThrow() +}) +it('rejects truncated or non-PE images', () => { + expect(() => assertAuthenticodeOnlyChange(Buffer.from('bad'), Buffer.from('bad'))).toThrow() +}) + +async function stagedFixture() { + const root = mkdtempSync(join(tmpdir(), 'signed-runtime-')) + roots.push(root) + const app = join(root, 'app') + const stage = join(root, 'signing-stage') + const relative = join('resources', 'cli-runtime') + const directory = join(app, relative) + await writeBundledCliRuntimeFixture(directory, 'win32', 'x64') + const { original, signed } = images() + pins.hash = hash(original) + mkdirSync(join(stage, relative), { recursive: true }) + writeFileSync(join(stage, relative, 'bun-runtime.exe'), original) + writeFileSync(join(directory, 'bun-runtime.exe'), signed) + const manifestPath = join(directory, 'runtime.json') + const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) + writeFileSync(manifestPath, JSON.stringify({ ...manifest, sha256: pins.hash })) + return { app, stage, directory, manifestPath, signed } +} +it('finalizes the signed full-file identity, preserving runtime verification and repeatability', async () => { + const f = await stagedFixture() + expect(() => verifyCliRuntimeDirectory(f.directory, 'win32', 'x64')).toThrow('checksum') + finalizeSignedCliRuntime(f.app, f.stage) + expect(JSON.parse(readFileSync(f.manifestPath, 'utf8'))).toMatchObject({ + sha256: hash(f.signed), + unsignedSha256: pins.hash + }) + expect(() => verifyCliRuntimeDirectory(f.directory, 'win32', 'x64')).not.toThrow() + expect(() => finalizeSignedCliRuntime(f.app, f.stage)).not.toThrow() + f.signed[510]++ + writeFileSync(join(f.directory, 'bun-runtime.exe'), f.signed) + expect(() => verifyCliRuntimeDirectory(f.directory, 'win32', 'x64')).toThrow('checksum') +}) +it('refuses to bless payload changes or unpinned original bytes', async () => { + const f = await stagedFixture() + f.signed[510]++ + writeFileSync(join(f.directory, 'bun-runtime.exe'), f.signed) + expect(() => finalizeSignedCliRuntime(f.app, f.stage)).toThrow('executable bytes') + pins.hash = '0'.repeat(64) + expect(() => finalizeSignedCliRuntime(f.app, f.stage)).toThrow('pinned release') +}) + +it('refuses a changed original manifest without rewriting it', async () => { + const f = await stagedFixture() + const manifest = JSON.parse(readFileSync(f.manifestPath, 'utf8')) + const changed = JSON.stringify({ ...manifest, sha256: 'f'.repeat(64) }) + writeFileSync(f.manifestPath, changed) + expect(() => finalizeSignedCliRuntime(f.app, f.stage)).toThrow('pinned release') + expect(readFileSync(f.manifestPath, 'utf8')).toBe(changed) +}) diff --git a/config/scripts/headless-serve-shutdown-workflow.test.mjs b/config/scripts/headless-serve-shutdown-workflow.test.mjs index e9f70ffc6abe..1a4e78639e3a 100644 --- a/config/scripts/headless-serve-shutdown-workflow.test.mjs +++ b/config/scripts/headless-serve-shutdown-workflow.test.mjs @@ -1,7 +1,10 @@ -import { readFileSync } from 'node:fs' +import { mkdtempSync, readFileSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' import { parse } from 'yaml' import { describe, expect, it } from 'vitest' +import { runProcess } from '../../src/shared/child-process/run-process' const workflow = parse(readFileSync('.github/workflows/pr.yml', 'utf8')) const headlessLinuxGuide = readFileSync('docs/reference/headless-linux-server.md', 'utf8') @@ -72,6 +75,49 @@ describe('headless serve shutdown PR gate', () => { ).toHaveLength(1) }) + it.skipIf(process.platform === 'win32')( + 'can observe empty logs before the child is scheduled', + async () => { + const directory = mkdtempSync(join(tmpdir(), 'orca-shutdown-readiness-')) + const preparation = signalCase.slice( + signalCase.indexOf('stdout_log='), + signalCase.indexOf('ulimit -c') + ) + expect(signalCase.indexOf('ulimit -c')).toBeLessThan(signalCase.indexOf('setsid env')) + try { + const negative = await runProcess({ + program: 'bash', + args: [ + '-c', + 'set -euo pipefail\nstate_dir=$1\ncat "$state_dir/stdout.log" "$state_dir/stderr.log"', + 'missing-logs', + directory + ], + timeoutMs: 5000 + }) + expect(negative.code).not.toBe(0) + const result = await runProcess({ + program: 'bash', + args: [ + '-c', + `set -euo pipefail +state_dir=$1 +${preparation} +cat "$stdout_log" "$stderr_log"`, + 'readiness-fixture', + directory + ], + timeoutMs: 5000 + }) + expect(result.code, result.stderr).toBe(0) + expect(result.stdout).toBe('') + expect(result.stderr).toBe('') + } finally { + rmSync(directory, { recursive: true, force: true }) + } + } + ) + it('keeps readiness polling finite and leak-free', () => { expect(signalCase).toContain('read_ready_line()') expect(signalCase).toContain("sed -u -n 's/^[^{]*//p'") diff --git a/config/scripts/linux-packaged-node-pty-floor-child.cjs b/config/scripts/linux-packaged-node-pty-floor-child.cjs deleted file mode 100644 index a31efe35ba51..000000000000 --- a/config/scripts/linux-packaged-node-pty-floor-child.cjs +++ /dev/null @@ -1,47 +0,0 @@ -const { join } = require('node:path') - -const resourcesDirectory = process.argv[2] -if (!resourcesDirectory) { - throw new Error('linux-packaged-node-pty-floor-resources-required') -} - -const pty = require(join(resourcesDirectory, 'node_modules', 'node-pty')) -const expected = 'orca-node-pty-floor-ok' -let output = '' -let settled = false -const child = pty.spawn('/bin/sh', ['-c', `printf ${expected}; sleep 0.1`], { - name: 'xterm-color', - cols: 80, - rows: 24, - cwd: '/tmp', - env: { PATH: process.env.PATH ?? '/usr/bin:/bin' } -}) - -const timeout = setTimeout(() => { - if (settled) { - return - } - settled = true - child.kill() - process.stderr.write('packaged node-pty floor smoke timed out\n') - process.exitCode = 1 -}, 5_000) - -child.onData((data) => { - output = `${output}${data}`.slice(-256) -}) -child.onExit(({ exitCode }) => { - if (settled) { - return - } - settled = true - clearTimeout(timeout) - if (exitCode !== 0 || output !== expected) { - process.stderr.write( - `packaged node-pty floor smoke failed: exit=${exitCode} bytes=${Buffer.byteLength(output)}\n` - ) - process.exitCode = 1 - return - } - process.stdout.write(`${expected}\n`) -}) diff --git a/config/scripts/mobile-web-bundle-packaging-workflow-contract.test.mjs b/config/scripts/mobile-web-bundle-packaging-workflow-contract.test.mjs index 364765adb23c..d362ecef117e 100644 --- a/config/scripts/mobile-web-bundle-packaging-workflow-contract.test.mjs +++ b/config/scripts/mobile-web-bundle-packaging-workflow-contract.test.mjs @@ -87,6 +87,7 @@ function packsWithBeforePack(text) { // workflow has to be added here deliberately, with its bundle step, rather than slipping in. const EXPECTED_PACKAGING_JOBS = [ 'adhoc-mac-build.yml build-adhoc-mac', + 'bun-profile-tests.yml linux_package', 'bun-profile-tests.yml signed_mac_package', 'daemon-relocation-spike.yml spike', 'daily-mac-build.yml build-daily-mac', diff --git a/config/scripts/node-pty-console-list-agent-patch.test.mjs b/config/scripts/node-pty-console-list-agent-patch.test.mjs deleted file mode 100644 index 07e97c548324..000000000000 --- a/config/scripts/node-pty-console-list-agent-patch.test.mjs +++ /dev/null @@ -1,82 +0,0 @@ -import { createRequire } from 'node:module' -import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' -import { join, resolve } from 'node:path' -import { afterEach, describe, expect, it } from 'vitest' - -const require = createRequire(import.meta.url) -const { - assertPatchedNodePtyConsoleListAgent, - patchNodePtyConsoleListAgent -} = require('../relay-assets/node-pty-1.1.0-console-list-agent-patch.cjs') -const projectDir = resolve(import.meta.dirname, '..', '..') -const cleanupDirs = [] - -afterEach(() => { - for (const dir of cleanupDirs.splice(0)) { - rmSync(dir, { recursive: true, force: true }) - } -}) - -describe('Windows SSH relay node-pty console-list patch', () => { - it('installs and verifies the fallback idempotently', () => { - const fixture = writeNodePtyFixture('1.1.0', publishedAgentSource()) - - patchNodePtyConsoleListAgent(fixture.root) - const once = readFileSync(fixture.agentPath, 'utf8') - expect(once).toContain('consoleProcessList = [shellPid];') - expect(existsSync(`${fixture.agentPath}.orca-patch-${process.pid}`)).toBe(false) - expect(() => assertPatchedNodePtyConsoleListAgent(fixture.root)).not.toThrow() - - patchNodePtyConsoleListAgent(fixture.root) - expect(readFileSync(fixture.agentPath, 'utf8')).toBe(once) - }) - - it('refuses a different package version or unexpected agent source', () => { - const wrongVersion = writeNodePtyFixture('1.2.0-beta.11', publishedAgentSource()) - expect(() => patchNodePtyConsoleListAgent(wrongVersion.root)).toThrow('expected 1.1.0') - - const drifted = writeNodePtyFixture('1.1.0', `${publishedAgentSource()}\n// drift`) - expect(() => patchNodePtyConsoleListAgent(drifted.root)).toThrow('unexpected node-pty') - - const tamperedPatch = writeNodePtyFixture('1.1.0', publishedAgentSource()) - patchNodePtyConsoleListAgent(tamperedPatch.root) - writeFileSync( - tamperedPatch.agentPath, - `${readFileSync(tamperedPatch.agentPath, 'utf8')}\n// drift` - ) - expect(() => assertPatchedNodePtyConsoleListAgent(tamperedPatch.root)).toThrow('not installed') - }) -}) - -function writeNodePtyFixture(version, agentSource) { - const root = mkdtempSync(join(projectDir, '.node-pty-console-list-patch-test-')) - cleanupDirs.push(root) - const nodePtyDir = join(root, 'node_modules', 'node-pty') - const libDir = join(nodePtyDir, 'lib') - const agentPath = join(libDir, 'conpty_console_list_agent.js') - mkdirSync(libDir, { recursive: true }) - writeFileSync(join(nodePtyDir, 'package.json'), JSON.stringify({ version })) - writeFileSync(agentPath, agentSource) - return { root, agentPath } -} - -function publishedAgentSource() { - return [ - '"use strict";', - '/**', - ' * Copyright (c) 2019, Microsoft Corporation (MIT License).', - ' *', - ' * This module fetches the console process list for a particular PID. It must be', - ' * called from a different process (child_process.fork) as there can only be a', - ' * single console attached to a process.', - ' */', - 'Object.defineProperty(exports, "__esModule", { value: true });', - 'var utils_1 = require("./utils");', - "var getConsoleProcessList = utils_1.loadNativeModule('conpty_console_list').module.getConsoleProcessList;", - 'var shellPid = parseInt(process.argv[2], 10);', - 'var consoleProcessList = getConsoleProcessList(shellPid);', - 'process.send({ consoleProcessList: consoleProcessList });', - 'process.exit(0);', - '//# sourceMappingURL=conpty_console_list_agent.js.map' - ].join('\n') -} diff --git a/config/scripts/node-pty-job-ownership.cjs b/config/scripts/node-pty-job-ownership.cjs deleted file mode 100644 index c639e94aa6a0..000000000000 --- a/config/scripts/node-pty-job-ownership.cjs +++ /dev/null @@ -1,227 +0,0 @@ -'use strict' - -const { existsSync, readFileSync } = require('node:fs') -const { dirname, join, resolve } = require('node:path') -const { PE_MACHINE, describePeMachine, readPeMachine } = require('./windows-pe-machine.cjs') - -const NODE_PTY_JOB_EXPORTS = ['listJobProcessIds', 'terminateJob', 'assignCurrentProcessToJob'] - -/** - * The wide literal `usesCygwinRuntime` probes for in conpty.cc, as it sits in - * the compiled addon. - * - * Why sniff the binary rather than trust the exports: all three job exports - * predate the Cygwin/MSYS breakaway denial, so symbol presence cannot tell a - * current build from one whose per-PTY job still carries - * JOB_OBJECT_LIMIT_BREAKAWAY_OK. Measured on Windows 11: such a build passes - * every export check, reports isPtyJobOwnershipAvailable() true, and passes - * windows-pty-job.win32.test.ts 6/6, while every child of a Git Bash pane is - * created outside the pane's job and survives terminatePtyJob. See - * docs/reference/windows-msys-job-breakaway.md. - * - * Same shape as stagedRelayAddonIsUnpatched() in - * src/main/windows/windows-process-table.ts, which already tells a patched - * addon from a published one by a binary import name. - */ -const CYGWIN_BREAKAWAY_MARKER_TEXT = 'msys-2.0.dll' -const CYGWIN_BREAKAWAY_MARKER = Buffer.from(CYGWIN_BREAKAWAY_MARKER_TEXT, 'utf16le') - -/** True when the addon carries the denial. Read errors propagate: callers that cannot read it must not pass. */ -function conptyDeniesCygwinBreakaway(addonPath) { - return readFileSync(addonPath).includes(CYGWIN_BREAKAWAY_MARKER) -} - -/** Where the patch adds the literal above, relative to node-pty's root. */ -const NODE_PTY_CONPTY_SOURCE = join('src', 'win', 'conpty.cc') - -/** - * The verdict on the source a Windows rebuild is about to compile. - * - * Why before compiling and not only after: pnpm materializes node_modules/node-pty - * from config/patches/node-pty@1.1.0.patch at install time, so an install that - * predates the denial holds source without it, and no rebuild of that source can - * yield an addon the post-rebuild gate accepts. Measured on a Windows dev - * checkout: `--force` compiled for minutes, rewrote conpty.node byte-identical - * and unpatched, and the gate then advised "rebuild from source" -- the step - * that had just run. The remedy is a reinstall, and that is what this says. - * - * An absent source file is not judged here: node-pty ships it, and the addon - * gate that follows the rebuild still reads the binary either way. - */ -function assertNodePtySourceDeniesMsysBreakaway({ nodePtyDir }) { - const sourcePath = join(nodePtyDir, NODE_PTY_CONPTY_SOURCE) - if (!existsSync(sourcePath)) { - return - } - if (readFileSync(sourcePath, 'utf8').includes(`L"${CYGWIN_BREAKAWAY_MARKER_TEXT}"`)) { - return - } - throw new Error( - [ - `node-pty's source at ${sourcePath} does not carry the Cygwin/MSYS job-breakaway denial,`, - 'so no rebuild of it can produce an addon that does; the compile was not started.', - 'pnpm applies config/patches/node-pty@1.1.0.patch when it installs node-pty, so run', - '`pnpm install` to re-materialize it with the current patch, then rerun this command.', - `If that patch does not itself add L"${CYGWIN_BREAKAWAY_MARKER_TEXT}" to ${NODE_PTY_CONPTY_SOURCE},`, - 'this checkout predates the denial and no reinstall can supply it.', - 'See docs/reference/windows-msys-job-breakaway.md.' - ].join(' ') - ) -} - -/** - * Why here and not only at packaging: a rebuild that did not honour `--arch` - * leaves a binary the target cannot load, the app falls back to the published - * prebuild, and the packaged gate then reports it two steps from the command - * that could fix it. `PE_MACHINE` covers the Windows arches Orca ships; anything - * else this cannot judge, so it does not pretend to. - */ -function assertRebuiltConptyMatchesArch(addonPath, rebuildArch) { - const expected = PE_MACHINE[rebuildArch] - if (expected === undefined) { - return - } - const machine = readPeMachine(addonPath) - if (machine === expected) { - return - } - const consequence = [ - ', so node-pty would fall back to the published prebuild, which predates the', - 'Cygwin/MSYS job-breakaway denial and leaks every MSYS pane child out of its job.' - ].join(' ') - throw new Error( - machine === null - ? `${addonPath} is not a PE image${consequence} Check the ` + - `node-pty build output above; a truncated or quarantined artifact looks like this.` - : `${addonPath} is ${describePeMachine(machine)}, but this rebuild targets ` + - `win32-${rebuildArch} (0x${expected.toString(16)}): node-gyp did not honour ` + - `--arch${consequence}` - ) -} - -/** - * The verdict on the addon a Windows rebuild just claimed to produce. - * - * Takes the host as arguments rather than reading `process`, because the branch - * that matters -- a rebuild for the very host running it -- is otherwise - * reachable only from Windows, and a gate nobody can run is a gate nobody - * checks. - * - * Absent is fatal on that host: `loadNativeModule` falls through to - * prebuilds/win32-, and the published prebuild predates the denial, so - * the app would load it with nothing said. A cross-host rebuild need not leave - * a win32 addon on this disk, and node-pty may not be installed at all -- - * neither is evidence of a bad build. - */ -function assertRebuiltConptyDeniesMsysBreakaway({ - nodePtyDir, - rebuildArch, - crossHost, - warn = console.warn -}) { - const addonPath = join(nodePtyDir, 'build', 'Release', 'conpty.node') - if (existsSync(addonPath)) { - assertRebuiltConptyMatchesArch(addonPath, rebuildArch) - assertCygwinBreakawayDenied(addonPath, { dir: addonPath }) - return - } - if (crossHost || !existsSync(nodePtyDir)) { - warn(`[rebuild] no addon at ${addonPath}; could not check the MSYS job-breakaway denial.`) - return - } - const prebuildPath = join(nodePtyDir, 'prebuilds', `win32-${rebuildArch}`, 'conpty.node') - throw new Error( - `the rebuild reported success but ${addonPath} is not there, so node-pty would fall through ` + - `to ${prebuildPath}. That published prebuild predates the Cygwin/MSYS ` + - 'job-breakaway denial: every Git Bash pane child would be created outside its job and ' + - 'survive terminatePtyJob. Check the node-pty build output above; a same-host source ' + - 'build must leave conpty.node in build/Release.' - ) -} - -/** - * Absolute path of the addon `loadNativeModule` just resolved. - * - * `native.dir` is relative to node-pty's own `lib/`, which is the only base - * every caller shares -- the project install, a staged rebuild and the packaged - * resources tree all reach the addon through a different root. - */ -function nodePtyAddonPath(nodePtyUtilsPath, native, nativeName) { - return resolve(dirname(nodePtyUtilsPath), native.dir, `${nativeName}.node`) -} - -function assertNodePtyJobOwnership({ nativeName, native, addonPath, platform = process.platform }) { - if (platform !== 'win32' || nativeName !== 'conpty') { - return - } - const exported = native?.module ?? native - const missing = NODE_PTY_JOB_EXPORTS.filter((name) => typeof exported?.[name] !== 'function') - if (missing.length > 0) { - throw new Error( - [ - `node-pty's conpty native is missing ${missing.join(', ')}.`, - `Resolved from: ${native?.dir ?? 'unknown'}`, - 'That build cannot own a PTY tree, so terminatePtyJob degrades to "unavailable"', - 'and pane teardown falls back to guessing by PID ancestry.', - 'Rebuild node-pty from source so config/patches/node-pty@1.1.0.patch applies.' - ].join(' ') - ) - } - assertCygwinBreakawayDenied(addonPath, native) -} - -/** - * Why this refuses instead of skipping when the addon cannot be read: an - * unreadable binary is exactly the state that used to pass. `loadNativeModule` - * has already required this file, so "cannot read it" means the caller did not - * say which file it loaded, and a gate that cannot see its subject is not a - * gate. - */ -function assertCygwinBreakawayDenied(addonPath, native) { - let binary - try { - binary = readFileSync(addonPath) - } catch (error) { - throw new Error( - [ - `Cannot read node-pty's conpty native at ${addonPath ?? ''}`, - `(resolved from ${native?.dir ?? 'unknown'}): ${error.message}.`, - 'Without the binary this cannot tell a current build from one that leaks', - 'every MSYS pane child out of its job, so it refuses rather than assume.' - ].join(' ') - ) - } - if (binary.includes(CYGWIN_BREAKAWAY_MARKER)) { - return - } - throw staleConptySourceBuildError(addonPath) -} - -/** The verdict on a source build that is simply out of date: rebuild it here. */ -function staleConptySourceBuildError(addonPath) { - return new Error( - [ - `node-pty's conpty native at ${addonPath} predates the Cygwin/MSYS job-breakaway denial.`, - 'It exports the job functions, so it looks patched, but its per-PTY job still carries', - 'JOB_OBJECT_LIMIT_BREAKAWAY_OK and every Git Bash child is created outside the job:', - 'terminatePtyJob reports "terminated" and leaves the tree running.', - 'Rebuild node-pty from source so the current config/patches/node-pty@1.1.0.patch applies', - '(a worktree sharing node_modules with its main checkout shares that stale addon).', - `If that patch no longer adds L"${CYGWIN_BREAKAWAY_MARKER_TEXT}" to conpty.cc then this marker is`, - 'stale, not the addon, and no rebuild can satisfy it.', - 'See docs/reference/windows-msys-job-breakaway.md.' - ].join(' ') - ) -} - -module.exports = { - CYGWIN_BREAKAWAY_MARKER, - CYGWIN_BREAKAWAY_MARKER_TEXT, - assertNodePtyJobOwnership, - assertCygwinBreakawayDenied, - assertNodePtySourceDeniesMsysBreakaway, - assertRebuiltConptyDeniesMsysBreakaway, - conptyDeniesCygwinBreakaway, - nodePtyAddonPath, - staleConptySourceBuildError -} diff --git a/config/scripts/node-pty-master-cloexec-patch.test.mjs b/config/scripts/node-pty-master-cloexec-patch.test.mjs deleted file mode 100644 index e323c5d61c85..000000000000 --- a/config/scripts/node-pty-master-cloexec-patch.test.mjs +++ /dev/null @@ -1,333 +0,0 @@ -import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' -import { createRequire } from 'node:module' -import { join, resolve } from 'node:path' -import { afterEach, describe, expect, it } from 'vitest' - -const require = createRequire(import.meta.url) -const { - SKIP_MARKER_FILENAME, - applyNodePtyMasterCloexecPatch, - assertPatchedNodePtyMasterCloexecSource, - patchNodePtyMasterCloexecSource, - revertNodePtyMasterCloexecSource -} = require('../relay-assets/node-pty-1.1.0-master-cloexec-patch.cjs') - -// Byte-exact src/unix/pty.cc from the npm tarball the relay installs. The patch is keyed by its -// sha256, so a fixture that drifted from what npm ships would make every assertion below vacuous. -const STOCK_SOURCE = readFileSync( - resolve(import.meta.dirname, '__fixtures__', 'node-pty-1.1.0-unix-pty.cc'), - 'utf8' -) -const projectDir = resolve(import.meta.dirname, '..', '..') -const cleanupDirs = [] - -afterEach(() => { - for (const dir of cleanupDirs.splice(0)) { - rmSync(dir, { recursive: true, force: true }) - } -}) - -describe('SSH relay node-pty pty fd-leak patch', () => { - it('adds the forkpty close-on-exec call and reverts to the published bytes', () => { - const fixture = writeRelayFixture() - - expect(patchNodePtyMasterCloexecSource(fixture.root)).toBe(true) - const patched = readFileSync(fixture.sourcePath, 'utf8') - expect(patched).toContain('pty_cloexec(int fd)') - expect(patched).toContain('if (pty_cloexec(master) == -1)') - expect(() => assertPatchedNodePtyMasterCloexecSource(fixture.root)).not.toThrow() - - expect(patchNodePtyMasterCloexecSource(fixture.root)).toBe(false) - expect(readFileSync(fixture.sourcePath, 'utf8')).toBe(patched) - - expect(revertNodePtyMasterCloexecSource(fixture.root)).toBe(true) - expect(readFileSync(fixture.sourcePath, 'utf8')).toBe(STOCK_SOURCE) - }) - - it('rewrites the Apple branch, which is the only one macOS executes', () => { - const fixture = writeRelayFixture() - patchNodePtyMasterCloexecSource(fixture.root) - const patched = readFileSync(fixture.sourcePath, 'utf8') - - // Stock's cleanup never runs: the first posix_openpt() already returns >= 2, so the loop - // breaks with count == 0 -- and where it does run it closes low_fds[count], never low_fds[0]. - expect(STOCK_SOURCE).toContain('for (; count > 0; count--) {') - expect(patched).not.toContain('for (; count > 0; count--) {') - expect(patched).toContain('int low_fds[3] = {-1, -1, -1};') - expect(patched).toContain('for (size_t i = 0; i <= count && i < 3; i++) {') - - // `default:` sits in the `#else` arm of PtyFork's `#if defined(__APPLE__)`, so marking only - // the forkpty call site left the master macOS actually opens unmarked. - expect(patched).toContain( - ' if (pty_cloexec(master) == -1) {\n' + - ' throw Napi::Error::New(napiEnv, "Could not set master fd to close-on-exec.");\n' + - ' }\n#else\n' - ) - }) - - it('refuses a different node-pty version or an unrecognized source', () => { - const wrongVersion = writeRelayFixture({ version: '1.2.0-beta.4' }) - expect(() => patchNodePtyMasterCloexecSource(wrongVersion.root)).toThrow('expected 1.1.0') - - const drifted = writeRelayFixture({ - source: `${STOCK_SOURCE}\n// drift\n` - }) - expect(() => patchNodePtyMasterCloexecSource(drifted.root)).toThrow('unexpected node-pty') - - const tampered = writeRelayFixture() - patchNodePtyMasterCloexecSource(tampered.root) - writeFileSync(tampered.sourcePath, `${readFileSync(tampered.sourcePath, 'utf8')}\n// drift\n`) - expect(() => assertPatchedNodePtyMasterCloexecSource(tampered.root)).toThrow('not installed') - }) - - it('keeps the rebuilt addon once a later child no longer inherits the master', () => { - const fixture = writeRelayFixture() - const calls = [] - - const status = applyNodePtyMasterCloexecPatch(fixture.root, { - platform: 'linux', - rebuild: () => { - calls.push('rebuild') - writeBuild(fixture, 'patched-build') - }, - verify: () => 'isolated' - }) - - expect(status).toBe('patched') - expect(calls).toEqual(['rebuild']) - expect(readFileSync(fixture.buildPath, 'utf8')).toBe('patched-build') - expect(readFileSync(fixture.sourcePath, 'utf8')).not.toBe(STOCK_SOURCE) - expect(existsSync(fixture.backupDir)).toBe(false) - expect(existsSync(fixture.skipMarkerPath)).toBe(false) - }) - - it('keeps a rebuilt addon whose flag /proc could not confirm', () => { - const fixture = writeRelayFixture() - - const status = applyNodePtyMasterCloexecPatch(fixture.root, { - platform: 'linux', - rebuild: () => writeBuild(fixture, 'patched-build'), - verify: () => 'unverified' - }) - - expect(status).toBe('patched-unverified') - expect(readFileSync(fixture.buildPath, 'utf8')).toBe('patched-build') - }) - - it('restores the working build when the compile fails, and never retries it', () => { - const fixture = writeRelayFixture() - const calls = [] - - const failed = applyNodePtyMasterCloexecPatch(fixture.root, { - platform: 'linux', - rebuild: () => { - calls.push('rebuild') - throw new Error('npm rebuild node-pty exited 1: no C++ toolchain') - }, - verify: () => 'isolated' - }) - - expect(failed).toContain('failed:') - expect(failed).toContain('no C++ toolchain') - expect(readFileSync(fixture.buildPath, 'utf8')).toBe('stock-build') - expect(readFileSync(fixture.sourcePath, 'utf8')).toBe(STOCK_SOURCE) - expect(existsSync(fixture.backupDir)).toBe(false) - expect(existsSync(fixture.skipMarkerPath)).toBe(true) - - // Bounded, not backed off: a relay directory gets one compile attempt, ever. - const again = applyNodePtyMasterCloexecPatch(fixture.root, { - platform: 'linux', - rebuild: () => calls.push('rebuild'), - verify: () => 'isolated' - }) - expect(again).toBe('skipped:earlier-attempt-failed') - expect(calls).toEqual(['rebuild']) - }) - - it('restores the working build when the rebuilt addon still leaks the master', () => { - const fixture = writeRelayFixture() - - const status = applyNodePtyMasterCloexecPatch(fixture.root, { - platform: 'linux', - rebuild: () => writeBuild(fixture, 'still-leaky-build'), - verify: () => { - throw new Error('rebuilt node-pty still leaks the pty master into later children') - } - }) - - expect(status).toContain('still leaks') - expect(readFileSync(fixture.buildPath, 'utf8')).toBe('stock-build') - expect(readFileSync(fixture.sourcePath, 'utf8')).toBe(STOCK_SOURCE) - }) - - it('never compiles on a platform with no pty fds to leak', () => { - const fixture = writeRelayFixture() - const calls = [] - const status = applyNodePtyMasterCloexecPatch(fixture.root, { - platform: 'win32', - rebuild: () => calls.push('rebuild'), - verify: () => 'isolated' - }) - expect(status).toBe('skipped:unsupported-platform') - expect(calls).toEqual([]) - expect(readFileSync(fixture.sourcePath, 'utf8')).toBe(STOCK_SOURCE) - }) - - it('compiles a macOS install out from under its shipped prebuild', () => { - // macOS has no build/ at all: node-pty runs `prebuilds/darwin-`, built from the leaky - // source. Moving `prebuilds` aside is what both arms the rollback and makes node-pty's own - // install script fall through from "prebuild found" to node-gyp. - const fixture = writeRelayFixture({ platform: 'darwin' }) - const prebuildsPresentDuringRebuild = [] - - const status = applyNodePtyMasterCloexecPatch(fixture.root, { - platform: 'darwin', - arch: fixture.arch, - rebuild: () => { - prebuildsPresentDuringRebuild.push(existsSync(fixture.prebuildsDir)) - writeCompiledBuild(fixture, 'patched-build') - }, - verify: () => 'isolated' - }) - - expect(status).toBe('patched') - expect(prebuildsPresentDuringRebuild).toEqual([false]) - expect(readFileSync(fixture.compiledPath, 'utf8')).toBe('patched-build') - // The published tree must hold no unpatched binary: node-pty's loader checks build/Release - // first, but falls back to a prebuild if that ever fails to load. - expect(existsSync(fixture.prebuildsDir)).toBe(false) - expect(existsSync(fixture.backupDir)).toBe(false) - }) - - it('restores the macOS prebuild when the first compile fails', () => { - // A macOS host has no toolchain guarantee at all, so this is the common failure, not the rare - // one -- and the relay has to come back on the prebuild exactly as it was installed. - const fixture = writeRelayFixture({ platform: 'darwin' }) - - const status = applyNodePtyMasterCloexecPatch(fixture.root, { - platform: 'darwin', - arch: fixture.arch, - rebuild: () => { - writeCompiledBuild(fixture, 'half-built') - throw new Error('npm rebuild node-pty exited 1: no C++ toolchain') - }, - verify: () => 'isolated' - }) - - expect(status).toContain('failed:') - expect(readFileSync(fixture.buildPath, 'utf8')).toBe('stock-build') - expect(existsSync(fixture.compiledPath)).toBe(false) - expect(readFileSync(fixture.sourcePath, 'utf8')).toBe(STOCK_SOURCE) - expect(existsSync(fixture.skipMarkerPath)).toBe(true) - }) - - it('will not rebuild a macOS install that has no prebuild to fall back on', () => { - const fixture = writeRelayFixture({ platform: 'darwin', build: false }) - const calls = [] - - const status = applyNodePtyMasterCloexecPatch(fixture.root, { - platform: 'darwin', - arch: fixture.arch, - rebuild: () => calls.push('rebuild'), - verify: () => 'isolated' - }) - - expect(status).toBe('skipped:no-prebuild') - expect(calls).toEqual([]) - expect(readFileSync(fixture.sourcePath, 'utf8')).toBe(STOCK_SOURCE) - }) - - it('leaves an already patched install alone', () => { - const fixture = writeRelayFixture() - patchNodePtyMasterCloexecSource(fixture.root) - const calls = [] - - const status = applyNodePtyMasterCloexecPatch(fixture.root, { - platform: 'linux', - rebuild: () => calls.push('rebuild'), - verify: () => 'isolated' - }) - - expect(status).toBe('already-patched') - expect(calls).toEqual([]) - }) - - it('will not rebuild an install that has no compiled addon to fall back on', () => { - const fixture = writeRelayFixture({ build: false }) - const calls = [] - - const status = applyNodePtyMasterCloexecPatch(fixture.root, { - platform: 'linux', - rebuild: () => calls.push('rebuild'), - verify: () => 'isolated' - }) - - expect(status).toBe('skipped:no-compiled-build') - expect(calls).toEqual([]) - expect(readFileSync(fixture.sourcePath, 'utf8')).toBe(STOCK_SOURCE) - }) - - it('discards a backup stranded by an interrupted rebuild', () => { - const fixture = writeRelayFixture() - mkdirSync(fixture.backupDir, { recursive: true }) - writeFileSync(join(fixture.backupDir, 'pty.node'), 'stranded-build') - - const status = applyNodePtyMasterCloexecPatch(fixture.root, { - platform: 'linux', - rebuild: () => writeBuild(fixture, 'patched-build'), - verify: () => 'isolated' - }) - - expect(status).toBe('patched') - expect(existsSync(fixture.backupDir)).toBe(false) - expect(readFileSync(fixture.buildPath, 'utf8')).toBe('patched-build') - }) -}) - -/** - * `buildPath` is the working build the patch has to be able to fall back on, which differs by - * platform: Linux compiles into build/Release at install time, macOS runs a shipped prebuild and - * has no build/ at all. `compiledPath` is where the rebuild writes on either. - */ -function writeRelayFixture({ - version = '1.1.0', - source = STOCK_SOURCE, - build = true, - platform = 'linux', - arch = 'arm64' -} = {}) { - const root = mkdtempSync(join(projectDir, '.node-pty-cloexec-patch-test-')) - cleanupDirs.push(root) - const nodePtyDir = join(root, 'node_modules', 'node-pty') - const sourcePath = join(nodePtyDir, 'src', 'unix', 'pty.cc') - const compiledPath = join(nodePtyDir, 'build', 'Release', 'pty.node') - const prebuildsDir = join(nodePtyDir, 'prebuilds') - mkdirSync(join(nodePtyDir, 'src', 'unix'), { recursive: true }) - writeFileSync(join(nodePtyDir, 'package.json'), JSON.stringify({ version })) - writeFileSync(sourcePath, source) - const fixture = { - root, - arch, - sourcePath, - compiledPath, - prebuildsDir, - buildPath: - platform === 'darwin' ? join(prebuildsDir, `darwin-${arch}`, 'pty.node') : compiledPath, - backupDir: join(nodePtyDir, '.orca-cloexec-prepatch-release'), - skipMarkerPath: join(root, SKIP_MARKER_FILENAME) - } - if (build) { - writeBuild(fixture, 'stock-build') - } - return fixture -} - -function writeBuild(fixture, contents) { - mkdirSync(resolve(fixture.buildPath, '..'), { recursive: true }) - writeFileSync(fixture.buildPath, contents) -} - -function writeCompiledBuild(fixture, contents) { - mkdirSync(resolve(fixture.compiledPath, '..'), { recursive: true }) - writeFileSync(fixture.compiledPath, contents) -} diff --git a/config/scripts/node-pty-windows-pty-teardown-patch.test.mjs b/config/scripts/node-pty-windows-pty-teardown-patch.test.mjs deleted file mode 100644 index ba3e64bfa2f8..000000000000 --- a/config/scripts/node-pty-windows-pty-teardown-patch.test.mjs +++ /dev/null @@ -1,223 +0,0 @@ -// The relay's copy of the ConPTY teardown release, and the guard that keeps it in lockstep with -// `config/patches/node-pty@1.1.0.patch`. pnpm patches do not cross the SSH boundary, so a relay runs -// the tree `npm install` put there, and every terminal on a Windows SSH host leaked one File handle -// for the life of the relay process. -// -// The ORDER of the conin release is the fix. Releasing it at the top of the branch -- the placement -// the desktop patch uses -- was measured at 3x WORSE than shipping nothing (File +2/terminal and a -// new Process +1/terminal); releasing it after the console-list fork and the native kill is flat. -// -// Those numbers are the `!useConptyDll` branch, which is the branch a RELAY runs. Every desktop -// site that opens a terminal pane sets `useConptyDll: true` and takes the other branch, where -// upstream already destroys the input socket. Two hidden rate-limit probes -// (`src/main/rate-limits/claude-pty.ts`, `codex-pty-rate-limit-probe.ts`) do omit the option and so -// do run this hunk, but no user-visible pane does. The divergence pinned below is about which -// branch each host runs for terminals -- not about a regression in the panes users open. -import { createRequire } from 'node:module' -import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' -import { join, resolve } from 'node:path' -import { afterEach, describe, expect, it } from 'vitest' - -const require = createRequire(import.meta.url) -const { - assertPatchedNodePtyWindowsTeardown, - patchNodePtyWindowsTeardown -} = require('../relay-assets/node-pty-1.1.0-windows-pty-teardown-patch.cjs') -const projectDir = resolve(import.meta.dirname, '..', '..') -const cleanupDirs = [] - -const PATCHED_FILES = ['windowsPtyAgent.js', 'windowsTerminal.js'] - -/** The hunks config/patches/node-pty@1.1.0.patch adds to the installed desktop tree. */ -const DESKTOP_HUNKS = { - 'windowsPtyAgent.js': [ - [ - [ - ' this._inSocket.readable = false;', - ' // The non-DLL path previously only flipped `readable`, leaving the', - ' // conin PipeWrap alive until the host exited (#947).', - ' this._inSocket.destroy();', - ' this._outSocket.readable = false;', - '' - ].join('\n'), - [ - ' this._inSocket.readable = false;', - ' this._outSocket.readable = false;', - '' - ].join('\n') - ], - // The useConptyDll branch, which only the DESKTOP runs -- the relay takes the - // non-DLL branch above, where the dispose is already unconditional. Listed here - // so un-applying still yields published; the relay asset needs no counterpart. - [ - [ - ' // Orca: dispose unconditionally, as the non-DLL branch above does.', - " // Waiting for another 'data' event leaks the conout worker on every", - ' // self-exiting shell, because no more data ever arrives (F24).', - ' this._conoutSocketWorker.dispose();', - '' - ].join('\n'), - [ - " this._outSocket.on('data', function () {", - ' _this._conoutSocketWorker.dispose();', - ' });', - '' - ].join('\n') - ] - ], - 'windowsTerminal.js': [ - [ - ' // Attach before readiness so a broken ConPTY output pipe cannot be unhandled.', - null - ], - [' // A ConPTY input-pipe error must retire only this terminal.', null] - ] -} - -function desktopPath(file) { - return join(projectDir, 'node_modules', 'node-pty', 'lib', file) -} - -afterEach(() => { - for (const dir of cleanupDirs.splice(0)) { - rmSync(dir, { recursive: true, force: true }) - } -}) - -describe('Windows SSH relay node-pty ConPTY teardown patch', () => { - // Why reconstruct rather than vendor upstream: the installed tree IS the published file plus the - // desktop's hunks, so un-applying them yields upstream exactly -- and pinning that against this - // asset's own hashes is what fails loudly if either side of the pair moves. - it('takes the desktop error listeners verbatim', () => { - const fixture = writeNodePtyFixture('1.1.0') - patchNodePtyWindowsTeardown(fixture.root) - - expect(readFileSync(join(fixture.libDir, 'windowsTerminal.js'), 'utf8')).toBe( - readFileSync(desktopPath('windowsTerminal.js'), 'utf8') - ) - }) - - // The one hunk that must NOT match the desktop patch, and the reason is measured, not stylistic: - // on the branch a relay runs, releasing conin before `_getConsoleProcessList()` forks aborts - // teardown partway. Desktop terminal panes take the other branch, so no pane is affected either - // way; what this guards is a patch sync putting the early placement onto the relay's branch. - it('releases conin after the console-list fork, unlike the desktop patch placement', () => { - const fixture = writeNodePtyFixture('1.1.0') - patchNodePtyWindowsTeardown(fixture.root) - const patched = readFileSync(join(fixture.libDir, 'windowsPtyAgent.js'), 'utf8') - - const branch = patched.slice( - patched.indexOf('if (!this._useConptyDll) {'), - patched.indexOf('else {', patched.indexOf('if (!this._useConptyDll) {')) - ) - expect(branch).toContain('this._inSocket.destroy();') - expect(branch.indexOf('this._inSocket.destroy();')).toBeGreaterThan( - branch.indexOf('this._conoutSocketWorker.dispose();') - ) - expect(branch.indexOf('this._inSocket.destroy();')).toBeGreaterThan( - branch.indexOf('this._getConsoleProcessList()') - ) - // Pinned so a future "sync the relay asset to config/patches" cannot copy the early placement - // onto the relay's branch, where it costs +2 File and +1 Process per terminal. - expect(patched).not.toBe(readFileSync(desktopPath('windowsPtyAgent.js'), 'utf8')) - }) - - it('installs and verifies idempotently', () => { - const fixture = writeNodePtyFixture('1.1.0') - - patchNodePtyWindowsTeardown(fixture.root) - const once = PATCHED_FILES.map((file) => readFileSync(join(fixture.libDir, file), 'utf8')) - for (const file of PATCHED_FILES) { - expect(existsSync(`${join(fixture.libDir, file)}.orca-patch-${process.pid}`)).toBe(false) - } - expect(() => assertPatchedNodePtyWindowsTeardown(fixture.root)).not.toThrow() - - patchNodePtyWindowsTeardown(fixture.root) - expect(PATCHED_FILES.map((file) => readFileSync(join(fixture.libDir, file), 'utf8'))).toEqual( - once - ) - }) - - it('refuses a different package version or unexpected source', () => { - const wrongVersion = writeNodePtyFixture('1.2.0-beta.11') - expect(() => patchNodePtyWindowsTeardown(wrongVersion.root)).toThrow('expected 1.1.0') - - for (const file of PATCHED_FILES) { - const drifted = writeNodePtyFixture('1.1.0') - const path = join(drifted.libDir, file) - writeFileSync(path, `${readFileSync(path, 'utf8')}\n// drift`) - expect(() => patchNodePtyWindowsTeardown(drifted.root)).toThrow('unexpected node-pty') - } - }) - - it('refuses a half-applied tree, so one file cannot pass for both', () => { - for (const file of PATCHED_FILES) { - const partial = writeNodePtyFixture('1.1.0') - const fixture = writeNodePtyFixture('1.1.0') - patchNodePtyWindowsTeardown(fixture.root) - writeFileSync(join(partial.libDir, file), readFileSync(join(fixture.libDir, file), 'utf8')) - expect(() => assertPatchedNodePtyWindowsTeardown(partial.root)).toThrow('is not installed') - } - }) -}) - -/** A published node-pty tree, rebuilt by un-applying the desktop hunks from the installed one. */ -function writeNodePtyFixture(version) { - const root = mkdtempSync(join(projectDir, '.node-pty-teardown-patch-test-')) - cleanupDirs.push(root) - const libDir = join(root, 'node_modules', 'node-pty', 'lib') - mkdirSync(libDir, { recursive: true }) - writeFileSync(join(root, 'node_modules', 'node-pty', 'package.json'), JSON.stringify({ version })) - for (const file of PATCHED_FILES) { - const desktop = readFileSync(desktopPath(file), 'utf8') - for (const [marker] of DESKTOP_HUNKS[file]) { - expect(desktop).toContain(marker) - } - writeFileSync(join(libDir, file), unapplyDesktopHunks(file, desktop)) - } - return { root, libDir } -} - -/** - * Reverse of the published-to-desktop transform. - * - * `windowsTerminal.js` is taken verbatim from the desktop, so the asset's own replacement table is - * the transform and reversing it is exact. `windowsPtyAgent.js` deliberately diverges, so its - * published form is rebuilt from the desktop hunk instead -- which is also what makes this file the - * place that notices if the desktop hunk itself ever moves. - */ -function unapplyDesktopHunks(file, desktop) { - if (file === 'windowsPtyAgent.js') { - let published = desktop - for (const [patched, original] of DESKTOP_HUNKS[file]) { - expect(published.split(patched).length - 1).toBe(1) - published = published.replace(patched, original) - } - return published - } - const asset = readFileSync( - join(projectDir, 'config', 'relay-assets', 'node-pty-1.1.0-windows-pty-teardown-patch.cjs'), - 'utf8' - ) - const { PATCH_TARGETS } = loadPatchTargets(asset) - const target = PATCH_TARGETS.find((entry) => entry.relativePath.at(-1) === file) - expect(target).toBeDefined() - let published = desktop - for (const [from, to] of target.replacements.toReversed()) { - expect(published.split(to).length - 1).toBe(1) - published = published.replace(to, from) - } - return published -} - -function loadPatchTargets(assetSource) { - const module = { exports: {} } - const factory = new Function( - 'module', - 'exports', - 'require', - `${assetSource}\nmodule.exports.PATCH_TARGETS = PATCH_TARGETS` - ) - factory(module, module.exports, require) - return module.exports -} diff --git a/config/scripts/nsis-bun-terminal-uninstall.test.mjs b/config/scripts/nsis-bun-terminal-uninstall.test.mjs new file mode 100644 index 000000000000..322f15984f46 --- /dev/null +++ b/config/scripts/nsis-bun-terminal-uninstall.test.mjs @@ -0,0 +1,100 @@ +import { readFileSync } from 'node:fs' +import { join } from 'node:path' +import { describe, expect, it } from 'vitest' +import { runProcessSync } from '../../src/shared/child-process/run-process' + +const hooks = readFileSync(new URL('../nsis/orca-installer-hooks.nsh', import.meta.url), 'utf8') +const source = readFileSync( + new URL('../nsis/orca-bun-terminal-uninstall.nsh', import.meta.url), + 'utf8' +).replaceAll('\r\n', '\n') +const command = source.match(/-Command "([^"\n]+)"`/)?.[1]?.replaceAll('$$', '$') +if (!command) { + throw new Error('Missing fixed Bun terminal uninstall command') +} + +it('only cleans the dedicated Bun runtime root during genuine uninstall', () => { + const customUninstall = hooks.match(/!macro customUnInstall\b[\s\S]*?!macroend/)?.[0] + expect(customUninstall).toMatch( + /\$\{ifNot\} \$\{isUpdated\}[\s\S]*?!insertmacro ORCA_UNINSTALL_BUN_TERMINAL_HOST[\s\S]*?\$\{endIf\}/ + ) + expect(source).toContain( + '${if} $0 == 0\n RMDir /r "$LOCALAPPDATA\\Orca\\terminal-daemon-host"' + ) + expect(command).toContain( + "[IO.Path]::Combine($env:LOCALAPPDATA,'Orca','terminal-daemon-host')+'\\'" + ) + expect(command).toContain('$null=$p.Handle') + expect(command).toContain("$p.StartTime.ToUniversalTime().ToString('yyyyMMddHHmmssffffff')") + expect(command).toContain('-MethodName GetOwnerSid') + expect(command).not.toMatch(/taskkill|ExecutionPolicy|EncodedCommand|Add-Type/i) + expect(source).not.toMatch(/\/IM.*bun-runtime/i) +}) + +const syntheticProcesses = ` +if((Get-ExecutionPolicy -Scope Process) -ne 'Restricted'){exit 19}; +$script:root=[IO.Path]::Combine($env:LOCALAPPDATA,'Orca','terminal-daemon-host'); +$script:created=[DateTime]::Parse('2026-01-02T00:00:00Z').ToUniversalTime(); +function Get-CimInstance { + if($env:ORCA_UNINSTALL_CASE -eq 'query-failure'){throw 'query failed'}; + $image=$script:root+'\\v1\\bun-runtime.exe'; + if($env:ORCA_UNINSTALL_CASE -eq 'unrelated'){$image=$script:root+'-other\\bun-runtime.exe'}; + if($env:ORCA_UNINSTALL_CASE -eq 'unknown-image'){$image=$null}; + [pscustomobject]@{Name='bun-runtime.exe';ExecutablePath=$image;ProcessId=123;CreationDate=$script:created} +}; +function Invoke-CimMethod { + param($InputObject,$MethodName); + $sid=[Security.Principal.WindowsIdentity]::GetCurrent().User.Value; + if($env:ORCA_UNINSTALL_CASE -eq 'foreign-owner'){$sid='foreign'}; + [pscustomobject]@{Sid=$sid} +}; +function Get-Process { + param($Id); + $start=$script:created; + if($env:ORCA_UNINSTALL_CASE -eq 'owned-submicrosecond'){$start=$start.AddTicks(1)}; + if($env:ORCA_UNINSTALL_CASE -eq 'reused-pid'){$start=$start.AddSeconds(1)}; + $p=[pscustomobject]@{Handle=1;Path=$script:root+'\\v1\\bun-runtime.exe';StartTime=$start}; + $p|Add-Member ScriptMethod Kill {[Console]::Out.WriteLine('KILLED_OWNED_FIXTURE')}; + $p|Add-Member ScriptMethod WaitForExit {param($milliseconds);return $true}; + $p|Add-Member ScriptMethod Dispose {}; + $p +};` + +describe.runIf(process.platform === 'win32')( + 'Bun uninstall ownership under Windows PowerShell', + () => { + it.each([ + ['owned', 0, true], + ['owned-submicrosecond', 0, true], + ['unrelated', 0, false], + ['foreign-owner', 1, false], + ['reused-pid', 1, false], + ['unknown-image', 1, false], + ['query-failure', 1, false] + ])('%s never kills an unproven owner', (scenario, code, kills) => { + const env = Object.fromEntries( + Object.entries(process.env).filter(([key]) => key.toLowerCase() !== 'psmodulepath') + ) + const result = runProcessSync({ + program: join( + process.env.SystemRoot, + 'System32', + 'WindowsPowerShell', + 'v1.0', + 'powershell.exe' + ), + args: ['-NoProfile', '-NonInteractive', '-Command', syntheticProcesses + command], + env: { + ...env, + LOCALAPPDATA: "C:\\Users\\O'Connor Test\\AppData\\Local", + ORCA_UNINSTALL_CASE: scenario, + ORCA_BACKGROUND_LAUNCH: '1', + PSExecutionPolicyPreference: 'Restricted' + }, + timeoutMs: 15_000 + }) + expect(result.code, JSON.stringify(result)).toBe(code) + expect(result.stdout.includes('KILLED_OWNED_FIXTURE')).toBe(kills) + }) + } +) diff --git a/config/scripts/orca-dev-bin.test.mjs b/config/scripts/orca-dev-bin.test.mjs index 5d7120f1cd53..abecdd3f27de 100644 --- a/config/scripts/orca-dev-bin.test.mjs +++ b/config/scripts/orca-dev-bin.test.mjs @@ -14,16 +14,21 @@ describe('orca-dev package bin', () => { expect(readFileSync(wrapperPath, 'utf8')).toMatch(/^#!\/usr\/bin\/env node\n/) }) + const launcherBuilt = existsSync(path.join(projectDir, 'out', 'cli', 'cli-bun-launcher.js')) + const required = process.env.ORCA_TEST_REQUIRE_CLI_ARTIFACTS === '1' + it.skipIf( - !existsSync( - path.join( - projectDir, - 'out', - 'cli-runtime', - `${process.platform}-${process.arch}`, - process.platform === 'win32' ? 'bun-runtime.exe' : 'bun-runtime' - ) - ) + !required && + (!launcherBuilt || + !existsSync( + path.join( + projectDir, + 'out', + 'cli-runtime', + `${process.platform}-${process.arch}`, + process.platform === 'win32' ? 'bun-runtime.exe' : 'bun-runtime' + ) + )) )('hands the dev CLI to staged Bun without requiring Bash', () => { const root = mkdtempSync(path.join(tmpdir(), 'orca-dev-bin-')) const cliEntry = path.join(root, 'cli-entry.cjs') diff --git a/config/scripts/orcad-artifact-version.test.mjs b/config/scripts/orcad-artifact-version.test.mjs index 01c1ae3f8ff8..2574740b593e 100644 --- a/config/scripts/orcad-artifact-version.test.mjs +++ b/config/scripts/orcad-artifact-version.test.mjs @@ -4,6 +4,7 @@ import { dirname, join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' import { ORCAD_BUILD_TARGET_FILENAME, + ORCAD_WINDOWS_CONPTY_ARTIFACTS, ORCAD_RIPGREP_ARTIFACTS, orcadArtifactFilenames } from '../../src/shared/orcad-artifacts.ts' @@ -32,6 +33,24 @@ function createArtifactDirectory(target = '') { } describe('standalone runtime version', () => { + it.each(ORCAD_WINDOWS_CONPTY_ARTIFACTS)( + 'hashes and requires Windows provider artifact %s', + async (filename) => { + const target = 'win32-x64' + const directory = createArtifactDirectory(target) + const options = { target } + const before = computeOrcadFullVersion(directory, options) + writeFileSync(join(directory, filename), 'changed provider artifact') + expect(computeOrcadFullVersion(directory, options)).not.toBe(before) + expect(computeOrcadFullVersion(directory, options)).toBe( + await readOrcadArtifactIdentity(directory) + ) + rmSync(join(directory, filename)) + expect(() => computeOrcadFullVersion(directory, options)).toThrow(filename) + await expect(readOrcadArtifactIdentity(directory)).rejects.toThrow() + } + ) + it('changes when a shipped search binary changes and rejects a missing binary', () => { const dir = createArtifactDirectory() const before = computeOrcadFullVersion(dir) diff --git a/config/scripts/orcad-companion-artifacts.test.mjs b/config/scripts/orcad-companion-artifacts.test.mjs index aec230388868..d00348d80c31 100644 --- a/config/scripts/orcad-companion-artifacts.test.mjs +++ b/config/scripts/orcad-companion-artifacts.test.mjs @@ -19,7 +19,10 @@ const entries = [ 'port-scan-command-worker-entry.js' ] -describe.skipIf(!existsSync(runtime))('packaged orcad companion processes', () => { +const built = existsSync(join(directory, '.version')) +const required = process.env.ORCA_TEST_REQUIRE_ORCAD_ARTIFACTS === '1' + +describe.skipIf(!built && !required)('packaged orcad companion processes', () => { it('reads a transcript and executes a probe without checkout dependencies', async () => { const root = await mkdtemp(join(tmpdir(), 'orca companion artifacts ')) try { diff --git a/config/scripts/orcad-entry-build.mjs b/config/scripts/orcad-entry-build.mjs index c0af0894d131..0c175a1742bc 100644 --- a/config/scripts/orcad-entry-build.mjs +++ b/config/scripts/orcad-entry-build.mjs @@ -1,5 +1,5 @@ import { build } from 'esbuild' -import { join } from 'node:path' +import { join, resolve } from 'node:path' const root = join(import.meta.dirname, '..', '..') @@ -22,7 +22,6 @@ export const ORCAD_CHILD_ENTRY_POINTS = { export const ORCAD_EXTERNAL_MODULES = [ 'electron', - 'node-pty', '@parcel/watcher', 'fsevents', 'bun:ffi', @@ -79,3 +78,21 @@ export function buildOrcadLauncher(outfile) { logLevel: 'error' }) } + +export function buildOrcadChildEntry(entryPoint, outfile) { + return build({ + entryPoints: [resolve(root, entryPoint)], + bundle: true, + platform: 'node', + target: ORCAD_BUN_TARGET, + format: 'cjs', + outfile, + external: ORCAD_EXTERNAL_MODULES, + plugins: [externalNativeAddons], + metafile: true, + minify: true, + sourcemap: false, + define: { 'process.env.NODE_ENV': '"production"' }, + logLevel: 'error' + }) +} diff --git a/config/scripts/orcad-launcher-build.test.mjs b/config/scripts/orcad-launcher-build.test.mjs index b1f89a701ebd..94d9bda7bcbe 100644 --- a/config/scripts/orcad-launcher-build.test.mjs +++ b/config/scripts/orcad-launcher-build.test.mjs @@ -1,5 +1,5 @@ import { existsSync, realpathSync, statSync } from 'node:fs' -import { copyFile, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises' +import { copyFile, cp, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join, resolve } from 'node:path' import { afterAll, beforeAll, describe, expect, it } from 'vitest' @@ -47,6 +47,9 @@ describe.skipIf(!existsSync(bundledRuntime))('bundled launcher application bound beforeAll(async () => { await copyFile(bundledRuntime, join(directory, orcadBunRuntimeFilename(process.platform))) await writeFile(join(directory, '.build-target'), `${process.platform}-${process.arch}\n`) + if (process.platform === 'win32') { + await cp(resolve('out/orcad/conpty'), join(directory, 'conpty'), { recursive: true }) + } await writeFile( join(directory, 'orcad-app.js'), ` diff --git a/config/scripts/packaged-node-pty-prebuild-prune.test.mjs b/config/scripts/packaged-node-pty-prebuild-prune.test.mjs deleted file mode 100644 index 196e6aceb237..000000000000 --- a/config/scripts/packaged-node-pty-prebuild-prune.test.mjs +++ /dev/null @@ -1,138 +0,0 @@ -import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' -import { tmpdir } from 'node:os' -import { createRequire } from 'node:module' -import { join } from 'node:path' -import { afterEach, beforeEach, describe, expect, it } from 'vitest' - -const require = createRequire(import.meta.url) -const { prunePackagedNodePty } = require('../packaged-runtime-node-modules.cjs') -const { PE_MACHINE } = require('./windows-pe-machine.cjs') - -/** - * node-pty's loader tries build/Release, then build/Debug, then - * prebuilds/-, swallowing failures in between. Only the source - * build carries Orca's job-object exports, so leaving the prebuilt conpty.node - * beside it means an ABI mismatch or an AV quarantine silently downgrades the - * app to a binary that cannot own a PTY tree. - * - * The siblings must survive: Orca's patch deletes the `conpty_console_list` and - * winpty `pty` gyp targets, so those binaries exist nowhere but here. - */ -describe('prunePackagedNodePty: the Windows conpty fallback', () => { - let resources - const HOST_ARCH = process.arch - - const nodePty = () => join(resources, 'node_modules', 'node-pty') - const write = (relative, contents = 'x') => { - const target = join(nodePty(), relative) - mkdirSync(join(target, '..'), { recursive: true }) - writeFileSync(target, contents) - } - - /** Minimal PE far enough to carry a `Machine`, which is what the prune now reads. */ - const peAddon = (arch) => { - const buffer = Buffer.alloc(0x50) - buffer.write('MZ') - buffer.writeUInt32LE(0x40, 0x3c) - buffer.write('PE\0\0', 0x40) - buffer.writeUInt16LE(PE_MACHINE[arch], 0x44) - return buffer - } - const prebuilt = (name, arch = HOST_ARCH) => join(nodePty(), 'prebuilds', `win32-${arch}`, name) - - /** What a real packaged win32 prebuilds/ directory holds. */ - const SIBLINGS = ['conpty_console_list.node', 'pty.node', 'winpty.dll', 'winpty-agent.exe'] - - /** `buildArch` is what build/Release actually holds, which is not always the slice's arch. */ - const seedWindowsTree = (arch = HOST_ARCH, buildArch = arch) => { - write(join('build', 'Release', 'conpty.node'), peAddon(buildArch)) - write(join('third_party', 'conpty', 'v1', `win10-${arch}`, 'conpty.dll')) - write(join('third_party', 'conpty', 'v1', `win10-${arch}`, 'OpenConsole.exe')) - write(join('prebuilds', `win32-${arch}`, 'conpty.node')) - for (const sibling of SIBLINGS) { - write(join('prebuilds', `win32-${arch}`, sibling)) - } - } - - beforeEach(() => { - resources = mkdtempSync(join(tmpdir(), 'orca-prune-')) - }) - afterEach(() => { - rmSync(resources, { recursive: true, force: true }) - }) - - it('removes the stale conpty fallback when the source build is present', () => { - seedWindowsTree() - - prunePackagedNodePty(resources, 'win32', HOST_ARCH) - - expect(existsSync(join(nodePty(), 'build', 'Release', 'conpty.node'))).toBe(true) - expect(existsSync(prebuilt('conpty.node'))).toBe(false) - }) - - it.each(SIBLINGS)('keeps %s, which exists nowhere else in a packaged build', (sibling) => { - // Orca's patch removes the conpty_console_list and winpty gyp targets, so a - // Windows source build never produces these. Deleting them kills console - // membership silently and breaks PTY spawn below Windows build 18309. - seedWindowsTree() - - prunePackagedNodePty(resources, 'win32', HOST_ARCH) - - expect(existsSync(prebuilt(sibling))).toBe(true) - }) - - it('keeps the fallback when there is no source build to prefer', () => { - write(join('prebuilds', `win32-${HOST_ARCH}`, 'conpty.node')) - write(join('third_party', 'conpty', 'v1', `win10-${HOST_ARCH}`, 'conpty.dll')) - write(join('third_party', 'conpty', 'v1', `win10-${HOST_ARCH}`, 'OpenConsole.exe')) - - prunePackagedNodePty(resources, 'win32', HOST_ARCH) - - expect(existsSync(prebuilt('conpty.node'))).toBe(true) - }) - - it('keeps the fallback when build/Release holds a different arch than the slice', () => { - // A cross-HOST package can copy a build/Release that the target cannot load; - // deleting the prebuild would remove the only binary the shipped app has. - const target = HOST_ARCH === 'arm64' ? 'x64' : 'arm64' - seedWindowsTree(target, HOST_ARCH) - - prunePackagedNodePty(resources, 'win32', target) - - expect(existsSync(prebuilt('conpty.node', target))).toBe(true) - }) - - it('removes the fallback on a cross-arch package whose build/Release IS the slice arch', () => { - // Measured on Windows: `electron-builder --win --arm64` on an x64 host does - // cross-rebuild a correct arm64 addon. Keying off the host arch skipped this - // case, so the slice shipped the unpatched prebuild as a reachable fallback. - const target = HOST_ARCH === 'arm64' ? 'x64' : 'arm64' - seedWindowsTree(target, target) - - prunePackagedNodePty(resources, 'win32', target) - - expect(existsSync(prebuilt('conpty.node', target))).toBe(false) - expect(existsSync(prebuilt('pty.node', target))).toBe(true) - }) - - it('keeps the fallback when build/Release is not a readable PE at all', () => { - seedWindowsTree() - write(join('build', 'Release', 'conpty.node'), 'not-a-pe') - - prunePackagedNodePty(resources, 'win32', HOST_ARCH) - - expect(existsSync(prebuilt('conpty.node'))).toBe(true) - }) - - it.each([ - ['darwin', 'arm64'], - ['linux', 'x64'] - ])('leaves %s prebuilds alone', (platform, arch) => { - write(join('build', 'Release', 'conpty.node'), peAddon('x64')) - write(join('prebuilds', `${platform}-${arch}`, 'pty.node')) - - prunePackagedNodePty(resources, platform, arch) - - expect(existsSync(join(nodePty(), 'prebuilds', `${platform}-${arch}`, 'pty.node'))).toBe(true) - }) -}) diff --git a/config/scripts/pr-code-change-scope.mjs b/config/scripts/pr-code-change-scope.mjs index 8ed879dcf2e8..dd6503559253 100644 --- a/config/scripts/pr-code-change-scope.mjs +++ b/config/scripts/pr-code-change-scope.mjs @@ -78,11 +78,18 @@ const XTERM_PREFIXES = [ ] const SHELL_PREFIXES = [ + 'src/main/shell-prompt-readiness-probe', + 'src/main/line-editor-ready-output-scanner', + 'src/main/daemon/session-shell-ready-barrier', + 'src/main/daemon/bun-pty-fd-lifecycle', + 'src/shared/shell-process-readiness', + 'src/shared/shell-terminal-path', + 'src/shared/pty-slave-line-discipline-echo', 'src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec', 'src/main/daemon/shell-ready', 'src/main/daemon/daemon-bash-shell-ready', 'src/main/daemon/daemon-shell-ready-wrapper', - 'src/main/daemon/node-pty-fd-leak', + 'src/main/daemon/pty-subprocess/bun-pty', 'src/main/providers/local-pty-shell-ready', 'src/main/providers/__tests__/shell-ready-framework-example', 'src/main/pty/', @@ -96,9 +103,8 @@ const SHELL_PREFIXES = [ 'src/shared/pty-reply-echo-shapes', 'src/shared/startup-shell-portability', 'src/shared/posix-command-path-lookup', - 'config/patches/node-pty@', 'config/scripts/ensure-native-runtime', - 'config/scripts/node-pty-job-ownership' + 'src/main/daemon/pty-subprocess/windows-bun-pty' ] const ORCAD_BROWSER_PREFIXES = [ @@ -173,14 +179,13 @@ const MANAGED_HOOK_PREFIXES = [ const NATIVE_RUNTIME_PREFIXES = [ 'config/scripts/ensure-native-runtime', 'config/scripts/rebuild-native-deps', - 'config/scripts/node-pty-job-ownership', + 'src/main/daemon/pty-subprocess/windows-bun-pty', 'config/scripts/windows-pe-machine', 'config/scripts/windows-pe-image-fixture', 'config/scripts/script-module-dependencies', 'config/scripts/windows-process-tree-creation-time', 'config/scripts/windows-process-tree-gyp-rebuild', 'config/scripts/electron-builder-native-rebuild', - 'config/patches/node-pty@', 'config/patches/@vscode__windows-process-tree' ] @@ -192,15 +197,14 @@ const NATIVE_CACHE_FILES = new Set([ 'config/scripts/rebuild-native-deps.mjs' ]) -const NATIVE_CACHE_PREFIXES = [ - 'config/patches/node-pty@', - 'config/patches/@vscode__windows-process-tree' -] +const NATIVE_CACHE_PREFIXES = ['config/patches/@vscode__windows-process-tree'] const SHARED_PACKAGE_PREFIXES = [ 'electron.vite.config.ts', 'config/electron-builder', 'config/packaged-runtime', + 'config/bundled-cli-runtime', + 'config/scripts/bundled-cli-runtime', 'config/build-plugins/', 'config/scripts/build-', 'config/scripts/smoke-packaged', @@ -226,6 +230,7 @@ const LINUX_PACKAGE_PREFIXES = [ 'config/docker/headless-serve-shutdown/', 'config/docker/daemon-shutdown-descendants/', 'config/scripts/run-linux-cli-launch-contract', + 'config/scripts/run-linux-packaged-terminal-floor-smoke', 'config/scripts/run-headless-linux-pairing-docker', 'config/scripts/run-daemon-shutdown-descendants-docker', 'config/scripts/static-appimage-package-contract', @@ -240,6 +245,7 @@ const WINDOWS_PACKAGE_PREFIXES = [ 'native/computer-use-windows/', 'resources/win32/', 'config/scripts/build-windows-cli-launcher', + 'config/scripts/finalize-signed-cli-runtime', 'config/scripts/windows-pty-native-capability', 'tests/tools/windows-pty-native-capability-smoke/' ] @@ -256,15 +262,13 @@ const WINDOWS_PACKAGE_TESTS = [ ...LINUX_PACKAGE_TESTS, 'config/scripts/rebuild-native-deps.test.mjs', 'config/scripts/rebuild-native-deps-windows-process-tree.test.mjs', - 'config/scripts/rebuild-native-deps-node-pty.test.mjs', 'config/scripts/nsis-process-check.test.mjs', - 'config/scripts/ensure-native-runtime-job-ownership.test.mjs', - 'config/scripts/verify-packaged-node-pty-job-ownership.test.mjs', + 'config/scripts/nsis-bun-terminal-uninstall.test.mjs', 'config/scripts/windows-pe-machine.test.mjs', 'config/scripts/script-module-dependencies.test.mjs', 'src/main/windows-registry-addon.test.ts', - 'src/main/providers/windows-conpty-wide-char-duplication.node-pty.test.ts', - 'src/main/providers/pty-repaint-wide-char-buffer.node-pty.test.ts', + 'src/main/providers/windows-conpty-wide-char-duplication.bun.test.ts', + 'src/main/providers/pty-repaint-wide-char-buffer.bun.test.ts', 'src/shared/child-process/windows-command-line.win32.test.ts', 'src/shared/child-process/windows-cmd-shim-resolution.test.ts', 'src/shared/child-process/windows-cmd-shim-resolution.win32.test.ts', @@ -273,6 +277,7 @@ const WINDOWS_PACKAGE_TESTS = [ 'src/main/codex/windows-hook-command.test.ts', 'src/main/codex/windows-hook-upgrade.test.ts', 'src/main/windows/windows-pty-job.win32.test.ts', + 'src/main/daemon/bun-pty-windows-io-failure.win32.test.ts', 'src/main/windows/windows-msys-job.win32.test.ts', 'src/main/providers/agent-foreground-process-git-bash.win32.test.ts', 'src/main/windows/windows-host-job.win32.test.ts', diff --git a/config/scripts/pr-code-change-scope.test.mjs b/config/scripts/pr-code-change-scope.test.mjs index 01ff8ac2bed2..6e7f9e821743 100644 --- a/config/scripts/pr-code-change-scope.test.mjs +++ b/config/scripts/pr-code-change-scope.test.mjs @@ -250,6 +250,20 @@ describe('per-job path classification', () => { }) }) + it('runs real shell contracts when readiness probes or terminal inspection change', () => { + for (const file of [ + 'src/main/shell-prompt-readiness-probe.ts', + 'src/main/line-editor-ready-output-scanner.ts', + 'src/main/daemon/session-shell-ready-barrier.ts', + 'src/main/daemon/bun-pty-fd-lifecycle.integration.test.ts', + 'src/shared/shell-process-readiness.ts', + 'src/shared/shell-terminal-path.test.ts', + 'src/shared/pty-slave-line-discipline-echo.ts' + ]) { + expect(classifyPrJobs([file]).shell_contracts).toBe(true) + } + }) + it('runs shell contracts when wrapper templates or live-shell fixtures change', () => { expectClassification(['src/main/shell-templates.ts'], { shell_contracts: true, @@ -371,7 +385,7 @@ describe('per-job path classification', () => { '.github/actions/install-node-dependencies/action.yml', 'config/scripts/ensure-native-runtime.mjs', 'config/scripts/rebuild-native-deps.mjs', - 'config/patches/node-pty@1.1.0.patch' + 'config/patches/@vscode__windows-process-tree@0.8.0.patch' ]) { expect(classifyPrJobs([file]).native_cache_changed, file).toBe(true) } @@ -623,3 +637,16 @@ describe('PR Checks skip wiring', () => { } }) }) + +describe('bundled terminal runtime packaging gates', () => { + it.each([ + ['config/bundled-cli-runtime.cjs', true, true], + ['config/scripts/bundled-cli-runtime.test.mjs', false, false], + ['config/scripts/run-linux-packaged-terminal-floor-smoke.mjs', true, false], + ['config/scripts/run-linux-packaged-terminal-floor-smoke.test.mjs', false, false], + ['config/scripts/finalize-signed-cli-runtime.mjs', false, true], + ['config/scripts/finalize-signed-cli-runtime.test.mjs', false, false] + ])('selects package checks for %s', (file, linux, windows) => { + expect(classifyPrJobs([file])).toMatchObject({ package: linux, package_windows: windows }) + }) +}) diff --git a/config/scripts/pr-workflow-parallelism.test.mjs b/config/scripts/pr-workflow-parallelism.test.mjs index a847b85d26e6..04b33f093c6f 100644 --- a/config/scripts/pr-workflow-parallelism.test.mjs +++ b/config/scripts/pr-workflow-parallelism.test.mjs @@ -21,18 +21,19 @@ const shellContractFiles = [ 'src/main/providers/local-pty-shell-ready-zsh-launch-environment.test.ts', 'src/main/providers/__tests__/shell-ready-framework-example.test.ts', 'src/main/pty/omp-shell-wrapper-alias-safety.test.ts', - 'src/main/pty/omp-shell-wrapper.node-pty.test.ts', + 'src/main/pty/omp-shell-wrapper.bun.test.ts', 'src/main/shell-startup-feature-channel.test.ts', 'src/main/zsh-scoped-histfile.live-shell.test.ts', 'src/main/zsh-startup-hook-user-config-equivalence.live-shell.test.ts', 'src/main/zsh-wrapper-version-mismatch.live-shell.test.ts', 'src/shared/posix-command-path-lookup.test.ts' ] -const patchedNodePtyContractFiles = [ - 'src/main/daemon/node-pty-fd-leak.test.ts', - 'src/shared/fish-query-reply-child-stdin.node-pty.test.ts' +const additionalNativeShellContractFiles = [ + 'src/main/daemon/pty-subprocess/bun-tests/pty-reply-echo-shapes.bun.test.ts', + 'src/main/daemon/bun-pty-fd-lifecycle.integration.test.ts', + 'src/main/daemon/pty-subprocess/bun-tests/fish-query-reply-child-stdin.bun.test.ts' ] -const nativeShellContractFiles = [...shellContractFiles, ...patchedNodePtyContractFiles] +const nativeShellContractFiles = [...shellContractFiles, ...additionalNativeShellContractFiles] const testFilePatterns = [ 'config/**/*.{test,spec}.{js,cjs,mjs,ts,tsx}', 'src/**/*.{test,spec}.{js,cjs,mjs,ts,tsx}', @@ -47,6 +48,16 @@ const realZshUsage = /(?:spawnSync|execFileSync|spawn)\(\s*['"](?:\/(?:usr\/)?bin\/)?zsh['"]|spawnSync\(\s*['"]which['"]\s*,\s*\[\s*['"]zsh['"]|name:\s*['"]zsh['"]\s*,\s*path:\s*executablePath|from '[^']*zsh-startup-hook-pty-harness'/ describe('PR workflow parallelism', () => { + it('keeps every explicitly selected shell test on disk', () => { + const step = workflow.jobs.shell_contracts.steps.find( + (candidate) => candidate.name === 'Test real shell contracts' + ) + expect(step).toBeDefined() + const files = step.run.match(/src\/[\w./-]+\.test\.ts/g) ?? [] + expect(files.length).toBeGreaterThan(0) + expect(files.filter((file) => !existsSync(file))).toEqual([]) + }) + it('keeps lightweight orchestration jobs on the free slim runner', () => { expect(workflow.jobs.code_paths['runs-on']).toBe('ubuntu-slim') expect(workflow.jobs.typecheck['runs-on']).toBe('ubuntu-24.04-arm') @@ -227,7 +238,9 @@ describe('PR workflow parallelism', () => { (step) => step.name === 'Build package inputs' ) - expect(buildStep.run).toContain('scripts=(build:relay build:electron-vite:parallel)') + expect(buildStep.run).toContain( + 'scripts=(build:relay build:terminal-daemon build:electron-vite:parallel)' + ) expect(buildStep.run).toContain('pnpm run "$script" &') expect( workflow.jobs.package.steps.find( @@ -404,14 +417,14 @@ describe('PR workflow parallelism', () => { 'steps.requested-node.outputs.node-version || steps.default-node.outputs.node-version' ) expect(cacheStep.with.key).toContain('steps.native-cache-scope.outputs.scope') - expect(cacheStep.with.key).toContain('config/patches/node-pty@1.1.0.patch') + expect(cacheStep.with.key).not.toContain('node-pty') expect(cacheStep.with.key).toContain( 'config/patches/@vscode__windows-process-tree@0.8.0.patch' ) expect(cacheStep.with.key).toContain('.github/actions/install-node-dependencies/action.yml') expect(cacheStep.with.key).toContain('config/scripts/ensure-native-runtime.mjs') expect(cacheStep.with.key).toContain('config/scripts/rebuild-native-deps.mjs') - expect(cacheStep.with.path).toContain('node-pty@*/node_modules/node-pty/build') + expect(cacheStep.with.path).not.toContain('node-pty') expect(cacheStep.with.path).toContain('native/windows-registry/build') expect(cacheStep.with.path).toContain('@vscode+windows-process-tre*') expect(cacheStep.with['restore-keys']).toBeUndefined() diff --git a/config/scripts/profile-state-worker-smoke.mjs b/config/scripts/profile-state-worker-smoke.mjs index 81ff1b5eaf13..282a9fbeed30 100644 --- a/config/scripts/profile-state-worker-smoke.mjs +++ b/config/scripts/profile-state-worker-smoke.mjs @@ -82,7 +82,7 @@ function runWorker(entry, workerData, steps, timeoutMs) { }) } -/** Exercise the shipped entries and copied state before publishing their content version. */ +/** Exercise the shipped entries and copied state with their installed content version. */ export async function smokeProfileStateWorkers(outDir, { timeoutMs = 30_000, runtimePath } = {}) { if (runtimePath) { const nonce = randomUUID() diff --git a/config/scripts/pty-transcript-bun-runtime.mjs b/config/scripts/pty-transcript-bun-runtime.mjs new file mode 100644 index 000000000000..487fdc475572 --- /dev/null +++ b/config/scripts/pty-transcript-bun-runtime.mjs @@ -0,0 +1,90 @@ +import { bunOwnedRuntimeArgs } from '../../src/shared/bun-owned-runtime-args.ts' +import { existsSync } from 'node:fs' +import { join } from 'node:path' + +/** Observe native bytes before the product adapter's streaming UTF-8 decoder. */ +export function tapBunTerminalBytes(runtime, onBytes) { + const tap = (options) => ({ + ...options, + data(terminal, bytes) { + onBytes(Buffer.from(bytes)) + options.data(terminal, bytes) + } + }) + return { + Terminal: class extends runtime.Terminal { + constructor(options) { + super(tap(options)) + } + }, + spawn(command, options) { + return runtime.spawn(command, { + ...options, + terminal: 'data' in options.terminal ? tap(options.terminal) : options.terminal + }) + } + } +} + +export async function spawnTranscriptPty(args, onBytes) { + const { spawnBunPty } = await import('../../src/main/daemon/pty-subprocess/bun-pty-process.ts') + const { resolveBunRuntime } = + await import('../../src/main/daemon/pty-subprocess/bun-pty-process-capabilities.ts') + const { createWindowsBunPtyLaunch } = + await import('../../src/main/daemon/pty-subprocess/windows-bun-pty-launch.ts') + const term = spawnBunPty(args, { + runtime: tapBunTerminalBytes(resolveBunRuntime(), onBytes), + createWindowsLaunch: (options) => + createWindowsBunPtyLaunch(options, { + workerPath: join( + import.meta.dirname, + '../../src/main/daemon/pty-subprocess/windows-bun-pty-gate-entry.ts' + ) + }) + }) + // The recorder consumes native bytes; do not retain the adapter's decoded replay buffer. + const subscription = term.onData(() => {}) + term.onExit(() => subscription.dispose()) + return term +} + +export async function relaunchTranscriptWithBun( + root, + argv, + entry = join(root, 'config/scripts/capture-agent-pty-transcript.mjs') +) { + const { ORCAD_BUN_VERSION } = await import('../../src/shared/orcad-bun-runtime.ts') + if (process.versions.bun) { + if (process.versions.bun !== ORCAD_BUN_VERSION) { + throw new Error(`Capture requires Bun ${ORCAD_BUN_VERSION}`) + } + return null + } + const { orcadBunRuntimeFilename } = await import('../../src/shared/orcad-artifacts.ts') + const filename = orcadBunRuntimeFilename(process.platform) + const runtime = [ + process.env.BUN_EXECUTABLE, + join(root, 'out/orcad', filename), + join(root, 'out/cli-runtime', `${process.platform}-${process.arch}`, filename) + ].find((candidate) => candidate && existsSync(candidate)) + if (!runtime) { + throw new Error( + 'Prepare bundled Bun with node config/scripts/build-orcad-bun.mjs --runtime-only --out-dir out/orcad' + ) + } + const { runProcessSync } = await import('./script-child-process.mjs') + const env = { ...process.env } + for (const key of Object.keys(env)) { + if (/^(NODE_OPTIONS|NODE_PATH|BUN_OPTIONS|BUN_INSPECT.*)$/i.test(key)) { + delete env[key] + } + } + const result = runProcessSync({ + program: runtime, + env, + args: [...bunOwnedRuntimeArgs(), entry, ...argv], + stdio: 'inherit', + timeoutMs: null + }) + return result.code ?? 1 +} diff --git a/config/scripts/pty-transcript-recording.mjs b/config/scripts/pty-transcript-recording.mjs new file mode 100644 index 000000000000..4df85aea74ae --- /dev/null +++ b/config/scripts/pty-transcript-recording.mjs @@ -0,0 +1,98 @@ +const STOP_KEY = 0x1d + +export async function recordPtyTranscript({ spawn, sink, stdin, stdout, options }) { + let recording = true + let term + let stopping = false + const timers = [] + const wasRaw = stdin.isTTY === true && stdin.isRaw === true + const stop = () => { + if (stopping) { + return + } + stopping = true + recording = false + try { + term.kill() + } catch { + // A concurrent physical exit can make the native handle unavailable. + } + } + const onInput = (chunk) => { + if (chunk.includes(STOP_KEY)) { + stop() + } else if (!stopping) { + term.write(chunk) + } + } + let sinkFailure + const onSinkError = (error) => { + sinkFailure = error + if (term) { + stop() + } + } + sink.on('error', onSinkError) + let exitCode + let captureFailure + try { + term = await spawn((bytes) => { + if (recording) { + sink.write(bytes) + } + stdout.write(bytes) + }) + const exit = new Promise((resolve) => term.onExit(({ exitCode }) => resolve(exitCode ?? 0))) + await term.waitForSpawn?.() + if (sinkFailure) { + throw sinkFailure + } + if (stdin.isTTY) { + stdin.setRawMode(true) + } + stdin.on('data', onInput) + stdin.resume() + for (const send of options.sends) { + timers.push( + setTimeout(() => { + if (!stopping) { + term.write(send.text) + } + }, send.atMs) + ) + } + if (options.duration !== null) { + timers.push(setTimeout(stop, options.duration * 1000)) + } + exitCode = await exit + } catch (error) { + captureFailure = error + } finally { + recording = false + for (const timer of timers) { + clearTimeout(timer) + } + stdin.off('data', onInput) + if (stdin.isTTY) { + stdin.setRawMode(wasRaw) + } + stdin.pause() + try { + term?.destroy() + } catch (error) { + captureFailure ??= error + } + await new Promise((resolve) => { + if (sink.destroyed) { + return resolve() + } + sink.once('error', resolve) + sink.end(resolve) + }) + sink.off('error', onSinkError) + } + if (captureFailure || sinkFailure) { + throw captureFailure ?? sinkFailure + } + return exitCode +} diff --git a/config/scripts/pty-transcript-recording.test.mjs b/config/scripts/pty-transcript-recording.test.mjs new file mode 100644 index 000000000000..5fe7be1e96f1 --- /dev/null +++ b/config/scripts/pty-transcript-recording.test.mjs @@ -0,0 +1,182 @@ +import { test } from 'vitest' +import assert from 'node:assert/strict' +import { PassThrough, Writable } from 'node:stream' +import { tapBunTerminalBytes } from './pty-transcript-bun-runtime.mjs' +import { recordPtyTranscript } from './pty-transcript-recording.mjs' + +test('raw tap preserves invalid UTF-8, split UTF-8 and control bytes in both terminal forms', () => { + for (const inline of [true, false]) { + let options + const raw = [] + const decoded = [] + const runtime = { + Terminal: class { + constructor(value) { + options = value + } + }, + spawn(_command, value) { + if (inline) { + options = value.terminal + } + } + } + const tapped = tapBunTerminalBytes(runtime, (bytes) => raw.push(bytes)) + const terminal = { data: (_term, bytes) => decoded.push(bytes) } + tapped.spawn([], { terminal: inline ? terminal : new tapped.Terminal(terminal) }) + for (const bytes of [[0xff, 0xc0], [0xe2], [0x82, 0xac, 0, 27, 13]]) { + options.data({}, Uint8Array.from(bytes)) + } + assert.deepEqual(Buffer.concat(raw), Buffer.from([255, 192, 226, 130, 172, 0, 27, 13])) + assert.equal(decoded.length, 3) + } +}) + +function harness({ fastExit = false, stop = false } = {}) { + const stdin = new PassThrough() + const chunks = [] + const sink = new Writable({ + write(chunk, _encoding, done) { + chunks.push(Buffer.from(chunk)) + done() + } + }) + const output = [] + const writes = [] + let disposed = 0 + return { + chunks, + writes, + get disposed() { + return disposed + }, + stdin, + run: () => + recordPtyTranscript({ + sink, + stdin, + stdout: { write: (bytes) => output.push(bytes) }, + options: { sends: [], duration: null }, + spawn: async (data) => { + data(Buffer.from([255, 27, 13])) + let exit + const term = { + onExit(listener) { + exit = listener + if (fastExit) { + listener({ exitCode: 7 }) + } + return { dispose() {} } + }, + write: (bytes) => writes.push(bytes), + kill() { + data(Buffer.from('SHUTDOWN')) + exit({ exitCode: 0 }) + }, + destroy() { + disposed++ + } + } + if (!fastExit) { + setImmediate(() => { + stdin.write(Buffer.from([0xc3, 0xa9])) + if (stop) { + stdin.write(Buffer.from([0x1d])) + } else { + data(Buffer.from([0xe2, 0x82, 0xac])) + exit({ exitCode: 3 }) + } + }) + } + return term + } + }) + } +} + +test('stop freezes transcript before killing while preserving raw input', async () => { + const fixture = harness({ stop: true }) + assert.equal(await fixture.run(), 0) + assert.deepEqual(Buffer.concat(fixture.chunks), Buffer.from([255, 27, 13])) + assert.deepEqual(fixture.writes, [Buffer.from([0xc3, 0xa9])]) + assert.equal(fixture.stdin.listenerCount('data'), 0) + assert.equal(fixture.disposed, 1) +}) + +test('natural exit retains final drained output', async () => { + const fixture = harness() + assert.equal(await fixture.run(), 3) + assert.deepEqual(Buffer.concat(fixture.chunks), Buffer.from([255, 27, 13, 226, 130, 172])) +}) + +test('fast exit replays completion without hanging', async () => { + const fixture = harness({ fastExit: true }) + assert.equal(await fixture.run(), 7) + assert.equal(fixture.disposed, 1) +}) + +test('spawn receipt failure cleans input and closes output', async () => { + const stdin = new PassThrough() + const sink = new PassThrough() + let destroyed = false + await assert.rejects( + recordPtyTranscript({ + stdin, + sink, + stdout: { write() {} }, + options: { sends: [], duration: null }, + spawn: async () => ({ + onExit() { + return { dispose() {} } + }, + waitForSpawn: async () => { + throw new Error('shell failed') + }, + destroy() { + destroyed = true + } + }) + }), + /shell failed/ + ) + assert.equal(destroyed, true) + assert.equal(sink.writableFinished, true) + assert.equal(stdin.listenerCount('data'), 0) +}) + +test('sink failure refuses success and retires terminal', async () => { + const stdin = new PassThrough() + const sink = new Writable({ + write(_chunk, _encoding, done) { + done(new Error('disk full')) + } + }) + let exit + let destroyed = false + await assert.rejects( + recordPtyTranscript({ + stdin, + sink, + stdout: { write() {} }, + options: { sends: [], duration: null }, + spawn: async (data) => { + setImmediate(() => data(Buffer.from('output'))) + return { + onExit(listener) { + exit = listener + return { dispose() {} } + }, + kill() { + exit({ exitCode: 1 }) + }, + destroy() { + destroyed = true + } + } + } + }), + /disk full/ + ) + assert.equal(destroyed, true) + assert.equal(stdin.listenerCount('data'), 0) +}) diff --git a/config/scripts/real-terminal-runtime-workflows.test.ts b/config/scripts/real-terminal-runtime-workflows.test.ts new file mode 100644 index 000000000000..212d9df9ee01 --- /dev/null +++ b/config/scripts/real-terminal-runtime-workflows.test.ts @@ -0,0 +1,72 @@ +import { readFileSync } from 'node:fs' +import { describe, expect, it } from 'vitest' +import { parse } from 'yaml' + +const prepare = 'node config/scripts/build-orcad-bun.mjs --runtime-only --out-dir out/orcad' + +describe('real terminal test runtime preparation', () => { + it.each([ + ['pr.yml', 'shell_contracts', 'Test real shell contracts'], + ['pr.yml', 'package_windows', 'Test Windows-specific boundaries'], + ['unit-tests.yml', 'test', 'Test shard'] + ])('prepares pinned Bun before %s/%s executes %s', (file, job, testStep) => { + const workflow = parse(readFileSync(`.github/workflows/${file}`, 'utf8')) + const steps = workflow.jobs[job].steps + const testIndex = steps.findIndex((step: { name?: string }) => step.name === testStep) + const preparation = steps.findIndex((step: { run?: string }) => step.run === prepare) + expect(preparation).toBeGreaterThan(-1) + expect(preparation).toBeLessThan(testIndex) + expect(steps[preparation].if).toBeUndefined() + expect(steps[preparation]['continue-on-error']).toBeUndefined() + }) +}) + +it('runs the shell exec regression with Bun before Node-based shell contracts', () => { + const workflow = parse(readFileSync('.github/workflows/pr.yml', 'utf8')) + const step = workflow.jobs.shell_contracts.steps.find( + (entry: { name?: string }) => entry.name === 'Test real shell contracts' + ) + const file = 'src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec.test.ts' + const [bun, ...node] = step.run.trim().split('\n') + expect(bun).toContain(`node config/scripts/run-bun-profile-tests.mjs --maxWorkers=1 ${file}`) + expect(node.join('\n')).not.toContain(file) +}) + +describe('desktop terminal build closure', () => { + it.each([ + ['pr.yml', 'package', 'Build package inputs'], + ['e2e.yml', 'build', 'Build E2E outputs'] + ])('builds the runtime and daemon before %s/%s packaging or upload', (file, job, stepName) => { + const workflow = parse(readFileSync(`.github/workflows/${file}`, 'utf8')) + const step = workflow.jobs[job].steps.find( + (entry: { name?: string }) => entry.name === stepName + ) + expect(step.run).toContain('build:cli') + expect(step.run).toContain('build:terminal-daemon') + }) + it('prepares the runtime and daemon before launching the development app', () => { + const source = readFileSync('config/scripts/run-electron-vite-dev.mjs', 'utf8') + expect(source).toContain('if (!isHelpOrVersion) {\n await buildCliRuntime') + const launch = source.indexOf('const child = spawn(') + for (const call of [ + 'await buildCliRuntime(process.platform, process.arch)', + 'await buildTerminalDaemon()' + ]) { + expect(source.indexOf(call)).toBeGreaterThan(-1) + expect(source.indexOf(call)).toBeLessThan(launch) + } + }) + it('repairs missing E2E runtime and daemon outputs even with SKIP_BUILD', () => { + const source = readFileSync('tests/e2e/global-setup.ts', 'utf8') + expect(source).toContain('!existsSync(runtimeManifest) || !existsSync(runtimeBinary)') + expect(source).toContain('!existsSync(terminalEntry) || !existsSync(terminalGate)') + expect(source).toContain('node config/scripts/build-terminal-daemon.mjs') + expect(source).toContain("['config/scripts/build-cli-runtime.mjs', '--arch', process.arch]") + }) + it('does not silently filter retired terminal suites out of reliability commands', () => { + const gates = readFileSync('config/reliability-gates.jsonc', 'utf8') + expect(gates).not.toMatch( + /(?:local-pty-provider-(?:foreground-process|shell-readiness|spawn-session|shutdown|session-inventory)|local-pty-shell-ready-startup-command|degraded-daemon-pty-provider|omp-shell-wrapper\.node-pty)\.test\.ts/ + ) + }) +}) diff --git a/config/scripts/rebuild-native-deps-addon-validation.test.mjs b/config/scripts/rebuild-native-deps-addon-validation.test.mjs new file mode 100644 index 000000000000..bb0e75bcbb12 --- /dev/null +++ b/config/scripts/rebuild-native-deps-addon-validation.test.mjs @@ -0,0 +1,208 @@ +import { readFileSync } from 'node:fs' +import { join } from 'node:path' +import { describe, expect, it } from 'vitest' +import { removeTreeSync } from '../../src/shared/windows-transient-lock-removal.ts' +import { + gitLineEndingEnv, + initGitWorkTree, + mkTempProject, + runRebuildScript, + writeFakeElectronRebuild, + writeFakeUsableElectronPackage, + writeFakeWindowsProcessTree, + writeFakeWindowsProcessTreeWithNodeAddonApi, + writeWindowsProcessTreePatchFile +} from './rebuild-native-deps-test-fixtures.mjs' + +describe('rebuild-native-deps addon validation', () => { + it('stages windows-process-tree node-addon-api headers before a Windows rebuild', () => { + const projectDir = mkTempProject() + + try { + writeFakeUsableElectronPackage(projectDir, { platform: 'win32' }) + writeFakeElectronRebuild(projectDir) + writeFakeWindowsProcessTreeWithNodeAddonApi(projectDir) + + const result = runRebuildScript( + projectDir, + { npm_config_platform: 'win32', npm_config_arch: 'x64' }, + ['--platform=win32', '--arch=x64', '--force'] + ) + + expect(result.status, result.stderr).toBe(0) + expect( + readFileSync( + join( + projectDir, + 'node_modules', + '@vscode', + 'windows-process-tree', + 'deps', + 'node-addon-api', + 'napi.h' + ), + 'utf8' + ) + ).toBe('// napi.h\n') + } finally { + removeTreeSync(projectDir) + } + }) + + const commandLineSourcePath = (projectDir) => + join( + projectDir, + 'node_modules', + '@vscode', + 'windows-process-tree', + 'src', + 'process_commandline.cc' + ) + + // Why inside a git work tree: `git apply` run under one prefixes patch paths + // with the cwd-relative prefix, silently skips what does not match, and still + // exits 0. The package dir is always under the project root in production, so + // a fixture in %TEMP% alone would pass while the real repair did nothing. + // + // Why both line-ending modes: the patch is stored LF while upstream ships this + // source CRLF, so whether the pre-image matches depends on `core.autocrlf` -- + // and under `false`, Git's own built-in default, it did not. The repair blinds + // git to the repo, so that value comes from global config, i.e. from whichever + // option the developer's installer wrote. Pinning both makes the case cover the + // host that breaks rather than the host that happens to run it. + for (const autocrlf of ['false', 'true']) { + it(`repairs an un-applied command-line patch in a work tree (autocrlf=${autocrlf})`, () => { + const projectDir = mkTempProject() + + try { + initGitWorkTree(projectDir) + writeFakeUsableElectronPackage(projectDir, { platform: 'win32' }) + writeFakeElectronRebuild(projectDir) + writeFakeWindowsProcessTreeWithNodeAddonApi(projectDir, { + commandLinePatchApplied: false + }) + writeWindowsProcessTreePatchFile(projectDir) + + const result = runRebuildScript( + projectDir, + { + npm_config_platform: 'win32', + npm_config_arch: 'x64', + ...gitLineEndingEnv(autocrlf) + }, + ['--platform=win32', '--arch=x64', '--force'] + ) + + expect(result.status, result.stderr).toBe(0) + expect(readFileSync(commandLineSourcePath(projectDir), 'utf8')).toContain( + 'kProcessCommandLineInformation' + ) + } finally { + removeTreeSync(projectDir) + } + }) + } + + // Why fail rather than build: an unpatched command-line reader compiles fine + // and then opens every process with PROCESS_VM_READ to walk its PEB, which is + // the primitive the patch exists to remove. + it('refuses a Windows rebuild when the command-line patch cannot be applied', () => { + const projectDir = mkTempProject() + + try { + initGitWorkTree(projectDir) + writeFakeUsableElectronPackage(projectDir, { platform: 'win32' }) + writeFakeElectronRebuild(projectDir) + writeFakeWindowsProcessTreeWithNodeAddonApi(projectDir, { commandLinePatchApplied: false }) + // No patch file, so the repair has nothing to apply. + + const result = runRebuildScript( + projectDir, + { npm_config_platform: 'win32', npm_config_arch: 'x64' }, + ['--platform=win32', '--arch=x64', '--force'] + ) + + expect(result.status).not.toBe(0) + expect(result.stderr).toContain('process_commandline.cc') + expect(readFileSync(commandLineSourcePath(projectDir), 'utf8')).not.toContain( + 'kProcessCommandLineInformation' + ) + } finally { + removeTreeSync(projectDir) + } + }) + + it('refuses a Windows rebuild when the process creation-time patch is missing', () => { + const projectDir = mkTempProject() + + try { + writeFakeUsableElectronPackage(projectDir, { platform: 'win32' }) + writeFakeElectronRebuild(projectDir) + writeFakeWindowsProcessTreeWithNodeAddonApi(projectDir, { creationTimePatchApplied: false }) + + const result = runRebuildScript( + projectDir, + { npm_config_platform: 'win32', npm_config_arch: 'x64' }, + ['--platform=win32', '--arch=x64', '--force'] + ) + + expect(result.status).not.toBe(0) + expect(result.stderr).toContain('process creation-time patch') + } finally { + removeTreeSync(projectDir) + } + }) + + it.skipIf(process.platform !== 'win32')( + 'rebuilds only the failing registry while the process-tree addon stays healthy', + () => { + const projectDir = mkTempProject() + + try { + const rebuildLogPath = join(projectDir, 'electron-rebuild.log') + writeFakeUsableElectronPackage(projectDir, { platform: 'win32' }) + writeFakeElectronRebuild(projectDir, { logPathEnv: 'ORCA_REBUILD_TEST_LOG' }) + writeFakeWindowsProcessTree(projectDir) + + const result = runRebuildScript(projectDir, { + ORCA_REBUILD_TEST_LOG: rebuildLogPath, + npm_config_platform: 'win32', + npm_config_arch: process.arch + }) + + expect(result.status, result.stderr).toBe(0) + expect(result.stdout).toContain('Rebuilding failed native modules: @orca/windows-registry') + const rebuildCall = JSON.parse(readFileSync(rebuildLogPath, 'utf8').trim()) + expect(rebuildCall.onlyModules).toEqual(['@orca/windows-registry']) + } finally { + removeTreeSync(projectDir) + } + } + ) + + for (const [addon, expected] of [ + ['unpatched', 'still imports ReadProcessMemory'], + ['none', 'is not there'] + ]) { + it(`fails a Windows rebuild that leaves ${addon} windows-process-tree bytes`, () => { + const projectDir = mkTempProject() + + try { + writeFakeUsableElectronPackage(projectDir, { platform: 'win32' }) + writeFakeElectronRebuild(projectDir, { addon }) + writeFakeWindowsProcessTreeWithNodeAddonApi(projectDir) + + const result = runRebuildScript( + projectDir, + { npm_config_platform: 'win32', npm_config_arch: 'x64' }, + ['--platform=win32', '--arch=x64', '--force'] + ) + + expect(result.status).not.toBe(0) + expect(result.stderr).toContain(expected) + } finally { + removeTreeSync(projectDir) + } + }) + } +}) diff --git a/config/scripts/rebuild-native-deps-node-pty.test.mjs b/config/scripts/rebuild-native-deps-node-pty.test.mjs deleted file mode 100644 index a9ec246dd95b..000000000000 --- a/config/scripts/rebuild-native-deps-node-pty.test.mjs +++ /dev/null @@ -1,606 +0,0 @@ -import { existsSync, readFileSync } from 'node:fs' -import { removeTreeSync } from '../../src/shared/windows-transient-lock-removal.ts' -import { join } from 'node:path' -import { describe, expect, it } from 'vitest' - -import { - gitLineEndingEnv, - initGitWorkTree, - mkTempProject, - runRebuildScript, - writeFakeElectronRebuild, - writeFakeLoadableNodePty, - writeFakeNodePtyConptyPayload, - writeFakeNodePtyConptySource, - writeFakeUsableElectronPackage, - writeFakeWindowsProcessTree, - writeFakeWindowsProcessTreeWithNodeAddonApi, - writeFakeWindowsRegistry, - writeNodePtyPatchFile, - writePatchedNodePtyBuildArtifacts, - writeWindowsProcessTreePatchFile -} from './rebuild-native-deps-test-fixtures.mjs' - -describe('rebuild-native-deps patched node-pty rebuild', () => { - it.skipIf(process.platform !== 'win32')( - 'repairs a missing ConPTY runtime before probing without recompiling node-pty', - () => { - const projectDir = mkTempProject() - - try { - const rebuildLogPath = join(projectDir, 'electron-rebuild.log') - writeFakeUsableElectronPackage(projectDir, { platform: 'win32' }) - writeFakeElectronRebuild(projectDir, { logPathEnv: 'ORCA_REBUILD_TEST_LOG' }) - writeFakeLoadableNodePty(projectDir, { nativeDir: '../build/Release/' }) - writeFakeWindowsRegistry(projectDir) - writeFakeWindowsProcessTree(projectDir) - writeFakeNodePtyConptyPayload(projectDir, process.arch) - - const result = runRebuildScript(projectDir, { - ORCA_REBUILD_TEST_LOG: rebuildLogPath, - npm_config_platform: 'win32', - npm_config_arch: process.arch - }) - - expect(result.status, result.stderr).toBe(0) - expect(result.stdout).toContain('Restored node-pty ConPTY runtime files') - expect(result.stdout).toContain( - 'Native modules already load in Electron; skipping rebuild.' - ) - expect(existsSync(rebuildLogPath)).toBe(false) - } finally { - removeTreeSync(projectDir) - } - } - ) - - it('stages windows-process-tree node-addon-api headers before a Windows rebuild', () => { - const projectDir = mkTempProject() - - try { - writeFakeUsableElectronPackage(projectDir, { platform: 'win32' }) - writeFakeElectronRebuild(projectDir) - writeFakeNodePtyConptyPayload(projectDir, 'x64') - writeFakeWindowsProcessTreeWithNodeAddonApi(projectDir) - - const result = runRebuildScript( - projectDir, - { npm_config_platform: 'win32', npm_config_arch: 'x64' }, - ['--platform=win32', '--arch=x64', '--force'] - ) - - expect(result.status, result.stderr).toBe(0) - expect( - readFileSync( - join( - projectDir, - 'node_modules', - '@vscode', - 'windows-process-tree', - 'deps', - 'node-addon-api', - 'napi.h' - ), - 'utf8' - ) - ).toBe('// napi.h\n') - } finally { - removeTreeSync(projectDir) - } - }) - - const commandLineSourcePath = (projectDir) => - join( - projectDir, - 'node_modules', - '@vscode', - 'windows-process-tree', - 'src', - 'process_commandline.cc' - ) - - // Why inside a git work tree: `git apply` run under one prefixes patch paths - // with the cwd-relative prefix, silently skips what does not match, and still - // exits 0. The package dir is always under the project root in production, so - // a fixture in %TEMP% alone would pass while the real repair did nothing. - // - // Why both line-ending modes: the patch is stored LF while upstream ships this - // source CRLF, so whether the pre-image matches depends on `core.autocrlf` -- - // and under `false`, Git's own built-in default, it did not. The repair blinds - // git to the repo, so that value comes from global config, i.e. from whichever - // option the developer's installer wrote. Pinning both makes the case cover the - // host that breaks rather than the host that happens to run it. - for (const autocrlf of ['false', 'true']) { - it(`repairs an un-applied command-line patch in a work tree (autocrlf=${autocrlf})`, () => { - const projectDir = mkTempProject() - - try { - initGitWorkTree(projectDir) - writeFakeUsableElectronPackage(projectDir, { platform: 'win32' }) - writeFakeElectronRebuild(projectDir) - writeFakeNodePtyConptyPayload(projectDir, 'x64') - writeFakeWindowsProcessTreeWithNodeAddonApi(projectDir, { - commandLinePatchApplied: false - }) - writeWindowsProcessTreePatchFile(projectDir) - - const result = runRebuildScript( - projectDir, - { - npm_config_platform: 'win32', - npm_config_arch: 'x64', - ...gitLineEndingEnv(autocrlf) - }, - ['--platform=win32', '--arch=x64', '--force'] - ) - - expect(result.status, result.stderr).toBe(0) - expect(readFileSync(commandLineSourcePath(projectDir), 'utf8')).toContain( - 'kProcessCommandLineInformation' - ) - } finally { - removeTreeSync(projectDir) - } - }) - } - - // Why fail rather than build: an unpatched command-line reader compiles fine - // and then opens every process with PROCESS_VM_READ to walk its PEB, which is - // the primitive the patch exists to remove. - it('refuses a Windows rebuild when the command-line patch cannot be applied', () => { - const projectDir = mkTempProject() - - try { - initGitWorkTree(projectDir) - writeFakeUsableElectronPackage(projectDir, { platform: 'win32' }) - writeFakeElectronRebuild(projectDir) - writeFakeNodePtyConptyPayload(projectDir, 'x64') - writeFakeWindowsProcessTreeWithNodeAddonApi(projectDir, { commandLinePatchApplied: false }) - // No patch file, so the repair has nothing to apply. - - const result = runRebuildScript( - projectDir, - { npm_config_platform: 'win32', npm_config_arch: 'x64' }, - ['--platform=win32', '--arch=x64', '--force'] - ) - - expect(result.status).not.toBe(0) - expect(result.stderr).toContain('process_commandline.cc') - expect(readFileSync(commandLineSourcePath(projectDir), 'utf8')).not.toContain( - 'kProcessCommandLineInformation' - ) - } finally { - removeTreeSync(projectDir) - } - }) - - it('refuses a Windows rebuild when the process creation-time patch is missing', () => { - const projectDir = mkTempProject() - - try { - writeFakeUsableElectronPackage(projectDir, { platform: 'win32' }) - writeFakeElectronRebuild(projectDir) - writeFakeNodePtyConptyPayload(projectDir, 'x64') - writeFakeWindowsProcessTreeWithNodeAddonApi(projectDir, { creationTimePatchApplied: false }) - - const result = runRebuildScript( - projectDir, - { npm_config_platform: 'win32', npm_config_arch: 'x64' }, - ['--platform=win32', '--arch=x64', '--force'] - ) - - expect(result.status).not.toBe(0) - expect(result.stderr).toContain('process creation-time patch') - } finally { - removeTreeSync(projectDir) - } - }) - - it('restores the ConPTY runtime payload after a Windows Electron rebuild', () => { - const projectDir = mkTempProject() - - try { - writeFakeUsableElectronPackage(projectDir, { platform: 'win32' }) - writeFakeElectronRebuild(projectDir) - writeFakeNodePtyConptyPayload(projectDir, 'x64') - - const result = runRebuildScript( - projectDir, - { npm_config_platform: 'win32', npm_config_arch: 'x64' }, - ['--platform=win32', '--arch=x64', '--force'] - ) - - expect(result.status, result.stderr).toBe(0) - expect(result.stdout).toContain('Restored node-pty ConPTY runtime files for win10-x64') - const runtimeDir = join(projectDir, 'node_modules', 'node-pty', 'build', 'Release', 'conpty') - expect(readFileSync(join(runtimeDir, 'conpty.dll'), 'utf8')).toBe('conpty.dll x64') - expect(readFileSync(join(runtimeDir, 'OpenConsole.exe'), 'utf8')).toBe('OpenConsole.exe x64') - } finally { - removeTreeSync(projectDir) - } - }) - - it.skipIf(process.platform !== 'win32')( - 'does not rebuild a healthy node-pty when another Windows addon fails its probe', - () => { - const projectDir = mkTempProject() - - try { - const rebuildLogPath = join(projectDir, 'electron-rebuild.log') - writeFakeUsableElectronPackage(projectDir, { platform: 'win32' }) - writeFakeElectronRebuild(projectDir, { logPathEnv: 'ORCA_REBUILD_TEST_LOG' }) - writeFakeLoadableNodePty(projectDir) - writeFakeWindowsProcessTree(projectDir) - writeFakeNodePtyConptyPayload(projectDir, process.arch) - - const result = runRebuildScript(projectDir, { - ORCA_REBUILD_TEST_LOG: rebuildLogPath, - npm_config_platform: 'win32', - npm_config_arch: process.arch - }) - - expect(result.status, result.stderr).toBe(0) - expect(result.stdout).toContain('Rebuilding failed native modules: @orca/windows-registry') - const rebuildCall = JSON.parse(readFileSync(rebuildLogPath, 'utf8').trim()) - expect(rebuildCall.onlyModules).toEqual(['@orca/windows-registry']) - } finally { - removeTreeSync(projectDir) - } - } - ) - - it.skipIf(process.platform !== 'win32')( - 'rebuilds a loadable ConPTY native that lacks Orca job ownership', - () => { - const projectDir = mkTempProject() - - try { - const rebuildLogPath = join(projectDir, 'electron-rebuild.log') - writeFakeUsableElectronPackage(projectDir, { platform: 'win32' }) - writeFakeElectronRebuild(projectDir, { logPathEnv: 'ORCA_REBUILD_TEST_LOG' }) - writeFakeLoadableNodePty(projectDir, { ownsPtyJob: false }) - writeFakeWindowsRegistry(projectDir) - writeFakeWindowsProcessTree(projectDir) - writeFakeNodePtyConptyPayload(projectDir, process.arch) - - const result = runRebuildScript(projectDir, { - ORCA_REBUILD_TEST_LOG: rebuildLogPath, - npm_config_platform: 'win32', - npm_config_arch: process.arch - }) - - expect(result.status, result.stderr).toBe(0) - expect(result.stdout).toContain('Rebuilding failed native modules: node-pty') - expect(result.stdout).toContain('missing listJobProcessIds') - const rebuildCall = JSON.parse(readFileSync(rebuildLogPath, 'utf8').trim()) - expect(rebuildCall.onlyModules).toEqual(['node-pty']) - } finally { - removeTreeSync(projectDir) - } - } - ) - - // The shape measured on a Windows dev checkout: the addon loads under - // Electron, exports all three job functions, and predates the denial. A bare - // require proves nothing about it; the probe has to read the binary. - it.skipIf(process.platform !== 'win32')( - 'rebuilds a loadable ConPTY native that owns its job but predates the MSYS breakaway denial', - () => { - const projectDir = mkTempProject() - - try { - const rebuildLogPath = join(projectDir, 'electron-rebuild.log') - writeFakeUsableElectronPackage(projectDir, { platform: 'win32' }) - writeFakeElectronRebuild(projectDir, { logPathEnv: 'ORCA_REBUILD_TEST_LOG' }) - writeFakeLoadableNodePty(projectDir, { cygwinBreakawayDenied: false }) - writeFakeWindowsRegistry(projectDir) - writeFakeWindowsProcessTree(projectDir) - writeFakeNodePtyConptyPayload(projectDir, process.arch) - writeFakeNodePtyConptySource(projectDir) - - const result = runRebuildScript(projectDir, { - ORCA_REBUILD_TEST_LOG: rebuildLogPath, - npm_config_platform: 'win32', - npm_config_arch: process.arch - }) - - expect(result.status, result.stderr).toBe(0) - expect(result.stdout).toContain('Rebuilding failed native modules: node-pty') - expect(result.stdout).toContain('predates the Cygwin/MSYS job-breakaway denial') - expect(result.stdout).not.toContain('skipping rebuild') - const rebuildCall = JSON.parse(readFileSync(rebuildLogPath, 'utf8').trim()) - expect(rebuildCall.onlyModules).toEqual(['node-pty']) - } finally { - removeTreeSync(projectDir) - } - } - ) - - it.skipIf(process.platform === 'win32')( - 'rebuilds when Electron can load node-pty but patched build artifacts are missing', - () => { - const projectDir = mkTempProject() - - try { - const rebuildLogPath = join(projectDir, 'electron-rebuild.log') - writeFakeUsableElectronPackage(projectDir) - writeFakeElectronRebuild(projectDir, { logPathEnv: 'ORCA_REBUILD_TEST_LOG' }) - writeFakeLoadableNodePty(projectDir) - writeNodePtyPatchFile(projectDir) - - const result = runRebuildScript(projectDir, { - ORCA_REBUILD_TEST_LOG: rebuildLogPath - }) - - expect(result.status, result.stderr).toBe(0) - expect(result.stdout).toContain( - 'Patched node-pty build artifacts are missing; rebuilding from source.' - ) - - const rebuildCall = JSON.parse(readFileSync(rebuildLogPath, 'utf8').trim()) - expect(rebuildCall.onlyModules).toEqual(['node-pty']) - expect(rebuildCall.ignoreModules).toEqual(['cpu-features']) - expect(rebuildCall.force).toBe(true) - } finally { - removeTreeSync(projectDir) - } - } - ) - - it.skipIf(process.platform === 'win32')( - 'keeps the Electron load-probe fast path once patched node-pty artifacts exist', - () => { - const projectDir = mkTempProject() - - try { - const rebuildLogPath = join(projectDir, 'electron-rebuild.log') - writeFakeUsableElectronPackage(projectDir) - writeFakeElectronRebuild(projectDir, { logPathEnv: 'ORCA_REBUILD_TEST_LOG' }) - writeFakeLoadableNodePty(projectDir, { nativeDir: '../build/Release/' }) - writeNodePtyPatchFile(projectDir) - writePatchedNodePtyBuildArtifacts(projectDir) - - const result = runRebuildScript(projectDir, { - ORCA_REBUILD_TEST_LOG: rebuildLogPath - }) - - expect(result.status, result.stderr).toBe(0) - expect(result.stdout).toContain( - 'Native modules already load in Electron; skipping rebuild.' - ) - expect(existsSync(rebuildLogPath)).toBe(false) - } finally { - removeTreeSync(projectDir) - } - } - ) - - it.skipIf(process.platform === 'win32')( - 'rebuilds when patched artifacts exist but Electron falls back to node-pty prebuilds', - () => { - const projectDir = mkTempProject() - - try { - const rebuildLogPath = join(projectDir, 'electron-rebuild.log') - writeFakeUsableElectronPackage(projectDir) - writeFakeElectronRebuild(projectDir, { logPathEnv: 'ORCA_REBUILD_TEST_LOG' }) - writeFakeLoadableNodePty(projectDir, { nativeDir: '../prebuilds/darwin-arm64/' }) - writeNodePtyPatchFile(projectDir) - writePatchedNodePtyBuildArtifacts(projectDir) - - const result = runRebuildScript(projectDir, { - ORCA_REBUILD_TEST_LOG: rebuildLogPath - }) - - expect(result.status, result.stderr).toBe(0) - expect(result.stdout).toContain('Rebuilding failed native modules: node-pty') - expect(result.stdout).toContain("expected build/Release so Orca's node-pty patch is active") - - const rebuildCall = JSON.parse(readFileSync(rebuildLogPath, 'utf8').trim()) - expect(rebuildCall.onlyModules).toEqual(['node-pty']) - expect(rebuildCall.force).toBe(true) - } finally { - removeTreeSync(projectDir) - } - } - ) - - // The binary this step produces is the one copied into the packaged app. The - // relay build checks its own artifact and ensure-native-runtime checks what it - // loads; nothing checked this one, so a rebuild that quietly emitted the - // upstream reader shipped. Both non-clean states have to fail, which is the - // caller the tri-state was missing: after a rebuild that reported success, an - // absent binary is a broken build, not an absence to shrug at. - for (const [addon, expected] of [ - ['unpatched', 'still imports ReadProcessMemory'], - ['none', 'is not there'] - ]) { - it(`fails a Windows rebuild that leaves ${addon} windows-process-tree bytes`, () => { - const projectDir = mkTempProject() - - try { - writeFakeUsableElectronPackage(projectDir, { platform: 'win32' }) - writeFakeElectronRebuild(projectDir, { addon }) - writeFakeNodePtyConptyPayload(projectDir, 'x64') - writeFakeWindowsProcessTreeWithNodeAddonApi(projectDir) - - const result = runRebuildScript( - projectDir, - { npm_config_platform: 'win32', npm_config_arch: 'x64' }, - ['--platform=win32', '--arch=x64', '--force'] - ) - - expect(result.status).not.toBe(0) - expect(result.stderr).toContain(expected) - } finally { - removeTreeSync(projectDir) - } - }) - } - - // The Electron probe carries this check too, but it is skipped whenever the - // Electron package binary is unusable. Every job export predates the MSYS - // breakaway denial, so without reading the binary this step would hand the - // packaged app one that leaks every Git Bash child out of its pane's job. - it('fails a Windows rebuild that leaves an addon predating the MSYS breakaway denial', () => { - const projectDir = mkTempProject() - - try { - writeFakeUsableElectronPackage(projectDir, { platform: 'win32' }) - writeFakeElectronRebuild(projectDir) - writeFakeNodePtyConptyPayload(projectDir, 'x64', { cygwinBreakawayDenied: false }) - writeFakeWindowsProcessTreeWithNodeAddonApi(projectDir) - - const result = runRebuildScript( - projectDir, - { npm_config_platform: 'win32', npm_config_arch: 'x64' }, - ['--platform=win32', '--arch=x64', '--force'] - ) - - expect(result.status).not.toBe(0) - expect(result.stderr).toContain('predates the Cygwin/MSYS job-breakaway denial') - } finally { - removeTreeSync(projectDir) - } - }) - - // Measured on a Windows dev checkout whose node_modules predated the denial: - // `--force` compiled for minutes, rewrote conpty.node byte-identical and - // unpatched, and the addon gate then said "rebuild from source" -- the step - // that had just run. The source is readable before the compile; read it. - it('refuses to compile node-pty source that lacks the denial, before the rebuild runs', () => { - const projectDir = mkTempProject() - - try { - const rebuildLogPath = join(projectDir, 'electron-rebuild.log') - writeFakeUsableElectronPackage(projectDir, { platform: 'win32' }) - writeFakeElectronRebuild(projectDir, { logPathEnv: 'ORCA_REBUILD_TEST_LOG' }) - writeFakeNodePtyConptyPayload(projectDir, 'x64') - writeFakeNodePtyConptySource(projectDir, { cygwinBreakawayDenied: false }) - writeFakeWindowsProcessTreeWithNodeAddonApi(projectDir) - - const result = runRebuildScript( - projectDir, - { - ORCA_REBUILD_TEST_LOG: rebuildLogPath, - npm_config_platform: 'win32', - npm_config_arch: 'x64' - }, - ['--platform=win32', '--arch=x64', '--force'] - ) - - expect(result.status).not.toBe(0) - expect(result.stderr).toContain(join('src', 'win', 'conpty.cc')) - expect(result.stderr).toContain('`pnpm install`') - expect(result.stderr).not.toContain('Rebuild node-pty from source') - expect(existsSync(rebuildLogPath)).toBe(false) - } finally { - removeTreeSync(projectDir) - } - }) - - it('compiles node-pty when its source carries the denial', () => { - const projectDir = mkTempProject() - - try { - const rebuildLogPath = join(projectDir, 'electron-rebuild.log') - writeFakeUsableElectronPackage(projectDir, { platform: 'win32' }) - writeFakeElectronRebuild(projectDir, { logPathEnv: 'ORCA_REBUILD_TEST_LOG' }) - writeFakeNodePtyConptyPayload(projectDir, 'x64') - writeFakeNodePtyConptySource(projectDir) - writeFakeWindowsProcessTreeWithNodeAddonApi(projectDir) - - const result = runRebuildScript( - projectDir, - { - ORCA_REBUILD_TEST_LOG: rebuildLogPath, - npm_config_platform: 'win32', - npm_config_arch: 'x64' - }, - ['--platform=win32', '--arch=x64', '--force'] - ) - - expect(result.status, result.stderr).toBe(0) - const rebuildCall = JSON.parse(readFileSync(rebuildLogPath, 'utf8').trim()) - expect(rebuildCall.onlyModules).toContain('node-pty') - } finally { - removeTreeSync(projectDir) - } - }) - - it('accepts a Windows rebuild whose addon carries the denial', () => { - const projectDir = mkTempProject() - - try { - writeFakeUsableElectronPackage(projectDir, { platform: 'win32' }) - writeFakeElectronRebuild(projectDir) - writeFakeNodePtyConptyPayload(projectDir, 'x64') - writeFakeWindowsProcessTreeWithNodeAddonApi(projectDir) - - const result = runRebuildScript( - projectDir, - { npm_config_platform: 'win32', npm_config_arch: 'x64' }, - ['--platform=win32', '--arch=x64', '--force'] - ) - - expect(result.status, result.stderr).toBe(0) - expect(result.stderr).not.toContain('job-breakaway denial') - } finally { - removeTreeSync(projectDir) - } - }) - - // A cross-host rebuild does not necessarily leave a win32 addon on this disk, - // and neither does a tree with no node-pty in it. That must warn, not fail an - // install that was working. - it('warns rather than fails when no addon is expected on this disk', () => { - const projectDir = mkTempProject() - - try { - writeFakeUsableElectronPackage(projectDir, { platform: 'win32' }) - writeFakeElectronRebuild(projectDir) - writeFakeWindowsProcessTreeWithNodeAddonApi(projectDir) - - const result = runRebuildScript( - projectDir, - { npm_config_platform: 'win32', npm_config_arch: 'x64' }, - ['--platform=win32', '--arch=x64', '--force'] - ) - - expect(result.status, result.stderr).toBe(0) - expect(result.stderr + result.stdout).toContain('could not check the MSYS job-breakaway') - } finally { - removeTreeSync(projectDir) - } - }) - - // The other half: on the host that will run this install, a missing addon is - // not an absence to shrug at. loadNativeModule falls through to the published - // prebuild, which is the binary that leaks every MSYS pane child. - // Runs only on Windows -- nothing else can make a win32 rebuild same-host. - it.skipIf(process.platform !== 'win32')( - 'fails a same-host Windows rebuild that left no addon, naming the prebuild that would load', - () => { - const projectDir = mkTempProject() - - try { - writeFakeUsableElectronPackage(projectDir, { platform: 'win32' }) - writeFakeElectronRebuild(projectDir) - writeFakeWindowsProcessTreeWithNodeAddonApi(projectDir) - writeFakeLoadableNodePty(projectDir, { nativeDir: `prebuilds/win32-${process.arch}` }) - - const result = runRebuildScript( - projectDir, - { npm_config_platform: 'win32', npm_config_arch: process.arch }, - ['--platform=win32', `--arch=${process.arch}`, '--force'] - ) - - expect(result.status).not.toBe(0) - expect(result.stderr).toContain(join('build', 'Release', 'conpty.node')) - expect(result.stderr).toContain(join('prebuilds', `win32-${process.arch}`, 'conpty.node')) - } finally { - removeTreeSync(projectDir) - } - } - ) -}) diff --git a/config/scripts/rebuild-native-deps-test-fixtures.mjs b/config/scripts/rebuild-native-deps-test-fixtures.mjs index 333c8f939ef5..726b4a0ba1bb 100644 --- a/config/scripts/rebuild-native-deps-test-fixtures.mjs +++ b/config/scripts/rebuild-native-deps-test-fixtures.mjs @@ -9,22 +9,10 @@ import { } from 'node:fs' import { createRequire } from 'node:module' import { tmpdir } from 'node:os' -import { join, resolve } from 'node:path' +import { join } from 'node:path' import { fileURLToPath } from 'node:url' import { copyScriptWithLocalModules } from './script-module-dependencies.mjs' -import { peImage } from './windows-pe-image-fixture.mjs' -/** - * The wide literal `usesCygwinRuntime` holds, as it sits in a real addon. A - * fixture addon without it is a build that predates the MSYS breakaway denial, - * which is what these tests need to be able to represent. - * - * Taken from the gate itself: a re-typed copy agrees with a stale gate by - * construction, which is the one thing these fixtures must not do. - */ -const { CYGWIN_BREAKAWAY_MARKER, CYGWIN_BREAKAWAY_MARKER_TEXT } = createRequire(import.meta.url)( - './node-pty-job-ownership.cjs' -) const { CREATION_TIME_FLAG } = createRequire(import.meta.url)( './windows-process-tree-creation-time.cjs' ) @@ -33,9 +21,6 @@ const sourceScriptPath = fileURLToPath(new URL('./rebuild-native-deps.mjs', impo const sourceInstallScriptPath = fileURLToPath( new URL('./install-electron-package-binary.mjs', import.meta.url) ) -const sourceNodePtyJobOwnershipPath = fileURLToPath( - new URL('./node-pty-job-ownership.cjs', import.meta.url) -) const sourceWindowsProcessTreeGypRebuildPath = fileURLToPath( new URL('./windows-process-tree-gyp-rebuild.mjs', import.meta.url) ) @@ -112,7 +97,6 @@ export function mkTempProject() { mkdirSync(join(projectDir, 'config', 'scripts'), { recursive: true }) copyFileSync(sourceScriptPath, join(projectDir, 'config', 'scripts', 'rebuild-native-deps.mjs')) copyScriptWithLocalModules(sourceInstallScriptPath, join(projectDir, 'config', 'scripts')) - copyScriptWithLocalModules(sourceNodePtyJobOwnershipPath, join(projectDir, 'config', 'scripts')) copyFileSync( sourceWindowsProcessTreeCreationTimePath, join(projectDir, 'config', 'scripts', 'windows-process-tree-creation-time.cjs') @@ -333,93 +317,6 @@ process.exit(result.status ?? 0) } } -export function writeFakeNodePtyConptyPayload( - projectDir, - arch, - { cygwinBreakawayDenied = true } = {} -) { - const releaseDir = join(projectDir, 'node_modules', 'node-pty', 'build', 'Release') - mkdirSync(releaseDir, { recursive: true }) - writeFileSync( - join(releaseDir, 'conpty.node'), - Buffer.concat([ - peImage({ arch }), - cygwinBreakawayDenied ? CYGWIN_BREAKAWAY_MARKER : Buffer.alloc(0) - ]) - ) - const sourceDir = join( - projectDir, - 'node_modules', - 'node-pty', - 'third_party', - 'conpty', - '0.1.0', - `win10-${arch}` - ) - mkdirSync(sourceDir, { recursive: true }) - writeFileSync(join(sourceDir, 'conpty.dll'), `conpty.dll ${arch}`) - writeFileSync(join(sourceDir, 'OpenConsole.exe'), `OpenConsole.exe ${arch}`) -} - -/** The C++ a Windows rebuild would compile; only the wide literal the source gate reads has to be real. */ -export function writeFakeNodePtyConptySource(projectDir, { cygwinBreakawayDenied = true } = {}) { - const sourceDir = join(projectDir, 'node_modules', 'node-pty', 'src', 'win') - mkdirSync(sourceDir, { recursive: true }) - writeFileSync( - join(sourceDir, 'conpty.cc'), - cygwinBreakawayDenied - ? `for (const wchar_t* dll : {L"${CYGWIN_BREAKAWAY_MARKER_TEXT}", L"cygwin1.dll"}) {}\n` - : 'JOB_OBJECT_LIMIT_BREAKAWAY_OK\n' - ) -} - -function writeFakeNodePtyAddon(nodePtyDir, nativeDir, { cygwinBreakawayDenied }) { - const addonDir = resolve(join(nodePtyDir, 'lib'), nativeDir) - mkdirSync(addonDir, { recursive: true }) - for (const nativeName of ['conpty', 'pty']) { - writeFileSync( - join(addonDir, `${nativeName}.node`), - Buffer.concat([ - peImage({ arch: process.arch === 'arm64' ? 'arm64' : 'x64' }), - cygwinBreakawayDenied ? CYGWIN_BREAKAWAY_MARKER : Buffer.alloc(0) - ]) - ) - } -} - -export function writeFakeLoadableNodePty( - projectDir, - { nativeDir = 'prebuilds/pty', ownsPtyJob = true, cygwinBreakawayDenied = true } = {} -) { - const nodePtyDir = join(projectDir, 'node_modules', 'node-pty') - mkdirSync(join(nodePtyDir, 'lib'), { recursive: true }) - // Why a real file: the job-ownership gate reads the addon it was told about, - // because every job export predates the MSYS breakaway denial and so cannot - // distinguish a current build from one that leaks Git Bash children. - writeFakeNodePtyAddon(nodePtyDir, nativeDir, { cygwinBreakawayDenied }) - writeFileSync(join(nodePtyDir, 'index.js'), 'module.exports = {}\n') - writeFileSync( - join(nodePtyDir, 'lib', 'utils.js'), - ` -exports.loadNativeModule = function loadNativeModule(nativeName) { - return { - dir: ${JSON.stringify(nativeDir)}, - module: { - nativeName, - ...(${JSON.stringify(ownsPtyJob)} - ? { - listJobProcessIds() {}, - terminateJob() {}, - assignCurrentProcessToJob() {} - } - : {}) - } - } -} -` - ) -} - export function writeFakeWindowsRegistry(projectDir) { const registryDir = join(projectDir, 'node_modules', '@orca', 'windows-registry') mkdirSync(registryDir, { recursive: true }) @@ -509,30 +406,3 @@ export function writeFakeWindowsProcessTreeWithNodeAddonApi( writeFileSync(join(nodeAddonApiDir, 'napi-inl.h'), '// napi-inl.h\n') writeFileSync(join(nodeAddonApiDir, 'napi-inl.deprecated.h'), '// napi-inl.deprecated.h\n') } - -export function writeNodePtyPatchFile(projectDir) { - mkdirSync(join(projectDir, 'config', 'patches'), { recursive: true }) - writeFileSync(join(projectDir, 'config', 'patches', 'node-pty@1.1.0.patch'), 'patch marker\n') -} - -export function writePatchedNodePtyBuildArtifacts( - projectDir, - { cygwinBreakawayDenied = true } = {} -) { - const buildDir = join(projectDir, 'node_modules', 'node-pty', 'build', 'Release') - mkdirSync(buildDir, { recursive: true }) - if (process.platform === 'win32') { - writeFileSync( - join(buildDir, 'conpty.node'), - cygwinBreakawayDenied ? CYGWIN_BREAKAWAY_MARKER : Buffer.alloc(0) - ) - mkdirSync(join(buildDir, 'conpty'), { recursive: true }) - writeFileSync(join(buildDir, 'conpty', 'conpty.dll'), '') - writeFileSync(join(buildDir, 'conpty', 'OpenConsole.exe'), '') - return - } - writeFileSync(join(buildDir, 'pty.node'), '') - if (process.platform === 'darwin') { - writeFileSync(join(buildDir, 'spawn-helper'), '') - } -} diff --git a/config/scripts/rebuild-native-deps-windows-process-tree.test.mjs b/config/scripts/rebuild-native-deps-windows-process-tree.test.mjs index 4f98c1b092d3..528cb848e4bc 100644 --- a/config/scripts/rebuild-native-deps-windows-process-tree.test.mjs +++ b/config/scripts/rebuild-native-deps-windows-process-tree.test.mjs @@ -9,7 +9,6 @@ import { mkTempProject, runRebuildScript, writeFakeElectronRebuild, - writeFakeNodePtyConptyPayload, writeFakeUsableElectronPackage, writeFakeWindowsProcessTreeWithNodeAddonApi } from './rebuild-native-deps-test-fixtures.mjs' @@ -75,7 +74,6 @@ describe.runIf(process.platform === 'win32')('rebuild-native-deps stale addon un try { writeFakeUsableElectronPackage(projectDir, { platform: 'win32' }) writeFakeElectronRebuild(projectDir) - writeFakeNodePtyConptyPayload(projectDir, process.arch) writeFakeWindowsProcessTreeWithNodeAddonApi(projectDir) holder = await stageLoadedStaleAddon(projectDir) diff --git a/config/scripts/rebuild-native-deps.mjs b/config/scripts/rebuild-native-deps.mjs index 68de5d49e218..a8cd655e3272 100644 --- a/config/scripts/rebuild-native-deps.mjs +++ b/config/scripts/rebuild-native-deps.mjs @@ -19,28 +19,17 @@ */ import { rebuild } from '@electron/rebuild' -import { execFileSync, spawnSync } from 'node:child_process' +import { spawnSync } from 'node:child_process' import { ensureWindowsProcessTreeCommandLinePatch, inspectWindowsProcessTreeAddon, stageWindowsProcessTreeNodeAddonApiHeaders, windowsProcessTreeAddonPath } from './windows-process-tree-gyp-rebuild.mjs' -import { - copyFileSync, - existsSync, - globSync, - mkdirSync, - readdirSync, - readFileSync, - writeFileSync -} from 'node:fs' -import { createRequire } from 'node:module' +import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs' import { platform as osPlatform } from 'node:os' import { join, resolve } from 'node:path' -const requireLocal = createRequire(import.meta.url) - const projectDir = process.cwd() let cliOptions try { @@ -66,7 +55,6 @@ const electronVersion = JSON.parse( ).version const ignoreModules = ['cpu-features'] -const NODE_PTY_CONPTY_RUNTIME_FILES = ['conpty.dll', 'OpenConsole.exe'] if (ignoreModules.length > 0) { console.log(`[rebuild] Skipping optional Electron rebuild modules: ${ignoreModules.join(', ')}`) @@ -77,7 +65,6 @@ if (ignoreModules.length > 0) { // rebuild via `onlyModules` ensures they're recompiled against Electron's Node // ABI regardless of the package manager's store layout. const NATIVE_MODULES = [ - 'node-pty', 'cpu-features', ...(rebuildPlatform === 'win32' ? ['@orca/windows-registry', '@vscode/windows-process-tree'] : []) ] @@ -89,13 +76,14 @@ const forceRebuild = let modulesToRebuild = onlyModules ensureElectronPackageInstalled() -restoreNodePtyWindowsConptyRuntime() -const patchedNodePtyRebuildReason = forceRebuild ? null : getPatchedNodePtyRebuildReason() +// An empty allowlist invokes @electron/rebuild's default scanner. +if (onlyModules.length === 0) { + console.log('[rebuild] No native modules selected; Electron package is ready.') + process.exit(0) +} -if (patchedNodePtyRebuildReason) { - console.log(`[rebuild] ${patchedNodePtyRebuildReason}`) -} else if (!forceRebuild) { +if (!forceRebuild) { // Why: independent probes avoid rebuilding healthy Windows DLLs that may already be loaded and locked. const probes = onlyModules.map((moduleName) => ({ moduleName, @@ -116,42 +104,11 @@ if (patchedNodePtyRebuildReason) { console.log(`[rebuild] Forcing native rebuild for ${rebuildPlatform}-${rebuildArch}.`) } -// Why: cpu-features ships without `buildcheck.gypi`; its own `install` script -// generates it by running `node buildcheck.js > buildcheck.gypi` before -// node-gyp. @electron/rebuild with `force: true` invokes node-gyp directly -// and bypasses that install hook, so if the file is missing (fresh install, -// store prune, or a prior failed run) node-gyp aborts with -// "buildcheck.gypi not found". Regenerate it here before rebuilding. -if (!ignoreModules.includes('cpu-features')) { - const cpuFeatureDirs = globSync('node_modules/.pnpm/cpu-features@*/node_modules/cpu-features', { - cwd: projectDir - }) - for (const relDir of cpuFeatureDirs) { - const dir = resolve(projectDir, relDir) - const gypiPath = resolve(dir, 'buildcheck.gypi') - if (existsSync(gypiPath)) { - continue - } - try { - const out = execFileSync(process.execPath, ['buildcheck.js'], { - cwd: dir, - encoding: 'utf8' - }) - writeFileSync(gypiPath, out) - console.log(`[rebuild] Generated ${relDir}/buildcheck.gypi`) - } catch (/** @type {any} */ err) { - console.error(`[rebuild] Failed to generate ${relDir}/buildcheck.gypi:`, err?.message ?? err) - process.exit(1) - } - } -} - try { // Why inside the try: the patch guard deletes a stale addon binary, and that // delete fails EPERM when the addon is loaded -- exactly the running-Orca case // the catch below is written for. Outside, it aborted `pnpm install` with a // raw stack instead of the "close running Orca/Electron processes" message. - assertNodePtyConptySourceDeniesMsysBreakaway() if ( rebuildPlatform === 'win32' && modulesToRebuild.includes('@vscode/windows-process-tree') && @@ -176,9 +133,7 @@ try { // Node before postinstall runs this script. force: true }) - restoreNodePtyWindowsConptyRuntime() assertWindowsProcessTreeAddonIsPatched() - assertNodePtyConptyDeniesMsysBreakaway() } catch (/** @type {any} */ err) { console.error('[rebuild] Native module rebuild failed:', err?.message ?? err) if (isWindowsNativeLockError(err)) { @@ -232,79 +187,6 @@ function assertWindowsProcessTreeAddonIsPatched() { ) } -/** - * The other half of the same problem, for the addon this rebuild just produced. - * - * The Electron probe below carries the marker check too, but it is skipped - * whenever the Electron package binary is unusable -- and "covered by another - * path" is not "this path checks". Reading the binary needs neither a loadable - * Electron nor an executable target arch, so it runs here regardless. - * - * Absent is fatal on the host that will run this install: loadNativeModule - * falls through to prebuilds/win32-, and the published prebuild predates - * the denial, so the app would load it with nothing said. A cross-host rebuild - * does not necessarily leave a win32 addon on this disk, and that must not fail - * an install that was working. - */ -function assertNodePtyConptyDeniesMsysBreakaway() { - if (rebuildPlatform !== 'win32' || !modulesToRebuild.includes('node-pty')) { - return - } - const { assertRebuiltConptyDeniesMsysBreakaway } = requireLocal('./node-pty-job-ownership.cjs') - assertRebuiltConptyDeniesMsysBreakaway({ - nodePtyDir: resolve(projectDir, 'node_modules', 'node-pty'), - rebuildArch, - crossHost: isCrossHostRebuild - }) -} - -/** - * Refuse to compile node-pty source that cannot yield the denial. Why before the - * rebuild: the gate above would spend the compile and then advise "rebuild from - * source" -- the step that just ran. Only the source is read; the addon is not - * touched, so a locked binary cannot turn this into a spurious EPERM. - */ -function assertNodePtyConptySourceDeniesMsysBreakaway() { - if (rebuildPlatform !== 'win32' || !modulesToRebuild.includes('node-pty')) { - return - } - const { assertNodePtySourceDeniesMsysBreakaway } = requireLocal('./node-pty-job-ownership.cjs') - assertNodePtySourceDeniesMsysBreakaway({ - nodePtyDir: resolve(projectDir, 'node_modules', 'node-pty') - }) -} - -function restoreNodePtyWindowsConptyRuntime() { - if (rebuildPlatform !== 'win32' || !onlyModules.includes('node-pty')) { - return - } - - const nodePtyDir = resolve(projectDir, 'node_modules', 'node-pty') - if (!existsSync(join(nodePtyDir, 'build', 'Release', 'conpty.node'))) { - return - } - const conptyRoot = join(nodePtyDir, 'third_party', 'conpty') - const sourceDir = readdirSync(conptyRoot, { withFileTypes: true }) - .filter((entry) => entry.isDirectory()) - .map((entry) => join(conptyRoot, entry.name, `win10-${rebuildArch}`)) - .find((candidate) => existsSync(candidate)) - if (!sourceDir) { - throw new Error(`node-pty has no ConPTY runtime payload for win10-${rebuildArch}`) - } - - const runtimeDir = join(nodePtyDir, 'build', 'Release', 'conpty') - mkdirSync(runtimeDir, { recursive: true }) - for (const filename of NODE_PTY_CONPTY_RUNTIME_FILES) { - const sourceFile = join(sourceDir, filename) - if (!existsSync(sourceFile)) { - throw new Error(`node-pty is missing ${sourceFile}`) - } - copyFileSync(sourceFile, join(runtimeDir, filename)) - } - // Why: @electron/rebuild bypasses node-pty's postinstall step that normally copies these DLLs. - console.log(`[rebuild] Restored node-pty ConPTY runtime files for win10-${rebuildArch}.`) -} - function ensureElectronPackageInstalled() { repairElectronPathFile() if (electronPackageIsUsable()) { @@ -509,53 +391,6 @@ function getElectronExecutablePath() { : resolve(electronPackageDir, 'dist', platformPath) } -function getPatchedNodePtyRebuildReason() { - if (!requiresPatchedNodePtySourceBuild()) { - return null - } - - // Why: Orca patches node-pty's native Unix spawn path and Windows job-object - // exports; upstream prebuilds can load while missing those patches. - const nodePtyDir = resolve(projectDir, 'node_modules', 'node-pty') - const artifactPaths = - rebuildPlatform === 'win32' - ? [ - resolve(nodePtyDir, 'build', 'Release', 'conpty.node'), - ...NODE_PTY_CONPTY_RUNTIME_FILES.map((filename) => - resolve(nodePtyDir, 'build', 'Release', 'conpty', filename) - ) - ] - : [ - resolve(nodePtyDir, 'build', 'Release', 'pty.node'), - ...(osPlatform() === 'darwin' - ? [resolve(nodePtyDir, 'build', 'Release', 'spawn-helper')] - : []) - ] - const missingArtifact = artifactPaths.find((artifactPath) => !existsSync(artifactPath)) - - if (!missingArtifact) { - return null - } - - return 'Patched node-pty build artifacts are missing; rebuilding from source.' -} - -function requiresPatchedNodePtySourceBuild() { - if (!onlyModules.includes('node-pty')) { - return false - } - if (rebuildPlatform !== osPlatform() || rebuildArch !== process.arch) { - return false - } - - const nodePtyPatchPath = resolve(projectDir, 'config', 'patches', 'node-pty@1.1.0.patch') - if (!existsSync(nodePtyPatchPath)) { - return false - } - - return existsSync(resolve(projectDir, 'node_modules', 'node-pty')) -} - function probeElectronNativeModules(moduleNames) { if (!electronPackageIsUsable()) { return { ok: false, status: null, stderr: 'Electron package binary is unavailable.' } @@ -564,12 +399,9 @@ function probeElectronNativeModules(moduleNames) { const probeSource = ` const { createRequire } = require('node:module') -const { existsSync } = require('node:fs') -const { release } = require('node:os') const { resolve } = require('node:path') const projectRequire = createRequire(resolve(process.cwd(), 'package.json')) const moduleNames = ${JSON.stringify(moduleNames)} -const requirePatchedNodePtySourceBuild = ${JSON.stringify(requiresPatchedNodePtySourceBuild())} const failures = [] for (const moduleName of moduleNames) { @@ -592,33 +424,6 @@ function loadNativeModule(moduleName) { registry.getRegistryKey(registry.HK.CU, 'Environment') return } - if (moduleName === 'node-pty') { - projectRequire('node-pty') - const { assertNodePtyJobOwnership, nodePtyAddonPath } = projectRequire( - './config/scripts/node-pty-job-ownership.cjs' - ) - const { loadNativeModule } = projectRequire('node-pty/lib/utils') - const nativeName = getNodePtyNativeModuleName() - const native = loadNativeModule(nativeName) - assertNodePtyWindowsConptyRuntime(native.dir) - assertNodePtyJobOwnership({ - nativeName, - native, - addonPath: nodePtyAddonPath( - projectRequire.resolve('node-pty/lib/utils'), - native, - nativeName - ) - }) - if (requirePatchedNodePtySourceBuild && !isNodePtyReleaseBuildDir(native.dir)) { - throw new Error( - 'node-pty resolved to ' + - native.dir + - '; expected build/Release so Orca\\'s node-pty patch is active' - ) - } - return - } if (moduleName === '@vscode/windows-process-tree') { // The tarball prebuilt loads under Electron too -- the addon is N-API, so // a bare require proves nothing about which source it was built from. @@ -631,39 +436,6 @@ function loadNativeModule(moduleName) { projectRequire(moduleName) } -function assertNodePtyWindowsConptyRuntime(nativeDir) { - if (process.platform !== 'win32' || !isNodePtyReleaseBuildDir(nativeDir)) { - return - } - const runtimeDir = resolve( - process.cwd(), - 'node_modules', - 'node-pty', - 'build', - 'Release', - 'conpty' - ) - for (const filename of ${JSON.stringify(NODE_PTY_CONPTY_RUNTIME_FILES)}) { - const runtimeFile = resolve(runtimeDir, filename) - if (!existsSync(runtimeFile)) { - throw new Error('node-pty ConPTY runtime file is missing: ' + runtimeFile) - } - } -} - -function isNodePtyReleaseBuildDir(nativeDir) { - return typeof nativeDir === 'string' && nativeDir.replace(/\\\\/g, '/').includes('build/Release/') -} - -function getNodePtyNativeModuleName() { - if (process.platform !== 'win32') { - return 'pty' - } - const match = /(\\d+)\\.(\\d+)\\.(\\d+)/g.exec(release()) - const buildNumber = match && match.length === 4 ? Number.parseInt(match[3], 10) : 0 - return buildNumber >= 18309 ? 'conpty' : 'pty' -} - function formatError(error) { return error instanceof Error ? error.message : String(error) } @@ -695,9 +467,7 @@ function isWindowsNativeLockError(error) { const text = [error?.message, error?.stack, error?.stdout, error?.stderr] .filter(Boolean) .join('\n') - return /(?:EPERM|operation not permitted)[\s\S]*(?:unlink|\.node|conpty\.node|pty\.node)/i.test( - text - ) + return /(?:EPERM|operation not permitted)[\s\S]*(?:unlink|\.node)/i.test(text) } function isPostinstall() { diff --git a/config/scripts/rebuild-native-deps.test.mjs b/config/scripts/rebuild-native-deps.test.mjs index d4db08d3e2e9..53000dce19cb 100644 --- a/config/scripts/rebuild-native-deps.test.mjs +++ b/config/scripts/rebuild-native-deps.test.mjs @@ -235,3 +235,40 @@ describe('rebuild-native-deps Electron install fallback', () => { } }) }) + +describe('rebuild-native-deps empty native selection', () => { + it.each([ + { name: 'ordinary rebuild', args: ['--platform=linux'], env: {} }, + { name: 'forced rebuild', args: ['--platform=linux', '--force'], env: {} }, + { + name: 'environment-forced rebuild', + args: ['--platform=linux'], + env: { ORCA_FORCE_NATIVE_REBUILD: '1' } + }, + { + name: 'cross-architecture rebuild', + args: ['--platform=linux', `--arch=${process.arch === 'arm64' ? 'x64' : 'arm64'}`], + env: {} + } + ])('installs Electron without scanning unrelated addons: $name', ({ args, env }) => { + const projectDir = mkTempProject() + try { + const logPath = join(projectDir, 'rebuild.log') + writeFakeElectronPackage(projectDir) + writeFakeElectronGet(projectDir) + writeFakeElectronExtractor(projectDir, { createExecutable: true }) + writeFakeElectronRebuild(projectDir, { logPathEnv: 'ORCA_REBUILD_TEST_LOG' }) + const result = runRebuildScript(projectDir, { ...env, ORCA_REBUILD_TEST_LOG: logPath }, args) + expect(result.status, result.stderr).toBe(0) + expect(readFileSync(join(projectDir, 'node_modules/electron/path.txt'), 'utf8')).toBe( + 'electron' + ) + expect(readFileSync(join(projectDir, 'electron-get.log'), 'utf8')).toBe( + 'download attempted\n' + ) + expect(existsSync(logPath)).toBe(false) + } finally { + removeTreeSync(projectDir) + } + }) +}) diff --git a/config/scripts/regenerate-xterm-patches.mjs b/config/scripts/regenerate-xterm-patches.mjs index 672be8bee61b..a54d313ad6a5 100644 --- a/config/scripts/regenerate-xterm-patches.mjs +++ b/config/scripts/regenerate-xterm-patches.mjs @@ -27,6 +27,11 @@ import { pnpmDiffEnvironment } from './xterm-patch-text.mjs' +import { + assertMappedSourcesMatch, + assertMappedPatchDerivation +} from './xterm-sourcemap-source-contract.mjs' + const DEFAULT_REPO_ROOT = path.resolve(import.meta.dirname, '..', '..') const MANIFEST_RELATIVE_PATH = path.join('config', 'patches', 'xterm-upstream.json') @@ -274,6 +279,12 @@ function assertToolchain(upstreamRoot, manifest) { */ function assertPristineSourceMatches(pristineDir, upstreamRoot, packageEntry) { const stampFile = packageEntry.versionStampFile + if (packageEntry.sourceMaps) { + for (const map of packageEntry.sourceMaps) { + assertMappedSourcesMatch(path.join(pristineDir, map), upstreamRoot) + } + return + } const sourceRoot = path.join(pristineDir, 'src') const drifted = listFilesRelative(sourceRoot) .map((relative) => path.join('src', relative)) @@ -312,7 +323,7 @@ function buildPackage(upstreamRoot, packageEntry, manifest) { /** Proves the pinned toolchain still reproduces the untouched published bundles. */ function assertReproducesPristineBundles(pristineDir, upstreamRoot, packageEntry) { - const packageRoot = path.join(upstreamRoot, packageEntry.packageDir) + const packageRoot = path.join(upstreamRoot, packageEntry.outputDir ?? packageEntry.packageDir) const drifted = listFilesRelative(pristineDir) .filter((relative) => packageEntry.generatedPaths.some((prefix) => toPosix(relative).startsWith(prefix)) @@ -340,7 +351,7 @@ function toPosix(value) { function overlayBuildOutput(pristineDir, upstreamRoot, packageEntry, destination) { rmSync(destination, { recursive: true, force: true }) cpSync(pristineDir, destination, { recursive: true }) - const packageRoot = path.join(upstreamRoot, packageEntry.packageDir) + const packageRoot = path.join(upstreamRoot, packageEntry.outputDir ?? packageEntry.packageDir) for (const relative of listFilesRelative(pristineDir)) { // package.json carries the registry's version/commit stamp, which the build // tree has no way to reproduce and which we never want to patch. @@ -431,7 +442,14 @@ function regeneratePackage(packageEntry, manifest, context) { ) } const patch = diffFolders(pristineDir, patchedDir) - assertSourceDerivationsAgree(source, patch) + if (packageEntry.sourceMaps) { + for (const map of packageEntry.sourceMaps) { + assertMappedSourcesMatch(path.join(patchedDir, map), upstreamRoot) + assertMappedPatchDerivation(source, patch, map) + } + } else { + assertSourceDerivationsAgree(source, patch) + } return { patch, source } } diff --git a/config/scripts/regenerate-xterm-patches.test.mjs b/config/scripts/regenerate-xterm-patches.test.mjs index 5261d0c74cd2..eb3d1e730f4f 100644 --- a/config/scripts/regenerate-xterm-patches.test.mjs +++ b/config/scripts/regenerate-xterm-patches.test.mjs @@ -1,3 +1,4 @@ +import { assertMappedPatchDerivation } from './xterm-sourcemap-source-contract.mjs' import { execFileSync } from 'node:child_process' import { createHash } from 'node:crypto' import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' @@ -487,7 +488,13 @@ describe('committed xterm patch artifacts', () => { for (const packageEntry of manifest.packages) { const patch = await readFile(path.join(REPO_ROOT, packageEntry.patch), 'utf8') const source = await readFile(path.join(REPO_ROOT, packageEntry.sourcePatch), 'utf8') - expect(sourceHunks(source)).toBe(sourceHunks(patch)) + if (packageEntry.sourceMaps) { + for (const map of packageEntry.sourceMaps) { + expect(() => assertMappedPatchDerivation(source, patch, map)).not.toThrow() + } + } else { + expect(sourceHunks(source)).toBe(sourceHunks(patch)) + } expect(generatedHunks(patch, packageEntry.generatedPaths)).not.toBe('') } }) diff --git a/config/scripts/run-bundled-runtime-tests.mjs b/config/scripts/run-bundled-runtime-tests.mjs index f10e7532af84..772cf503d0d2 100644 --- a/config/scripts/run-bundled-runtime-tests.mjs +++ b/config/scripts/run-bundled-runtime-tests.mjs @@ -13,7 +13,12 @@ const runtime = join( `${process.platform}-${process.arch}`, cliRuntimeFilename(process.platform) ) -const env = { ...process.env, ORCA_BACKGROUND_LAUNCH: '1', BUN_EXECUTABLE: runtime } +const env = { + ...process.env, + ORCA_BACKGROUND_LAUNCH: '1', + BUN_EXECUTABLE: runtime, + ORCA_TEST_REQUIRE_CLI_ARTIFACTS: '1' +} function run(args, environment = env) { const result = runProcessSync({ program: process.execPath, @@ -59,6 +64,9 @@ run([ 'src/main/browser/wsl-browser-network-bun.integration.test.ts', 'src/main/cli/windows-cli-bun.integration.test.ts', 'src/cli/cli-bun-runtime.integration.test.ts', + 'src/cli/runtime/cli-bun-environment.integration.test.ts', + 'src/shared/child-process/fork-process-bun.integration.test.ts', + 'src/shared/bun-owned-runtime-args.integration.test.ts', 'src/cli/runtime/serve-bun-ipc.integration.test.ts', 'src/cli/cli-bin.integration.test.ts', 'src/cli/cli-bin-signals.integration.test.ts', diff --git a/config/scripts/run-daemon-shutdown-descendants-docker.mjs b/config/scripts/run-daemon-shutdown-descendants-docker.mjs index 30af813f7bd3..f89bb07eece5 100644 --- a/config/scripts/run-daemon-shutdown-descendants-docker.mjs +++ b/config/scripts/run-daemon-shutdown-descendants-docker.mjs @@ -1,9 +1,10 @@ -import { execFileSync, spawnSync } from 'node:child_process' -import { existsSync, mkdtempSync, rmSync } from 'node:fs' +import { copyFileSync, existsSync, mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { delimiter, dirname, join, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { build } from 'esbuild' +import { materializeRuntime } from './build-orcad-bun.mjs' +import { runProcessSync } from './script-child-process.mjs' const repo = resolve(fileURLToPath(new URL('../..', import.meta.url))) const docker = @@ -49,10 +50,16 @@ const platform = process.env.ORCA_DOCKER_PLATFORM ?? (process.arch === 'arm64' ? 'linux/arm64' : 'linux/amd64') function gitSource(relativePath, ref) { - return execFileSync('git', ['show', `${ref}:${relativePath}`], { + const result = runProcessSync({ + program: 'git', + args: ['show', `${ref}:${relativePath}`], cwd: repo, - encoding: 'utf8' + maxOutputBytes: 1024 * 1024 }) + if (result.code !== 0 || result.outputTruncated) { + throw new Error(`Could not read baseline source ${ref}:${relativePath}: ${result.stderr}`) + } + return result.stdout } function baselinePlugin(ref) { @@ -78,8 +85,8 @@ async function bundle(outfile, plugin) { bundle: true, platform: 'node', format: 'cjs', - target: 'node22', - external: ['node-pty'], + target: 'es2024', + external: ['bun:ffi'], plugins: plugin ? [plugin] : [], outfile, sourcemap: false, @@ -87,25 +94,35 @@ async function bundle(outfile, plugin) { }) } -function runDocker(args, allowFailure = false) { - const result = spawnSync(docker, args, { +function runDocker(args, allowFailure = false, timeoutMs = 60_000) { + const result = runProcessSync({ + program: docker, + args, cwd: repo, - encoding: 'utf8', - maxBuffer: 16 * 1024 * 1024, + maxOutputBytes: 16 * 1024 * 1024, + timeoutMs, env: dockerEnv }) - if (result.error) { - throw result.error - } - if (result.status !== 0 && !allowFailure) { + if ((result.code !== 0 || result.timedOut) && !allowFailure) { process.stdout.write(result.stdout ?? '') process.stderr.write(result.stderr ?? '') - throw new Error(`docker ${args[0]} failed with ${result.status}`) + throw new Error(`docker ${args[0]} failed: exit=${result.code} timeout=${result.timedOut}`) } return result } +if (!['linux/amd64', 'linux/arm64'].includes(platform)) { + rmSync(temp, { recursive: true, force: true }) + throw new Error(`Unsupported daemon oracle platform: ${platform}`) +} + +let imageBuilt = false try { + for (const name of ['Dockerfile', 'run-case.sh', 'fixture.cjs']) { + copyFileSync(join(dockerDir, name), join(temp, name)) + } + const target = platform === 'linux/arm64' ? 'linux-arm64-glibc' : 'linux-x64-glibc' + await materializeRuntime(target, join(temp, 'bun-runtime')) const candidate = join(temp, 'candidate.cjs') await bundle(candidate) const bundles = [['candidate', candidate]] @@ -115,28 +132,46 @@ try { bundles.unshift(['baseline', baseline]) } - runDocker(['build', '--platform', platform, '-t', image, dockerDir]) + runDocker(['build', '--platform', platform, '-t', image, temp], false, 300_000) + imageBuilt = true for (const [mode, bundlePath] of bundles) { - const result = runDocker( - [ - 'run', - '--rm', - '--platform', - platform, - '-e', - 'ORCA_BACKGROUND_LAUNCH=1', - '-v', - `${bundlePath}:/fixtures/${mode}.cjs:ro`, - image, - `/fixtures/${mode}.cjs`, - mode - ], - true - ) + const containerName = `${image.replace(':', '-')}-${mode}` + let result + let cleanup + try { + result = runDocker( + [ + 'run', + '--rm', + '--name', + containerName, + '--platform', + platform, + '-e', + 'ORCA_BACKGROUND_LAUNCH=1', + '-v', + `${bundlePath}:/fixtures/${mode}.cjs:ro`, + image, + `/fixtures/${mode}.cjs`, + mode + ], + true + ) + } finally { + // Killing the Docker client does not stop its container. + cleanup = runDocker(['rm', '--force', containerName], true, 10_000) + } + if (cleanup.timedOut || (cleanup.code !== 0 && !cleanup.stderr.includes('No such container'))) { + throw new Error( + `Could not remove qualification container ${containerName}: ${cleanup.stderr}` + ) + } process.stdout.write(result.stdout ?? '') process.stderr.write(result.stderr ?? '') - if (result.status !== 0) { - throw new Error(`${mode} daemon-shutdown oracle failed with ${result.status}`) + if (result.code !== 0 || result.timedOut) { + throw new Error( + `${mode} daemon-shutdown oracle failed: exit=${result.code} timeout=${result.timedOut}` + ) } } console.log( @@ -145,6 +180,8 @@ try { : 'Linux daemon shutdown descendant oracle passed (candidate only): descendant reaped, canary survives.' ) } finally { - runDocker(['image', 'rm', image], true) + if (imageBuilt) { + runDocker(['image', 'rm', image], true) + } rmSync(temp, { recursive: true, force: true }) } diff --git a/config/scripts/run-daemon-shutdown-descendants-docker.test.mjs b/config/scripts/run-daemon-shutdown-descendants-docker.test.mjs new file mode 100644 index 000000000000..fbd46f8cde61 --- /dev/null +++ b/config/scripts/run-daemon-shutdown-descendants-docker.test.mjs @@ -0,0 +1,129 @@ +import { readFileSync, writeFileSync, existsSync } from 'node:fs' +import { join } from 'node:path' +import { afterEach, describe, expect, it, vi } from 'vitest' + +const state = vi.hoisted(() => ({ + target: '', + builds: [], + runs: [], + gitReads: [], + commands: [], + timeout: '' +})) +vi.mock('./build-orcad-bun.mjs', () => ({ + materializeRuntime: async (target, destination) => { + state.target = target + writeFileSync(destination, 'verified-runtime-fixture') + } +})) +vi.mock('./script-child-process.mjs', () => ({ + runProcessSync: ({ program, args, timeoutMs }) => { + state.commands.push({ program, args, timeoutMs }) + if (program === 'git') { + state.gitReads.push(args[1]) + return { + code: 0, + stdout: readFileSync(args[1].split(':').slice(1).join(':'), 'utf8'), + stderr: '' + } + } + if (args[0] === 'build') { + const context = args.at(-1) + state.builds.push({ + runtime: readFileSync(join(context, 'bun-runtime'), 'utf8'), + dockerfile: readFileSync(join(context, 'Dockerfile'), 'utf8'), + fixture: readFileSync(join(context, 'fixture.cjs'), 'utf8'), + candidate: readFileSync(join(context, 'candidate.cjs'), 'utf8'), + baseline: existsSync(join(context, 'baseline.cjs')), + context + }) + } + if (args[0] === 'run') { + state.runs.push(args) + } + return { + code: state.timeout === args[0] ? 255 : 0, + timedOut: state.timeout === args[0], + stdout: '', + stderr: '' + } + } +})) + +const argv = process.argv +const platform = process.env.ORCA_DOCKER_PLATFORM +afterEach(() => { + process.argv = argv + if (platform === undefined) { + delete process.env.ORCA_DOCKER_PLATFORM + } else { + process.env.ORCA_DOCKER_PLATFORM = platform + } + state.builds.length = 0 + state.runs.length = 0 + state.gitReads.length = 0 + state.target = '' + state.commands.length = 0 + state.timeout = '' + vi.resetModules() +}) + +async function run(target, args = []) { + process.env.ORCA_DOCKER_PLATFORM = target + process.argv = ['node', 'runner', ...args] + await import('./run-daemon-shutdown-descendants-docker.mjs') +} + +describe('Bun daemon descendant shutdown oracle', () => { + it.each([ + ['linux/amd64', 'linux-x64-glibc'], + ['linux/arm64', 'linux-arm64-glibc'] + ])('stages the verified runtime and real provider for %s', async (platform, target) => { + await run(platform) + expect(state.target).toBe(target) + const staged = state.builds[0] + expect(staged.runtime).toBe('verified-runtime-fixture') + expect(staged.dockerfile).not.toMatch(/node-pty|npm|FROM node:/) + expect(staged.fixture).toContain('spawnBunPty({') + expect(staged.candidate).toContain('spawnBunPty') + expect(staged.candidate).not.toMatch(/require\(["']node-pty["']\)/) + expect(state.runs[0]).toContain('candidate') + expect(existsSync(staged.context)).toBe(false) + }) + + it('compares selected baseline shutdown sources on the same Bun provider', async () => { + await run('linux/amd64', ['--baseline', 'baseline-ref']) + expect(state.gitReads).toHaveLength(5) + expect(state.gitReads.every((source) => source.startsWith('baseline-ref:'))).toBe(true) + expect(state.builds[0].baseline).toBe(true) + expect(state.runs.map((args) => args.at(-1))).toEqual(['baseline', 'candidate']) + }) + + it('gives image builds their own budget and reports timeouts', async () => { + state.timeout = 'build' + await expect(run('linux/amd64')).rejects.toThrow('docker build failed: exit=255 timeout=true') + expect(state.commands.find((command) => command.args[0] === 'build').timeoutMs).toBe(300_000) + expect(state.runs).toEqual([]) + expect(existsSync(state.builds[0].context)).toBe(false) + }) + + it('removes the exact owned container after a timed-out Docker client', async () => { + state.timeout = 'run' + await expect(run('linux/amd64')).rejects.toThrow('candidate daemon-shutdown oracle failed') + const launch = state.commands.find((command) => command.args[0] === 'run') + const containerName = launch.args[launch.args.indexOf('--name') + 1] + expect(launch.timeoutMs).toBe(60_000) + expect(state.commands).toContainEqual({ + program: expect.any(String), + args: ['rm', '--force', containerName], + timeoutMs: 10_000 + }) + expect(existsSync(state.builds[0].context)).toBe(false) + }) + + it('rejects unsupported architectures before materializing a runtime', async () => { + await expect(run('linux/386')).rejects.toThrow('Unsupported daemon oracle platform') + expect(state.target).toBe('') + expect(state.builds).toEqual([]) + }) +}) diff --git a/config/scripts/run-electron-vite-dev.mjs b/config/scripts/run-electron-vite-dev.mjs index c520083cb6ad..f99efb08f2e1 100644 --- a/config/scripts/run-electron-vite-dev.mjs +++ b/config/scripts/run-electron-vite-dev.mjs @@ -16,6 +16,8 @@ import net from 'node:net' import { createRequire } from 'node:module' import path from 'node:path' +import { buildTerminalDaemon } from './build-terminal-daemon.mjs' +import { buildCliRuntime } from './build-cli-runtime.mjs' import { prepareDevCliTerminalWrappers } from './dev-cli-terminal-wrapper.mjs' import { DEV_BUNDLE_MARKER_FILENAME, @@ -646,6 +648,10 @@ if (!userPassedPort && !isHelpOrVersion) { } } prepareDevWebClient() +if (!isHelpOrVersion) { + await buildCliRuntime(process.platform, process.arch) + await buildTerminalDaemon() +} const forwardedArgs = ['dev', ...forwardedRaw, ...forwardedExtras] const child = spawn(process.execPath, [electronViteCli, ...forwardedArgs], { stdio: 'inherit', diff --git a/config/scripts/run-linux-packaged-node-pty-floor-smoke.mjs b/config/scripts/run-linux-packaged-node-pty-floor-smoke.mjs deleted file mode 100644 index 17f2d678e4e1..000000000000 --- a/config/scripts/run-linux-packaged-node-pty-floor-smoke.mjs +++ /dev/null @@ -1,94 +0,0 @@ -import { existsSync } from 'node:fs' -import { spawnSync } from 'node:child_process' -import { isAbsolute, relative, resolve, sep } from 'node:path' -import { pathToFileURL } from 'node:url' - -const FLOOR_IMAGE = 'ubuntu:20.04' -const LINUX_EXECUTABLE = 'orca-ide' -const FLOOR_PACKAGES = [ - 'ca-certificates', - 'libasound2', - 'libatspi2.0-0', - 'libdrm2', - 'libgbm1', - 'libgtk-3-0', - 'libnss3', - 'libx11-xcb1', - 'libxkbcommon0', - 'libxss1' -] - -function workspaceRelativePath(workspaceDirectory, inputPath) { - const absolutePath = resolve(workspaceDirectory, inputPath) - const relativePath = relative(workspaceDirectory, absolutePath) - if (!relativePath || relativePath === '..' || relativePath.startsWith(`..${sep}`)) { - throw new Error('linux-packaged-node-pty-floor-app-directory-invalid') - } - return relativePath.split(sep).join('/') -} - -export function packagedNodePtyFloorDockerArgs({ workspaceDirectory, appDirectory }) { - const relativeAppDirectory = workspaceRelativePath(workspaceDirectory, appDirectory) - const containerAppDirectory = `/workspace/${relativeAppDirectory}` - const command = [ - 'export DEBIAN_FRONTEND=noninteractive', - 'apt-get update -qq', - `apt-get install -y -qq ${FLOOR_PACKAGES.join(' ')} >/dev/null`, - `ELECTRON_RUN_AS_NODE=1 ${containerAppDirectory}/${LINUX_EXECUTABLE} ` + - '/workspace/config/scripts/linux-packaged-node-pty-floor-child.cjs ' + - `${containerAppDirectory}/resources` - ].join(' && ') - return [ - 'run', - '--rm', - '--mount', - `type=bind,src=${workspaceDirectory},dst=/workspace,readonly`, - '--workdir', - '/workspace', - FLOOR_IMAGE, - '/bin/bash', - '-lc', - command - ] -} - -function parseAppDirectory(argv) { - const index = argv.indexOf('--app-dir') - const value = index !== -1 ? argv[index + 1] : undefined - if (!value || isAbsolute(value) || value.startsWith('-')) { - throw new Error('Usage: run-linux-packaged-node-pty-floor-smoke.mjs --app-dir ') - } - return value -} - -export function runPackagedNodePtyFloorSmoke({ - workspaceDirectory = process.cwd(), - appDirectory, - spawn = spawnSync -}) { - if (process.platform !== 'linux') { - throw new Error('linux-packaged-node-pty-floor-smoke-requires-linux') - } - const absoluteAppDirectory = resolve(workspaceDirectory, appDirectory) - if (!existsSync(resolve(absoluteAppDirectory, LINUX_EXECUTABLE))) { - throw new Error(`linux-packaged-node-pty-floor-executable-missing: ${absoluteAppDirectory}`) - } - const result = spawn( - 'docker', - packagedNodePtyFloorDockerArgs({ workspaceDirectory, appDirectory }), - { encoding: 'utf8', maxBuffer: 8 * 1024 * 1024 } - ) - if (result.error || result.signal || result.status !== 0) { - const detail = - result.error?.message || - result.stderr?.trim() || - `status-${result.status}-signal-${result.signal ?? 'none'}` - throw new Error(`linux-packaged-node-pty-floor-smoke-failed: ${detail}`) - } - process.stdout.write(result.stdout) -} - -const isMain = process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url -if (isMain) { - runPackagedNodePtyFloorSmoke({ appDirectory: parseAppDirectory(process.argv.slice(2)) }) -} diff --git a/config/scripts/run-linux-packaged-node-pty-floor-smoke.test.mjs b/config/scripts/run-linux-packaged-node-pty-floor-smoke.test.mjs deleted file mode 100644 index c04aced995f7..000000000000 --- a/config/scripts/run-linux-packaged-node-pty-floor-smoke.test.mjs +++ /dev/null @@ -1,36 +0,0 @@ -import { readFileSync } from 'node:fs' -import { describe, expect, it } from 'vitest' -import { packagedNodePtyFloorDockerArgs } from './run-linux-packaged-node-pty-floor-smoke.mjs' - -describe('packaged node-pty Linux floor smoke', () => { - it('mounts the exact package read-only in an Ubuntu 20.04 container', () => { - const args = packagedNodePtyFloorDockerArgs({ - workspaceDirectory: '/repo', - appDirectory: 'dist/linux-arm64-unpacked' - }) - - expect(args).toContain('type=bind,src=/repo,dst=/workspace,readonly') - expect(args).toContain('ubuntu:20.04') - expect(args.at(-1)).toContain('/workspace/dist/linux-arm64-unpacked/orca-ide') - expect(args.at(-1)).toContain('ELECTRON_RUN_AS_NODE=1') - }) - - it('rejects an app directory outside the workspace', () => { - expect(() => - packagedNodePtyFloorDockerArgs({ - workspaceDirectory: '/repo', - appDirectory: '../outside' - }) - ).toThrow('linux-packaged-node-pty-floor-app-directory-invalid') - }) - - it('loads node-pty from packaged runtime resources', () => { - const childSource = readFileSync( - new URL('./linux-packaged-node-pty-floor-child.cjs', import.meta.url), - 'utf8' - ) - - expect(childSource).toContain("join(resourcesDirectory, 'node_modules', 'node-pty')") - expect(childSource).not.toContain("join(resourcesDirectory, 'app.asar'") - }) -}) diff --git a/config/scripts/run-linux-packaged-terminal-floor-smoke.mjs b/config/scripts/run-linux-packaged-terminal-floor-smoke.mjs new file mode 100644 index 000000000000..0e9a143d7827 --- /dev/null +++ b/config/scripts/run-linux-packaged-terminal-floor-smoke.mjs @@ -0,0 +1,120 @@ +import { build } from 'esbuild' +import { randomUUID } from 'node:crypto' +import { createRequire } from 'node:module' +import { existsSync, mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join, resolve } from 'node:path' +import { fileURLToPath, pathToFileURL } from 'node:url' + +const ROOT = resolve(fileURLToPath(new URL('../..', import.meta.url))) +const PACKAGES = + 'ca-certificates libasound2 libatspi2.0-0 libdrm2 libgbm1 libgtk-3-0 libnss3 libx11-xcb1 libxkbcommon0 libxss1' +const CHILD = `require(process.argv[1]).qualifyPackagedTerminal(process.argv[2]).then(report => console.log(JSON.stringify(report)), error => { console.error(error); process.exitCode = 1 })` + +export async function buildPackagedTerminalFloorFixture(outputDirectory) { + const outfile = join(outputDirectory, 'packaged-terminal-floor.cjs') + await build({ + entryPoints: [join(ROOT, 'src/main/daemon/linux-packaged-terminal-floor-fixture.ts')], + outfile, + bundle: true, + platform: 'node', + format: 'cjs', + target: 'node24', + external: ['electron'], + logLevel: 'silent' + }) + return outfile +} + +export function packagedTerminalFloorDockerArgs({ appDirectory, fixtureDirectory, containerName }) { + for (const path of [appDirectory, fixtureDirectory]) { + if (path.includes(',')) { + throw new Error('Docker bind paths must not contain commas') + } + } + return [ + 'run', + '--rm', + ...(containerName ? ['--name', containerName] : []), + '--network', + 'bridge', + '--mount', + `type=bind,src=${resolve(appDirectory)},dst=/artifact,readonly`, + '--mount', + `type=bind,src=${resolve(fixtureDirectory)},dst=/qualification,readonly`, + '--env', + 'ORCA_BACKGROUND_LAUNCH=1', + '--env', + 'ELECTRON_RUN_AS_NODE=1', + 'ubuntu:20.04', + '/bin/bash', + '-ec', + `export DEBIAN_FRONTEND=noninteractive; apt-get update -qq; apt-get install -y -qq ${PACKAGES} >/dev/null; exec /artifact/orca-ide -e "$1" /qualification/packaged-terminal-floor.cjs /artifact/resources`, + 'terminal-floor', + CHILD + ] +} + +export async function runPackagedTerminalFloorSmoke(appDirectory) { + if (process.platform !== 'linux') { + throw new Error('Packaged Linux floor smoke requires Linux') + } + const app = resolve(appDirectory) + for (const path of [ + 'orca-ide', + 'resources/cli-runtime/bun-runtime', + 'resources/terminal-daemon/daemon-entry.js' + ]) { + if (!existsSync(join(app, path))) { + throw new Error(`Missing packaged artifact: ${join(app, path)}`) + } + } + const fixtureDirectory = mkdtempSync(join(tmpdir(), 'orca-terminal-floor-runner-')) + try { + const fixture = await buildPackagedTerminalFloorFixture(fixtureDirectory) + const { runProcess } = createRequire(import.meta.url)(fixture) + const containerName = `orca-terminal-floor-${randomUUID()}` + let result + let cleanup + try { + result = await runProcess({ + program: 'docker', + args: packagedTerminalFloorDockerArgs({ + appDirectory: app, + fixtureDirectory, + containerName + }), + timeoutMs: 300_000, + maxOutputBytes: 8 * 1024 * 1024 + }) + } finally { + // A terminated Docker client does not stop its container. + cleanup = await runProcess({ + program: 'docker', + args: ['rm', '--force', containerName], + timeoutMs: 10_000, + maxOutputBytes: 8192 + }) + } + if (cleanup.timedOut || (cleanup.code !== 0 && !cleanup.stderr.includes('No such container'))) { + throw new Error( + `Could not remove qualification container ${containerName}: ${cleanup.stderr}` + ) + } + if (result.timedOut || result.outputTruncated || result.code !== 0) { + throw new Error( + `Packaged terminal floor smoke failed: ${result.stderr || `exit=${result.code} timeout=${result.timedOut}`}` + ) + } + process.stdout.write(result.stdout) + } finally { + rmSync(fixtureDirectory, { recursive: true, force: true }) + } +} + +if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) { + if (process.argv[2] !== '--app-dir' || !process.argv[3]) { + throw new Error('Usage: run-linux-packaged-terminal-floor-smoke.mjs --app-dir ') + } + await runPackagedTerminalFloorSmoke(process.argv[3]) +} diff --git a/config/scripts/run-linux-packaged-terminal-floor-smoke.test.mjs b/config/scripts/run-linux-packaged-terminal-floor-smoke.test.mjs new file mode 100644 index 000000000000..db955f1c6ad9 --- /dev/null +++ b/config/scripts/run-linux-packaged-terminal-floor-smoke.test.mjs @@ -0,0 +1,97 @@ +import { describe, expect, it } from 'vitest' +import { readFileSync } from 'node:fs' +import { parse } from 'yaml' +import { packagedTerminalFloorDockerArgs } from './run-linux-packaged-terminal-floor-smoke.mjs' + +describe('packaged terminal Linux floor qualification', () => { + it('isolates exact packaged artifacts from writable fixture state on Ubuntu 20.04', () => { + const args = packagedTerminalFloorDockerArgs({ + appDirectory: '/tmp/app with spaces', + fixtureDirectory: '/tmp/gate' + }) + expect(args).toContain('type=bind,src=/tmp/app with spaces,dst=/artifact,readonly') + expect(args).toContain('type=bind,src=/tmp/gate,dst=/qualification,readonly') + expect(args).toContain('ubuntu:20.04') + expect(args).toContain('ELECTRON_RUN_AS_NODE=1') + expect(args).toContain('ORCA_BACKGROUND_LAUNCH=1') + expect(args.at(-3)).toContain('/artifact/orca-ide') + expect(args.at(-3)).toContain('/artifact/resources') + }) + it('does not splice caller paths into a shell command', () => { + const args = packagedTerminalFloorDockerArgs({ + appDirectory: '/tmp/$(unexpected)', + fixtureDirectory: '/tmp/gate' + }) + expect(args.at(-3)).not.toContain('unexpected') + expect(args).toContain('type=bind,src=/tmp/$(unexpected),dst=/artifact,readonly') + expect(() => + packagedTerminalFloorDockerArgs({ + appDirectory: '/tmp/app,readonly', + fixtureDirectory: '/tmp/gate' + }) + ).toThrow('commas') + }) + it('offers nonpublishing native Linux package qualification only by explicit dispatch', () => { + const workflow = parse(readFileSync('.github/workflows/bun-profile-tests.yml', 'utf8')) + expect(workflow.on.workflow_dispatch.inputs.linux_package).toMatchObject({ + type: 'boolean', + default: false + }) + const job = workflow.jobs.linux_package + expect(job.if).toContain("github.event_name == 'workflow_dispatch'") + expect(job.if).toContain('inputs.linux_package') + expect(workflow.permissions).toEqual({ contents: 'read' }) + expect(job.permissions).toBeUndefined() + expect(job.environment).toBeUndefined() + expect(JSON.stringify(job)).not.toContain('secrets.') + expect(job.env.ORCA_BACKGROUND_LAUNCH).toBe('1') + expect(job.strategy.matrix.include).toEqual([ + { os: 'ubuntu-22.04', arch: 'x64', unpacked_dir: 'dist/linux-unpacked' }, + { os: 'ubuntu-24.04-arm', arch: 'arm64', unpacked_dir: 'dist/linux-arm64-unpacked' } + ]) + const steps = job.steps + const packageStep = steps.find((step) => step.name === 'Package without release publication') + expect(packageStep.run).toContain('--linux AppImage deb rpm --"$PACKAGE_ARCH" --publish never') + expect(packageStep.run).not.toContain('--publish always') + const gate = steps.find( + (step) => step.name === 'Qualify packaged terminal on Ubuntu 20.04 userspace' + ) + expect(gate.run).toContain('set -euo pipefail') + expect(gate.run).toContain( + 'run-linux-packaged-terminal-floor-smoke.mjs --app-dir "$PACKAGE_DIRECTORY"' + ) + expect(gate.run).toContain('floor-smoke.log') + const evidence = steps.find((step) => step.name === 'Preserve exact package and provenance') + expect(evidence.run).toContain('git rev-parse HEAD') + expect(evidence.run).toContain('payload-sha256.txt') + expect(evidence.run).toContain('unpacked-$PACKAGE_ARCH.tar.gz') + expect(steps.indexOf(evidence)).toBeGreaterThan(steps.indexOf(packageStep)) + expect(steps.indexOf(evidence)).toBeLessThan(steps.indexOf(gate)) + const upload = steps.find((step) => step.uses?.startsWith('actions/upload-artifact')) + expect(upload.if).toBe('${{ always() }}') + expect(upload.with.path).toContain('linux-package-evidence/') + expect(upload.with.path).toContain('dist/*.AppImage') + expect(steps.indexOf(upload)).toBeGreaterThan(steps.indexOf(gate)) + }) + it('runs the exact packaged terminal on both release architectures before upload', () => { + const workflow = parse(readFileSync('.github/workflows/release-cut.yml', 'utf8')) + const steps = workflow.jobs.build.steps + const gate = steps.find( + (step) => step.name === 'Qualify packaged Bun terminal on the Linux floor' + ) + expect(gate.if).toContain("matrix.platform == 'linux-x64'") + expect(gate.if).toContain("matrix.platform == 'linux-arm64'") + expect(gate.with.command).toContain('run-linux-packaged-terminal-floor-smoke.mjs') + expect(gate.with.command).toContain('${{ matrix.unpacked_dir }}') + expect(steps.indexOf(gate)).toBeLessThan( + steps.findIndex((step) => step.uses?.startsWith('actions/upload-artifact')) + ) + expect( + packagedTerminalFloorDockerArgs({ + appDirectory: '/app', + fixtureDirectory: '/gate', + containerName: 'owned-probe' + }) + ).toContain('owned-probe') + }) +}) diff --git a/config/scripts/script-module-dependencies.test.mjs b/config/scripts/script-module-dependencies.test.mjs index 68482954ee3e..117d00a4fe2f 100644 --- a/config/scripts/script-module-dependencies.test.mjs +++ b/config/scripts/script-module-dependencies.test.mjs @@ -8,7 +8,6 @@ import { } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' -import { fileURLToPath } from 'node:url' import { describe, expect, it } from 'vitest' import { copyScriptWithLocalModules } from './script-module-dependencies.mjs' @@ -108,18 +107,4 @@ describe('copyScriptWithLocalModules', () => { copyScriptWithLocalModules(join(sourceDir, 'entry.mjs'), destinationDir) expect(existsSync(join(destinationDir, 'entry.mjs'))).toBe(true) }) - - // The real tree this stages: the packaged-addon gate reaches its PE reader by - // require, so a walker that missed it would break every rebuild fixture. - it('stages the node-pty job-ownership gate with everything it requires', () => { - const destinationDir = join(mkdtempSync(join(fixtureDir, 'dest-')), 'scripts') - copyScriptWithLocalModules( - fileURLToPath(new URL('./node-pty-job-ownership.cjs', import.meta.url)), - destinationDir - ) - expect(readdirSync(destinationDir).sort()).toEqual([ - 'node-pty-job-ownership.cjs', - 'windows-pe-machine.cjs' - ]) - }) }) diff --git a/config/scripts/skill-sharing-release-workflow.test.mjs b/config/scripts/skill-sharing-release-workflow.test.mjs index aa823b20bdd6..43a843393b68 100644 --- a/config/scripts/skill-sharing-release-workflow.test.mjs +++ b/config/scripts/skill-sharing-release-workflow.test.mjs @@ -129,7 +129,7 @@ describe('skill-sharing release workflow', () => { it('loads each exact Linux package on the glibc 2.31 floor', () => { const build = workflow.jobs.build - const smoke = stepNamed(build, 'Load packaged node-pty on the Linux floor') + const smoke = stepNamed(build, 'Qualify packaged Bun terminal on the Linux floor') const linuxEntries = build.strategy.matrix.include.filter(({ platform }) => platform.startsWith('linux-') ) @@ -142,7 +142,7 @@ describe('skill-sharing release workflow', () => { }) ]) expect(smoke.if).toContain("matrix.platform == 'linux-x64'") - expect(smoke.with.command).toContain('run-linux-packaged-node-pty-floor-smoke.mjs') + expect(smoke.with.command).toContain('run-linux-packaged-terminal-floor-smoke.mjs') expect(smoke.with.command).toContain('${{ matrix.unpacked_dir }}') }) }) diff --git a/config/scripts/verify-linux-glibc-floor.cjs b/config/scripts/verify-linux-glibc-floor.cjs index ebfdb2b88161..525df98f18a9 100644 --- a/config/scripts/verify-linux-glibc-floor.cjs +++ b/config/scripts/verify-linux-glibc-floor.cjs @@ -139,8 +139,7 @@ function findFloorViolations(needs, filePath = '') { // On stock Ubuntu 20.04 (glibc 2.31) these symbols live ONLY in these DSOs — // glibc kept openpty/forkpty in libutil until the 2.34 merge. A binary that // imports them must keep the DSO in DT_NEEDED or they will not resolve on the -// floor. This guards config/patches/node-pty@1.1.0.patch's forced -// `-l:libutil.so.1`: if a toolchain change ever dropped that ldflag, the pinned +// floor. Keep `-l:libutil.so.1` when pinning these symbols: without it, the pinned // openpty@GLIBC_2.2.5 would still resolve from libc's compat alias at build time // (so the version-floor check passes) yet fail to load on 20.04. libpthread // (pthread_sigmask) is intentionally omitted — the Node/Electron host always @@ -488,7 +487,7 @@ function verifyLinuxGlibcFloor(rootDir, options = {}) { `[verify-linux-glibc-floor] ${offenders.length} bundled native binar${offenders.length === 1 ? 'y' : 'ies'} ` + `will not load on ${FLOOR_LABEL}, so the app will crash on startup there:\n${detail}\n` + 'See docs/reference/linux-glibc-compatibility.md — rebuild the offending module against an older ' + - 'toolchain or pin the relocated symbols (as config/patches/node-pty@1.1.0.patch does).' + 'toolchain, or pin relocated symbols and retain their provider libraries in DT_NEEDED.' ) } diff --git a/config/scripts/verify-packaged-daemon-entry.cjs b/config/scripts/verify-packaged-daemon-entry.cjs index 8f5e802825c3..d6d5c3d8c031 100644 --- a/config/scripts/verify-packaged-daemon-entry.cjs +++ b/config/scripts/verify-packaged-daemon-entry.cjs @@ -2,39 +2,38 @@ const { existsSync } = require('node:fs') const { spawnSync } = require('node:child_process') const { join } = require('node:path') -// Why: `asarUnpack` in config/electron-builder.config.cjs lists -// out/main/daemon-entry.js on every platform, and the packaged daemon fork -// (src/main/daemon/daemon-init.ts) resolves exactly this unpacked path. A -// missing entry means the package layout regressed, so the check throws -// instead of skipping — a silent skip false-passed exactly the layout bug -// this gate exists to catch. -function assertPackagedDaemonEntryExists(resourcesDir) { - const entryPath = join(resourcesDir, 'app.asar.unpacked', 'out', 'main', 'daemon-entry.js') +function assertPackagedDaemonEntryExists(resourcesDir, platform = process.platform) { + const entryPath = join(resourcesDir, 'terminal-daemon', 'daemon-entry.js') if (!existsSync(entryPath)) { throw new Error( - `[verify-packaged-daemon-entry] missing unpacked daemon entry at ${entryPath} — ` + - `asarUnpack expects out/main/daemon-entry.js on every platform, so the packaged ` + - `daemon cannot be forked from this layout` + `[verify-packaged-daemon-entry] missing terminal daemon entry at ${entryPath}; run build:terminal-daemon` + ) + } + if ( + platform === 'win32' && + !existsSync(join(resourcesDir, 'terminal-daemon', 'windows-bun-pty-gate-entry.js')) + ) { + throw new Error( + '[verify-packaged-daemon-entry] missing Windows Bun PTY gate; run build:terminal-daemon' ) } return entryPath } -// Why: v1.4.129-rc.1 shipped a terminal daemon that could not load (an electron -// `require` leaked into its bundle) while every build check passed. This boots -// the PACKAGED daemon-entry under plain Node against the asar-unpacked layout, -// so a bundling / asar-unpack regression fails packaging instead of reaching -// users. Module-load proof only: with no args the entry must reach argv parsing -// and print its "Usage: daemon-entry" error — a MODULE_NOT_FOUND or a missing -// usage line means the packaged graph does not load and the build must fail. -// -// resourcesDir is the packaged Resources dir (Contents/Resources on macOS, -// /resources elsewhere). execPath defaults to the packaging Node. +// Check the real resource layout with its shipped runtime before signing. function verifyPackagedDaemonEntryBoots(resourcesDir, options = {}) { - const execPath = options.execPath || process.execPath - const entryPath = assertPackagedDaemonEntryExists(resourcesDir) + const platform = options.platform ?? process.platform + const execPath = + options.execPath ?? + join(resourcesDir, 'cli-runtime', platform === 'win32' ? 'bun-runtime.exe' : 'bun-runtime') + const entryPath = assertPackagedDaemonEntryExists(resourcesDir, platform) - const result = spawnSync(execPath, [entryPath], { encoding: 'utf8', timeout: 10_000 }) + const result = spawnSync(execPath, [entryPath], { + encoding: 'utf8', + timeout: 10_000, + windowsHide: true, + env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1' } + }) if (result.error) { throw new Error( `[verify-packaged-daemon-entry] could not launch daemon-entry.js: ${result.error.message}` @@ -43,7 +42,7 @@ function verifyPackagedDaemonEntryBoots(resourcesDir, options = {}) { const stderr = result.stderr || '' if (/Cannot find module|MODULE_NOT_FOUND/.test(stderr)) { throw new Error( - `[verify-packaged-daemon-entry] packaged daemon-entry.js failed to load under plain Node:\n${stderr}` + `[verify-packaged-daemon-entry] packaged daemon-entry.js failed to load under bundled Bun:\n${stderr}` ) } if (!stderr.includes('Usage: daemon-entry')) { @@ -52,7 +51,7 @@ function verifyPackagedDaemonEntryBoots(resourcesDir, options = {}) { `(expected the "Usage: daemon-entry" error). stderr:\n${stderr}` ) } - console.log('[verify-packaged-daemon-entry] OK — packaged daemon-entry loads under plain Node') + console.log('[verify-packaged-daemon-entry] OK — packaged daemon-entry loads under bundled Bun') } module.exports = { assertPackagedDaemonEntryExists, verifyPackagedDaemonEntryBoots } diff --git a/config/scripts/verify-packaged-daemon-entry.test.mjs b/config/scripts/verify-packaged-daemon-entry.test.mjs index 8705953ba619..d105f1fc0621 100644 --- a/config/scripts/verify-packaged-daemon-entry.test.mjs +++ b/config/scripts/verify-packaged-daemon-entry.test.mjs @@ -7,7 +7,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest' const require = createRequire(import.meta.url) const { assertPackagedDaemonEntryExists, - verifyPackagedDaemonEntryBoots + verifyPackagedDaemonEntryBoots: verifyBoot } = require('./verify-packaged-daemon-entry.cjs') describe('verify-packaged-daemon-entry', () => { @@ -21,8 +21,11 @@ describe('verify-packaged-daemon-entry', () => { rmSync(resourcesDir, { recursive: true, force: true }) }) + const verifyPackagedDaemonEntryBoots = (directory) => + verifyBoot(directory, { execPath: process.execPath, platform: 'linux' }) + function writePackagedEntry(source) { - const entryDir = join(resourcesDir, 'app.asar.unpacked', 'out', 'main') + const entryDir = join(resourcesDir, 'terminal-daemon') mkdirSync(entryDir, { recursive: true }) writeFileSync(join(entryDir, 'daemon-entry.js'), source) } @@ -30,12 +33,26 @@ describe('verify-packaged-daemon-entry', () => { // Why: a silent skip on a missing entry false-passed exactly the packaged // layout regression this gate exists to catch (rc.1 daemon-load incident). it('throws when the unpacked daemon entry is missing', () => { - expect(() => assertPackagedDaemonEntryExists(resourcesDir)).toThrow( - /missing unpacked daemon entry/ + expect(() => assertPackagedDaemonEntryExists(resourcesDir, 'linux')).toThrow( + /missing terminal daemon entry/ ) expect(() => verifyPackagedDaemonEntryBoots(resourcesDir)).toThrow( - /missing unpacked daemon entry/ + /missing terminal daemon entry/ + ) + }) + + it('requires the Windows gate even when the daemon entry exists', () => { + writePackagedEntry('process.exit(0)') + expect(() => assertPackagedDaemonEntryExists(resourcesDir, 'win32')).toThrow( + /missing Windows Bun PTY gate/ ) + writeFileSync(join(resourcesDir, 'terminal-daemon', 'windows-bun-pty-gate-entry.js'), '') + expect(() => assertPackagedDaemonEntryExists(resourcesDir, 'win32')).not.toThrow() + }) + + it('fails when the packaged Bun runtime is absent', () => { + writePackagedEntry('console.error("Usage: daemon-entry"); process.exit(1)') + expect(() => verifyBoot(resourcesDir, { platform: 'linux' })).toThrow(/could not launch/) }) it('passes when the packaged entry loads and reaches argv parsing', () => { @@ -46,7 +63,7 @@ describe('verify-packaged-daemon-entry', () => { it('fails when the packaged entry cannot resolve its module graph', () => { writePackagedEntry('require("orca-module-that-does-not-exist")\n') expect(() => verifyPackagedDaemonEntryBoots(resourcesDir)).toThrow( - /failed to load under plain Node/ + /failed to load under bundled Bun/ ) }) diff --git a/config/scripts/verify-packaged-node-pty-job-ownership.cjs b/config/scripts/verify-packaged-node-pty-job-ownership.cjs deleted file mode 100644 index 2d746d236612..000000000000 --- a/config/scripts/verify-packaged-node-pty-job-ownership.cjs +++ /dev/null @@ -1,199 +0,0 @@ -const { existsSync } = require('node:fs') -const { createRequire } = require('node:module') -const { join } = require('node:path') -const { - assertNodePtyJobOwnership, - conptyDeniesCygwinBreakaway, - nodePtyAddonPath, - staleConptySourceBuildError -} = require('./node-pty-job-ownership.cjs') -const { normalizeNodePtyWindowsArch } = require('../packaged-runtime-node-modules.cjs') -const { PE_MACHINE, describePeMachine, readPeMachine } = require('./windows-pe-machine.cjs') - -/** - * Every conpty.node the packaged tree can hand `loadNativeModule`, in its order. - * - * Why the order matters: the loader swallows each require failure and falls - * through, so a wrong-arch or otherwise unloadable build hands the pane to the - * next candidate. First loadable wins, and the published prebuild is always the - * last one standing. - */ -function packagedConptyCandidates(resourcesDir, targetArch) { - const nodePtyDir = join(resourcesDir, 'node_modules', 'node-pty') - const layouts = [ - { segments: ['build', 'Release'], prebuilt: false }, - { segments: ['build', 'Debug'], prebuilt: false }, - { segments: ['prebuilds', `win32-${targetArch}`], prebuilt: true } - ] - // Each layout is tried relative to node-pty's root, then to lib/, before the - // next layout -- the unbundled then bundled pair node-pty's loader walks. - return layouts.flatMap(({ segments, prebuilt }) => - [nodePtyDir, join(nodePtyDir, 'lib')].map((root) => ({ - path: join(root, ...segments, 'conpty.node'), - prebuilt - })) - ) -} - -function describeCandidates(candidates) { - return candidates - .map((candidate) => `${candidate.path} (${describePeMachine(candidate.machine)})`) - .join(', ') -} - -function loadPackagedConpty(resourcesDir) { - const packagedRequire = createRequire(join(resourcesDir, 'package.json')) - const utilsPath = packagedRequire.resolve('./node_modules/node-pty/lib/utils') - const { loadNativeModule } = packagedRequire(utilsPath) - const native = loadNativeModule('conpty') - return { native, addonPath: nodePtyAddonPath(utilsPath, native, 'conpty') } -} - -function verifyPackagedNodePtyJobOwnership(resourcesDir, options = {}) { - const platform = options.platform ?? process.platform - if (platform !== 'win32') { - return - } - - const { native, addonPath } = (options.loadNative ?? loadPackagedConpty)(resourcesDir) - assertNodePtyJobOwnership({ platform, nativeName: 'conpty', native, addonPath }) - if (!native.dir.replace(/\\/g, '/').includes('build/Release/')) { - throw new Error(`Packaged node-pty resolved to ${native.dir}; expected patched build/Release`) - } - console.log('[verify-packaged-node-pty] OK — packaged ConPTY owns process trees') -} - -/** - * The half of the packaged check that survives a cross-host build. - * - * The export check has to load the addon, so it cannot run when the packaging - * host is not the target platform/arch -- and that skip is how a Windows - * release built elsewhere could ship a node-pty that leaks every MSYS pane - * child out of its job. Reading the binary needs neither. - * - * It resolves the addon the way the loader does rather than reading one path: - * only the PE machine field separates a cross-arch package that built correctly - * from one whose rebuild silently emitted the host's arch, and the first is a - * correct package whose leftover prebuild is never reached. See the table in - * docs/reference/windows-msys-job-breakaway.md. - * - * Nothing loadable is fatal, not skipped: that package has no ConPTY backend, - * which a gate must not shrug at. - */ -function verifyPackagedConptyBreakawayMarker(resourcesDir, targetArch, options = {}) { - // Deliberately no host-platform gate: the caller has already established that - // the *target* is Windows, and gating on the host is the very skip this - // closes. - const architecture = normalizeNodePtyWindowsArch(targetArch) - const candidates = packagedConptyCandidates(resourcesDir, architecture) - const exists = options.exists ?? existsSync - const present = candidates.filter((candidate) => exists(candidate.path)) - if (present.length === 0) { - throw new Error( - [ - `Packaged node-pty for win32-${architecture} has no conpty.node on any path its loader`, - `tries (${candidates.map((c) => c.path).join(', ')}), so the packaged app has no`, - 'ConPTY backend at all.', - 'Nothing here can be checked for the Cygwin/MSYS job-breakaway denial, and a gate that', - 'cannot see its subject refuses rather than assume.' - ].join(' ') - ) - } - // Read once: the same header answers "which one loads" and "what did we find". - const inspected = present.map((candidate) => ({ - ...candidate, - machine: readPeMachine(candidate.path) - })) - const loaded = inspected.find((candidate) => candidate.machine === PE_MACHINE[architecture]) - if (!loaded) { - throw new Error( - [ - `Packaged node-pty for win32-${architecture} has conpty.node at`, - `${describeCandidates(inspected)},`, - 'and the app can load none of them: a Windows process only loads a PE of its own', - `machine, which for win32-${architecture} is`, - `0x${PE_MACHINE[architecture].toString(16)}.`, - 'Rebuild node-pty for the target architecture and repackage.' - ].join(' ') - ) - } - const addonPath = loaded.path - if (conptyDeniesCygwinBreakaway(addonPath)) { - console.log( - `[verify-packaged-node-pty] OK — win32-${architecture} loads ${addonPath}, which denies ` + - 'MSYS job breakaway' - ) - return - } - if (!loaded.prebuilt) { - throw staleConptySourceBuildError(addonPath) - } - // Past here the app falls back to the published prebuild, which never carries - // the patch. Why it fell back decides the remedy, and the three are different - // enough that naming the wrong one wastes the reader's build. - const unusableSourceBuilds = inspected.filter((candidate) => !candidate.prebuilt) - if (unusableSourceBuilds.some((candidate) => candidate.machine === null)) { - throw new Error( - [ - `Packaged node-pty for win32-${architecture} falls back to ${addonPath}, which predates`, - 'the Cygwin/MSYS job-breakaway denial, because the source build beside it is not a PE', - `image at all: ${describeCandidates(unusableSourceBuilds)}.`, - 'A truncated, empty or quarantined build artifact looks like this. Rebuild node-pty and', - 'repackage. See docs/reference/windows-msys-job-breakaway.md.' - ].join(' ') - ) - } - if (unusableSourceBuilds.length > 0) { - throw new Error( - [ - `Packaged node-pty for win32-${architecture} falls back to ${addonPath}, which predates`, - 'the Cygwin/MSYS job-breakaway denial, because the source build beside it is the wrong', - `architecture: ${describeCandidates(unusableSourceBuilds)}.`, - 'A cross-arch rebuild that did not honour --arch looks exactly like this. Re-run', - `config/scripts/rebuild-native-deps.mjs --platform=win32 --arch=${architecture},`, - 'confirm it emitted a conpty.node of that machine, and repackage.', - 'See docs/reference/windows-msys-job-breakaway.md.' - ].join(' ') - ) - } - throw new Error( - [ - `Packaged node-pty for win32-${architecture} loads ${addonPath}, the published prebuilt`, - 'fallback, which predates the Cygwin/MSYS job-breakaway denial: its per-PTY job still', - 'carries JOB_OBJECT_LIMIT_BREAKAWAY_OK, so every Git Bash pane child is created outside', - 'the job and survives terminatePtyJob.', - 'It is here because this package holds no node-pty source build at all for', - 'prunePackagedNodePty to have replaced it with, and only a host that can build node-pty', - `for win32-${architecture} produces one.`, - `If this IS a Windows ${architecture} host, the rebuild did not leave one -- check the`, - 'beforeBuild output above. Otherwise package this Windows slice on a host that can.', - 'See docs/reference/windows-msys-job-breakaway.md.' - ].join(' ') - ) -} - -/** - * The whole Windows verdict for one packaged slice. - * - * Both halves live here rather than in the afterPack hook so that "the marker - * sweep runs even when the export check cannot" is a tested claim instead of - * the shape of an if/else somebody could re-nest. - */ -function verifyPackagedWindowsNodePty(resourcesDir, targetArch, options = {}) { - ;(options.verifyMarker ?? verifyPackagedConptyBreakawayMarker)(resourcesDir, targetArch) - const hostPlatform = options.hostPlatform ?? process.platform - if (hostPlatform !== 'win32' || !options.canExecuteTargetArch) { - console.log( - '[verify-packaged-node-pty] skipped the export check on a cross-platform or cross-arch package' - ) - return - } - ;(options.verifyExports ?? verifyPackagedNodePtyJobOwnership)(resourcesDir) -} - -module.exports = { - packagedConptyCandidates, - verifyPackagedConptyBreakawayMarker, - verifyPackagedNodePtyJobOwnership, - verifyPackagedWindowsNodePty -} diff --git a/config/scripts/verify-packaged-node-pty-job-ownership.test.mjs b/config/scripts/verify-packaged-node-pty-job-ownership.test.mjs deleted file mode 100644 index 9d2317c8a339..000000000000 --- a/config/scripts/verify-packaged-node-pty-job-ownership.test.mjs +++ /dev/null @@ -1,430 +0,0 @@ -import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs' -import { createRequire } from 'node:module' -import { tmpdir } from 'node:os' -import { dirname, join } from 'node:path' -import { describe, expect, it, vi } from 'vitest' -import { peImage } from './windows-pe-image-fixture.mjs' - -const require = createRequire(import.meta.url) -const { - packagedConptyCandidates, - verifyPackagedConptyBreakawayMarker, - verifyPackagedNodePtyJobOwnership, - verifyPackagedWindowsNodePty -} = require('./verify-packaged-node-pty-job-ownership.cjs') -const { CYGWIN_BREAKAWAY_MARKER } = require('./node-pty-job-ownership.cjs') - -const fixtureDir = mkdtempSync(join(tmpdir(), 'packaged-node-pty-job-')) -const ELECTRON_BUILDER_CONFIG = readFileSync( - new URL('../electron-builder.config.cjs', import.meta.url), - 'utf8' -) - -/** A real enough addon: a machine field the arch check reads, and the marker. */ -function conptyImage({ arch = 'x64', cygwinBreakawayDenied = true } = {}) { - return Buffer.concat([ - peImage({ arch }), - cygwinBreakawayDenied ? CYGWIN_BREAKAWAY_MARKER : Buffer.alloc(0) - ]) -} - -function writeAddon(name, options) { - const path = join(fixtureDir, name) - writeFileSync(path, conptyImage(options)) - return path -} - -/** A packaged resources tree carrying exactly the conpty.node files named. */ -function packagedResources(addons) { - const resourcesDir = mkdtempSync(join(fixtureDir, 'resources-')) - const nodePtyDir = join(resourcesDir, 'node_modules', 'node-pty') - for (const [relativePath, options] of Object.entries(addons)) { - const addonPath = join(nodePtyDir, ...relativePath.split('/')) - mkdirSync(dirname(addonPath), { recursive: true }) - writeFileSync(addonPath, conptyImage(options)) - } - return resourcesDir -} - -const CURRENT_ADDON = writeAddon('current.node', { cygwinBreakawayDenied: true }) -const PRE_MSYS_ADDON = writeAddon('pre-msys.node', { cygwinBreakawayDenied: false }) - -const PATCHED = { - dir: '../build/Release/', - module: { - listJobProcessIds: () => [], - terminateJob: () => true, - assignCurrentProcessToJob: () => true - } -} - -const packaged = (native, addonPath = CURRENT_ADDON) => ({ - platform: 'win32', - loadNative: () => ({ native, addonPath }) -}) - -describe('verifyPackagedNodePtyJobOwnership', () => { - it('accepts the packaged patched ConPTY binding', () => { - expect(() => verifyPackagedNodePtyJobOwnership('resources', packaged(PATCHED))).not.toThrow() - }) - - it('rejects a packaged upstream prebuild', () => { - expect(() => - verifyPackagedNodePtyJobOwnership( - 'resources', - packaged({ dir: '../prebuilds/win32-x64/', module: {} }) - ) - ).toThrow(/missing listJobProcessIds, terminateJob, assignCurrentProcessToJob/) - }) - - // A release built against a stale native cache ships the MSYS orphan bug - // while exporting every job function, so packaging has to read the binary. - it('rejects a packaged build that predates the Cygwin/MSYS breakaway denial', () => { - expect(() => - verifyPackagedNodePtyJobOwnership('resources', packaged(PATCHED, PRE_MSYS_ADDON)) - ).toThrow(/predates the Cygwin\/MSYS job-breakaway denial/) - }) - - it('requires the patched source-build directory', () => { - expect(() => - verifyPackagedNodePtyJobOwnership( - 'resources', - packaged({ ...PATCHED, dir: '../prebuilds/win32-x64/' }) - ) - ).toThrow(/expected patched build\/Release/) - }) - - it('does not load Windows natives for other targets', () => { - const loadNative = vi.fn() - verifyPackagedNodePtyJobOwnership('resources', { platform: 'linux', loadNative }) - expect(loadNative).not.toHaveBeenCalled() - }) -}) - -describe('packagedConptyCandidates', () => { - // Pinned because the gate resolves the addon by walking this list in order: - // a wrong order blesses a binary the app would never reach. - it('walks the paths node-pty tries, in node-pty order', () => { - expect(packagedConptyCandidates('RES', 'arm64').map((candidate) => candidate.path)).toEqual([ - join('RES', 'node_modules', 'node-pty', 'build', 'Release', 'conpty.node'), - join('RES', 'node_modules', 'node-pty', 'lib', 'build', 'Release', 'conpty.node'), - join('RES', 'node_modules', 'node-pty', 'build', 'Debug', 'conpty.node'), - join('RES', 'node_modules', 'node-pty', 'lib', 'build', 'Debug', 'conpty.node'), - join('RES', 'node_modules', 'node-pty', 'prebuilds', 'win32-arm64', 'conpty.node'), - join('RES', 'node_modules', 'node-pty', 'lib', 'prebuilds', 'win32-arm64', 'conpty.node') - ]) - }) - - // Only the published prebuild gets the "no rebuild here can fix this" advice. - it('knows which of them node-pty publishes prebuilt', () => { - expect(packagedConptyCandidates('RES', 'x64').map((candidate) => candidate.prebuilt)).toEqual([ - false, - false, - false, - false, - true, - true - ]) - }) -}) - -describe('verifyPackagedConptyBreakawayMarker', () => { - // The release as built today: prunePackagedNodePty already dropped the - // same-arch prebuild because a patched source build replaced it. - it('passes a package whose only ConPTY load path carries the denial', () => { - const resourcesDir = packagedResources({ 'build/Release/conpty.node': {} }) - expect(() => verifyPackagedConptyBreakawayMarker(resourcesDir, 'x64')).not.toThrow() - }) - - // The cross-host package. No host but Windows can build conpty.node, so there - // is no build/Release for prune to have replaced the prebuild with -- and the - // published prebuild is exactly the binary that leaks every MSYS pane child. - it('fails a cross-host package left holding the published prebuild', () => { - const resourcesDir = packagedResources({ - 'prebuilds/win32-x64/conpty.node': { cygwinBreakawayDenied: false } - }) - expect(() => verifyPackagedConptyBreakawayMarker(resourcesDir, 'x64')).toThrow( - /predates the Cygwin\/MSYS job-breakaway denial/ - ) - }) - - it('tells that package how to fix it, which is not a rebuild it can run', () => { - const resourcesDir = packagedResources({ - 'prebuilds/win32-x64/conpty.node': { cygwinBreakawayDenied: false } - }) - expect(() => verifyPackagedConptyBreakawayMarker(resourcesDir, 'x64')).toThrow( - /this package holds no node-pty source build at all[\s\S]*Otherwise package this Windows slice on a host that can/ - ) - }) - - // The same state reaches this from a capable host too, when the rebuild left - // nothing: telling that packager to change hosts would send them nowhere. - it('does not assume the packaging host is the wrong one', () => { - const resourcesDir = packagedResources({ - 'prebuilds/win32-x64/conpty.node': { cygwinBreakawayDenied: false } - }) - expect(() => verifyPackagedConptyBreakawayMarker(resourcesDir, 'x64')).toThrow( - /If this IS a Windows x64 host, the rebuild did not leave one/ - ) - }) - - // The cross-arch package that worked: beforeBuild rebuilds node-pty for the - // TARGET arch, so build/Release is patched and loadable and the prebuild - // prune left behind is never reached. Failing this would be a false positive - // whose advice -- change hosts -- is both wrong and impossible. - it('passes a cross-arch package whose build/Release really is the target arch', () => { - const resourcesDir = packagedResources({ - 'build/Release/conpty.node': { arch: 'arm64' }, - 'prebuilds/win32-arm64/conpty.node': { arch: 'arm64', cygwinBreakawayDenied: false } - }) - expect(() => verifyPackagedConptyBreakawayMarker(resourcesDir, 'arm64')).not.toThrow() - }) - - // The cross-arch package that silently did not: build/Release is the - // packaging host's own arch, the target cannot load it, and the loader falls - // through to the unpatched prebuild underneath. - it('fails a cross-arch package whose build/Release is the packaging host arch', () => { - const resourcesDir = packagedResources({ - 'build/Release/conpty.node': { arch: 'x64' }, - 'prebuilds/win32-arm64/conpty.node': { arch: 'arm64', cygwinBreakawayDenied: false } - }) - expect(() => verifyPackagedConptyBreakawayMarker(resourcesDir, 'arm64')).toThrow( - /prebuilds[\\/]win32-arm64/ - ) - }) - - it('refuses a package whose every conpty.node is the wrong architecture', () => { - const resourcesDir = packagedResources({ 'build/Release/conpty.node': { arch: 'x64' } }) - expect(() => verifyPackagedConptyBreakawayMarker(resourcesDir, 'arm64')).toThrow( - /the app can load none of them/ - ) - }) - - // Naming the machine it found is what separates a cross-arch build from a - // truncated download, which are the same "cannot load this" to the loader. - it('names what it found rather than guessing why', () => { - const resourcesDir = packagedResources({ 'build/Release/conpty.node': { arch: 'x64' } }) - expect(() => verifyPackagedConptyBreakawayMarker(resourcesDir, 'arm64')).toThrow( - /machine 0x8664[\s\S]*0xaa64/ - ) - }) - - it('calls a candidate that is not a PE image what it is', () => { - const resourcesDir = packagedResources({}) - const addonPath = join( - resourcesDir, - 'node_modules', - 'node-pty', - 'build', - 'Release', - 'conpty.node' - ) - mkdirSync(dirname(addonPath), { recursive: true }) - writeFileSync(addonPath, Buffer.alloc(0x200)) - expect(() => verifyPackagedConptyBreakawayMarker(resourcesDir, 'x64')).toThrow(/not a PE image/) - }) - - // A truncated or quarantined artifact reaches the gate looking exactly like a - // cross-arch build, and "re-run with --arch" is not the command that fixes it. - it('does not blame --arch for a source build that is not a PE image', () => { - const resourcesDir = packagedResources({ - 'prebuilds/win32-x64/conpty.node': { cygwinBreakawayDenied: false } - }) - const addonPath = join( - resourcesDir, - 'node_modules', - 'node-pty', - 'build', - 'Release', - 'conpty.node' - ) - mkdirSync(dirname(addonPath), { recursive: true }) - writeFileSync(addonPath, Buffer.alloc(0x200)) - expect(() => verifyPackagedConptyBreakawayMarker(resourcesDir, 'x64')).toThrow( - /is not a PE image at all[\s\S]*truncated, empty or quarantined/ - ) - }) - - // The remedy for this one is a rebuild, not a different host, and the - // difference is a build somebody has to run twice to find out. - it('blames the wrong-arch source build rather than the host, when there is one', () => { - const resourcesDir = packagedResources({ - 'build/Release/conpty.node': { arch: 'x64' }, - 'prebuilds/win32-arm64/conpty.node': { arch: 'arm64', cygwinBreakawayDenied: false } - }) - expect(() => verifyPackagedConptyBreakawayMarker(resourcesDir, 'arm64')).toThrow( - /the source build beside it is the wrong architecture[\s\S]*machine 0x8664/ - ) - }) - - it('tells that build the command that would fix it', () => { - const resourcesDir = packagedResources({ - 'build/Release/conpty.node': { arch: 'x64' }, - 'prebuilds/win32-arm64/conpty.node': { arch: 'arm64', cygwinBreakawayDenied: false } - }) - expect(() => verifyPackagedConptyBreakawayMarker(resourcesDir, 'arm64')).toThrow( - /rebuild-native-deps\.mjs --platform=win32 --arch=arm64/ - ) - }) - - it('does not tell it to change hosts, which would not help', () => { - const resourcesDir = packagedResources({ - 'build/Release/conpty.node': { arch: 'x64' }, - 'prebuilds/win32-arm64/conpty.node': { arch: 'arm64', cygwinBreakawayDenied: false } - }) - expect(() => verifyPackagedConptyBreakawayMarker(resourcesDir, 'arm64')).toThrow( - /^(?![\s\S]*Package this Windows slice on such a host)[\s\S]*$/ - ) - }) - - it('ignores a prebuild for an arch this slice will never load', () => { - const resourcesDir = packagedResources({ - 'build/Release/conpty.node': {}, - 'prebuilds/win32-arm64/conpty.node': { arch: 'arm64', cygwinBreakawayDenied: false } - }) - expect(() => verifyPackagedConptyBreakawayMarker(resourcesDir, 'x64')).not.toThrow() - }) - - // build/Debug sits between Release and the prebuilds in the load order, and - // nothing prunes it, so it wins whenever Release cannot be loaded. - it('resolves past a Release build the target cannot load', () => { - const resourcesDir = packagedResources({ - 'build/Release/conpty.node': { arch: 'x64' }, - 'build/Debug/conpty.node': { arch: 'arm64', cygwinBreakawayDenied: false } - }) - expect(() => verifyPackagedConptyBreakawayMarker(resourcesDir, 'arm64')).toThrow( - /build[\\/]Debug/ - ) - }) - - // A stale source build is the packaging host's own to rebuild, so it gets the - // advice that actually works rather than the cross-host one. - it('tells a stale source build to rebuild, not to change hosts', () => { - const resourcesDir = packagedResources({ - 'build/Release/conpty.node': { cygwinBreakawayDenied: false } - }) - expect(() => verifyPackagedConptyBreakawayMarker(resourcesDir, 'x64')).toThrow( - /Rebuild node-pty from source/ - ) - }) - - // Nothing to load is not "a layout we do not recognise", it is a package with - // no ConPTY backend, and a gate that cannot see its subject is not a gate. - it('refuses rather than skip a package with no conpty.node at all', () => { - const resourcesDir = packagedResources({}) - expect(() => verifyPackagedConptyBreakawayMarker(resourcesDir, 'x64')).toThrow( - /no conpty\.node on any path its loader tries/ - ) - }) - - it('names every path it looked at when it finds none', () => { - const resourcesDir = packagedResources({}) - expect(() => verifyPackagedConptyBreakawayMarker(resourcesDir, 'x64')).toThrow( - /build[\\/]Release[\s\S]*build[\\/]Debug[\s\S]*prebuilds[\\/]win32-x64/ - ) - }) - - // Present but unreadable is the state that used to pass, so it must not warn. - // The read error itself is the message; the point is that it does not return. - it('fails rather than pass a candidate it cannot read', () => { - const resourcesDir = packagedResources({}) - mkdirSync(join(resourcesDir, 'node_modules', 'node-pty', 'build', 'Release', 'conpty.node'), { - recursive: true - }) - expect(() => verifyPackagedConptyBreakawayMarker(resourcesDir, 'x64')).toThrow() - }) - - it('accepts the electron-builder Arch enum the afterPack hook passes', () => { - const resourcesDir = packagedResources({ - 'prebuilds/win32-arm64/conpty.node': { arch: 'arm64' } - }) - expect(() => verifyPackagedConptyBreakawayMarker(resourcesDir, 3)).not.toThrow() - }) - - it('refuses a target arch no Windows slice ships', () => { - const resourcesDir = packagedResources({ 'build/Release/conpty.node': {} }) - expect(() => verifyPackagedConptyBreakawayMarker(resourcesDir, 'ia32')).toThrow( - /Unsupported packaged node-pty Windows architecture/ - ) - }) - - it('looks where electron-builder actually lands the addon', () => { - const exists = vi.fn().mockReturnValue(false) - expect(() => - verifyPackagedConptyBreakawayMarker(join('out', 'win-unpacked', 'resources'), 'x64', { - exists - }) - ).toThrow() - expect(exists).toHaveBeenCalledWith( - join( - 'out', - 'win-unpacked', - 'resources', - 'node_modules', - 'node-pty', - 'build', - 'Release', - 'conpty.node' - ) - ) - }) -}) - -describe('verifyPackagedWindowsNodePty', () => { - const spies = () => ({ verifyMarker: vi.fn(), verifyExports: vi.fn() }) - - // The bug this replaced: the marker check sat in the else of the host gate, so - // the cross-host package it exists for was the one package it never checked. - it.each([ - ['a cross-platform host', { hostPlatform: 'darwin', canExecuteTargetArch: true }], - ['a cross-arch slice', { hostPlatform: 'win32', canExecuteTargetArch: false }], - ['both', { hostPlatform: 'linux', canExecuteTargetArch: false }], - ['neither', { hostPlatform: 'win32', canExecuteTargetArch: true }] - ])('checks the marker on %s', (_case, host) => { - const { verifyMarker, verifyExports } = spies() - verifyPackagedWindowsNodePty('resources', 'x64', { ...host, verifyMarker, verifyExports }) - expect(verifyMarker).toHaveBeenCalledWith('resources', 'x64') - }) - - it('loads the addon for the export check only where that can work', () => { - const { verifyMarker, verifyExports } = spies() - verifyPackagedWindowsNodePty('resources', 'x64', { - hostPlatform: 'win32', - canExecuteTargetArch: true, - verifyMarker, - verifyExports - }) - expect(verifyExports).toHaveBeenCalledWith('resources') - }) - - it.each([ - ['a cross-platform host', { hostPlatform: 'darwin', canExecuteTargetArch: true }], - ['a cross-arch slice', { hostPlatform: 'win32', canExecuteTargetArch: false }] - ])('skips the export check on %s', (_case, host) => { - const { verifyMarker, verifyExports } = spies() - verifyPackagedWindowsNodePty('resources', 'x64', { ...host, verifyMarker, verifyExports }) - expect(verifyExports).not.toHaveBeenCalled() - }) - - // Swallowing the marker verdict would leave a gate that runs and decides - // nothing, which is the failure mode this whole change is about. - it('lets the marker verdict fail the package', () => { - const verifyMarker = vi.fn(() => { - throw new Error('predates the Cygwin/MSYS job-breakaway denial') - }) - expect(() => - verifyPackagedWindowsNodePty('resources', 'x64', { - hostPlatform: 'win32', - canExecuteTargetArch: true, - verifyMarker, - verifyExports: vi.fn() - }) - ).toThrow(/predates the Cygwin\/MSYS job-breakaway denial/) - }) - - it('is what the afterPack hook calls for a Windows slice', () => { - expect(ELECTRON_BUILDER_CONFIG).toContain( - 'verifyPackagedWindowsNodePty(resourcesDir, context.arch, { canExecuteTargetArch })' - ) - }) -}) diff --git a/config/scripts/win32-test-lane-registration.test.mjs b/config/scripts/win32-test-lane-registration.test.mjs index 26d5951e1a05..7e3644673905 100644 --- a/config/scripts/win32-test-lane-registration.test.mjs +++ b/config/scripts/win32-test-lane-registration.test.mjs @@ -84,7 +84,6 @@ const UNREGISTERED_ON_MAIN = [ // half of the file still runs on ubuntu, the Windows half runs nowhere. 'src/main/antigravity/windows-hook-payload-delivery.test.ts', // `.win32.test.ts` by name yet in neither list -- the plainest instance of the class. - 'src/main/daemon/node-pty-windows-input-error.win32.test.ts', // Same shape as the antigravity file: a win32-only sibling suite that never runs. 'src/main/grok/windows-grok-hook-script.test.ts', // Whole file is `describe.runIf(platform === 'win32')`; runs on no machine. diff --git a/config/scripts/windows-daemon-workspace-close-repro.mjs b/config/scripts/windows-daemon-workspace-close-repro.mjs index 1385ffc17d2e..08ce66a0bd68 100644 --- a/config/scripts/windows-daemon-workspace-close-repro.mjs +++ b/config/scripts/windows-daemon-workspace-close-repro.mjs @@ -5,7 +5,7 @@ * graceful-then-immediate kill pair emitted when Orca closes a workspace. * The daemon PID and witness session must survive every iteration. */ -import { fork } from 'node:child_process' +import { daemonSmokeEntry, spawnDaemonSmoke } from './daemon-smoke-launch.mjs' import { randomUUID } from 'node:crypto' import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs' import { connect } from 'node:net' @@ -13,7 +13,7 @@ import { tmpdir } from 'node:os' import { join, resolve } from 'node:path' const projectDir = resolve(import.meta.dirname, '../..') -const entryPath = join(projectDir, 'out', 'main', 'daemon-entry.js') +const entryPath = daemonSmokeEntry(projectDir) const iterations = Number(process.env.ORCA_WINDOWS_DAEMON_CLOSE_ITERATIONS ?? 25) const requestTimeoutMs = 15_000 @@ -287,22 +287,21 @@ async function main() { return } if (!existsSync(entryPath)) { - throw new Error(`Missing ${entryPath}; run pnpm build:electron-vite first`) + throw new Error(`Missing ${entryPath}; run pnpm build:terminal-daemon first`) } const scratch = mkdtempSync(join(tmpdir(), 'orca-windows-daemon-close-')) const socketPath = `\\\\.\\pipe\\orca-daemon-close-${process.pid}-${randomUUID()}` const tokenPath = join(scratch, 'daemon.token') const daemonLogPath = join(scratch, 'daemon.log') - const child = fork( - entryPath, - ['--socket', socketPath, '--token', tokenPath, '--log-file', daemonLogPath], - { - stdio: ['ignore', 'ignore', 'pipe', 'ipc'], - windowsHide: true, - env: { ...process.env, ORCA_USER_DATA_PATH: scratch } - } - ) + const child = spawnDaemonSmoke(projectDir, scratch, [ + '--socket', + socketPath, + '--token', + tokenPath, + '--log-file', + daemonLogPath + ]) const daemonPid = child.pid let stderr = '' child.stderr?.on('data', (chunk) => { diff --git a/config/scripts/windows-process-tree-creation-time.cjs b/config/scripts/windows-process-tree-creation-time.cjs index 769cd8854349..d265f8e23b89 100644 --- a/config/scripts/windows-process-tree-creation-time.cjs +++ b/config/scripts/windows-process-tree-creation-time.cjs @@ -3,7 +3,7 @@ /** * Prove the compiled addon supports table and synchronous process creation-time reads. * - * Unlike node-pty, this package ships a prebuilt `.node` at the same + * This package ships a prebuilt `.node` at the same * `build/Release/` path node-gyp writes to, so neither a load nor a path check * can tell a stale prebuilt from a source build. pnpm patches the source tree * and leaves that prebuilt in place, which is how `ProcessDataFlag.CreationTime` @@ -11,8 +11,7 @@ * read true and every row came back without `creationTimeMs`. * * `supportedProcessDataFlags` is exported by the patched `addon.cc`, so its - * presence is the binary's own answer. Shared by the Node and Electron probes - * the way `node-pty-job-ownership.cjs` is. + * presence is the binary's own answer, checked by both Node and Electron probes. */ /** `ProcessDataFlags::CREATIONTIME` in src/process.h. */ diff --git a/config/scripts/windows-pty-native-capability-workflow.test.mjs b/config/scripts/windows-pty-native-capability-workflow.test.mjs index e1742a43bb09..116401725ad2 100644 --- a/config/scripts/windows-pty-native-capability-workflow.test.mjs +++ b/config/scripts/windows-pty-native-capability-workflow.test.mjs @@ -56,7 +56,6 @@ describe('packaged Windows PTY native capability routing', () => { expect(cache.with.key).toContain('config/scripts/rebuild-native-deps.mjs') } expect(ensureNativeRuntime).toContain("runPnpm(['exec', 'node-gyp', 'rebuild']") - expect(ensureNativeRuntime).toContain("resolve(moduleDir, 'scripts', 'post-install.js')") expect(build.run).toBe('pnpm run build:release:parallel') expect(build.env.ORCA_REUSE_WINDOWS_CLI_LAUNCHER).toBe('1') expect(prepare.run).toBe('node config/scripts/ensure-native-runtime.mjs --runtime=electron') @@ -66,15 +65,15 @@ describe('packaged Windows PTY native capability routing', () => { expect(verify.run).toContain('"$PACKAGE_WINDOWS"') }) - it('keeps the native probe event-based, scoped, and runnable in packaged Node mode', () => { + it('keeps the native probe event-based, scoped, and runnable under the packaged Bun runtime', () => { const driver = readFileSync('tests/tools/windows-pty-native-capability-smoke/run.mjs', 'utf8') const probe = readFileSync( - 'tests/tools/windows-pty-native-capability-smoke/packaged-node-pty-capability-probe.cjs', + 'tests/tools/windows-pty-native-capability-smoke/packaged-bun-capability-probe.cjs', 'utf8' ) const source = `${driver}\n${probe}` - expect(driver).toContain("ELECTRON_RUN_AS_NODE: '1'") + expect(driver).toContain("'bun-runtime.exe'") expect(source).not.toMatch(/\b(?:sleep|tasklist|taskkill)\b/i) expect(source).not.toContain('maxRetries') expect(source).not.toContain('retryDelay') @@ -83,6 +82,6 @@ describe('packaged Windows PTY native capability routing', () => { expect(probe).toContain("'System32', 'wscript.exe'") expect(probe).toContain('real-orca-detached-launcher.vbs') expect(probe).not.toMatch(/cmd\.exe|start "" \/b/i) - expect(probe).toContain('native.terminateJob(target._pty, target.pid)') + expect(probe).toContain('target.terminateOwnedTree()') }) }) diff --git a/config/scripts/windows-pty-table-stress.cjs b/config/scripts/windows-pty-table-stress.mjs similarity index 68% rename from config/scripts/windows-pty-table-stress.cjs rename to config/scripts/windows-pty-table-stress.mjs index b703e18d19ce..7f51c72a4831 100644 --- a/config/scripts/windows-pty-table-stress.cjs +++ b/config/scripts/windows-pty-table-stress.mjs @@ -1,9 +1,13 @@ -'use strict' +import { createWindowsBunPtyLaunch } from '../../src/main/daemon/pty-subprocess/windows-bun-pty-launch.ts' +import { fileURLToPath } from 'node:url' +import { spawnBunPty } from '../../src/main/daemon/pty-subprocess/bun-pty-process.ts' +import { listPtyJobProcessIds } from '../../src/main/windows/windows-pty-job.ts' +import { createRequire } from 'node:module' +const require = createRequire(import.meta.url) const assert = require('node:assert/strict') const { createHash } = require('node:crypto') const { readFileSync, writeSync } = require('node:fs') -const { dirname, resolve } = require('node:path') function report(phase, details = {}) { writeSync(1, `${JSON.stringify({ phase, hostPid: process.pid, ...details })}\n`) @@ -13,31 +17,39 @@ async function exerciseTable() { assert.equal(process.platform, 'win32', 'This probe requires real Windows ConPTY') const rounds = Number(process.env.ORCA_PTY_TABLE_STRESS_ROUNDS ?? 8) assert.ok(Number.isInteger(rounds) && rounds > 0 && rounds <= 2000) - const pty = require('node-pty') - const nativePath = require.resolve('node-pty/lib/utils') - const loaded = require(nativePath).loadNativeModule('conpty') - const native = loaded.module - const addonPath = resolve(dirname(nativePath), loaded.dir, 'conpty.node') - report('native', { - addonPath, - sha256: createHash('sha256').update(readFileSync(addonPath)).digest('hex'), - node: process.version, + report('runtime', { + runtime: process.execPath, + sha256: createHash('sha256').update(readFileSync(process.execPath)).digest('hex'), + bun: process.versions.bun, rounds }) - // Unlike production's fallback, this crash probe requires a host that permits nested jobs. - const hostJobAssigned = native.assignCurrentProcessToJob() - report('host-job-precondition', { assigned: hostJobAssigned }) - assert.equal(hostJobAssigned, true, 'Probe precondition: host must permit a crash-cleanup job') const spawned = [] function spawn(round, slot) { report('spawn', { round, slot }) - const proc = pty.spawn(process.env.ComSpec || 'cmd.exe', ['/d', '/q'], { - cwd: process.cwd(), - cols: 80, - rows: 24, - useConptyDll: true - }) + const proc = spawnBunPty( + { + file: process.env.ComSpec || 'cmd.exe', + args: ['/d', '/q'], + env: Object.fromEntries( + Object.entries(process.env).filter(([, value]) => value !== undefined) + ), + cwd: process.cwd(), + cols: 80, + rows: 24 + }, + { + createWindowsLaunch: (launch) => + createWindowsBunPtyLaunch(launch, { + workerPath: fileURLToPath( + new URL( + '../../src/main/daemon/pty-subprocess/windows-bun-pty-gate-entry.ts', + import.meta.url + ) + ) + }) + } + ) const record = { proc, output: '', exited: false, closed: false } const marker = `ORCA_PTY_READY_${spawned.length}` let resolveReady @@ -59,7 +71,15 @@ async function exerciseTable() { }) spawned.push(record) // Escaping one letter keeps echoed input from satisfying the output marker. - proc.write(`echo ${marker.replace('READY', 'REA^DY')}\r`) + void proc.waitForSpawn().then( + () => { + proc.write(`echo ${marker.replace('READY', 'REA^DY')}\r`) + }, + (error) => { + record.output += String(error) + resolveReady(false) + } + ) return record } @@ -83,8 +103,11 @@ async function exerciseTable() { clearTimeout(timer) } for (const { proc } of records) { - const members = native.listJobProcessIds(proc._pty, proc.pid) - assert.ok(members?.includes(proc.pid), `Ready shell ${proc.pid} must retain its job`) + const members = listPtyJobProcessIds(proc) + assert.ok( + members?.includes(proc.shellProcessId), + `Ready shell ${proc.pid} must retain its job` + ) } report('ready', { shellPids: records.map(({ proc }) => proc.pid) }) } @@ -110,8 +133,11 @@ async function exerciseTable() { report('resize-clear-list', { round, slot, shellPid: survivor.proc.pid }) survivor.proc.resize(80 + (round % 2), 24) survivor.proc.clear() - const members = native.listJobProcessIds(survivor.proc._pty, survivor.proc.pid) - assert.ok(members?.includes(survivor.proc.pid), 'A live survivor must retain its job') + const members = listPtyJobProcessIds(survivor.proc) + assert.ok( + members?.includes(survivor.proc.shellProcessId), + 'A live survivor must retain its job' + ) } // A ready terminal runs kill/resize/clear immediately instead of deferring them. await waitForReady(slots) diff --git a/config/scripts/windows-signing-workflow-contract.test.mjs b/config/scripts/windows-signing-workflow-contract.test.mjs index c321db8cfd23..9c1ccc9a6dcd 100644 --- a/config/scripts/windows-signing-workflow-contract.test.mjs +++ b/config/scripts/windows-signing-workflow-contract.test.mjs @@ -17,18 +17,18 @@ describe('Windows signing workflow contract', () => { name === 'Install SignPath PowerShell module' ? [index] : [] ) const buildIndex = stepNames.indexOf('Build Windows release artifacts') - const verifyNodePtyIndex = stepNames.indexOf('Verify Windows node-pty ConPTY runtime') + const verifyRuntimeIndex = stepNames.indexOf('Verify Windows Bun and ConPTY runtime') const uploadIndex = stepNames.indexOf('Upload unsigned Windows installer for SignPath') const downloadIndex = stepNames.indexOf('Download signed Windows installer from SignPath') - expect(verifyNodePtyIndex).toBe(buildIndex + 1) - expect(installStepIndexes).toEqual([verifyNodePtyIndex + 1]) + expect(verifyRuntimeIndex).toBe(buildIndex + 1) + expect(installStepIndexes).toEqual([verifyRuntimeIndex + 1]) expect(installStepIndexes[0]).toBeLessThan(uploadIndex) - expect(steps[verifyNodePtyIndex].run).toContain( - 'dist/win-unpacked/resources/node_modules/node-pty/build/Release' - ) - expect(steps[verifyNodePtyIndex].run).toContain('conpty/conpty.dll') + expect(steps[verifyRuntimeIndex].run).toContain('dist/win-unpacked/resources/cli-runtime') + expect(steps[verifyRuntimeIndex].run).toContain('verifyCliRuntimeDirectory') + expect(steps[verifyRuntimeIndex].run).toContain("'win32', 'x64'") + expect(steps[verifyRuntimeIndex].run).toContain('$LASTEXITCODE') const uploadThroughDownloadScript = steps .slice(uploadIndex, downloadIndex + 1) @@ -456,3 +456,32 @@ describe('Windows NSIS uninstaller signing', () => { delete require.cache[require.resolve(configPath)] }) }) + +it.each(['release-cut.yml', 'windows-signing-rehearsal.yml'])( + 'finalizes signed Bun identity before packaging in %s', + (filename) => { + const workflow = readWorkflow(`.github/workflows/${filename}`) + const steps = Object.values(workflow.jobs).flatMap((job) => job.steps ?? []) + const stage = steps.find((step) => step.run?.includes("$list -contains 'Orca.exe'")) + expect(stage.run).toContain("'*windows_process_tree.node'") + expect(stage.run).not.toContain('conpty_console_list.node') + const restoreIndex = steps.findIndex( + (step) => step.name === 'Restore signed inner binaries into unpacked app' + ) + const restore = steps[restoreIndex] + expect(restore.run).toContain( + "node config/scripts/finalize-signed-cli-runtime.mjs $root 'signing-stage'" + ) + expect(restore.run).toContain( + "if ($LASTEXITCODE -ne 0) { throw 'Signed runtime identity finalization failed.' }" + ) + expect(restore.run.indexOf('finalize-signed-cli-runtime.mjs')).toBeGreaterThan( + restore.run.indexOf('if ($failures.Count -gt 0)') + ) + const rebuildIndex = steps.findIndex( + (step) => + step.run?.includes('electron-builder --config') && step.run?.includes('--prepackaged') + ) + expect(rebuildIndex).toBeGreaterThan(restoreIndex) + } +) diff --git a/config/scripts/windows-ssh-attach-console-repro.mjs b/config/scripts/windows-ssh-attach-console-repro.mjs deleted file mode 100644 index ce0b9b1d40aa..000000000000 --- a/config/scripts/windows-ssh-attach-console-repro.mjs +++ /dev/null @@ -1,436 +0,0 @@ -import { spawn, spawnSync } from 'node:child_process' -import { createHash } from 'node:crypto' -import { copyFileSync, cpSync, existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs' -import { createConnection } from 'node:net' -import { join, resolve } from 'node:path' - -const projectDir = resolve(import.meta.dirname, '..', '..') -const relayBuildDir = join(projectDir, 'out', 'relay', 'win32-x64') -const SENTINEL = Buffer.from('ORCA-RELAY v0.1.0 READY\n') -const HEADER_LENGTH = 13 -const REGRESSION_TIMEOUT_MS = 15_000 -const NODE_PTY_PATCH_FILENAME = 'node-pty-1.1.0-console-list-agent-patch.cjs' - -if (process.platform !== 'win32') { - console.log('SKIP: Windows ConPTY regression only runs on Windows.') - process.exit(0) -} - -const options = parseArgs(process.argv.slice(2)) -const nodePath = resolve(options.node ?? process.execPath) -const nodePtyDir = resolve(options.nodePty ?? join(projectDir, 'node_modules', 'node-pty')) -const expectFailure = options.expect === 'attach-console-failure' - -for (const required of [ - nodePath, - nodePtyDir, - join(relayBuildDir, 'relay.js'), - join(relayBuildDir, '.version') -]) { - if (!existsSync(required)) { - throw new Error(`Required regression input is missing: ${required}`) - } -} - -const runDir = mkdtempSync(join(projectDir, '.issue-9586-relay-repro-')) -const relayPath = join(runDir, 'relay.js') -const stdoutLog = join(runDir, 'relay.log') -const stderrLog = join(runDir, 'relay.err.log') -const socketPath = `\\\\.\\pipe\\orca-issue-9586-${process.pid}-${Date.now()}` -let relayPid - -try { - prepareRelayTree(runDir, nodePtyDir) - if (!options.skipRelayPatch) { - applyPackagedNodePtyPatch(nodePath, runDir) - } - relayPid = launchRelayWithoutConsole({ - nodePath, - relayPath, - runDir, - socketPath, - stdoutLog, - stderrLog - }) - await waitForPipe(socketPath, 5_000) - const observation = await exerciseRelayClient({ - nodePath, - relayPath, - runDir, - socketPath, - shell: options.shell - }) - await waitForExit(relayPid, 8_000) - - const relayStdout = readIfPresent(stdoutLog) - const relayStderr = readIfPresent(stderrLog) - const attachConsoleFailed = relayStderr.includes('Error: AttachConsole failed') - const installedNodePtyDir = join(runDir, 'node_modules', 'node-pty') - const agentPath = join(installedNodePtyDir, 'lib', 'conpty_console_list_agent.js') - const nativeBindingPath = join( - installedNodePtyDir, - 'prebuilds', - `${process.platform}-${process.arch}`, - 'conpty.node' - ) - const summary = { - node: observation.nodeVersion, - nodePty: JSON.parse(readFileSync(join(nodePtyDir, 'package.json'), 'utf8')).version, - shell: options.shell, - relayPid, - handshake: observation.handshake, - ptySpawn: observation.ptySpawn, - ptyShutdown: observation.ptyShutdown, - relayAliveAfterPtyShutdown: observation.relayAliveAfterPtyShutdown, - bridgeExit: observation.bridgeExit, - relayPatchApplied: !options.skipRelayPatch, - agentSha256: fileSha256(agentPath), - nativeBindingPresent: existsSync(nativeBindingPath), - attachConsoleFailed, - relayExitedAfterClientDisconnect: !isProcessAlive(relayPid) - } - console.log(JSON.stringify(summary, null, 2)) - - if (expectFailure !== attachConsoleFailed) { - throw new Error( - expectFailure - ? `Expected the real console-list agent to fail AttachConsole. stderr:\n${relayStderr}` - : `The real console-list agent failed AttachConsole. stderr:\n${relayStderr}` - ) - } - if ( - !observation.handshake || - !observation.ptySpawn || - !observation.ptyShutdown || - !observation.relayAliveAfterPtyShutdown - ) { - throw new Error( - `Relay lifecycle did not complete. stdout:\n${relayStdout}\nstderr:\n${relayStderr}` - ) - } -} finally { - if (relayPid && isProcessAlive(relayPid)) { - stopExactProcess(relayPid, relayPath) - } - rmSync(runDir, { recursive: true, force: true, maxRetries: 20, retryDelay: 100 }) -} - -function parseArgs(args) { - const parsed = { expect: 'clean', shell: 'cmd.exe', skipRelayPatch: false } - for (let index = 0; index < args.length; index++) { - const flag = args[index] - if (flag === '--skip-relay-patch') { - parsed.skipRelayPatch = true - continue - } - const value = args[index + 1] - if (!value || !['--node', '--node-pty', '--expect', '--shell'].includes(flag)) { - throw new Error( - 'Usage: node windows-ssh-attach-console-repro.mjs [--node PATH] [--node-pty DIR] [--shell PATH] [--skip-relay-patch] [--expect clean|attach-console-failure]' - ) - } - if (flag === '--node') { - parsed.node = value - } - if (flag === '--node-pty') { - parsed.nodePty = value - } - if (flag === '--expect') { - parsed.expect = value - } - if (flag === '--shell') { - parsed.shell = value - } - index++ - } - if (!['clean', 'attach-console-failure'].includes(parsed.expect)) { - throw new Error(`Unsupported expectation: ${parsed.expect}`) - } - return parsed -} - -function prepareRelayTree(runDir, nodePtyDir) { - for (const filename of [ - 'relay.js', - 'relay-watcher.js', - 'relay-ai-vault-service.js', - 'managed-hook-runtime.js', - NODE_PTY_PATCH_FILENAME, - '.version' - ]) { - copyFileSync(join(relayBuildDir, filename), join(runDir, filename)) - } - cpSync(nodePtyDir, join(runDir, 'node_modules', 'node-pty'), { recursive: true }) -} - -function applyPackagedNodePtyPatch(nodePath, runDir) { - const result = spawnSync(nodePath, [join(runDir, NODE_PTY_PATCH_FILENAME)], { - cwd: runDir, - encoding: 'utf8', - windowsHide: true - }) - if (result.status !== 0) { - throw new Error(`Packaged node-pty patch failed: ${result.stderr || result.stdout}`) - } -} - -function launchRelayWithoutConsole({ - nodePath, - relayPath, - runDir, - socketPath, - stdoutLog, - stderrLog -}) { - const relayArgs = [ - quoteWindowsArg(nodePath), - quoteWindowsArg(relayPath), - '--detached', - '--grace-time', - '5', - '--sock-path', - quoteWindowsArg(socketPath), - '--endpoint-dir', - quoteWindowsArg(join(runDir, 'endpoint')), - '--log-file', - quoteWindowsArg(stdoutLog), - `1>${quoteWindowsArg(stdoutLog)}`, - `2>${quoteWindowsArg(stderrLog)}` - ].join(' ') - const commandLine = `cmd.exe /d /s /c "${relayArgs}"` - const script = [ - `$result = Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{ CommandLine = ${powerShellLiteral(commandLine)}; CurrentDirectory = ${powerShellLiteral(runDir)} }`, - `if ($result.ReturnValue -ne 0) { throw "Win32_Process.Create failed with $($result.ReturnValue)" }`, - '$result.ProcessId' - ].join('; ') - const launched = runPowerShell(script) - const pid = Number(launched.stdout.trim()) - if (!Number.isInteger(pid) || pid <= 0) { - throw new Error(`Could not parse detached relay pid from: ${launched.stdout}`) - } - return pid -} - -function exerciseRelayClient({ nodePath, relayPath, runDir, socketPath, shell }) { - return new Promise((resolvePromise, rejectPromise) => { - const bridge = spawn(nodePath, [relayPath, '--connect', '--sock-path', socketPath], { - cwd: runDir, - windowsHide: true, - stdio: ['pipe', 'pipe', 'pipe'] - }) - const observation = { - nodeVersion: readNodeVersion(nodePath), - handshake: false, - ptySpawn: false, - ptyShutdown: false, - relayAliveAfterPtyShutdown: false, - bridgeExit: null - } - let stderr = '' - let buffer = Buffer.alloc(0) - let sentinelRead = false - let outgoingSequence = 1 - let settled = false - const timeout = setTimeout( - () => finish(new Error(`Timed out waiting for relay client lifecycle. stderr:\n${stderr}`)), - REGRESSION_TIMEOUT_MS - ) - - bridge.stderr.on('data', (data) => { - stderr += data.toString() - }) - bridge.on('error', finish) - bridge.on('close', (code, signal) => { - observation.bridgeExit = { code, signal } - if (!settled && observation.relayAliveAfterPtyShutdown) { - finish() - } else if (!settled) { - finish(new Error(`Relay bridge closed before lifecycle completed. stderr:\n${stderr}`)) - } - }) - bridge.stdout.on('data', (data) => { - try { - buffer = Buffer.concat([buffer, data]) - if (!sentinelRead) { - const sentinelIndex = buffer.indexOf(SENTINEL) - if (sentinelIndex === -1) { - return - } - buffer = buffer.subarray(sentinelIndex + SENTINEL.length) - sentinelRead = true - observation.handshake = true - request(1, 'pty.spawn', { - shellOverride: shell, - cwd: projectDir, - cols: 80, - rows: 24, - env: {} - }) - } - for (const message of drainMessages()) { - if (message.id === 1) { - throwResponseError(message) - observation.ptySpawn = true - request(2, 'pty.shutdown', { id: message.result.id, immediate: true }) - } else if (message.id === 2) { - throwResponseError(message) - observation.ptyShutdown = true - request(3, 'relay.status', {}) - } else if (message.id === 3) { - throwResponseError(message) - observation.relayAliveAfterPtyShutdown = message.result.ptys.active === 0 - bridge.stdin.end() - } - } - } catch (error) { - finish(error) - } - }) - - function request(id, method, params) { - bridge.stdin.write(encodeRequestFrame({ id, method, params }, outgoingSequence++)) - } - - function drainMessages() { - const messages = [] - while (buffer.length >= HEADER_LENGTH) { - const type = buffer[0] - const payloadLength = buffer.readUInt32BE(9) - if (buffer.length < HEADER_LENGTH + payloadLength) { - break - } - const payload = buffer.subarray(HEADER_LENGTH, HEADER_LENGTH + payloadLength) - buffer = buffer.subarray(HEADER_LENGTH + payloadLength) - if (type === 1) { - messages.push(JSON.parse(payload.toString('utf8'))) - } - } - return messages - } - - function finish(error) { - if (settled) { - return - } - settled = true - clearTimeout(timeout) - if (error) { - bridge.kill() - rejectPromise(error) - } else { - resolvePromise(observation) - } - } - }) -} - -function waitForPipe(socketPath, timeoutMs) { - const deadline = Date.now() + timeoutMs - return new Promise((resolvePromise, rejectPromise) => { - const attempt = () => { - const socket = createConnection(socketPath) - let settled = false - const retry = () => { - if (settled) { - return - } - settled = true - socket.destroy() - if (Date.now() >= deadline) { - rejectPromise(new Error(`Detached relay did not listen on ${socketPath}`)) - } else { - setTimeout(attempt, 50) - } - } - socket.once('connect', () => { - if (settled) { - return - } - settled = true - socket.destroy() - resolvePromise() - }) - socket.once('error', retry) - socket.setTimeout(250, retry) - } - attempt() - }) -} - -function encodeRequestFrame({ id, method, params }, sequence) { - const payload = Buffer.from(JSON.stringify({ jsonrpc: '2.0', id, method, params })) - const header = Buffer.alloc(HEADER_LENGTH) - header[0] = 1 - header.writeUInt32BE(sequence, 1) - header.writeUInt32BE(0, 5) - header.writeUInt32BE(payload.length, 9) - return Buffer.concat([header, payload]) -} - -function throwResponseError(message) { - if (message.error) { - throw new Error(`Relay RPC failed: ${message.error.message}`) - } -} - -function readNodeVersion(nodePath) { - return spawnSync(nodePath, ['--version'], { encoding: 'utf8' }).stdout.trim() -} - -function waitForExit(pid, timeoutMs) { - const deadline = Date.now() + timeoutMs - return new Promise((resolvePromise) => { - const poll = () => { - if (!isProcessAlive(pid) || Date.now() >= deadline) { - resolvePromise() - return - } - setTimeout(poll, 50) - } - poll() - }) -} - -function isProcessAlive(pid) { - const result = runPowerShell( - `if (Get-Process -Id ${pid} -ErrorAction SilentlyContinue) { 'ALIVE' }` - ) - return result.stdout.includes('ALIVE') -} - -function stopExactProcess(pid, relayPath) { - const script = [ - `$process = Get-CimInstance Win32_Process -Filter ${powerShellLiteral(`ProcessId = ${pid}`)}`, - `if ($process -and $process.CommandLine -like ${powerShellLiteral(`*${relayPath}*`)}) { Stop-Process -Id ${pid} -Force }` - ].join('; ') - runPowerShell(script) -} - -function runPowerShell(script) { - const encoded = Buffer.from(script, 'utf16le').toString('base64') - const result = spawnSync( - 'powershell.exe', - ['-NoLogo', '-NoProfile', '-NonInteractive', '-EncodedCommand', encoded], - { encoding: 'utf8' } - ) - if (result.status !== 0) { - throw new Error(`PowerShell failed (${result.status}): ${result.stderr || result.stdout}`) - } - return result -} - -function quoteWindowsArg(value) { - return `"${value.replaceAll('"', '\\"')}"` -} - -function powerShellLiteral(value) { - return `'${value.replaceAll("'", "''")}'` -} - -function readIfPresent(path) { - return existsSync(path) ? readFileSync(path, 'utf8') : '' -} - -function fileSha256(path) { - return createHash('sha256').update(readFileSync(path)).digest('hex') -} diff --git a/config/scripts/xterm-headless-ascii-contract.test.mjs b/config/scripts/xterm-headless-ascii-contract.test.mjs new file mode 100644 index 000000000000..eedf57bfe4ae --- /dev/null +++ b/config/scripts/xterm-headless-ascii-contract.test.mjs @@ -0,0 +1,156 @@ +import { createRequire } from 'node:module' +import { afterEach, describe, expect, it } from 'vitest' +import { activateOrcaTerminalUnicodeProvider } from '../../src/shared/terminal-unicode-provider.ts' +import { + buildAgentTuiStreamOps, + mulberry32, + splitIntoRandomChunks +} from '../../src/shared/agent-tui-ansi-fuzz-stream.ts' +import { Terminal as EsmTerminal } from '@xterm/headless/lib-headless/xterm-headless.mjs' + +const require = createRequire(import.meta.url) +const { Terminal: CjsTerminal } = require('@xterm/headless') +const { Unicode11Addon } = require('@xterm/addon-unicode11') + +const terminals = [] +afterEach(() => terminals.splice(0).forEach((terminal) => terminal.dispose())) + +function createTerminal(Terminal, fast, opts = {}) { + const t = new Terminal({ cols: 11, rows: 5, scrollback: 50, allowProposedApi: true, ...opts }) + terminals.push(t) + t.loadAddon(new Unicode11Addon()) + activateOrcaTerminalUnicodeProvider(t) + const real = t._core.unicodeService._providers['orca-11-zwj'] + let calls = 0 + if (!fast) { + t.unicode.register({ + version: 'ref', + wcwidth: (c) => real.wcwidth(c), + charProperties: (c, p) => real.charProperties(c, p) + }) + t.unicode.activeVersion = 'ref' + } else { + const orig = real.charProperties.bind(real) + real.charProperties = (c, p) => (calls++, orig(c, p)) + } + return { t, calls: () => calls } +} + +function snapshot(t) { + const core = t._core + const dump = (b) => ({ + x: b.x, + y: b.y, + ybase: b.ybase, + ydisp: b.ydisp, + rows: Array.from({ length: b.lines.length }, (_, i) => { + const r = b.lines.get(i) + return [Array.from(r._data), r._combined, r._extendedAttrs, r.isWrapped] + }) + }) + return { + normal: dump(core.buffers.normal), + alt: dump(core.buffers.alt), + active: core.buffers.active === core.buffers.alt, + join: core._inputHandler._parser.precedingJoinState, + links: [...core._oscLinkService._dataByLinkId].map(([id, e]) => [ + id, + e.lines.map((m) => m.line) + ]) + } +} + +const EDGE = + 'abcdefghijk' + // exactly cols -> pending wrap + 'lmnop‍qrs‍\u{1F419}é~ \x7f\x1b[?7lxxxxxxxxxxxxxxx\x1b[?7h' + + '\x1b[4hins\x1b[4l\x1b(0lqk\x1b(B\x1b]8;;u\x07L\x1b]8;;\x07tail­x' + + '日本abc\x1b[3D\x1b[1;38;2;1;2;3mZZ\x1b[0m\x1b[5GAB\r\n' + +describe.each([ + ['cjs', CjsTerminal], + ['esm', EsmTerminal] +])('%s patched headless with real Orca provider', (_name, Terminal) => { + it('uses a different provider when ASCII has custom width', () => { + const { t } = createTerminal(Terminal, true) + let calls = 0 + t.unicode.register({ + version: 'custom-ascii', + wcwidth: () => 2, + charProperties: () => { + calls++ + return calls === 1 ? 2 : 4 + } + }) + t.unicode.activeVersion = 'custom-ascii' + t._core.writeSync('abc') + expect(calls).toBe(3) + expect(t._core.buffers.active.x).toBe(5) + }) + + it('takes the fast path for plain ASCII', () => { + const fast = createTerminal(Terminal, true) + fast.t._core.writeSync(`x${'a'.repeat(9)}`) + expect(fast.calls()).toBeLessThan(3) + }) + + it.each([1, 3, 64])('edge stream matches reference at chunk %i', (size) => { + const a = createTerminal(Terminal, true) + const b = createTerminal(Terminal, false) + const s = EDGE.repeat(4) + for (let i = 0; i < s.length; i += size) { + a.t._core.writeSync(s.slice(i, i + size)) + b.t._core.writeSync(s.slice(i, i + size)) + expect(snapshot(a.t)).toEqual(snapshot(b.t)) + } + a.t.resize(4, 7) + b.t.resize(4, 7) + a.t._core.writeSync(s) + b.t._core.writeSync(s) + expect(snapshot(a.t)).toEqual(snapshot(b.t)) + }) + + it.each([1, 4, 64])('IRM over content and multi-line OSC 8 match at chunk %i', (size) => { + const a = createTerminal(Terminal, true) + const b = createTerminal(Terminal, false) + const s = + 'abcdefghij\rXY\x1b[4hPQRS\x1b[4l\r\n' + + '\x1b]8;;https://example.org\x07link text that wraps across rows\x1b]8;;\x07\r\n' + for (const input of [s, s.repeat(8)]) { + for (let i = 0; i < input.length; i += size) { + a.t._core.writeSync(input.slice(i, i + size)) + b.t._core.writeSync(input.slice(i, i + size)) + } + expect(snapshot(a.t)).toEqual(snapshot(b.t)) + } + }) + + it('screenReaderMode output matches', () => { + const a = createTerminal(Terminal, true, { screenReaderMode: true }) + const b = createTerminal(Terminal, false, { screenReaderMode: true }) + const ca = [] + const cb = [] + a.t._core._inputHandler.onA11yChar((c) => ca.push(c)) + b.t._core._inputHandler.onA11yChar((c) => cb.push(c)) + a.t._core.writeSync(EDGE) + b.t._core.writeSync(EDGE) + expect(ca).toEqual(cb) + expect(snapshot(a.t)).toEqual(snapshot(b.t)) + }) + + it.each([1, 2, 3, 4, 5, 6, 7, 8])('agent TUI fuzz seed %i matches', (seed) => { + const rng = mulberry32(seed) + const dims = { cols: 11, rows: 5 } + const ops = buildAgentTuiStreamOps(rng, dims, { + includeMouseModes: true, + includeOscHyperlinks: true, + opCount: 400 + }) + const a = createTerminal(Terminal, true, dims) + const b = createTerminal(Terminal, false, dims) + for (const chunk of splitIntoRandomChunks(rng, ops.join(''), { minLen: 1, maxLen: 40 })) { + a.t._core.writeSync(chunk) + b.t._core.writeSync(chunk) + } + expect(snapshot(a.t)).toEqual(snapshot(b.t)) + }) +}) diff --git a/config/scripts/xterm-sourcemap-source-contract.mjs b/config/scripts/xterm-sourcemap-source-contract.mjs new file mode 100644 index 000000000000..035f2648cb8e --- /dev/null +++ b/config/scripts/xterm-sourcemap-source-contract.mjs @@ -0,0 +1,102 @@ +import { execFileSync } from 'node:child_process' +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import path from 'node:path' +import { splitPatchEntries } from './xterm-patch-text.mjs' + +function mappedSourcePath(source) { + return source.match(/^(?:webpack:\/\/@xterm\/xterm\/\.\/|\.\.\/\.\.\/)(src\/.+)$/)?.[1] +} + +export function mappedSourceFiles(map) { + const files = new Map() + for (const [index, source] of map.sources.entries()) { + const relative = mappedSourcePath(source) + if (!relative) { + continue + } + if (relative.split('/').includes('..')) { + throw new Error(`Unsafe mapped source: ${relative}`) + } + if (files.has(relative)) { + throw new Error(`Duplicate mapped source: ${relative}`) + } + if (typeof map.sourcesContent[index] !== 'string') { + throw new Error(`Missing mapped content: ${relative}`) + } + files.set(relative, map.sourcesContent[index]) + } + if (files.size === 0) { + throw new Error('Published source map contains no upstream sources') + } + return files +} + +// Headless publishes source only inside its map; require exact-byte provenance. +export function assertMappedSourcesMatch(mapPath, checkoutRoot) { + const files = mappedSourceFiles(JSON.parse(readFileSync(mapPath, 'utf8'))) + for (const [relative, content] of files) { + if (readFileSync(path.join(checkoutRoot, relative), 'utf8') !== content) { + throw new Error(`Mapped source differs from pinned checkout: ${relative}`) + } + } +} + +export function assertMappedPatchDerivation(sourcePatch, generatedPatch, sourceMap) { + const entry = splitPatchEntries(generatedPatch).find((entry) => entry.path === sourceMap) + const readSide = (sign) => { + const lines = entry?.text.split('\n') ?? [] + const hunks = lines.filter((line) => line.startsWith('@@')) + if (hunks.length !== 1 || !/^@@ -1(?:,\d+)? \+1(?:,\d+)? @@/.test(hunks[0])) { + throw new Error(`Source map patch must contain one complete JSON hunk: ${sourceMap}`) + } + const body = lines + .slice(lines.indexOf(hunks[0]) + 1) + .filter((line) => line.startsWith(sign) || line.startsWith(' ')) + .map((line) => line.slice(1)) + .join('\n') + return JSON.parse(body) + } + const beforeMap = readSide('-'), + afterMap = readSide('+') + if (JSON.stringify(beforeMap.sources) !== JSON.stringify(afterMap.sources)) { + throw new Error('Source map source names changed') + } + const before = mappedSourceFiles(beforeMap), + after = mappedSourceFiles(afterMap) + for (const [index, source] of beforeMap.sources.entries()) { + if ( + !mappedSourcePath(source) && + beforeMap.sourcesContent[index] !== afterMap.sourcesContent[index] + ) { + throw new Error(`Unrecognized mapped source changed: ${source}`) + } + } + const directory = mkdtempSync(path.join(tmpdir(), 'orca-xterm-mapped-source-')) + try { + for (const [relative, content] of before) { + mkdirSync(path.dirname(path.join(directory, relative)), { recursive: true }) + writeFileSync(path.join(directory, relative), content) + } + execFileSync('git', ['apply', '--whitespace=nowarn', '-'], { + cwd: directory, + input: sourcePatch, + stdio: ['pipe', 'pipe', 'pipe'] + }) + for (const entry of splitPatchEntries(sourcePatch)) { + if (!after.has(entry.path)) { + throw new Error(`Changed source missing from generated map: ${entry.path}`) + } + } + if (before.size !== after.size) { + throw new Error('Source map source set changed') + } + for (const [relative, content] of after) { + if (readFileSync(path.join(directory, relative), 'utf8') !== content) { + throw new Error(`Source patch and generated map disagree: ${relative}`) + } + } + } finally { + rmSync(directory, { recursive: true, force: true }) + } +} diff --git a/config/scripts/xterm-sourcemap-source-contract.test.mjs b/config/scripts/xterm-sourcemap-source-contract.test.mjs new file mode 100644 index 000000000000..71c68b9fb84c --- /dev/null +++ b/config/scripts/xterm-sourcemap-source-contract.test.mjs @@ -0,0 +1,66 @@ +import { readFileSync } from 'node:fs' +import { describe, expect, it } from 'vitest' +import { + mappedSourceFiles, + assertMappedPatchDerivation +} from './xterm-sourcemap-source-contract.mjs' + +const source = 'webpack://@xterm/xterm/./src/common/InputHandler.ts' +const makeMap = (sources, sourcesContent) => ({ sources, sourcesContent }) + +describe('mapped source provenance', () => { + it('recognizes both published source namespaces', () => { + expect(mappedSourceFiles(makeMap([source], ['source'])).get('src/common/InputHandler.ts')).toBe( + 'source' + ) + expect( + mappedSourceFiles(makeMap(['../../src/common/InputHandler.ts'], ['source'])).get( + 'src/common/InputHandler.ts' + ) + ).toBe('source') + }) + it('rejects unknown namespaces, missing content, duplicate and unsafe source paths', () => { + expect(() => mappedSourceFiles(makeMap(['unknown/InputHandler.ts'], ['source']))).toThrow( + /no upstream/ + ) + expect(() => mappedSourceFiles(makeMap([source], [null]))).toThrow(/Missing mapped/) + expect(() => mappedSourceFiles(makeMap([source, source], ['a', 'a']))).toThrow(/Duplicate/) + expect(() => mappedSourceFiles(makeMap(['../../src/../outside.ts'], ['a']))).toThrow(/Unsafe/) + }) + it('rejects a source patch that differs from the generated map', () => { + const root = new URL('../patches/', import.meta.url) + const patch = readFileSync(new URL('@xterm__headless@6.1.0-beta.302.patch', root), 'utf8') + const sourcePatch = readFileSync( + new URL('xterm-src/@xterm__headless@6.1.0-beta.302.src.patch', root), + 'utf8' + ) + expect(() => + assertMappedPatchDerivation( + sourcePatch.replace('pos--;', 'pos -= 2;'), + patch, + 'lib-headless/xterm-headless.js.map' + ) + ).toThrow(/disagree/) + const omitted = sourcePatch.replaceAll('src/common/InputHandler.ts', 'src/common/Unmapped.ts') + expect(() => + assertMappedPatchDerivation(omitted, patch, 'lib-headless/xterm-headless.js.map') + ).toThrow() + }) +}) + +it.each(['webpack/bootstrap', '../../src/'])( + 'rejects changed unknown source %s alongside valid sources', + (unknown) => { + const before = { + version: 3, + sources: [source, unknown], + sourcesContent: ['a', 'unchanged'] + } + const after = { ...before, sourcesContent: ['a', 'modified'] } + const mapPath = 'lib-headless/xterm-headless.js.map' + const patch = `diff --git a/${mapPath} b/${mapPath}\n--- a/${mapPath}\n+++ b/${mapPath}\n@@ -1 +1 @@\n-${JSON.stringify(before)}\n+${JSON.stringify(after)}\n` + expect(() => assertMappedPatchDerivation('', patch, mapPath)).toThrow( + /Unrecognized mapped source changed/ + ) + } +) diff --git a/docs/reference/agent-pty-transcript-capture.md b/docs/reference/agent-pty-transcript-capture.md index 934f0028a93f..3ae1d613f3c8 100644 --- a/docs/reference/agent-pty-transcript-capture.md +++ b/docs/reference/agent-pty-transcript-capture.md @@ -37,21 +37,23 @@ Each capture also writes `.meta.json` with the timestamp, platform PTY size, note and exit code. Commit it with the transcript; the version and account type behind a screen are not recoverable from the bytes. -**Prerequisite:** `node-pty` must be built for plain Node: +**Prerequisite:** prepare the pinned bundled Bun runtime (no native Node PTY rebuild): ``` -node config/scripts/ensure-native-runtime.mjs --runtime=node +node config/scripts/build-orcad-bun.mjs --runtime-only --out-dir out/orcad ``` -Orca itself does not need to be running, and the recorder never touches Orca state. +The Node entry launches capture under bundled Bun; `--scan` and `--help` stay Node-only. +The recorder taps native bytes before UTF-8 decoding and reuses the production terminal +backend, including Windows job ownership and exit draining. Orca itself does not need to +be running, and the recorder never touches Orca state. ### Platform notes - **macOS / Linux:** nothing special. `TERM=xterm-256color` is set for the child. - **Windows:** run it from Windows Terminal / PowerShell, not a Git Bash (MSYS) pane — MSYS - rewrites arguments that start with `/`, which mangles the `cmd.exe /c` hand-off. A `.cmd` or - `.bat` agent shim cannot be spawned by node-pty directly, so the recorder routes those through - `cmd.exe` for you. + rewrites arguments that start with `/`, which mangles the `cmd.exe /c` hand-off. The production Windows gate resolves recognised npm/pnpm agent shims directly; + other `.cmd`/`.bat` scripts use the canonical batch argument encoder. - **WSL:** capture _inside_ the distro (run the recorder from the distro's checkout). Recording `wsl.exe` from the Windows side adds the login-shell banner to the transcript. - **SSH:** record on the execution host. A transcript recorded locally is not evidence about what diff --git a/docs/reference/linux-glibc-compatibility.md b/docs/reference/linux-glibc-compatibility.md index 3fab0cfcb5e0..38c9c0014956 100644 --- a/docs/reference/linux-glibc-compatibility.md +++ b/docs/reference/linux-glibc-compatibility.md @@ -17,8 +17,7 @@ requirement. ## Why this needs attention A native module (`.node`) links against the glibc of the machine that compiled -it. Our release CI compiles node-pty from source on GitHub's `ubuntu-latest` -runner, whose glibc rises over time as the image is bumped. A binary compiled on +it. Build runners' glibc rises over time as their images are bumped. A binary compiled on a newer glibc can reference symbol versions that do not exist on an older target, and the dynamic loader then refuses to load it: @@ -26,8 +25,8 @@ and the dynamic loader then refuses to load it: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.34' not found (required by .../pty.node) ``` -Because the Orca main process loads node-pty at startup, that failure crashes the -whole app before a window appears — this is exactly what shipped in v1.4.150 and +Older Orca releases loaded node-pty in the main process at startup, so that failure +crashed the whole app before a window appeared. This shipped in v1.4.150 and broke launch on Ubuntu 20.04 ([#9902](https://github.com/stablyai/orca/issues/9902)). The specific trap is glibc's 2.32–2.34 "libpthread/libutil merge", which moved @@ -45,20 +44,11 @@ the floor, so node-pty was the sole blocker. ## How we keep the floor -**1. Pin the relocated symbols (the fix).** -[`config/patches/node-pty@1.1.0.patch`](../../config/patches/node-pty@1.1.0.patch) -adds a `.symver` shim in `src/unix/pty.cc` that binds `openpty`, `forkpty`, and -`pthread_sigmask` to their pre-merge version node — `GLIBC_2.2.5` on x64, -`GLIBC_2.17` on arm64 (each architecture's baseline glibc). glibc still ships -those as compatibility aliases, so the reference resolves on both new build hosts -and old targets. - -The catch: gcc defaults to `--as-needed` and, since the pinned symbols now -resolve from libc's compat aliases at build time, it drops `libutil`/`libpthread` -from `DT_NEEDED`. On the target those libraries are where the symbols actually -live, so the patch's `binding.gyp` `ldflags` force -`-Wl,--no-as-needed,-l:libutil.so.1,-l:libpthread.so.0` back into `DT_NEEDED`. -The shim is guarded by `#if defined(__linux__)`; macOS and Windows are untouched. +**1. Use the qualified bundled terminal runtime.** Desktop and headless terminal +services use pinned Bun. The former node-pty symbol-version patch is retired; +the historical failure above still explains why every native artifact needs a +floor check. Native dependencies compiled from source must use a compatible +sysroot or explicitly preserve the older symbol versions and required libraries. **2. Gate packaging (the regression guard).** [`config/scripts/verify-linux-glibc-floor.cjs`](../../config/scripts/verify-linux-glibc-floor.cjs) @@ -76,12 +66,11 @@ only in libutil. A future runner bump, a new native dependency, or a dropped ldflag therefore fails the release build instead of shipping a Linux app that crashes on launch. -> The gate is a static invariant, not an integration test. The load path was -> verified by hand for this fix (real Ubuntu 20.04, x64 + arm64: `require` -> node-pty and spawn a shell). A CI smoke test that loads the packaged -> `pty.node` in a glibc-2.31 container and spawns a shell is the recommended -> follow-up — it would make the load path self-verifying and stay valid even if -> the build ever moves to an old-glibc sysroot. +The static gate is paired with +`config/scripts/run-linux-packaged-terminal-floor-smoke.mjs`, which launches the +packaged Bun daemon through the packaged Electron client in Ubuntu 20.04. It +checks real shell output, resize, history/snapshots, reconnect and exit handling +on the release architectures. The one carve-out is the `sherpa-onnx` speech prebuilt, which already requires `GLIBCXX_3.4.29` (GCC 11). It loads lazily in the speech worker @@ -92,8 +81,8 @@ app itself still launches on stock 20.04. **3. Qualify the bundled headless runtime and its native dependencies.** Orcad and the SSH relay use pinned Bun for terminals. They do not install node-pty -prebuilds or compile node-pty on the remote host. Desktop Electron still uses the -patched node-pty dependency and the build gates above. +prebuilds or compile node-pty on the remote host. Desktop Electron uses the same +terminal service runtime; the build gates above cover its packaged artifacts. The Bun runtime catalog selects glibc or musl artifacts for each supported CPU. The bundled-runtime CI checks the Linux glibc floor and runs native dependency @@ -107,7 +96,7 @@ proof that a host cannot start. - Prefer packages that ship prebuilt binaries compiled against an old toolchain (manylinux / `glibc 2.17`-class), like `@parcel/watcher`. - For a module we compile from source, if the gate flags it, either pin the - offending symbols the way node-pty does, or build it in an old-glibc container. + offending symbols to compatible versions, or build it in an old-glibc container. - To check locally on a Linux host, list what a binary requires (skipping the weak `0x02`-flagged needs the loader tolerates): diff --git a/docs/reference/windows-daemon-host-relocation.md b/docs/reference/windows-daemon-host-relocation.md index c5cb34e8ab54..29baa6abe1c1 100644 --- a/docs/reference/windows-daemon-host-relocation.md +++ b/docs/reference/windows-daemon-host-relocation.md @@ -1,13 +1,21 @@ # Windows daemon-host relocation -On Windows the terminal daemon does not run from the install directory. Before it forks the -daemon, Orca materializes a trimmed copy of its own runtime under -`%LOCALAPPDATA%\Orca\daemon-host\\` and forks the daemon from there -(`src/main/daemon/daemon-host-relocation.ts`). This is what keeps live terminals alive across an -auto-update and across a crash of the main process. - -Read this before changing the copy plan, the host exe name, the LOCALAPPDATA layout, or -`config/nsis/orca-installer-hooks.nsh`. +On Windows the terminal daemon runs from an immutable Bun runtime copy outside +the install directory. `src/main/daemon/daemon-bun-runtime.ts` verifies and +materializes its entry, runtime, ConPTY provider and process-inspection addon under +`%LOCALAPPDATA%\Orca\terminal-daemon-host\managed-v1\bun-\`. +Launch pins protect publication through authenticated readiness; runtime retention +preserves live or unverifiable owners. A packaged Windows launch requires +LOCALAPPDATA; missing storage fails visibly rather than using an update-unsafe +install directory or a roaming profile. + +Existing Electron-hosted daemons keep their original owners and files until their +sessions drain. `daemon-host-relocation.ts` now only prunes those legacy copies. +The Electron copy plan and executable-name discussion below record that older +implementation; do not restore its materialization commands for Bun. + +Read this before changing runtime placement or `config/nsis/orca-installer-hooks.nsh`. +The installer must preserve both live legacy hosts and current Bun hosts. ## What the relocation actually escapes diff --git a/docs/reference/windows-msys-job-breakaway.md b/docs/reference/windows-msys-job-breakaway.md index 7265293bd1be..dd93074b1944 100644 --- a/docs/reference/windows-msys-job-breakaway.md +++ b/docs/reference/windows-msys-job-breakaway.md @@ -5,10 +5,11 @@ object unless the job is created **without** `JOB_OBJECT_LIMIT_BREAKAWAY_OK`. `terminatePtyJob` then reports `terminated` and leaves the child running — the orphan that holds a worktree directory open. -The denial is already in `config/patches/node-pty@1.1.0.patch` -(`usesCygwinRuntime`, added in #19068). This page records the measurement -behind it, because the failure mode it prevents is indistinguishable from a -stale native addon and the gates of the day could not tell the two apart. +Current terminals use the Bun Windows job implementation in +`src/main/daemon/pty-subprocess/windows-bun-pty-job.ts`; native MSYS coverage remains +in `src/main/windows/windows-msys-job.win32.test.ts`. The node-pty patch and addon +rebuild commands below describe the retired backend and are historical evidence, +not installation instructions for the current build. ## The mechanism diff --git a/docs/reference/windows-process-enumeration.md b/docs/reference/windows-process-enumeration.md index e56bec2cd7a8..017d48767a4b 100644 --- a/docs/reference/windows-process-enumeration.md +++ b/docs/reference/windows-process-enumeration.md @@ -249,7 +249,7 @@ why only absence does. ## The relay has no binding, and falls back -Relay deployment installs only `node-pty` and `@parcel/watcher` on the remote +The older relay deployment installed only `node-pty` and `@parcel/watcher` on the remote host (`RELAY_NATIVE_DEPS` in `src/main/ssh/ssh-relay-deploy.ts`), so a Windows machine used as an SSH host has no `@vscode/windows-process-tree` at all. It is not added there on purpose. Both ways of installing it fail, and both were @@ -547,7 +547,20 @@ inside one call with a blocking `Sleep(1000)` in the middle, which would hold a libuv threadpool slot for a full second out of the Resource Manager's two-second poll. -## Owning a PTY's process tree +## Current terminal ownership + +Desktop and headless terminals use Bun's PTY adapter. Per-terminal jobs live in +`src/main/daemon/pty-subprocess/windows-bun-pty-job.ts`, with native operations in +`windows-bun-pty-native.ts`. The MSYS no-breakaway and daemon crash-reaping +contracts below still apply. `@vscode/windows-process-tree` remains a separate +patched native dependency for process inspection; it is still rebuilt and +validated by the install/package scripts. + +## Historical node-pty ownership implementation + +The remainder records the retired backend's behavior and incident analysis. +Its node-pty patch, source-build commands and addon gates no longer apply to +current local installations. `src/main/windows/windows-pty-job.ts` is the counterpart to reading the table: it answers "is this tree mine, and how do I kill it?" with a handle instead of diff --git a/docs/reference/wsl-runner-verification.md b/docs/reference/wsl-runner-verification.md index 59667f513ded..16a95aafb9be 100644 --- a/docs/reference/wsl-runner-verification.md +++ b/docs/reference/wsl-runner-verification.md @@ -25,7 +25,7 @@ Each fails on a **new** offender _and_ on a **stale** entry, so the count can on ## 3. Real-binary — the assertions nothing else can make -**Windows CI** (`package (windows)` job in `pr.yml`) rebuilds node-pty from patched source and runs the `win32` suites against a real ConPTY: a real detached grandchild, a real job kill, and the inverse — a clean `exit` must leave backgrounded work alone. +**Windows CI** (`package (windows)` job in `pr.yml`) prepares the pinned Bun runtime and ConPTY provider, then runs the `win32` suites against real terminals: a real detached grandchild, a real job kill, and the inverse — a clean `exit` must leave backgrounded work alone. **Real WSL distro** — not in CI; WSL isn't available on hosted runners. diff --git a/docs/reference/xterm-patch-regeneration.md b/docs/reference/xterm-patch-regeneration.md index d398f10a6bba..02f8852c5881 100644 --- a/docs/reference/xterm-patch-regeneration.md +++ b/docs/reference/xterm-patch-regeneration.md @@ -288,6 +288,19 @@ pnpm's diff flags and normalization, hunk splitting, round-trip stability, the commit and build-order assertions, and lockfile coupling — with no network and no build, so they run in the ordinary test shards. +## Headless ASCII patch + +`@xterm/headless` uses the same pinned source and generator, with output under +`headless/lib-headless/`. Its tarball omits `src/`, so the manifest lists both +source maps explicitly. The generator checks embedded source against the pinned +checkout and applies the readable patch to the old mapped source to verify the +new maps exactly. Unrecognized mapped sources must remain unchanged. + +Headless leaves `Version.ts` at its upstream value; do not add a publish-time +version stamp. Upstream `package-headless` runs `npm publish --dry-run` afterward; +it publishes nothing, but can print an already-published-version warning. Bundle +and map reproduction, rather than that warning, determines generator success. + ## Known Gaps `@xterm/addon-ligatures` is still patched by hand, and can stay that way: the diff --git a/docs/site/content/docs/ssh.mdx b/docs/site/content/docs/ssh.mdx index c97765d3481d..2a1baa00ee1f 100644 --- a/docs/site/content/docs/ssh.mdx +++ b/docs/site/content/docs/ssh.mdx @@ -82,18 +82,16 @@ You do not flip a separate “Kerberos mode” switch for config-imported hosts Hardware-backed OpenSSH identities (`ed25519-sk`, `ecdsa-sk`, including agent-backed security keys) also use **system OpenSSH** rather than the built-in ssh2 client. Orca detects the key type from your identity files and hands the connection to the OS OpenSSH binary so the touch / PIN prompt works as usual. Regular Ed25519, ECDSA, and RSA keys stay on the built-in transport. If OpenSSH is not available on the machine, those FIDO2 targets cannot authenticate until it is installed and on `PATH` (or in the usual Windows/macOS/Linux locations). -## Linux hosts without a C/C++ toolchain +## Remote terminal runtime -On first connect, Orca installs a small relay on the remote. Remote terminals need a native `node-pty` module. Linux packages often compile on the host; macOS/Windows relays use prebuilds. +On first connect, Orca installs a relay with a pinned Bun runtime and the native +artifacts for the remote platform. Current remote terminals do not require a +C/C++ toolchain or a node-pty build on the host. The remote must support the +runtime's operating-system and CPU requirements. -If the remote is missing **make**, a **C++ compiler**, and **python3**, Orca still finishes connect for **files, git, and the editor**, but **remote terminals will not work** until build tools are installed. Example install hints Orca may surface: - -- Debian/Ubuntu: `sudo apt-get install -y build-essential python3` -- Fedora/RHEL: `sudo dnf install -y make gcc gcc-c++ python3` -- Alpine: `sudo apk add build-base python3` -- Arch: `sudo pacman -S --needed base-devel python` - -Install the tools, then reconnect so the relay can install native modules. +Older Orca clients can still deploy the previous Node-based relay. If that client +reports a missing native build toolchain, update the client or follow its +installation guidance before reconnecting. ## Port forwarding diff --git a/package.json b/package.json index b4374a3dc932..cabcd2c8f04f 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "test": "node config/scripts/ensure-native-runtime.mjs --runtime=node && vitest run --config config/vitest.config.ts", "test:bun:profile": "node config/scripts/run-bun-profile-tests.mjs", "test:skill-sharing:release": "vitest run --config config/vitest.config.ts src/main/skills src/main/runtime/rpc/methods/skills.test.ts src/relay/skill-install-handler.test.ts src/shared/skill-bundle-install-contract.test.ts src/shared/skill-install-contract.test.ts src/shared/skill-install-failure.test.ts src/shared/skill-package-manifest.test.ts", - "test:repro:remote-agent-session": "pnpm run build:cli && pnpm run build:electron-vite && node config/scripts/remote-agent-session-authority-repro.mjs", + "test:repro:remote-agent-session": "pnpm run build:cli && pnpm run build:terminal-daemon && pnpm run build:electron-vite && node config/scripts/remote-agent-session-authority-repro.mjs", "capture:agent-transcript": "node config/scripts/ensure-native-runtime.mjs --runtime=node && node config/scripts/capture-agent-pty-transcript.mjs", "check:reliability-gates": "node config/scripts/check-reliability-gates.mjs", "check:max-lines-ratchet": "node config/scripts/check-max-lines-ratchet.mjs", @@ -87,24 +87,24 @@ "verify:localization-extraction": "node config/scripts/verify-localization-extraction.mjs", "verify:localization-coverage": "node config/scripts/audit-localization-coverage.mjs --check", "audit:localization": "node config/scripts/audit-localization-coverage.mjs", + "build:terminal-daemon": "node config/scripts/build-terminal-daemon.mjs", "build:cli-runtime": "node config/scripts/build-cli-runtime.mjs", "build:cli": "pnpm run build:cli-runtime && tsc -p config/tsconfig.cli.json --outDir out --composite false --incremental false && node config/scripts/build-cli-bin.mjs && node config/scripts/verify-cli-bin.mjs --fix-executable --fix-package-json && node config/scripts/install-dev-cli.mjs", "test:linux-cli-contract": "node config/scripts/run-linux-cli-launch-contract-docker.mjs --appimage dist/orca-linux.AppImage", - "test:repro:skills-cli-runtime": "pnpm run build:cli && pnpm run build:electron-vite && pnpm run verify:built-skills-cli", + "test:repro:skills-cli-runtime": "pnpm run build:cli && pnpm run build:terminal-daemon && pnpm run build:electron-vite && pnpm run verify:built-skills-cli", "build:electron-vite": "node config/scripts/run-electron-vite-build.mjs", "build:electron-vite:parallel": "node config/scripts/run-electron-vite-targets-in-parallel.mjs", "build:web": "node config/scripts/run-vite-web-build.mjs && node config/scripts/verify-web-build.mjs", "build:web-from-renderer": "node config/scripts/project-renderer-web-client.mjs && node config/scripts/verify-web-build.mjs", "build:mobile-web": "node config/scripts/build-mobile-web-app-bundle.mjs && node config/scripts/verify-mobile-web-app-bundle.mjs", - "build:desktop": "pnpm run typecheck && pnpm run build:relay && pnpm run build:cli && pnpm run build:electron-vite && pnpm run verify:built-skills-cli && pnpm run build:web-from-renderer && pnpm run build:mobile-web", + "build:desktop": "pnpm run typecheck && pnpm run build:relay && pnpm run build:cli && pnpm run build:terminal-daemon && pnpm run build:electron-vite && pnpm run verify:built-skills-cli && pnpm run build:web-from-renderer && pnpm run build:mobile-web", "build": "pnpm run build:desktop && pnpm run build:native", - "build:release": "pnpm run build:relay && pnpm run build:native && pnpm run verify:computer-native && pnpm run build:cli && pnpm run build:electron-vite && pnpm run verify:built-skills-cli && pnpm run build:web-from-renderer && pnpm run build:mobile-web", - "build:release:parallel": "pnpm run build:relay && pnpm run build:native && pnpm run verify:computer-native && pnpm run build:cli && pnpm run build:electron-vite:parallel && pnpm run verify:built-skills-cli && pnpm run build:web-from-renderer && pnpm run build:mobile-web", + "build:release": "pnpm run build:relay && pnpm run build:native && pnpm run verify:computer-native && pnpm run build:cli && pnpm run build:terminal-daemon && pnpm run build:electron-vite && pnpm run verify:built-skills-cli && pnpm run build:web-from-renderer && pnpm run build:mobile-web", + "build:release:parallel": "pnpm run build:relay && pnpm run build:native && pnpm run verify:computer-native && pnpm run build:cli && pnpm run build:terminal-daemon && pnpm run build:electron-vite:parallel && pnpm run verify:built-skills-cli && pnpm run build:web-from-renderer && pnpm run build:mobile-web", "postinstall": "node config/scripts/rebuild-native-deps.mjs", "rebuild:electron": "node config/scripts/rebuild-native-deps.mjs", "reclaim:electron-dists": "node config/scripts/reclaim-electron-dists.mjs", "reclaim:dev-bundles": "node config/scripts/reclaim-dev-electron-bundles.mjs", - "rebuild:node": "pnpm rebuild node-pty", "build:unpack": "pnpm run build && pnpm run ensure:electron-runtime && electron-builder --config config/electron-builder.config.cjs --dir", "build:win": "pnpm run build:desktop && pnpm run ensure:electron-runtime && electron-builder --config config/electron-builder.config.cjs --win", "build:icons": "bash resources/icon-source/generate.sh", @@ -184,7 +184,6 @@ "electron-updater": "^6.8.9", "i18next": "^26.3.1", "jsonc-parser": "^3.3.1", - "node-pty": "^1.1.0", "posthog-node": "^5.52.5", "proper-lockfile": "4.1.2", "qrcode": "^1.5.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c05265397b9a..00c2270b5090 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -115,9 +115,9 @@ patchedDependencies: '@xterm/addon-search@0.17.0-beta.300': eee5338dd2621ece46e79c61ec06766cd7fadaf79ffdb24e2a8ab68e97ef31f0 '@xterm/addon-serialize@0.15.0-beta.300': b35533fe252e7e45433150170348889f4e08a6c17f7017ac34ea694d831fec7f '@xterm/addon-webgl@0.20.0-beta.299': 94687e89a0115e6e6aa102837f986debdc029c091527ee5eb4a4e17ceaf9473e + '@xterm/headless@6.1.0-beta.302': 7f6b065658ad4b8e970a53df82d6582f0fd498bb4810cb2107865ce3bda7845d '@xterm/xterm@6.1.0-beta.303': dd0ccc59cd1ccf99f4d76e5aa2456da165fa0804dce19a833d7638bd07ffa393 lint-staged@16.4.0: 7333b3837f80a7fbd045964db6d76ba4fc118e49134bdbabb00585b6b7b60673 - node-pty@1.1.0: 92c95cffab383d86b3b13a460c75a08074468ebf1f3911db8e874e083201f192 importers: @@ -146,7 +146,7 @@ importers: version: 0.15.0-beta.300(patch_hash=b35533fe252e7e45433150170348889f4e08a6c17f7017ac34ea694d831fec7f)(@xterm/xterm@6.1.0-beta.303(patch_hash=dd0ccc59cd1ccf99f4d76e5aa2456da165fa0804dce19a833d7638bd07ffa393)) '@xterm/headless': specifier: 6.1.0-beta.302 - version: 6.1.0-beta.302 + version: 6.1.0-beta.302(patch_hash=7f6b065658ad4b8e970a53df82d6582f0fd498bb4810cb2107865ce3bda7845d) agent-browser: specifier: ~0.27.0 version: 0.27.0 @@ -159,9 +159,6 @@ importers: jsonc-parser: specifier: ^3.3.1 version: 3.3.1 - node-pty: - specifier: ^1.1.0 - version: 1.1.0(patch_hash=92c95cffab383d86b3b13a460c75a08074468ebf1f3911db8e874e083201f192) posthog-node: specifier: ^5.52.5 version: 5.52.5 @@ -6015,9 +6012,6 @@ packages: node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} - node-pty@1.1.0: - resolution: {integrity: sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg==} - node-releases@2.0.54: resolution: {integrity: sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==} engines: {node: '>=18'} @@ -10521,7 +10515,7 @@ snapshots: dependencies: '@xterm/xterm': 6.1.0-beta.303(patch_hash=dd0ccc59cd1ccf99f4d76e5aa2456da165fa0804dce19a833d7638bd07ffa393) - '@xterm/headless@6.1.0-beta.302': {} + '@xterm/headless@6.1.0-beta.302(patch_hash=7f6b065658ad4b8e970a53df82d6582f0fd498bb4810cb2107865ce3bda7845d)': {} '@xterm/xterm@6.1.0-beta.303(patch_hash=dd0ccc59cd1ccf99f4d76e5aa2456da165fa0804dce19a833d7638bd07ffa393)': {} @@ -13098,10 +13092,6 @@ snapshots: node-int64@0.4.0: {} - node-pty@1.1.0(patch_hash=92c95cffab383d86b3b13a460c75a08074468ebf1f3911db8e874e083201f192): - dependencies: - node-addon-api: 7.1.1 - node-releases@2.0.54: {} nopt@9.0.0: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 1cf678294784..2b17fa7d76fe 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -51,7 +51,6 @@ allowBuilds: electron-winstaller: false esbuild: true msw: false - node-pty: true sherpa-onnx: true ssh2: false '@orca/windows-registry': false @@ -60,8 +59,8 @@ overrides: monaco-editor>dompurify: 3.4.15 patchedDependencies: + '@xterm/headless@6.1.0-beta.302': config/patches/@xterm__headless@6.1.0-beta.302.patch '@xterm/addon-image@0.10.0-beta.300': config/patches/@xterm__addon-image@0.10.0-beta.300.patch - node-pty@1.1.0: config/patches/node-pty@1.1.0.patch '@xterm/addon-ligatures@0.11.0-beta.300': config/patches/@xterm__addon-ligatures@0.11.0-beta.300.patch '@xterm/addon-webgl@0.20.0-beta.299': config/patches/@xterm__addon-webgl@0.20.0-beta.299.patch '@xterm/addon-search@0.17.0-beta.300': config/patches/@xterm__addon-search@0.17.0-beta.300.patch diff --git a/src/cli/runtime/cli-bun-environment.integration.test.ts b/src/cli/runtime/cli-bun-environment.integration.test.ts new file mode 100644 index 000000000000..701d0f202b33 --- /dev/null +++ b/src/cli/runtime/cli-bun-environment.integration.test.ts @@ -0,0 +1,53 @@ +import { mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { build } from 'esbuild' +import { expect, it } from 'vitest' +import { runProcess } from '../../shared/child-process/run-process' + +const runtime = process.env.BUN_EXECUTABLE +it.skipIf(!runtime)('does not load project dotenv files when launching the CLI', async () => { + if (!runtime) { + throw new Error('BUN_EXECUTABLE is required') + } + const root = await mkdtemp(join(tmpdir(), 'orca-cli-environment-')) + try { + const launcher = join(root, 'launcher.cjs') + const entry = join(root, 'entry.cjs') + await build({ + stdin: { + contents: + "import { launchBunCli } from './src/cli/runtime/cli-bun-launcher'; launchBunCli(process.argv[2], process.argv[3], [])", + resolveDir: process.cwd(), + loader: 'ts' + }, + bundle: true, + platform: 'node', + format: 'cjs', + outfile: launcher + }) + await writeFile( + join(root, '.env'), + 'ORCA_USER_DATA_PATH=foreign-profile\nORCA_CLI_CWD=foreign-workspace\n' + ) + await writeFile( + entry, + 'console.log(JSON.stringify({profile:process.env.ORCA_USER_DATA_PATH??null,cwd:process.env.ORCA_CLI_CWD??null}));process.disconnect?.()' + ) + const env: NodeJS.ProcessEnv = { ...process.env, ORCA_BACKGROUND_LAUNCH: '1' } + delete env.ORCA_USER_DATA_PATH + delete env.ORCA_CLI_CWD + const result = await runProcess({ + program: process.execPath, + args: [launcher, runtime, entry], + cwd: root, + env, + timeoutMs: 10_000 + }) + expect(result.code, result.stderr).toBe(0) + expect(result.timedOut).toBe(false) + expect(JSON.parse(result.stdout)).toEqual({ profile: null, cwd: null }) + } finally { + await rm(root, { recursive: true, force: true }) + } +}) diff --git a/src/main/__fixtures__/shell-wrapper-snapshots/relay-bash-rcfile.txt b/src/main/__fixtures__/shell-wrapper-snapshots/relay-bash-rcfile.txt index 61bdd01dd506..02e9402ced5a 100644 --- a/src/main/__fixtures__/shell-wrapper-snapshots/relay-bash-rcfile.txt +++ b/src/main/__fixtures__/shell-wrapper-snapshots/relay-bash-rcfile.txt @@ -26,6 +26,13 @@ fi [[ -n "${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="${ORCA_OPENCODE_CONFIG_DIR}" [[ -n "${ORCA_MIMOCODE_HOME:-}" ]] && export MIMOCODE_HOME="${ORCA_MIMOCODE_HOME}" [[ -n "${ORCA_REMOTE_CLI_BIN_DIR:-}" ]] && case ":$PATH:" in *:"${ORCA_REMOTE_CLI_BIN_DIR}":*) ;; *) export PATH="${ORCA_REMOTE_CLI_BIN_DIR}:$PATH" ;; esac +if [ -n "${ORCA_WSL_CLI_DIR:-}" ]; then + if [ -x "$ORCA_WSL_CLI_DIR/${ORCA_CLI_COMMAND:-}" ]; then + export PATH="$ORCA_WSL_CLI_DIR${PATH:+:$PATH}" + else + printf 'Orca CLI unavailable: cannot run %s. Check WSL Windows-drive mount options.\n' "$ORCA_WSL_CLI_DIR/${ORCA_CLI_COMMAND:-}" >&2 + fi +fi # Why: OMP does not auto-load Orca's managed status extension; wrap only # interactive launch invocations so subcommands such as `omp config` keep # their normal argv shape. diff --git a/src/main/__fixtures__/shell-wrapper-snapshots/relay-zsh-zshenv.txt b/src/main/__fixtures__/shell-wrapper-snapshots/relay-zsh-zshenv.txt index ff90115d30d5..f0818f8f05cf 100644 --- a/src/main/__fixtures__/shell-wrapper-snapshots/relay-zsh-zshenv.txt +++ b/src/main/__fixtures__/shell-wrapper-snapshots/relay-zsh-zshenv.txt @@ -41,6 +41,13 @@ __orca_deferred_init() { builtin typeset -g _orca_deferred_init_done=1 builtin typeset -g precmd_functions precmd_functions=(${precmd_functions:#__orca_deferred_init}) + if [ -n "${ORCA_WSL_CLI_DIR:-}" ]; then + if [ -x "$ORCA_WSL_CLI_DIR/${ORCA_CLI_COMMAND:-}" ]; then + export PATH="$ORCA_WSL_CLI_DIR${PATH:+:$PATH}" + else + printf 'Orca CLI unavailable: cannot run %s. Check WSL Windows-drive mount options.\n' "$ORCA_WSL_CLI_DIR/${ORCA_CLI_COMMAND:-}" >&2 + fi + fi if __orca_has_feature overlay; then # Why: remote startup files can re-export user defaults after relay spawn. [[ -n "${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="${ORCA_OPENCODE_CONFIG_DIR}" diff --git a/src/main/agent-hooks/restored-subagent-liveness-sweep.test.ts b/src/main/agent-hooks/restored-subagent-liveness-sweep.test.ts index b3257e6972c4..ad2b4de7d658 100644 --- a/src/main/agent-hooks/restored-subagent-liveness-sweep.test.ts +++ b/src/main/agent-hooks/restored-subagent-liveness-sweep.test.ts @@ -82,6 +82,7 @@ function sweepWith( server: AgentHookServer, overrides: { probeLiveLocalPty?: (ptyId: string) => boolean | null | Promise + probeLiveWslPty?: (ptyId: string) => Promise executionHostId?: string | null boundPtyIdByPaneKey?: Record persistedPtyIdByPaneKey?: Record @@ -90,6 +91,7 @@ function sweepWith( return sweepRestoredSubagentsWithoutLiveAgent({ probeLiveLocalPty: async (ptyId) => overrides.probeLiveLocalPty ? await overrides.probeLiveLocalPty(ptyId) : false, + probeLiveWslPty: overrides.probeLiveWslPty, isLocalExecutionHost: () => isLocalExecutionHost( overrides.executionHostId === undefined ? 'local' : overrides.executionHostId @@ -114,6 +116,32 @@ function paneStatus( } describe('restored subagent liveness sweep', () => { + it.each([undefined, true, null, false])( + 'uses only the owning guest provider to reap WSL panes (%s)', + async (guestLive) => { + const server = await restartWithInFlightSubagent() + const guestId = 'wsl:Ubuntu@@daemon-build@@guest-pty' + const nativeProbe = vi.fn(async () => false) + const guestProbe = vi.fn(async () => guestLive ?? null) + try { + expect( + await sweepWith(server, { + persistedPtyIdByPaneKey: { [PANE]: guestId }, + probeLiveLocalPty: nativeProbe, + ...(guestLive === undefined ? {} : { probeLiveWslPty: guestProbe }) + }) + ).toBe(guestLive === false ? 1 : 0) + expect(nativeProbe).not.toHaveBeenCalled() + if (guestLive !== undefined) { + expect(guestProbe).toHaveBeenCalledWith(guestId) + } + expect(paneStatus(server).state).toBe(guestLive === false ? 'done' : 'working') + } finally { + server.stop() + } + } + ) + it('reaps the phantom seed so a slept-through pane reaches done', async () => { const server = await restartWithInFlightSubagent() try { diff --git a/src/main/agent-hooks/restored-subagent-liveness-sweep.ts b/src/main/agent-hooks/restored-subagent-liveness-sweep.ts index 3402ab7bf5be..1800a740e924 100644 --- a/src/main/agent-hooks/restored-subagent-liveness-sweep.ts +++ b/src/main/agent-hooks/restored-subagent-liveness-sweep.ts @@ -12,6 +12,7 @@ import { parseWorkspaceKey } from '../../shared/workspace-scope' export type RestoredSubagentLivenessSweepDeps = { /** Targeted provider liveness, or null when the provider cannot prove either state. */ probeLiveLocalPty: (ptyId: string) => Promise + probeLiveWslPty?: (ptyId: string) => Promise isLocalExecutionHost: (worktreeId: string | undefined) => boolean /** PTY bound to this pane in the current session, if it has one. */ getBoundPtyIdForPaneKey: (paneKey: string) => string | undefined @@ -44,7 +45,10 @@ export async function sweepRestoredSubagentsWithoutLiveAgent( try { let probe = probesByPtyId.get(ptyId) if (!probe) { - probe = deps.probeLiveLocalPty(ptyId) + // Native inventory cannot prove absence on a guest daemon. + probe = ptyId.startsWith('wsl:') + ? (deps.probeLiveWslPty?.(ptyId) ?? Promise.resolve(null)) + : deps.probeLiveLocalPty(ptyId) probesByPtyId.set(ptyId, probe) } const live = await probe diff --git a/src/main/agent-hooks/wsl-hook-fs-adapter.ts b/src/main/agent-hooks/wsl-hook-fs-adapter.ts index 71699f57d284..2bca557220f6 100644 --- a/src/main/agent-hooks/wsl-hook-fs-adapter.ts +++ b/src/main/agent-hooks/wsl-hook-fs-adapter.ts @@ -21,6 +21,7 @@ export async function installWslGuestHooks(options: { guestHome: string codexHomePath: string | null distro: string + user?: string installHooks: typeof installRemoteManagedAgentHooks settings: ManagedHookDetectionSettings warn: (message: string) => void @@ -50,7 +51,7 @@ export async function installWslGuestHooks(options: { if (agents.length === 0) { return } - if (agents.includes('codex') && codexHomePath) { + if (agents.includes('codex') && codexHomePath && options.user === undefined) { try { const status = await installCodex(codexHomePath, distro) if (status?.state === 'error') { @@ -64,11 +65,11 @@ export async function installWslGuestHooks(options: { ) } } - // Codex is redirected into the runtime home and must use the canonical - // runtime-host writer above; the relay adapter owns all other agents. - const remoteAgents = agents.filter((agent) => agent !== 'codex') + // Explicit users install through their own home-scoped relay, never the default-user writer. + const remoteAgents = agents.filter((agent) => agent !== 'codex' || options.user !== undefined) const results = await installHooks(createWslHookSftpAdapter(mux), guestHome, { agents: remoteAgents, + ...(options.user && codexHomePath ? { codexHomeDir: codexHomePath } : {}), ...(claudeVersion ? { claudeVersion } : {}) }) const failed = results.filter((r) => r.state === 'error').length diff --git a/src/main/agent-hooks/wsl-hook-relay-deps.ts b/src/main/agent-hooks/wsl-hook-relay-deps.ts index 1ee3ee64114b..417db4f9a7dd 100644 --- a/src/main/agent-hooks/wsl-hook-relay-deps.ts +++ b/src/main/agent-hooks/wsl-hook-relay-deps.ts @@ -60,7 +60,7 @@ export type WslHookRelayManagerDeps = { readBundle: (jsPath: string) => Buffer listDistros: () => Promise isDistroRunning: typeof isWslDistroRunning - prepareRuntime: (distro: string) => Promise + prepareRuntime: (distro: string, user?: string) => Promise spawnRelay: typeof spawnWslRelayProcess runInstall: typeof runWslInstallProcess waitForSentinel: typeof waitForWslRelaySentinel @@ -99,7 +99,8 @@ export const defaultWslHookRelayDeps: WslHookRelayManagerDeps = { readBundle: (jsPath) => readFileSync(jsPath), listDistros: () => listWslDistrosAsync(), isDistroRunning: isWslDistroRunning, - prepareRuntime: (distro) => ensureWslBunRuntime(createRunningWslRuntimeRunner(distro)), + prepareRuntime: (distro, user) => + ensureWslBunRuntime(createRunningWslRuntimeRunner(distro, undefined, user)), spawnRelay: spawnWslRelayProcess, runInstall: runWslInstallProcess, waitForSentinel: waitForWslRelaySentinel, diff --git a/src/main/agent-hooks/wsl-hook-relay-guest-install.ts b/src/main/agent-hooks/wsl-hook-relay-guest-install.ts index 103a5e9909b5..9fcbdb242440 100644 --- a/src/main/agent-hooks/wsl-hook-relay-guest-install.ts +++ b/src/main/agent-hooks/wsl-hook-relay-guest-install.ts @@ -22,6 +22,7 @@ type GuestInstallDeps = { /** Structural slice of the manager's DistroState this pass reads and writes. */ type GuestInstallState = { distro: string + user?: string mux?: SshChannelMultiplexer guestHome?: string codexHomePath?: string @@ -72,6 +73,7 @@ async function installGuestHooksAndPlugins( guestHome, codexHomePath: state.codexHomePath ?? null, distro: state.distro, + user: state.user, installHooks: deps.installHooks, installCodex: deps.installCodex, settings: deps.managedHookSettings(), diff --git a/src/main/agent-hooks/wsl-hook-relay-launch.test.ts b/src/main/agent-hooks/wsl-hook-relay-launch.test.ts index 6dcd93bdda85..a075b07668ae 100644 --- a/src/main/agent-hooks/wsl-hook-relay-launch.test.ts +++ b/src/main/agent-hooks/wsl-hook-relay-launch.test.ts @@ -12,6 +12,26 @@ import { listRunningWslDistrosAsync } from '../wsl' import { isWslDistroRunning, spawnWslRelayProcess } from './wsl-hook-relay-launch' describe('spawnWslRelayProcess', () => { + it('pins the explicit user before --exec', () => { + spawnWslRelayProcess('Ubuntu', {}, '1.2.3', '/home/ada/bun', 'ada') + expect(spawnMock).toHaveBeenLastCalledWith( + expect.objectContaining({ + args: [ + '-d', + 'Ubuntu', + '--user', + 'ada', + '--exec', + 'sh', + '-c', + expect.any(String), + 'orca-hook-relay', + '/home/ada/bun' + ] + }) + ) + }) + it('names an explicit Windows directory rather than inheriting one', () => { spawnWslRelayProcess('Ubuntu', {}, '1.2.3', '/home/ada/bun') diff --git a/src/main/agent-hooks/wsl-hook-relay-launch.ts b/src/main/agent-hooks/wsl-hook-relay-launch.ts index 7b814ad3bcfe..5fab731c742c 100644 --- a/src/main/agent-hooks/wsl-hook-relay-launch.ts +++ b/src/main/agent-hooks/wsl-hook-relay-launch.ts @@ -124,7 +124,8 @@ export function spawnWslRelayProcess( distro: string, env: NodeJS.ProcessEnv, version: string, - executable: string + executable: string, + user?: string ): ChildProcessWithoutNullStreams { // Why: --exec bypasses the distro's default login shell — a bare `--` // routes through it (a fish/nushell chsh could mangle the command) and @@ -134,7 +135,7 @@ export function spawnWslRelayProcess( const command = `exec sh "${guestRelayDirExpr(version)}/launch.sh" "$1"` return spawnProcess({ program: 'wsl.exe', - args: buildWslExecArgs(distro, ['sh', '-c', command, 'orca-hook-relay', executable]), + args: buildWslExecArgs(distro, ['sh', '-c', command, 'orca-hook-relay', executable], user), env, // Why explicit (#16463): the guest path is in `command`, so the Windows cwd // only decides whether CreateProcessW succeeds -- and an inherited one is a @@ -162,10 +163,12 @@ export async function runWslInstallProcess( script: string, // Unused: the install script embeds its own version/paths and reads // nothing from the crossed guest environment. - _env: NodeJS.ProcessEnv + _env: NodeJS.ProcessEnv, + user?: string ): Promise<{ code: number | null; stderr: string }> { const result = await runWslProcess({ distro, + user, loginPath: 'none', script, // Declared because the payload is opaque here: it is POSIX plus a heredoc. @@ -186,7 +189,7 @@ const TRANSIENT_RETRY_LIMIT = 2 export type WslRelayLaunchIo = { isDistroRunning: typeof isWslDistroRunning - prepareRuntime: (distro: string) => Promise + prepareRuntime: (distro: string, user?: string) => Promise spawnRelay: typeof spawnWslRelayProcess waitForSentinel: typeof waitForWslRelaySentinel runInstall: typeof runWslInstallProcess @@ -201,6 +204,7 @@ export type WslRelayLaunchIo = { * through `onFailure`; non-startup errors propagate to the caller. */ export async function launchWslRelayWithInstall(options: { distro: string + user?: string env: NodeJS.ProcessEnv bundleJsPath: string version: string @@ -228,14 +232,20 @@ export async function launchWslRelayWithInstall(options: { } return running } - const executable = await io.prepareRuntime(distro) + const executable = await io.prepareRuntime(distro, ...(options.user ? [options.user] : [])) let installTried = false let transientRetries = 0 for (;;) { if (!(await requireRunning())) { return } - const child = io.spawnRelay(distro, env, version, executable) + const child = io.spawnRelay( + distro, + env, + version, + executable, + ...(options.user ? [options.user] : []) + ) options.onChild(child) try { const transport = await io.waitForSentinel(child) @@ -269,7 +279,12 @@ export async function launchWslRelayWithInstall(options: { return } const script = buildGuestInstallScript(io.readBundle(bundleJsPath), version) - const result = await io.runInstall(distro, script, env) + const result = await io.runInstall( + distro, + script, + env, + ...(options.user ? [options.user] : []) + ) if (result.code === 0) { continue } diff --git a/src/main/agent-hooks/wsl-hook-relay-manager.test.ts b/src/main/agent-hooks/wsl-hook-relay-manager.test.ts index 68ce8954c332..528583254301 100644 --- a/src/main/agent-hooks/wsl-hook-relay-manager.test.ts +++ b/src/main/agent-hooks/wsl-hook-relay-manager.test.ts @@ -184,6 +184,7 @@ describe('WslHookRelayManager', () => { function guestTransport( options: { registerInstallPlugins?: boolean + guestHome?: string detectedAgents?: string[] claudeVersion?: string } = {} @@ -191,7 +192,7 @@ describe('WslHookRelayManager', () => { const { registerInstallPlugins = true, detectedAgents = ['codex'], claudeVersion } = options const harness = createGuestHarness() harnesses.push(harness) - registerWslHookFsHandlers(harness.guestDispatcher, home) + registerWslHookFsHandlers(harness.guestDispatcher, options.guestHome ?? home) harness.guestDispatcher.onRequest(AGENT_HOOK_REQUEST_REPLAY_METHOD, async () => ({ replayed: 0 })) @@ -260,6 +261,70 @@ describe('WslHookRelayManager', () => { return { manager: new WslHookRelayManager(deps), deps } } + it('isolates explicit guest users and preserves them across hooks-off restart', async () => { + const aliceHome = `${home}/alice` + const bobHome = `${home}/bob` + const waitForSentinel = vi.fn() + for (const guestHome of [aliceHome, bobHome, aliceHome, bobHome]) { + waitForSentinel.mockImplementationOnce(async () => guestTransport({ guestHome })) + } + const { manager, deps } = createManager({ waitForSentinel }) + try { + await manager.ensureForDistro('Ubuntu', `${aliceHome}/.codex`, undefined, 'alice') + await manager.ensureForDistro('Ubuntu', `${bobHome}/.codex`, undefined, 'bob') + expect(deps.spawnRelay).toHaveBeenCalledTimes(2) + expect(deps.prepareRuntime).toHaveBeenCalledWith('Ubuntu', 'alice') + expect(deps.prepareRuntime).toHaveBeenCalledWith('Ubuntu', 'bob') + expect(manager.getGuestEndpointFilePath('Ubuntu')).toBeNull() + expect(manager.getGuestEndpointFilePath('Ubuntu', 'alice')).not.toBeNull() + expect(manager.getGuestEndpointFilePath('Ubuntu', 'bob')).not.toBeNull() + expect(deps.installCodex).not.toHaveBeenCalled() + expect(deps.installHooks).toHaveBeenCalledWith( + expect.anything(), + aliceHome, + expect.objectContaining({ agents: ['codex'], codexHomeDir: `${aliceHome}/.codex` }) + ) + expect(deps.installHooks).toHaveBeenCalledWith( + expect.anything(), + bobHome, + expect.objectContaining({ agents: ['codex'], codexHomeDir: `${bobHome}/.codex` }) + ) + await manager.ensureForDistro('ubuntu', undefined, undefined, 'alice') + expect(deps.spawnRelay).toHaveBeenCalledTimes(2) + manager.disposeAll({ permanent: false }) + manager.resumeStoppedRelays() + await vi.waitFor(() => expect(deps.spawnRelay).toHaveBeenCalledTimes(4)) + const users = vi.mocked(deps.spawnRelay).mock.calls.map((call) => call[4]) + expect(users).toEqual(['alice', 'bob', 'alice', 'bob']) + } finally { + manager.disposeAll() + } + }) + + it('keeps the explicit user through stale-install retry', async () => { + const { manager, deps } = createManager({ + waitForSentinel: vi + .fn() + .mockRejectedValueOnce(startupError(78)) + .mockImplementation(async () => guestTransport()) + }) + try { + await manager.ensureForDistro('Ubuntu', undefined, undefined, 'alice') + expect(deps.runInstall).toHaveBeenCalledWith( + 'Ubuntu', + expect.any(String), + expect.anything(), + 'alice' + ) + expect(vi.mocked(deps.spawnRelay).mock.calls.map((call) => call[4])).toEqual([ + 'alice', + 'alice' + ]) + } finally { + manager.disposeAll() + } + }) + it('starts one relay per distro, installs hooks, exposes the guest endpoint path, and forwards envelopes', async () => { const { manager, deps } = createManager({}) manager.ensureForDistro('Ubuntu', codexHome) diff --git a/src/main/agent-hooks/wsl-hook-relay-manager.ts b/src/main/agent-hooks/wsl-hook-relay-manager.ts index 22dcd55dcd9e..ff51d76f1a39 100644 --- a/src/main/agent-hooks/wsl-hook-relay-manager.ts +++ b/src/main/agent-hooks/wsl-hook-relay-manager.ts @@ -38,7 +38,10 @@ export class WslHookRelayManager { private deps: WslHookRelayManagerDeps private recovery: WslRelayRecovery private states = new Map() - private stoppedByHooksOff = new Map() + private stoppedByHooksOff = new Map< + string, + Pick + >() private defaultDistro: string | null = null private disposed = false private warnedBundleMissing = false @@ -48,10 +51,12 @@ export class WslHookRelayManager { isDistroRunning: (distro) => this.deps.isDistroRunning(distro), warn: (message) => this.deps.warn(message), isDisposed: () => this.disposed, - isCurrent: (state) => this.states.get(wslHookRelayStateKey(state.distro)) === state, - restart: (distro) => this.ensureForDistro(distro, this.stateFor(distro)?.codexHomePath), + isCurrent: (state) => + this.states.get(wslHookRelayStateKey(state.distro, state.user)) === state, + restart: (distro, user) => + this.ensureForDistro(distro, this.stateFor(distro, user)?.codexHomePath, undefined, user), dropState: (state) => { - const key = wslHookRelayStateKey(state.distro) + const key = wslHookRelayStateKey(state.distro, state.user) if (this.states.get(key) === state) { this.states.delete(key) } @@ -65,36 +70,36 @@ export class WslHookRelayManager { async ensureForDistro( distro: string | null, codexHomePath?: string | null, - launchKind?: 'pi' | 'omp' + launchKind?: 'pi' | 'omp', + user?: string ): Promise { if (this.disposed || !isWslHookRelayAllowed(this.deps)) { return } - await this.ensureInternal(distro, codexHomePath ?? undefined, launchKind).catch((err) => { + await this.ensureInternal(distro, codexHomePath ?? undefined, launchKind, user).catch((err) => { const detail = err instanceof Error ? err.message : String(err) this.deps.warn(`[agent-hooks] WSL hook relay ensure failed: ${detail}`) }) } - private stateFor(distro: string | null): WslRelayDistroState | undefined { - return this.states.get(wslHookRelayStateKey(distro ?? this.defaultDistro ?? '')) + private stateFor(distro: string | null, user?: string): WslRelayDistroState | undefined { + return this.states.get(wslHookRelayStateKey(distro ?? this.defaultDistro ?? '', user)) } /** Guest endpoint path once install completes. */ - getGuestEndpointFilePath(distro: string | null): string | null { - return this.stateFor(distro)?.connectedAt - ? (this.stateFor(distro)?.guestEndpointFilePath ?? null) + getGuestEndpointFilePath(distro: string | null, user?: string): string | null { + return this.stateFor(distro, user)?.connectedAt + ? (this.stateFor(distro, user)?.guestEndpointFilePath ?? null) : null } getOpenCodeOverlayDir( distro: string | null, - agent: 'opencode' | 'opencode2' = 'opencode' + agent: 'opencode' | 'opencode2' = 'opencode', + user?: string ): string | null { - const state = this.stateFor(distro) - return agent === 'opencode2' - ? (state?.opencode2OverlayDir ?? null) - : (state?.opencodeOverlayDir ?? null) + const state = this.stateFor(distro, user) + return state?.[agent === 'opencode2' ? 'opencode2OverlayDir' : 'opencodeOverlayDir'] ?? null } - getGuestAgentPath(distro: string | null, kind: 'pi' | 'omp'): string | null { - const state = this.stateFor(distro) + getGuestAgentPath(distro: string | null, kind: 'pi' | 'omp', user?: string): string | null { + const state = this.stateFor(distro, user) return kind === 'pi' ? (state?.piAgentDir ?? null) : (state?.ompStatusExtension ?? null) } /** Kills every live relay. Non-permanent (hooks switched off mid-session) leaves the @@ -106,7 +111,11 @@ export class WslHookRelayManager { state.mux?.dispose() state.child?.kill() if (!permanent) { - this.stoppedByHooksOff.set(state.distro, state.codexHomePath) + this.stoppedByHooksOff.set(wslHookRelayStateKey(state.distro, state.user), { + distro: state.distro, + user: state.user, + codexHomePath: state.codexHomePath + }) } } this.states.clear() @@ -114,22 +123,25 @@ export class WslHookRelayManager { /** Restarts what a hooks-off teardown stopped. Skips distros the user has since shut * down: `wsl -d` BOOTS a stopped distro, and nothing in it is waiting on status. */ resumeStoppedRelays(): void { - resumeWslStoppedRelays(this.stoppedByHooksOff, this.deps.isDistroRunning, (distro, home) => - this.ensureForDistro(distro, home) + resumeWslStoppedRelays( + this.stoppedByHooksOff, + this.deps.isDistroRunning, + (distro, home, user) => this.ensureForDistro(distro, home, undefined, user) ) } private async ensureInternal( requestedDistro: string | null, requestedCodexHomePath?: string, - launchKind?: 'pi' | 'omp' + launchKind?: 'pi' | 'omp', + user?: string ): Promise { const distro = requestedDistro ?? (await this.resolveDefaultDistro()) if (!distro || this.disposed) { return } - const key = wslHookRelayStateKey(distro) + const key = wslHookRelayStateKey(distro, user) const existing = this.states.get(key) - if (requestedCodexHomePath) { + if (requestedCodexHomePath && user === undefined) { recordManagedWslCodexHome(distro, requestedCodexHomePath) } if (existing) { @@ -173,6 +185,7 @@ export class WslHookRelayManager { } const state: WslRelayDistroState = { distro, + user, phase: 'starting', failures: existing?.failures ?? 0, opencodeOverlayDir: existing?.opencodeOverlayDir, @@ -187,6 +200,7 @@ export class WslHookRelayManager { const env = buildWslRelaySpawnEnv(coords, bundle.version, instanceKey) state.startup = launchWslRelayWithInstall({ distro: state.distro, + user: state.user, env, bundleJsPath: bundle.jsPath, version: bundle.version, diff --git a/src/main/agent-hooks/wsl-hook-relay-reattach.test.ts b/src/main/agent-hooks/wsl-hook-relay-reattach.test.ts index 8e9e425a05c3..8fde244706d5 100644 --- a/src/main/agent-hooks/wsl-hook-relay-reattach.test.ts +++ b/src/main/agent-hooks/wsl-hook-relay-reattach.test.ts @@ -13,6 +13,11 @@ describe('ensureWslHookRelayForReattach', () => { }) it.each([ + [ + 'guest daemon owner', + { id: 'wsl:Ubuntu@@owner@@terminal', isReattach: true, wslDistro: 'Ubuntu' }, + null + ], ['fresh WSL spawn', { wslDistro: 'Ubuntu' }, null], ['native reattach', { isReattach: true, wslDistro: null }, null], ['legacy reattach without ownership context', { isReattach: true }, null], diff --git a/src/main/agent-hooks/wsl-hook-relay-reattach.ts b/src/main/agent-hooks/wsl-hook-relay-reattach.ts index 837fe82b9837..faceb3fb88d9 100644 --- a/src/main/agent-hooks/wsl-hook-relay-reattach.ts +++ b/src/main/agent-hooks/wsl-hook-relay-reattach.ts @@ -1,7 +1,8 @@ import type { PtySpawnResult } from '../providers/pty-spawn-result' import { wslHookRelayManager } from './wsl-hook-relay-manager' -type ReattachResult = Pick +type ReattachResult = Pick & + Partial> export function ensureWslHookRelayForReattach( result: ReattachResult, @@ -10,7 +11,13 @@ export function ensureWslHookRelayForReattach( wslHookRelayManager.ensureForDistro(distro) ): void { // Why: the current renderer preference can differ from the surviving PTY's proven distro ownership. - if (connectionId || result.isReattach !== true || typeof result.wslDistro !== 'string') { + // Guest daemon hooks must reconnect through their captured user, never the current default. + if ( + connectionId || + result.id?.startsWith('wsl:') || + result.isReattach !== true || + typeof result.wslDistro !== 'string' + ) { return } const distro = result.wslDistro.trim() diff --git a/src/main/agent-hooks/wsl-hook-relay-recovery.test.ts b/src/main/agent-hooks/wsl-hook-relay-recovery.test.ts index 778b9e36172d..57e00173b58d 100644 --- a/src/main/agent-hooks/wsl-hook-relay-recovery.test.ts +++ b/src/main/agent-hooks/wsl-hook-relay-recovery.test.ts @@ -14,21 +14,24 @@ function waitFor(condition: () => boolean, timeoutMs = 3_000): Promise { } describe('WslRelayRecovery', () => { - it('re-ensures the distro when the restart timer fires and the distro is running', async () => { - const restart = vi.fn() - const recovery = new WslRelayRecovery({ - isDistroRunning: async () => true, - warn: vi.fn(), - isDisposed: () => false, - isCurrent: () => true, - restart, - dropState: vi.fn() - }) - const state = makeState() - recovery.scheduleRestart(state) - await waitFor(() => restart.mock.calls.length === 1) - expect(restart).toHaveBeenCalledWith('Ubuntu') - }) + it.each([undefined, 'alice'])( + 're-ensures the same user %s when the running distro recovers', + async (user) => { + const restart = vi.fn() + const recovery = new WslRelayRecovery({ + isDistroRunning: async () => true, + warn: vi.fn(), + isDisposed: () => false, + isCurrent: () => true, + restart, + dropState: vi.fn() + }) + const state = { ...makeState(), user } + recovery.scheduleRestart(state) + await waitFor(() => restart.mock.calls.length === 1) + expect(restart).toHaveBeenCalledWith('Ubuntu', ...(user ? [user] : [])) + } + ) it('drops the state instead of booting a stopped distro', async () => { const restart = vi.fn() diff --git a/src/main/agent-hooks/wsl-hook-relay-recovery.ts b/src/main/agent-hooks/wsl-hook-relay-recovery.ts index bebc264d4335..13968c55795c 100644 --- a/src/main/agent-hooks/wsl-hook-relay-recovery.ts +++ b/src/main/agent-hooks/wsl-hook-relay-recovery.ts @@ -5,6 +5,7 @@ const RUNNING_PROBE_RETRY_DELAY_MS = 60_000 export type WslRelayRecoveryState = { distro: string + user?: string cooldownUntil: number restartTimer?: ReturnType reinstallTimer?: ReturnType @@ -16,7 +17,7 @@ export type WslRelayRecoveryIo = { isDisposed: () => boolean /** True while this state object is still the one in the manager's map. */ isCurrent: (state: WslRelayRecoveryState) => boolean - restart: (distro: string) => void + restart: (distro: string, user?: string) => void dropState: (state: WslRelayRecoveryState) => void } @@ -92,6 +93,6 @@ export class WslRelayRecovery { this.io.dropState(state) return } - this.io.restart(state.distro) + this.io.restart(state.distro, ...(state.user ? [state.user] : [])) } } diff --git a/src/main/agent-hooks/wsl-hook-relay-state-key.ts b/src/main/agent-hooks/wsl-hook-relay-state-key.ts index 45962f22c3a3..26b37393ca0f 100644 --- a/src/main/agent-hooks/wsl-hook-relay-state-key.ts +++ b/src/main/agent-hooks/wsl-hook-relay-state-key.ts @@ -1,3 +1,7 @@ -export function wslHookRelayStateKey(distro: string): string { - return distro.trim().toLowerCase() +export function wslHookRelayStateKey(distro: string, user?: string): string { + if (user !== undefined && (!user || user.trim() !== user || /[\0\r\n]/.test(user))) { + throw new Error('Invalid WSL hook relay user') + } + const normalized = distro.trim().toLowerCase() + return user === undefined ? normalized : JSON.stringify([normalized, user]) } diff --git a/src/main/agent-hooks/wsl-hook-relay-state-machine.ts b/src/main/agent-hooks/wsl-hook-relay-state-machine.ts index 04944712a57a..f2753116b478 100644 --- a/src/main/agent-hooks/wsl-hook-relay-state-machine.ts +++ b/src/main/agent-hooks/wsl-hook-relay-state-machine.ts @@ -40,17 +40,17 @@ export function markWslRelayFailed( } export function resumeWslStoppedRelays( - stopped: Map, + stopped: Map, isDistroRunning: (distro: string) => Promise, - ensure: (distro: string, home: string | undefined) => void + ensure: (distro: string, home: string | undefined, user?: string) => void ): void { - const distros = [...stopped] + const owners = [...stopped.values()] stopped.clear() - for (const [distro, home] of distros) { + for (const { distro, codexHomePath: home, user } of owners) { void isDistroRunning(distro) .then((running) => { if (running) { - ensure(distro, home) + ensure(distro, home, ...(user ? [user] : [])) } }) .catch(() => undefined) diff --git a/src/main/agent-hooks/wsl-hook-relay-state.ts b/src/main/agent-hooks/wsl-hook-relay-state.ts index 509231b97d9d..b11f5392ade9 100644 --- a/src/main/agent-hooks/wsl-hook-relay-state.ts +++ b/src/main/agent-hooks/wsl-hook-relay-state.ts @@ -2,6 +2,7 @@ import type { SshChannelMultiplexer } from '../ssh/ssh-channel-multiplexer' export type WslRelayDistroState = { distro: string + user?: string phase: 'starting' | 'running' | 'failed' child?: { kill: () => void } mux?: SshChannelMultiplexer diff --git a/src/main/ai-vault/session-scanner-opencode-sqlite-process.test.ts b/src/main/ai-vault/session-scanner-opencode-sqlite-process.test.ts index 914028857210..50d7f29b2da8 100644 --- a/src/main/ai-vault/session-scanner-opencode-sqlite-process.test.ts +++ b/src/main/ai-vault/session-scanner-opencode-sqlite-process.test.ts @@ -5,7 +5,7 @@ import { join, resolve } from 'node:path' import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' import * as processRunner from '../../shared/child-process/run-process' import { runProcess, spawnProcess } from '../../shared/child-process/run-process' -import { ORCAD_BUN_VERSION } from '../../shared/orcad-bun-runtime' +import { ORCAD_BUN_VERSION, orcadBunVersionProbeArgs } from '../../shared/orcad-bun-runtime' import { orcadBunRuntimeFilename } from '../../shared/orcad-artifacts' import SyncDatabase from '../sqlite/sync-database' import { appendTurns, writeOpenCodeSqliteDatabase } from './session-scanner-opencode-sqlite-fixture' @@ -64,9 +64,11 @@ for (const [runtime, executable] of [ describe.skipIf(!existsSync(executable))(`OpenCode SQLite process under ${runtime}`, () => { it('uses the deployed runtime and reads live WAL, full prompts and captures in one persistent child', async () => { if (runtime === 'Bun') { - expect((await runProcess({ program: executable, args: ['--version'] })).stdout.trim()).toBe( - ORCAD_BUN_VERSION - ) + expect( + ( + await runProcess({ program: executable, args: [...orcadBunVersionProbeArgs()] }) + ).stdout.trim() + ).toBe(ORCAD_BUN_VERSION) } expect(existsSync(`${dbPath}-wal`)).toBe(true) const spawn = vi.spyOn(processRunner, 'spawnProcess') diff --git a/src/main/bundled-bun-test-execution.test.ts b/src/main/bundled-bun-test-execution.test.ts new file mode 100644 index 000000000000..838784898335 --- /dev/null +++ b/src/main/bundled-bun-test-execution.test.ts @@ -0,0 +1,78 @@ +import { runInNewContext } from 'node:vm' +import { ORCAD_BUN_VERSION } from '../shared/orcad-bun-runtime' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { runBundledBunFixture } from './bundled-bun-test-execution' + +const { run, exists } = vi.hoisted(() => ({ run: vi.fn(), exists: vi.fn() })) +vi.mock('node:fs', () => ({ existsSync: exists })) +vi.mock('../shared/child-process/run-process', () => ({ runProcess: run })) +afterEach(() => { + vi.unstubAllEnvs() + vi.clearAllMocks() +}) + +describe('bundled Bun fixture launch environment', () => { + it.each([undefined, { output: 'captured' }])( + 'serializes a completed fixture result: %j', + async (value) => { + exists.mockReturnValue(true) + run.mockImplementationOnce(async (spec) => { + let stdout = '' + await runInNewContext(spec.args[1], { + require: (name: string) => + name === 'node:fs' + ? { readFileSync: () => spec.input } + : { capture: async () => value }, + process: { + versions: { bun: ORCAD_BUN_VERSION }, + stdout: { + write: (chunk: string) => { + stdout += chunk + } + } + }, + console + }) + return { code: 0, stdout, stderr: '', timedOut: false, outputTruncated: false } + }) + expect(await runBundledBunFixture('/fixture.ts', 'capture', null, 1000)).toEqual( + value ?? null + ) + } + ) + it('removes runtime injection while retaining explicit fixture arguments and shell environment', async () => { + exists.mockReturnValue(true) + run.mockResolvedValue({ + code: 0, + stdout: '"done"', + stderr: '', + timedOut: false, + outputTruncated: false + }) + for (const key of [ + 'NODE_OPTIONS', + 'NODE_PATH', + 'BUN_OPTIONS', + 'BUN_INSPECT', + 'BUN_INSPECT_WAIT' + ]) { + vi.stubEnv(key, 'injected-runtime-setting') + } + vi.stubEnv('SHELL', '/bin/zsh') + const args = { env: { NODE_OPTIONS: '--fixture-shell-option' } } + expect(await runBundledBunFixture('/fixture.ts', 'capture', args, 1000)).toBe('done') + const spec = run.mock.calls[0][0] + for (const key of [ + 'NODE_OPTIONS', + 'NODE_PATH', + 'BUN_OPTIONS', + 'BUN_INSPECT', + 'BUN_INSPECT_WAIT' + ]) { + expect(spec.env[key]).toBeUndefined() + } + expect(spec.env.SHELL).toBe('/bin/zsh') + expect(spec.env.ORCA_BACKGROUND_LAUNCH).toBe('1') + expect(JSON.parse(spec.input)).toEqual(args) + }) +}) diff --git a/src/main/bundled-bun-test-execution.ts b/src/main/bundled-bun-test-execution.ts new file mode 100644 index 000000000000..01418bd6477c --- /dev/null +++ b/src/main/bundled-bun-test-execution.ts @@ -0,0 +1,50 @@ +import { existsSync } from 'node:fs' +import { join } from 'node:path' +import { runProcess } from '../shared/child-process/run-process' +import { ORCAD_BUN_VERSION } from '../shared/orcad-bun-runtime' +import { orcadBunRuntimeFilename } from '../shared/orcad-artifacts' + +/** Runs real PTY fixtures under the same pinned runtime as the terminal service. */ +export async function runBundledBunFixture( + modulePath: string, + exportName: string, + args: unknown, + timeoutMs: number +): Promise { + const filename = orcadBunRuntimeFilename(process.platform) + const runtime = [ + process.env.BUN_EXECUTABLE, + join(process.cwd(), 'out', 'orcad', filename), + join(process.cwd(), 'out', 'cli-runtime', `${process.platform}-${process.arch}`, filename) + ].find((candidate): candidate is string => Boolean(candidate && existsSync(candidate))) + if (!runtime) { + throw new Error('Real terminal tests require the pinned bundled Bun runtime') + } + const script = ` + if (process.versions.bun !== ${JSON.stringify(ORCAD_BUN_VERSION)}) throw new Error('Unexpected Bun version'); + const args = JSON.parse(require('node:fs').readFileSync(0, 'utf8')); + const fixture = require(${JSON.stringify(modulePath)}); + Promise.resolve(fixture[${JSON.stringify(exportName)}](args)).then( + result => process.stdout.write(JSON.stringify(result ?? null)), + error => { console.error(error); process.exitCode = 1 } + ); + ` + const env = { ...process.env, ORCA_BACKGROUND_LAUNCH: '1' } + for (const key of Object.keys(env)) { + if (/^(NODE_OPTIONS|NODE_PATH|BUN_OPTIONS|BUN_INSPECT.*)$/i.test(key)) { + Reflect.deleteProperty(env, key) + } + } + const result = await runProcess({ + program: runtime, + args: ['-e', script], + input: JSON.stringify(args), + env, + timeoutMs, + maxOutputBytes: 4 * 1024 * 1024 + }) + if (result.code !== 0 || result.timedOut || result.outputTruncated) { + throw new Error(`Bun terminal fixture failed: ${result.stderr}`) + } + return JSON.parse(result.stdout) +} diff --git a/src/main/claude-accounts/runtime-auth-captured-wsl-owner.test.ts b/src/main/claude-accounts/runtime-auth-captured-wsl-owner.test.ts new file mode 100644 index 000000000000..70c957f56a57 --- /dev/null +++ b/src/main/claude-accounts/runtime-auth-captured-wsl-owner.test.ts @@ -0,0 +1,107 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { + cleanupRuntimeAuthTestState, + createClaudeAccount, + createClaudeCredentialsJson, + createElectronMock, + createKeychainMock, + createManagedClaudeAuth, + createOauthRefreshMock, + createSettings, + createStore, + resetRuntimeAuthTestState, + setPlatform, + testState +} from './runtime-auth-service-test-harness' +vi.mock('electron', () => createElectronMock()) +vi.mock('./oauth-refresh', () => createOauthRefreshMock()) +vi.mock('./keychain', () => createKeychainMock()) +const execution = { distro: 'Ubuntu', userName: 'alice', userId: '1000', home: '/home/alice' } +const target = { runtime: 'wsl' as const, wslDistro: 'Ubuntu' } +beforeEach(() => { + resetRuntimeAuthTestState() + setPlatform('win32') +}) +afterEach(cleanupRuntimeAuthTestState) + +describe('captured WSL Claude account preparation', () => { + it.each([true, false])( + 'pins ownership reads and preserves selection when owned=%s', + async (owned) => { + const localPath = createManagedClaudeAuth( + testState.userDataDir, + 'alice-account', + createClaudeCredentialsJson('alice@example.test', 'token') + ) + let defaultUser = 'alice' + const runWslProcess = vi.fn(async (spec: { user?: string; script?: string }) => { + await Promise.resolve() + defaultUser = 'bob' + expect(spec.user).toBe('alice') + expect(spec.script).toContain('test "$(id -u)" = \'1000\'') + expect(spec.script).toContain('test "$HOME" = \'/home/alice\'') + return { + code: owned ? 0 : 35, + stdout: spec.script?.includes('exec cat') + ? createClaudeCredentialsJson('alice@example.test', 'token') + : '/home/alice/.local/share/orca/claude-accounts/alice-account/auth', + stderr: '', + timedOut: false + } + }) + vi.doMock('../wsl/wsl-runner', () => ({ runWslProcess })) + vi.doMock('../wsl', () => ({ + getDefaultWslDistro: () => 'Ubuntu', + getWslHome: () => `\\\\wsl.localhost\\Ubuntu\\home\\${defaultUser}`, + toWindowsWslPath: () => localPath + })) + const settings = createSettings({ + localAccountRuntime: 'host', + claudeManagedAccounts: [ + createClaudeAccount( + 'alice-account', + '\\\\wsl.localhost\\Ubuntu\\home\\alice\\.local\\share\\orca\\claude-accounts\\alice-account\\auth', + { + managedAuthRuntime: 'wsl', + wslDistro: 'Ubuntu', + wslLinuxAuthPath: '/home/alice/.local/share/orca/claude-accounts/alice-account/auth' + } + ) + ], + activeClaudeManagedAccountIdsByRuntime: { host: null, wsl: { Ubuntu: 'alice-account' } } + }) + const store = createStore(settings) + const { ClaudeRuntimeAuthService } = await import('./runtime-auth-service') + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Auth preparation uses only the fixture getSettings/updateSettings methods. + const service = new ClaudeRuntimeAuthService(store as never) + if (owned) { + expect(await service.prepareForClaudeLaunch(target, execution)).toMatchObject({ + wslExecution: execution, + envPatch: { + CLAUDE_CONFIG_DIR: '/home/alice/.local/share/orca/claude-accounts/alice-account/auth' + } + }) + } else { + await expect(service.prepareForClaudeLaunch(target, execution)).rejects.toThrow( + 'captured execution owner' + ) + } + expect(runWslProcess).toHaveBeenCalledTimes(owned ? 2 : 1) + expect(settings.activeClaudeManagedAccountIdsByRuntime?.wsl.Ubuntu).toBe('alice-account') + } + ) + it('uses the captured home for system auth after the distro default changes', async () => { + vi.doMock('../wsl', () => ({ + getDefaultWslDistro: () => 'Ubuntu', + getWslHome: () => '\\\\wsl.localhost\\Ubuntu\\home\\bob' + })) + const store = createStore(createSettings({ localAccountRuntime: 'host' })) + const { ClaudeRuntimeAuthService } = await import('./runtime-auth-service') + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Auth preparation uses only the fixture getSettings/updateSettings methods. + const service = new ClaudeRuntimeAuthService(store as never) + expect(await service.prepareForClaudeLaunch(target, execution)).toMatchObject({ + wslExecution: execution, + wslLinuxConfigDir: '/home/alice/.claude' + }) + }) +}) diff --git a/src/main/claude-accounts/runtime-auth-service.ts b/src/main/claude-accounts/runtime-auth-service.ts index dead38a7780f..03209be94b02 100644 --- a/src/main/claude-accounts/runtime-auth-service.ts +++ b/src/main/claude-accounts/runtime-auth-service.ts @@ -1,3 +1,4 @@ +import type { WslAccountExecutionContext } from '../wsl/wsl-account-execution-context' import type { Store } from '../persistence' import { getSelectedClaudeAccountIdForTarget, @@ -16,11 +17,30 @@ export class ClaudeRuntimeAuthService extends ClaudeRuntimeAuthSync { } async prepareForClaudeLaunch( - target?: ClaudeAccountSelectionTarget + target?: ClaudeAccountSelectionTarget, + execution?: WslAccountExecutionContext ): Promise { - const effectiveTarget = target ?? this.getDefaultAccountSelectionTarget() - await this.syncForCurrentSelection(effectiveTarget) - return this.getPreparation(effectiveTarget) + const effectiveTarget = { ...(target ?? this.getDefaultAccountSelectionTarget()) } + const captured = execution ? Object.freeze({ ...execution }) : undefined + return this.serializeMutation(async () => { + const selected = getSelectedClaudeAccountIdForTarget( + this.store.getSettings(), + effectiveTarget + ) + const before = captured ? this.getPreparation(effectiveTarget, captured) : undefined + await this.doSyncForCurrentSelection(effectiveTarget, captured) + const preparation = this.getPreparation(effectiveTarget, captured) + if ( + captured && + (selected !== + getSelectedClaudeAccountIdForTarget(this.store.getSettings(), effectiveTarget) || + before?.configDir !== preparation.configDir || + before?.wslLinuxConfigDir !== preparation.wslLinuxConfigDir) + ) { + throw new Error('Claude account selection changed during guest preparation') + } + return { ...preparation, ...(captured ? { wslExecution: captured } : {}) } + }) } async prepareForRateLimitFetch( diff --git a/src/main/claude-accounts/runtime-auth/runtime-auth-managed-credentials.ts b/src/main/claude-accounts/runtime-auth/runtime-auth-managed-credentials.ts index ad68d59eadb7..4099cf7573ed 100644 --- a/src/main/claude-accounts/runtime-auth/runtime-auth-managed-credentials.ts +++ b/src/main/claude-accounts/runtime-auth/runtime-auth-managed-credentials.ts @@ -1,3 +1,4 @@ +import type { WslAccountExecutionContext } from '../../wsl/wsl-account-execution-context' import { existsSync } from 'node:fs' import type { ClaudeManagedAccount } from '../../../shared/managed-account-types' import { parseWslUncPath } from '../../../shared/wsl-paths' @@ -22,11 +23,36 @@ function shellQuote(value: string): string { } export class ClaudeRuntimeAuthManagedCredentials extends ClaudeRuntimeAuthCredentialIdentity { - protected async readManagedCredentials(account: ClaudeManagedAccount): Promise { - const managedAuthPath = await this.getOwnedManagedAuthPath(account) + protected async readManagedCredentials( + account: ClaudeManagedAccount, + execution?: WslAccountExecutionContext + ): Promise { + const managedAuthPath = await this.getOwnedManagedAuthPath(account, execution) if (!managedAuthPath) { return null } + if (execution) { + const result = await runWslProcess({ + distro: execution.distro, + user: execution.userName, + loginPath: 'none', + script: [ + 'set -e', + `test "$(id -u)" = ${shellQuote(execution.userId)}`, + `test "$HOME" = ${shellQuote(execution.home)}`, + 'exec cat -- "$1"' + ].join('\n'), + args: [`${account.wslLinuxAuthPath}/.credentials.json`], + timeoutMs: 5_000, + maxOutputBytes: 256 * 1024 + }) + if (result.code !== 0 || result.timedOut) { + throw new Error( + 'Selected Claude credentials are unavailable for the captured execution owner' + ) + } + return result.stdout + } if (process.platform === 'darwin') { return readManagedClaudeKeychainCredentials(account.id) } @@ -89,8 +115,19 @@ export class ClaudeRuntimeAuthManagedCredentials extends ClaudeRuntimeAuthCreden } } - protected async getOwnedManagedAuthPath(account: ClaudeManagedAccount): Promise { + protected async getOwnedManagedAuthPath( + account: ClaudeManagedAccount, + execution?: WslAccountExecutionContext + ): Promise { const wslInfo = parseWslUncPath(account.managedAuthPath) + if ( + execution && + (!wslInfo || + wslInfo.distro.toLowerCase() !== execution.distro.toLowerCase() || + account.wslLinuxAuthPath !== wslInfo.linuxPath) + ) { + throw new Error('Managed Claude account belongs to another execution owner') + } if (wslInfo) { if ( !wslInfo.linuxPath.includes('/.local/share/orca/claude-accounts/') || @@ -102,10 +139,17 @@ export class ClaudeRuntimeAuthManagedCredentials extends ClaudeRuntimeAuthCreden try { const owned = await runWslProcess({ distro: wslInfo.distro, + ...(execution ? { user: execution.userName } : {}), loginPath: 'none', shell: 'bash', script: [ 'set -euo pipefail', + ...(execution + ? [ + `test "$(id -u)" = ${shellQuote(execution.userId)}`, + `test "$HOME" = ${shellQuote(execution.home)}` + ] + : []), `candidate=${shellQuote(wslInfo.linuxPath)}`, 'managed_root="${HOME%/}/.local/share/orca/claude-accounts"', 'candidate_real=$(readlink -f -- "$candidate")', @@ -129,7 +173,7 @@ export class ClaudeRuntimeAuthManagedCredentials extends ClaudeRuntimeAuthCreden // caller persists that -- clearing the user's account selection. A // slow distro must not decide ownership. Swallowing it here is what // made the previous guard dead code. - if (error instanceof Error && error.message === OWNERSHIP_PROBE_TIMEOUT) { + if (execution || (error instanceof Error && error.message === OWNERSHIP_PROBE_TIMEOUT)) { throw error } return null diff --git a/src/main/claude-accounts/runtime-auth/runtime-auth-preparation.ts b/src/main/claude-accounts/runtime-auth/runtime-auth-preparation.ts index dabcd9d472f3..4650617fde00 100644 --- a/src/main/claude-accounts/runtime-auth/runtime-auth-preparation.ts +++ b/src/main/claude-accounts/runtime-auth/runtime-auth-preparation.ts @@ -1,7 +1,11 @@ +import { + assertWslAccountExecutionTarget, + type WslAccountExecutionContext +} from '../../wsl/wsl-account-execution-context' +import { parseWslUncPath, toWindowsWslUncPath } from '../../../shared/wsl-paths' import { join } from 'node:path' import type { ClaudeManagedAccount } from '../../../shared/managed-account-types' import { resolveLocalAccountRuntimeTarget } from '../../../shared/local-account-runtime' -import { parseWslUncPath } from '../../../shared/wsl-paths' import { shouldStripClaudeAuthEnvForAccount } from '../environment' import { getDefaultWslDistro, getWslHome } from '../../wsl' import { @@ -13,7 +17,13 @@ import { ClaudeRuntimeAuthSnapshotRestore } from './runtime-auth-snapshot-restor import type { ClaudeRuntimeAuthPreparation } from './runtime-auth-types' export class ClaudeRuntimeAuthPreparationService extends ClaudeRuntimeAuthSnapshotRestore { - protected getPreparation(target?: ClaudeAccountSelectionTarget): ClaudeRuntimeAuthPreparation { + protected getPreparation( + target?: ClaudeAccountSelectionTarget, + execution?: WslAccountExecutionContext + ): ClaudeRuntimeAuthPreparation { + if (execution) { + assertWslAccountExecutionTarget(execution, target) + } const settings = this.store.getSettings() const paths = this.pathResolver.getRuntimePaths() const normalizedTarget = this.resolveWslDefaultTarget( @@ -39,7 +49,11 @@ export class ClaudeRuntimeAuthPreparationService extends ClaudeRuntimeAuthSnapsh if (normalizeClaudeAccountSelectionTarget(normalizedTarget).runtime === 'wsl') { const distro = normalizeClaudeAccountSelectionTarget(normalizedTarget).wslDistro ?? getDefaultWslDistro() - const wslHome = distro ? getWslHome(distro) : null + const wslHome = execution + ? toWindowsWslUncPath(execution.home, execution.distro) + : distro + ? getWslHome(distro) + : null const wslHomeInfo = wslHome ? parseWslUncPath(wslHome) : null if (distro && wslHome && wslHomeInfo) { const windowsConfigDir = join(wslHome, '.claude') diff --git a/src/main/claude-accounts/runtime-auth/runtime-auth-sync.ts b/src/main/claude-accounts/runtime-auth/runtime-auth-sync.ts index f191689a4466..cbb3632edc18 100644 --- a/src/main/claude-accounts/runtime-auth/runtime-auth-sync.ts +++ b/src/main/claude-accounts/runtime-auth/runtime-auth-sync.ts @@ -1,3 +1,7 @@ +import { + assertWslAccountExecutionTarget, + type WslAccountExecutionContext +} from '../../wsl/wsl-account-execution-context' import { existsSync, readFileSync } from 'node:fs' import { getSelectedClaudeAccountIdForTarget, @@ -12,12 +16,28 @@ import { writeActiveClaudeKeychainCredentialsForRuntime } from '../keychain' import { ClaudeRuntimeAuthPreparationService } from './runtime-auth-preparation' export class ClaudeRuntimeAuthSync extends ClaudeRuntimeAuthPreparationService { - protected async doSyncForCurrentSelection(target?: ClaudeAccountSelectionTarget): Promise { + protected async doSyncForCurrentSelection( + target?: ClaudeAccountSelectionTarget, + execution?: WslAccountExecutionContext + ): Promise { const settings = this.store.getSettings() const effectiveTarget = this.resolveWslDefaultTarget(target) const normalizedTarget = normalizeClaudeAccountSelectionTarget(effectiveTarget) const activeAccountId = getSelectedClaudeAccountIdForTarget(settings, normalizedTarget) const activeAccount = this.getActiveAccount(settings.claudeManagedAccounts, activeAccountId) + if (execution) { + assertWslAccountExecutionTarget(execution, effectiveTarget) + if (activeAccountId && (!activeAccount || activeAccount.managedAuthRuntime !== 'wsl')) { + throw new Error('Selected Claude account is unavailable for the captured execution owner') + } + if (activeAccount) { + const credentials = await this.readManagedCredentials(activeAccount, execution) + if (!credentials || !this.isValidCredentialsJsonObject(credentials)) { + throw new Error('Selected Claude account is unavailable for the captured execution owner') + } + } + return + } const previousAccount = this.getActiveAccount( settings.claudeManagedAccounts, this.lastSyncedAccountId diff --git a/src/main/claude-accounts/runtime-auth/runtime-auth-types.ts b/src/main/claude-accounts/runtime-auth/runtime-auth-types.ts index 77f0394e5719..788221792a21 100644 --- a/src/main/claude-accounts/runtime-auth/runtime-auth-types.ts +++ b/src/main/claude-accounts/runtime-auth/runtime-auth-types.ts @@ -1,7 +1,9 @@ +import type { WslAccountExecutionContext } from '../../wsl/wsl-account-execution-context' import type { ClaudeManagedAccount } from '../../../shared/managed-account-types' import type { ClaudeEnvPatch } from '../environment' export type ClaudeRuntimeAuthPreparation = { + wslExecution?: WslAccountExecutionContext configDir: string runtime?: 'host' | 'wsl' wslDistro?: string | null diff --git a/src/main/cli/packaged-cli-assets.test.ts b/src/main/cli/packaged-cli-assets.test.ts index 3391a8a8c0fb..f7d0735f7621 100644 --- a/src/main/cli/packaged-cli-assets.test.ts +++ b/src/main/cli/packaged-cli-assets.test.ts @@ -124,7 +124,6 @@ describe('packaged CLI assets', () => { join('node_modules', 'zod'), join('node_modules', 'yaml'), join('node_modules', 'jsonc-parser'), - join('node_modules', 'node-pty'), join('node_modules', 'sherpa-onnx-darwin-${arch}'), join('node_modules', 'sherpa-onnx-linux-${arch}'), join('node_modules', 'sherpa-onnx-win-x64') diff --git a/src/main/codex-accounts/captured-wsl-account-process.test.ts b/src/main/codex-accounts/captured-wsl-account-process.test.ts new file mode 100644 index 000000000000..08bc4f11091d --- /dev/null +++ b/src/main/codex-accounts/captured-wsl-account-process.test.ts @@ -0,0 +1,144 @@ +import { mkdtempSync, existsSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { runProcess } from '../../shared/child-process/run-process' +import { shellEscape } from '../ssh/ssh-connection-utils' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { runCapturedCodexWslProcess } from './captured-wsl-account-process' +import { + drainLegacyWslRuntimeAuth, + startLegacyWslRuntimeAuthDrain, + _internals +} from './legacy-wsl-runtime-auth-drain' +import { startWslCodexSessionBridgeInBackground } from '../codex/wsl-codex-session-bridge' + +const { runWslProcessMock } = vi.hoisted(() => ({ runWslProcessMock: vi.fn() })) +vi.mock('../wsl/wsl-runner', () => ({ runWslProcess: runWslProcessMock })) +const owner = { distro: 'Ubuntu', userName: 'alice', userId: '1000', home: '/home/alice' } +const result = (code = 0, stdout = '') => ({ + code, + stdout, + stderr: '', + timedOut: false, + environmentResolved: true +}) +const options = () => ({ + distro: 'Ubuntu', + guestHomeLinuxPath: '/home/alice', + legacyPanePresent: true, + execution: { ...owner }, + resolveDestination: vi.fn(() => null) +}) + +beforeEach(() => { + runWslProcessMock.mockReset() + _internals.resetDrainQueue() +}) + +describe('captured WSL Codex owner', () => { + it.skipIf(process.platform === 'win32')( + 'refuses UID drift before a real shell writes credentials', + async () => { + const directory = mkdtempSync(join(tmpdir(), 'orca-captured-wsl-')) + const destination = join(directory, 'auth.json') + runWslProcessMock.mockImplementation((spec) => + runProcess({ program: '/bin/sh', args: ['-c', spec.script] }) + ) + try { + const response = await runCapturedCodexWslProcess( + { distro: 'Ubuntu', loginPath: 'none', script: `touch ${shellEscape(destination)}` }, + { ...owner, userId: String((process.getuid?.() ?? 0) + 1) } + ) + expect(response.code).toBe(79) + expect(existsSync(destination)).toBe(false) + } finally { + rmSync(directory, { recursive: true, force: true }) + } + } + ) + + it('pins the user and checks UID/home before the credential script', async () => { + runWslProcessMock.mockResolvedValue(result()) + await runCapturedCodexWslProcess( + { distro: 'Ubuntu', script: 'touch auth.json', loginPath: 'none' }, + owner + ) + const spec = runWslProcessMock.mock.calls[0]?.[0] + expect(spec.user).toBe('alice') + expect(spec.script).toContain('id -u') + expect(spec.script).toContain('1000') + expect(spec.script).toContain('/home/alice') + expect(spec.script.indexOf('exit 79')).toBeLessThan(spec.script.indexOf('touch auth.json')) + }) + + it('rejects a different distro and drain home before guest work', async () => { + expect(() => + runCapturedCodexWslProcess( + { distro: 'Debian', script: 'touch auth.json', loginPath: 'none' }, + owner + ) + ).toThrow('captured execution owner') + await expect( + drainLegacyWslRuntimeAuth({ ...options(), guestHomeLinuxPath: '/home/bob' }) + ).rejects.toThrow('captured owner') + expect(runWslProcessMock).not.toHaveBeenCalled() + }) + + it('keeps the captured user through an asynchronous drain destination lookup', async () => { + const execution = { ...owner } + const fresh = '{"tokens":{"expires_at":2000}}' + const stale = '{"tokens":{"expires_at":1000}}' + runWslProcessMock + .mockResolvedValueOnce( + result(0, [Buffer.from(fresh).toString('base64'), 'missing', ''].join('\n')) + ) + .mockResolvedValue(result()) + await drainLegacyWslRuntimeAuth({ + ...options(), + execution, + resolveDestination: async () => { + execution.userName = 'bob' + execution.userId = '1001' + execution.home = '/home/bob' + await Promise.resolve() + return { authContents: stale, linuxHomePath: '/home/alice/.codex' } + } + }) + expect(runWslProcessMock).toHaveBeenCalledTimes(2) + for (const [spec] of runWslProcessMock.mock.calls) { + expect(spec.user).toBe('alice') + expect(spec.script).toContain('/home/alice') + expect(spec.script).not.toContain('/home/bob') + } + }) + + it('does not coalesce drains for two users in the same distro', async () => { + runWslProcessMock.mockResolvedValue(result(0, '')) + await Promise.all([ + startLegacyWslRuntimeAuthDrain(options()), + startLegacyWslRuntimeAuthDrain({ + ...options(), + guestHomeLinuxPath: '/home/bob', + execution: { ...owner, userName: 'bob', userId: '1001', home: '/home/bob' } + }) + ]) + expect(runWslProcessMock.mock.calls.map(([spec]) => spec.user).sort()).toEqual(['alice', 'bob']) + }) + + it('does not coalesce session bridges across captured users', async () => { + runWslProcessMock.mockResolvedValue(result()) + const target = { + distro: 'Ubuntu', + systemCodexHomePath: '/shared/system', + managedCodexHomePath: '/shared/managed' + } + await Promise.all([ + startWslCodexSessionBridgeInBackground({ ...target, execution: owner }), + startWslCodexSessionBridgeInBackground({ + ...target, + execution: { ...owner, userName: 'bob', userId: '1001', home: '/home/bob' } + }) + ]) + expect(runWslProcessMock.mock.calls.map(([spec]) => spec.user).sort()).toEqual(['alice', 'bob']) + }) +}) diff --git a/src/main/codex-accounts/captured-wsl-account-process.ts b/src/main/codex-accounts/captured-wsl-account-process.ts new file mode 100644 index 000000000000..d8413050af87 --- /dev/null +++ b/src/main/codex-accounts/captured-wsl-account-process.ts @@ -0,0 +1,19 @@ +import { shellEscape } from '../ssh/ssh-connection-utils' +import { runWslProcess, type WslSpec } from '../wsl/wsl-runner' +import { + assertWslAccountExecutionTarget, + type WslAccountExecutionContext +} from '../wsl/wsl-account-execution-context' + +/** Recheck the captured UID/home in the same guest shell before reading or mutating credentials. */ +export function runCapturedCodexWslProcess(spec: WslSpec, execution?: WslAccountExecutionContext) { + if (!execution) { + return runWslProcess(spec) + } + assertWslAccountExecutionTarget(execution, { runtime: 'wsl', wslDistro: spec.distro }) + if (spec.script === undefined) { + throw new Error('Captured Codex account operations require a script') + } + const guard = `[ "$(id -u)" = ${shellEscape(execution.userId)} ] && [ "$HOME" = ${shellEscape(execution.home)} ] || { echo 'WSL account owner changed' >&2; exit 79; }` + return runWslProcess({ ...spec, user: execution.userName, script: `${guard}\n${spec.script}` }) +} diff --git a/src/main/codex-accounts/legacy-wsl-runtime-auth-drain-owner.ts b/src/main/codex-accounts/legacy-wsl-runtime-auth-drain-owner.ts new file mode 100644 index 000000000000..05858bb028f3 --- /dev/null +++ b/src/main/codex-accounts/legacy-wsl-runtime-auth-drain-owner.ts @@ -0,0 +1,40 @@ +import { + assertWslAccountExecutionTarget, + type WslAccountExecutionContext +} from '../wsl/wsl-account-execution-context' +import type { LegacyWslRuntimeAuthDestination } from './legacy-wsl-runtime-auth-drain' + +export type LegacyWslRuntimeAuthDrainOptions = { + execution?: WslAccountExecutionContext + distro: string + guestHomeLinuxPath: string + legacyPanePresent: boolean + resolveDestination: ( + runtimeAuthContents: string + ) => LegacyWslRuntimeAuthDestination | null | Promise +} + +export function captureDrainOptions( + options: LegacyWslRuntimeAuthDrainOptions +): LegacyWslRuntimeAuthDrainOptions { + if (!options.execution) { + return options + } + const execution = Object.freeze({ ...options.execution }) + assertWslAccountExecutionTarget(execution, { runtime: 'wsl', wslDistro: options.distro }) + if (options.guestHomeLinuxPath !== execution.home) { + throw new Error('WSL drain home does not match captured owner') + } + return { ...options, execution } +} + +export function drainOwnerKey(options: LegacyWslRuntimeAuthDrainOptions): string { + return options.execution + ? JSON.stringify([ + options.distro.toLowerCase(), + options.execution.userName, + options.execution.userId, + options.execution.home + ]) + : options.distro.trim().toLowerCase() +} diff --git a/src/main/codex-accounts/legacy-wsl-runtime-auth-drain.ts b/src/main/codex-accounts/legacy-wsl-runtime-auth-drain.ts index f282e879a77f..9439a24bedc2 100644 --- a/src/main/codex-accounts/legacy-wsl-runtime-auth-drain.ts +++ b/src/main/codex-accounts/legacy-wsl-runtime-auth-drain.ts @@ -1,8 +1,14 @@ +import type { WslAccountExecutionContext } from '../wsl/wsl-account-execution-context' import { createHash } from 'node:crypto' import { posix as pathPosix } from 'node:path' import { wslCodexRuntimeHomeForGuestHome } from '../pty/codex-home-wsl-env' import { WSL_SESSION_BRIDGE_TIMEOUT_MS } from '../codex/wsl-codex-session-bridge-script' -import { runWslProcess } from '../wsl/wsl-runner' +import { runCapturedCodexWslProcess } from './captured-wsl-account-process' +import { + captureDrainOptions, + drainOwnerKey, + type LegacyWslRuntimeAuthDrainOptions +} from './legacy-wsl-runtime-auth-drain-owner' import { compareCodexAuthFreshness, codexAuthIsFresher } from './codex-auth-identity' import { APPLY_LEGACY_AUTH_SCRIPT, @@ -27,15 +33,6 @@ type LegacyWslRuntimeInspection = { credentials: { kind: 'missing' } | { kind: 'present'; contents: string } } -type LegacyWslRuntimeAuthDrainOptions = { - distro: string - guestHomeLinuxPath: string - legacyPanePresent: boolean - resolveDestination: ( - runtimeAuthContents: string - ) => LegacyWslRuntimeAuthDestination | null | Promise -} - const drainQueueByDistro = new Map>() const completedDistroKeys = new Set() const pendingSessionBridgeRouteByDistro = new Map() @@ -69,7 +66,8 @@ export function startLegacyWslRuntimeAuthDrain( options: LegacyWslRuntimeAuthDrainOptions, startOptions: { throwOnFailure?: boolean } = {} ): Promise { - const key = options.distro.trim().toLowerCase() + options = captureDrainOptions(options) + const key = drainOwnerKey(options) if (completedDistroKeys.has(key)) { return Promise.resolve() } @@ -104,22 +102,26 @@ function logDrainFailure(task: Promise): Promise { export async function drainLegacyWslRuntimeAuth( options: LegacyWslRuntimeAuthDrainOptions ): Promise<'complete' | 'pending'> { - const distroKey = options.distro.trim().toLowerCase() + options = captureDrainOptions(options) + const distroKey = drainOwnerKey(options) const paths = resolveLegacyRuntimePaths(options.guestHomeLinuxPath) - const inspection = await runWslProcess({ - distro: options.distro, - loginPath: 'none', - script: INSPECT_LEGACY_AUTH_SCRIPT, - args: [paths.runtimeHome, paths.activeHome, paths.marker], - timeoutMs: 5_000, - maxOutputBytes: 2 * 1024 * 1024 - }) + const inspection = await runCapturedCodexWslProcess( + { + distro: options.distro, + loginPath: 'none', + script: INSPECT_LEGACY_AUTH_SCRIPT, + args: [paths.runtimeHome, paths.activeHome, paths.marker], + timeoutMs: 5_000, + maxOutputBytes: 2 * 1024 * 1024 + }, + options.execution + ) if (inspection.code === MARKER_PRESENT_EXIT) { return 'complete' } if (inspection.code === LEGACY_HOME_ABSENT_EXIT) { if (!options.legacyPanePresent) { - return finalizeAbsentLegacyAuth(options.distro, paths) + return finalizeAbsentLegacyAuth(options.distro, paths, options.execution) } return 'pending' } @@ -147,29 +149,34 @@ export async function drainLegacyWslRuntimeAuth( ].join('\0') const bridgeAllSessions = deleteSource || pendingSessionBridgeRouteByDistro.get(distroKey) !== sessionBridgeRoute - const result = await runWslProcess({ - distro: options.distro, - loginPath: 'none', - script: APPLY_LEGACY_AUTH_SCRIPT, - args: [ - paths.runtimeHome, - paths.activeHome, - paths.marker, - destination.linuxHomePath, - sha256(inspected.authContents), - sha256(destination.authContents), - promoteAuth ? '1' : '0', - deleteSource ? '1' : '0', - inspected.credentials.kind === 'present' ? sha256(inspected.credentials.contents) : 'missing', - bridgeAllSessions ? 'full' : 'recent' - ], - timeoutMs: bridgeAllSessions ? WSL_SESSION_BRIDGE_TIMEOUT_MS : 5_000, - maxOutputBytes: 16 * 1024 - }) + const result = await runCapturedCodexWslProcess( + { + distro: options.distro, + loginPath: 'none', + script: APPLY_LEGACY_AUTH_SCRIPT, + args: [ + paths.runtimeHome, + paths.activeHome, + paths.marker, + destination.linuxHomePath, + sha256(inspected.authContents), + sha256(destination.authContents), + promoteAuth ? '1' : '0', + deleteSource ? '1' : '0', + inspected.credentials.kind === 'present' + ? sha256(inspected.credentials.contents) + : 'missing', + bridgeAllSessions ? 'full' : 'recent' + ], + timeoutMs: bridgeAllSessions ? WSL_SESSION_BRIDGE_TIMEOUT_MS : 5_000, + maxOutputBytes: 16 * 1024 + }, + options.execution + ) try { assertSuccessfulDrainStep('apply', result) } catch { - return recoverAfterFailedApply(options.distro, paths) + return recoverAfterFailedApply(options.distro, paths, options.execution) } if (!deleteSource) { rememberPendingRoute(distroKey, sessionBridgeRoute) @@ -179,16 +186,20 @@ export async function drainLegacyWslRuntimeAuth( async function recoverAfterFailedApply( distro: string, - paths: ReturnType + paths: ReturnType, + execution?: WslAccountExecutionContext ): Promise<'complete' | 'pending'> { - const recovery = await runWslProcess({ - distro, - loginPath: 'none', - script: INSPECT_LEGACY_AUTH_SCRIPT, - args: [paths.runtimeHome, paths.activeHome, paths.marker], - timeoutMs: 5_000, - maxOutputBytes: 2 * 1024 * 1024 - }) + const recovery = await runCapturedCodexWslProcess( + { + distro, + loginPath: 'none', + script: INSPECT_LEGACY_AUTH_SCRIPT, + args: [paths.runtimeHome, paths.activeHome, paths.marker], + timeoutMs: 5_000, + maxOutputBytes: 2 * 1024 * 1024 + }, + execution + ) if (recovery.code === MARKER_PRESENT_EXIT) { return 'complete' } @@ -248,16 +259,20 @@ function resolveLegacyRuntimePaths(guestHomeLinuxPath: string): { async function finalizeAbsentLegacyAuth( distro: string, - paths: ReturnType + paths: ReturnType, + execution?: WslAccountExecutionContext ): Promise<'complete' | 'pending'> { - const result = await runWslProcess({ - distro, - loginPath: 'none', - script: FINALIZE_ABSENT_AUTH_SCRIPT, - args: [paths.runtimeHome, paths.activeHome, paths.marker], - timeoutMs: 5_000, - maxOutputBytes: 16 * 1024 - }) + const result = await runCapturedCodexWslProcess( + { + distro, + loginPath: 'none', + script: FINALIZE_ABSENT_AUTH_SCRIPT, + args: [paths.runtimeHome, paths.activeHome, paths.marker], + timeoutMs: 5_000, + maxOutputBytes: 16 * 1024 + }, + execution + ) if (result.code === LEGACY_HOME_STILL_PRESENT_EXIT) { return 'pending' } diff --git a/src/main/codex-accounts/runtime-home-captured-wsl-launch.test.ts b/src/main/codex-accounts/runtime-home-captured-wsl-launch.test.ts new file mode 100644 index 000000000000..eaba9f3e2e84 --- /dev/null +++ b/src/main/codex-accounts/runtime-home-captured-wsl-launch.test.ts @@ -0,0 +1,116 @@ +import type { CodexManagedAccount } from '../../shared/managed-account-types' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { toWindowsWslUncPath } from '../../shared/wsl-paths' +import type { WslAccountExecutionContext } from '../wsl/wsl-account-execution-context' +import type { CodexAccountSelectionTarget } from './runtime-selection' +import { createSettings } from './runtime-home-settings-test-fixtures' +import { + createStore, + setupRuntimeHomeTest, + teardownRuntimeHomeTest, + testState +} from './runtime-home-service-test-harness' + +vi.mock('electron', () => ({ app: { getPath: () => testState.userDataDir } })) +vi.mock('../wsl', () => ({ + getDefaultWslDistro: () => 'Other', + getWslHome: () => '/home/default-changed' +})) + +beforeEach(setupRuntimeHomeTest) +afterEach(teardownRuntimeHomeTest) + +const owner = { distro: 'Ubuntu', userName: 'alice', userId: '1000', home: '/home/alice' } + +describe('captured Codex launch preparation', () => { + it.each([ + { distro: 'Debian', home: '/home/alice/account' }, + { distro: 'Ubuntu', home: '/home/bob/account' } + ])( + 'refuses a selected managed account outside the captured owner: $distro $home', + async ({ distro, home }) => { + const { CodexRuntimeHomeService } = await import('./runtime-home-service') + class CapturedService extends CodexRuntimeHomeService { + resolveAccount(account: CodexManagedAccount): string | null { + return this.getWslLaunchCodexHomePath(account, 'Ubuntu', owner) + } + } + const store = createStore(createSettings({})) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The fixture supplies the settings-store operations used during service construction. + const service = new CapturedService(store as never) + const account: CodexManagedAccount = { + id: 'selected', + email: 'test@example.com', + managedHomePath: toWindowsWslUncPath(home, distro), + managedHomeRuntime: 'wsl', + wslDistro: distro, + wslLinuxHomePath: home, + providerAccountId: null, + workspaceLabel: null, + workspaceAccountId: null, + createdAt: 1, + updatedAt: 1, + lastAuthenticatedAt: 1 + } + expect(() => service.resolveAccount(account)).toThrow('captured WSL') + } + ) + + it('pins the selected home and finishing context while the drain awaits', async () => { + const { CodexRuntimeHomeService } = await import('./runtime-home-service') + const execution = { ...owner } + const finish = vi.fn() + class CapturedService extends CodexRuntimeHomeService { + protected override async startLegacyWslAuthDrain(): Promise { + execution.userName = 'bob' + execution.home = '/home/bob' + await Promise.resolve() + } + protected override finishWslLaunchPreparation( + target: CodexAccountSelectionTarget, + home: string | null, + captured?: WslAccountExecutionContext + ): void { + finish(target, home, captured) + } + } + const store = createStore( + createSettings({ + activeCodexManagedAccountId: null, + activeCodexManagedAccountIdsByRuntime: { host: null, wsl: { Ubuntu: null } } + }) + ) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The fixture supplies every settings-store operation used by launch preparation. + const service = new CapturedService(store as never) + const home = await service.prepareForCodexLaunchAsync( + { runtime: 'wsl', wslDistro: 'Ubuntu' }, + undefined, + { wslExecution: execution } + ) + expect(home).toBe(toWindowsWslUncPath('/home/alice/.codex', 'Ubuntu')) + expect(finish).toHaveBeenCalledWith({ runtime: 'wsl', wslDistro: 'Ubuntu' }, home, owner) + expect(Object.isFrozen(finish.mock.calls[0]?.[2])).toBe(true) + }) + + it('refuses a mismatched target before drain or finishing', async () => { + const { CodexRuntimeHomeService } = await import('./runtime-home-service') + const work = vi.fn() + class CapturedService extends CodexRuntimeHomeService { + protected override async startLegacyWslAuthDrain(): Promise { + work() + } + protected override finishWslLaunchPreparation(): void { + work() + } + } + const store = createStore(createSettings({})) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The fixture supplies the settings-store operations used during service construction. + const service = new CapturedService(store as never) + await expect( + service.prepareForCodexLaunchAsync({ runtime: 'wsl', wslDistro: 'Debian' }, undefined, { + wslExecution: owner + }) + ).rejects.toThrow('captured execution owner') + expect(work).not.toHaveBeenCalled() + }) +}) diff --git a/src/main/codex-accounts/runtime-home-service-home-routing.ts b/src/main/codex-accounts/runtime-home-service-home-routing.ts index 19eceda6c0a3..38ef99543d4b 100644 --- a/src/main/codex-accounts/runtime-home-service-home-routing.ts +++ b/src/main/codex-accounts/runtime-home-service-home-routing.ts @@ -1,3 +1,7 @@ +import { + assertWslAccountExecutionTarget, + type WslAccountExecutionContext +} from '../wsl/wsl-account-execution-context' import { posix as pathPosix } from 'node:path' import { parseWslUncPath, toLinuxPath, toWindowsWslUncPath } from '../../shared/wsl-paths' import { normalizeRuntimePathForComparison } from '../../shared/cross-platform-path' @@ -209,7 +213,14 @@ export abstract class CodexRuntimeHomeRouting extends CodexRuntimeHomeManagedHom await Promise.all(drains) } - protected getWslSystemCodexHomePath(target: CodexAccountSelectionTarget): string | null { + protected getWslSystemCodexHomePath( + target: CodexAccountSelectionTarget, + execution?: WslAccountExecutionContext + ): string | null { + if (execution) { + assertWslAccountExecutionTarget(execution, target) + return toWindowsWslUncPath(pathPosix.join(execution.home, '.codex'), execution.distro) + } if (process.platform !== 'win32') { return null } @@ -229,15 +240,17 @@ export abstract class CodexRuntimeHomeRouting extends CodexRuntimeHomeManagedHom protected finishWslLaunchPreparation( target: CodexAccountSelectionTarget, - homePath: string | null + homePath: string | null, + execution?: WslAccountExecutionContext ): void { - this.syncWslConfigAndGlobalInstructionsForLaunch(target, homePath) - this.startWslSessionBridgeForLaunch(target, homePath) + this.syncWslConfigAndGlobalInstructionsForLaunch(target, homePath, execution) + this.startWslSessionBridgeForLaunch(target, homePath, execution) } protected syncWslConfigAndGlobalInstructionsForLaunch( target: CodexAccountSelectionTarget, - runtimeHomePath: string | null + runtimeHomePath: string | null, + execution?: WslAccountExecutionContext ): void { if (!runtimeHomePath) { return @@ -247,7 +260,10 @@ export abstract class CodexRuntimeHomeRouting extends CodexRuntimeHomeManagedHom if (!distro) { return } - const systemHomePath = this.getWslSystemCodexHomePath({ runtime: 'wsl', wslDistro: distro }) + const systemHomePath = this.getWslSystemCodexHomePath( + { runtime: 'wsl', wslDistro: distro }, + execution + ) if (!systemHomePath || systemHomePath === runtimeHomePath) { return } diff --git a/src/main/codex-accounts/runtime-home-service-launch.ts b/src/main/codex-accounts/runtime-home-service-launch.ts index 7356d041e774..006e19470b20 100644 --- a/src/main/codex-accounts/runtime-home-service-launch.ts +++ b/src/main/codex-accounts/runtime-home-service-launch.ts @@ -1,3 +1,7 @@ +import { + assertWslAccountExecutionTarget, + type WslAccountExecutionContext +} from '../wsl/wsl-account-execution-context' import { resolveHostCodexSessionSourceHome } from '../codex/codex-session-source-home' import { startSystemCodexSessionBridgeInBackground } from '../codex/codex-session-bridge' import { @@ -40,13 +44,17 @@ export abstract class CodexRuntimeHomeLaunch extends CodexRuntimeHomeRouting { prepareForCodexLaunch( target?: CodexAccountSelectionTarget, launchEnv?: NodeJS.ProcessEnv, - options?: { unavailableManagedHomePath?: string } + options?: { unavailableManagedHomePath?: string; wslExecution?: WslAccountExecutionContext } ): string | null { + const execution = options?.wslExecution ? Object.freeze({ ...options.wslExecution }) : undefined + if (execution) { + assertWslAccountExecutionTarget(execution, target) + } if (target?.runtime === 'wsl') { const wslTarget = this.resolveWslDefaultTarget(target) - const homePath = this.getWslCodexHomePathForSelection(wslTarget) - this.startLegacyWslAuthDrain(wslTarget) - this.finishWslLaunchPreparation(wslTarget, homePath) + const homePath = this.getWslCodexHomePathForSelection(wslTarget, execution) + this.startLegacyWslAuthDrain(wslTarget, { wslExecution: execution }) + this.finishWslLaunchPreparation(wslTarget, homePath, execution) return homePath } const selfContainedAccount = this.getSelfContainedManagedHostAccount() @@ -114,17 +122,21 @@ export abstract class CodexRuntimeHomeLaunch extends CodexRuntimeHomeRouting { async prepareForCodexLaunchAsync( target?: CodexAccountSelectionTarget, launchEnv?: NodeJS.ProcessEnv, - options?: { unavailableManagedHomePath?: string } + options?: { unavailableManagedHomePath?: string; wslExecution?: WslAccountExecutionContext } ): Promise { + const execution = options?.wslExecution ? Object.freeze({ ...options.wslExecution }) : undefined + if (execution) { + assertWslAccountExecutionTarget(execution, target) + } if (target?.runtime !== 'wsl') { return this.prepareForCodexLaunch(target, launchEnv, options) } const wslTarget = this.resolveWslDefaultTarget(target) - const homePath = this.getWslCodexHomePathForSelection(wslTarget) + const homePath = this.getWslCodexHomePathForSelection(wslTarget, execution) // Why: the retired home may hold the freshest credential, so the first // direct-home Codex spawn must wait for its bounded guest transaction. - await this.startLegacyWslAuthDrain(wslTarget, { throwOnFailure: true }) - this.finishWslLaunchPreparation(wslTarget, homePath) + await this.startLegacyWslAuthDrain(wslTarget, { throwOnFailure: true, wslExecution: execution }) + this.finishWslLaunchPreparation(wslTarget, homePath, execution) return homePath } diff --git a/src/main/codex-accounts/runtime-home-service-managed-home.ts b/src/main/codex-accounts/runtime-home-service-managed-home.ts index 817b2f7adc41..e133748c350a 100644 --- a/src/main/codex-accounts/runtime-home-service-managed-home.ts +++ b/src/main/codex-accounts/runtime-home-service-managed-home.ts @@ -1,3 +1,4 @@ +import type { WslAccountExecutionContext } from '../wsl/wsl-account-execution-context' import { join } from 'node:path' import { syncSystemCodexResourcesIntoManagedHome, @@ -252,7 +253,8 @@ export abstract class CodexRuntimeHomeManagedHome extends CodexRuntimeHomeSync { protected startWslSessionBridgeForLaunch( target: CodexAccountSelectionTarget, - runtimeHomePath: string | null + runtimeHomePath: string | null, + execution?: WslAccountExecutionContext ): void { if (process.platform !== 'win32' || !runtimeHomePath) { return @@ -265,13 +267,14 @@ export abstract class CodexRuntimeHomeManagedHome extends CodexRuntimeHomeSync { // Why: history-only override lets custom-CODEX_HOME users bridge from their real home; falls back to /.codex. const systemCodexHomePath = resolveWslCodexSessionSourceHome(this.store.getSettings(), distro) ?? - this.getWslSystemCodexHomePath({ runtime: 'wsl', wslDistro: distro }) + this.getWslSystemCodexHomePath({ runtime: 'wsl', wslDistro: distro }, execution) if (systemCodexHomePath && systemCodexHomePath !== runtimeHomePath) { // Why: WSL history must be hardlinked inside the distro; host-side links can't bridge Windows and WSL filesystems in a resume-visible way. void startWslCodexSessionBridgeInBackground({ distro, systemCodexHomePath, - managedCodexHomePath: runtimeHomePath + managedCodexHomePath: runtimeHomePath, + ...(execution ? { execution } : {}) }) } } diff --git a/src/main/codex-accounts/runtime-home-service-state.ts b/src/main/codex-accounts/runtime-home-service-state.ts index d6a6691fbb44..3f4511a2b9ca 100644 --- a/src/main/codex-accounts/runtime-home-service-state.ts +++ b/src/main/codex-accounts/runtime-home-service-state.ts @@ -1,3 +1,4 @@ +import type { WslAccountExecutionContext } from '../wsl/wsl-account-execution-context' import type { CodexManagedAccount } from '../../shared/managed-account-types' import type { Store } from '../persistence' import { CodexCredentialAbsenceGrace } from './codex-credential-absence-grace' @@ -41,12 +42,12 @@ export abstract class CodexRuntimeHomeState { abstract prepareForCodexLaunch( target?: CodexAccountSelectionTarget, launchEnv?: NodeJS.ProcessEnv, - options?: { unavailableManagedHomePath?: string } + options?: { unavailableManagedHomePath?: string; wslExecution?: WslAccountExecutionContext } ): string | null abstract prepareForCodexLaunchAsync( target?: CodexAccountSelectionTarget, launchEnv?: NodeJS.ProcessEnv, - options?: { unavailableManagedHomePath?: string } + options?: { unavailableManagedHomePath?: string; wslExecution?: WslAccountExecutionContext } ): Promise abstract beginHostSystemDefaultSessionMigrationLaunch( codexHomePath: string | null, @@ -83,7 +84,8 @@ export abstract class CodexRuntimeHomeState { ): boolean | null protected abstract startWslSessionBridgeForLaunch( target: CodexAccountSelectionTarget, - runtimeHomePath: string | null + runtimeHomePath: string | null, + execution?: WslAccountExecutionContext ): void abstract getHostCodexHomePathsForSessionDiscovery(): string[] @@ -100,14 +102,19 @@ export abstract class CodexRuntimeHomeState { abstract reconcileLegacySharedHomeForRetainedPanes(): void abstract syncActiveWslSelectionsBeforeRestart(): Promise - protected abstract getWslSystemCodexHomePath(target: CodexAccountSelectionTarget): string | null + protected abstract getWslSystemCodexHomePath( + target: CodexAccountSelectionTarget, + execution?: WslAccountExecutionContext + ): string | null protected abstract finishWslLaunchPreparation( target: CodexAccountSelectionTarget, - homePath: string | null + homePath: string | null, + execution?: WslAccountExecutionContext ): void protected abstract syncWslConfigAndGlobalInstructionsForLaunch( target: CodexAccountSelectionTarget, - runtimeHomePath: string | null + runtimeHomePath: string | null, + execution?: WslAccountExecutionContext ): void abstract prepareForRateLimitFetch( target?: CodexAccountSelectionTarget @@ -136,19 +143,22 @@ export abstract class CodexRuntimeHomeState { target: CodexAccountSelectionTarget ): string | null protected abstract getWslCodexHomePathForSelection( - target: CodexAccountSelectionTarget + target: CodexAccountSelectionTarget, + execution?: WslAccountExecutionContext ): string | null protected abstract getWslLaunchCodexHomePath( account: CodexManagedAccount, - targetDistro: string | undefined + targetDistro: string | undefined, + execution?: WslAccountExecutionContext ): string | null protected abstract startLegacyWslAuthDrain( target: CodexAccountSelectionTarget, - options?: { throwOnFailure?: boolean } + options?: { throwOnFailure?: boolean; wslExecution?: WslAccountExecutionContext } ): Promise protected abstract resolveLegacyWslAuthDestination( distro: string, - runtimeAuthContents: string + runtimeAuthContents: string, + execution?: WslAccountExecutionContext ): Promise protected abstract joinWslPath(basePath: string, ...segments: string[]): string protected abstract resolveWslDefaultTarget( diff --git a/src/main/codex-accounts/runtime-home-service-wsl.ts b/src/main/codex-accounts/runtime-home-service-wsl.ts index cf0ec32a23f1..23bf2caa6ffd 100644 --- a/src/main/codex-accounts/runtime-home-service-wsl.ts +++ b/src/main/codex-accounts/runtime-home-service-wsl.ts @@ -1,3 +1,7 @@ +import { + assertWslAccountExecutionTarget, + type WslAccountExecutionContext +} from '../wsl/wsl-account-execution-context' import { join, win32 as pathWin32 } from 'node:path' import { parseWslUncPath, toLinuxPath, toWindowsWslUncPath } from '../../shared/wsl-paths' import { @@ -20,7 +24,10 @@ export abstract class CodexRuntimeHomeWsl extends CodexRuntimeHomeWslCore { return this.getWslCodexHomePathForSelection(target) } - protected getWslCodexHomePathForSelection(target: CodexAccountSelectionTarget): string | null { + protected getWslCodexHomePathForSelection( + target: CodexAccountSelectionTarget, + execution?: WslAccountExecutionContext + ): string | null { const settings = this.store.getSettings() const account = this.getActiveAccount( settings.codexManagedAccounts, @@ -28,24 +35,34 @@ export abstract class CodexRuntimeHomeWsl extends CodexRuntimeHomeWslCore { ) if (account) { const targetDistro = this.resolveWslDefaultTarget(target).wslDistro?.trim() - const accountHome = this.getWslLaunchCodexHomePath(account, targetDistro) + const accountHome = this.getWslLaunchCodexHomePath(account, targetDistro, execution) if (accountHome) { return accountHome } } - return this.getWslSystemCodexHomePath(target) + return this.getWslSystemCodexHomePath(target, execution) } protected getWslLaunchCodexHomePath( account: CodexManagedAccount, - targetDistro: string | undefined + targetDistro: string | undefined, + execution?: WslAccountExecutionContext ): string | null { const wslHome = this.getWslManagedHomeIdentity(account) if (!wslHome) { + if (execution) { + throw new Error('Selected Codex account has no captured WSL home') + } return null } + if (execution && !wslHome.linuxHomePath.startsWith(`${execution.home.replace(/\/$/, '')}/`)) { + throw new Error('Selected Codex account does not belong to the captured WSL home') + } const accountDistro = wslHome.distro if (targetDistro && accountDistro.toLowerCase() !== targetDistro.toLowerCase()) { + if (execution) { + throw new Error('Selected Codex account does not belong to the captured WSL distro') + } return null } if (/^[A-Za-z]:[\\/]/.test(account.managedHomePath)) { @@ -56,7 +73,7 @@ export abstract class CodexRuntimeHomeWsl extends CodexRuntimeHomeWslCore { protected startLegacyWslAuthDrain( target: CodexAccountSelectionTarget, - options: { throwOnFailure?: boolean } = {} + options: { throwOnFailure?: boolean; wslExecution?: WslAccountExecutionContext } = {} ): Promise { if (process.platform !== 'win32') { return Promise.resolve() @@ -65,7 +82,11 @@ export abstract class CodexRuntimeHomeWsl extends CodexRuntimeHomeWslCore { if (!distro) { return Promise.resolve() } - const guestHome = getWslHome(distro) + const execution = options.wslExecution + if (execution) { + assertWslAccountExecutionTarget(execution, target) + } + const guestHome = execution?.home ?? getWslHome(distro) const guestHomeLinuxPath = guestHome ? toLinuxPath(guestHome).trim() : '' if (!guestHomeLinuxPath.startsWith('/')) { return Promise.resolve() @@ -83,8 +104,9 @@ export abstract class CodexRuntimeHomeWsl extends CodexRuntimeHomeWslCore { distro, guestHomeLinuxPath, legacyPanePresent, + ...(execution ? { execution } : {}), resolveDestination: (runtimeAuthContents) => - this.resolveLegacyWslAuthDestination(distro, runtimeAuthContents) + this.resolveLegacyWslAuthDestination(distro, runtimeAuthContents, execution) }, options ) @@ -92,23 +114,32 @@ export abstract class CodexRuntimeHomeWsl extends CodexRuntimeHomeWslCore { protected async resolveLegacyWslAuthDestination( distro: string, - runtimeAuthContents: string + runtimeAuthContents: string, + execution?: WslAccountExecutionContext ): Promise { const accountHomes = this.store.getSettings().codexManagedAccounts.flatMap((account) => { const wslHome = this.getWslManagedHomeIdentity(account) - return wslHome?.distro.toLowerCase() === distro.toLowerCase() + return wslHome?.distro.toLowerCase() === distro.toLowerCase() && + (!execution || wslHome.linuxHomePath.startsWith(`${execution.home.replace(/\/$/, '')}/`)) ? [{ account, linuxPath: wslHome.linuxHomePath }] : [] }) const accounts = accountHomes.map(({ account }) => account) - const systemHome = this.getWslSystemCodexHomePath({ runtime: 'wsl', wslDistro: distro }) + const systemHome = this.getWslSystemCodexHomePath( + { runtime: 'wsl', wslDistro: distro }, + execution + ) const parsedSystemHome = systemHome ? parseWslUncPath(systemHome) : null let reads: WslCodexAuthRead[] try { - reads = await readWslCodexAuths(distro, [ - ...accountHomes.map(({ linuxPath }) => linuxPath), - ...(parsedSystemHome ? [parsedSystemHome.linuxPath] : []) - ]) + reads = await readWslCodexAuths( + distro, + [ + ...accountHomes.map(({ linuxPath }) => linuxPath), + ...(parsedSystemHome ? [parsedSystemHome.linuxPath] : []) + ], + execution + ) } catch { reads = accountHomes.map(() => ({ kind: 'unreadable' })) if (parsedSystemHome) { diff --git a/src/main/codex-accounts/wsl-codex-auth-batch-reader.ts b/src/main/codex-accounts/wsl-codex-auth-batch-reader.ts index 40870fcd5ef7..63918b0a3c4a 100644 --- a/src/main/codex-accounts/wsl-codex-auth-batch-reader.ts +++ b/src/main/codex-accounts/wsl-codex-auth-batch-reader.ts @@ -1,4 +1,5 @@ -import { runWslProcess } from '../wsl/wsl-runner' +import { runCapturedCodexWslProcess } from './captured-wsl-account-process' +import type { WslAccountExecutionContext } from '../wsl/wsl-account-execution-context' export type WslCodexAuthRead = | { kind: 'missing' | 'unreadable' } @@ -6,19 +7,23 @@ export type WslCodexAuthRead = export async function readWslCodexAuths( distro: string, - linuxHomePaths: readonly string[] + linuxHomePaths: readonly string[], + execution?: WslAccountExecutionContext ): Promise { if (linuxHomePaths.length === 0) { return [] } - const result = await runWslProcess({ - distro, - loginPath: 'none', - script: READ_AUTHS_SCRIPT, - args: linuxHomePaths, - timeoutMs: 5_000, - maxOutputBytes: 2 * 1024 * 1024 - }) + const result = await runCapturedCodexWslProcess( + { + distro, + loginPath: 'none', + script: READ_AUTHS_SCRIPT, + args: linuxHomePaths, + timeoutMs: 5_000, + maxOutputBytes: 2 * 1024 * 1024 + }, + execution + ) if (result.code !== 0 || result.timedOut) { return linuxHomePaths.map(() => ({ kind: 'unreadable' })) } diff --git a/src/main/codex/codex-pane-account-registry.ts b/src/main/codex/codex-pane-account-registry.ts index ebad86326762..d170e28cec85 100644 --- a/src/main/codex/codex-pane-account-registry.ts +++ b/src/main/codex/codex-pane-account-registry.ts @@ -342,7 +342,7 @@ export function hasRecordedManagedHostCodexPane(): boolean { ) } -/** Drops records whose daemon PTYs are authoritatively absent. */ +/** Native daemon inventory can retire native records, including legacy Windows-owned WSL. */ export function reconcileCodexPaneAccountsWithLivePtys(livePtyIds: readonly string[]): void { // Why: this deletes every pane not in the live list. Against an empty stand-in // it is a no-op, but the write it guards would still persist that stand-in @@ -362,7 +362,7 @@ export function reconcileCodexPaneAccountsWithLivePtys(livePtyIds: readonly stri changed = true } for (const ptyId of Object.keys(registry.panes)) { - if (!livePtyIdSet.has(ptyId)) { + if (!ptyId.startsWith('wsl:') && !ptyId.startsWith('ssh:') && !livePtyIdSet.has(ptyId)) { delete registry.panes[ptyId] changed = true } diff --git a/src/main/codex/codex-stale-pane-accounts.test.ts b/src/main/codex/codex-stale-pane-accounts.test.ts index 027d059f3424..0158b4b447ee 100644 --- a/src/main/codex/codex-stale-pane-accounts.test.ts +++ b/src/main/codex/codex-stale-pane-accounts.test.ts @@ -244,6 +244,26 @@ describe('codex pane account registry', () => { expect(hasRecordedLegacySharedCodexPane()).toBe(false) }) + it('preserves guest account provenance when native inventory is empty', () => { + const guestId = 'wsl:Ubuntu@@daemon-build@@guest-pty' + recordCodexPaneAccount(guestId, { + selectionKey: 'wsl:Ubuntu', + accountId: 'captured-account', + homeRoute: 'account-home' + }) + recordCodexPaneAccount('legacy-windows-owned-wsl', { + selectionKey: 'wsl:Ubuntu', + accountId: null, + homeRoute: 'wsl-home' + }) + + reconcileCodexPaneAccountsWithLivePtys([]) + _internals.resetCache() + + expect(getCodexPaneAccount(guestId)).toMatchObject({ accountId: 'captured-account' }) + expect(getCodexPaneAccount('legacy-windows-owned-wsl')).toBeNull() + }) + it('forgets a PTY so a reused id cannot inherit a dead pane account', () => { recordCodexPaneAccount('pty-1', { selectionKey: 'host', accountId: 'account-a' }) diff --git a/src/main/codex/codex-wsl-resume-file-probe.test.ts b/src/main/codex/codex-wsl-resume-file-probe.test.ts new file mode 100644 index 000000000000..94d55fbf741f --- /dev/null +++ b/src/main/codex/codex-wsl-resume-file-probe.test.ts @@ -0,0 +1,97 @@ +import { afterEach, expect, it, vi } from 'vitest' +import { mkdtempSync, mkdirSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { runProcess } from '../../shared/child-process/run-process' +import { + CODEX_WSL_RESUME_FILE_PROBE, + probeCodexWslResumeFiles +} from './codex-wsl-resume-file-probe' +import { runCapturedCodexWslProcess } from '../codex-accounts/captured-wsl-account-process' +vi.mock('../codex-accounts/captured-wsl-account-process', () => ({ + runCapturedCodexWslProcess: vi.fn() +})) +const roots: string[] = [] +afterEach(() => { + for (const root of roots.splice(0)) { + rmSync(root, { recursive: true, force: true }) + } +}) +const id = '11111111-2222-3333-4444-555555555555' +function fixture() { + const root = realpathSync(mkdtempSync(join(tmpdir(), 'codex-resume-owner-'))) + roots.push(root) + const home = join(root, 'account'), + date = join(home, 'sessions', '2026', '09', '27') + mkdirSync(date, { recursive: true }) + const file = join(date, `rollout-date-${id}.jsonl`) + writeFileSync(file, '{}\n') + return { root, home, file, date } +} +async function probe(home: string, transcript: string, prefix = '') { + const statShim = + process.platform === 'darwin' ? 'stat() { /usr/bin/stat -f %u "${@: -1}"; }\n' : '' + return runProcess({ + program: 'bash', + args: ['-c', statShim + prefix + CODEX_WSL_RESUME_FILE_PROBE, 'probe', id, transcript, home], + timeoutMs: 5000, + maxOutputBytes: 65536 + }) +} +it.skipIf(process.platform === 'win32')( + 'executes guest verification for provenance and legacy scans without reading rollout contents', + async () => { + const { home, file } = fixture() + for (const path of [file, '']) { + const result = await probe(home, path) + expect(result.code).toBe(0) + expect(result.stdout).toBe(`${file}\0ORCA_CODEX_RESUME_PROBE_COMPLETE\0`) + } + } +) +it.skipIf(process.platform === 'win32')( + 'rejects symlink provenance and a symlink account home', + async () => { + const { root, home, file, date } = fixture() + const alias = join(date, `rollout-alias-${id}.jsonl`) + symlinkSync(file, alias) + expect((await probe(home, alias)).stdout).toBe('ORCA_CODEX_RESUME_PROBE_COMPLETE\0') + const linkedHome = join(root, 'linked') + symlinkSync(home, linkedHome) + expect((await probe(linkedHome, '')).code).toBe(80) + } +) +it.skipIf(process.platform === 'win32')( + 'rejects a file whose owner differs from the captured user', + async () => { + const { home, file } = fixture() + const result = await probe( + home, + file, + `stat() { if [ "\${@: -1}" = '${file}' ]; then echo 999999; else id -u; fi; }\n` + ) + expect(result.stdout).toBe('ORCA_CODEX_RESUME_PROBE_COMPLETE\0') + } +) +it('treats truncated guest output as failure rather than another-account fallback', async () => { + vi.mocked(runCapturedCodexWslProcess).mockResolvedValue({ + code: 0, + stdout: 'partial', + stderr: '', + timedOut: false, + environmentResolved: true + }) + const execution = { distro: 'Ubuntu', userName: 'alice', userId: '1000', home: '/home/alice' } + await expect( + probeCodexWslResumeFiles({ + execution, + homes: ['/home/alice/.codex'], + sessionId: id, + transcriptPath: '' + }) + ).rejects.toThrow('could not be verified') + expect(runCapturedCodexWslProcess).toHaveBeenCalledWith( + expect.objectContaining({ distro: 'Ubuntu', shell: 'bash', loginPath: 'none' }), + execution + ) +}) diff --git a/src/main/codex/codex-wsl-resume-file-probe.ts b/src/main/codex/codex-wsl-resume-file-probe.ts new file mode 100644 index 000000000000..d189f10eefcb --- /dev/null +++ b/src/main/codex/codex-wsl-resume-file-probe.ts @@ -0,0 +1,70 @@ +import { runCapturedCodexWslProcess } from '../codex-accounts/captured-wsl-account-process' +import type { WslAccountExecutionContext } from '../wsl/wsl-account-execution-context' + +/** Guest-local checks reject aliases and files owned by another user before selecting credentials. */ +export const CODEX_WSL_RESUME_FILE_PROBE = ` +set -euo pipefail +session_id=$1 +transcript=$2 +shift 2 +check_file() { + file=$1 + [ -f "$file" ] && [ ! -L "$file" ] && [ -r "$file" ] || return 0 + [ "$(readlink -f -- "$file")" = "$file" ] || return 0 + [ "$(stat -c %u -- "$file")" = "$(id -u)" ] || return 0 + printf '%s\\0' "$file" +} +for codex_home in "$@"; do + if [ ! -e "$codex_home" ] && [ ! -L "$codex_home" ]; then continue; fi + [ -d "$codex_home" ] && [ ! -L "$codex_home" ] && [ -r "$codex_home" ] && [ -x "$codex_home" ] || exit 80 + [ "$(readlink -f -- "$codex_home")" = "$codex_home" ] || exit 80 + [ "$(stat -c %u -- "$codex_home")" = "$(id -u)" ] || exit 80 + if [ -n "$transcript" ]; then + case "$transcript" in "$codex_home"/sessions/*) + plain=\${transcript%.zst} + check_file "$plain" + check_file "$plain.zst" + ;; + esac + else + sessions="$codex_home/sessions" + if [ ! -e "$sessions" ] && [ ! -L "$sessions" ]; then continue; fi + [ -d "$sessions" ] && [ ! -L "$sessions" ] && [ -r "$sessions" ] && [ -x "$sessions" ] || exit 80 + find "$sessions" -mindepth 4 -maxdepth 4 -type f \\( -name "rollout-*-$session_id.jsonl" -o -name "rollout-*-$session_id.jsonl.zst" \\) -print0 | + while IFS= read -r -d '' file; do check_file "$file"; done + fi +done +printf 'ORCA_CODEX_RESUME_PROBE_COMPLETE\\0' +` + +export async function probeCodexWslResumeFiles(args: { + execution: WslAccountExecutionContext + homes: readonly string[] + sessionId: string + transcriptPath: string +}): Promise> { + const result = await runCapturedCodexWslProcess( + { + distro: args.execution.distro, + loginPath: 'none', + shell: 'bash', + script: CODEX_WSL_RESUME_FILE_PROBE, + args: [args.sessionId, args.transcriptPath, ...args.homes], + timeoutMs: 10_000, + maxOutputBytes: 256 * 1024 + }, + args.execution + ) + const complete = result.stdout.endsWith('ORCA_CODEX_RESUME_PROBE_COMPLETE\0') + if ( + result.code !== 0 || + result.timedOut || + !complete || + Buffer.byteLength(result.stdout) >= 256 * 1024 + ) { + throw new Error('The captured WSL Codex session files could not be verified. Retry resume.') + } + return new Set( + result.stdout.slice(0, -'ORCA_CODEX_RESUME_PROBE_COMPLETE\0'.length).split('\0').filter(Boolean) + ) +} diff --git a/src/main/codex/codex-wsl-session-resume.test.ts b/src/main/codex/codex-wsl-session-resume.test.ts new file mode 100644 index 000000000000..cb7f35e40d64 --- /dev/null +++ b/src/main/codex/codex-wsl-session-resume.test.ts @@ -0,0 +1,102 @@ +import { beforeEach, expect, it, vi } from 'vitest' +import type { CodexManagedAccount } from '../../shared/managed-account-types' +import { toWindowsWslUncPath } from '../../shared/wsl-paths' +import { prepareCapturedWslCodexSessionResume } from './codex-wsl-session-resume' +import { probeCodexWslResumeFiles } from './codex-wsl-resume-file-probe' +vi.mock('./codex-wsl-resume-file-probe', () => ({ probeCodexWslResumeFiles: vi.fn() })) +const owner = { distro: 'Ubuntu', userName: 'alice', userId: '1000', home: '/home/alice' } +const id = '11111111-2222-3333-4444-555555555555' +const a = '/home/alice/accounts/a', + b = '/home/alice/accounts/b' +const rollout = (home: string) => `${home}/sessions/2026/09/27/rollout-2026-${id}.jsonl` +const account = (home: string, id: string): CodexManagedAccount => ({ + id, + managedHomePath: toWindowsWslUncPath(home, 'Ubuntu'), + managedHomeRuntime: 'wsl', + wslDistro: 'Ubuntu', + wslLinuxHomePath: home, + email: 'test@example.com', + createdAt: 1, + updatedAt: 1, + lastAuthenticatedAt: 1 +}) +const prepare = (transcriptPath?: string) => + prepareCapturedWslCodexSessionResume({ + execution: owner, + target: { runtime: 'wsl', wslDistro: 'Ubuntu' }, + providerSession: { key: 'session_id', id, transcriptPath }, + accounts: [account(a, 'a'), account(b, 'b')], + selectedAccountId: 'b' + }) +beforeEach(() => { + vi.mocked(probeCodexWslResumeFiles).mockReset() +}) +it.each([false, true])( + 'pins verified transcript origin rather than currently selected account (UNC=%s)', + async (unc) => { + vi.mocked(probeCodexWslResumeFiles).mockResolvedValue(new Set([rollout(a)])) + expect(await prepare(unc ? toWindowsWslUncPath(rollout(a), 'Ubuntu') : rollout(a))).toEqual({ + outcome: 'resume', + codexHomePath: a + }) + } +) +it('does not fall back to another account after claimed provenance becomes unavailable', async () => { + vi.mocked(probeCodexWslResumeFiles).mockResolvedValue(new Set([rollout(b)])) + expect(await prepare(rollout(a))).toEqual({ outcome: 'fresh', claimedCodexProvenance: true }) +}) +it('reuses selected-account ranking only for legacy metadata with no transcript', async () => { + vi.mocked(probeCodexWslResumeFiles).mockResolvedValue(new Set([rollout(a), rollout(b)])) + expect(await prepare()).toEqual({ outcome: 'resume', codexHomePath: b }) +}) +it.each([ + toWindowsWslUncPath(rollout(a), 'Debian'), + rollout('/home/bob/.codex'), + rollout('/home/alice/../bob/.codex') +])('refuses foreign or noncanonical transcript %s before guest I/O', async (path) => { + expect(await prepare(path)).toEqual({ outcome: 'fresh', claimedCodexProvenance: true }) + expect(probeCodexWslResumeFiles).not.toHaveBeenCalled() +}) +it('refuses an unverified selected home without substituting system default', async () => { + await expect( + prepareCapturedWslCodexSessionResume({ + execution: owner, + target: { runtime: 'wsl', wslDistro: 'Ubuntu' }, + providerSession: { key: 'session_id', id }, + accounts: [account('/home/bob/.codex', 'b')], + selectedAccountId: 'b' + }) + ).rejects.toThrow('captured WSL owner') +}) +it('pins execution identity before asynchronous verification', async () => { + const execution = { ...owner } + vi.mocked(probeCodexWslResumeFiles).mockImplementation(async (args) => { + execution.home = '/home/bob' + execution.userName = 'bob' + expect(args.execution).toEqual(owner) + expect(Object.isFrozen(args.execution)).toBe(true) + return new Set([rollout(a)]) + }) + expect( + await prepareCapturedWslCodexSessionResume({ + execution, + target: { runtime: 'wsl', wslDistro: 'Ubuntu' }, + providerSession: { key: 'session_id', id, transcriptPath: rollout(a) }, + accounts: [account(a, 'a')], + selectedAccountId: 'a' + }) + ).toEqual({ outcome: 'resume', codexHomePath: a }) +}) + +it('refuses distinct Linux homes that shared Unicode normalization would merge', async () => { + await expect( + prepareCapturedWslCodexSessionResume({ + execution: owner, + target: { runtime: 'wsl', wslDistro: 'Ubuntu' }, + providerSession: { key: 'session_id', id }, + accounts: [account('/home/alice/caf\u00e9', 'a'), account('/home/alice/cafe\u0301', 'b')], + selectedAccountId: 'b' + }) + ).rejects.toThrow('ambiguous WSL path identities') + expect(probeCodexWslResumeFiles).not.toHaveBeenCalled() +}) diff --git a/src/main/codex/codex-wsl-session-resume.ts b/src/main/codex/codex-wsl-session-resume.ts new file mode 100644 index 000000000000..f63b6904fec2 --- /dev/null +++ b/src/main/codex/codex-wsl-session-resume.ts @@ -0,0 +1,110 @@ +import { posix } from 'node:path' +import type { AgentProviderSessionMetadata } from '../../shared/agent-session-resume' +import type { CodexManagedAccount } from '../../shared/managed-account-types' +import { parseWslUncPath, toWindowsWslUncPath } from '../../shared/wsl-paths' +import { normalizeRuntimePathForComparison } from '../../shared/cross-platform-path' +import { + assertWslAccountExecutionTarget, + type WslAccountExecutionContext +} from '../wsl/wsl-account-execution-context' +import type { CodexAccountSelectionTarget } from '../codex-accounts/runtime-selection' +import { + claimsCodexRolloutLayout, + type CodexSessionResumePreparation +} from './codex-session-resume-home' +import { prepareCodexSessionResume } from './codex-session-resume-preparation' +import { probeCodexWslResumeFiles } from './codex-wsl-resume-file-probe' + +function guestPath(path: string, execution: WslAccountExecutionContext): string | null { + const unc = parseWslUncPath(path) + if (unc && unc.distro.toLowerCase() !== execution.distro.toLowerCase()) { + return null + } + const candidate = unc?.linuxPath ?? path + return candidate.startsWith(`${execution.home}/`) && + posix.normalize(candidate) === candidate && + !/[\\\0\r\n]/.test(candidate) + ? candidate + : null +} + +/** Reuse provenance and legacy ranking, but never inspect guest files through Windows filesystem APIs. */ +export async function prepareCapturedWslCodexSessionResume(args: { + execution: WslAccountExecutionContext + target: CodexAccountSelectionTarget + providerSession: AgentProviderSessionMetadata + accounts: readonly CodexManagedAccount[] + selectedAccountId: string | null +}): Promise { + const execution = Object.freeze({ ...args.execution }) + const providerSession = Object.freeze({ ...args.providerSession }) + assertWslAccountExecutionTarget(execution, args.target) + const systemHome = posix.join(execution.home, '.codex') + const homes = new Map() + for (const account of args.accounts) { + const unc = parseWslUncPath(account.managedHomePath) + const distro = account.managedHomeRuntime === 'wsl' ? account.wslDistro : unc?.distro + const path = account.managedHomeRuntime === 'wsl' ? account.wslLinuxHomePath : unc?.linuxPath + if (distro?.toLowerCase() === execution.distro.toLowerCase() && path) { + const home = guestPath(path, execution) + if (home) { + homes.set(account.id, home) + } + } + } + const selectedHome = args.selectedAccountId ? homes.get(args.selectedAccountId) : null + if (args.selectedAccountId && !selectedHome) { + throw new Error('Selected Codex account does not belong to the captured WSL owner') + } + const claimed = providerSession.transcriptPath?.trim() + const transcriptPath = claimed ? guestPath(claimed, execution) : null + const fresh: CodexSessionResumePreparation = { + outcome: 'fresh', + claimedCodexProvenance: claimsCodexRolloutLayout(claimed) + } + if ( + (claimed && !transcriptPath) || + (!claimed && !/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i.test(providerSession.id)) + ) { + return fresh + } + const trustedHomes = [...new Set([systemHome, ...homes.values()])] + // Linux permits distinct NFC/NFD homes; shared comparison must never merge their accounts. + if (new Set(trustedHomes.map(normalizeRuntimePathForComparison)).size !== trustedHomes.length) { + throw new Error('Codex account homes have ambiguous WSL path identities') + } + const files = await probeCodexWslResumeFiles({ + execution, + homes: trustedHomes, + sessionId: providerSession.id, + transcriptPath: transcriptPath ?? '' + }) + const asUnc = (path: string) => toWindowsWslUncPath(path, execution.distro) + const checkedFiles = new Map( + [...files].map((path) => [normalizeRuntimePathForComparison(asUnc(path)), path]) + ) + return prepareCodexSessionResume({ + sessionId: providerSession.id, + transcriptPath: transcriptPath ? asUnc(transcriptPath) : undefined, + trustedCodexHomes: trustedHomes.map(asUnc), + getSelectedAccountCodexHome: () => (selectedHome ? asUnc(selectedHome) : null), + systemCodexHomePath: asUnc(systemHome), + sharedRuntimeCodexHomePath: null, + fileIsRegular: (path) => checkedFiles.has(normalizeRuntimePathForComparison(path)), + async *listSessionFiles(sessionsRoot) { + const prefix = `${normalizeRuntimePathForComparison(sessionsRoot)}/` + for (const [key, path] of checkedFiles) { + if (key.startsWith(prefix)) { + yield asUnc(path) + } + } + }, + resolveVerifiedResumeHome: async ({ homePath }) => { + const home = guestPath(homePath, execution) + if (!home) { + throw new Error('Verified Codex session escaped its captured WSL owner') + } + return home + } + }) +} diff --git a/src/main/codex/wsl-codex-session-bridge.ts b/src/main/codex/wsl-codex-session-bridge.ts index a84b10fa6150..32215bca78d0 100644 --- a/src/main/codex/wsl-codex-session-bridge.ts +++ b/src/main/codex/wsl-codex-session-bridge.ts @@ -1,6 +1,10 @@ import { posix as pathPosix } from 'node:path' import { parseWslUncPath } from '../../shared/wsl-paths' -import { runWslProcess } from '../wsl/wsl-runner' +import { runCapturedCodexWslProcess } from '../codex-accounts/captured-wsl-account-process' +import { + assertWslAccountExecutionTarget, + type WslAccountExecutionContext +} from '../wsl/wsl-account-execution-context' import { buildWslCodexSessionBridgeShellCommand, WSL_SESSION_BRIDGE_TIMEOUT_MS @@ -9,6 +13,7 @@ import { export { buildWslCodexSessionBridgeShellCommand } from './wsl-codex-session-bridge-script' export type WslCodexSessionBridgeTarget = { + execution?: WslAccountExecutionContext distro: string systemCodexHomePath: string managedCodexHomePath: string @@ -29,6 +34,7 @@ const backgroundWslSessionBridgeTasks = new Map>() export function startWslCodexSessionBridgeInBackground( target: WslCodexSessionBridgeTarget ): Promise { + target = captureTarget(target) const taskKey = getWslSessionBridgeTaskKey(target) const existingTask = backgroundWslSessionBridgeTasks.get(taskKey) if (existingTask) { @@ -52,19 +58,23 @@ export function startWslCodexSessionBridgeInBackground( export async function syncWslCodexSessionsIntoManagedHome( target: WslCodexSessionBridgeTarget ): Promise { + target = captureTarget(target) const paths = resolveWslCodexSessionBridgeLinuxPaths(target) if (!paths) { return emptySummary } - const result = await runWslProcess({ - distro: target.distro, - loginPath: 'none', - script: buildWslCodexSessionBridgeShellCommand(paths), - // Process substitution and `read -d` are bash-only; dash rejects both. - shell: 'bash', - timeoutMs: WSL_SESSION_BRIDGE_TIMEOUT_MS - }) + const result = await runCapturedCodexWslProcess( + { + distro: target.distro, + loginPath: 'none', + script: buildWslCodexSessionBridgeShellCommand(paths), + // Process substitution and `read -d` are bash-only; dash rejects both. + shell: 'bash', + timeoutMs: WSL_SESSION_BRIDGE_TIMEOUT_MS + }, + target.execution + ) if (result.code !== 0 || result.timedOut) { throw Object.assign( new Error(`WSL codex session bridge failed for ${target.distro} (code ${result.code})`), @@ -89,8 +99,24 @@ export function resolveWslCodexSessionBridgeLinuxPaths( } } +function captureTarget(target: WslCodexSessionBridgeTarget): WslCodexSessionBridgeTarget { + if (!target.execution) { + return target + } + const execution = Object.freeze({ ...target.execution }) + assertWslAccountExecutionTarget(execution, { runtime: 'wsl', wslDistro: target.distro }) + return { ...target, execution } +} + function getWslSessionBridgeTaskKey(target: WslCodexSessionBridgeTarget): string { - return [target.distro, target.systemCodexHomePath, target.managedCodexHomePath].join('\0') + return [ + target.distro, + target.systemCodexHomePath, + target.managedCodexHomePath, + ...(target.execution + ? [target.execution.userName, target.execution.userId, target.execution.home] + : []) + ].join('\0') } function getLinuxPathForWslDistro(path: string, distro: string): string | null { diff --git a/src/main/daemon/agent-startup-prompt-bun-fixture.ts b/src/main/daemon/agent-startup-prompt-bun-fixture.ts new file mode 100644 index 000000000000..fa4db0ab31d6 --- /dev/null +++ b/src/main/daemon/agent-startup-prompt-bun-fixture.ts @@ -0,0 +1,95 @@ +import assert from 'node:assert/strict' +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { setAppEnvironment } from '../../shared/app-environment' +import type { Session } from './session' +const COMMAND = "printf 'AGENT_%s\\n' STARTED" + +export async function launchPromptFixture({ + shell, + slow, + legacy +}: { + shell: string + slow: boolean + legacy: boolean +}): Promise<{ output: string; ms: number }> { + const root = mkdtempSync(join(tmpdir(), 'orca-startup-latency-')) + const bash = shell.endsWith('bash') + const pause = slow ? 'sleep 0.6\n' : '' + const prompt = slow ? "PS1='$(sleep 0.3)prompt> '\n" : "PS1='prompt> '\n" + writeFileSync( + join(root, bash ? '.bash_profile' : '.zshrc'), + `${pause}${bash ? '' : 'setopt PROMPT_SUBST\n'}${prompt}` + ) + process.env.HOME = root + process.env.ZDOTDIR = root + process.env.ORCA_ORIG_ZDOTDIR = root + setAppEnvironment({ + getPath: () => root, + getAppPath: () => root, + getVersion: () => 'test', + isPackaged: () => false, + onWillQuit() {}, + exit: (code) => process.exit(code), + getAppMetrics: () => [] + }) + const { createPtySubprocess } = await import('./pty-subprocess') + const { Session } = await import('./session') + let session: Session | undefined + let timer: ReturnType | undefined + let legacyTimer: ReturnType | undefined + const readinessEvents: string[] = [] + const started = performance.now() + try { + const subprocess = await createPtySubprocess({ + sessionId: 'startup-latency', + cols: 120, + rows: 30, + cwd: root, + shellOverride: shell, + command: COMMAND, + env: { HOME: root, SHELL: shell, TERM: 'xterm-256color' } + }) + session = new Session({ + sessionId: 'startup-latency', + cols: 120, + rows: 30, + subprocess, + shellReadySupported: !legacy, + reportReadinessEvent: (event) => readinessEvents.push(event) + }) + const active = session + return await new Promise((resolve, reject) => { + let output = '' + timer = setTimeout( + () => reject(new Error(`Startup timed out: ${JSON.stringify(output)}`)), + 5000 + ) + active.attachClient({ + onExit: () => {}, + onData: (data) => { + output += data + if (output.includes('AGENT_STARTED')) { + resolve({ output, ms: performance.now() - started }) + } + } + }) + if (legacy) { + legacyTimer = setTimeout(() => active.write(`${COMMAND}\n`), 300) + } else { + active.write(`${COMMAND}\n`) + } + }) + } finally { + clearTimeout(timer) + clearTimeout(legacyTimer) + if (session) { + await session.forceKillAndWaitForExit(3000) + session.dispose() + } + rmSync(root, { recursive: true, force: true }) + assert.deepEqual(readinessEvents, []) + } +} diff --git a/src/main/daemon/agent-startup-prompt-latency.bun.test.ts b/src/main/daemon/agent-startup-prompt-latency.bun.test.ts new file mode 100644 index 000000000000..4efb8bfae006 --- /dev/null +++ b/src/main/daemon/agent-startup-prompt-latency.bun.test.ts @@ -0,0 +1,70 @@ +import { appendFileSync, existsSync } from 'node:fs' +import { join } from 'node:path' +import { describe, expect, it } from 'vitest' +import { runBundledBunFixture } from '../bundled-bun-test-execution' + +const SHELLS = process.platform === 'win32' ? [] : ['/bin/bash', '/bin/zsh'].filter(existsSync) +const COMMAND = "printf 'AGENT_%s\\n' STARTED" + +async function launch( + shell: string, + slow: boolean, + legacy = false +): Promise<{ output: string; ms: number }> { + const result = await runBundledBunFixture( + join(__dirname, 'agent-startup-prompt-bun-fixture.ts'), + 'launchPromptFixture', + { shell, slow, legacy }, + 10_000 + ) + if ( + typeof result !== 'object' || + result === null || + !('output' in result) || + typeof result.output !== 'string' || + !('ms' in result) || + typeof result.ms !== 'number' + ) { + throw new Error('Invalid prompt fixture result') + } + return { output: result.output, ms: result.ms } +} + +describe('agent startup at the rendered shell prompt', () => { + it.each(SHELLS)( + '%s displays the command once after slow startup and prompt expansion', + async (shell) => { + const before = await launch(shell, true, true) + expect(before.output.split(COMMAND)).toHaveLength(3) + const result = await launch(shell, true) + expect(result.output).not.toContain('orca-shell-ready') + expect(result.output.split(COMMAND)).toHaveLength(2) + expect(result.output.indexOf('prompt> ')).toBeLessThan(result.output.indexOf(COMMAND)) + } + ) + + it.skipIf(!process.env.ORCA_STARTUP_BENCH || SHELLS.length === 0)( + 'compares legacy input timing with prompt delivery', + async () => { + for (const shell of SHELLS) { + for (const slow of [false, true]) { + const legacy: number[] = [] + const current: number[] = [] + for (let i = 0; i < 5; i++) { + legacy.push((await launch(shell, slow, true)).ms) + const result = await launch(shell, slow) + expect(result.output).not.toContain('orca-shell-ready') + expect(result.output.split(COMMAND)).toHaveLength(2) + current.push(result.ms) + } + const result = JSON.stringify({ shell, slow, legacy, current }) + if (process.env.ORCA_STARTUP_BENCH_OUTPUT) { + appendFileSync(process.env.ORCA_STARTUP_BENCH_OUTPUT, `${result}\n`) + } + console.log(result) + } + } + }, + 60_000 + ) +}) diff --git a/src/main/daemon/agent-startup-prompt-latency.node-pty.test.ts b/src/main/daemon/agent-startup-prompt-latency.node-pty.test.ts deleted file mode 100644 index cfa01fed8e56..000000000000 --- a/src/main/daemon/agent-startup-prompt-latency.node-pty.test.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { appendFileSync, existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import { afterEach, describe, expect, it, vi } from 'vitest' -import { createPtySubprocess } from './pty-subprocess' -import { Session } from './session' - -const SHELLS = process.platform === 'win32' ? [] : ['/bin/bash', '/bin/zsh'].filter(existsSync) -const COMMAND = "printf 'AGENT_%s\\n' STARTED" - -async function launch( - shell: string, - slow: boolean, - legacy = false -): Promise<{ output: string; ms: number }> { - const root = mkdtempSync(join(tmpdir(), 'orca-startup-latency-')) - const bash = shell.endsWith('bash') - const pause = slow ? 'sleep 0.6\n' : '' - const prompt = slow ? "PS1='$(sleep 0.3)prompt> '\n" : "PS1='prompt> '\n" - writeFileSync( - join(root, bash ? '.bash_profile' : '.zshrc'), - `${pause}${bash ? '' : 'setopt PROMPT_SUBST\n'}${prompt}` - ) - vi.stubEnv('HOME', root) - vi.stubEnv('ZDOTDIR', root) - vi.stubEnv('ORCA_ORIG_ZDOTDIR', root) - let session: Session | undefined - let timer: ReturnType | undefined - let legacyTimer: ReturnType | undefined - const readinessEvents: string[] = [] - const started = performance.now() - try { - const subprocess = await createPtySubprocess({ - sessionId: 'startup-latency', - cols: 120, - rows: 30, - cwd: root, - shellOverride: shell, - command: COMMAND, - env: { HOME: root, SHELL: shell, TERM: 'xterm-256color' } - }) - session = new Session({ - sessionId: 'startup-latency', - cols: 120, - rows: 30, - subprocess, - shellReadySupported: !legacy, - reportReadinessEvent: (event) => readinessEvents.push(event) - }) - const active = session - return await new Promise((resolve, reject) => { - let output = '' - timer = setTimeout( - () => reject(new Error(`Startup timed out: ${JSON.stringify(output)}`)), - 5000 - ) - active.attachClient({ - onExit: () => {}, - onData: (data) => { - output += data - if (output.includes('AGENT_STARTED')) { - resolve({ output, ms: performance.now() - started }) - } - } - }) - if (legacy) { - legacyTimer = setTimeout(() => active.write(`${COMMAND}\n`), 300) - } else { - active.write(`${COMMAND}\n`) - } - }) - } finally { - clearTimeout(timer) - clearTimeout(legacyTimer) - if (session) { - await session.forceKillAndWaitForExit(3000) - session.dispose() - } - vi.unstubAllEnvs() - rmSync(root, { recursive: true, force: true }) - expect(readinessEvents).toEqual([]) - } -} - -describe('agent startup at the rendered shell prompt', () => { - afterEach(() => vi.unstubAllEnvs()) - it.each(SHELLS)( - '%s displays the command once after slow startup and prompt expansion', - async (shell) => { - const before = await launch(shell, true, true) - expect(before.output.split(COMMAND)).toHaveLength(3) - const result = await launch(shell, true) - expect(result.output).not.toContain('orca-shell-ready') - expect(result.output.split(COMMAND)).toHaveLength(2) - expect(result.output.indexOf('prompt> ')).toBeLessThan(result.output.indexOf(COMMAND)) - } - ) - - it.skipIf(!process.env.ORCA_STARTUP_BENCH || SHELLS.length === 0)( - 'compares legacy input timing with prompt delivery', - async () => { - for (const shell of SHELLS) { - for (const slow of [false, true]) { - const legacy: number[] = [] - const current: number[] = [] - for (let i = 0; i < 5; i++) { - legacy.push((await launch(shell, slow, true)).ms) - const result = await launch(shell, slow) - expect(result.output).not.toContain('orca-shell-ready') - expect(result.output.split(COMMAND)).toHaveLength(2) - current.push(result.ms) - } - const result = JSON.stringify({ shell, slow, legacy, current }) - if (process.env.ORCA_STARTUP_BENCH_OUTPUT) { - appendFileSync(process.env.ORCA_STARTUP_BENCH_OUTPUT, `${result}\n`) - } - console.log(result) - } - } - }, - 60_000 - ) -}) diff --git a/src/main/daemon/bun-pty-fd-lifecycle.integration.test.ts b/src/main/daemon/bun-pty-fd-lifecycle.integration.test.ts new file mode 100644 index 000000000000..70344281bec2 --- /dev/null +++ b/src/main/daemon/bun-pty-fd-lifecycle.integration.test.ts @@ -0,0 +1,89 @@ +import { existsSync } from 'node:fs' +import { resolve } from 'node:path' +import { describe, expect, it } from 'vitest' +import { runProcess } from '../../shared/child-process/run-process' +import { orcadBunRuntimeFilename } from '../../shared/orcad-artifacts' + +const runtime = + process.env.BUN_EXECUTABLE ?? resolve('out/orcad', orcadBunRuntimeFilename(process.platform)) +const enabled = process.platform !== 'win32' && existsSync(runtime) + +const script = ` +import { readdirSync, readlinkSync } from 'node:fs' +import { spawnBunPty } from './src/main/daemon/pty-subprocess/bun-pty-process' +import { runProcessSync } from './src/shared/child-process/run-process' +const pause = () => new Promise(resolve => setTimeout(resolve, 50)) +function terminalDescriptors() { + if (process.platform === 'linux') { + return readdirSync('/proc/self/fd').flatMap(fd => { + try { return [readlinkSync('/proc/self/fd/' + fd)] } catch { return [] } + }).filter(path => /\\/dev\\/(ptmx|pts\\/)/.test(path)).length + } + const result = runProcessSync({ program: 'lsof', args: ['-p', String(process.pid)], timeoutMs: 5000 }) + if (result.code !== 0) throw new Error('lsof failed: ' + result.stderr) + return result.stdout.split('\\n').filter(line => /\\/dev\\/(ptmx|ttys[0-9])|\\(revoked\\)/.test(line)).length +} +function terminal(file = '/bin/sh', args = ['-c', 'exit 0']) { + return spawnBunPty({ file, args, cwd: process.cwd(), env: process.env, cols: 80, rows: 24 }) +} +async function exitedShell() { + const proc = terminal() + try { await new Promise(resolve => proc.onExit(resolve)) } finally { proc.destroy() } +} +await exitedShell() +await pause() +const before = terminalDescriptors() +for (let index = 0; index < 50; index++) await exitedShell() +await pause() +const afterExit = terminalDescriptors() +let failedSpawns = 0 +for (let index = 0; index < 20; index++) { + try { terminal('/orca-no-such-terminal-shell', []) } catch { failedSpawns++ } +} +await pause() +const afterFailure = terminalDescriptors() +let inherited = null +if (process.platform === 'linux') { + const first = terminal('/bin/sh', ['-c', 'read value']) + let second + try { + const laterChild = runProcessSync({ program: '/bin/sh', args: ['-c', 'ls -l /proc/self/fd'], timeoutMs: 5000 }) + if (laterChild.code !== 0) throw new Error('child descriptor listing failed') + second = terminal('/bin/sh', ['-c', "read value; printf '__FD_LISTING_READY__\\n'; ls -l /proc/self/fd"]) + let output = '' + second.onData(data => { output += data }) + const done = new Promise(resolve => second.onExit(resolve)) + second.write('go\\n') + await done + if (!output.includes('__FD_LISTING_READY__') || !/\\/dev\\/pts\\//.test(output)) throw new Error('missing terminal descriptor evidence') + inherited = /ptmx/.test(laterChild.stdout) || /ptmx/.test(output) + } finally { + second?.destroy() + const done = new Promise(resolve => first.onExit(resolve)) + first.write('done\\n') + await done + first.destroy() + } +} +console.log(JSON.stringify({ exitLeak: afterExit - before, failedLeak: afterFailure - before, failedSpawns, inherited })) +` + +describe.skipIf(!enabled)('Bun native PTY descriptor lifecycle', () => { + it('releases exited and failed-spawn handles and prevents later-child inheritance', async () => { + const result = await runProcess({ + program: runtime, + args: ['--no-install', '--eval', script], + cwd: process.cwd(), + env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1' }, + timeoutMs: 20_000, + terminationBarrier: true + }) + expect(result.code, result.stderr).toBe(0) + expect(JSON.parse(result.stdout)).toEqual({ + exitLeak: 0, + failedLeak: 0, + failedSpawns: 20, + inherited: process.platform === 'linux' ? false : null + }) + }, 25_000) +}) diff --git a/src/main/daemon/bun-pty-retired-handle-fixture.ts b/src/main/daemon/bun-pty-retired-handle-fixture.ts new file mode 100644 index 000000000000..eeea7406cd33 --- /dev/null +++ b/src/main/daemon/bun-pty-retired-handle-fixture.ts @@ -0,0 +1,71 @@ +import { readdirSync, readlinkSync } from 'node:fs' +import { spawnBunPty } from './pty-subprocess/bun-pty-process' + +function masterDescriptors(): Set { + if (process.platform !== 'linux') { + return new Set() + } + return new Set( + readdirSync('/proc/self/fd').filter((fd) => { + try { + return /\/dev\/(?:pts\/)?ptmx$/.test(readlinkSync(`/proc/self/fd/${fd}`)) + } catch { + return false + } + }) + ) +} + +function shell(command: string, file = '/bin/sh') { + return spawnBunPty({ + file, + args: ['-c', command], + cwd: process.cwd(), + env: { PATH: process.env.PATH ?? '/usr/bin:/bin', TERM: 'xterm-256color' }, + cols: 80, + rows: 24 + }) +} + +export async function exerciseRetiredHandle({ destroy }: { destroy: boolean }) { + const before = masterDescriptors() + const retired = shell('read value') + const retiredDescriptors = [...masterDescriptors()].filter((fd) => !before.has(fd)) + const retiredExit = new Promise((resolve) => retired.onExit(() => resolve())) + retired.write('done\r') + await retiredExit + if (destroy) { + retired.destroy() + } + + const live = shell('read value; stty size; printf "__READ:%s__\n" "$value"', '/bin/bash') + const liveDescriptors = masterDescriptors() + let output = '' + live.onData((data) => { + output += data + }) + const liveExit = new Promise((resolve) => + live.onExit(({ exitCode }) => resolve(exitCode)) + ) + try { + retired.resize(200, 50) + retired.write('leak\r') + const retiredProcess = retired.process + live.write('hello\r') + const exitCode = await liveExit + return { + retiredSize: [retired.cols, retired.rows], + retiredProcess, + output, + exitCode, + reusedDescriptor: + process.platform === 'linux' + ? retiredDescriptors.some((fd) => liveDescriptors.has(fd)) + : null + } + } finally { + live.destroy() + retired.destroy() + await liveExit + } +} diff --git a/src/main/daemon/bun-pty-retired-handle.integration.test.ts b/src/main/daemon/bun-pty-retired-handle.integration.test.ts new file mode 100644 index 000000000000..985675878379 --- /dev/null +++ b/src/main/daemon/bun-pty-retired-handle.integration.test.ts @@ -0,0 +1,39 @@ +import { join } from 'node:path' +import { describe, expect, it } from 'vitest' +import { z } from 'zod' +import { runBundledBunFixture } from '../bundled-bun-test-execution' + +const resultSchema = z.object({ + retiredSize: z.array(z.number()), + retiredProcess: z.string(), + output: z.string(), + exitCode: z.number(), + reusedDescriptor: z.boolean().nullable() +}) + +describe.skipIf(process.platform === 'win32')('Bun retired PTY handle isolation', () => { + it.each([false, true])( + 'does not resize, write into or inspect a later PTY after exit (explicit destroy: %s)', + async (destroy) => { + const result = resultSchema.parse( + await runBundledBunFixture( + join(__dirname, 'bun-pty-retired-handle-fixture.ts'), + 'exerciseRetiredHandle', + { destroy }, + 10_000 + ) + ) + expect(result.retiredSize).toEqual([80, 24]) + expect(result.retiredProcess).toBe('/bin/sh') + expect(result.exitCode).toBe(0) + expect(result.output).toContain('24 80') + expect(result.output).toContain('__READ:hello__') + expect(result.output).not.toContain('leak') + if (process.platform === 'linux') { + // Prove the kernel actually reused the descriptor instead of testing unrelated handles. + expect(result.reusedDescriptor).toBe(true) + } + }, + 15_000 + ) +}) diff --git a/src/main/daemon/bun-pty-windows-io-failure-fixture.ts b/src/main/daemon/bun-pty-windows-io-failure-fixture.ts new file mode 100644 index 000000000000..87647ba5f811 --- /dev/null +++ b/src/main/daemon/bun-pty-windows-io-failure-fixture.ts @@ -0,0 +1,175 @@ +import { join } from 'node:path' +import { setTimeout as delay } from 'node:timers/promises' +import { resolveBunRuntime } from './pty-subprocess/bun-pty-process-capabilities' +import type { BunTerminalOptions } from './pty-subprocess/bun-pty-process-contract' +import { spawnBunPty } from './pty-subprocess/bun-pty-process' +import { createWindowsBunPtyLaunch } from './pty-subprocess/windows-bun-pty-launch' + +const CHILD = ` +process.stdin.setRawMode(true); +process.stdin.resume(); +process.stdout.write('NATIVE_READY'); +process.stdin.on('data', chunk => { + if (chunk.toString().includes('finish')) { + process.stdout.write('FINAL_STATE_END', () => process.exit(17)); + } else { + process.stdout.write('WITNESS_REPLY_END'); + } +});` + +export async function runWindowsNativeIoFailure(options: { + operation: 'write-close' | 'late-write' | 'drain-close' + workerPath?: string +}) { + if (process.platform !== 'win32') { + throw new Error('Windows native qualification only') + } + const runtime = resolveBunRuntime() + let closeOnWrite = false + let nativeFailure = '' + class ClosingTerminal extends runtime.Terminal { + constructor(args: BunTerminalOptions) { + super(args) + const nativeWrite = this.write.bind(this) + this.write = (data) => { + if (closeOnWrite) { + closeOnWrite = false + this.close() + } + try { + return nativeWrite(data) + } catch (error) { + nativeFailure = error instanceof Error ? error.message : String(error) + throw error + } + } + } + } + const env = Object.fromEntries( + Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined) + ) + const launch = (victim: boolean) => { + const proc = spawnBunPty( + { + file: process.execPath, + args: ['--no-env-file', '-e', CHILD], + cwd: process.cwd(), + env, + cols: 120, + rows: 30, + windowsJobKillOnClose: true + }, + { + ...(victim + ? { runtime: { Terminal: ClosingTerminal, spawn: runtime.spawn.bind(runtime) } } + : {}), + createWindowsLaunch: (args) => + createWindowsBunPtyLaunch(args, { + workerPath: + options.workerPath ?? join(__dirname, 'pty-subprocess/windows-bun-pty-gate-entry.ts') + }) + } + ) + let output = '' + let exitCount = 0 + let finalOutputBeforeExit = false + proc.onData((data) => { + output += data + }) + const exited = new Promise((resolve) => { + proc.onExit(({ exitCode }) => { + finalOutputBeforeExit = output.includes('FINAL_STATE_END') + exitCount++ + resolve(exitCode) + }) + }) + return { + proc, + exited, + output: () => output, + exitCount: () => exitCount, + finalOutputBeforeExit: () => finalOutputBeforeExit + } + } + const uncaught: string[] = [] + const onUncaught = (error: Error) => { + uncaught.push(error.message) + } + process.on('uncaughtException', onUncaught) + const terminals: ReturnType[] = [] + const bounded = async (promise: Promise): Promise => { + let timer: ReturnType | undefined + try { + return await Promise.race([ + promise, + new Promise((_, reject) => { + timer = setTimeout( + () => reject(new Error('Native I/O fixture deadline exceeded')), + 15_000 + ) + }) + ]) + } finally { + clearTimeout(timer) + } + } + const waitFor = async (terminal: ReturnType, marker: string) => { + const deadline = Date.now() + 15_000 + while (!terminal.output().includes(marker)) { + if (Date.now() > deadline) { + throw new Error(`Missing ${marker}: ${terminal.output()}`) + } + await delay(10) + } + } + try { + const victim = launch(true) + terminals.push(victim) + const witness = launch(false) + terminals.push(witness) + await bounded(Promise.all(terminals.map(async (t) => t.proc.waitForSpawn?.()))) + await Promise.all(terminals.map((t) => waitFor(t, 'NATIVE_READY'))) + if (options.operation === 'write-close') { + // Close the real native handle after the adapter's check; no synthetic exception. + closeOnWrite = true + victim.proc.write('trigger') + } else if (options.operation === 'late-write') { + victim.proc.kill() + } else { + victim.proc.write('finish') + } + const exitCode = await bounded(victim.exited) + victim.proc.write('late input') + victim.proc.resize(90, 24) + await delay(1_500) + witness.proc.write('probe') + await waitFor(witness, 'WITNESS_REPLY_END') + return { + nativeFailure, + exitCode, + victimExitCount: victim.exitCount(), + witnessExitCount: witness.exitCount(), + witnessWritable: true, + finalOutputBeforeExit: victim.finalOutputBeforeExit(), + uncaught + } + } finally { + try { + await bounded( + Promise.all( + terminals.map(async ({ proc, exited }) => { + try { + proc.kill() + await exited + } finally { + proc.destroy() + } + }) + ) + ) + } finally { + await delay(1_500) + process.off('uncaughtException', onUncaught) + } + } +} diff --git a/src/main/daemon/bun-pty-windows-io-failure.win32.test.ts b/src/main/daemon/bun-pty-windows-io-failure.win32.test.ts new file mode 100644 index 000000000000..a657b54dc984 --- /dev/null +++ b/src/main/daemon/bun-pty-windows-io-failure.win32.test.ts @@ -0,0 +1,30 @@ +import { join } from 'node:path' +import { describe, expect, it } from 'vitest' +import { runBundledBunFixture } from '../bundled-bun-test-execution' + +describe.skipIf(process.platform !== 'win32')('Bun Windows native terminal I/O isolation', () => { + it.each(['write-close', 'late-write', 'drain-close'] as const)( + 'contains %s while an unrelated native terminal remains writable', + async (operation) => { + const result = await runBundledBunFixture( + join(__dirname, 'bun-pty-windows-io-failure-fixture.ts'), + 'runWindowsNativeIoFailure', + { operation }, + 60_000 + ) + expect(result).toMatchObject({ + victimExitCount: 1, + witnessExitCount: 0, + witnessWritable: true, + uncaught: [] + }) + if (operation === 'write-close') { + expect(result).toMatchObject({ nativeFailure: 'Terminal is closed' }) + } + if (operation === 'drain-close') { + expect(result).toMatchObject({ exitCode: 17, finalOutputBeforeExit: true }) + } + }, + 65_000 + ) +}) diff --git a/src/main/daemon/client.test.ts b/src/main/daemon/client.test.ts index 2d7973bae880..8683c3ab8deb 100644 --- a/src/main/daemon/client.test.ts +++ b/src/main/daemon/client.test.ts @@ -164,6 +164,8 @@ describe('DaemonClient', () => { pid: 123, startedAtMs: 456, launchNonce: 'launch-a', + linuxStartTicks: '12345', + bootId: 'guest-boot', entryPath: '/Applications/Orca.app/Contents/Resources/daemon-entry.js', appVersion: '1.2.3', spawnerExecPath: '/Applications/Orca.app/Contents/MacOS/Orca' @@ -176,6 +178,18 @@ describe('DaemonClient', () => { expect(client.getDaemonIdentity()).toEqual(identity) }) + it.each([ + { linuxStartTicks: '123' }, + { bootId: 'boot' }, + { linuxStartTicks: 'not-ticks', bootId: 'boot' } + ])('rejects incomplete or malformed guest incarnation %#', async (extra) => { + await startMockDaemon({ + helloIdentity: () => ({ pid: 123, startedAtMs: 456, launchNonce: 'launch', ...extra }) + }) + client = new DaemonClient({ socketPath, tokenPath }) + await expect(client.ensureConnected()).rejects.toThrow('Invalid daemon identity') + }) + it('rejects a v24 daemon that omits endpoint identity', async () => { await startMockDaemon({ omitHelloIdentity: true }) client = new DaemonClient({ socketPath, tokenPath }) diff --git a/src/main/daemon/client.ts b/src/main/daemon/client.ts index 37d628a1464d..0fe204207dcf 100644 --- a/src/main/daemon/client.ts +++ b/src/main/daemon/client.ts @@ -1,5 +1,10 @@ -import type { Socket } from 'node:net' -import { readFileSync } from 'node:fs' +import { DaemonConnectionAttempt } from './daemon-client-connection-attempt' +import type { Duplex } from 'node:stream' +import { + resolveDaemonTransport, + type DaemonClientOptions, + type DaemonClientTransport +} from './daemon-client-transport' import { randomUUID } from 'node:crypto' import { encodeNdjson } from './ndjson' import { @@ -12,7 +17,6 @@ import { import { writeRefused, type WriteSettlement } from '../../shared/pty-write-settlement' import { armDaemonSocketCloseHandlers, - connectDaemonSocket, waitForDaemonConnectionAttempt } from './daemon-client-socket-connect' import { DaemonClientListeners } from './daemon-client-listener-registry' @@ -26,24 +30,17 @@ import { writeNotifyWithSettlement } from './daemon-client-notify-settlement' import { requestDaemonRpc } from './daemon-client-rpc-request' const CONNECT_TIMEOUT_MS = 5000 -const CONNECTION_ATTEMPT_WAIT_MS = CONNECT_TIMEOUT_MS * 4 -const REQUEST_TIMEOUT_MS = 30000 const NOTIFY_SETTLEMENT_TIMEOUT_MS = 5000 -export type DaemonClientOptions = { - socketPath: string - tokenPath: string - protocolVersion?: number -} - +export type { DaemonClientOptions, DaemonClientTransport } from './daemon-client-transport' export class DaemonClient { - private socketPath: string - private tokenPath: string + private transport: DaemonClientTransport + private connectionAbort: AbortController | null = null private protocolVersion: number private clientId = randomUUID() - private controlSocket: Socket | null = null - private streamSocket: Socket | null = null + private controlSocket: Duplex | null = null + private streamSocket: Duplex | null = null private connected = false private disconnectArmed = false // Why: after a disconnect + reconnect (daemon respawn), a stale 'close' @@ -65,9 +62,8 @@ export class DaemonClient { private requestCounter = 0 private cleanupSocketListeners: (() => void) | null = null - constructor(opts: DaemonClientOptions) { - this.socketPath = opts.socketPath - this.tokenPath = opts.tokenPath + constructor(private readonly opts: DaemonClientOptions) { + this.transport = resolveDaemonTransport(opts) this.protocolVersion = opts.protocolVersion ?? PROTOCOL_VERSION } @@ -101,7 +97,7 @@ export class DaemonClient { if (this.connectingPromise) { // Why: a normal connection may legitimately consume one timeout for each // socket and hello; bounded teardown calls instead keep their one shared budget. - const waiterTimeoutMs = sharedBudget ? timeoutMs : CONNECTION_ATTEMPT_WAIT_MS + const waiterTimeoutMs = sharedBudget ? timeoutMs : CONNECT_TIMEOUT_MS * 5 return waitForDaemonConnectionAttempt(this.connectingPromise, waiterTimeoutMs) } @@ -114,67 +110,60 @@ export class DaemonClient { } } - // Why: a missing token must not preempt the connect that proves whether the endpoint is gone. - private readToken(): string { - try { - return readFileSync(this.tokenPath, 'utf-8').trim() - } catch (error) { - if ((error as NodeJS.ErrnoException | null)?.code === 'ENOENT') { - return '' - } - throw error - } - } - private async doConnect( timeoutMs: number, attemptGeneration: number, sharedBudget: boolean ): Promise { - const token = this.readToken() - const deadlineMs = Date.now() + timeoutMs - const remainingMs = (): number => - sharedBudget ? Math.max(1, deadlineMs - Date.now()) : timeoutMs - const pendingListenerCleanups: (() => void)[] = [] - const cleanupPendingListeners = (): void => { - for (const cleanup of pendingListenerCleanups.splice(0)) { - cleanup() - } - } + const attempt = new DaemonConnectionAttempt(this.transport, timeoutMs, sharedBudget) + const controller = attempt.controller + this.connectionAbort = controller + const cleanupPendingListeners = () => attempt.releaseReaders() try { + const token = await attempt.readToken() + this.assertConnectionAttemptCurrent(attemptGeneration) // Sequential: control first, then stream - const pendingControlSocket = await connectDaemonSocket(this.socketPath, remainingMs()) + const pendingControlSocket = await attempt.connect('control') this.assertConnectionAttemptCurrent(attemptGeneration, pendingControlSocket) this.controlSocket = pendingControlSocket const controlIdentity = await this.sendHello( - this.controlSocket, + pendingControlSocket, token, 'control', - remainingMs() + attempt.remainingMs() ) this.assertConnectionAttemptCurrent(attemptGeneration, this.controlSocket) - pendingListenerCleanups.push( + attempt.readerCleanups.push( attachControlResponseReader(this.controlSocket, (response) => this.pendingRequests.settle(response) ) ) - const pendingStreamSocket = await connectDaemonSocket(this.socketPath, remainingMs()) + const pendingStreamSocket = await attempt.connect('stream') this.assertConnectionAttemptCurrent(attemptGeneration, pendingStreamSocket) this.streamSocket = pendingStreamSocket - const streamIdentity = await this.sendHello(this.streamSocket, token, 'stream', remainingMs()) + const streamIdentity = await this.sendHello( + pendingStreamSocket, + token, + 'stream', + attempt.remainingMs() + ) this.assertConnectionAttemptCurrent(attemptGeneration, this.streamSocket) if (!sameDaemonIdentity(controlIdentity, streamIdentity)) { throw new DaemonProtocolError('Daemon identity changed during connection') } - pendingListenerCleanups.push( + + await this.opts.admitIdentity?.(controlIdentity) + this.assertConnectionAttemptCurrent(attemptGeneration) + if (controller.signal.aborted) { + throw controller.signal.reason + } + attempt.readerCleanups.push( attachStreamEventReader(this.streamSocket, (event) => { this.eventListeners.each((listener) => listener(event)) }) ) - - this.assertConnectionAttemptCurrent(attemptGeneration) this.connected = true this.observedAuthenticatedDisconnect = false this.daemonIdentity = controlIdentity @@ -182,13 +171,14 @@ export class DaemonClient { this.connectionGeneration++ const gen = this.connectionGeneration - pendingListenerCleanups.push( + attempt.readerCleanups.push( armDaemonSocketCloseHandlers(this.controlSocket, this.streamSocket, () => this.handleDisconnect(gen) ) ) this.cleanupSocketListeners = cleanupPendingListeners } catch (error) { + controller.abort(error) cleanupPendingListeners() this.controlSocket?.destroy() this.streamSocket?.destroy() @@ -198,13 +188,18 @@ export class DaemonClient { this.daemonIdentity = null this.disconnectArmed = false throw error + } finally { + attempt.releaseGuards() + if (this.connectionAbort === controller) { + this.connectionAbort = null + } } } async request( type: string, payload: unknown, - timeoutMs = REQUEST_TIMEOUT_MS, + timeoutMs = 30_000, signal?: AbortSignal ): Promise { if (!this.connected || !this.controlSocket) { @@ -283,6 +278,7 @@ export class DaemonClient { } disconnect(): void { + this.connectionAbort?.abort(new DaemonProtocolError('Disconnected')) this.connectionAttemptGeneration++ this.connected = false this.daemonIdentity = null @@ -297,7 +293,7 @@ export class DaemonClient { this.streamSocket = null } - private assertConnectionAttemptCurrent(attemptGeneration: number, socket?: Socket): void { + private assertConnectionAttemptCurrent(attemptGeneration: number, socket?: Duplex): void { if (attemptGeneration === this.connectionAttemptGeneration) { return } @@ -306,7 +302,7 @@ export class DaemonClient { } private sendHello( - socket: Socket, + socket: Duplex, token: string, role: 'control' | 'stream', timeoutMs: number @@ -315,6 +311,7 @@ export class DaemonClient { socket, token, role, + signal: this.connectionAbort?.signal, timeoutMs, protocolVersion: this.protocolVersion, clientId: this.clientId diff --git a/src/main/providers/local-pty-bun-artifact.integration.test.ts b/src/main/daemon/daemon-bun-pty-artifact.integration.test.ts similarity index 67% rename from src/main/providers/local-pty-bun-artifact.integration.test.ts rename to src/main/daemon/daemon-bun-pty-artifact.integration.test.ts index 487127887a34..18f85b914059 100644 --- a/src/main/providers/local-pty-bun-artifact.integration.test.ts +++ b/src/main/daemon/daemon-bun-pty-artifact.integration.test.ts @@ -12,16 +12,17 @@ import { removeTreeSync } from '../../shared/windows-transient-lock-removal' const runtime = process.env.BUN_EXECUTABLE ?? resolve('out/orcad', orcadBunRuntimeFilename(process.platform)) -describe.skipIf(!existsSync(runtime))('isolated Bun in-process PTY artifact', () => { +describe.skipIf(!existsSync(runtime))('isolated Bun daemon PTY artifact', () => { it('spawns, reattaches, delivers data and retires a shell without node-pty installed', async () => { - const directory = await mkdtemp(join(tmpdir(), 'orca-bun-local-pty-')) + const directory = await mkdtemp(join(tmpdir(), 'orca-bun-daemon-pty-')) try { - const entry = join(directory, 'local-pty.cjs') + const entry = join(directory, 'daemon-pty.cjs') const external = ['node-pty', 'electron', 'bun:ffi', '@parcel/watcher', '*.node'] await build({ stdin: { contents: ` - import { LocalPtyProvider } from './src/main/providers/local-pty-provider' + import { TerminalHost } from './src/main/daemon/terminal-host' + import { createPtySubprocess } from './src/main/daemon/pty-subprocess' import { setAppEnvironment } from './src/shared/app-environment' import { getCmdExePath } from './src/shared/windows-batch-spawn' try { require.resolve('node-pty'); throw new Error('node-pty unexpectedly available') } @@ -31,29 +32,31 @@ describe.skipIf(!existsSync(runtime))('isolated Bun in-process PTY artifact', () getVersion: () => 'test', isPackaged: () => true, onWillQuit() {}, exit: code => process.exit(code), getAppMetrics: () => [] }) - const provider = new LocalPtyProvider() + const host = new TerminalHost({ spawnSubprocess: createPtySubprocess }) let output = '', resolveExit const exit = new Promise(resolve => { resolveExit = resolve }) - provider.onData(event => { output += event.data }) - provider.onExit(event => resolveExit(event.code)) - const deadline = setTimeout(() => { provider.killAll(); process.exit(98) }, 10_000) + const streamClient = { + onData: data => { output += data }, onExit: code => resolveExit(code) + } + const deadline = setTimeout(() => { void host.dispose().finally(() => process.exit(98)) }, 10_000) ;(async () => { - const first = await provider.spawn({ - sessionId: 'isolated-bun-fallback', cols: 80, rows: 24, cwd: process.cwd(), + const first = await host.createOrAttach({ + sessionId: 'isolated-bun-daemon', streamClient, cols: 80, rows: 24, cwd: process.cwd(), shellOverride: process.platform === 'win32' ? getCmdExePath() : '/bin/sh' }) - const again = await provider.spawn({sessionId:first.id, cols:100, rows:30}) - provider.write(first.id, process.platform === 'win32' - ? 'echo ORCA_BUN_FALLBACK_READY & exit 17\\r' - : 'printf ORCA_BUN_FALLBACK_READY; exit 17\\r') + const again = await host.createOrAttach({sessionId:'isolated-bun-daemon', streamClient, cols:100, rows:30}) + host.write('isolated-bun-daemon', process.platform === 'win32' + ? 'echo ORCA_BUN_DAEMON_READY & exit 17\\r' + : 'printf ORCA_BUN_DAEMON_READY; exit 17\\r') const code = await exit clearTimeout(deadline) console.log(JSON.stringify({ - version:process.versions.bun, code, output:output.includes('ORCA_BUN_FALLBACK_READY'), - reattached:again.isReattach === true && again.pid === first.pid, - retired:provider.getPtyProcess(first.id) === undefined + version:process.versions.bun, code, output:output.includes('ORCA_BUN_DAEMON_READY'), + reattached:again.isNew === false && again.pid === first.pid, + retired:host.listSessions().length === 0 })) - })().catch(error => { clearTimeout(deadline); provider.killAll(); console.error(error);process.exitCode=1 }) + await host.dispose() + })().catch(error => { clearTimeout(deadline); void host.dispose(); console.error(error);process.exitCode=1 }) `, resolveDir: process.cwd(), loader: 'ts' @@ -61,7 +64,7 @@ describe.skipIf(!existsSync(runtime))('isolated Bun in-process PTY artifact', () bundle: true, platform: 'node', format: 'cjs', - target: 'node18', + target: 'es2022', external, outfile: entry, logLevel: 'silent' @@ -81,7 +84,7 @@ describe.skipIf(!existsSync(runtime))('isolated Bun in-process PTY artifact', () await copyFile(runtime, isolatedRuntime) const result = await runProcess({ program: isolatedRuntime, - args: ['--no-install', entry], + args: ['--no-env-file', '--no-install', entry], cwd: directory, env: { ...process.env, diff --git a/src/main/daemon/daemon-bun-runtime-retention.test.ts b/src/main/daemon/daemon-bun-runtime-retention.test.ts new file mode 100644 index 000000000000..d22da00ccded --- /dev/null +++ b/src/main/daemon/daemon-bun-runtime-retention.test.ts @@ -0,0 +1,367 @@ +import type * as FileSystem from 'node:fs/promises' +import { + mkdtemp, + mkdir, + readdir, + rm, + symlink, + writeFile, + realpath, + readFile +} from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { acquireProfileStateRuntimeAdmission } from '../persistence/profile-state/profile-state-access' +import { + acquireDaemonRuntimeLaunchPin, + collectPinnedDaemonRuntimeDirectories, + MANAGED_DAEMON_RUNTIME_DIRECTORY, + pruneDaemonBunRuntimes +} from './daemon-bun-runtime-retention' + +const { removingTrash } = vi.hoisted(() => ({ removingTrash: vi.fn() })) +vi.mock('node:fs/promises', async (importOriginal) => { + const fs = await importOriginal() + return { + ...fs, + rm: async (...args: Parameters) => { + if (String(args[0]).includes('.bun-trash-')) { + await removingTrash() + } + return fs.rm(...args) + } + } +}) +const { snapshot, available, identities } = vi.hoisted(() => ({ + snapshot: vi.fn(), + available: vi.fn(), + identities: vi.fn() +})) +vi.mock('../windows/windows-process-table', () => ({ + readWindowsProcessTableFresh: snapshot, + isWindowsProcessTableAvailable: available, + isWindowsProcessStartTimeAvailable: identities, + readWindowsProcessCreationTime: () => null +})) +const roots: string[] = [] +beforeEach(() => { + removingTrash.mockReset().mockResolvedValue(undefined) + snapshot.mockReset().mockResolvedValue([]) + available.mockReturnValue(true) + identities.mockReturnValue(true) +}) +afterEach(async () => { + await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true }))) +}) +async function fixture(count = 8) { + const host = await realpath(await mkdtemp(join(tmpdir(), 'bun-retention-'))) + roots.push(host) + const root = join(host, MANAGED_DAEMON_RUNTIME_DIRECTORY) + await mkdir(root) + const paths: string[] = [] + for (let i = 0; i < count; i++) { + const path = join(root, `bun-${i.toString(16).padStart(64, '0')}`) + await mkdir(path) + await writeFile(join(path, 'daemon-entry.js'), 'entry') + paths.push(path) + } + return { host, root, paths } +} +async function generations(root: string) { + return (await readdir(root)).filter((name) => name.startsWith('bun-')) +} +describe('managed Windows daemon runtime retention', () => { + it('bounds each pass and retains two unused generations without touching legacy paths', async () => { + const { host, root } = await fixture() + await mkdir(join(host, 'legacy-version')) + await pruneDaemonBunRuntimes(host) + expect(await generations(root)).toHaveLength(4) + await pruneDaemonBunRuntimes(host) + expect(await generations(root)).toHaveLength(2) + expect(await readdir(host)).toContain('legacy-version') + }) + it('excludes pruning while any profile is resolving or launching', async () => { + const { host, root } = await fixture() + const first = await acquireDaemonRuntimeLaunchPin(root) + const second = await acquireDaemonRuntimeLaunchPin(root) + first.release() + await pruneDaemonBunRuntimes(host) + expect(snapshot).not.toHaveBeenCalled() + expect(await generations(root)).toHaveLength(8) + second.release() + await pruneDaemonBunRuntimes(host) + expect(await generations(root)).toHaveLength(4) + }) + it('a launch waits for the exclusive snapshot to finish', async () => { + const { host, root } = await fixture() + let finish: (() => void) | undefined + snapshot.mockImplementation( + () => + new Promise((resolve) => { + finish = () => resolve([]) + }) + ) + const pruning = pruneDaemonBunRuntimes(host) + await vi.waitFor(() => expect(snapshot).toHaveBeenCalled()) + let admitted = false + const launching = acquireDaemonRuntimeLaunchPin(root).then((pin) => { + admitted = true + return pin + }) + await new Promise((resolve) => setTimeout(resolve, 30)) + expect(admitted).toBe(false) + finish?.() + await pruning + ;(await launching).release() + }) + it('admits launches while retired files are still being deleted', async () => { + const { host, root } = await fixture() + let finish: (() => void) | undefined + removingTrash.mockImplementationOnce( + () => + new Promise((resolve) => { + finish = resolve + }) + ) + const pruning = pruneDaemonBunRuntimes(host) + await vi.waitFor(() => expect(removingTrash).toHaveBeenCalled()) + const pin = await acquireDaemonRuntimeLaunchPin(root) + try { + expect(await generations(root)).toHaveLength(4) + expect((await readdir(root)).some((name) => name.startsWith('.bun-trash-'))).toBe(true) + } finally { + pin.release() + finish?.() + await pruning + } + expect((await readdir(root)).some((name) => name.startsWith('.bun-trash-'))).toBe(false) + }) + it('retries deletion of retired directories left by interrupted pruning', async () => { + const { host, root } = await fixture(0) + const trash = join(root, '.bun-trash-12345678-1234-1234-1234-123456789abc') + await mkdir(trash) + await writeFile(join(trash, 'daemon-entry.js'), 'entry') + await pruneDaemonBunRuntimes(host) + expect((await readdir(root)).filter((name) => name.startsWith('.bun-trash-'))).toEqual([]) + }) + it.each(['bun-runtime.exe', 'bun.exe', 'OpenConsole.exe'])( + 'retains live %s closures', + async (name) => { + const { host, root, paths } = await fixture(4) + snapshot.mockResolvedValue( + paths.map((path, index) => ({ + pid: index + 100, + ppid: 1, + name, + creationTimeMs: 100, + command: `"C:${path.replaceAll('/', '\\')}\\${name}"` + })) + ) + await pruneDaemonBunRuntimes(host) + expect(await generations(root)).toHaveLength(4) + } + ) + it.each([ + { command: '', creationTimeMs: 100 }, + { command: 'bun-runtime.exe daemon-entry.js', creationTimeMs: 100 }, + { command: '"C:\\elsewhere\\bun.exe"', creationTimeMs: undefined } + ])('preserves all files when a relevant owner is unverifiable: %j', async (row) => { + const { host, root } = await fixture() + snapshot.mockResolvedValue([{ pid: 100, ppid: 1, name: 'bun-runtime.exe', ...row }]) + await pruneDaemonBunRuntimes(host) + expect(await generations(root)).toHaveLength(8) + }) + it('preserves everything when the snapshot fails or creation-time support is missing', async () => { + const { host, root } = await fixture() + snapshot.mockRejectedValue(new Error('unavailable')) + await pruneDaemonBunRuntimes(host) + expect(await generations(root)).toHaveLength(8) + snapshot.mockClear() + identities.mockReturnValue(false) + await pruneDaemonBunRuntimes(host) + expect(snapshot).not.toHaveBeenCalled() + }) + it('cleans staging only under exclusive ownership, regardless of its age', async () => { + const { host, root } = await fixture(0) + const name = '.bun-staging-12345678-1234-1234-1234-123456789abc' + await mkdir(join(root, name)) + const pin = acquireProfileStateRuntimeAdmission(root) + await pruneDaemonBunRuntimes(host) + expect(await readdir(root)).toContain(name) + pin.release() + await pruneDaemonBunRuntimes(host) + expect(await readdir(root)).not.toContain(name) + }) + it('does not traverse symlinked generations or roots', async () => { + const { host, root } = await fixture(0) + const external = await mkdtemp(join(tmpdir(), 'bun-external-')) + roots.push(external) + await writeFile(join(external, 'precious'), 'live') + await symlink(external, join(root, `bun-${'a'.repeat(64)}`), 'dir') + await pruneDaemonBunRuntimes(host) + expect(await readdir(external)).toEqual(['precious']) + await rm(root, { recursive: true }) + await symlink(external, root, 'dir') + snapshot.mockClear() + await pruneDaemonBunRuntimes(host) + expect(snapshot).not.toHaveBeenCalled() + }) + it('reclaims a pin only when its recorded process has exited', async () => { + const { host, root } = await fixture() + acquireProfileStateRuntimeAdmission(root) + const participants = join(root, '.profile-state-access', 'participants') + const [owner] = await readdir(participants) + const path = join(participants, owner, `${owner}.owner`) + const record = JSON.parse(await readFile(path, 'utf8')) + await writeFile(path, JSON.stringify({ ...record, pid: 2147483647 })) + await pruneDaemonBunRuntimes(host) + expect(await generations(root)).toHaveLength(4) + expect(await readdir(participants)).toEqual([]) + }) + it('preserves malformed stale launch owners rather than stealing them by age', async () => { + const { host, root } = await fixture() + const pin = acquireProfileStateRuntimeAdmission(root) + const participants = join(root, '.profile-state-access', 'participants') + const [owner] = await readdir(participants) + await writeFile(join(participants, owner, `${owner}.owner`), '{}') + await pruneDaemonBunRuntimes(host) + expect(await generations(root)).toHaveLength(8) + // The malformed owner is retained deliberately; the fixture removes it after the test. + expect(pin).toBeDefined() + }) +}) + +describe('runtime executable aliases', () => { + const directory = 'C:\\Users\\Owner Name\\Orca\\managed-v1\\bun-generation' + function processRow(command: string) { + return { pid: 100, ppid: 1, name: 'bun-runtime.exe', creationTimeMs: 100, command } + } + it.each([ + 'C:\\Users\\OWNERN~1\\Orca\\managed-v1\\bun-generation\\bun-runtime.exe', + 'C:\\runtime-junction\\bun-runtime.exe' + ])('pins an executable reached through %s', async (alias) => { + const resolve = vi.fn().mockResolvedValue(`${directory}\\bun-runtime.exe`) + const pinned = await collectPinnedDaemonRuntimeDirectories( + [processRow(`"${alias}" daemon-entry.js`)], + [directory], + resolve + ) + expect(resolve).toHaveBeenCalledWith(alias) + expect(pinned).toEqual(new Set([directory])) + }) + it('vetoes deletion when a known executable cannot be canonicalized', async () => { + const resolve = vi.fn().mockRejectedValue(new Error('EACCES')) + expect( + await collectPinnedDaemonRuntimeDirectories( + [processRow('"C:\\unknown\\bun-runtime.exe"')], + [directory], + resolve + ) + ).toBeNull() + }) + it('still pins a gate script when its executable belongs to another generation', async () => { + const resolve = vi.fn().mockResolvedValue('C:\\other\\bun-runtime.exe') + const pinned = await collectPinnedDaemonRuntimeDirectories( + [processRow(`"C:\\other\\bun-runtime.exe" "${directory}\\windows-bun-pty-gate-entry.js"`)], + [directory], + resolve + ) + expect(pinned).toEqual(new Set([directory])) + }) + it('does not confuse a sibling prefix with an owned directory', async () => { + const resolve = vi + .fn() + .mockImplementation(async (path: string) => + path === join(directory, 'bun-runtime.exe') + ? `${directory}\\bun-runtime.exe` + : `${directory}.repair-1\\bun-runtime.exe` + ) + expect( + await collectPinnedDaemonRuntimeDirectories( + [processRow('"C:\\other\\bun-runtime.exe"')], + [directory], + resolve + ) + ).toEqual(new Set()) + }) +}) + +it.each(['bun-runtime.exe', 'OpenConsole.exe'])( + 'pins %s when file realpath expands an alias that directory realpath retains', + async (name) => { + const directory = 'C:\\Users\\RUNNER~1\\runtime\\bun-generation' + const executable = `C:\\runtime-junction\\${name}` + const candidateImage = + name === 'OpenConsole.exe' ? join(directory, 'conpty', name) : join(directory, name) + const expanded = `C:\\Users\\runneradmin\\runtime\\bun-generation\\${name}` + const resolve = vi.fn().mockImplementation(async (path: string) => { + if (path === executable || path === candidateImage) { + return expanded + } + throw new Error('unexpected path') + }) + expect( + await collectPinnedDaemonRuntimeDirectories( + [{ pid: 100, ppid: 1, name, creationTimeMs: 100, command: `"${executable}"` }], + [directory], + resolve + ) + ).toEqual(new Set([directory])) + expect(resolve).toHaveBeenCalledWith(candidateImage) + } +) + +it('vetoes deletion when candidate file identity cannot be compared', async () => { + const executable = 'C:\\other\\bun-runtime.exe' + const resolve = vi.fn().mockImplementation(async (path: string) => { + if (path === executable) { + return executable + } + throw new Error('EACCES') + }) + expect( + await collectPinnedDaemonRuntimeDirectories( + [ + { + pid: 100, + ppid: 1, + name: 'bun-runtime.exe', + creationTimeMs: 100, + command: `"${executable}"` + } + ], + ['C:\\runtime\\bun-generation'], + resolve + ) + ).toBeNull() +}) + +it('ignores absent staging images but vetoes unknown and published identities', async () => { + const staging = 'C:\\runtime\\.bun-staging-12345678-1234-1234-1234-123456789abc' + const executable = 'C:\\unrelated\\bun-runtime.exe' + const row = { + pid: 100, + ppid: 1, + name: 'bun-runtime.exe', + creationTimeMs: 100, + command: `"${executable}"` + } + let code = 'ENOENT' + const resolve = vi.fn().mockImplementation(async (path: string) => { + if (path === executable) { + return executable + } + throw Object.assign(new Error(code), { code }) + }) + expect(await collectPinnedDaemonRuntimeDirectories([row], [staging], resolve)).toEqual(new Set()) + expect( + await collectPinnedDaemonRuntimeDirectories( + [row], + [`C:\\runtime\\bun-${'a'.repeat(64)}`], + resolve + ) + ).toBeNull() + code = 'EACCES' + expect(await collectPinnedDaemonRuntimeDirectories([row], [staging], resolve)).toBeNull() +}) diff --git a/src/main/daemon/daemon-bun-runtime-retention.ts b/src/main/daemon/daemon-bun-runtime-retention.ts new file mode 100644 index 000000000000..ca695f5d5f2e --- /dev/null +++ b/src/main/daemon/daemon-bun-runtime-retention.ts @@ -0,0 +1,193 @@ +import { randomUUID } from 'node:crypto' +import { lstat, readdir, realpath, rename, rm } from 'node:fs/promises' +import { join, win32 } from 'node:path' +import { setTimeout as delay } from 'node:timers/promises' +import { + acquireProfileStateMaintenance, + acquireProfileStateRuntimeAdmission, + ProfileStateAccessError, + type ProfileStateRuntimeAdmission +} from '../persistence/profile-state/profile-state-access' +import { + isWindowsProcessStartTimeAvailable, + isWindowsProcessTableAvailable, + readWindowsProcessTableFresh, + type WindowsProcessRow +} from '../windows/windows-process-table' + +export const MANAGED_DAEMON_RUNTIME_DIRECTORY = 'managed-v1' +const GENERATION = /^bun-[a-f0-9]{64}(?:\.repair-[1-9][0-9]*)?$/u +const STAGING = /^\.bun-staging-[a-f0-9-]{36}$/u +const TRASH = /^\.bun-trash-[a-f0-9-]{36}$/u +const RETAIN_UNUSED = 2 +const DELETE_LIMIT = 4 + +/** The profile admission protocol also provides host-wide shared/exclusive process ownership. */ +export async function acquireDaemonRuntimeLaunchPin( + root: string +): Promise { + for (let attempt = 0; ; attempt++) { + try { + return acquireProfileStateRuntimeAdmission(root) + } catch (error) { + if (!(error instanceof ProfileStateAccessError) || attempt >= 30) { + throw error + } + await delay(100) + } + } +} + +function normalized(path: string): string { + return path.replaceAll('/', '\\').toLowerCase() +} + +/** Unknown image paths veto removal; a basename alone cannot locate a loaded runtime. */ +export async function collectPinnedDaemonRuntimeDirectories( + rows: WindowsProcessRow[], + directories: string[], + resolveExecutablePath: (path: string) => Promise = realpath +): Promise | null> { + const pinned = new Set() + const canonicalImages = new Map() + for (const row of rows) { + if (!/^(?:bun(?:-runtime)?|openconsole)\.exe$/iu.test(row.name)) { + continue + } + if (!row.creationTimeMs || !Number.isSafeInteger(row.creationTimeMs) || !row.command) { + return null + } + const executable = row.command.startsWith('"') + ? /^"([^"\r\n]+)"/u.exec(row.command)?.[1] + : /^([^\s"]+)/u.exec(row.command)?.[1] + if ( + !executable || + !win32.isAbsolute(executable) || + !/^(?:[a-z]:\\|\\\\)/iu.test(normalized(executable)) + ) { + return null + } + let canonicalExecutable: string + try { + canonicalExecutable = normalized(await resolveExecutablePath(executable)) + } catch { + return null + } + const command = normalized(row.command) + for (const directory of directories) { + const prefix = `${normalized(directory)}\\` + if (canonicalExecutable.startsWith(prefix) || command.includes(prefix)) { + pinned.add(directory) + continue + } + const image = + row.name.toLowerCase() === 'openconsole.exe' + ? join(directory, 'conpty', 'OpenConsole.exe') + : join(directory, 'bun-runtime.exe') + let canonicalImage = canonicalImages.get(image) + if (canonicalImage === undefined) { + try { + canonicalImage = normalized(await resolveExecutablePath(image)) + canonicalImages.set(image, canonicalImage) + } catch (error) { + // Staging and retired trees may lack images; unknown published identities veto deletion. + if ( + (!STAGING.test(win32.basename(directory)) && !TRASH.test(win32.basename(directory))) || + !(error instanceof Error) || + !('code' in error) || + error.code !== 'ENOENT' + ) { + return null + } + canonicalImage = null + canonicalImages.set(image, null) + } + } + if (canonicalImage === canonicalExecutable) { + pinned.add(directory) + } + } + } + return pinned +} + +/** Only this namespace participates in launch pins; older runtimes are never candidates. */ +export async function pruneDaemonBunRuntimes(hostRoot: string): Promise { + if (!isWindowsProcessTableAvailable() || !isWindowsProcessStartTimeAvailable()) { + return + } + const requestedRoot = join(hostRoot, MANAGED_DAEMON_RUNTIME_DIRECTORY) + const retired: string[] = [] + let admission: ProfileStateRuntimeAdmission | undefined + try { + if ((await lstat(requestedRoot)).isSymbolicLink()) { + return + } + const root = await realpath(requestedRoot) + admission = acquireProfileStateMaintenance(root) + const candidates: { path: string; modified: number; staging: boolean }[] = [] + for (const entry of await readdir(root, { withFileTypes: true })) { + if ( + !entry.isDirectory() || + (!GENERATION.test(entry.name) && !STAGING.test(entry.name) && !TRASH.test(entry.name)) + ) { + continue + } + const path = join(root, entry.name) + const stat = await lstat(path) + if (!stat.isDirectory() || stat.isSymbolicLink() || (await realpath(path)) !== path) { + continue + } + candidates.push({ + path, + modified: stat.mtimeMs, + staging: STAGING.test(entry.name) || TRASH.test(entry.name) + }) + } + const pinned = await collectPinnedDaemonRuntimeDirectories( + await readWindowsProcessTableFresh(), + candidates.map(({ path }) => path) + ) + if (!pinned) { + return + } + const unused = candidates + .filter(({ path, staging }) => !staging && !pinned.has(path)) + .sort((a, b) => b.modified - a.modified) + .slice(RETAIN_UNUSED) + const abandonedStaging = candidates.filter(({ path, staging }) => staging && !pinned.has(path)) + for (const candidate of [...abandonedStaging, ...unused].slice(0, DELETE_LIMIT)) { + admission.assertActive() + if ((await lstat(candidate.path)).isSymbolicLink()) { + continue + } + const trash = join(root, `.bun-trash-${randomUUID()}`) + await rename(candidate.path, trash) + retired.push(trash) + } + } catch { + // Retention is optional; missing identity, busy admissions and disk errors preserve files. + } finally { + try { + admission?.release() + } catch (error) { + console.warn('[daemon] Could not release runtime retention ownership', error) + } + } + // Detached names cannot be launched; slow antivirus deletion must not block launch pins. + for (const path of retired) { + await rm(path, { recursive: true, force: true }).catch(() => {}) + } +} + +const pendingPrunes = new Set() +export function scheduleDaemonBunRuntimePrune(hostRoot: string): void { + if (pendingPrunes.has(hostRoot)) { + return + } + pendingPrunes.add(hostRoot) + const timer = setTimeout(() => { + void pruneDaemonBunRuntimes(hostRoot).finally(() => pendingPrunes.delete(hostRoot)) + }, 0) + timer.unref() +} diff --git a/src/main/daemon/daemon-bun-runtime.test.ts b/src/main/daemon/daemon-bun-runtime.test.ts new file mode 100644 index 000000000000..ebd1b7e51609 --- /dev/null +++ b/src/main/daemon/daemon-bun-runtime.test.ts @@ -0,0 +1,291 @@ +import { createHash } from 'node:crypto' +import { mkdtemp, mkdir, writeFile, readFile, rm, rename, realpath } from 'node:fs/promises' +import type * as NodeFsPromises from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, describe, expect, it, vi } from 'vitest' +const { renameMock, environment } = vi.hoisted(() => ({ + renameMock: vi.fn(), + environment: { + appPath: '', + localRoot: '', + getAppPath() { + return this.appPath + }, + getVersion: () => 'test-version', + getPath: () => '/unused-user-data' + } +})) +vi.mock('node:fs/promises', async (importOriginal) => { + const actual = await importOriginal() + renameMock.mockImplementation(actual.rename) + return { ...actual, rename: renameMock } +}) +vi.mock('../../shared/windows-conpty-release', async () => { + const { createHash } = await import('node:crypto') + const hashes = { + 'conpty.dll': createHash('sha256').update('conpty fixture').digest('hex'), + 'OpenConsole.exe': createHash('sha256').update('console fixture').digest('hex') + } + return { WINDOWS_CONPTY_FILES: { x64: hashes, arm64: hashes } } +}) +vi.mock('../../shared/app-environment', () => ({ getAppEnvironment: () => environment })) +import { resolveDaemonBunRuntime, resolveDesktopDaemonBunRuntime } from './daemon-bun-runtime' +import { ORCAD_BUN_VERSION } from '../../shared/orcad-bun-runtime' +const roots: string[] = [] +afterEach(async () => { + vi.unstubAllGlobals() + await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true }))) +}) +async function fixture(platform: 'darwin' | 'win32' = 'darwin') { + const root = await mkdtemp(join(tmpdir(), 'daemon-bun-test-')) + roots.push(root) + const bundleDir = join(root, 'bundle') + const runtimeDir = join(root, 'runtime') + const windowsProcessTreeDir = join(root, 'native') + for (const dir of [bundleDir, runtimeDir, windowsProcessTreeDir]) { + await mkdir(dir) + } + if (platform === 'win32') { + await mkdir(join(runtimeDir, 'conpty')) + await writeFile(join(runtimeDir, 'conpty', 'conpty.dll'), 'conpty fixture') + await writeFile(join(runtimeDir, 'conpty', 'OpenConsole.exe'), 'console fixture') + await writeFile(join(runtimeDir, 'conpty', 'LICENSE.txt'), 'license fixture') + } + await writeFile(join(bundleDir, 'daemon-entry.js'), 'daemon') + await writeFile(join(bundleDir, 'windows-bun-pty-gate-entry.js'), 'gate') + await writeFile(join(windowsProcessTreeDir, 'package.json'), '{}') + await mkdir(join(windowsProcessTreeDir, 'lib')) + await mkdir(join(windowsProcessTreeDir, 'build', 'Release'), { recursive: true }) + await writeFile(join(windowsProcessTreeDir, 'lib', 'index.js'), 'native loader') + await writeFile( + join(windowsProcessTreeDir, 'build', 'Release', 'windows_process_tree.node'), + 'native' + ) + await writeFile(join(runtimeDir, platform === 'win32' ? 'bun-runtime.exe' : 'bun-runtime'), 'bun') + await writeFile( + join(runtimeDir, 'runtime.json'), + JSON.stringify({ + target: `${platform}-arm64`, + version: ORCAD_BUN_VERSION, + sha256: createHash('sha256').update('bun').digest('hex') + }) + ) + return { root, bundleDir, runtimeDir, platform, arch: 'arm64', windowsProcessTreeDir } +} +describe('desktop Bun daemon runtime', () => { + it('uses shipped runtime directly outside Windows relocation', async () => { + const options = await fixture() + expect(await resolveDaemonBunRuntime(options)).toEqual({ + execPath: join(options.runtimeDir, 'bun-runtime'), + entryPath: join(options.bundleDir, 'daemon-entry.js') + }) + }) + it('selects the verified source provider for unpackaged Windows launches', async () => { + const options = await fixture('win32') + const runtime = await resolveDaemonBunRuntime(options) + expect(runtime.conptyLibraryPath).toBe(join(options.runtimeDir, 'conpty', 'conpty.dll')) + }) + + it('rejects corrupt runtime bytes before launching', async () => { + const options = await fixture() + await writeFile(join(options.runtimeDir, 'bun-runtime'), 'bad') + await expect(resolveDaemonBunRuntime(options)).rejects.toThrow('identity mismatch') + }) + it('copies Windows runtime and native closure outside install and repairs beside existing bytes', async () => { + const fixtureOptions = await fixture('win32') + const options = { + ...fixtureOptions, + relocationRoot: join(fixtureOptions.root, 'local', 'version') + } + const first = await resolveDaemonBunRuntime(options) + expect(first.execPath.startsWith(options.relocationRoot)).toBe(true) + expect(first.conptyLibraryPath).toBe(join(first.entryPath, '..', 'conpty', 'conpty.dll')) + expect(await resolveDaemonBunRuntime(options)).toEqual(first) + expect(await readFile(join(first.entryPath, '..', 'conpty', 'conpty.dll'), 'utf8')).toBe( + 'conpty fixture' + ) + expect(await readFile(join(first.entryPath, '..', 'conpty', 'OpenConsole.exe'), 'utf8')).toBe( + 'console fixture' + ) + expect(await readFile(join(first.entryPath, '..', 'conpty', 'LICENSE.txt'), 'utf8')).toBe( + 'license fixture' + ) + expect( + await readFile( + join( + first.entryPath, + '..', + 'node_modules', + '@vscode', + 'windows-process-tree', + 'package.json' + ), + 'utf8' + ) + ).toBe('{}') + await writeFile(first.execPath, 'old-live-bytes') + const second = await resolveDaemonBunRuntime(options) + expect(second.execPath).not.toBe(first.execPath) + expect(await readFile(first.execPath, 'utf8')).toBe('old-live-bytes') + expect(await readFile(second.execPath, 'utf8')).toBe('bun') + }) + it('retains process-table runtime siblings without copying native build intermediates', async () => { + const options = await fixture('win32') + await writeFile(join(options.windowsProcessTreeDir, 'lib', 'binding.js'), 'runtime sibling') + await writeFile( + join(options.windowsProcessTreeDir, 'build', 'Release', 'unused.pdb'), + 'symbols' + ) + await mkdir(join(options.windowsProcessTreeDir, 'src')) + await writeFile(join(options.windowsProcessTreeDir, 'src', 'unused.cc'), 'source') + const relocated = await resolveDaemonBunRuntime({ + ...options, + relocationRoot: join(options.root, 'cache') + }) + const nativeRoot = join( + relocated.entryPath, + '..', + 'node_modules', + '@vscode', + 'windows-process-tree' + ) + expect(await readFile(join(nativeRoot, 'lib', 'binding.js'), 'utf8')).toBe('runtime sibling') + await expect( + readFile(join(nativeRoot, 'build', 'Release', 'unused.pdb')) + ).rejects.toMatchObject({ code: 'ENOENT' }) + await expect(readFile(join(nativeRoot, 'src', 'unused.cc'))).rejects.toMatchObject({ + code: 'ENOENT' + }) + }) + + it.each(['conpty.dll', 'OpenConsole.exe'])( + 'rejects corrupt Windows provider %s before relocation', + async (filename) => { + const options = await fixture('win32') + await writeFile(join(options.runtimeDir, 'conpty', filename), 'foreign provider') + await expect( + resolveDaemonBunRuntime({ ...options, relocationRoot: join(options.root, 'cache') }) + ).rejects.toThrow('ConPTY identity mismatch') + } + ) + + it('rejects a Windows payload missing its native process table before relocation', async () => { + const options = await fixture('win32') + await rm(join(options.windowsProcessTreeDir, 'build', 'Release', 'windows_process_tree.node')) + await expect( + resolveDaemonBunRuntime({ ...options, relocationRoot: join(options.root, 'cache') }) + ).rejects.toThrow('Windows terminal process inspection artifact is missing') + }) + + it('repairs a corrupt gate without replacing a runtime directory used by another owner', async () => { + const options = await fixture('win32') + const relocated = { ...options, relocationRoot: join(options.root, 'cache') } + const first = await resolveDaemonBunRuntime(relocated) + const firstGate = join(first.entryPath, '..', 'windows-bun-pty-gate-entry.js') + await writeFile(firstGate, 'foreign gate') + const second = await resolveDaemonBunRuntime(relocated) + expect(second.entryPath).not.toBe(first.entryPath) + expect(await readFile(firstGate, 'utf8')).toBe('foreign gate') + expect( + await readFile(join(second.entryPath, '..', 'windows-bun-pty-gate-entry.js'), 'utf8') + ).toBe('gate') + }) + it('repairs a changed ConPTY beside the previous owner and keeps the full provider after install removal', async () => { + const options = await fixture('win32') + const relocated = { ...options, relocationRoot: join(options.root, 'cache') } + const first = await resolveDaemonBunRuntime(relocated) + const oldLibrary = join(first.entryPath, '..', 'conpty', 'conpty.dll') + await writeFile(oldLibrary, 'old-owner bytes') + const second = await resolveDaemonBunRuntime(relocated) + expect(second.entryPath).not.toBe(first.entryPath) + expect(await readFile(oldLibrary, 'utf8')).toBe('old-owner bytes') + await rm(options.runtimeDir, { recursive: true }) + expect(await readFile(join(second.entryPath, '..', 'conpty', 'conpty.dll'), 'utf8')).toBe( + 'conpty fixture' + ) + expect(await readFile(join(second.entryPath, '..', 'conpty', 'OpenConsole.exe'), 'utf8')).toBe( + 'console fixture' + ) + }) + + it('retries a transient Windows publication lock without bypassing relocation', async () => { + const options = await fixture('win32') + vi.stubGlobal('process', { ...process, platform: 'win32' }) + renameMock.mockRejectedValueOnce(Object.assign(new Error('scanner lock'), { code: 'EPERM' })) + const runtime = await resolveDaemonBunRuntime({ + ...options, + relocationRoot: join(options.root, 'cache') + }) + expect(runtime.execPath.startsWith(join(options.root, 'cache'))).toBe(true) + expect(await readFile(runtime.execPath, 'utf8')).toBe('bun') + }) + + it('does not fall back to the install directory after permanent publication failure', async () => { + const options = await fixture('win32') + renameMock.mockRejectedValueOnce(Object.assign(new Error('disk full'), { code: 'ENOSPC' })) + await expect( + resolveDaemonBunRuntime({ + ...options, + relocationRoot: join(options.root, 'cache') + }) + ).rejects.toThrow('disk full') + }) + + it('surfaces a persistent Windows publication lock after bounded retries', async () => { + const options = await fixture('win32') + vi.stubGlobal('process', { ...process, platform: 'win32' }) + const error = Object.assign(new Error('persistent scanner lock'), { code: 'EBUSY' }) + for (let attempt = 0; attempt < 6; attempt++) { + renameMock.mockRejectedValueOnce(error) + } + const callsBefore = renameMock.mock.calls.length + await expect( + resolveDaemonBunRuntime({ + ...options, + relocationRoot: join(options.root, 'cache') + }) + ).rejects.toThrow('persistent scanner lock') + expect(renameMock.mock.calls.length - callsBefore).toBe(6) + }) + + it('requires a managed local runtime root for packaged Windows', async () => { + environment.appPath = join('/resources', 'app.asar') + vi.stubGlobal('process', { + ...process, + platform: 'win32', + versions: { ...process.versions, electron: '43.7.0' }, + env: { ...process.env, LOCALAPPDATA: '' } + }) + await expect(resolveDesktopDaemonBunRuntime()).rejects.toThrow('LOCALAPPDATA is required') + }) + + it('keeps packaged Windows copies outside the legacy profile-scoped prune root', async () => { + const options = await fixture('win32') + const resources = join(options.root, 'resources') + await mkdir(join(resources, 'node_modules', '@vscode'), { recursive: true }) + await rename(options.bundleDir, join(resources, 'terminal-daemon')) + await rename(options.runtimeDir, join(resources, 'cli-runtime')) + await rename( + options.windowsProcessTreeDir, + join(resources, 'node_modules', '@vscode', 'windows-process-tree') + ) + environment.appPath = join(resources, 'app.asar') + const localAppData = join(options.root, "O'Connor Local Data") + vi.stubGlobal('process', { + ...process, + platform: 'win32', + arch: 'arm64', + versions: { ...process.versions, electron: '43.7.0' }, + env: { ...process.env, LOCALAPPDATA: localAppData } + }) + const runtime = await resolveDesktopDaemonBunRuntime() + expect( + runtime?.execPath.startsWith( + join(await realpath(localAppData), 'Orca', 'terminal-daemon-host', 'managed-v1') + ) + ).toBe(true) + expect(runtime?.execPath.startsWith(join(localAppData, 'Orca', 'daemon-host'))).toBe(false) + runtime?.releaseLaunchPin?.() + }) +}) diff --git a/src/main/daemon/daemon-bun-runtime.ts b/src/main/daemon/daemon-bun-runtime.ts new file mode 100644 index 000000000000..4619a9e70514 --- /dev/null +++ b/src/main/daemon/daemon-bun-runtime.ts @@ -0,0 +1,247 @@ +import { createHash, randomUUID } from 'node:crypto' +import { createReadStream } from 'node:fs' +import { copyFile, mkdir, readFile, readdir, realpath, rm } from 'node:fs/promises' +import { dirname, join } from 'node:path' +import { z } from 'zod' +import { getAppEnvironment } from '../../shared/app-environment' +import { ORCAD_BUN_VERSION } from '../../shared/orcad-bun-runtime' +import { WINDOWS_CONPTY_FILES } from '../../shared/windows-conpty-release' +import { + isRuntimeProcessTreePath, + WINDOWS_PROCESS_TREE_REQUIRED +} from '../windows/windows-process-tree-runtime-manifest' +import { renameFileWithWindowsRetryAsync } from '../codex-accounts/fs-utils' +import { findOrcadCachePath } from '../ssh/orcad-cache-path' +import { + acquireDaemonRuntimeLaunchPin, + MANAGED_DAEMON_RUNTIME_DIRECTORY, + scheduleDaemonBunRuntimePrune +} from './daemon-bun-runtime-retention' + +export type DaemonBunRuntime = { + execPath: string + entryPath: string + conptyLibraryPath?: string + releaseLaunchPin?: () => void +} +type RuntimeFile = { source: string; relative: string; sha256: string } +const Manifest = z.object({ + target: z.string(), + version: z.literal(ORCAD_BUN_VERSION), + sha256: z.string().regex(/^[a-f0-9]{64}$/u) +}) + +async function digest(path: string): Promise { + const hash = createHash('sha256') + for await (const chunk of createReadStream(path)) { + hash.update(chunk) + } + return hash.digest('hex') +} + +async function bundleFiles( + root: string, + relative = '', + include: (relative: string) => boolean = () => true +): Promise { + const files: RuntimeFile[] = [] + for (const entry of await readdir(join(root, relative), { withFileTypes: true })) { + const name = join(relative, entry.name) + if (!include(name)) { + continue + } + if (entry.isDirectory()) { + files.push(...(await bundleFiles(root, name, include))) + } else if (entry.isFile()) { + files.push({ + source: join(root, name), + relative: name, + sha256: await digest(join(root, name)) + }) + } else { + throw new Error(`Unsupported terminal daemon artifact: ${name}`) + } + } + return files.sort((a, b) => a.relative.localeCompare(b.relative)) +} + +export async function resolveDaemonBunRuntime(options: { + bundleDir: string + runtimeDir: string + platform: NodeJS.Platform + arch: string + relocationRoot?: string + windowsProcessTreeDir?: string +}): Promise { + const filename = options.platform === 'win32' ? 'bun-runtime.exe' : 'bun-runtime' + const runtime = join(options.runtimeDir, filename) + const manifest = Manifest.parse( + JSON.parse(await readFile(join(options.runtimeDir, 'runtime.json'), 'utf8')) + ) + const target = `${options.platform}-${options.arch}${options.platform === 'linux' ? '-glibc' : ''}` + if (manifest.target !== target || (await digest(runtime)) !== manifest.sha256) { + throw new Error('Terminal daemon Bun runtime identity mismatch') + } + const files = await bundleFiles(options.bundleDir) + if (!files.some((file) => file.relative === 'daemon-entry.js')) { + throw new Error('Terminal daemon entry is missing') + } + if (options.platform === 'win32') { + if (!options.windowsProcessTreeDir) { + throw new Error('Windows terminal process inspection artifacts are missing') + } + if (!files.some((file) => file.relative === 'windows-bun-pty-gate-entry.js')) { + throw new Error('Windows terminal gate is missing') + } + if (options.arch !== 'x64' && options.arch !== 'arm64') { + throw new Error(`Unsupported Windows terminal architecture: ${options.arch}`) + } + const conptyFiles = await bundleFiles(join(options.runtimeDir, 'conpty')) + for (const [filename, hash] of Object.entries(WINDOWS_CONPTY_FILES[options.arch])) { + if (!conptyFiles.some((file) => file.relative === filename && file.sha256 === hash)) { + throw new Error(`Windows terminal ConPTY identity mismatch: ${filename}`) + } + } + for (const file of conptyFiles) { + files.push({ ...file, relative: join('conpty', file.relative) }) + } + const nativeFiles = await bundleFiles(options.windowsProcessTreeDir, '', (relative) => + isRuntimeProcessTreePath(relative.replaceAll('\\', '/')) + ) + for (const required of WINDOWS_PROCESS_TREE_REQUIRED) { + if (!nativeFiles.some((file) => file.relative === join(...required.split('/')))) { + throw new Error(`Windows terminal process inspection artifact is missing: ${required}`) + } + } + for (const file of nativeFiles) { + files.push({ + ...file, + relative: join('node_modules', '@vscode', 'windows-process-tree', file.relative) + }) + } + } + if (!options.relocationRoot) { + return { + execPath: runtime, + entryPath: join(options.bundleDir, 'daemon-entry.js'), + ...(options.platform === 'win32' + ? { conptyLibraryPath: join(options.runtimeDir, 'conpty', 'conpty.dll') } + : {}) + } + } + files.push({ source: runtime, relative: filename, sha256: manifest.sha256 }) + const identity = createHash('sha256') + .update(JSON.stringify(files.map(({ relative, sha256 }) => [relative, sha256]))) + .digest('hex') + const verify = async (root: string): Promise => { + try { + for (const file of files) { + if ((await digest(join(root, file.relative))) !== file.sha256) { + return false + } + } + return true + } catch { + return false + } + } + const cached = await findOrcadCachePath( + (attempt) => + join(options.relocationRoot!, `bun-${identity}${attempt ? `.repair-${attempt}` : ''}`), + verify + ) + if (!cached.verified) { + const staging = join(options.relocationRoot, `.bun-staging-${randomUUID()}`) + try { + for (const file of files) { + const destination = join(staging, file.relative) + await mkdir(dirname(destination), { recursive: true }) + await copyFile(file.source, destination) + } + if (!(await verify(staging))) { + throw new Error('Terminal daemon artifacts changed while copying') + } + try { + await renameFileWithWindowsRetryAsync(staging, cached.path) + } catch (error) { + if (!(await verify(cached.path))) { + throw error + } + } + } finally { + await rm(staging, { recursive: true, force: true }) + } + } + return { + execPath: join(cached.path, filename), + entryPath: join(cached.path, 'daemon-entry.js'), + ...(options.platform === 'win32' + ? { conptyLibraryPath: join(cached.path, 'conpty', 'conpty.dll') } + : {}) + } +} + +export function desktopDaemonBundleDir(): string { + const appPath = getAppEnvironment().getAppPath() + return appPath.includes('app.asar') + ? join(dirname(appPath), 'terminal-daemon') + : join(appPath, 'out', 'terminal-daemon') +} + +export async function resolveDesktopDaemonBunRuntime(): Promise { + if (!process.versions.electron) { + return null + } + const environment = getAppEnvironment() + const appPath = environment.getAppPath() + const packaged = appPath.includes('app.asar') + if (process.platform === 'win32' && packaged && !process.env.LOCALAPPDATA) { + throw new Error('LOCALAPPDATA is required to preserve terminal sessions across app updates') + } + const hostRoot = + process.platform === 'win32' && packaged && process.env.LOCALAPPDATA + ? join(process.env.LOCALAPPDATA, 'Orca', 'terminal-daemon-host') + : undefined + const managedRoot = hostRoot ? join(hostRoot, MANAGED_DAEMON_RUNTIME_DIRECTORY) : undefined + const pin = managedRoot ? await acquireDaemonRuntimeLaunchPin(managedRoot) : undefined + let released = false + const releaseLaunchPin = (): void => { + if (released) { + return + } + released = true + try { + pin?.release() + } catch (error) { + console.warn('[daemon] Could not release runtime launch pin', error) + } + if (hostRoot) { + scheduleDaemonBunRuntimePrune(hostRoot) + } + } + try { + const runtime = await resolveDaemonBunRuntime({ + bundleDir: desktopDaemonBundleDir(), + runtimeDir: packaged + ? join(dirname(appPath), 'cli-runtime') + : join(appPath, 'out', 'cli-runtime', `${process.platform}-${process.arch}`), + platform: process.platform, + arch: process.arch, + ...(process.platform === 'win32' + ? { + windowsProcessTreeDir: join( + packaged ? dirname(appPath) : appPath, + 'node_modules', + '@vscode', + 'windows-process-tree' + ) + } + : {}), + ...(managedRoot ? { relocationRoot: await realpath(managedRoot) } : {}) + }) + return pin ? { ...runtime, releaseLaunchPin } : runtime + } catch (error) { + releaseLaunchPin() + throw error + } +} diff --git a/src/main/daemon/daemon-client-connection-attempt.ts b/src/main/daemon/daemon-client-connection-attempt.ts new file mode 100644 index 000000000000..80a43a091df4 --- /dev/null +++ b/src/main/daemon/daemon-client-connection-attempt.ts @@ -0,0 +1,67 @@ +import type { Duplex } from 'node:stream' +import { + awaitDaemonTransport, + destroyDaemonTransport, + type DaemonClientTransport +} from './daemon-client-transport' +import { DaemonProtocolError } from './types' + +export class DaemonConnectionAttempt { + readonly controller = new AbortController() + private deadline: number + readonly readerCleanups: (() => void)[] = [] + releaseReaders(): void { + for (const cleanup of this.readerCleanups.splice(0)) { + cleanup() + } + } + private guards: (() => void)[] = [] + constructor( + private transport: DaemonClientTransport, + private timeoutMs: number, + private sharedBudget: boolean + ) { + this.deadline = Date.now() + timeoutMs + } + remainingMs(): number { + return this.sharedBudget ? Math.max(1, this.deadline - Date.now()) : this.timeoutMs + } + wait(run: (signal: AbortSignal) => T | Promise, late?: (value: T) => void): Promise { + return awaitDaemonTransport( + run, + { timeoutMs: this.remainingMs(), signal: this.controller.signal }, + late + ) + } + readToken(): Promise { + return this.wait((signal) => + this.transport.readToken({ timeoutMs: this.remainingMs(), signal }) + ) + } + async connect(role: 'control' | 'stream'): Promise { + const stream = await this.wait( + (signal) => this.transport.connect(role, { timeoutMs: this.remainingMs(), signal }), + destroyDaemonTransport + ) + const fail = (): void => { + queueMicrotask(() => + this.controller.abort(new DaemonProtocolError('Connection closed during setup')) + ) + } + stream.on('error', fail) + stream.on('close', fail) + this.guards.push(() => { + stream.off('error', fail) + stream.off('close', fail) + }) + if (stream.destroyed || !stream.readable || !stream.writable) { + fail() + } + return stream + } + releaseGuards(): void { + for (const cleanup of this.guards.splice(0)) { + cleanup() + } + } +} diff --git a/src/main/daemon/daemon-client-connections.ts b/src/main/daemon/daemon-client-connections.ts index 7eedef1285bf..03696123fbe4 100644 --- a/src/main/daemon/daemon-client-connections.ts +++ b/src/main/daemon/daemon-client-connections.ts @@ -22,6 +22,8 @@ type DaemonClientConnectionOptions = { identity: { launchNonce: string | null startedAtMs: number | null + linuxStartTicks?: string + bootId?: string entryPath: string | null appVersion: string | null spawnerExecPath: string | null @@ -151,6 +153,9 @@ export class DaemonClientConnections { pid: process.pid, startedAtMs: identity.startedAtMs, launchNonce: identity.launchNonce, + ...(identity.linuxStartTicks && identity.bootId + ? { linuxStartTicks: identity.linuxStartTicks, bootId: identity.bootId } + : {}), ...(identity.entryPath ? { entryPath: identity.entryPath } : {}), ...(identity.appVersion ? { appVersion: identity.appVersion } : {}), ...(identity.spawnerExecPath ? { spawnerExecPath: identity.spawnerExecPath } : {}) diff --git a/src/main/daemon/daemon-client-duplex.test.ts b/src/main/daemon/daemon-client-duplex.test.ts new file mode 100644 index 000000000000..d4c5495aa702 --- /dev/null +++ b/src/main/daemon/daemon-client-duplex.test.ts @@ -0,0 +1,279 @@ +import { Duplex, PassThrough, Readable, Writable } from 'node:stream' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { DaemonClient, type DaemonClientTransport } from './client' +import { encodeNdjson, createNdjsonParser } from './ndjson' + +const identity = { pid: 42, startedAtMs: 123, launchNonce: 'owner' } +const clients: DaemonClient[] = [] +const streams: Duplex[] = [] +afterEach(() => { + for (const client of clients.splice(0)) { + client.disconnect() + } + for (const stream of streams.splice(0)) { + stream.destroy() + } +}) +function pair() { + const toServer = new PassThrough() + const toClient = new PassThrough() + const client = Duplex.from({ + readable: Readable.toWeb(toClient), + writable: Writable.toWeb(toServer) + }) + const server = Duplex.from({ + readable: Readable.toWeb(toServer), + writable: Writable.toWeb(toClient) + }) + streams.push(client, server) + for (const stream of [client, server]) { + stream.on('error', () => {}) + } + return { client, server } +} +function deferred() { + let resolve!: (value: T) => void + const promise = new Promise((complete) => { + resolve = complete + }) + return { promise, resolve } +} +function harness( + otherIdentity = identity, + admitIdentity?: (identity: typeof otherIdentity | null) => Promise +) { + const endpoints = new Map>() + const hellos: unknown[] = [] + const transport: DaemonClientTransport = { + readToken: async () => 'token', + connect: async (role) => { + const endpoint = pair() + endpoints.set(role, endpoint) + const parser = createNdjsonParser( + (message) => { + if (!message || typeof message !== 'object' || !('type' in message)) { + return + } + if (message.type === 'hello') { + hellos.push(message) + endpoint.server.write( + encodeNdjson({ + ok: true, + daemonIdentity: role === 'control' ? identity : otherIdentity + }) + ) + } else if ('id' in message && typeof message.id === 'string') { + endpoint.server.write( + encodeNdjson({ + id: message.id, + ok: true, + payload: { echoed: 'payload' in message ? message.payload : undefined } + }) + ) + } + }, + () => {} + ) + endpoint.server.on('data', (data: Buffer) => parser.feed(data.toString())) + return endpoint.client + } + } + const client = new DaemonClient({ transport, admitIdentity }) + clients.push(client) + return { client, transport, endpoints, hellos } +} + +describe('daemon ordered duplex transport', () => { + it('does not publish a connection or its events before admission and fences a canceled admission', async () => { + const first = deferred() + const second = deferred() + const admit = vi.fn().mockReturnValueOnce(first.promise).mockReturnValueOnce(second.promise) + const { client, endpoints } = harness(identity, admit) + const event = vi.fn() + client.onEvent(event) + const connecting = expect(client.ensureConnected()).rejects.toThrow('Disconnected') + await vi.waitFor(() => expect(admit).toHaveBeenCalledOnce()) + endpoints.get('stream')!.server.write( + encodeNdjson({ + type: 'event', + event: 'data', + sessionId: 's', + payload: { data: 'unadmitted' } + }) + ) + await new Promise((resolve) => setImmediate(resolve)) + expect(event).not.toHaveBeenCalled() + await expect(client.request('ping', undefined)).rejects.toThrow('Not connected') + client.disconnect() + first.resolve() + await connecting + const reconnected = client.ensureConnected() + await vi.waitFor(() => expect(admit).toHaveBeenCalledTimes(2)) + await expect(client.request('ping', undefined)).rejects.toThrow('Not connected') + second.resolve() + await reconnected + await expect(client.request('ping', undefined)).resolves.toEqual({}) + expect(event).not.toHaveBeenCalled() + }) + + it('authenticates both roles, handles RPC/events and settles notify writes', async () => { + const { client, endpoints, hellos } = harness() + const event = vi.fn() + client.onEvent(event) + await client.ensureConnected() + expect(hellos).toEqual([ + expect.objectContaining({ role: 'control', token: 'token', clientId: expect.any(String) }), + expect.objectContaining({ role: 'stream', token: 'token', clientId: expect.any(String) }) + ]) + expect(await client.request('echo', 'hello')).toEqual({ echoed: 'hello' }) + const payload = { type: 'event', event: 'output', data: '界' } + endpoints.get('stream')!.server.write(encodeNdjson(payload)) + await vi.waitFor(() => expect(event).toHaveBeenCalledWith(payload)) + expect(await client.notifyWithSettlement('input', 'x')).toEqual({ outcome: 'accepted' }) + expect(client.getDaemonIdentity()).toEqual(identity) + }) + + it('preserves an event coalesced with the stream hello reply', async () => { + const event = vi.fn() + const payload = { type: 'event', event: 'output', data: '界' } + const transport: DaemonClientTransport = { + readToken: () => 'token', + connect: async (role) => { + const endpoint = pair() + endpoint.server.once('data', () => + endpoint.server.write( + encodeNdjson({ ok: true, daemonIdentity: identity }) + + (role === 'stream' ? encodeNdjson(payload) : '') + ) + ) + return endpoint.client + } + } + const client = new DaemonClient({ transport }) + clients.push(client) + client.onEvent(event) + await client.ensureConnected() + await vi.waitFor(() => expect(event).toHaveBeenCalledExactlyOnceWith(payload)) + }) + + it('refuses distinct owners on paired connections and destroys both', async () => { + const { client, endpoints } = harness({ ...identity, launchNonce: 'other' }) + await expect(client.ensureConnected()).rejects.toThrow('identity changed') + expect(client.isConnected()).toBe(false) + expect([...endpoints.values()].every(({ client: stream }) => stream.destroyed)).toBe(true) + }) + + it('cancels an asynchronous token read without ever connecting', async () => { + const token = deferred() + const connect = vi.fn() + let signal: AbortSignal | undefined + const client = new DaemonClient({ + transport: { + readToken: (operation) => { + signal = operation.signal + return token.promise + }, + connect + } + }) + clients.push(client) + const pending = client.ensureConnected() + const rejection = expect(pending).rejects.toThrow('Disconnected') + await vi.waitFor(() => expect(signal).toBeDefined()) + client.disconnect() + await rejection + expect(signal?.aborted).toBe(true) + token.resolve('late-token') + await Promise.resolve() + expect(connect).not.toHaveBeenCalled() + }) + + it('destroys a connection that arrives after cancellation', async () => { + const connected = deferred() + const connect = vi.fn(() => connected.promise) + const client = new DaemonClient({ transport: { readToken: () => 'token', connect } }) + clients.push(client) + const pending = client.ensureConnected() + const rejection = expect(pending).rejects.toThrow('Disconnected') + await vi.waitFor(() => expect(connect).toHaveBeenCalled()) + client.disconnect() + await rejection + const late = pair().client + connected.resolve(late) + await vi.waitFor(() => expect(late.destroyed).toBe(true)) + }) + + it('closes authenticated control when stream connection fails', async () => { + const { client, transport, endpoints } = harness() + const connect = transport.connect + transport.connect = (role, operation) => + role === 'stream' ? Promise.reject(new Error('bridge failed')) : connect(role, operation) + await expect(client.ensureConnected()).rejects.toThrow('bridge failed') + expect(endpoints.get('control')?.client.destroyed).toBe(true) + expect(client.isConnected()).toBe(false) + }) + + it('aborts a pending stream bridge if authenticated control closes', async () => { + const { client, transport, endpoints } = harness() + const connect = transport.connect + const pendingStream = deferred() + transport.connect = (role, operation) => + role === 'stream' ? pendingStream.promise : connect(role, operation) + const pending = client.ensureConnected() + const rejection = expect(pending).rejects.toThrow('closed during setup') + await vi.waitFor(() => expect(endpoints.get('control')).toBeDefined()) + endpoints.get('control')!.client.destroy() + await rejection + const late = pair().client + pendingStream.resolve(late) + await vi.waitFor(() => expect(late.destroyed).toBe(true)) + }) + + it('waits for the writable callback under bridge backpressure', async () => { + let releaseWrite: (() => void) | undefined + const { client, transport } = harness() + const connect = transport.connect + transport.connect = async (role, operation) => { + if (role !== 'control') { + return connect(role, operation) + } + const stream = new Duplex({ + read() {}, + write(chunk, _encoding, callback) { + const message: unknown = JSON.parse(chunk.toString()) + if ( + message && + typeof message === 'object' && + 'type' in message && + message.type === 'hello' + ) { + this.push(encodeNdjson({ ok: true, daemonIdentity: identity })) + callback() + } else { + releaseWrite = () => callback() + } + } + }) + streams.push(stream) + return stream + } + await client.ensureConnected() + let settled = false + const write = client.notifyWithSettlement('input', 'x').then((value) => { + settled = true + return value + }) + await vi.waitFor(() => expect(releaseWrite).toBeDefined()) + expect(settled).toBe(false) + releaseWrite?.() + await expect(write).resolves.toEqual({ outcome: 'accepted' }) + }) + + it('bounds asynchronous token work by the shared connection deadline', async () => { + const client = new DaemonClient({ + transport: { readToken: () => new Promise(() => {}), connect: vi.fn() } + }) + clients.push(client) + await expect(client.ensureConnectedWithin(15)).rejects.toThrow('timed out') + }) +}) diff --git a/src/main/daemon/daemon-client-hello-handshake.ts b/src/main/daemon/daemon-client-hello-handshake.ts index 681177c28c5b..acdf4143ad0e 100644 --- a/src/main/daemon/daemon-client-hello-handshake.ts +++ b/src/main/daemon/daemon-client-hello-handshake.ts @@ -1,15 +1,15 @@ -import type { Socket } from 'node:net' -import { StringDecoder } from 'node:string_decoder' +import type { Duplex } from 'node:stream' import { encodeNdjson } from './ndjson' import { CLEAN_DISCONNECT_PROTOCOL_VERSION, DaemonProtocolError } from './types' import type { DaemonEndpointIdentity, HelloMessage, HelloResponse } from './types' import { addNodePtyRecoveryHint } from './node-pty-error-hints' export type DaemonHelloRequest = { - socket: Socket + socket: Duplex token: string role: 'control' | 'stream' timeoutMs: number + signal?: AbortSignal protocolVersion: number clientId: string } @@ -17,7 +17,7 @@ export type DaemonHelloRequest = { export function sendDaemonHello( request: DaemonHelloRequest ): Promise { - const { socket, token, role, timeoutMs, protocolVersion, clientId } = request + const { socket, token, role, timeoutMs, protocolVersion, clientId, signal } = request return new Promise((resolve, reject) => { const hello: HelloMessage = { type: 'hello', @@ -27,7 +27,7 @@ export function sendDaemonHello( role } - let buffer = '' + let buffer = Buffer.alloc(0) let settled = false let timer: ReturnType | null = null const cleanup = (): void => { @@ -38,6 +38,7 @@ export function sendDaemonHello( socket.removeListener('data', onData) socket.removeListener('error', onError) socket.removeListener('close', onClose) + signal?.removeEventListener('abort', onAbort) } const finish = (error?: Error, identity: DaemonEndpointIdentity | null = null): void => { if (settled) { @@ -51,17 +52,14 @@ export function sendDaemonHello( } resolve(identity) } - // Why: daemon socket chunks can split emoji/box-drawing UTF-8 bytes. - // Decoding each Buffer independently would permanently inject U+FFFD. - const decoder = new StringDecoder('utf8') const onData = (chunk: Buffer): void => { - buffer += decoder.write(chunk) - const newlineIdx = buffer.indexOf('\n') + buffer = Buffer.concat([buffer, chunk]) + const newlineIdx = buffer.indexOf(10) if (newlineIdx === -1) { return } - const line = buffer.slice(0, newlineIdx) + const line = buffer.subarray(0, newlineIdx).toString('utf8') try { const response = JSON.parse(line) as HelloResponse if (response.ok) { @@ -73,6 +71,12 @@ export function sendDaemonHello( finish(new DaemonProtocolError('Invalid daemon identity')) return } + // Preserve frames coalesced with hello until the role reader is installed. + socket.pause() + const remainder = buffer.subarray(newlineIdx + 1) + if (remainder.length) { + socket.unshift(remainder) + } finish(undefined, identity) } else { finish( @@ -87,6 +91,12 @@ export function sendDaemonHello( const onClose = (): void => finish(new DaemonProtocolError('Connection closed before hello response')) + const onAbort = (): void => finish(new DaemonProtocolError('Disconnected')) + signal?.addEventListener('abort', onAbort, { once: true }) + if (signal?.aborted) { + onAbort() + return + } timer = setTimeout(() => { // Why: a stale daemon can accept the socket but never answer hello; // without a handshake timeout, startup waits forever on ensureConnected(). @@ -100,21 +110,18 @@ export function sendDaemonHello( }) } -function parseDaemonEndpointIdentity(value: unknown): DaemonEndpointIdentity | null { - if (!value || typeof value !== 'object') { +function isDaemonIdentityRecord(value: unknown): value is Record { + return value !== null && typeof value === 'object' +} + +function parseDaemonEndpointIdentity(identity: unknown): DaemonEndpointIdentity | null { + if (!isDaemonIdentityRecord(identity)) { return null } - const identity = value as { - pid?: unknown - startedAtMs?: unknown - launchNonce?: unknown - entryPath?: unknown - appVersion?: unknown - spawnerExecPath?: unknown - } if ( + typeof identity.pid !== 'number' || !Number.isSafeInteger(identity.pid) || - (identity.pid as number) <= 0 || + identity.pid <= 0 || typeof identity.startedAtMs !== 'number' || !Number.isFinite(identity.startedAtMs) || identity.startedAtMs <= 0 || @@ -123,8 +130,21 @@ function parseDaemonEndpointIdentity(value: unknown): DaemonEndpointIdentity | n ) { return null } + if ( + (identity.linuxStartTicks !== undefined || identity.bootId !== undefined) && + (typeof identity.linuxStartTicks !== 'string' || + !/^\d+$/.test(identity.linuxStartTicks) || + typeof identity.bootId !== 'string' || + !identity.bootId || + /[\0\r\n]/.test(identity.bootId)) + ) { + return null + } return { - pid: identity.pid as number, + ...(typeof identity.linuxStartTicks === 'string' && typeof identity.bootId === 'string' + ? { linuxStartTicks: identity.linuxStartTicks, bootId: identity.bootId } + : {}), + pid: identity.pid, startedAtMs: identity.startedAtMs, launchNonce: identity.launchNonce, ...(typeof identity.entryPath === 'string' && identity.entryPath.length > 0 @@ -149,6 +169,8 @@ export function sameDaemonIdentity( right !== null && left.pid === right.pid && left.startedAtMs === right.startedAtMs && - left.launchNonce === right.launchNonce) + left.launchNonce === right.launchNonce && + left.linuxStartTicks === right.linuxStartTicks && + left.bootId === right.bootId) ) } diff --git a/src/main/daemon/daemon-client-ndjson-readers.ts b/src/main/daemon/daemon-client-ndjson-readers.ts index 5a04098e6e1d..7ae799cd2a9e 100644 --- a/src/main/daemon/daemon-client-ndjson-readers.ts +++ b/src/main/daemon/daemon-client-ndjson-readers.ts @@ -1,10 +1,10 @@ -import type { Socket } from 'node:net' +import type { Duplex } from 'node:stream' import { StringDecoder } from 'node:string_decoder' import { createNdjsonParser } from './ndjson' import type { DaemonEvent, RpcResponse } from './types' export function attachControlResponseReader( - socket: Socket, + socket: Duplex, onResponse: (response: RpcResponse) => void ): () => void { // Why: control responses may contain terminal/startup data with multibyte @@ -17,11 +17,12 @@ export function attachControlResponseReader( const onData = (chunk: Buffer) => parser.feed(decoder.write(chunk)) socket.on('data', onData) + socket.resume() return () => socket.off('data', onData) } export function attachStreamEventReader( - socket: Socket, + socket: Duplex, onEvent: (event: DaemonEvent) => void ): () => void { // Why: PTY output streams include emoji/box-drawing tables; socket chunks @@ -39,5 +40,6 @@ export function attachStreamEventReader( const onData = (chunk: Buffer) => parser.feed(decoder.write(chunk)) socket.on('data', onData) + socket.resume() return () => socket.off('data', onData) } diff --git a/src/main/daemon/daemon-client-notify-settlement.ts b/src/main/daemon/daemon-client-notify-settlement.ts index 4fe0d3693a11..68e8466e8aea 100644 --- a/src/main/daemon/daemon-client-notify-settlement.ts +++ b/src/main/daemon/daemon-client-notify-settlement.ts @@ -1,4 +1,4 @@ -import type { Socket } from 'node:net' +import type { Duplex } from 'node:stream' import { encodeNdjson } from './ndjson' import { WRITE_ACCEPTED, @@ -8,7 +8,7 @@ import { } from '../../shared/pty-write-settlement' export type NotifySettlementRequest = { - socket: Socket + socket: Duplex message: unknown timeoutMs: number // Why: a notify that never drains is the only local evidence a dead endpoint leaves. diff --git a/src/main/daemon/daemon-client-rpc-request.ts b/src/main/daemon/daemon-client-rpc-request.ts index 2bd4f6741de9..442f4dfaef69 100644 --- a/src/main/daemon/daemon-client-rpc-request.ts +++ b/src/main/daemon/daemon-client-rpc-request.ts @@ -1,4 +1,4 @@ -import type { Socket } from 'node:net' +import type { Duplex } from 'node:stream' import { encodeNdjson } from './ndjson' import { DAEMON_UNAVAILABLE_RECONNECT_MESSAGE, @@ -9,7 +9,7 @@ import { isTerminalAttachCanceledMessage } from './daemon-errors' import type { DaemonPendingRequests } from './daemon-client-pending-requests' type DaemonRpcRequestOptions = { - socket: Socket + socket: Duplex pendingRequests: DaemonPendingRequests id: string type: string diff --git a/src/main/daemon/daemon-client-socket-connect.ts b/src/main/daemon/daemon-client-socket-connect.ts index e6a2f615e17f..54feec2a8bf9 100644 --- a/src/main/daemon/daemon-client-socket-connect.ts +++ b/src/main/daemon/daemon-client-socket-connect.ts @@ -1,4 +1,5 @@ import { connect, type Socket } from 'node:net' +import type { Duplex } from 'node:stream' import { DaemonProtocolError } from './types' export function connectDaemonSocket(socketPath: string, timeoutMs: number): Promise { @@ -31,8 +32,8 @@ export function connectDaemonSocket(socketPath: string, timeoutMs: number): Prom // Why: capture both sockets by value — the fields they came from are nulled on teardown, // so a cleanup reading them back would unhook nothing. export function armDaemonSocketCloseHandlers( - controlSocket: Socket, - streamSocket: Socket, + controlSocket: Duplex, + streamSocket: Duplex, handleClose: () => void ): () => void { controlSocket.on('close', handleClose) diff --git a/src/main/daemon/daemon-client-transport.ts b/src/main/daemon/daemon-client-transport.ts new file mode 100644 index 000000000000..e5f6fd59dee5 --- /dev/null +++ b/src/main/daemon/daemon-client-transport.ts @@ -0,0 +1,93 @@ +import { readFileSync } from 'node:fs' +import type { Duplex } from 'node:stream' +import { connectDaemonSocket } from './daemon-client-socket-connect' +import type { DaemonEndpointIdentity } from './daemon-hello-protocol' +import { DaemonProtocolError } from './types' + +export type DaemonTransportOperation = { timeoutMs: number; signal: AbortSignal } +export type DaemonClientTransport = { + readToken: (operation: DaemonTransportOperation) => string | Promise + /** Return a connected ordered byte stream; destroy must release its bridge resources. */ + connect: (role: 'control' | 'stream', operation: DaemonTransportOperation) => Promise +} +export type DaemonClientOptions = { + protocolVersion?: number + admitIdentity?: (identity: DaemonEndpointIdentity | null) => Promise +} & ( + | { socketPath: string; tokenPath: string; transport?: never } + | { transport: DaemonClientTransport; socketPath?: never; tokenPath?: never } +) + +export function resolveDaemonTransport(options: DaemonClientOptions): DaemonClientTransport { + if (options.transport) { + return options.transport + } + return { + readToken: () => { + try { + return readFileSync(options.tokenPath, 'utf-8').trim() + } catch (error) { + // Missing auth must not preempt the connection that proves endpoint absence. + if (error instanceof Error && 'code' in error && error.code === 'ENOENT') { + return '' + } + throw error + } + }, + connect: (_role, { timeoutMs }) => connectDaemonSocket(options.socketPath, timeoutMs) + } +} + +/** Bound transports that ignore cancellation, disposing streams that arrive too late. */ +export function awaitDaemonTransport( + run: (signal: AbortSignal) => T | Promise, + operation: DaemonTransportOperation, + disposeLate?: (value: T) => void +): Promise { + const controller = new AbortController() + return new Promise((resolve, reject) => { + let settled = false + const settle = (complete: () => void): void => { + if (settled) { + return + } + settled = true + clearTimeout(timer) + operation.signal.removeEventListener('abort', abort) + complete() + } + const fail = (error: unknown): void => + settle(() => { + controller.abort(error) + reject(error) + }) + const abort = (): void => + fail(operation.signal.reason ?? new DaemonProtocolError('Disconnected')) + const timer = setTimeout( + () => fail(new DaemonProtocolError('Connection timed out')), + operation.timeoutMs + ) + operation.signal.addEventListener('abort', abort, { once: true }) + if (operation.signal.aborted) { + abort() + return + } + Promise.resolve() + .then(() => { + if (controller.signal.aborted) { + throw controller.signal.reason + } + return run(controller.signal) + }) + .then( + (value) => (settled ? disposeLate?.(value) : settle(() => resolve(value))), + (error) => fail(error) + ) + }) +} + +export function destroyDaemonTransport(stream: Duplex): void { + // A bridge can report its exit asynchronously after destroy. + stream.once('error', () => {}) + stream.destroy() +} diff --git a/src/main/daemon/daemon-entry.ts b/src/main/daemon/daemon-entry.ts index 8d409a9dc871..6f5f6d7e7cab 100644 --- a/src/main/daemon/daemon-entry.ts +++ b/src/main/daemon/daemon-entry.ts @@ -1,11 +1,4 @@ -/** - * Daemon entry point — runs as a standalone Node.js process. - * - * Usage: node daemon-entry.js --socket /path/to/sock --token /path/to/token - * - * Signals readiness to parent via IPC: { type: 'ready' } - * Shuts down cleanly on SIGTERM. - */ +/** Standalone Bun daemon; reports readiness over IPC and drains sessions on SIGTERM. */ import { readFileSync } from 'node:fs' import { startDaemon, type DaemonHandle } from './daemon-main' import { createPtySubprocess } from './pty-subprocess' @@ -274,7 +267,9 @@ async function main(): Promise { tokenPath, ...(pidPath ? { pidPath } : {}), ...(launchNonce ? { launchNonce } : {}), - ...(pidPath ? { startedAtMs } : {}), + startedAtMs, + linuxStartTicks: readyIdentity.linuxStartTicks, + bootId: readyIdentity.bootId, ...(entryPath ? { entryPath } : {}), ...(appVersion ? { appVersion } : {}), ...(spawnerExecPath ? { spawnerExecPath } : {}), diff --git a/src/main/daemon/daemon-folder-access-probe.test.ts b/src/main/daemon/daemon-folder-access-probe.test.ts index b7f18f7c2166..e52f435a6967 100644 --- a/src/main/daemon/daemon-folder-access-probe.test.ts +++ b/src/main/daemon/daemon-folder-access-probe.test.ts @@ -1,6 +1,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -const { runProcessMock } = vi.hoisted(() => ({ runProcessMock: vi.fn() })) +const { runProcessMock, runtimeMock } = vi.hoisted(() => ({ + runProcessMock: vi.fn(), + runtimeMock: vi.fn() +})) +vi.mock('./daemon-bun-runtime', () => ({ resolveDesktopDaemonBunRuntime: runtimeMock })) vi.mock('../../shared/child-process/run-process', () => ({ runProcess: runProcessMock })) import { probeFolderAccessForFreshDaemon } from './daemon-folder-access-probe' @@ -11,6 +15,7 @@ type RunProcessSpec = { env: NodeJS.ProcessEnv timeoutMs: number maxOutputBytes: number + onChildTerminated?: () => void } function settled(stdout: string, overrides: Record = {}): void { @@ -33,6 +38,7 @@ const DOCUMENTS = '/Users/alice/Documents/repo' beforeEach(() => { runProcessMock.mockReset() + runtimeMock.mockResolvedValue(null) }) describe('probeFolderAccessForFreshDaemon', () => { @@ -72,7 +78,8 @@ describe('probeFolderAccessForFreshDaemon', () => { settled('{"outcome":"ok"}\n') await probeFolderAccessForFreshDaemon(DOCUMENTS) - expect(lastSpec().timeoutMs).toBe(3_000) + expect(lastSpec().timeoutMs).toBeGreaterThan(0) + expect(lastSpec().timeoutMs).toBeLessThanOrEqual(3_000) expect(lastSpec().maxOutputBytes).toBe(1024) }) @@ -120,3 +127,134 @@ describe('probeFolderAccessForFreshDaemon', () => { await expect(probeFolderAccessForFreshDaemon(DOCUMENTS)).resolves.toBe('unknown') }) }) + +it('uses the same Bun executable as a fresh desktop daemon', async () => { + runtimeMock.mockResolvedValue({ + execPath: '/app/cli-runtime/bun-runtime', + entryPath: '/app/terminal-daemon/daemon-entry.js' + }) + settled('{"outcome":"ok"}\n') + await expect(probeFolderAccessForFreshDaemon(DOCUMENTS)).resolves.toBe('ok') + expect(lastSpec().program).toBe('/app/cli-runtime/bun-runtime') + expect(lastSpec().env.ELECTRON_RUN_AS_NODE).toBeUndefined() +}) + +it('bounds runtime resolution and never launches a child after the deadline', async () => { + vi.useFakeTimers() + try { + let finish!: (value: null) => void + runtimeMock.mockReturnValue( + new Promise((resolve) => { + finish = resolve + }) + ) + const result = probeFolderAccessForFreshDaemon(DOCUMENTS) + await vi.advanceTimersByTimeAsync(3_000) + await expect(result).resolves.toBe('unknown') + finish(null) + await vi.advanceTimersByTimeAsync(1) + expect(runProcessMock).not.toHaveBeenCalled() + } finally { + vi.useRealTimers() + } +}) + +it('gives the child only the budget left after runtime resolution', async () => { + vi.useFakeTimers() + try { + runtimeMock.mockImplementation( + () => new Promise((resolve) => setTimeout(() => resolve(null), 2_000)) + ) + settled('{"outcome":"ok"}\n') + const result = probeFolderAccessForFreshDaemon(DOCUMENTS) + await vi.advanceTimersByTimeAsync(2_000) + await expect(result).resolves.toBe('ok') + expect(lastSpec().timeoutMs).toBe(1_000) + } finally { + vi.useRealTimers() + } +}) + +it('releases a launch pin that resolves after the caller deadline', async () => { + vi.useFakeTimers() + try { + const releaseLaunchPin = vi.fn() + let finish: + | ((value: { execPath: string; entryPath: string; releaseLaunchPin: () => void }) => void) + | undefined + runtimeMock.mockReturnValue( + new Promise((resolve) => { + finish = resolve + }) + ) + const result = probeFolderAccessForFreshDaemon(DOCUMENTS) + await vi.advanceTimersByTimeAsync(3_000) + await expect(result).resolves.toBe('unknown') + finish?.({ execPath: 'bun', entryPath: 'daemon-entry.js', releaseLaunchPin }) + await vi.advanceTimersByTimeAsync(1) + expect(releaseLaunchPin).toHaveBeenCalledTimes(1) + expect(runProcessMock).not.toHaveBeenCalled() + } finally { + vi.useRealTimers() + } +}) + +it.each([false, true])( + 'releases the launch pin when child termination is confirmed (failure=%s)', + async (failure) => { + const releaseLaunchPin = vi.fn() + runtimeMock.mockResolvedValue({ + execPath: 'bun', + entryPath: 'daemon-entry.js', + releaseLaunchPin + }) + runProcessMock.mockImplementation(async (spec: RunProcessSpec) => { + expect(releaseLaunchPin).not.toHaveBeenCalled() + spec.onChildTerminated?.() + if (failure) { + throw new Error('spawn failed') + } + return { code: 0, stdout: '{"outcome":"ok"}', timedOut: false } + }) + await expect(probeFolderAccessForFreshDaemon(DOCUMENTS)).resolves.toBe( + failure ? 'unknown' : 'ok' + ) + expect(releaseLaunchPin).toHaveBeenCalledTimes(1) + } +) + +it.each(['resolve', 'reject'])( + 'retains the runtime after abort and process promise %s until confirmed termination', + async (outcome) => { + vi.useFakeTimers() + try { + const releaseLaunchPin = vi.fn() + runtimeMock.mockResolvedValue({ + execPath: 'bun', + entryPath: 'daemon-entry.js', + releaseLaunchPin + }) + let settle: (() => void) | undefined + runProcessMock.mockImplementation( + () => + new Promise((resolve, reject) => { + settle = () => + outcome === 'resolve' + ? resolve({ code: null, stdout: '', timedOut: true }) + : reject(new Error('could not kill child')) + }) + ) + const result = probeFolderAccessForFreshDaemon(DOCUMENTS) + await vi.advanceTimersByTimeAsync(3_000) + await expect(result).resolves.toBe('unknown') + expect(releaseLaunchPin).not.toHaveBeenCalled() + settle?.() + await vi.advanceTimersByTimeAsync(1) + expect(releaseLaunchPin).not.toHaveBeenCalled() + lastSpec().onChildTerminated?.() + expect(releaseLaunchPin).toHaveBeenCalledTimes(1) + } finally { + vi.useRealTimers() + } + } +) diff --git a/src/main/daemon/daemon-folder-access-probe.ts b/src/main/daemon/daemon-folder-access-probe.ts index 716158a38595..a799dbb44c1c 100644 --- a/src/main/daemon/daemon-folder-access-probe.ts +++ b/src/main/daemon/daemon-folder-access-probe.ts @@ -1,9 +1,11 @@ +import { bunOwnedRuntimeArgs } from '../../shared/bun-owned-runtime-args' // Answers the one question the running daemon cannot (STA-7948): would a daemon forked by THIS // app, right now, be able to list this folder? macOS attributes a TCC grant to the process that // forked the child, so only a fresh child of the current app binary can tell the user whether // restarting the terminal service is the remedy or whether they must re-allow Orca first. -import { bunOwnedRuntimeArgs } from '../../shared/bun-owned-runtime-args' +import { waitForPromiseWithSignal } from '../../shared/abort-signal-reason' +import { resolveDesktopDaemonBunRuntime, type DaemonBunRuntime } from './daemon-bun-runtime' import { isAbsolute } from 'node:path' import { runProcess } from '../../shared/child-process/run-process' import type { DirectoryEnumerationOutcome } from './directory-enumeration-probe' @@ -16,13 +18,11 @@ const PROBE_MAX_OUTPUT_BYTES = 1024 /** Everything the child needs; a scrubbed env keeps app-only state out of the probe's TCC context. */ const INHERITED_ENV_NAMES = ['PATH', 'HOME', 'TMPDIR'] as const -// Mirrors enumerateDirectoryOnce's errno mapping. Inlined rather than imported because the child -// runs as plain Node against argv only — it can load nothing from the app bundle. +// Match enumerateDirectoryOnce without importing application code into the probe runtime. const PROBE_SCRIPT = `const fs=require('node:fs');let d;let o;try{d=fs.opendirSync(process.argv[1]);d.readSync();o='ok'}catch(e){const c=e&&e.code;o=c==='EPERM'||c==='EACCES'?'denied':c==='ENOENT'||c==='ENOTDIR'?'missing':'other'}finally{try{if(d)d.closeSync()}catch(_){}}process.stdout.write(JSON.stringify({outcome:o})+'\\n')` -function probeEnvironment(): NodeJS.ProcessEnv { - // Why ELECTRON_RUN_AS_NODE: the app binary is Electron; the daemon is forked the same way. - const env: NodeJS.ProcessEnv = { ELECTRON_RUN_AS_NODE: '1' } +function probeEnvironment(bunRuntime: boolean): NodeJS.ProcessEnv { + const env: NodeJS.ProcessEnv = bunRuntime ? {} : { ELECTRON_RUN_AS_NODE: '1' } for (const name of INHERITED_ENV_NAMES) { const value = process.env[name] if (value !== undefined) { @@ -70,19 +70,52 @@ export async function probeFolderAccessForFreshDaemon( if (!isAbsolute(path)) { return 'unknown' } + const deadline = Date.now() + PROBE_DEADLINE_MS + const controller = new AbortController() + const timer = setTimeout(() => controller.abort(), PROBE_DEADLINE_MS) + let runtime: DaemonBunRuntime | null = null + let finished = false + let childOwnsPin = false try { - const result = await runProcess({ - program: process.execPath, - args: [...(process.versions.bun ? bunOwnedRuntimeArgs() : []), '-e', PROBE_SCRIPT, path], - env: probeEnvironment(), - timeoutMs: PROBE_DEADLINE_MS, - maxOutputBytes: PROBE_MAX_OUTPUT_BYTES - }) + runtime = await waitForPromiseWithSignal( + resolveDesktopDaemonBunRuntime().then((resolved) => { + if (finished || controller.signal.aborted) { + resolved?.releaseLaunchPin?.() + } + return resolved + }), + controller.signal + ) + const remainingMs = deadline - Date.now() + if (controller.signal.aborted || remainingMs <= 0) { + return 'unknown' + } + const usesBun = runtime !== null || Boolean(process.versions.bun) + childOwnsPin = true + const result = await waitForPromiseWithSignal( + runProcess({ + program: runtime?.execPath ?? process.execPath, + args: [...(usesBun ? bunOwnedRuntimeArgs() : []), '-e', PROBE_SCRIPT, path], + env: probeEnvironment(usesBun), + timeoutMs: remainingMs, + signal: controller.signal, + maxOutputBytes: PROBE_MAX_OUTPUT_BYTES, + onChildTerminated: runtime?.releaseLaunchPin + }), + controller.signal + ) if (result.timedOut || result.code !== 0 || result.outputTruncated === true) { return 'unknown' } return parseProbeOutcome(result.stdout) } catch { return 'unknown' + } finally { + finished = true + // The caller deadline does not prove the child has stopped using its runtime. + if (!childOwnsPin) { + runtime?.releaseLaunchPin?.() + } + clearTimeout(timer) } } diff --git a/src/main/daemon/daemon-fresh-spawn-admission.test.ts b/src/main/daemon/daemon-fresh-spawn-admission.test.ts new file mode 100644 index 000000000000..2ca203185d00 --- /dev/null +++ b/src/main/daemon/daemon-fresh-spawn-admission.test.ts @@ -0,0 +1,28 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { DaemonFreshSpawnAdmission } from './daemon-fresh-spawn-admission' + +afterEach(() => vi.useRealTimers()) + +describe('daemon fresh spawn admission', () => { + it('throttles failed health probes and recovers without replacing the service', async () => { + vi.useFakeTimers() + const probe = vi.fn().mockRejectedValueOnce(new Error('unreachable')).mockResolvedValue(true) + const admission = new DaemonFreshSpawnAdmission(probe) + await expect(admission.recover()).resolves.toBe(false) + await expect(admission.recover()).resolves.toBe(false) + expect(probe).toHaveBeenCalledOnce() + expect(admission.unavailable).toBe(true) + await vi.advanceTimersByTimeAsync(30_000) + await expect(admission.recover()).resolves.toBe(true) + expect(admission.unavailable).toBe(false) + await expect(admission.recover()).resolves.toBe(true) + expect(probe).toHaveBeenCalledTimes(2) + }) + it('lets an explicit retry bypass the automatic cooldown', async () => { + const probe = vi.fn().mockResolvedValueOnce(false).mockResolvedValue(true) + const admission = new DaemonFreshSpawnAdmission(probe) + await expect(admission.recover()).resolves.toBe(false) + await expect(admission.recover(true)).resolves.toBe(true) + expect(probe).toHaveBeenCalledTimes(2) + }) +}) diff --git a/src/main/daemon/daemon-fresh-spawn-admission.ts b/src/main/daemon/daemon-fresh-spawn-admission.ts new file mode 100644 index 000000000000..c82dddfa80f7 --- /dev/null +++ b/src/main/daemon/daemon-fresh-spawn-admission.ts @@ -0,0 +1,75 @@ +import type { PtySpawnOptions } from '../providers/types' +import { terminalServiceUnavailable } from '../providers/unavailable-pty-provider' +import { STABLE_PANE_ATTACH_ONLY_DAEMON_PROTOCOL_VERSION } from './daemon-protocol-version' + +export class DaemonFreshSpawnAdmission { + private recovery: Promise | null = null + private retryAfter = 0 + private generation = 0 + + constructor(private probe: (() => Promise) | null) {} + + reset(probe: (() => Promise) | null): void { + this.generation += 1 + this.probe = probe + this.recovery = null + this.retryAfter = 0 + } + + get unavailable(): boolean { + return this.probe !== null + } + + recover(force = false): Promise { + if (!this.probe) { + return Promise.resolve(true) + } + if (this.recovery) { + return this.recovery + } + if (!force && Date.now() < this.retryAfter) { + return Promise.resolve(false) + } + const generation = this.generation + this.recovery = Promise.resolve() + .then(this.probe) + .catch(() => false) + .then((healthy) => { + if (generation !== this.generation) { + return this.recover(force) + } + if (healthy) { + this.probe = null + } else { + this.retryAfter = Date.now() + 30_000 + } + return healthy + }) + .finally(() => { + if (generation === this.generation) { + this.recovery = null + } + }) + return this.recovery + } +} + +export async function admitDaemonSpawn( + admission: DaemonFreshSpawnAdmission, + opts: PtySpawnOptions, + protocolVersion: number, + sessionExists: () => Promise +): Promise { + if (opts.attachOnly || (await admission.recover())) { + return opts + } + if ( + opts.isNewSession || + protocolVersion < STABLE_PANE_ATTACH_ONLY_DAEMON_PROTOCOL_VERSION || + !(await sessionExists()) + ) { + throw terminalServiceUnavailable() + } + // Prevent a retained session's exit during admission from turning attachment into creation. + return { ...opts, attachOnly: true } +} diff --git a/src/main/daemon/daemon-guest-spawn-options.ts b/src/main/daemon/daemon-guest-spawn-options.ts new file mode 100644 index 000000000000..5b8d514bebd3 --- /dev/null +++ b/src/main/daemon/daemon-guest-spawn-options.ts @@ -0,0 +1,24 @@ +import type { PtySpawnOptions } from '../providers/types' +import type { GuestDaemonExecution } from './daemon-pty-runtime-state' +import { normalizeWslColdRestoreCwd } from './wsl-cold-restore-cwd' + +export function guestDaemonSpawnOptions( + guest: GuestDaemonExecution, + options: PtySpawnOptions +): PtySpawnOptions { + const cwd = normalizeWslColdRestoreCwd({ + recoveredCwd: options.cwd ?? guest.defaultCwd, + wslDistro: guest.distro, + guestExecution: true + }) + if (!cwd) { + throw new Error('Guest daemon spawn requires a path in its selected distro') + } + return { + ...options, + cwd, + shellOverride: options.shellOverride || guest.defaultShell, + terminalWindowsWslDistro: undefined, + terminalWindowsPowerShellImplementation: undefined + } +} diff --git a/src/main/daemon/daemon-hello-protocol.ts b/src/main/daemon/daemon-hello-protocol.ts index 5d9d031cb9d3..cc6e48ea04d8 100644 --- a/src/main/daemon/daemon-hello-protocol.ts +++ b/src/main/daemon/daemon-hello-protocol.ts @@ -10,6 +10,8 @@ export type DaemonEndpointIdentity = { pid: number startedAtMs: number launchNonce: string + linuxStartTicks?: string + bootId?: string /** Optional launch metadata. Absent from daemons that predate it; readers must fall back. */ entryPath?: string appVersion?: string @@ -22,3 +24,8 @@ export type HelloResponse = { error?: string daemonIdentity?: DaemonEndpointIdentity } + +export type PingRequest = { + id: string + type: 'ping' +} diff --git a/src/main/daemon/daemon-host-manifest.ts b/src/main/daemon/daemon-host-manifest.ts deleted file mode 100644 index 83d6d4984da7..000000000000 --- a/src/main/daemon/daemon-host-manifest.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { cpSync, existsSync, mkdirSync } from 'node:fs' -import { dirname, join, win32 as winPath } from 'node:path' - -// What the relocated host is made of: which files are mirrored, where each lands, -// and which of a package's files are runtime rather than bulk. The lifecycle -// around it -- when to materialize, what keeps a host valid, pruning -- is in -// daemon-host-relocation.ts. - -/** - * The host exe keeps the app exe's own file name, so the relocated image is a byte-for-byte, - * name-included copy of a signed binary — nothing for EDR to read as a renamed image (MITRE T1036). - * Survival comes from the path (see daemon-host-relocation.ts). The one name-sensitive updater path is the - * no-PowerShell `taskkill /IM` fallback, where the daemon is killed and terminals cold-restore — - * the documented pre-relocation outcome, not a failure. - */ -export const daemonHostExeName = (execPath: string): string => winPath.basename(execPath) - -// V8 snapshots + ICU data the Electron bootstrap reads even under ELECTRON_RUN_AS_NODE; siblings of Orca.exe. -const RUNTIME_DATA_FILES = ['icudtl.dat', 'snapshot_blob.bin', 'v8_context_snapshot.bin'] - -/** - * Everything `require('@vscode/windows-process-tree')` walks, package-relative. - * - * One list, read three times: the copy plan mirrors the package containing them, - * materialization refuses to start without them, and a materialized host is only - * valid while it still has them. Keeping those three in one place is what stops - * "what we copy" and "what we accept" from drifting apart -- drift there means a - * host that is copied, published, refused, and copied again on every launch. - */ -export const WINDOWS_PROCESS_TREE_REQUIRED = [ - 'package.json', - 'lib/index.js', - 'build/Release/windows_process_tree.node' -] as const - -export type CopyOp = { - sourcePath: string - /** Destination path relative to the host root, posix-separated. */ - destRel: string - kind: 'file' | 'dir' - /** When true, a missing source is skipped rather than failing the copy. */ - optional?: boolean - /** Per-source-path predicate for dir copies: return false to skip a path. */ - filter?: (sourcePath: string) => boolean -} - -export type DaemonHostSources = { - appDir: string - execPath: string - resourcesPath: string - entrySourcePath: string - entryRelPath: string - /** The addon's package; without it the daemon forks a shell per poll (#16905). */ - windowsProcessTreeDir: string -} - -/** A required file, a directory on the way to one, or anything in lib/ (index.js requires its siblings). */ -function isRuntimeProcessTreePath(packageRel: string): boolean { - return ( - packageRel === '' || - packageRel.startsWith('lib/') || - WINDOWS_PROCESS_TREE_REQUIRED.some( - (required) => required === packageRel || required.startsWith(`${packageRel}/`) - ) - ) -} - -// win32 path semantics so Windows paths decompose correctly off-win32 in cross-platform unit tests; production runs on win32 only. -export function toPosixRelative(fromDir: string, absPath: string): string { - return winPath.relative(fromDir, absPath).split(winPath.sep).join('/') -} - -export function destPath(root: string, destRel: string): string { - return join(root, ...destRel.split('/')) -} - -// Drop node-pty's .pdb symbols and non-host-arch prebuilds (its bulk); keyed on host arch so a future win32-arm64 build keeps the prebuild it needs. -const HOST_WIN_PREBUILD_DIR = `win32-${process.arch}`.toLowerCase() - -function isRuntimeNodePtyPath(sourcePath: string): boolean { - const p = sourcePath.toLowerCase() - if (p.endsWith('.pdb')) { - return false - } - // Keep only the host arch's win32 prebuild; drop any other win32- dir. - const prebuild = p.match(/prebuilds[\\/](win32-[^\\/]+)/) - return !prebuild || prebuild[1] === HOST_WIN_PREBUILD_DIR -} - -/** - * The ordered copy plan. Every destRel mirrors the source's win-unpacked relative path so require() - * and node-pty's loader resolve the mirror identically to the packaged app. Pure so tests can assert layout. - */ -export function buildDaemonHostManifest(sources: DaemonHostSources): CopyOp[] { - const { appDir, execPath, resourcesPath, entrySourcePath, entryRelPath } = sources - const ops: CopyOp[] = [] - - // Host exe (verbatim name) + V8/ICU blobs at dest root. Top-level DLLs omitted: GPU/media libs a windowless run-as-node host never loads (~48MB saved). - ops.push({ sourcePath: execPath, destRel: daemonHostExeName(execPath), kind: 'file' }) - for (const name of RUNTIME_DATA_FILES) { - ops.push({ sourcePath: join(appDir, name), destRel: name, kind: 'file', optional: true }) - } - - // Daemon bundle: entry + sibling chunks/ + out/package.json (CJS/ESM loader resolution), mirrored verbatim. - ops.push({ sourcePath: entrySourcePath, destRel: entryRelPath, kind: 'file' }) - const chunksDir = join(winPath.dirname(entrySourcePath), 'chunks') - ops.push({ - sourcePath: chunksDir, - destRel: toPosixRelative(appDir, chunksDir), - kind: 'dir', - optional: true - }) - const pkgJson = join(resourcesPath, 'app.asar.unpacked', 'out', 'package.json') - ops.push({ - sourcePath: pkgJson, - destRel: toPosixRelative(appDir, pkgJson), - kind: 'file', - optional: true - }) - - // @vscode/windows-process-tree, mirrored so the daemon's require() resolves it; without it every snapshot forks a powershell.exe (#16905). - // Filtered to the runtime files: the installed package is ~25MB of gyp intermediates (.obj/.pdb) around a ~150KB addon. - const { windowsProcessTreeDir } = sources - ops.push({ - sourcePath: windowsProcessTreeDir, - destRel: toPosixRelative(appDir, windowsProcessTreeDir), - kind: 'dir', - filter: (sourcePath) => - isRuntimeProcessTreePath(toPosixRelative(windowsProcessTreeDir, sourcePath)) - }) - // node-pty tree, mirrored so require('node-pty') resolves it; filtered to drop unused .pdb/other-arch prebuilds. - const nodePtyDir = join(resourcesPath, 'node_modules', 'node-pty') - ops.push({ - sourcePath: nodePtyDir, - destRel: toPosixRelative(appDir, nodePtyDir), - kind: 'dir', - filter: isRuntimeNodePtyPath - }) - - return ops -} - -export function executeManifest(ops: CopyOp[], stagingRoot: string): void { - for (const op of ops) { - if (!existsSync(op.sourcePath)) { - if (op.optional) { - continue - } - throw new Error(`daemon-host relocation: missing required input ${op.sourcePath}`) - } - const dest = destPath(stagingRoot, op.destRel) - mkdirSync(dirname(dest), { recursive: true }) - const { filter } = op - // Dereference symlinks so the copy holds no link back into the install dir. - cpSync(op.sourcePath, dest, { - recursive: op.kind === 'dir', - dereference: true, - force: true, - ...(filter ? { filter: (src: string) => filter(src) } : {}) - }) - } -} diff --git a/src/main/daemon/daemon-host-relocation.test.ts b/src/main/daemon/daemon-host-relocation.test.ts index 7b81bdcfa1e0..b9f935b6a734 100644 --- a/src/main/daemon/daemon-host-relocation.test.ts +++ b/src/main/daemon/daemon-host-relocation.test.ts @@ -4,16 +4,12 @@ import { mkdirSync, mkdtempSync, readFileSync, - readdirSync, - realpathSync, - renameSync, rmSync, utimesSync, writeFileSync } from 'node:fs' import os from 'node:os' -import { createRequire } from 'node:module' -import { basename, dirname, join } from 'node:path' +import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { setAppEnvironment, type AppEnvironment } from '../../shared/app-environment' @@ -39,11 +35,8 @@ function installHostApp(): void { } as AppEnvironment) } -import { buildDaemonHostManifest } from './daemon-host-manifest' import { collectPinnedDaemonVersions, - getRelocatedDaemonHost, - materializeRelocatedDaemonHost, pruneOldDaemonHosts, reclaimUnownedDaemonHostDir } from './daemon-host-relocation' @@ -62,67 +55,6 @@ function setProcessProp(key: string, value: unknown): void { Object.defineProperty(process, key, { value, configurable: true, writable: true }) } -// Build a win-unpacked fixture: exe + blobs + DLLs at root, daemon bundle and -// node-pty under resources, mirroring the packaged layout the copy expects. -function buildInstallFixture(root: string): void { - mkdirSync(root, { recursive: true }) - writeFileSync(join(root, 'Orca.exe'), 'exe-bytes') - for (const name of ['icudtl.dat', 'snapshot_blob.bin', 'v8_context_snapshot.bin']) { - writeFileSync(join(root, name), name) - } - writeFileSync(join(root, 'ffmpeg.dll'), 'dll') - writeFileSync(join(root, 'libEGL.dll'), 'dll') - const mainDir = join(root, 'resources', 'app.asar.unpacked', 'out', 'main') - mkdirSync(join(mainDir, 'chunks'), { recursive: true }) - writeFileSync(join(mainDir, 'daemon-entry.js'), 'entry') - writeFileSync(join(mainDir, 'chunks', 'a.js'), 'chunk') - writeFileSync(join(root, 'resources', 'app.asar.unpacked', 'out', 'package.json'), '{}') - const nativeDir = join(root, 'resources', 'node_modules', 'node-pty', 'build', 'Release') - mkdirSync(nativeDir, { recursive: true }) - writeFileSync(join(nativeDir, 'conpty.node'), 'native') - writeFileSync(join(nativeDir, 'conpty.pdb'), 'debug-symbols') - mkdirSync(join(nativeDir, 'conpty'), { recursive: true }) - writeFileSync(join(nativeDir, 'conpty', 'conpty.dll'), 'conpty-dll') - // Both win32 prebuilds exist in the packaged tree (build-time prune keeps the - // `win32-` prefix); the copy filter keeps the host arch's and drops the other. - const prebuildsRoot = join(root, 'resources', 'node_modules', 'node-pty', 'prebuilds') - for (const arch of ['win32-x64', 'win32-arm64']) { - mkdirSync(join(prebuildsRoot, arch), { recursive: true }) - writeFileSync(join(prebuildsRoot, arch, 'pty.node'), `${arch}-prebuild`) - } - const processTreeDir = join(root, 'resources', 'node_modules', '@vscode', 'windows-process-tree') - mkdirSync(join(processTreeDir, 'build', 'Release'), { recursive: true }) - mkdirSync(join(processTreeDir, 'lib'), { recursive: true }) - mkdirSync(join(processTreeDir, 'src'), { recursive: true }) - writeFileSync(join(processTreeDir, 'package.json'), '{"main":"lib/index.js"}') - writeFileSync(join(processTreeDir, 'lib', 'index.js'), 'module.exports = {}') - writeFileSync( - join(processTreeDir, 'build', 'Release', 'windows_process_tree.node'), - 'process-tree-native' - ) - writeFileSync(join(processTreeDir, 'build', 'Release', 'windows_process_tree.pdb'), 'symbols') - writeFileSync(join(processTreeDir, 'src', 'process.cc'), 'source') - writeFileSync(join(processTreeDir, 'lib', 'promises.js'), 'require("./index")') - mkdirSync(join(processTreeDir, 'build', 'Release', 'obj'), { recursive: true }) - writeFileSync(join(processTreeDir, 'build', 'Release', 'obj', 'addon.obj'), 'intermediate') -} - -const PROCESS_TREE_DIR_REL = join('resources', 'node_modules', '@vscode', 'windows-process-tree') - -const PROCESS_TREE_ADDON_REL = join( - 'resources', - 'node_modules', - '@vscode', - 'windows-process-tree', - 'build', - 'Release', - 'windows_process_tree.node' -) - -// The win32 prebuild dir the running host arch loads vs. the one that is pruned. -const HOST_PREBUILD = `win32-${process.arch}` -const OTHER_PREBUILD = HOST_PREBUILD === 'win32-arm64' ? 'win32-x64' : 'win32-arm64' - beforeEach(() => { tempDir = mkdtempSync(join(os.tmpdir(), 'daemon-host-relocation-')) installDir = join(tempDir, 'app') @@ -131,7 +63,6 @@ beforeEach(() => { localAppDataDir = join(tempDir, 'localAppData') mkdirSync(localAppDataDir, { recursive: true }) process.env.LOCALAPPDATA = localAppDataDir - buildInstallFixture(installDir) hostApp.isPackaged = true hostApp.userDataPath = userDataDir hostApp.appPath = join(installDir, 'resources', 'app.asar') @@ -160,271 +91,6 @@ afterEach(() => { } }) -describe('buildDaemonHostManifest', () => { - it('mirrors the win-unpacked layout: exe + data blobs + resources tree, no GPU DLLs', () => { - const appDir = 'C:\\app' - const ops = buildDaemonHostManifest({ - appDir, - execPath: 'C:\\app\\Orca.exe', - resourcesPath: 'C:\\app\\resources', - entrySourcePath: 'C:\\app\\resources\\app.asar.unpacked\\out\\main\\daemon-entry.js', - entryRelPath: 'resources/app.asar.unpacked/out/main/daemon-entry.js', - windowsProcessTreeDir: 'C:\\app\\resources\\node_modules\\@vscode\\windows-process-tree' - }) - const byDest = new Map(ops.map((op) => [op.destRel, op])) - // The host exe keeps the source basename: a verbatim, signature-preserving copy with no - // image-name mismatch. What escapes the updater's sweep is the path, not the name. - expect(byDest.get('Orca.exe')?.kind).toBe('file') - const exeOp = ops.find((op) => op.sourcePath === 'C:\\app\\Orca.exe') - expect(exeOp?.destRel).toBe('Orca.exe') - // Without this op the relocated daemon cannot resolve the native process - // table and falls back to a powershell.exe scan per snapshot (#16905). - expect(byDest.get('resources/node_modules/@vscode/windows-process-tree')?.kind).toBe('dir') - // V8/ICU data blobs are read by the Electron bootstrap and kept. - expect(byDest.has('icudtl.dat')).toBe(true) - // GPU/graphics DLLs are never loaded by the windowless host, so not copied. - expect(byDest.has('ffmpeg.dll')).toBe(false) - expect(byDest.has('libEGL.dll')).toBe(false) - // Daemon bundle + node-pty mirrored at their real resources-relative paths. - expect(byDest.get('resources/app.asar.unpacked/out/main/daemon-entry.js')?.kind).toBe('file') - expect(byDest.get('resources/app.asar.unpacked/out/main/chunks')?.kind).toBe('dir') - // node-pty is copied with a filter dropping .pdb + non-host-arch prebuilds. - const nodePtyOp = byDest.get('resources/node_modules/node-pty') - expect(nodePtyOp?.kind).toBe('dir') - expect(nodePtyOp?.filter?.('node-pty/build/Release/conpty.node')).toBe(true) - expect(nodePtyOp?.filter?.('node-pty/build/Release/conpty.pdb')).toBe(false) - expect(nodePtyOp?.filter?.(`node-pty/prebuilds/${HOST_PREBUILD}/pty.node`)).toBe(true) - expect(nodePtyOp?.filter?.(`node-pty/prebuilds/${OTHER_PREBUILD}/pty.node`)).toBe(false) - }) -}) - -describe('materializeRelocatedDaemonHost', () => { - it('copies the tree, writes the marker, and returns mirrored fork paths', () => { - const result = materializeRelocatedDaemonHost() - expect(result).not.toBeNull() - const dest = join(localAppDataDir, 'Orca', 'daemon-host', '9.9.9') - expect(result?.execPath).toBe(join(dest, 'Orca.exe')) - expect(result?.entryPath).toBe( - join(dest, 'resources', 'app.asar.unpacked', 'out', 'main', 'daemon-entry.js') - ) - expect(existsSync(result!.execPath)).toBe(true) - expect(existsSync(result!.entryPath)).toBe(true) - // node-pty native + conpty runtime copied at the require-resolvable path. - expect( - existsSync( - join(dest, 'resources', 'node_modules', 'node-pty', 'build', 'Release', 'conpty.node') - ) - ).toBe(true) - expect( - existsSync(join(dest, 'resources', 'app.asar.unpacked', 'out', 'main', 'chunks', 'a.js')) - ).toBe(true) - // Trim: GPU DLLs, .pdb debug symbols, and non-host-arch prebuilds excluded; - // the host arch's prebuild is retained so node-pty resolves its native addon. - expect(existsSync(join(dest, 'ffmpeg.dll'))).toBe(false) - expect(existsSync(join(dest, 'libEGL.dll'))).toBe(false) - expect( - existsSync( - join(dest, 'resources', 'node_modules', 'node-pty', 'build', 'Release', 'conpty.pdb') - ) - ).toBe(false) - const prebuildsDest = join(dest, 'resources', 'node_modules', 'node-pty', 'prebuilds') - expect(existsSync(join(prebuildsDest, HOST_PREBUILD, 'pty.node'))).toBe(true) - expect(existsSync(join(prebuildsDest, OTHER_PREBUILD, 'pty.node'))).toBe(false) - // Marker records the version + entry rel path, written into the published dir. - const marker = JSON.parse(readFileSync(join(dest, '.materialized.json'), 'utf8')) - expect(marker.version).toBe('9.9.9') - expect(marker.entryRelPath).toBe('resources/app.asar.unpacked/out/main/daemon-entry.js') - // The whole package is mirrored, so require() finds every hop it walks. - const processTreeDest = join( - dest, - 'resources', - 'node_modules', - '@vscode', - 'windows-process-tree' - ) - expect(existsSync(join(processTreeDest, 'build', 'Release', 'windows_process_tree.node'))).toBe( - true - ) - expect(existsSync(join(processTreeDest, 'lib', 'index.js'))).toBe(true) - expect(existsSync(join(processTreeDest, 'lib', 'promises.js'))).toBe(true) - // Build intermediates are ~25MB of the installed package; the loader never reads them. - expect(existsSync(join(processTreeDest, 'build', 'Release', 'windows_process_tree.pdb'))).toBe( - false - ) - expect(existsSync(join(processTreeDest, 'build', 'Release', 'obj'))).toBe(false) - expect(existsSync(join(processTreeDest, 'src'))).toBe(false) - // The loader requires the BARE package, so resolution runs through the copied - // package.json's `main`. Asserting the .node subpath instead would still pass - // with package.json dropped from the filter, while the daemon's own require - // failed and it silently went back to the CIM scan. - expect( - createRequire( - join(dest, 'resources', 'app.asar.unpacked', 'out', 'main', 'chunks', 'a.js') - ).resolve('@vscode/windows-process-tree') - ).toBe(realpathSync(join(processTreeDest, 'lib', 'index.js'))) - }) - - it('copies the exe verbatim: same file name and same bytes as the install-dir exe', () => { - const result = materializeRelocatedDaemonHost() - const sourceExe = join(installDir, 'Orca.exe') - // Byte-for-byte under the same name is what preserves the Authenticode signature and leaves - // no renamed-image signal for endpoint detection to read as masquerading. - expect(basename(result!.execPath)).toBe(basename(sourceExe)) - expect(readFileSync(result!.execPath)).toEqual(readFileSync(sourceExe)) - }) - - it('tracks a differently-named app exe rather than pinning an image name of its own', () => { - // A dev-channel or rebranded build ships a different executableName; the host copy must follow - // it, which is what keeps the copy verbatim instead of reintroducing a name mismatch. - renameSync(join(installDir, 'Orca.exe'), join(installDir, 'Orca Nightly.exe')) - setProcessProp('execPath', join(installDir, 'Orca Nightly.exe')) - const result = materializeRelocatedDaemonHost() - const dest = join(localAppDataDir, 'Orca', 'daemon-host', '9.9.9') - expect(result?.execPath).toBe(join(dest, 'Orca Nightly.exe')) - expect(existsSync(join(dest, 'orca-terminal-daemon.exe'))).toBe(false) - // Re-resolution must agree with materialization or the fork would target a missing exe. - expect(getRelocatedDaemonHost()?.execPath).toBe(join(dest, 'Orca Nightly.exe')) - }) - - it.each([ - ['the binary', PROCESS_TREE_ADDON_REL], - ['package.json', join(PROCESS_TREE_DIR_REL, 'package.json')], - ['lib/index.js', join(PROCESS_TREE_DIR_REL, 'lib', 'index.js')] - ])('refuses a mirror that lost %s', (_label, relativePath) => { - // Any one of them missing means require() cannot reach the addon, and a host - // that cannot load it runs anyway -- forking a shell per snapshot (#16905). - materializeRelocatedDaemonHost() - const dest = join(localAppDataDir, 'Orca', 'daemon-host', '9.9.9') - expect(getRelocatedDaemonHost()).not.toBeNull() - - rmSync(join(dest, relativePath)) - - expect(getRelocatedDaemonHost()).toBeNull() - }) - - it('rematerializes a host whose copied addon went missing', () => { - materializeRelocatedDaemonHost() - const dest = join(localAppDataDir, 'Orca', 'daemon-host', '9.9.9') - const relocatedAddon = join(dest, PROCESS_TREE_ADDON_REL) - - rmSync(relocatedAddon) - // A sentinel proves the host was rebuilt rather than reused. - const sentinel = join(dest, 'sentinel.txt') - writeFileSync(sentinel, 'remove') - - expect(materializeRelocatedDaemonHost()).not.toBeNull() - expect(readFileSync(relocatedAddon, 'utf8')).toBe('process-tree-native') - expect(existsSync(sentinel)).toBe(false) - }) - - it('does not read the install dir to decide an existing host is still good', () => { - // Relocation exists to outlive the install dir, so validity cannot depend on - // it: an updater mid-copy would otherwise condemn an intact host. A real - // upgrade changes the version keying this directory, which already forces a - // rebuild, and nothing else in the mirror is source-verified either. - materializeRelocatedDaemonHost() - const dest = join(localAppDataDir, 'Orca', 'daemon-host', '9.9.9') - const sentinel = join(dest, 'sentinel.txt') - writeFileSync(sentinel, 'keep') - - rmSync(join(installDir, 'resources', 'node_modules', '@vscode'), { - recursive: true, - force: true - }) - - expect(getRelocatedDaemonHost()).not.toBeNull() - expect(existsSync(sentinel)).toBe(true) - }) - - it.each([ - ['the binary', PROCESS_TREE_ADDON_REL], - ['package.json', join(PROCESS_TREE_DIR_REL, 'package.json')], - ['lib/index.js', join(PROCESS_TREE_DIR_REL, 'lib', 'index.js')] - ])('refuses to copy anything when the install lost %s', (_label, relativePath) => { - // Each of these would be accepted by the copy plan and then refused by the - // mirror check, which is a ~260MB copy per launch to reach a verdict the - // source could have given for free. Sharing one list is what prevents that. - rmSync(join(installDir, relativePath)) - - expect(materializeRelocatedDaemonHost()).toBeNull() - // Not even the host root: the source is checked before any directory is made. - expect(existsSync(join(localAppDataDir, 'Orca', 'daemon-host'))).toBe(false) - }) - - it('is idempotent: a valid marker short-circuits without recopying', () => { - materializeRelocatedDaemonHost() - const dest = join(localAppDataDir, 'Orca', 'daemon-host', '9.9.9') - // A recopy would rm the dest; a sentinel inside it must survive the 2nd call. - const sentinel = join(dest, 'sentinel.txt') - writeFileSync(sentinel, 'keep') - const result = materializeRelocatedDaemonHost() - expect(result?.execPath).toBe(join(dest, 'Orca.exe')) - expect(existsSync(sentinel)).toBe(true) - }) - - it('fails open on a missing required input, leaving no dest or staging dir', () => { - rmSync(join(installDir, 'resources', 'node_modules', 'node-pty'), { - recursive: true, - force: true - }) - const result = materializeRelocatedDaemonHost() - expect(result).toBeNull() - const hostRoot = join(localAppDataDir, 'Orca', 'daemon-host') - // Neither the published dest nor any leftover staging dir remains. - const remaining = existsSync(hostRoot) ? readdirSync(hostRoot) : [] - expect(remaining).toEqual([]) - }) - - it('returns null off win32', () => { - setProcessProp('platform', 'darwin') - expect(materializeRelocatedDaemonHost()).toBeNull() - expect(existsSync(join(localAppDataDir, 'Orca', 'daemon-host'))).toBe(false) - }) - - it('does nothing for a packaged host with no asar root (orcad on win32)', () => { - // orcad answers isPackaged() true — it is a shipped build — but it is plain Node: no - // asar, no resourcesPath, and no NSIS updater to escape. Relocation staging a copy of - // an Electron tree that is not there is the isPackaged-honesty defect, and it would - // silently produce a null host on a path whose failures are meant to be visible. - hostApp.appPath = join(installDir, 'resources', 'app') - installHostApp() - expect(materializeRelocatedDaemonHost()).toBeNull() - expect(getRelocatedDaemonHost()).toBeNull() - expect(existsSync(join(localAppDataDir, 'Orca', 'daemon-host'))).toBe(false) - }) -}) - -describe('getRelocatedDaemonHost', () => { - it('returns null when the marker version does not match the current version', () => { - const dest = join(localAppDataDir, 'Orca', 'daemon-host', '9.9.9') - mkdirSync(dirname(join(dest, 'x')), { recursive: true }) - writeFileSync(join(dest, 'Orca.exe'), 'exe') - mkdirSync(join(dest, 'resources', 'app.asar.unpacked', 'out', 'main'), { recursive: true }) - writeFileSync( - join(dest, 'resources', 'app.asar.unpacked', 'out', 'main', 'daemon-entry.js'), - 'e' - ) - // The process-table files too, or this passes because the mirror has no addon - // and never exercises the version comparison it is named for. - mkdirSync(join(dest, PROCESS_TREE_DIR_REL, 'lib'), { recursive: true }) - mkdirSync(dirname(join(dest, PROCESS_TREE_ADDON_REL)), { recursive: true }) - writeFileSync(join(dest, PROCESS_TREE_DIR_REL, 'package.json'), '{}') - writeFileSync(join(dest, PROCESS_TREE_DIR_REL, 'lib', 'index.js'), 'm') - writeFileSync(join(dest, PROCESS_TREE_ADDON_REL), 'n') - writeFileSync( - join(dest, '.materialized.json'), - JSON.stringify({ - version: '8.8.8', - completedAt: '', - entryRelPath: 'resources/app.asar.unpacked/out/main/daemon-entry.js' - }) - ) - expect(getRelocatedDaemonHost()).toBeNull() - }) -}) - -// Why: quarantine refuses records written in the last minute, because an in-flight publish is -// indistinguishable from a torn one. Age a record so it stands for a settled corrupt record. function ageRecordPastQuarantineFloor(recordPath: string): void { const aged = new Date(Date.now() - 5 * 60_000) utimesSync(recordPath, aged, aged) diff --git a/src/main/daemon/daemon-host-relocation.ts b/src/main/daemon/daemon-host-relocation.ts index 51fed83530af..9eb1372cef46 100644 --- a/src/main/daemon/daemon-host-relocation.ts +++ b/src/main/daemon/daemon-host-relocation.ts @@ -1,79 +1,15 @@ -import { randomBytes } from 'node:crypto' -import { - existsSync, - mkdirSync, - readFileSync, - readdirSync, - renameSync, - rmSync, - writeFileSync -} from 'node:fs' -import { join, win32 as winPath } from 'node:path' +import { readFileSync, readdirSync, rmSync } from 'node:fs' +import { join } from 'node:path' import { getAppEnvironment } from '../../shared/app-environment' -import { - buildDaemonHostManifest, - daemonHostExeName, - destPath, - executeManifest, - toPosixRelative, - WINDOWS_PROCESS_TREE_REQUIRED, - type DaemonHostSources -} from './daemon-host-manifest' import type { ProcessLivenessVerdict } from './daemon-incarnation-evidence-types' import { parseDaemonPidFile } from './daemon-pid-file-parse' import { quarantineCorruptDaemonPidRecord } from './daemon-pid-record-quarantine' import { inspectProcessLiveness, mergeProcessLivenessVerdict } from './daemon-process-inspection' -/** - * Relocate the terminal daemon's process image out of the app install dir into LOCAL userData so it - * survives Windows auto-updates: the NSIS installer deletes the old install and force-kills every process - * imaged under it, which would otherwise kill the daemon and its live terminals. The relocated exe is a - * run-as-node Orca.exe copy (not node.exe) so there's no console flash and asar still resolves. Fail-open: - * any failure returns null and the caller forks the install-dir host (pre-relocation behavior). - * - * What escapes the updater is the PATH, not the file name: electron-builder's kill sweep selects - * processes whose image path sits under $INSTDIR. See docs/reference/windows-daemon-host-relocation.md - * for the survival contract and why the exe is copied verbatim rather than renamed. - */ - -export type RelocatedDaemonHost = { - /** The relocated host exe to fork the daemon from (run as node). */ - execPath: string - /** The copied daemon-entry.js, mirrored under the relocated resources tree. */ - entryPath: string -} - +// Legacy Electron hosts remain protected until their original owners have exited. const HOST_SUBDIR = 'daemon-host' -const MARKER_NAME = '.materialized.json' - -// LOCAL appData (not roaming) so OneDrive/roaming never syncs this ~260MB runtime. Shared with NSIS uninstall (config/nsis/orca-installer-hooks.nsh) — keep in sync. const LOCAL_HOST_ROOT_NAME = 'Orca' -type MaterializeMarker = { - version: string - completedAt: string - entryRelPath: string -} - -// Mirror getDaemonEntryPath()'s resolution order so the copied entry is the exact file the in-dir fork would run. -function resolveEntrySourcePath(resourcesPath: string): string { - const unpackedRoot = join(resourcesPath, 'app.asar.unpacked') - const direct = join(unpackedRoot, 'daemon-entry.js') - if (existsSync(direct)) { - return direct - } - return join(unpackedRoot, 'out', 'main', 'daemon-entry.js') -} - -/** - * Whether this process is a packaged ELECTRON app on win32 — the only shape relocation - * addresses, because what it escapes is the NSIS updater's kill zone. - * - * Why asar and not isPackaged alone: orcad answers isPackaged() true (it is a shipped build, - * not a dev checkout) while having no asar, no resourcesPath and no NSIS installer. Asking - * whether the app root is an asar archive is the same honesty fix the watcher path uses, and - * it keeps a Node host from staging a copy of an Electron tree it does not have. - */ function isPackagedElectronWin32(): boolean { const environment = getAppEnvironment() return ( @@ -83,57 +19,6 @@ function isPackagedElectronWin32(): boolean { ) } -// Relocation inputs from the live packaged process, or null when it doesn't apply (non-win32, dev, or missing resourcesPath). -function collectDaemonHostSources(): DaemonHostSources | null { - if (!isPackagedElectronWin32()) { - return null - } - const resourcesPath = process.resourcesPath - if (typeof resourcesPath !== 'string' || resourcesPath.length === 0) { - return null - } - const execPath = process.execPath - const appDir = winPath.dirname(execPath) - const entrySourcePath = resolveEntrySourcePath(resourcesPath) - return { - appDir, - execPath, - resourcesPath, - entrySourcePath, - entryRelPath: toPosixRelative(appDir, entrySourcePath), - windowsProcessTreeDir: join(resourcesPath, 'node_modules', '@vscode', 'windows-process-tree') - } -} - -function readMarker(dir: string): MaterializeMarker | null { - try { - const parsed = JSON.parse( - readFileSync(join(dir, MARKER_NAME), 'utf8') - ) as Partial - if (typeof parsed.version === 'string' && typeof parsed.entryRelPath === 'string') { - return { - version: parsed.version, - completedAt: typeof parsed.completedAt === 'string' ? parsed.completedAt : '', - entryRelPath: parsed.entryRelPath - } - } - } catch { - // Missing/corrupt marker — treat as not materialized. - } - return null -} - -function processTreeRelDir(sources: DaemonHostSources): string { - return toPosixRelative(sources.appDir, sources.windowsProcessTreeDir) -} - -/** True when any file require() needs is absent from a copy of the package. */ -function missingProcessTreeFiles(packageDir: string): boolean { - return WINDOWS_PROCESS_TREE_REQUIRED.some( - (relative) => !existsSync(join(packageDir, ...relative.split('/'))) - ) -} - function hostRootDir(): string { // Prefer LOCAL appData (see LOCAL_HOST_ROOT_NAME); fall back to userData only if LOCALAPPDATA is unset. const localAppData = process.env.LOCALAPPDATA @@ -144,86 +29,6 @@ function hostRootDir(): string { return join(base, HOST_SUBDIR) } -/** - * The relocated host for the current version, or null. Valid only when the marker matches this version - * AND the exe + entry exist, so a partial or stale copy never reports ready. - */ -export function getRelocatedDaemonHost(): RelocatedDaemonHost | null { - const sources = collectDaemonHostSources() - if (!sources) { - return null - } - const version = getAppEnvironment().getVersion() - const dest = join(hostRootDir(), version) - const marker = readMarker(dest) - if (!marker || marker.version !== version) { - return null - } - const execPath = join(dest, daemonHostExeName(sources.execPath)) - const entryPath = destPath(dest, marker.entryRelPath) - if (!existsSync(execPath) || !existsSync(entryPath)) { - return null - } - // A mirror the daemon cannot load the addon from still runs -- it just forks a - // shell per snapshot (#16905) -- so treat it as unmaterialized and rebuild. Hosts - // from before this shipped have none of these files. Checked in the mirror, never - // in the install dir, which is the thing relocation exists to outlive. - if (missingProcessTreeFiles(destPath(dest, processTreeRelDir(sources)))) { - return null - } - return { execPath, entryPath } -} - -/** - * Materialize the current version's daemon host, returning its fork paths or null (fail-open). Idempotent - * via marker; stages into a temp sibling and publishes by atomic rename, so a crash mid-copy never leaves a half-populated dest. - */ -export function materializeRelocatedDaemonHost(): RelocatedDaemonHost | null { - const existing = getRelocatedDaemonHost() - if (existing) { - return existing - } - const sources = collectDaemonHostSources() - if (!sources) { - return null - } - // Checked against the source before copying: the mirror check below would refuse - // the result anyway, and re-copying ~260MB on every launch to reach that verdict - // is the loop this shares its list with the copy plan to prevent. - if (missingProcessTreeFiles(sources.windowsProcessTreeDir)) { - return null - } - const version = getAppEnvironment().getVersion() - const root = hostRootDir() - const dest = join(root, version) - const staging = join(root, `${version}.staging-${randomBytes(6).toString('hex')}`) - try { - mkdirSync(root, { recursive: true }) - rmSync(staging, { recursive: true, force: true }) - executeManifest(buildDaemonHostManifest(sources), staging) - // Marker written LAST so an interrupted copy leaves a marker-less staging dir the next launch discards. - const marker: MaterializeMarker = { - version, - completedAt: new Date().toISOString(), - entryRelPath: sources.entryRelPath - } - writeFileSync(join(staging, MARKER_NAME), JSON.stringify(marker)) - // Replace any stale/partial dest, then publish atomically. Windows refuses to delete a running - // image, so a live daemon already hosted in THIS version's dir (same-version reinstall, or a dev - // channel reusing a version) throws here and materialization fails open to the install-dir host. - rmSync(dest, { recursive: true, force: true }) - renameSync(staging, dest) - } catch { - try { - rmSync(staging, { recursive: true, force: true }) - } catch { - // Best-effort staging cleanup. - } - return null - } - return getRelocatedDaemonHost() -} - export type PinnedDaemonVersionsEvidence = | { status: 'complete'; versionLiveness: ReadonlyMap } | { status: 'unverifiable'; reason: string } diff --git a/src/main/daemon/daemon-init-dependency-mocks.ts b/src/main/daemon/daemon-init-dependency-mocks.ts index d7217d4df635..14d843d0fc7b 100644 --- a/src/main/daemon/daemon-init-dependency-mocks.ts +++ b/src/main/daemon/daemon-init-dependency-mocks.ts @@ -122,6 +122,12 @@ export function createDaemonInitModuleFactories(state: DaemonInitMockState) { readonly onData: Mock readonly onExit: Mock readonly callOrder: string[] + get freshSpawnsUnavailable(): boolean { + return this.options.freshSpawnAdmission?.unavailable ?? false + } + recoverFreshSpawnRouting(force = false): Promise { + return this.options.freshSpawnAdmission?.recover(force) ?? Promise.resolve(true) + } constructor(opts: MockAdapter['options']) { this.protocolVersion = opts.protocolVersion ?? PROTOCOL_VERSION this.options = opts diff --git a/src/main/daemon/daemon-init-endpoint-adoption.test.ts b/src/main/daemon/daemon-init-endpoint-adoption.test.ts index 2222cec7e361..92cdc5c118a1 100644 --- a/src/main/daemon/daemon-init-endpoint-adoption.test.ts +++ b/src/main/daemon/daemon-init-endpoint-adoption.test.ts @@ -123,7 +123,7 @@ describe('daemon-init: runRestartDaemon (7-step sequence)', () => { it('adopts the winner when a launched daemon loses the endpoint race', async () => { // Why: losing the publish race is an expected outcome, not a crash. Reporting it as a - // startup failure strands this app on local non-persistent PTYs beside a healthy daemon. + // startup failure blocks new terminals beside a healthy daemon. const mod = await importFresh() await mod.initDaemonPtyProvider(undefined, { macosLoginSessionWatch: true }) @@ -458,7 +458,7 @@ describe('daemon-init: runRestartDaemon (7-step sequence)', () => { try { const handle = await launcher('/fake/socket', '/fake/token', '/fake/daemon.pid', 'launch-new') - expect(handle.mode).toBe('degraded-new-pty-fallback') + expect(handle.mode).toBe('fresh-spawns-unavailable') expect(forkMock.mock.calls.length).toBe(forkCallsBefore) handle.releaseAdoptionLease?.() } finally { @@ -563,7 +563,7 @@ describe('daemon-init: runRestartDaemon (7-step sequence)', () => { try { const handle = await launcher('/fake/socket', '/fake/token', '/fake/daemon.pid', 'launch-new') - expect(handle.mode).toBe('degraded-new-pty-fallback') + expect(handle.mode).toBe('fresh-spawns-unavailable') handle.releaseAdoptionLease?.() } finally { warn.mockRestore() diff --git a/src/main/daemon/daemon-init-live-session-preservation.test.ts b/src/main/daemon/daemon-init-live-session-preservation.test.ts index d8f988112cb8..4413e0d85903 100644 --- a/src/main/daemon/daemon-init-live-session-preservation.test.ts +++ b/src/main/daemon/daemon-init-live-session-preservation.test.ts @@ -76,6 +76,7 @@ describe('daemon-init: runRestartDaemon (7-step sequence)', () => { } }) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The spawner fixture captures the launcher passed by initDaemonPtyProvider. const launcher = spawnerInstances[0].launcher as ( socketPath: string, tokenPath: string @@ -117,6 +118,7 @@ describe('daemon-init: runRestartDaemon (7-step sequence)', () => { } }) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The spawner fixture captures the launcher passed by initDaemonPtyProvider. const launcher = spawnerInstances[0].launcher as ( socketPath: string, tokenPath: string @@ -135,6 +137,7 @@ describe('daemon-init: runRestartDaemon (7-step sequence)', () => { const mod = await importFresh() await mod.initDaemonPtyProvider() + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The spawner fixture captures the launcher passed by initDaemonPtyProvider. const launcher = spawnerInstances[0].launcher as ( socketPath: string, tokenPath: string @@ -216,6 +219,7 @@ describe('daemon-init: runRestartDaemon (7-step sequence)', () => { } }) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The spawner fixture captures the launcher passed by initDaemonPtyProvider. const launcher = spawnerInstances[0].launcher as ( socketPath: string, tokenPath: string @@ -255,6 +259,7 @@ describe('daemon-init: runRestartDaemon (7-step sequence)', () => { } }) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The spawner fixture captures the launcher passed by initDaemonPtyProvider. const launcher = spawnerInstances[0].launcher as ( socketPath: string, tokenPath: string @@ -292,6 +297,7 @@ describe('daemon-init: runRestartDaemon (7-step sequence)', () => { } }) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The spawner fixture captures the launcher passed by initDaemonPtyProvider. const launcher = spawnerInstances[0].launcher as ( socketPath: string, tokenPath: string @@ -328,11 +334,12 @@ describe('daemon-init: runRestartDaemon (7-step sequence)', () => { } }) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The spawner fixture captures the launcher passed by initDaemonPtyProvider. const launcher = spawnerInstances[0].launcher as ( socketPath: string, tokenPath: string ) => Promise<{ - mode?: 'degraded-new-pty-fallback' + mode?: 'fresh-spawns-unavailable' shutdown(): Promise }> checkDaemonHealthMock.mockResolvedValueOnce('pty-spawn-unhealthy') @@ -340,7 +347,7 @@ describe('daemon-init: runRestartDaemon (7-step sequence)', () => { const handle = await launcher('/fake/socket', '/fake/token') expect(requestMock).toHaveBeenCalledWith('listSessions', undefined, expect.any(Number)) - expect(handle.mode).toBe('degraded-new-pty-fallback') + expect(handle.mode).toBe('fresh-spawns-unavailable') expect(killStaleDaemonMock).not.toHaveBeenCalled() expect(forkMock).not.toHaveBeenCalled() }) @@ -362,6 +369,7 @@ describe('daemon-init: runRestartDaemon (7-step sequence)', () => { } }) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The spawner fixture captures the launcher passed by initDaemonPtyProvider. const launcher = spawnerInstances[0].launcher as ( socketPath: string, tokenPath: string @@ -399,6 +407,7 @@ describe('daemon-init: runRestartDaemon (7-step sequence)', () => { const mod = await importFresh() await mod.initDaemonPtyProvider() + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The spawner fixture captures the launcher passed by initDaemonPtyProvider. const launcher = spawnerInstances[0].launcher as ( socketPath: string, tokenPath: string diff --git a/src/main/daemon/daemon-init-mock-types.ts b/src/main/daemon/daemon-init-mock-types.ts index 341fcd26df7b..08ee0103c924 100644 --- a/src/main/daemon/daemon-init-mock-types.ts +++ b/src/main/daemon/daemon-init-mock-types.ts @@ -1,3 +1,4 @@ +import type { DaemonFreshSpawnAdmission } from './daemon-fresh-spawn-admission' import type { Mock } from 'vitest' /** Fake DaemonSpawner instance every mocked `new DaemonSpawner()` records. */ @@ -16,6 +17,7 @@ export type MockAdapter = { options: { socketPath: string tokenPath: string + freshSpawnAdmission?: DaemonFreshSpawnAdmission historyPath?: string packagedAppVersion?: string | null respawn?: ( @@ -46,7 +48,7 @@ export type MockAdapterConstructor = new (opts: MockAdapter['options']) => MockA /** Handle the fake spawner hands back from ensureRunning/getHandle. */ export type MockSpawnerHandle = { - mode?: 'degraded-new-pty-fallback' + mode?: 'fresh-spawns-unavailable' adopted?: true releaseAdoptionLease?: () => void shutdown: () => Promise @@ -66,9 +68,8 @@ export type LaunchedDaemonIdentity = { launchNonce: string } -/** Local (non-daemon) PTY provider the degraded/fallback paths install. */ +/** Local provider fixture for daemon initialization tests. */ export type MockLocalPtyProvider = { - routesFreshSpawnsToLocalProvider: undefined spawn: Mock<(opts: { sessionId?: string }) => Promise<{ id: string }>> attach: Mock<() => Promise> hasPty: Mock<() => boolean> @@ -95,7 +96,7 @@ export type MockLocalPtyProvider = { export type EnsureRunningOverride = () => Promise<{ socketPath: string tokenPath: string - mode?: 'degraded-new-pty-fallback' + mode?: 'fresh-spawns-unavailable' adopted?: true }> diff --git a/src/main/daemon/daemon-init-provider-installation.test.ts b/src/main/daemon/daemon-init-provider-installation.test.ts index 423ee9ee34f2..6a0aa772293f 100644 --- a/src/main/daemon/daemon-init-provider-installation.test.ts +++ b/src/main/daemon/daemon-init-provider-installation.test.ts @@ -145,7 +145,39 @@ describe('daemon-init: runRestartDaemon (7-step sequence)', () => { } }) - it('does not install a late daemon provider after startup fallback aborts the init attempt', async () => { + it('installs a healthy daemon that completes after the startup gate deadline', async () => { + const mod = await importFresh() + const { startFirstWindowStartupServices, LOCAL_PTY_STARTUP_FAIL_OPEN_TIMEOUT_MS } = + await import('../startup/first-window-startup-services') + let finish!: (value: { socketPath: string; tokenPath: string }) => void + ensureRunningOverrides.push( + () => + new Promise((resolve) => { + finish = resolve + }) + ) + vi.useFakeTimers() + try { + const services = startFirstWindowStartupServices({ + startDaemonPtyProvider: (signal) => mod.initDaemonPtyProvider(signal), + startAgentHookServer: async () => {}, + onDaemonError: vi.fn(), + onAgentHookServerError: vi.fn() + }) + await vi.advanceTimersByTimeAsync(LOCAL_PTY_STARTUP_FAIL_OPEN_TIMEOUT_MS) + await services.localPtyReady + expect(setLocalPtyProviderMock).not.toHaveBeenCalled() + finish({ socketPath: '/fake/socket-late', tokenPath: '/fake/token-late' }) + await vi.advanceTimersByTimeAsync(0) + expect(setLocalPtyProviderMock).toHaveBeenCalledOnce() + expect(rebindLocalProviderListenersMock).toHaveBeenCalledOnce() + expect(mod.getDaemonProvider()).not.toBeNull() + } finally { + vi.useRealTimers() + } + }) + + it('does not install a late daemon provider after explicit cancellation', async () => { const mod = await importFresh() let resolveEnsureRunning!: (value: { socketPath: string; tokenPath: string }) => void ensureRunningOverrides.push( @@ -273,29 +305,27 @@ describe('daemon-init: runRestartDaemon (7-step sequence)', () => { vi.restoreAllMocks() }) - it('routes fresh PTYs to the local fallback when a preserved daemon cannot spawn new PTYs', async () => { + it('refuses fresh PTYs when a preserved daemon cannot spawn new PTYs', async () => { const mod = await importFresh() ensureRunningOverrides.push(async () => ({ socketPath: '/fake/degraded-socket', tokenPath: '/fake/degraded-token', - mode: 'degraded-new-pty-fallback' + mode: 'fresh-spawns-unavailable' })) await mod.initDaemonPtyProvider() - const { DegradedDaemonPtyProvider } = await import('./degraded-daemon-pty-provider') + const { DaemonPtyRouter } = await import('./daemon-pty-router') const provider = mod.getDaemonProvider() - expect(provider).toBeInstanceOf(DegradedDaemonPtyProvider) - expect(getLocalPtyProviderMock).toHaveBeenCalledOnce() + expect(provider).toBeInstanceOf(DaemonPtyRouter) + expect(getLocalPtyProviderMock).not.toHaveBeenCalled() expect(setLocalPtyProviderMock).toHaveBeenCalledWith(provider) - const result = await provider!.spawn({ cols: 80, rows: 24 }) - - expect(result.id).toBe('local-fallback-pty') - expect(localFallbackProvider.spawn).toHaveBeenCalledWith({ - cols: 80, - rows: 24 - }) + checkDaemonHealthMock.mockResolvedValue('unhealthy') + await expect(provider!.spawn({ cols: 80, rows: 24 })).rejects.toThrow( + 'Terminal service unavailable' + ) + expect(localFallbackProvider.spawn).not.toHaveBeenCalled() expect(adapterInstances[0].listProcesses).toHaveBeenCalled() }) @@ -313,11 +343,11 @@ describe('daemon-init: runRestartDaemon (7-step sequence)', () => { ensureRunningOverrides.push(async () => ({ socketPath: '/fake/degraded-socket', tokenPath: '/fake/degraded-token', - mode: 'degraded-new-pty-fallback' + mode: 'fresh-spawns-unavailable' })) await degraded.initDaemonPtyProvider() - const { DegradedDaemonPtyProvider } = await import('./degraded-daemon-pty-provider') - expect(degraded.getDaemonProvider()).toBeInstanceOf(DegradedDaemonPtyProvider) + const { DaemonPtyRouter } = await import('./daemon-pty-router') + expect(degraded.getDaemonProvider()).toBeInstanceOf(DaemonPtyRouter) expect(degraded.daemonOwnsFreshPersistentPtys()).toBe(false) }) @@ -326,22 +356,39 @@ describe('daemon-init: runRestartDaemon (7-step sequence)', () => { ensureRunningOverrides.push(async () => ({ socketPath: '/fake/degraded-socket', tokenPath: '/fake/degraded-token', - mode: 'degraded-new-pty-fallback' + mode: 'fresh-spawns-unavailable' })) await mod.initDaemonPtyProvider() checkDaemonHealthMock.mockClear() - const { DegradedDaemonPtyProvider } = await import('./degraded-daemon-pty-provider') + const { DaemonPtyRouter } = await import('./daemon-pty-router') const provider = mod.getDaemonProvider() - expect(provider).toBeInstanceOf(DegradedDaemonPtyProvider) - const degradedProvider = provider as InstanceType + expect(provider).toBeInstanceOf(DaemonPtyRouter) + if (!(provider instanceof DaemonPtyRouter)) { + throw new Error('Expected daemon router') + } + const degradedProvider = provider await expect(degradedProvider.recoverFreshSpawnRouting()).resolves.toBe(true) expect(checkDaemonHealthMock).toHaveBeenCalledWith( '/fake/degraded-socket', '/fake/degraded-token' ) - expect(degradedProvider.routesFreshSpawnsToLocalProvider).toBeUndefined() + expect(degradedProvider.freshSpawnsUnavailable).toBe(false) + }) + + it('coalesces initial failure retries without stopping an existing service', async () => { + const mod = await importFresh() + ensureRunningOverrides.push(async () => { + throw new Error('runtime unavailable') + }) + await expect(mod.initDaemonPtyProvider()).rejects.toThrow('runtime unavailable') + await Promise.all([mod.retryDaemonPtyProvider(), mod.retryDaemonPtyProvider()]) + expect(spawnerInstances).toHaveLength(2) + expect(setLocalPtyProviderMock).toHaveBeenCalledOnce() + expect(mod.daemonOwnsFreshPersistentPtys()).toBe(true) + await mod.retryDaemonPtyProvider() + expect(spawnerInstances).toHaveLength(2) }) it('keeps legacy daemon pid/token files when the probe fails but the pid-file process is alive', async () => { diff --git a/src/main/daemon/daemon-init-restart-sequence.test.ts b/src/main/daemon/daemon-init-restart-sequence.test.ts index 71417db986c6..f92b827ecf30 100644 --- a/src/main/daemon/daemon-init-restart-sequence.test.ts +++ b/src/main/daemon/daemon-init-restart-sequence.test.ts @@ -94,7 +94,7 @@ describe('daemon-init: runRestartDaemon (7-step sequence)', () => { expect(adapterInstances[1].disconnectOnly).toHaveBeenCalledOnce() expect(spawnerInstances[0].shutdown).not.toHaveBeenCalled() expect(mod.getDaemonProvider()).toBe(originalProvider) - expect(unbindLocalProviderListenersMock).toHaveBeenCalledOnce() + expect(unbindLocalProviderListenersMock).not.toHaveBeenCalled() expect(rebindLocalProviderListenersMock).toHaveBeenCalledTimes(2) }) @@ -103,20 +103,23 @@ describe('daemon-init: runRestartDaemon (7-step sequence)', () => { ensureRunningOverrides.push(async () => ({ socketPath: '/fake/degraded-socket', tokenPath: '/fake/degraded-token', - mode: 'degraded-new-pty-fallback' + mode: 'fresh-spawns-unavailable' })) await mod.initDaemonPtyProvider() - const { DegradedDaemonPtyProvider } = await import('./degraded-daemon-pty-provider') + const { DaemonPtyRouter } = await import('./daemon-pty-router') const provider = mod.getDaemonProvider() - expect(provider).toBeInstanceOf(DegradedDaemonPtyProvider) - const degradedProvider = provider as InstanceType + expect(provider).toBeInstanceOf(DaemonPtyRouter) + if (!(provider instanceof DaemonPtyRouter)) { + throw new Error('Expected daemon router') + } + const degradedProvider = provider const originalAdapter = adapterInstances[0] originalAdapter.listProcesses.mockResolvedValueOnce([ { id: 'preserved-current-session', cwd: '/repo', title: 'shell' } ]) - await degradedProvider.discoverDaemonSessions() + await degradedProvider.discoverLegacySessions() const order: string[] = [] degradedProvider.onExit((payload) => { @@ -361,12 +364,12 @@ describe('daemon-init: runRestartDaemon (7-step sequence)', () => { await mod.restartDaemon() void originalEnsureRunning // keep ref so tslint doesn't complain - // Full 7-step order; Step 3 (cleanup) has no observable in the dead-socket branch, so it's pinned implicitly by resetHandle running after unbind. + // Keep subscriptions through replacement I/O; detach only immediately before the swap. expect(trace).toEqual([ 'fanout', - 'unbind', 'resetHandle', 'ensureRunning', + 'unbind', 'replaceProvider', 'rebind' ]) @@ -526,10 +529,9 @@ describe('daemon-init: runRestartDaemon (7-step sequence)', () => { expect(adapterInstances).toHaveLength(3) }) - it('throws when restartDaemon is called before initDaemonPtyProvider', async () => { + it('initializes when restartDaemon is called before initDaemonPtyProvider', async () => { const mod = await importFresh() - await expect(mod.restartDaemon()).rejects.toThrow( - 'restartDaemon called before initDaemonPtyProvider' - ) + await expect(mod.restartDaemon()).resolves.toEqual({ killedCount: 0 }) + expect(mod.getDaemonProvider()).not.toBeNull() }) }) diff --git a/src/main/daemon/daemon-init-test-harness.ts b/src/main/daemon/daemon-init-test-harness.ts index 6060ed9b759d..6db0f3822f10 100644 --- a/src/main/daemon/daemon-init-test-harness.ts +++ b/src/main/daemon/daemon-init-test-harness.ts @@ -125,7 +125,6 @@ function createDaemonInitMockState(): DaemonInitMockState { } = { current: null } const localFallbackProvider: MockLocalPtyProvider = { - routesFreshSpawnsToLocalProvider: undefined, spawn: vi.fn(async (opts: { sessionId?: string }) => ({ id: opts.sessionId ?? 'local-fallback-pty' })), diff --git a/src/main/daemon/daemon-init.ts b/src/main/daemon/daemon-init.ts index 918547d8b79d..466aa42840cc 100644 --- a/src/main/daemon/daemon-init.ts +++ b/src/main/daemon/daemon-init.ts @@ -1,5 +1,5 @@ export { createLegacyDaemonAdapters } from './daemon-legacy-adapters' -export { initDaemonPtyProvider } from './daemon-provider-init' +export { initDaemonPtyProvider, retryDaemonPtyProvider } from './daemon-provider-init' export { restartDaemon, type RestartDaemonResult } from './daemon-provider-restart' export { daemonOwnsFreshPersistentPtys, diff --git a/src/main/daemon/daemon-launch-paths.ts b/src/main/daemon/daemon-launch-paths.ts index 532bc75c574d..b05b1db71805 100644 --- a/src/main/daemon/daemon-launch-paths.ts +++ b/src/main/daemon/daemon-launch-paths.ts @@ -1,3 +1,4 @@ +import { desktopDaemonBundleDir } from './daemon-bun-runtime' import { existsSync } from 'node:fs' import { connect } from 'node:net' import { join } from 'node:path' @@ -27,6 +28,9 @@ export function getDaemonHistoryDir(): string { } export function getDaemonEntryPath(): string { + if (process.versions.electron) { + return join(desktopDaemonBundleDir(), 'daemon-entry.js') + } const appPath = getAppEnvironment().getAppPath() // Why: packaged getAppPath() points at app.asar, so redirect to app.asar.unpacked where daemon-entry.js is fork-executable. // Why asar and not isPackaged: orcad is a packaged non-Electron host whose bundle root holds diff --git a/src/main/daemon/daemon-launched-child-spawn.test.ts b/src/main/daemon/daemon-launched-child-spawn.test.ts index 1b636261fa11..fc48a92a238c 100644 --- a/src/main/daemon/daemon-launched-child-spawn.test.ts +++ b/src/main/daemon/daemon-launched-child-spawn.test.ts @@ -1,5 +1,4 @@ import { afterEach, describe, expect, it, vi } from 'vitest' -import { bunOwnedRuntimeArgs } from '../../shared/bun-owned-runtime-args' import { spawnDaemonChildProcess } from './daemon-launched-child-spawn' const { spawn, fork } = vi.hoisted(() => ({ spawn: vi.fn(), fork: vi.fn() })) @@ -23,8 +22,8 @@ const options = { afterEach(() => { vi.clearAllMocks() + vi.unstubAllGlobals() vi.unstubAllEnvs() - vi.restoreAllMocks() }) describe('daemon launch scope ownership', () => { @@ -55,46 +54,164 @@ describe('daemon launch scope ownership', () => { }) }) -describe('headless Bun daemon launch', () => { - function useBun(): void { - vi.spyOn(process, 'versions', 'get').mockReturnValue({ ...process.versions, bun: '1.4.2' }) - vi.stubEnv('NODE_OPTIONS', '--require=untrusted.js') - vi.stubEnv('NODE_PATH', '/untrusted') - vi.stubEnv('BUN_OPTIONS', '--preload=untrusted.js') - } - - it('isolates scoped Bun launches before the daemon entry', () => { - useBun() - spawnDaemonChildProcess(options, true) +it('launches the Bun payload directly without inherited Electron preload options', () => { + vi.stubEnv('NODE_OPTIONS', '--require=/user/preload.js') + vi.stubEnv('BUN_OPTIONS', '--preload=/user/preload.js') + try { + spawnDaemonChildProcess( + { + ...options, + bunRuntime: true, + relocatedExecPath: '/runtime/bun', + conptyLibraryPath: 'C:\\runtime\\conpty\\conpty.dll' + }, + false + ) + expect(fork).not.toHaveBeenCalled() const call = spawn.mock.calls[0][0] - const executableIndex = call.args.indexOf(process.execPath) - expect(call.args.slice(executableIndex + 1, executableIndex + 5)).toEqual([ - ...bunOwnedRuntimeArgs(), + expect(call.program).toBe('/runtime/bun') + expect(call.args.slice(0, 4)).toEqual([ + '--no-env-file', + process.platform === 'win32' ? '--config=NUL' : '--config=/dev/null', + '--no-install', options.forkEntryPath ]) - for (const key of ['ELECTRON_RUN_AS_NODE', 'NODE_OPTIONS', 'NODE_PATH', 'BUN_OPTIONS']) { - expect(call.env[key]).toBeUndefined() - } + expect(call.stdio).toContain('ipc') + expect(call.env.NODE_OPTIONS).toBeUndefined() + expect(call.env.NODE_PATH).toBeUndefined() + expect(call.env.BUN_OPTIONS).toBeUndefined() + expect(call.env.ELECTRON_RUN_AS_NODE).toBeUndefined() + expect(call.args).toContain('--spawner-exec-path') + } finally { + vi.unstubAllEnvs() + } +}) + +it('selects only the verified Windows provider instead of an inherited override', () => { + vi.stubGlobal('process', { + ...process, + platform: 'win32', + env: { ...process.env, BUN_CONPTY_LIBRARY: 'C:\\foreign\\conpty.dll' } }) + const library = 'C:\\Orca Runtime\\conpty\\conpty.dll' + spawnDaemonChildProcess( + { + ...options, + bunRuntime: true, + relocatedExecPath: 'C:\\Orca Runtime\\bun-runtime.exe', + conptyLibraryPath: library + }, + false + ) + expect(spawn.mock.calls[0][0].env.BUN_CONPTY_LIBRARY).toBe(library) +}) - it('scrubs direct Bun forks while preserving the selected ConPTY library', () => { - useBun() - vi.stubEnv('BUN_CONPTY_LIBRARY', '/verified/conpty.dll') - spawnDaemonChildProcess(options, false) - expect(fork.mock.calls[0][0].env).toEqual( - expect.objectContaining({ - BUN_CONPTY_LIBRARY: '/verified/conpty.dll', - ORCA_USER_DATA_PATH: options.userDataPath - }) - ) - expect(fork.mock.calls[0][0].env.NODE_OPTIONS).toBeUndefined() +it.each([undefined, 'conpty.dll'])( + 'refuses a Windows Bun launch without an absolute provider: %s', + (conptyLibraryPath) => { + vi.stubGlobal('process', { ...process, platform: 'win32' }) + expect(() => + spawnDaemonChildProcess( + { + ...options, + bunRuntime: true, + relocatedExecPath: 'C:\\runtime\\bun-runtime.exe', + conptyLibraryPath + }, + false + ) + ).toThrow('ConPTY library path must be absolute') + expect(spawn).not.toHaveBeenCalled() + } +) + +it('does not pass an inherited ConPTY selector to a POSIX Bun daemon', () => { + vi.stubGlobal('process', { + ...process, + platform: 'linux', + env: { ...process.env, BUN_CONPTY_LIBRARY: '/foreign/conpty.dll' } }) + spawnDaemonChildProcess( + { ...options, bunRuntime: true, relocatedExecPath: '/runtime/bun' }, + false + ) + expect(spawn.mock.calls[0][0].env.BUN_CONPTY_LIBRARY).toBeUndefined() +}) - it('preserves an explicitly selected Node executable', () => { - useBun() - spawnDaemonChildProcess({ ...options, relocatedExecPath: '/alternate/node' }, true) - const call = spawn.mock.calls[0][0] - expect(call.args).not.toContain('--no-install') - expect(call.env.ELECTRON_RUN_AS_NODE).toBe('1') +it('places the Bun dotenv guard before the entry in a durable scope', () => { + spawnDaemonChildProcess( + { + ...options, + bunRuntime: true, + relocatedExecPath: '/runtime/bun', + conptyLibraryPath: 'C:\\runtime\\conpty\\conpty.dll' + }, + true + ) + const args: string[] = spawn.mock.calls[0][0].args + const runtimeIndex = args.indexOf('/runtime/bun') + expect(runtimeIndex).toBeGreaterThan(-1) + expect(args.slice(runtimeIndex + 1, runtimeIndex + 5)).toEqual([ + '--no-env-file', + process.platform === 'win32' ? '--config=NUL' : '--config=/dev/null', + '--no-install', + options.forkEntryPath + ]) +}) + +it.each([false, true])( + 'isolates the current headless Bun runtime with durable scope=%s', + (scoped) => { + vi.stubGlobal('process', { + ...process, + platform: 'linux', + versions: { ...process.versions, electron: undefined, bun: '1.4.2' }, + env: { + ...process.env, + BUN_OPTIONS: '--preload=/foreign/preload.js', + NODE_OPTIONS: '--require=/foreign/preload.js', + NODE_PATH: '/foreign/modules' + } + }) + spawnDaemonChildProcess(options, scoped) + const call = scoped ? spawn.mock.calls[0][0] : fork.mock.calls[0][0] + expect(call.env.BUN_OPTIONS).toBeUndefined() + expect(call.env.NODE_OPTIONS).toBeUndefined() + expect(call.env.NODE_PATH).toBeUndefined() + expect(call.env.ELECTRON_RUN_AS_NODE).toBeUndefined() + if (scoped) { + const index = call.args.indexOf(process.execPath) + expect(index).toBeGreaterThan(-1) + expect(call.args.slice(index + 1, index + 5)).toEqual([ + '--no-env-file', + '--config=/dev/null', + '--no-install', + options.forkEntryPath + ]) + } + } +) + +it('keeps verified ConPTY selection when headless Bun forks itself on Windows', () => { + vi.stubGlobal('process', { + ...process, + platform: 'win32', + versions: { ...process.versions, electron: undefined, bun: '1.4.2' }, + env: { ...process.env, BUN_CONPTY_LIBRARY: 'C:\\verified\\conpty.dll' } }) + spawnDaemonChildProcess(options, false) + expect(fork.mock.calls[0][0].env.BUN_CONPTY_LIBRARY).toBe('C:\\verified\\conpty.dll') +}) + +it('does not add Bun flags when a Bun parent explicitly selects another runtime', () => { + vi.stubGlobal('process', { ...process, versions: { ...process.versions, bun: '1.4.2' } }) + spawnDaemonChildProcess({ ...options, relocatedExecPath: '/other/node' }, true) + expect(spawn.mock.calls[0][0].args).not.toContain('--no-env-file') + expect(spawn.mock.calls[0][0].env.ELECTRON_RUN_AS_NODE).toBe('1') +}) + +it.each([false, true])('requires the selected desktop Bun runtime with scope=%s', (scoped) => { + expect(() => spawnDaemonChildProcess({ ...options, bunRuntime: true }, scoped)).toThrow() + expect(spawn).not.toHaveBeenCalled() + expect(fork).not.toHaveBeenCalled() }) diff --git a/src/main/daemon/daemon-launched-child-spawn.ts b/src/main/daemon/daemon-launched-child-spawn.ts index 7dbae59c330f..c1b280f0fcde 100644 --- a/src/main/daemon/daemon-launched-child-spawn.ts +++ b/src/main/daemon/daemon-launched-child-spawn.ts @@ -1,3 +1,4 @@ +import { win32 } from 'node:path' import { forkProcess, type ForkSpec } from '../../shared/child-process/fork-process' import { spawnProcess, type SpawnedProcess } from '../../shared/child-process/run-process' import { bunOwnedRuntimeArgs } from '../../shared/bun-owned-runtime-args' @@ -9,6 +10,8 @@ export type DaemonChildSpawnOptions = { entryPath: string forkEntryPath: string relocatedExecPath?: string + bunRuntime?: boolean + conptyLibraryPath?: string userDataPath: string socketPath: string tokenPath: string @@ -39,19 +42,7 @@ function buildDaemonScriptArgs(options: DaemonChildSpawnOptions): string[] { ] } -/** - * The two ways the daemon's OS process comes into being: the long-standing direct `fork()`, and - * (`useDurableScope`) the identical command line wrapped in `systemd-run --user --scope` so the - * daemon lands in a sibling cgroup instead of the caller's — see daemon-cgroup-scope.ts. The - * wrapper needs `spawn()` because it is not a Node script, but it takes the same `'ipc'` stdio - * contract and the process it execs into inherits the channel, so the readiness handshake is - * unchanged. Do not read the daemon's PID off the returned child; the daemon reports its own - * (see daemon-ready-identity.ts). - * - * Both arms go through the shared child-process chokepoint (`src/shared/child-process`), which - * is what every caller outside that directory must use — `forkProcess` for the Node-module arm, - * `spawnProcess` for the program arm. - */ +/** Preserve detached lifetime and readiness IPC, including the optional sibling systemd scope. */ export function spawnDaemonChildProcess( options: DaemonChildSpawnOptions, useDurableScope: boolean @@ -59,13 +50,14 @@ export function spawnDaemonChildProcess( const { forkEntryPath, relocatedExecPath, userDataPath, launchNonce } = options const scriptArgs = buildDaemonScriptArgs(options) const usesBun = Boolean( - process.versions.bun && (!relocatedExecPath || relocatedExecPath === process.execPath) + options.bunRuntime || + (process.versions.bun && (!relocatedExecPath || relocatedExecPath === process.execPath)) ) - // Why: run as plain Node so Electron's GPU/display init can't interfere with node-pty's posix_spawn of the spawn-helper. + // Desktop launches select Bun explicitly; orcad forks its current Bun runtime. const daemonEnv: NodeJS.ProcessEnv = { ...process.env, ELECTRON_RUN_AS_NODE: '1', - // Why: the detached plain-Node daemon has no AppEnvironment, but shell rcfiles must live outside swept tmp. + // Detached daemons need shell rcfiles outside swept temporary directories. ORCA_USER_DATA_PATH: userDataPath } if (usesBun) { @@ -74,6 +66,15 @@ export function spawnDaemonChildProcess( delete daemonEnv.NODE_PATH delete daemonEnv.BUN_OPTIONS } + if (options.bunRuntime) { + delete daemonEnv.BUN_CONPTY_LIBRARY + if (process.platform === 'win32') { + if (!options.conptyLibraryPath || !win32.isAbsolute(options.conptyLibraryPath)) { + throw new Error('Windows Bun daemon ConPTY library path must be absolute') + } + daemonEnv.BUN_CONPTY_LIBRARY = options.conptyLibraryPath + } + } // Why cwd: detached daemons outlive dev worktrees; userData keeps process.cwd() valid after a repo/worktree is deleted. // Why detached/stdio: detached+unref outlives Electron; stdout 'ignore' (else blocks exit), stderr 'pipe' captures startup crashes lost in v1.4.129-rc.1. const childOptions: Pick = { @@ -81,6 +82,17 @@ export function spawnDaemonChildProcess( detached: true, stdio: ['ignore', 'ignore', 'pipe', 'ipc'] } + if (options.bunRuntime && !relocatedExecPath) { + throw new Error('Bun daemon runtime path is missing') + } + if (!useDurableScope && options.bunRuntime && relocatedExecPath) { + return spawnProcess({ + ...childOptions, + program: relocatedExecPath, + args: [...bunOwnedRuntimeArgs(), forkEntryPath, ...scriptArgs], + env: daemonEnv + }) + } if (!useDurableScope) { return forkProcess({ ...childOptions, diff --git a/src/main/daemon/daemon-main.ts b/src/main/daemon/daemon-main.ts index b2683b0af999..9040e1f0d673 100644 --- a/src/main/daemon/daemon-main.ts +++ b/src/main/daemon/daemon-main.ts @@ -8,6 +8,8 @@ export type DaemonStartOptions = { pidPath?: string launchNonce?: string startedAtMs?: number + linuxStartTicks?: string + bootId?: string publishEndpointOwnership?: DaemonServerOptions['publishEndpointOwnership'] entryPath?: string appVersion?: string @@ -30,6 +32,8 @@ export type DaemonHandle = { export async function startDaemon(opts: DaemonStartOptions): Promise { const server = new DaemonServer({ + linuxStartTicks: opts.linuxStartTicks, + bootId: opts.bootId, socketPath: opts.socketPath, tokenPath: opts.tokenPath, ...(opts.pidPath ? { pidPath: opts.pidPath } : {}), diff --git a/src/main/daemon/daemon-out-of-process-launcher.ts b/src/main/daemon/daemon-out-of-process-launcher.ts index 80a77fe5f884..968d09f08027 100644 --- a/src/main/daemon/daemon-out-of-process-launcher.ts +++ b/src/main/daemon/daemon-out-of-process-launcher.ts @@ -13,7 +13,7 @@ import { terminateLaunchedDaemonChild } from './daemon-launched-child' import { getDaemonEntryPath, probeDaemonSocket as probeSocket } from './daemon-launch-paths' -import { materializeRelocatedDaemonHost } from './daemon-host-relocation' +import { resolveDesktopDaemonBunRuntime, type DaemonBunRuntime } from './daemon-bun-runtime' import { DAEMON_RECOVERY_BUDGET_MS, daemonRecoveryProbeTimeoutMs } from './daemon-recovery-budget' import { cleanupDaemonForProtocol } from './daemon-protocol-cleanup' import { @@ -38,7 +38,7 @@ export function attributeNextDaemonReplacement(reason: DaemonReplaceReason): voi function createPreservedDaemonHandle( runtimeDir: string, protocolVersion = PROTOCOL_VERSION, - mode?: 'degraded-new-pty-fallback' + mode?: 'fresh-spawns-unavailable' ): DaemonProcessHandle { const handle: DaemonProcessHandle = { adopted: true, @@ -87,7 +87,7 @@ export function createOutOfProcessLauncher( adoptionClient = null } const preserveDaemon = async ( - mode?: 'degraded-new-pty-fallback' + mode?: 'fresh-spawns-unavailable' ): Promise => { const connectedClient = adoptionClient ?? undefined adoptionClient = null @@ -100,6 +100,10 @@ export function createOutOfProcessLauncher( pidPath ) } + let bunHost: DaemonBunRuntime | null = null + let runtimePromise: ReturnType | undefined + const getReplacementRuntime = (): ReturnType => + (runtimePromise ??= resolveDesktopDaemonBunRuntime()) try { const preservedHandle = await prepareDaemonReplacement({ runtimeDir, @@ -108,6 +112,10 @@ export function createOutOfProcessLauncher( entryPath, recoveryDeadlineMs, attributedReason, + prepareReplacementRuntime: async () => { + bunHost = await getReplacementRuntime() + return bunHost !== null + }, releaseAdoptionClient, preserveDaemon, launchNonce @@ -117,16 +125,16 @@ export function createOutOfProcessLauncher( } const userDataPath = getAppEnvironment().getPath('userData') - // Why: on win32 packaged, stage a daemon-host copy in userData so its image escapes the NSIS updater's kill zone; lazy so it's off first-paint. Fail-open: null → in-dir host. - const relocatedHost = materializeRelocatedDaemonHost() - // Fork the relocated entry when available; otherwise the install-dir entry. - const forkEntryPath = relocatedHost ? relocatedHost.entryPath : entryPath + bunHost = await getReplacementRuntime() + // Windows Bun artifacts are immutable copies outside the installer kill zone. + const forkEntryPath = bunHost?.entryPath ?? entryPath let launched try { launched = await launchDaemonChild({ entryPath, forkEntryPath, - relocatedExecPath: relocatedHost?.execPath, + relocatedExecPath: bunHost?.execPath, + ...(bunHost ? { bunRuntime: true, conptyLibraryPath: bunHost.conptyLibraryPath } : {}), userDataPath, socketPath, tokenPath, @@ -140,7 +148,7 @@ export function createOutOfProcessLauncher( } // Why adopt rather than retry: another daemon proved it owns the endpoint and is // answering on it. Forking again would lose the same race, and reporting a startup - // failure strands this app on local non-persistent PTYs beside a healthy daemon. + // failure blocks new terminals beside a healthy daemon. console.warn( '[daemon] Endpoint was taken by another daemon during startup — adopting it instead' ) @@ -196,27 +204,24 @@ export function createOutOfProcessLauncher( } } catch (error) { releaseAdoptionClient() - // Why: the launcher may now fork onto an endpoint it could not classify, because the - // publisher is the real guard — and that guard works by refusing to overwrite what it - // cannot prove dead, so the child exits instead of splitting the brain. Correct, but - // giving up here costs the user every persistent session for the whole run. Something - // answering the endpoint now is a daemon worth adopting, not a reason to fall back to - // local PTYs. + // A failed replacement may have lost to another live owner; preserve it before reporting failure. // Why unbudgeted: the recovery deadline bounds the adopt-or-replace decision, and this runs // after it — past the kill, the fork and the lease. Clamping to the remainder yields a 1ms // probe that loses to its own timer against a live socket, turning the rescue into the total // daemon loss it exists to prevent. if (await probeSocket(socketPath)) { console.warn( - '[daemon] DEGRADED MODE: adopting the daemon that owns the endpoint after a replacement could not publish onto it. Existing sessions keep working; fresh terminals run on the local provider WITHOUT daemon persistence until you restart the daemon (Manage Sessions → Restart).' + '[daemon] DEGRADED MODE: adopting the daemon that owns the endpoint after a replacement could not publish onto it. Existing sessions keep their owner; new terminals require a successful terminal-service health check. Retry after the service recovers.' ) try { - return await preserveDaemon('degraded-new-pty-fallback') + return await preserveDaemon('fresh-spawns-unavailable') } catch { // It stopped answering between the probe and the adoption; report the launch failure. } } throw error + } finally { + bunHost?.releaseLaunchPin?.() } } } diff --git a/src/main/daemon/daemon-provider-init.ts b/src/main/daemon/daemon-provider-init.ts index c1d73d95a277..d80dfdbb674f 100644 --- a/src/main/daemon/daemon-provider-init.ts +++ b/src/main/daemon/daemon-provider-init.ts @@ -1,10 +1,10 @@ -import { getLocalPtyProvider, rebindLocalProviderListeners } from '../ipc/pty' +import { DaemonFreshSpawnAdmission } from './daemon-fresh-spawn-admission' +import { rebindLocalProviderListeners } from '../ipc/pty' import { confirmSeededClaudeLivePtys, hasSeededUnconfirmedClaudePtys } from '../claude-accounts/live-pty-gate' import { isStartupDiagnosticsEnabled, logStartupDiagnostic } from '../startup/startup-diagnostics' -import { checkDaemonHealth } from './daemon-health' import { collectPinnedDaemonVersions, pruneOldDaemonHosts } from './daemon-host-relocation' import { cleanupFailedDaemonAdoption, @@ -21,9 +21,12 @@ import { attributeNextDaemonReplacement, createOutOfProcessLauncher } from './daemon-out-of-process-launcher' -import type { DaemonProvider } from './daemon-provider-routing' -import { installDaemonProvider } from './daemon-provider-state' -import { DegradedDaemonPtyProvider } from './degraded-daemon-pty-provider' +import { + createDaemonProviderRouting, + resetDaemonFreshSpawnAdmission, + type DaemonProvider +} from './daemon-provider-routing' +import { getDaemonProvider, installDaemonProvider } from './daemon-provider-state' import { trackDaemonAdopted } from './daemon-adoption-telemetry-event' import { readDaemonPidRecord } from './daemon-endpoint-incarnation' import { trackDaemonRetired } from './daemon-lifecycle-event' @@ -44,7 +47,38 @@ function logDaemonMilestone(event: string, details: Record = {} } } -export async function initDaemonPtyProvider( +let pendingInitialization: Promise | null = null +let initializationOptions: { macosLoginSessionWatch?: boolean } = {} + +export function initDaemonPtyProvider( + signal?: AbortSignal, + options: { macosLoginSessionWatch?: boolean } = {} +): Promise { + if (pendingInitialization) { + return pendingInitialization + } + initializationOptions = { ...options } + pendingInitialization = initializeDaemonPtyProvider(signal, options).finally(() => { + pendingInitialization = null + }) + return pendingInitialization +} + +export async function retryDaemonPtyProvider(): Promise { + await pendingInitialization?.catch(() => {}) + const provider = getDaemonProvider() + if (provider) { + if (!(await provider.recoverFreshSpawnRouting(true))) { + throw new Error( + 'Terminal service is still unable to start new terminals. Existing terminals remain running.' + ) + } + return + } + await initDaemonPtyProvider(undefined, initializationOptions) +} + +async function initializeDaemonPtyProvider( signal?: AbortSignal, options: { macosLoginSessionWatch?: boolean } = {} ): Promise { @@ -65,10 +99,9 @@ export async function initDaemonPtyProvider( const info = await newSpawner.ensureRunning() // Why: reclaim superseded daemon-host copies on EVERY launch (spawns are rare), keeping current + live-daemon-pinned versions. pruneOldDaemonHosts(collectPinnedDaemonVersions(runtimeDir)) - const launchMode = newSpawner.getHandle()?.mode logDaemonMilestone('daemon-current-ready') if (signal?.aborted) { - // Why: fail-open may already have spawned fallback PTYs; don't install late, but retire an empty daemon (live sessions reject it and survive). + // Explicit cancellation releases adoption without installing a provider or killing live sessions. const abortedStartupAdapter = new DaemonPtyAdapter({ socketPath: info.socketPath, tokenPath: info.tokenPath, @@ -81,7 +114,14 @@ export async function initDaemonPtyProvider( return } + const freshSpawnAdmission = new DaemonFreshSpawnAdmission(null) + resetDaemonFreshSpawnAdmission( + freshSpawnAdmission, + info, + newSpawner.getHandle()?.mode === 'fresh-spawns-unavailable' + ) const newAdapter = new DaemonPtyAdapter({ + freshSpawnAdmission, socketPath: info.socketPath, tokenPath: info.tokenPath, pidPath: getDaemonPidPath(runtimeDir), @@ -107,7 +147,12 @@ export async function initDaemonPtyProvider( attributeNextDaemonReplacement(reason) } newSpawner.resetHandle() - await newSpawner.ensureRunning() + const replacement = await newSpawner.ensureRunning() + resetDaemonFreshSpawnAdmission( + freshSpawnAdmission, + replacement, + newSpawner.getHandle()?.mode === 'fresh-spawns-unavailable' + ) return takeDaemonAdoptionLeaseRelease(newSpawner.getHandle()) } }) @@ -119,29 +164,12 @@ export async function initDaemonPtyProvider( releaseDaemonAdoptionLease(newSpawner.getHandle()) legacyAdapters = await createLegacyDaemonAdapters(runtimeDir) - routedAdapter = - launchMode === 'degraded-new-pty-fallback' - ? new DegradedDaemonPtyProvider({ - current: newAdapter, - legacy: legacyAdapters, - fallback: getLocalPtyProvider(), - probeCurrentDaemonSpawn: async () => - (await checkDaemonHealth(info.socketPath, info.tokenPath)) === 'healthy' - }) - : legacyAdapters.length > 0 - ? new DaemonPtyRouter({ - current: newAdapter, - legacy: legacyAdapters - }) - : newAdapter - if (routedAdapter instanceof DegradedDaemonPtyProvider) { - // Why: preserved daemon can't create fresh terminals; discover its live session ids so only they route to it (fresh panes fall back locally). - await routedAdapter.discoverDaemonSessions() - } else if (routedAdapter instanceof DaemonPtyRouter) { + routedAdapter = createDaemonProviderRouting(newAdapter, legacyAdapters, freshSpawnAdmission) + if (routedAdapter instanceof DaemonPtyRouter) { await routedAdapter.discoverLegacySessions() } if (signal?.aborted) { - // Why: same late-swap guard after legacy discovery; release uninstalled adapter leases without killing live sessions. + // Cancellation can also arrive while discovering legacy sessions. await routedAdapter.disconnectOnly() return } @@ -196,10 +224,7 @@ async function reconcileSeededClaudeLivePtys(provider: DaemonProvider): Promise< return } try { - const adapters = - provider instanceof DaemonPtyRouter || provider instanceof DegradedDaemonPtyProvider - ? provider.getAllAdapters() - : [provider] + const adapters = provider instanceof DaemonPtyRouter ? provider.getAllAdapters() : [provider] const results = await Promise.allSettled(adapters.map((entry) => entry.listSessions())) if (results.some((result) => result.status === 'rejected')) { console.warn('[daemon] Keeping seeded Claude live-PTY gate — session listing failed') diff --git a/src/main/daemon/daemon-provider-restart.ts b/src/main/daemon/daemon-provider-restart.ts index 7cc2ffa7fd1e..73aa712a1bab 100644 --- a/src/main/daemon/daemon-provider-restart.ts +++ b/src/main/daemon/daemon-provider-restart.ts @@ -1,5 +1,5 @@ +import { DaemonFreshSpawnAdmission } from './daemon-fresh-spawn-admission' import { rebindLocalProviderListeners, unbindLocalProviderListeners } from '../ipc/pty' -import { DegradedDaemonPtyProvider } from './degraded-daemon-pty-provider' import { cleanupFailedDaemonAdoption, releaseDaemonAdoptionLease, @@ -13,12 +13,15 @@ import { import { trackDaemonRetired } from './daemon-lifecycle-event' import { attributeNextDaemonReplacement } from './daemon-out-of-process-launcher' import { + createDaemonProviderRouting, + resetDaemonFreshSpawnAdmission, disposeProviderSubscriptionsOnly, getCurrentDaemonAdapter, getLegacyDaemonAdapters, type DaemonProvider } from './daemon-provider-routing' import { getDaemonProvider, getDaemonSpawner, replaceDaemonProvider } from './daemon-provider-state' +import { retryDaemonPtyProvider } from './daemon-provider-init' import { DaemonPtyAdapter } from './daemon-pty-adapter' import type { DaemonRespawnReason } from './daemon-pty-runtime-state' import { DaemonPtyRouter } from './daemon-pty-router' @@ -42,7 +45,8 @@ async function runRestartDaemon(): Promise { const currentSpawner = getDaemonSpawner() const currentAdapter = getDaemonProvider() if (!currentSpawner || !currentAdapter) { - throw new Error('restartDaemon called before initDaemonPtyProvider') + await retryDaemonPtyProvider() + return { killedCount: 0 } } const runtimeDir = getRuntimeDir() @@ -53,24 +57,16 @@ async function runRestartDaemon(): Promise { const legacyAdapters = getLegacyDaemonAdapters(currentAdapter) // Step 1: synthesize pty:exit for every active session BEFORE teardown — the daemon's shutdown path never fans onExit to clients (session.ts:246-252), so the renderer would otherwise never see exits. - const fallbackKilledCount = - currentAdapter instanceof DegradedDaemonPtyProvider - ? await currentAdapter.shutdownFallbackSessions() - : 0 const currentDaemonSessionIds = - currentAdapter instanceof DegradedDaemonPtyProvider - ? currentAdapter.getCurrentDaemonSessionIds() - : [] - const killedCount = - new Set([...currentOnly.getActiveSessionIds(), ...currentDaemonSessionIds]).size + - fallbackKilledCount + currentAdapter instanceof DaemonPtyRouter ? currentAdapter.getCurrentDaemonSessionIds() : [] + const killedCount = new Set([...currentOnly.getActiveSessionIds(), ...currentDaemonSessionIds]) + .size currentOnly.fanoutSyntheticExits(-1) - if (currentAdapter instanceof DegradedDaemonPtyProvider) { + if (currentAdapter instanceof DaemonPtyRouter) { currentAdapter.fanoutCurrentDaemonSyntheticExits(-1) } - // Step 2: detach renderer listeners — after step 1 (so synthesized exits land) and before step 6 (no stale binding). - unbindLocalProviderListeners() + // Keep unaffected guest streams subscribed while replacement awaits I/O. // Step 3: kill the current-protocol daemon process; legacy adapters untouched. let info: Awaited> @@ -81,13 +77,20 @@ async function runRestartDaemon(): Promise { currentSpawner.resetHandle() info = await currentSpawner.ensureRunning() } catch (error) { - // Why: old provider stays authoritative until the final swap; rebind since relaunch failed after teardown. + // The prior provider remains authoritative if replacement fails. rebindLocalProviderListeners() throw error } // Step 5: build a fresh current adapter against the respawned daemon. + const freshSpawnAdmission = new DaemonFreshSpawnAdmission(null) + resetDaemonFreshSpawnAdmission( + freshSpawnAdmission, + info, + currentSpawner.getHandle()?.mode === 'fresh-spawns-unavailable' + ) const newCurrent = new DaemonPtyAdapter({ + freshSpawnAdmission, socketPath: info.socketPath, tokenPath: info.tokenPath, pidPath: getDaemonPidPath(runtimeDir), @@ -112,7 +115,12 @@ async function runRestartDaemon(): Promise { attributeNextDaemonReplacement(reason) } currentSpawner.resetHandle() - await currentSpawner.ensureRunning() + const replacement = await currentSpawner.ensureRunning() + resetDaemonFreshSpawnAdmission( + freshSpawnAdmission, + replacement, + currentSpawner.getHandle()?.mode === 'fresh-spawns-unavailable' + ) return takeDaemonAdoptionLeaseRelease(currentSpawner.getHandle()) } }) @@ -122,11 +130,7 @@ async function runRestartDaemon(): Promise { await newCurrent.establishLifecycleLease() releaseDaemonAdoptionLease(currentSpawner.getHandle()) - // Re-wrap in a router only if legacy adapters exist; they're preserved by reference and still route to their pre-upgrade daemons. - newProvider = - legacyAdapters.length > 0 - ? new DaemonPtyRouter({ current: newCurrent, legacy: legacyAdapters }) - : newCurrent + newProvider = createDaemonProviderRouting(newCurrent, legacyAdapters, freshSpawnAdmission) if (newProvider instanceof DaemonPtyRouter) { await newProvider.discoverLegacySessions() } @@ -140,7 +144,7 @@ async function runRestartDaemon(): Promise { } catch (caught) { cleanupError = caught } - // Previous provider stays module-authoritative until the swap; restore its renderer bindings when adoption fails. + // Reconcile guest registrations against the still-authoritative provider. rebindLocalProviderListeners() if (cleanupError) { throw new AggregateError([error, cleanupError], 'Daemon restart and cleanup both failed') @@ -149,6 +153,7 @@ async function runRestartDaemon(): Promise { } // Drain the old router's subscriptions via the router-only variant (plain dispose() would tear down the shared legacy adapters), after the new provider exists (no unhandled events) and before the swap (atomic for the renderer). + unbindLocalProviderListeners() disposeProviderSubscriptionsOnly(currentAdapter) // Step 6: swap module state (adapter + localProvider) atomically. diff --git a/src/main/daemon/daemon-provider-routing.ts b/src/main/daemon/daemon-provider-routing.ts index 0198bf2893c7..7ccd50f2e98d 100644 --- a/src/main/daemon/daemon-provider-routing.ts +++ b/src/main/daemon/daemon-provider-routing.ts @@ -1,18 +1,19 @@ -import { DegradedDaemonPtyProvider } from './degraded-daemon-pty-provider' +import type { DaemonFreshSpawnAdmission } from './daemon-fresh-spawn-admission' +import { checkDaemonHealth } from './daemon-health' import type { DaemonPtyAdapter } from './daemon-pty-adapter' import { DaemonPtyRouter } from './daemon-pty-router' -export type DaemonProvider = DaemonPtyRouter | DaemonPtyAdapter | DegradedDaemonPtyProvider +export type DaemonProvider = DaemonPtyRouter | DaemonPtyAdapter export function getCurrentDaemonAdapter(provider: DaemonProvider): DaemonPtyAdapter { - if (provider instanceof DaemonPtyRouter || provider instanceof DegradedDaemonPtyProvider) { + if (provider instanceof DaemonPtyRouter) { return provider.getCurrentAdapter() } return provider } export function getLegacyDaemonAdapters(provider: DaemonProvider): DaemonPtyAdapter[] { - if (provider instanceof DaemonPtyRouter || provider instanceof DegradedDaemonPtyProvider) { + if (provider instanceof DaemonPtyRouter) { return [...provider.getLegacyAdapters()] } return [] @@ -21,9 +22,27 @@ export function getLegacyDaemonAdapters(provider: DaemonProvider): DaemonPtyAdap export function disposeProviderSubscriptionsOnly(provider: DaemonProvider): void { if (provider instanceof DaemonPtyRouter) { provider.disposeRouterOnly() - return - } - if (provider instanceof DegradedDaemonPtyProvider) { - provider.disposeProviderOnly() } } + +export function createDaemonProviderRouting( + current: DaemonPtyAdapter, + legacy: DaemonPtyAdapter[], + freshSpawns: DaemonFreshSpawnAdmission +): DaemonProvider { + return legacy.length > 0 || freshSpawns.unavailable + ? new DaemonPtyRouter({ current, legacy, freshSpawnAdmission: freshSpawns }) + : current +} + +export function resetDaemonFreshSpawnAdmission( + admission: DaemonFreshSpawnAdmission, + endpoint: { socketPath: string; tokenPath: string }, + unavailable: boolean +): void { + admission.reset( + unavailable + ? async () => (await checkDaemonHealth(endpoint.socketPath, endpoint.tokenPath)) === 'healthy' + : null + ) +} diff --git a/src/main/daemon/daemon-provider-state.ts b/src/main/daemon/daemon-provider-state.ts index 412dda3af83e..c47edecfda6d 100644 --- a/src/main/daemon/daemon-provider-state.ts +++ b/src/main/daemon/daemon-provider-state.ts @@ -1,6 +1,5 @@ import { readFileSync } from 'node:fs' import { setLocalPtyProvider } from '../ipc/pty' -import { DegradedDaemonPtyProvider } from './degraded-daemon-pty-provider' import { getDaemonRuntimeDir as getRuntimeDir } from './daemon-launch-paths' import { parseDaemonPidFile, type ParsedDaemonPid } from './daemon-pid-file-parse' import type { DaemonProvider } from './daemon-provider-routing' @@ -25,17 +24,9 @@ export function getDaemonSpawner(): DaemonSpawner | null { return spawner } -// Why: a narrow getter (not a raw export) keeps the "swap on restart" invariant in one place (replaceDaemonProvider). -/** - * Whether the installed provider is a daemon that will own FRESH terminals too. - * - * Why not `getDaemonProvider() !== null`: DegradedDaemonPtyProvider routes the daemon's - * EXISTING sessions to the daemon but spawns new ones on the in-process local provider, so - * those die with this process. A host that answered "I can recover persistent local PTYs" - * from that state would be advertising recovery for terminals that cannot be recovered. - */ +/** Existing sessions can remain attached while an unhealthy service refuses new terminals. */ export function daemonOwnsFreshPersistentPtys(): boolean { - return adapter !== null && !(adapter instanceof DegradedDaemonPtyProvider) + return adapter !== null && !adapter.freshSpawnsUnavailable } /** Endpoint coordinates of the daemon this process installed, for out-of-band health probes. */ @@ -100,10 +91,7 @@ export async function listLiveDaemonPtyIds(): Promise { if (!adapter) { return null } - const adapters = - adapter instanceof DaemonPtyRouter || adapter instanceof DegradedDaemonPtyProvider - ? adapter.getAllAdapters() - : [adapter] + const adapters = adapter instanceof DaemonPtyRouter ? adapter.getAllAdapters() : [adapter] const inventories = await Promise.allSettled( adapters.map((daemonAdapter) => daemonAdapter.listProcesses()) ) diff --git a/src/main/daemon/daemon-pty-adapter-subscription-fanout.ts b/src/main/daemon/daemon-pty-adapter-subscription-fanout.ts index 1d82ef70b7ba..bf7c40f162e3 100644 --- a/src/main/daemon/daemon-pty-adapter-subscription-fanout.ts +++ b/src/main/daemon/daemon-pty-adapter-subscription-fanout.ts @@ -69,6 +69,13 @@ export class DaemonPtyAdapterSubscriptionFanout { } } + publishExit(payload: DaemonPtyRouterExitEvent): void { + // oxlint-disable-next-line unicorn/no-useless-spread -- listeners may unsubscribe during exit delivery + for (const listener of [...this.exitListeners]) { + listener(payload) + } + } + dispose(): void { for (const unsubscribe of this.unsubscribers.splice(0)) { unsubscribe() diff --git a/src/main/daemon/daemon-pty-adapter.ts b/src/main/daemon/daemon-pty-adapter.ts index dfe694fbcad1..81f5690328af 100644 --- a/src/main/daemon/daemon-pty-adapter.ts +++ b/src/main/daemon/daemon-pty-adapter.ts @@ -2,9 +2,21 @@ import { emitPtyListeners, createPtyExitPayload } from './daemon-pty-listener-em import { DaemonPtyDaemonRecovery } from './daemon-pty-daemon-recovery' import { supportsMode2031UnsubscribeFact, type DaemonEvent } from './types' import type { DaemonEndpointIdentity } from './daemon-hello-protocol' -import type { IPtyProvider } from '../providers/types' +import type { IPtyProvider, PtySpawnResult } from '../providers/types' export class DaemonPtyAdapter extends DaemonPtyDaemonRecovery implements IPtyProvider { + get freshSpawnsUnavailable(): boolean { + return this.freshSpawnAdmission.unavailable + } + + recoverFreshSpawnRouting(force = false): Promise { + return this.freshSpawnAdmission.recover(force) + } + + didExitBeforeSpawnReply(result: PtySpawnResult): boolean { + return result.exitedBeforeSpawnReply === true + } + /** Identity of the daemon behind this adapter; null until hello completes or after a disconnect. */ getDaemonIdentity(): DaemonEndpointIdentity | null { return this.client.getDaemonIdentity() diff --git a/src/main/daemon/daemon-pty-connection-lifecycle.ts b/src/main/daemon/daemon-pty-connection-lifecycle.ts index fa8153ad47a7..cce48777e987 100644 --- a/src/main/daemon/daemon-pty-connection-lifecycle.ts +++ b/src/main/daemon/daemon-pty-connection-lifecycle.ts @@ -53,7 +53,9 @@ export abstract class DaemonPtyConnectionLifecycle extends DaemonPtyEventSubscri this.getSizeUnsupported = false } this.lastAuthenticatedIdentity = { ...current } - this.exactDaemonIncarnation = exactDaemonIncarnationForPidRecord(current, this.pidRecord) + this.exactDaemonIncarnation = this.guest + ? null + : exactDaemonIncarnationForPidRecord(current, this.pidRecord) if (!previous) { return } @@ -123,6 +125,10 @@ export abstract class DaemonPtyConnectionLifecycle extends DaemonPtyEventSubscri additionalEvidenceSources: readonly DaemonEvidenceSource[] = [], endpointGoneProof?: 'windows_named_pipe_missing' ): void { + // Guest PIDs and endpoints have no meaning in the desktop OS namespace. + if (this.guest) { + return + } void this.resolveExactDaemonIncarnation(exactIncarnation) .then((resolvedIncarnation) => { this.cacheExactDaemonIncarnation(resolvedIncarnation) @@ -136,6 +142,9 @@ export abstract class DaemonPtyConnectionLifecycle extends DaemonPtyEventSubscri } protected publishAuditObservation(observation: DaemonAuditObservation): void { + if (this.guest) { + return + } this.lastAuditObservation = observation this.trackAuditEligibility(observation) notifyDaemonAuditListeners(this.auditObservationListeners, observation) diff --git a/src/main/daemon/daemon-pty-daemon-recovery.ts b/src/main/daemon/daemon-pty-daemon-recovery.ts index 374c39d1c214..ce1c91f4b59d 100644 --- a/src/main/daemon/daemon-pty-daemon-recovery.ts +++ b/src/main/daemon/daemon-pty-daemon-recovery.ts @@ -12,7 +12,9 @@ import type { PtyBackgroundStreamEvent } from '../providers/types' export abstract class DaemonPtyDaemonRecovery extends DaemonPtyCheckpointPersistence { // Why: the token read no longer throws, so audit its absence directly after an authenticated drop. protected isRetiredEndpointTokenMissing(): boolean { - return this.client.hasObservedAuthenticatedDisconnect() && !existsSync(this.tokenPath) + return ( + !this.guest && this.client.hasObservedAuthenticatedDisconnect() && !existsSync(this.tokenPath) + ) } protected async withDaemonRetry(fn: () => Promise): Promise { @@ -118,7 +120,7 @@ export abstract class DaemonPtyDaemonRecovery extends DaemonPtyCheckpointPersist } protected async replaceUnhealthyMacResolverDaemonBeforeNewPty(): Promise { - if (!this.respawnFn) { + if (this.guest || !this.respawnFn) { return } @@ -157,7 +159,7 @@ export abstract class DaemonPtyDaemonRecovery extends DaemonPtyCheckpointPersist /** Replace a stale packaged daemon only after its live sessions drain. */ protected async replaceStaleBundleDaemonBeforeNewPty(): Promise { - if (!this.respawnFn || !this.runtimeDir || !this.packagedAppVersion) { + if (this.guest || !this.respawnFn || !this.runtimeDir || !this.packagedAppVersion) { return } if (!this.staleBundleReplacementPromise) { @@ -212,7 +214,7 @@ export abstract class DaemonPtyDaemonRecovery extends DaemonPtyCheckpointPersist /** Replace a TCC-severed daemon only after its live sessions drain. */ protected async replaceSeveredMacTccDaemonBeforeNewPty(): Promise { // Why no platform gate: getMacDaemonTccAttributionHealth returns 'unknown' off macOS. - if (!this.respawnFn || !this.runtimeDir) { + if (this.guest || !this.respawnFn || !this.runtimeDir) { return } diff --git a/src/main/daemon/daemon-pty-guest-execution.test.ts b/src/main/daemon/daemon-pty-guest-execution.test.ts new file mode 100644 index 000000000000..cc7744096bf6 --- /dev/null +++ b/src/main/daemon/daemon-pty-guest-execution.test.ts @@ -0,0 +1,240 @@ +import type { Socket } from 'node:net' +import './mock-descendant-sweep' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { readFileSync, rmSync } from 'node:fs' +import { join } from 'node:path' +import { DaemonPtyAdapter } from './daemon-pty-adapter' +import { connectDaemonSocket } from './daemon-client-socket-connect' +import { + createMockSubprocess, + startDaemonAdapterHarness, + waitFor, + type DaemonAdapterHarness +} from './daemon-pty-adapter-test-harness' +import { WslDaemonPtyProvider } from '../wsl/wsl-daemon-pty-provider' +import { toAppWslPtyId, toRelayWslPtyId } from '../../shared/wsl-pty-id' +import { HistoryReader } from './history-reader' +import type { DaemonPtyRouterDataEvent } from './daemon-pty-router-events' + +const { reportCwd } = vi.hoisted(() => ({ reportCwd: vi.fn() })) +vi.mock('./daemon-adoption-telemetry-event', () => ({ reportDaemonPtyCwdVerdict: reportCwd })) + +const owner = { distro: 'Ubuntu', relayBuildId: 'profile-user-endpoint' } + +describe('guest daemon execution boundary', () => { + let harness: DaemonAdapterHarness + let subprocess: ReturnType + let adapters: DaemonPtyAdapter[] + const spawn = vi.fn() + const connectRoles: string[] = [] + const connections: Socket[] = [] + const guest = () => ({ + distro: owner.distro, + defaultShell: '/guest-only/bin/bash', + defaultCwd: '/home/guest', + profiles: [{ name: 'Guest bash', path: '/guest-only/bin/bash' }], + transport: { + readToken: () => readFileSync(harness.tokenPath, 'utf8').trim(), + connect: async (role: 'control' | 'stream', operation: { timeoutMs: number }) => { + connectRoles.push(role) + const socket = await connectDaemonSocket(harness.socketPath, operation.timeoutMs) + connections.push(socket) + return socket + } + } + }) + function provider(historyPath?: string) { + const adapter = new DaemonPtyAdapter({ guest: guest(), historyPath }) + adapters.push(adapter) + return { adapter, provider: new WslDaemonPtyProvider(owner, adapter) } + } + beforeEach(async () => { + adapters = [] + connectRoles.length = 0 + connections.length = 0 + spawn.mockReset() + reportCwd.mockReset() + subprocess = createMockSubprocess() + harness = await startDaemonAdapterHarness((options) => { + spawn(options) + return subprocess + }) + }) + afterEach(async () => { + adapters.forEach((adapter) => adapter.dispose()) + harness.adapter.dispose() + await harness.server.shutdown() + rmSync(harness.dir, { recursive: true, force: true }) + }) + + it('uses guest defaults and both authenticated channels without desktop paths', async () => { + const { provider: p, adapter } = provider() + const result = await p.spawn({ cols: 80, rows: 24, isNewSession: true }) + expect(result.id).toMatch(/^wsl:/) + expect(result.wslDistro).toBe('Ubuntu') + expect(spawn).toHaveBeenCalledWith( + expect.objectContaining({ + cwd: '/home/guest', + shellOverride: '/guest-only/bin/bash' + }) + ) + expect(connectRoles).toEqual(['control', 'stream']) + expect(await p.getDefaultShell()).toBe('/guest-only/bin/bash') + expect(await p.getProfiles()).toEqual(guest().profiles) + expect(adapter.getLastAuthenticatedDaemonIdentity()).not.toBeNull() + await p.listProcesses() + expect(adapter.getLastAuditObservation()).toBeNull() + expect(reportCwd).not.toHaveBeenCalled() + }) + + it('delivers plain guest shell input without waiting for an unrequested startup marker', async () => { + const { provider: p } = provider() + const result = await p.spawn({ cols: 80, rows: 24, isNewSession: true }) + await p.writeWithSettlement(result.id, 'echo ready\r') + await waitFor(() => subprocess.write.mock.calls.length > 0) + expect(subprocess.write).toHaveBeenCalledWith('echo ready\r') + }) + + it('does not spawn before durable admission and refuses a connection changed while waiting', async () => { + let release!: () => void + const admission = new Promise((resolve) => { + release = resolve + }) + const admitted = vi.fn(() => admission) + const adapter = new DaemonPtyAdapter({ guest: { ...guest(), admitIdentity: admitted } }) + adapters.push(adapter) + const spawning = expect( + adapter.spawn({ cols: 80, rows: 24, isNewSession: true }) + ).rejects.toThrow('Connection lost') + await vi.waitFor(() => expect(admitted).toHaveBeenCalledOnce()) + expect(spawn).not.toHaveBeenCalled() + for (const socket of connections) { + socket.destroy() + } + await new Promise((resolve) => setImmediate(resolve)) + release() + await spawning + expect(spawn).not.toHaveBeenCalled() + }) + + it('rejects desktop endpoint mixing and foreign paths before spawning', async () => { + expect(() => new DaemonPtyAdapter({ guest: guest(), tokenPath: '/desktop/token' })).toThrow( + 'cannot use desktop' + ) + const { provider: p } = provider() + await expect(p.spawn({ cols: 80, rows: 24, cwd: 'C:\\Users\\guest' })).rejects.toThrow( + 'selected distro' + ) + expect(spawn).not.toHaveBeenCalled() + }) + + it('preserves snapshot authority, event coordinates and owner identity across detach', async () => { + const historyPath = join(harness.dir, 'guest-history') + const { provider: first, adapter } = provider(historyPath) + const result = await first.spawn({ cols: 91, rows: 31, isNewSession: true }) + const events: DaemonPtyRouterDataEvent[] = [] + const sourceEvents: DaemonPtyRouterDataEvent[] = [] + adapter.onData((event) => sourceEvents.push(event)) + first.onData((event) => events.push(event)) + const text = `IMPORTANT HEADER\r\n${'\rprogress'.repeat(14000)}` + subprocess._simulateData(text) + await waitFor(() => events.length > 0) + const before = await first.getBufferSnapshot(result.id) + expect(before).toMatchObject({ + data: expect.stringContaining('IMPORTANT HEADER'), + cols: 91, + rows: 31, + seq: text.length, + source: 'headless' + }) + expect(events.every((event) => event.id === result.id)).toBe(true) + expect(events).toEqual(sourceEvents.map((event) => ({ ...event, id: result.id }))) + expect(first.canProvideAuthoritativeBufferSnapshot(result.id)).toBe(true) + await first.disconnectOnly() + expect(subprocess.kill).not.toHaveBeenCalled() + expect(await first.hasChildProcesses(result.id)).toBe(true) + expect(await first.probePtyLiveness(result.id)).toBeNull() + expect( + await new HistoryReader(historyPath).detectColdRestoreState( + toRelayWslPtyId(owner, result.id), + { wslDistro: owner.distro } + ) + ).toMatchObject({ + status: 'restored', + restoreInfo: { cols: 91, rows: 31, cwd: '/home/guest' } + }) + const { provider: second } = provider(historyPath) + const attached = await second.spawn({ + cols: 91, + rows: 31, + sessionId: result.id, + attachOnly: true + }) + expect(attached.id).toBe(result.id) + expect(spawn).toHaveBeenCalledTimes(1) + expect(await second.getBufferSnapshot(result.id)).toEqual({ ...before, cwd: '/home/guest' }) + expect(await second.listProcesses()).toEqual([ + expect.objectContaining({ id: result.id, wslDistro: 'Ubuntu' }) + ]) + }) + + it('reports connector loss for every pane without declaring exit or replacing shells', async () => { + const { provider: p } = provider() + const first = await p.spawn({ cols: 80, rows: 24, isNewSession: true }) + const second = await p.spawn({ cols: 80, rows: 24, isNewSession: true }) + const unavailable = vi.fn() + const exited = vi.fn() + p.onWriteUnavailable(unavailable) + p.onExit(exited) + connections[0]!.destroy() + await waitFor(() => unavailable.mock.calls.length === 2) + expect(unavailable.mock.calls.map(([event]) => event.id).sort()).toEqual( + [first.id, second.id].sort() + ) + expect(exited).not.toHaveBeenCalled() + expect(subprocess.kill).not.toHaveBeenCalled() + const attached = await p.spawn({ + cols: 80, + rows: 24, + sessionId: first.id, + attachOnly: true + }) + expect(attached.id).toBe(first.id) + expect(spawn).toHaveBeenCalledTimes(2) + await p.disconnectOnly() + expect(unavailable).toHaveBeenCalledTimes(2) + }) + + it('publishes guest exit identity without losing the incarnation', async () => { + const { provider: p } = provider() + const result = await p.spawn({ cols: 80, rows: 24, isNewSession: true }) + const exited = vi.fn() + p.onExit(exited) + subprocess._simulateExit(17) + await waitFor(() => exited.mock.calls.length === 1) + expect(exited).toHaveBeenCalledWith( + expect.objectContaining({ + id: result.id, + code: 17, + incarnationId: result.incarnationId + }) + ) + expect(p.hasPty(result.id)).toBe(false) + }) + + it('rejects raw and foreign-owner attach IDs without contacting the owner', async () => { + const { provider: p } = provider() + await expect( + p.spawn({ cols: 80, rows: 24, sessionId: 'raw', attachOnly: true }) + ).rejects.toThrow('encoded WSL') + await expect( + p.spawn({ + cols: 80, + rows: 24, + attachOnly: true, + sessionId: toAppWslPtyId({ ...owner, relayBuildId: 'other-user' }, 'pty') + }) + ).rejects.toThrow('different distro or relay build') + expect(connectRoles).toEqual([]) + }) +}) diff --git a/src/main/daemon/daemon-pty-router-test-adapter.ts b/src/main/daemon/daemon-pty-router-test-adapter.ts new file mode 100644 index 000000000000..f826eb281f83 --- /dev/null +++ b/src/main/daemon/daemon-pty-router-test-adapter.ts @@ -0,0 +1,164 @@ +import { vi } from 'vitest' +import type { DaemonPtyAdapter } from './daemon-pty-adapter' +import { settledWriteStub } from '../providers/settled-pty-write-stub' +import type { PtyBackgroundStreamEvent, PtySpawnOptions, PtySpawnResult } from '../providers/types' +import { + AGENT_SESSION_CLAIM_DAEMON_PROTOCOL_VERSION, + AGENT_SESSION_CREATE_OPERATION_DAEMON_PROTOCOL_VERSION, + GIT_CREDENTIAL_GUARD_HOST_PROTOCOL_VERSION +} from './types' +import { SNAPSHOT_SERIALIZER_FIDELITY_DAEMON_PROTOCOL_VERSION } from './daemon-protocol-version' +type AdapterMock = DaemonPtyAdapter & { + emitData: (id: string, data: string, sequenceChars?: number) => void + emitBackground: (event: PtyBackgroundStreamEvent) => void + emitExit: (id: string, code: number, incarnationId?: string) => void + emitIdentityChange: () => void + triggerWriteUnavailable: (id: string) => void +} + +export function createAdapter( + label: string, + sessions: string[] = [], + reconcileResult?: { alive: string[]; killed: string[] }, + protocolVersion = GIT_CREDENTIAL_GUARD_HOST_PROTOCOL_VERSION +): AdapterMock { + const writes: { id: string; data: string }[] = [] + const dataListeners: ((payload: { id: string; data: string; sequenceChars?: number }) => void)[] = + [] + const backgroundListeners: ((payload: PtyBackgroundStreamEvent) => void)[] = [] + const writeUnavailableListeners: ((payload: { id: string }) => void)[] = [] + const exitListeners: ((payload: { id: string; code: number; incarnationId?: string }) => void)[] = + [] + const identityChangeListeners: (() => void)[] = [] + return { + protocolVersion, + supportsGitCredentialGuardHost: () => + protocolVersion >= GIT_CREDENTIAL_GUARD_HOST_PROTOCOL_VERSION, + supportsAgentSessionClaims: () => + protocolVersion >= AGENT_SESSION_CLAIM_DAEMON_PROTOCOL_VERSION, + supportsAgentSessionCreateOperations: () => + protocolVersion >= AGENT_SESSION_CREATE_OPERATION_DAEMON_PROTOCOL_VERSION, + providesAgentSessionOwnerListings: () => + protocolVersion >= AGENT_SESSION_CLAIM_DAEMON_PROTOCOL_VERSION, + canProvideAuthoritativeBufferSnapshot: () => + protocolVersion >= SNAPSHOT_SERIALIZER_FIDELITY_DAEMON_PROTOCOL_VERSION, + spawn: vi.fn(async (opts: PtySpawnOptions): Promise => { + const id = opts.sessionId ?? `${label}-new` + sessions.push(id) + return { id } + }), + listProcesses: vi.fn(async () => + sessions.map((id) => ({ + id, + cwd: '', + title: label + })) + ), + hasPty: vi.fn((id: string) => sessions.includes(id)), + probePtyLiveness: vi.fn(async (id: string) => sessions.includes(id)), + write: vi.fn((id: string, data: string) => { + writes.push({ id, data }) + }), + writeWithSettlement: vi.fn(settledWriteStub()), + resize: vi.fn(), + setPtyBackgrounded: vi.fn(), + getBufferSnapshot: vi.fn(async () => null), + shutdown: vi.fn(async (id: string) => { + const idx = sessions.indexOf(id) + if (idx !== -1) { + sessions.splice(idx, 1) + } + }), + attach: vi.fn(async () => {}), + sendSignal: vi.fn(async () => {}), + getCwd: vi.fn(async () => ''), + getInitialCwd: vi.fn(async () => ''), + clearBuffer: vi.fn(async () => {}), + acknowledgeDataEvent: vi.fn(), + hasChildProcesses: vi.fn(async () => false), + getForegroundProcess: vi.fn(async () => null), + inspectProcess: vi.fn(async () => ({ foregroundProcess: null, hasChildProcesses: false })), + confirmForegroundProcess: vi.fn(async () => `${label}-confirmed`), + serialize: vi.fn(async () => '{}'), + revive: vi.fn(async () => {}), + getDefaultShell: vi.fn(async () => '/bin/zsh'), + getProfiles: vi.fn(async () => []), + onData: vi.fn( + (callback: (payload: { id: string; data: string; sequenceChars?: number }) => void) => { + dataListeners.push(callback) + return () => { + const idx = dataListeners.indexOf(callback) + if (idx !== -1) { + dataListeners.splice(idx, 1) + } + } + } + ), + onBackgroundStreamEvent: vi.fn((callback: (payload: PtyBackgroundStreamEvent) => void) => { + backgroundListeners.push(callback) + return () => { + const idx = backgroundListeners.indexOf(callback) + if (idx !== -1) { + backgroundListeners.splice(idx, 1) + } + } + }), + onWriteUnavailable: vi.fn((callback: (payload: { id: string }) => void) => { + writeUnavailableListeners.push(callback) + return () => { + const idx = writeUnavailableListeners.indexOf(callback) + if (idx !== -1) { + writeUnavailableListeners.splice(idx, 1) + } + } + }), + onExit: vi.fn( + (callback: (payload: { id: string; code: number; incarnationId?: string }) => void) => { + exitListeners.push(callback) + return () => { + const idx = exitListeners.indexOf(callback) + if (idx !== -1) { + exitListeners.splice(idx, 1) + } + } + } + ), + onDaemonIdentityChanged: vi.fn((callback: () => void) => { + identityChangeListeners.push(callback) + return () => { + const idx = identityChangeListeners.indexOf(callback) + if (idx !== -1) { + identityChangeListeners.splice(idx, 1) + } + } + }), + ackColdRestore: vi.fn(), + clearTombstone: vi.fn(), + reconcileOnStartup: vi.fn(async () => reconcileResult ?? { alive: sessions, killed: [] }), + dispose: vi.fn(), + disconnectOnly: vi.fn(async () => {}), + emitData: (id: string, data: string, sequenceChars?: number) => { + for (const listener of dataListeners) { + listener({ id, data, ...(sequenceChars === undefined ? {} : { sequenceChars }) }) + } + }, + emitBackground: (event: PtyBackgroundStreamEvent) => { + for (const listener of backgroundListeners) { + listener(event) + } + }, + emitExit: (id: string, code: number, incarnationId?: string) => { + for (const listener of exitListeners) { + listener({ id, code, ...(incarnationId ? { incarnationId } : {}) }) + } + }, + emitIdentityChange: () => identityChangeListeners.forEach((listener) => listener()), + triggerWriteUnavailable: (id: string) => { + for (const listener of writeUnavailableListeners) { + listener({ id }) + } + }, + _writes: writes + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: test adapter supplies every method exercised by router tests. + } as unknown as AdapterMock +} diff --git a/src/main/daemon/daemon-pty-router.test.ts b/src/main/daemon/daemon-pty-router.test.ts index 61db990b21c6..63b647454e57 100644 --- a/src/main/daemon/daemon-pty-router.test.ts +++ b/src/main/daemon/daemon-pty-router.test.ts @@ -1,29 +1,20 @@ +import { createAdapter } from './daemon-pty-router-test-adapter' import { describe, expect, it, vi } from 'vitest' import { DaemonPtyRouter } from './daemon-pty-router' import { SessionNotFoundError, TerminalSessionOwnerUnverifiedError } from './daemon-errors' import type { DaemonPtyAdapter } from './daemon-pty-adapter' -import { settledWriteStub, stubWriteSettlement } from '../providers/settled-pty-write-stub' -import type { PtyBackgroundStreamEvent, PtySpawnOptions, PtySpawnResult } from '../providers/types' +import { stubWriteSettlement } from '../providers/settled-pty-write-stub' +import type { PtySpawnResult } from '../providers/types' import { AGENT_SESSION_CLAIM_DAEMON_PROTOCOL_VERSION, - AGENT_SESSION_CREATE_OPERATION_DAEMON_PROTOCOL_VERSION, - GIT_CREDENTIAL_GUARD_HOST_PROTOCOL_VERSION + AGENT_SESSION_CREATE_OPERATION_DAEMON_PROTOCOL_VERSION } from './types' import { HISTORY_SEED_TRANSFER_PROTOCOL_VERSION, PROTOCOL_VERSION, - SNAPSHOT_SERIALIZER_FIDELITY_DAEMON_PROTOCOL_VERSION, STABLE_PANE_ATTACH_ONLY_DAEMON_PROTOCOL_VERSION } from './daemon-protocol-version' -type AdapterMock = DaemonPtyAdapter & { - emitData: (id: string, data: string, sequenceChars?: number) => void - emitBackground: (event: PtyBackgroundStreamEvent) => void - emitExit: (id: string, code: number, incarnationId?: string) => void - emitIdentityChange: () => void - triggerWriteUnavailable: (id: string) => void -} - const LARGE_RECONCILE_SESSION_COUNT = 150_000 function buildSessionIds(prefix: string, count: number): string[] { @@ -34,152 +25,6 @@ function buildSessionIds(prefix: string, count: number): string[] { return ids } -function createAdapter( - label: string, - sessions: string[] = [], - reconcileResult?: { alive: string[]; killed: string[] }, - protocolVersion = GIT_CREDENTIAL_GUARD_HOST_PROTOCOL_VERSION -): AdapterMock { - const writes: { id: string; data: string }[] = [] - const dataListeners: ((payload: { id: string; data: string; sequenceChars?: number }) => void)[] = - [] - const backgroundListeners: ((payload: PtyBackgroundStreamEvent) => void)[] = [] - const writeUnavailableListeners: ((payload: { id: string }) => void)[] = [] - const exitListeners: ((payload: { id: string; code: number; incarnationId?: string }) => void)[] = - [] - const identityChangeListeners: (() => void)[] = [] - return { - protocolVersion, - supportsGitCredentialGuardHost: () => - protocolVersion >= GIT_CREDENTIAL_GUARD_HOST_PROTOCOL_VERSION, - supportsAgentSessionClaims: () => - protocolVersion >= AGENT_SESSION_CLAIM_DAEMON_PROTOCOL_VERSION, - supportsAgentSessionCreateOperations: () => - protocolVersion >= AGENT_SESSION_CREATE_OPERATION_DAEMON_PROTOCOL_VERSION, - providesAgentSessionOwnerListings: () => - protocolVersion >= AGENT_SESSION_CLAIM_DAEMON_PROTOCOL_VERSION, - canProvideAuthoritativeBufferSnapshot: () => - protocolVersion >= SNAPSHOT_SERIALIZER_FIDELITY_DAEMON_PROTOCOL_VERSION, - spawn: vi.fn(async (opts: PtySpawnOptions): Promise => { - const id = opts.sessionId ?? `${label}-new` - sessions.push(id) - return { id } - }), - listProcesses: vi.fn(async () => - sessions.map((id) => ({ - id, - cwd: '', - title: label - })) - ), - hasPty: vi.fn((id: string) => sessions.includes(id)), - probePtyLiveness: vi.fn(async (id: string) => sessions.includes(id)), - write: vi.fn((id: string, data: string) => { - writes.push({ id, data }) - }), - writeWithSettlement: vi.fn(settledWriteStub()), - resize: vi.fn(), - setPtyBackgrounded: vi.fn(), - getBufferSnapshot: vi.fn(async () => null), - shutdown: vi.fn(async (id: string) => { - const idx = sessions.indexOf(id) - if (idx !== -1) { - sessions.splice(idx, 1) - } - }), - attach: vi.fn(async () => {}), - sendSignal: vi.fn(async () => {}), - getCwd: vi.fn(async () => ''), - getInitialCwd: vi.fn(async () => ''), - clearBuffer: vi.fn(async () => {}), - acknowledgeDataEvent: vi.fn(), - hasChildProcesses: vi.fn(async () => false), - getForegroundProcess: vi.fn(async () => null), - inspectProcess: vi.fn(async () => ({ foregroundProcess: null, hasChildProcesses: false })), - confirmForegroundProcess: vi.fn(async () => `${label}-confirmed`), - serialize: vi.fn(async () => '{}'), - revive: vi.fn(async () => {}), - getDefaultShell: vi.fn(async () => '/bin/zsh'), - getProfiles: vi.fn(async () => []), - onData: vi.fn( - (callback: (payload: { id: string; data: string; sequenceChars?: number }) => void) => { - dataListeners.push(callback) - return () => { - const idx = dataListeners.indexOf(callback) - if (idx !== -1) { - dataListeners.splice(idx, 1) - } - } - } - ), - onBackgroundStreamEvent: vi.fn((callback: (payload: PtyBackgroundStreamEvent) => void) => { - backgroundListeners.push(callback) - return () => { - const idx = backgroundListeners.indexOf(callback) - if (idx !== -1) { - backgroundListeners.splice(idx, 1) - } - } - }), - onWriteUnavailable: vi.fn((callback: (payload: { id: string }) => void) => { - writeUnavailableListeners.push(callback) - return () => { - const idx = writeUnavailableListeners.indexOf(callback) - if (idx !== -1) { - writeUnavailableListeners.splice(idx, 1) - } - } - }), - onExit: vi.fn( - (callback: (payload: { id: string; code: number; incarnationId?: string }) => void) => { - exitListeners.push(callback) - return () => { - const idx = exitListeners.indexOf(callback) - if (idx !== -1) { - exitListeners.splice(idx, 1) - } - } - } - ), - onDaemonIdentityChanged: vi.fn((callback: () => void) => { - identityChangeListeners.push(callback) - return () => { - const idx = identityChangeListeners.indexOf(callback) - if (idx !== -1) { - identityChangeListeners.splice(idx, 1) - } - } - }), - ackColdRestore: vi.fn(), - clearTombstone: vi.fn(), - reconcileOnStartup: vi.fn(async () => reconcileResult ?? { alive: sessions, killed: [] }), - dispose: vi.fn(), - disconnectOnly: vi.fn(async () => {}), - emitData: (id: string, data: string, sequenceChars?: number) => { - for (const listener of dataListeners) { - listener({ id, data, ...(sequenceChars === undefined ? {} : { sequenceChars }) }) - } - }, - emitBackground: (event: PtyBackgroundStreamEvent) => { - for (const listener of backgroundListeners) { - listener(event) - } - }, - emitExit: (id: string, code: number, incarnationId?: string) => { - for (const listener of exitListeners) { - listener({ id, code, ...(incarnationId ? { incarnationId } : {}) }) - } - }, - emitIdentityChange: () => identityChangeListeners.forEach((listener) => listener()), - triggerWriteUnavailable: (id: string) => { - for (const listener of writeUnavailableListeners) { - listener({ id }) - } - }, - _writes: writes - } as unknown as AdapterMock -} - it('forwards dead-endpoint write-unavailable signals from every routed adapter', () => { // Why revert-sensitive: main subscribes on the ROUTED provider, so if the router // does not forward this the STA-2373 fan-out never reaches the renderer and only @@ -924,3 +769,50 @@ describe('DaemonPtyRouter', () => { }) }) }) + +describe('DaemonPtyRouter fresh spawn admission', () => { + it('preserves existing routes while refusing new terminals and coalesces recovery', async () => { + const current = createAdapter('current', ['current-existing']) + const legacy = createAdapter('legacy', ['legacy-existing']) + let finishProbe!: (healthy: boolean) => void + const probe = vi.fn( + () => + new Promise((resolve) => { + finishProbe = resolve + }) + ) + const router = new DaemonPtyRouter({ current, legacy: [legacy], probeFreshSpawn: probe }) + await router.discoverLegacySessions() + router.write('legacy-existing', 'hello') + router.resize('current-existing', 90, 30) + expect(legacy.write).toHaveBeenCalledWith('legacy-existing', 'hello') + expect(current.resize).toHaveBeenCalledWith('current-existing', 90, 30) + const first = router.spawn({ cols: 80, rows: 24 }) + const second = router.spawn({ cols: 80, rows: 24 }) + await Promise.resolve() + expect(probe).toHaveBeenCalledOnce() + finishProbe(false) + await expect(first).rejects.toThrow('Terminal service unavailable') + await expect(second).rejects.toThrow('Terminal service unavailable') + expect(current.spawn).not.toHaveBeenCalled() + expect(legacy.shutdown).not.toHaveBeenCalled() + expect(current.shutdown).not.toHaveBeenCalled() + await expect( + router.spawn({ cols: 80, rows: 24, sessionId: 'legacy-existing', attachOnly: true }) + ).resolves.toMatchObject({ id: 'legacy-existing' }) + expect(router.freshSpawnsUnavailable).toBe(true) + router.disposeRouterOnly() + }) + + it('admits new terminals after a successful probe without replacing the owner', async () => { + const current = createAdapter('current') + const probe = vi.fn(async () => true) + const router = new DaemonPtyRouter({ current, legacy: [], probeFreshSpawn: probe }) + await expect(router.spawn({ cols: 80, rows: 24 })).resolves.toMatchObject({ id: 'current-new' }) + await router.spawn({ cols: 80, rows: 24 }) + expect(probe).toHaveBeenCalledOnce() + expect(router.freshSpawnsUnavailable).toBe(false) + expect(current.shutdown).not.toHaveBeenCalled() + router.disposeRouterOnly() + }) +}) diff --git a/src/main/daemon/daemon-pty-router.ts b/src/main/daemon/daemon-pty-router.ts index 2fbfd17a0391..d0fe44fcd265 100644 --- a/src/main/daemon/daemon-pty-router.ts +++ b/src/main/daemon/daemon-pty-router.ts @@ -1,3 +1,5 @@ +import { DaemonFreshSpawnAdmission } from './daemon-fresh-spawn-admission' +import { terminalServiceUnavailable } from '../providers/unavailable-pty-provider' import type { DaemonPtyAdapter } from './daemon-pty-adapter' import { DaemonPtyAdapterSubscriptionFanout } from './daemon-pty-adapter-subscription-fanout' import type { @@ -21,7 +23,16 @@ export class DaemonPtyRouter implements IPtyProvider { private readonly ownerResolver: DaemonSessionOwnerResolver private readonly subscriptions: DaemonPtyAdapterSubscriptionFanout - constructor(opts: { current: DaemonPtyAdapter; legacy: DaemonPtyAdapter[] }) { + private readonly freshSpawns: DaemonFreshSpawnAdmission + + constructor(opts: { + current: DaemonPtyAdapter + legacy: DaemonPtyAdapter[] + freshSpawnAdmission?: DaemonFreshSpawnAdmission + probeFreshSpawn?: () => Promise + }) { + this.freshSpawns = + opts.freshSpawnAdmission ?? new DaemonFreshSpawnAdmission(opts.probeFreshSpawn ?? null) this.current = opts.current this.legacy = opts.legacy this.ownerResolver = new DaemonSessionOwnerResolver(this.allAdapters(), this.sessionAdapters) @@ -38,12 +49,21 @@ export class DaemonPtyRouter implements IPtyProvider { await this.ownerResolver.discoverRoutes() } + get freshSpawnsUnavailable(): boolean { + return this.freshSpawns.unavailable + } + + recoverFreshSpawnRouting = (force = false): Promise => this.freshSpawns.recover(force) + async spawn(opts: PtySpawnOptions): Promise { if (opts.attachOnly && opts.sessionId) { return await this.ownerResolver.spawnAttachOnly({ ...opts, sessionId: opts.sessionId }) } const adapter = opts.sessionId ? this.sessionAdapters.get(opts.sessionId) : undefined const target = adapter ?? this.current + if (!adapter && this.freshSpawnsUnavailable && !(await this.recoverFreshSpawnRouting())) { + throw terminalServiceUnavailable() + } const result = await target.spawn(opts) // Why: the adapter filters intentional recovery exits and canonical-ID races before publishing proof. if (!result.exitedBeforeSpawnReply) { @@ -309,6 +329,17 @@ export class DaemonPtyRouter implements IPtyProvider { // (pre-#1323) the legacy list is set once at construction and never mutated, // so returning the internal array by reference is safe for the intended // read-only use. + getCurrentDaemonSessionIds(): string[] { + return [...this.sessionAdapters].filter(([, owner]) => owner === this.current).map(([id]) => id) + } + + fanoutCurrentDaemonSyntheticExits(code: number): void { + for (const id of this.getCurrentDaemonSessionIds()) { + this.ownerResolver.forgetRoute(id) + this.subscriptions.publishExit({ id, code }) + } + } + getCurrentAdapter(): DaemonPtyAdapter { return this.current } diff --git a/src/main/daemon/daemon-pty-runtime-state.ts b/src/main/daemon/daemon-pty-runtime-state.ts index 38480af1489c..aa84ec1081ba 100644 --- a/src/main/daemon/daemon-pty-runtime-state.ts +++ b/src/main/daemon/daemon-pty-runtime-state.ts @@ -1,4 +1,5 @@ -import { DaemonClient } from './client' +import { DaemonFreshSpawnAdmission } from './daemon-fresh-spawn-admission' +import { DaemonClient, type DaemonClientOptions } from './client' import { createDaemonAuditEligibilityTracker } from './daemon-audit-eligibility-event' import type { DaemonAuditContext, @@ -48,15 +49,26 @@ export type SnapshotCheckpointResult = { snapshot: NonNullable | null } +export type GuestDaemonExecution = { + distro: string + transport: NonNullable + defaultShell: string + defaultCwd: string + profiles: readonly { name: string; path: string }[] + admitIdentity?: (identity: DaemonEndpointIdentity | null) => Promise +} + export type DaemonPtyAdapterOptions = { - socketPath: string - tokenPath: string + socketPath?: string + tokenPath?: string pidPath?: string + guest?: GuestDaemonExecution profileScope?: string protocolVersion?: number historyPath?: string runtimeDir?: string packagedAppVersion?: string | null + freshSpawnAdmission?: DaemonFreshSpawnAdmission respawn?: (reason: DaemonRespawnReason) => Promise void)> } @@ -73,6 +85,7 @@ export type DaemonIdentityChangeEvent = { export abstract class DaemonPtyRuntimeState { readonly protocolVersion: number + protected readonly guest: GuestDaemonExecution | null protected socketPath: string protected tokenPath: string protected pidPath: string | null @@ -87,6 +100,7 @@ export abstract class DaemonPtyRuntimeState { protected identityChangeListeners: ((event: DaemonIdentityChangeEvent) => void)[] = [] protected historyManager: HistoryManager | null protected historyReader: HistoryReader | null + protected readonly freshSpawnAdmission: DaemonFreshSpawnAdmission protected respawnFn: DaemonPtyAdapterOptions['respawn'] | null protected runtimeDir: string | null protected packagedAppVersion: string | null @@ -149,6 +163,7 @@ export abstract class DaemonPtyRuntimeState { additionalEvidenceSources?: readonly DaemonEvidenceSource[], endpointGoneProof?: 'windows_named_pipe_missing' ): void + protected abstract notifyActiveSessionsWriteUnavailable(): void protected abstract clearSessionAwaitingDaemonRecovery(sessionId: string): void protected abstract stopCheckpointTimerIfIdle(): void @@ -190,27 +205,43 @@ export abstract class DaemonPtyRuntimeState { constructor(opts: DaemonPtyAdapterOptions) { this.protocolVersion = opts.protocolVersion ?? PROTOCOL_VERSION - this.socketPath = opts.socketPath - this.tokenPath = opts.tokenPath - this.pidPath = opts.pidPath ?? null + this.guest = opts.guest ?? null + if ( + this.guest && + (opts.socketPath || + opts.tokenPath || + opts.pidPath || + opts.runtimeDir || + opts.packagedAppVersion) + ) { + throw new Error('Guest daemon execution cannot use desktop endpoint or runtime paths') + } + if (!this.guest && (!opts.socketPath || !opts.tokenPath)) { + throw new Error('Local daemon execution requires socket and token paths') + } + this.socketPath = opts.socketPath ?? '' + this.tokenPath = opts.tokenPath ?? '' + this.pidPath = this.guest ? null : (opts.pidPath ?? null) this.pidRecord = readDaemonPidRecord(this.pidPath) this.auditContext = { protocolGeneration: this.protocolVersion, provider: 'local-daemon', - endpoint: opts.socketPath, - tokenPath: opts.tokenPath, + endpoint: this.socketPath, + tokenPath: this.tokenPath, endpointKind: process.platform === 'win32' ? 'windows-named-pipe' : 'unix-socket', profileScope: opts.profileScope ?? '' } this.client = new DaemonClient({ - socketPath: opts.socketPath, - tokenPath: opts.tokenPath, + ...(this.guest + ? { transport: this.guest.transport, admitIdentity: this.guest.admitIdentity } + : { socketPath: this.socketPath, tokenPath: this.tokenPath }), protocolVersion: opts.protocolVersion }) this.historyManager = opts.historyPath ? new HistoryManager(opts.historyPath) : null this.historyReader = opts.historyPath ? new HistoryReader(opts.historyPath) : null + this.freshSpawnAdmission = opts.freshSpawnAdmission ?? new DaemonFreshSpawnAdmission(null) this.respawnFn = opts.respawn ?? null - this.runtimeDir = opts.runtimeDir ?? opts.profileScope ?? null + this.runtimeDir = this.guest ? null : (opts.runtimeDir ?? opts.profileScope ?? null) this.packagedAppVersion = opts.packagedAppVersion ?? null this.supportsCheckpoints = this.protocolVersion >= 4 this.supportsIncrementalCheckpoints = this.protocolVersion >= 13 @@ -230,6 +261,9 @@ export abstract class DaemonPtyRuntimeState { } this.pausedProducerSessionIds.clear() this.observeAuditFailure('transport_closed') + if (this.guest && !this.respawnAdoptionClosed) { + this.notifyActiveSessionsWriteUnavailable() + } }) } diff --git a/src/main/daemon/daemon-pty-session-control.ts b/src/main/daemon/daemon-pty-session-control.ts index 610b2e1407ca..0b683a32a245 100644 --- a/src/main/daemon/daemon-pty-session-control.ts +++ b/src/main/daemon/daemon-pty-session-control.ts @@ -142,7 +142,9 @@ export abstract class DaemonPtySessionControl extends DaemonPtySessionInput { cwd: normalizeWslColdRestoreCwd({ recoveredCwd: detected.cwd, - requestedCwd: this.initialCwds.get(id) ?? resolveSafePtyDefaultCwd(), + requestedCwd: + this.initialCwds.get(id) ?? this.guest?.defaultCwd ?? resolveSafePtyDefaultCwd(), + guestExecution: this.guest !== null, wslDistro }) ?? '' } diff --git a/src/main/daemon/daemon-pty-session-inventory.ts b/src/main/daemon/daemon-pty-session-inventory.ts index b08c34830b4a..f1afe1705849 100644 --- a/src/main/daemon/daemon-pty-session-inventory.ts +++ b/src/main/daemon/daemon-pty-session-inventory.ts @@ -165,6 +165,9 @@ export abstract class DaemonPtySessionInventory extends DaemonPtyProcessInspecti } async getDefaultShell(): Promise { + if (this.guest) { + return this.guest.defaultShell + } if (process.platform === 'win32') { return process.env.COMSPEC || 'powershell.exe' } @@ -172,6 +175,9 @@ export abstract class DaemonPtySessionInventory extends DaemonPtyProcessInspecti } async getProfiles(): Promise<{ name: string; path: string }[]> { + if (this.guest) { + return this.guest.profiles.map((profile) => ({ ...profile })) + } if (process.platform === 'win32') { return [ { name: 'PowerShell', path: 'powershell.exe' }, diff --git a/src/main/daemon/daemon-pty-session-spawn.ts b/src/main/daemon/daemon-pty-session-spawn.ts index 388919dd2e30..e1f87ff61a04 100644 --- a/src/main/daemon/daemon-pty-session-spawn.ts +++ b/src/main/daemon/daemon-pty-session-spawn.ts @@ -1,3 +1,4 @@ +import { admitDaemonSpawn } from './daemon-fresh-spawn-admission' import { recognizeAgentProcessFromCommandLine } from '../../shared/agent-process-recognition' import { shouldUseShellReadyStartupDelivery } from '../../shared/codex-startup-delivery' import { CODEX_SHELL_READY_TIMEOUT_MS } from './session-shell-ready-barrier' @@ -17,9 +18,10 @@ import { shellReadyMarkerComesFromLineEditor } from '../../shared/shell-ready-ma import { getRecoveredHistorySeedSegments } from './terminal-history-seed-segments' import { AGENT_SESSION_CLAIM_DAEMON_PROTOCOL_VERSION, type CreateOrAttachResult } from './types' import { normalizeWslColdRestoreCwd } from './wsl-cold-restore-cwd' +import { guestDaemonSpawnOptions } from './daemon-guest-spawn-options' import { resolveWslSessionContext } from './wsl-session-context' import { resolveSafePtyDefaultCwd } from '../providers/pty-default-cwd' -import { resolveUnixShellPath } from '../providers/local-pty-utils' +import { resolveUnixShellPath } from '../providers/pty-spawn-validation' import type { PtySpawnOptions, PtySpawnResult } from '../providers/types' import { injectHistoryEnv, injectWslFishHistoryEnv, logHistoryInjection } from '../terminal-history' import { addWslEnvKeys } from '../wsl-env' @@ -63,6 +65,9 @@ export abstract class DaemonPtySessionSpawn extends DaemonPtySpawnResult { } protected withHistoryIsolation(opts: PtySpawnOptions): PtySpawnOptions { + if (this.guest) { + return guestDaemonSpawnOptions(this.guest, opts) + } const wslContext = resolveWslSessionContext({ cwd: opts.cwd, sessionId: opts.sessionId, @@ -113,16 +118,18 @@ export abstract class DaemonPtySessionSpawn extends DaemonPtySpawnResult { } const requestedSessionId = opts.sessionId! // Why: v30 daemons survive upgrades; reject their accidental create result before publication. - const attachOnly = opts.attachOnly === true + let attachOnly = opts.attachOnly === true const emulateLegacyAttachOnly = attachOnly && this.protocolVersion < STABLE_PANE_ATTACH_ONLY_DAEMON_PROTOCOL_VERSION let sessionId = requestedSessionId - let wslDistro = resolveWslSessionContext({ - cwd: opts.cwd, - sessionId, - shellOverride: opts.shellOverride, - terminalWindowsWslDistro: opts.terminalWindowsWslDistro - })?.distro + let wslDistro = + this.guest?.distro ?? + resolveWslSessionContext({ + cwd: opts.cwd, + sessionId, + shellOverride: opts.shellOverride, + terminalWindowsWslDistro: opts.terminalWindowsWslDistro + })?.distro let activeSpawnContext: DaemonPtySpawnContext | null = null const freezeHistory = async (): Promise => { if (!this.historyManager) { @@ -167,8 +174,9 @@ export abstract class DaemonPtySessionSpawn extends DaemonPtySpawnResult { cwd: normalizeWslColdRestoreCwd({ recoveredCwd: restoreInfo.cwd, - requestedCwd: opts.cwd ?? resolveSafePtyDefaultCwd(), - wslDistro: recoveryWslDistro + requestedCwd: opts.cwd ?? this.guest?.defaultCwd ?? resolveSafePtyDefaultCwd(), + wslDistro: recoveryWslDistro, + guestExecution: this.guest !== null }) ?? '' } } @@ -184,6 +192,13 @@ export abstract class DaemonPtySessionSpawn extends DaemonPtySpawnResult { } await this.ensureConnected() + opts = await admitDaemonSpawn( + this.freshSpawnAdmission, + opts, + this.protocolVersion, + async () => (await this.getAppliedSize(sessionId)) !== null + ) + attachOnly = opts.attachOnly === true // Why before createOrAttach: a preserved daemon may still think this session is backgrounded — from // a v19 that thins without a recoverable seq, or (#9993) from a pre-v29 that a previous desktop // handed 2031 scan authority to and can never retract it. Clear it before any bytes are attached. @@ -213,11 +228,13 @@ export abstract class DaemonPtySessionSpawn extends DaemonPtySpawnResult { let effectiveCols = restoreInfo?.cols ?? opts.cols let effectiveRows = restoreInfo?.rows ?? opts.rows - const effectiveShellPath = - process.platform !== 'win32' && opts.command + const effectiveShellPath = this.guest + ? opts.shellOverride || this.guest.defaultShell + : process.platform !== 'win32' && opts.command ? resolveUnixShellPath(opts.shellOverride || resolvePtyShellPath(opts.env ?? {})) : '' - const shellReadySupported = shellPathSupportsPtyStartupBarrier(effectiveShellPath) + const shellReadySupported = + Boolean(opts.command) && shellPathSupportsPtyStartupBarrier(effectiveShellPath) const immediateMarker = shellReadyMarkerComesFromLineEditor(effectiveShellPath) const shellReadyTimeoutMs = shellReadySupported && @@ -252,7 +269,7 @@ export abstract class DaemonPtySessionSpawn extends DaemonPtySpawnResult { } activeSpawnContext = context const result = await this.createOrAttachSpawn(context, context.historySeedSegments) - if (result.isNew && !attachOnly) { + if (result.isNew && !attachOnly && !this.guest) { // Not awaited: the app-side read behind it can sit on an unanswered macOS folder prompt. void reportDaemonPtyCwdVerdict({ cwd: effectiveCwd, @@ -288,17 +305,12 @@ export abstract class DaemonPtySessionSpawn extends DaemonPtySpawnResult { } this.clearExitedSessionState(sessionId, matchingExit.code, result.incarnationId) // Why: stream exit can beat the control reply or post-reply recovery work; return proof without republishing dead state. - const exitedResult: PtySpawnResult = { + return { id: sessionId, exitedBeforeSpawnReply: true, ...(result.incarnationId ? { incarnationId: result.incarnationId } : {}), ...(result.agentSessionEnsure ? { agentSessionEnsure: result.agentSessionEnsure } : {}), ...(!result.isNew ? { isReattach: true } : {}) } - return exitedResult - } - - didExitBeforeSpawnReply(result: PtySpawnResult): boolean { - return result.exitedBeforeSpawnReply === true } } diff --git a/src/main/daemon/daemon-pty-spawn-result.ts b/src/main/daemon/daemon-pty-spawn-result.ts index 5e799230fa1f..345805bc2082 100644 --- a/src/main/daemon/daemon-pty-spawn-result.ts +++ b/src/main/daemon/daemon-pty-spawn-result.ts @@ -82,7 +82,8 @@ export abstract class DaemonPtySpawnResult extends DaemonPtySpawnRequest { result.agentSessionEnsure ? { agentSessionEnsure: result.agentSessionEnsure } : {} const incarnationResult = (): Pick | Record => result.incarnationId ? { incarnationId: result.incarnationId } : {} - let providerWslDistro = result.wslDistro === undefined ? wslDistro : result.wslDistro + let providerWslDistro = + this.guest?.distro ?? (result.wslDistro === undefined ? wslDistro : result.wslDistro) // Why: explicit null from a current daemon overrides the caller's WSL preference; undefined keeps compatibility with older daemons. wslDistro = providerWslDistro ?? undefined context.wslDistro = wslDistro @@ -151,7 +152,8 @@ export abstract class DaemonPtySpawnResult extends DaemonPtySpawnRequest { if (result.incarnationId) { this.sessionIncarnations.set(sessionId, result.incarnationId) } - providerWslDistro = result.wslDistro === undefined ? wslDistro : result.wslDistro + providerWslDistro = + this.guest?.distro ?? (result.wslDistro === undefined ? wslDistro : result.wslDistro) wslDistro = providerWslDistro ?? undefined context.wslDistro = wslDistro if (wslDistro) { @@ -288,7 +290,6 @@ export abstract class DaemonPtySpawnResult extends DaemonPtySpawnRequest { const isAltScreen = reattachSnapshot.modes.alternateScreen const snapshotPrefix = reattachSnapshot.scrollbackAnsi + reattachSnapshot.rehydrateSequences const snapshotFrame = reattachSnapshot.snapshotAnsi - const snapshotPayload = snapshotPrefix + snapshotFrame // Why kitty flags ride beside the payload, not inside it: the snapshot reaches renderer xterms where POST_REPLAY_REATTACH_RESET's kitty reset must win (terminal-query-authority.md §kitty). // Why known `0` is no longer dropped: the pane tracker must be able to tell // "the app negotiated nothing" from "this reattach proved nothing". @@ -302,7 +303,7 @@ export abstract class DaemonPtySpawnResult extends DaemonPtySpawnRequest { ...claimResult(), ...launchIdentity(), ...(providerWslDistro !== undefined ? { wslDistro: providerWslDistro } : {}), - snapshot: snapshotPayload, + snapshot: snapshotPrefix + snapshotFrame, snapshotCols: reattachSnapshot.cols, snapshotRows: reattachSnapshot.rows, // Why only for an alt frame: normal history remains safe to replay at its capture grid. diff --git a/src/main/daemon/daemon-pty-startup-delivery.test.ts b/src/main/daemon/daemon-pty-startup-delivery.test.ts index 4b72070bb425..60c808cc38a2 100644 --- a/src/main/daemon/daemon-pty-startup-delivery.test.ts +++ b/src/main/daemon/daemon-pty-startup-delivery.test.ts @@ -1,7 +1,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { rmSync } from 'node:fs' import { basename, join } from 'node:path' -import * as localPtyUtils from '../providers/local-pty-utils' +import * as localPtyUtils from '../providers/pty-spawn-validation' import { createMockSubprocess, startDaemonAdapterHarness, diff --git a/src/main/daemon/daemon-replacement-preflight.ts b/src/main/daemon/daemon-replacement-preflight.ts index 1573755efef8..b1424db1611a 100644 --- a/src/main/daemon/daemon-replacement-preflight.ts +++ b/src/main/daemon/daemon-replacement-preflight.ts @@ -23,7 +23,7 @@ import { PROTOCOL_VERSION } from './types' // would spin hot for the whole budget. export const WEDGED_DAEMON_GRACE_RETRIES = 11 -type PreserveDaemon = (mode?: 'degraded-new-pty-fallback') => Promise +type PreserveDaemon = (mode?: 'fresh-spawns-unavailable') => Promise type ReplacementPreflightOptions = { runtimeDir: string @@ -33,6 +33,8 @@ type ReplacementPreflightOptions = { /** Absolute deadline for the whole adopt-or-replace decision; see DAEMON_RECOVERY_BUDGET_MS. */ recoveryDeadlineMs: number attributedReason: DaemonReplaceReason | null + /** False only when this runtime needs no artifact preparation. */ + prepareReplacementRuntime: () => Promise releaseAdoptionClient: () => void preserveDaemon: PreserveDaemon launchNonce: string @@ -48,6 +50,7 @@ export async function prepareDaemonReplacement( entryPath, recoveryDeadlineMs, attributedReason, + prepareReplacementRuntime, releaseAdoptionClient, preserveDaemon, launchNonce @@ -59,6 +62,7 @@ export async function prepareDaemonReplacement( } | undefined let confirmedReplacement = false + let cleanupProtocol = false const health = await checkDaemonHealth(socketPath, tokenPath) if (health === 'healthy') { const pidRecord = readDaemonPidRecord(getDaemonPidPath(runtimeDir)) @@ -87,7 +91,7 @@ export async function prepareDaemonReplacement( reason: 'unhealthy_resolver', liveSessionCount } - confirmedReplacement = (await cleanupDaemonForProtocol(runtimeDir, PROTOCOL_VERSION)).cleaned + cleanupProtocol = true } else { // Why: a protocol-healthy daemon can outlive its launching app bundle (dev worktree rebuild, or packaged update replacing the app path). const identity = await getDaemonLaunchIdentity(runtimeDir, socketPath, tokenPath, entryPath) @@ -124,8 +128,7 @@ export async function prepareDaemonReplacement( reason: stalePackagedBundle ? 'stale_bundle' : 'different_app_path', liveSessionCount: 0 } - confirmedReplacement = (await cleanupDaemonForProtocol(runtimeDir, PROTOCOL_VERSION)) - .cleaned + cleanupProtocol = true } else { const attributionHealth = await getMacDaemonTccAttributionHealth( runtimeDir, @@ -145,8 +148,7 @@ export async function prepareDaemonReplacement( '[daemon] Replacing daemon whose macOS TCC attribution is severed (spawning app binary no longer exists)' ) pendingReplacement = { reason: 'severed_tcc_attribution', liveSessionCount } - confirmedReplacement = (await cleanupDaemonForProtocol(runtimeDir, PROTOCOL_VERSION)) - .cleaned + cleanupProtocol = true } else { return preserveDaemon() } @@ -183,9 +185,9 @@ export async function prepareDaemonReplacement( if (liveSessionCount !== null && liveSessionCount > 0) { if (health === 'pty-spawn-unhealthy') { console.warn( - `[daemon] DEGRADED MODE: preserving daemon that failed the PTY spawn health check because it owns ${liveSessionCount} live session${liveSessionCount === 1 ? '' : 's'}. Existing sessions keep working; fresh terminals run on the local provider WITHOUT daemon persistence until you restart the daemon (Manage Sessions → Restart).` + `[daemon] DEGRADED MODE: preserving daemon that failed the PTY spawn health check because it owns ${liveSessionCount} live session${liveSessionCount === 1 ? '' : 's'}. Existing sessions keep their owner; new terminals require a successful terminal-service health check. Retry after the service recovers.` ) - return preserveDaemon('degraded-new-pty-fallback') + return preserveDaemon('fresh-spawns-unavailable') } console.warn( `[daemon] Preserving daemon that failed the health check because it owns ${liveSessionCount} live session${liveSessionCount === 1 ? '' : 's'}` @@ -211,6 +213,27 @@ export async function prepareDaemonReplacement( } } + // Adoption needs no new artifacts; replacement must be ready before stopping its owner. + const preparedRuntime = await prepareReplacementRuntime() + if (preparedRuntime) { + // Artifact preparation invalidates the earlier count; an expired budget gives no new proof. + const currentSessionCount = + Date.now() < recoveryDeadlineMs + ? await getAliveDaemonSessionCount(socketPath, tokenPath, recoveryDeadlineMs) + : null + if ( + (currentSessionCount !== null && currentSessionCount > 0) || + (cleanupProtocol && currentSessionCount === null) + ) { + return preserveDaemon( + health === 'pty-spawn-unhealthy' ? 'fresh-spawns-unavailable' : undefined + ) + } + } + if (cleanupProtocol) { + confirmedReplacement = (await cleanupDaemonForProtocol(runtimeDir, PROTOCOL_VERSION)).cleaned + } + // Why: a raw socket can outlive a broken daemon; kill by PID before respawn so the new daemon doesn't race the stale one. releaseAdoptionClient() const killOutcome = await killStaleDaemon(runtimeDir, socketPath, tokenPath) @@ -220,10 +243,10 @@ export async function prepareDaemonReplacement( // no daemon at all, and we have just proved something still answers the endpoint — // so adopt it in degraded mode: existing sessions keep working, new PTYs run locally. console.warn( - '[daemon] DEGRADED MODE: adopting a daemon that could not be confirmed stopped. Existing sessions keep working; fresh terminals run on the local provider WITHOUT daemon persistence until you restart the daemon (Manage Sessions → Restart).' + '[daemon] DEGRADED MODE: adopting a daemon that could not be confirmed stopped. Existing sessions keep their owner; new terminals require a successful terminal-service health check. Retry after the service recovers.' ) try { - return await preserveDaemon('degraded-new-pty-fallback') + return await preserveDaemon('fresh-spawns-unavailable') } catch { // It died between the probe and the adoption; the endpoint is genuinely free now. throw new DaemonEndpointOwnershipError( diff --git a/src/main/daemon/daemon-replacement-runtime-readiness.test.ts b/src/main/daemon/daemon-replacement-runtime-readiness.test.ts new file mode 100644 index 000000000000..5eaa6dc2aa6e --- /dev/null +++ b/src/main/daemon/daemon-replacement-runtime-readiness.test.ts @@ -0,0 +1,132 @@ +import { beforeEach, expect, it, vi } from 'vitest' +const mocks = vi.hoisted(() => ({ + health: vi.fn(), + resolver: vi.fn(), + sessions: vi.fn(), + identity: vi.fn(), + attribution: vi.fn(), + cleanup: vi.fn(), + kill: vi.fn() +})) +vi.mock('../../shared/app-environment', () => ({ + getAppEnvironment: () => ({ isPackaged: () => false }) +})) +vi.mock('./daemon-health', () => ({ + checkDaemonHealth: mocks.health, + getMacDaemonSystemResolverHealth: mocks.resolver +})) +vi.mock('./daemon-launch-paths', () => ({ + getAliveDaemonSessionCount: mocks.sessions, + probeDaemonSocket: async () => false, + DAEMON_SOCKET_PROBE_TIMEOUT_MS: 10 +})) +vi.mock('./daemon-pid-identity', () => ({ getDaemonLaunchIdentity: mocks.identity })) +vi.mock('./daemon-endpoint-incarnation', () => ({ readDaemonPidRecord: () => null })) +vi.mock('./daemon-tcc-attribution', () => ({ getMacDaemonTccAttributionHealth: mocks.attribution })) +vi.mock('./daemon-protocol-cleanup', () => ({ cleanupDaemonForProtocol: mocks.cleanup })) +vi.mock('./daemon-stale-kill', () => ({ killStaleDaemon: mocks.kill })) +vi.mock('./daemon-lifecycle-event', () => ({ trackDaemonReplaced: vi.fn() })) +import { prepareDaemonReplacement } from './daemon-replacement-preflight' +function options() { + return { + runtimeDir: '/runtime', + socketPath: '/socket', + tokenPath: '/token', + entryPath: '/entry', + recoveryDeadlineMs: Date.now() + 1000, + attributedReason: null, + launchNonce: 'nonce', + prepareReplacementRuntime: vi.fn(async () => true), + releaseAdoptionClient: vi.fn(), + preserveDaemon: vi.fn(async () => ({ adopted: true as const, shutdown: async () => {} })) + } +} +beforeEach(() => { + vi.resetAllMocks() + mocks.health.mockResolvedValue('healthy') + mocks.resolver.mockResolvedValue('healthy') + mocks.sessions.mockResolvedValue(0) + mocks.identity.mockResolvedValue('match') + mocks.attribution.mockResolvedValue('healthy') + mocks.cleanup.mockResolvedValue({ cleaned: true }) + mocks.kill.mockResolvedValue({ killed: true, liveOwnerSurvived: false }) +}) +it.each(['resolver', 'identity', 'attribution', 'unhealthy'])( + 'does not stop an incumbent after runtime preparation fails (%s)', + async (reason) => { + if (reason === 'resolver') { + mocks.resolver.mockResolvedValue('unhealthy') + } + if (reason === 'identity') { + mocks.identity.mockResolvedValue('mismatch') + } + if (reason === 'attribution') { + mocks.attribution.mockResolvedValue('severed') + } + if (reason === 'unhealthy') { + mocks.health.mockResolvedValue('rejected') + } + const args = options() + args.prepareReplacementRuntime.mockRejectedValue(new Error('runtime unavailable')) + await expect(prepareDaemonReplacement(args)).rejects.toThrow('runtime unavailable') + expect(mocks.cleanup).not.toHaveBeenCalled() + expect(mocks.kill).not.toHaveBeenCalled() + expect(args.releaseAdoptionClient).not.toHaveBeenCalled() + } +) +it('keeps healthy adoption independent of new runtime files', async () => { + const args = options() + args.prepareReplacementRuntime.mockRejectedValue(new Error('missing runtime')) + expect(await prepareDaemonReplacement(args)).toMatchObject({ adopted: true }) + expect(args.prepareReplacementRuntime).not.toHaveBeenCalled() +}) +it('prepares once before protocol cleanup and stale process termination', async () => { + mocks.identity.mockResolvedValue('mismatch') + const args = options() + await prepareDaemonReplacement(args) + expect(args.prepareReplacementRuntime).toHaveBeenCalledTimes(1) + expect(args.prepareReplacementRuntime.mock.invocationCallOrder[0]).toBeLessThan( + mocks.cleanup.mock.invocationCallOrder[0] + ) + expect(mocks.cleanup.mock.invocationCallOrder[0]).toBeLessThan( + mocks.kill.mock.invocationCallOrder[0] + ) +}) + +it.each([1, null])( + 'preserves an incumbent when preparation changes session evidence to %s', + async (count) => { + mocks.identity.mockResolvedValue('mismatch') + mocks.sessions.mockResolvedValueOnce(0).mockResolvedValue(count) + expect(await prepareDaemonReplacement(options())).toMatchObject({ adopted: true }) + expect(mocks.cleanup).not.toHaveBeenCalled() + expect(mocks.kill).not.toHaveBeenCalled() + } +) + +it('retains the prior evidence without a redundant probe when no runtime is materialized', async () => { + mocks.identity.mockResolvedValue('mismatch') + const args = options() + args.prepareReplacementRuntime.mockResolvedValue(false) + await prepareDaemonReplacement(args) + expect(mocks.sessions).toHaveBeenCalledOnce() + expect(mocks.kill).toHaveBeenCalledOnce() +}) +it('does not extend an exhausted budget or treat pre-copy emptiness as current proof', async () => { + mocks.identity.mockResolvedValue('mismatch') + const args = options() + const clock = vi.spyOn(Date, 'now') + clock.mockReturnValue(args.recoveryDeadlineMs - 100) + args.prepareReplacementRuntime.mockImplementation(async () => { + clock.mockReturnValue(args.recoveryDeadlineMs) + return true + }) + try { + expect(await prepareDaemonReplacement(args)).toMatchObject({ adopted: true }) + expect(mocks.sessions).toHaveBeenCalledOnce() + expect(mocks.cleanup).not.toHaveBeenCalled() + expect(mocks.kill).not.toHaveBeenCalled() + } finally { + clock.mockRestore() + } +}) diff --git a/src/main/daemon/daemon-request-router-transient.test.ts b/src/main/daemon/daemon-request-router-transient.test.ts new file mode 100644 index 000000000000..524cad5a5cb5 --- /dev/null +++ b/src/main/daemon/daemon-request-router-transient.test.ts @@ -0,0 +1,25 @@ +import { expect, it, vi } from 'vitest' +import { DaemonRequestRouter } from './daemon-request-router' + +it('refuses fresh transient probes during retirement but permits owner cleanup', async () => { + const transientRoute = vi.fn(() => ({})) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Only transient routing is exercised, using these three dependencies exclusively. + const options = { + connections: { get: () => ({ authenticatedPairEstablished: true, streamSocket: {} }) }, + lifecycle: { isAcceptingWork: () => false }, + transientPtys: { route: transientRoute } + } as unknown as ConstructorParameters[0] + const router = new DaemonRequestRouter(options) + await expect( + router.route('owner', { + id: 'create', + type: 'createTransientPty', + payload: { id: 'probe', file: '/tool', args: [], env: {}, cwd: '/tmp', cols: 80, rows: 24 } + }) + ).rejects.toThrow('shutting down') + expect(transientRoute).not.toHaveBeenCalled() + await expect( + router.route('owner', { id: 'close', type: 'closeTransientPty', payload: { id: 'probe' } }) + ).resolves.toEqual({}) + expect(transientRoute).toHaveBeenCalledOnce() +}) diff --git a/src/main/daemon/daemon-request-router.ts b/src/main/daemon/daemon-request-router.ts index ae007366dd02..fe91958c0f3d 100644 --- a/src/main/daemon/daemon-request-router.ts +++ b/src/main/daemon/daemon-request-router.ts @@ -1,3 +1,4 @@ +import type { DaemonTransientPtys } from './daemon-transient-pty' import { performance } from 'node:perf_hooks' import { readCurrentProcessMacSystemResolverHealth } from '../network/macos-system-resolver-health' import type { ConnectedDaemonClient, DaemonClientConnections } from './daemon-client-connections' @@ -14,6 +15,7 @@ import type { TerminalHost } from './terminal-host' import { SessionNotFoundError, type DaemonRequest } from './types' type DaemonRequestRouterOptions = { + transientPtys: DaemonTransientPtys host: TerminalHost connections: DaemonClientConnections lifecycle: DaemonServerLifecycle @@ -32,9 +34,17 @@ export class DaemonRequestRouter { async route(clientId: string, request: DaemonRequest): Promise { const client = this.options.connections.get(clientId) + const paired = Boolean(client?.authenticatedPairEstablished && client.streamSocket) switch (request.type) { + case 'createTransientPty': + case 'writeTransientPty': + case 'closeTransientPty': + if (request.type === 'createTransientPty' && !this.options.lifecycle.isAcceptingWork()) { + throw new Error('Terminal service is shutting down') + } + return this.options.transientPtys.route(clientId, request, paired) case 'startHistorySeedTransfer': { - if (!client?.authenticatedPairEstablished || client.streamSocket === null) { + if (!paired) { throw new Error('Daemon client connection is incomplete; reconnect') } return { @@ -144,7 +154,7 @@ export class DaemonRequestRouter { { teardownSnapshot: request.payload.teardownSnapshot === true } ) case 'ping': - return { pong: true } + return this.options.transientPtys.ping() case 'systemResolverHealth': return { health: await readCurrentProcessMacSystemResolverHealth() } case 'ptySpawnHealth': diff --git a/src/main/daemon/daemon-respawn-spawn-admission.test.ts b/src/main/daemon/daemon-respawn-spawn-admission.test.ts new file mode 100644 index 000000000000..95a56158eea0 --- /dev/null +++ b/src/main/daemon/daemon-respawn-spawn-admission.test.ts @@ -0,0 +1,105 @@ +import './mock-descendant-sweep' +import { rmSync } from 'node:fs' +import { afterEach, expect, it, vi } from 'vitest' +import { DaemonFreshSpawnAdmission } from './daemon-fresh-spawn-admission' +import { DaemonPtyAdapter } from './daemon-pty-adapter' +import { DaemonServer } from './daemon-server' +import { createMockSubprocess, startDaemonAdapterHarness } from './daemon-pty-adapter-test-harness' + +const cleanups: (() => Promise | void)[] = [] +afterEach(async () => { + for (const cleanup of cleanups.splice(0).toReversed()) { + await cleanup() + } +}) + +it('checks the newly degraded admission after a spawn reconnects through respawn', async () => { + const spawn = vi.fn(() => createMockSubprocess()) + const harness = await startDaemonAdapterHarness(spawn) + cleanups.push( + () => rmSync(harness.dir, { recursive: true, force: true }), + () => harness.server.shutdown(), + () => harness.adapter.dispose() + ) + const admission = new DaemonFreshSpawnAdmission(null) + const healthy = vi.fn(async () => false) + const adapter = new DaemonPtyAdapter({ + socketPath: harness.socketPath, + tokenPath: harness.tokenPath, + freshSpawnAdmission: admission, + respawn: async () => { + const replacement = new DaemonServer({ + socketPath: harness.socketPath, + tokenPath: harness.tokenPath, + spawnSubprocess: spawn + }) + await replacement.start() + cleanups.push(() => replacement.shutdown()) + admission.reset(healthy) + } + }) + cleanups.push(() => adapter.dispose()) + await adapter.spawn({ cols: 80, rows: 24 }) + await harness.server.shutdown() + await expect(adapter.spawn({ cols: 80, rows: 24 })).rejects.toThrow( + 'Terminal service unavailable' + ) + expect(spawn).toHaveBeenCalledOnce() + healthy.mockResolvedValue(true) + await expect(adapter.recoverFreshSpawnRouting(true)).resolves.toBe(true) + await adapter.spawn({ cols: 80, rows: 24 }) + expect(spawn).toHaveBeenCalledTimes(2) +}) + +it('allows only retained attach while degraded, never cold-creating a missing ID', async () => { + const spawn = vi.fn(() => createMockSubprocess()) + const harness = await startDaemonAdapterHarness(spawn) + cleanups.push( + () => rmSync(harness.dir, { recursive: true, force: true }), + () => harness.server.shutdown(), + () => harness.adapter.dispose() + ) + const retained = await harness.adapter.spawn({ cols: 80, rows: 24 }) + const admission = new DaemonFreshSpawnAdmission(async () => false) + const adapter = new DaemonPtyAdapter({ + socketPath: harness.socketPath, + tokenPath: harness.tokenPath, + freshSpawnAdmission: admission + }) + cleanups.push(() => adapter.dispose()) + await expect( + adapter.spawn({ sessionId: retained.id, cols: 80, rows: 24 }) + ).resolves.toMatchObject({ id: retained.id, isReattach: true }) + await expect( + adapter.spawn({ sessionId: retained.id, attachOnly: true, cols: 80, rows: 24 }) + ).resolves.toMatchObject({ id: retained.id, isReattach: true }) + await expect(adapter.spawn({ sessionId: 'missing', cols: 80, rows: 24 })).rejects.toThrow( + 'Terminal service unavailable' + ) + const getSize = adapter.getAppliedSize.bind(adapter) + vi.spyOn(adapter, 'getAppliedSize').mockImplementationOnce(async (id) => { + const size = await getSize(id) + await harness.adapter.shutdown(id, { immediate: true }) + return size + }) + await expect(adapter.spawn({ sessionId: retained.id, cols: 80, rows: 24 })).rejects.toThrow() + expect(spawn).toHaveBeenCalledOnce() +}) + +it('does not let a stale successful health probe admit a replacement incarnation', async () => { + let finish!: (healthy: boolean) => void + const admission = new DaemonFreshSpawnAdmission( + () => + new Promise((resolve) => { + finish = resolve + }) + ) + const first = admission.recover() + await Promise.resolve() + const replacement = vi.fn(async () => false) + admission.reset(replacement) + finish(true) + await expect(first).resolves.toBe(false) + expect(admission.unavailable).toBe(true) + expect(replacement).toHaveBeenCalledOnce() +}) diff --git a/src/main/daemon/daemon-restart-provider-continuity.test.ts b/src/main/daemon/daemon-restart-provider-continuity.test.ts new file mode 100644 index 000000000000..a53a5e307481 --- /dev/null +++ b/src/main/daemon/daemon-restart-provider-continuity.test.ts @@ -0,0 +1,165 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { IPtyProvider } from '../providers/types' + +const { + checkDaemonHealthMock, + ensureRunningOverrides, + adapterInstances, + unbindLocalProviderListenersMock, + rebindLocalProviderListenersMock, + importFresh, + installDefaultNetConnectStub, + moduleFactories +} = await vi.hoisted(async () => + (await import('./daemon-init-test-harness')).createDaemonInitMocks() +) + +vi.mock('fs', () => moduleFactories.fs()) +vi.mock('child_process', async (importOriginal) => + moduleFactories.childProcess(await importOriginal>()) +) +vi.mock('net', () => moduleFactories.net()) +vi.mock('./daemon-health', () => moduleFactories.daemonHealth()) +vi.mock('./daemon-pid-identity', () => moduleFactories.daemonPidIdentity()) +vi.mock('./daemon-tcc-attribution', () => moduleFactories.daemonTccAttribution()) +vi.mock('./daemon-bundle-staleness', () => moduleFactories.daemonBundleStaleness()) +vi.mock('./daemon-stale-kill', () => moduleFactories.daemonStaleKill()) +vi.mock('./daemon-process-start-time', () => moduleFactories.daemonProcessStartTime()) +vi.mock('./daemon-pid-file-parse', () => moduleFactories.daemonPidFileParse()) +vi.mock('./client', () => moduleFactories.client()) +vi.mock('./daemon-lifecycle-event', () => moduleFactories.daemonLifecycleEvent()) +vi.mock('./daemon-spawner', () => moduleFactories.daemonSpawner()) +vi.mock('./daemon-pty-adapter', () => moduleFactories.daemonPtyAdapter()) +vi.mock('../ipc/pty', () => moduleFactories.ipcPty()) + +describe('restart provider continuity', () => { + beforeEach(() => installDefaultNetConnectStub()) + afterEach(() => vi.clearAllMocks()) + + it('delivers existing guest output and exit while local replacement is unresolved', async () => { + const mod = await importFresh() + await mod.initDaemonPtyProvider() + const { bindProviderListeners } = await import('../ipc/pty/provider/bind-listeners') + const lifecycle = await import('../ipc/pty/provider/listener-lifecycle') + const { registerWslPtyProvider } = await import('../ipc/pty/provider/registry') + const { createUnavailablePtyProvider } = await import('../providers/unavailable-pty-provider') + const dataListeners = new Set[0]>() + const exitListeners = new Set[0]>() + const release = registerWslPtyProvider( + { distro: 'Ubuntu', relayBuildId: 'continuity' }, + { + ...createUnavailablePtyProvider(), + onData: (listener) => { + dataListeners.add(listener) + return () => { + dataListeners.delete(listener) + } + }, + onExit: (listener) => { + exitListeners.add(listener) + return () => { + exitListeners.delete(listener) + } + } + } + ) + const session = { + acceptPtyDataForRenderer: vi.fn(), + sendModelRestoreNeededMarker: vi.fn(), + consumeSyntheticKillExit: vi.fn(), + sendPtyExitToRenderer: vi.fn() + } + bindProviderListeners(session) + unbindLocalProviderListenersMock.mockImplementation(lifecycle.unbindLocalProviderListeners) + rebindLocalProviderListenersMock.mockImplementation(() => bindProviderListeners(session)) + let finish!: () => void + ensureRunningOverrides.push(async () => { + await new Promise((resolve) => { + finish = resolve + }) + return { socketPath: '/fake/replacement', tokenPath: '/fake/token' } + }) + const restarting = mod.restartDaemon() + try { + await vi.waitFor(() => expect(finish).toBeDefined()) + dataListeners.forEach((listener) => listener({ id: 'guest', data: 'during restart' })) + exitListeners.forEach((listener) => listener({ id: 'guest', code: 7 })) + expect(session.acceptPtyDataForRenderer).toHaveBeenCalledWith( + { id: 'guest', data: 'during restart' }, + undefined + ) + expect(session.sendPtyExitToRenderer).toHaveBeenCalledWith({ id: 'guest', code: 7 }) + expect(unbindLocalProviderListenersMock).not.toHaveBeenCalled() + } finally { + finish?.() + await restarting + release() + lifecycle.unbindLocalProviderListeners() + } + }) + + it('retries initialization after startup failed without installing a provider', async () => { + const mod = await importFresh() + ensureRunningOverrides.push(async () => { + throw new Error('startup unavailable') + }) + await expect(mod.initDaemonPtyProvider()).rejects.toThrow('startup unavailable') + expect(mod.getDaemonProvider()).toBeNull() + await expect(mod.restartDaemon()).resolves.toEqual({ killedCount: 0 }) + expect(mod.getDaemonProvider()).not.toBeNull() + }) + + it.each([false, true])( + 're-arms admission on automatic respawn (restarted=%s)', + async (restarted) => { + const mod = await importFresh() + await mod.initDaemonPtyProvider() + if (restarted) { + await mod.restartDaemon() + } + const adapter = adapterInstances.at(-1)! + ensureRunningOverrides.push(async () => ({ + socketPath: '/fake/degraded-respawn', + tokenPath: '/fake/token', + mode: 'fresh-spawns-unavailable' + })) + checkDaemonHealthMock.mockResolvedValue('unhealthy') + await adapter.options.respawn?.('daemon_died') + expect(adapter.options.freshSpawnAdmission?.unavailable).toBe(true) + await expect(mod.retryDaemonPtyProvider()).rejects.toThrow('still unable') + checkDaemonHealthMock.mockResolvedValue('healthy') + await mod.retryDaemonPtyProvider() + expect(adapter.options.freshSpawnAdmission?.unavailable).toBe(false) + } + ) + + it('keeps replacement degraded mode attachable while rejecting fresh spawn', async () => { + const mod = await importFresh() + await mod.initDaemonPtyProvider() + ensureRunningOverrides.push(async () => ({ + socketPath: '/fake/degraded', + tokenPath: '/fake/token', + mode: 'fresh-spawns-unavailable' + })) + checkDaemonHealthMock.mockResolvedValue('unhealthy') + await mod.restartDaemon() + const { DaemonPtyRouter } = await import('./daemon-pty-router') + const provider = mod.getDaemonProvider() + if (!(provider instanceof DaemonPtyRouter)) { + throw new Error('Expected degraded router') + } + expect(provider.freshSpawnsUnavailable).toBe(true) + await expect(provider.spawn({ cols: 80, rows: 24 })).rejects.toThrow( + 'Terminal service unavailable' + ) + const adapter = provider.getCurrentAdapter() + const attach = vi.fn(async () => {}) + Object.assign(adapter, { attach }) + adapterInstances[1].listProcesses.mockResolvedValue([ + { id: 'retained', cwd: '/repo', title: 'shell' } + ]) + await provider.discoverLegacySessions() + await provider.attach('retained') + expect(attach).toHaveBeenCalledWith('retained') + }) +}) diff --git a/src/main/daemon/daemon-restart-state.ts b/src/main/daemon/daemon-restart-state.ts index e37fad69709f..d65ff39bdcad 100644 --- a/src/main/daemon/daemon-restart-state.ts +++ b/src/main/daemon/daemon-restart-state.ts @@ -5,6 +5,10 @@ export type RestartDaemonResult = { // Why: coalesce concurrent restartDaemon() calls so two entries can't race the 7-step sequence against a half-spawned replacement. let restartInFlight: Promise | null = null +export function getDaemonRestartInFlight(): Promise | null { + return restartInFlight +} + export function isDaemonRestartInFlight(): boolean { return restartInFlight !== null } diff --git a/src/main/daemon/daemon-runtime-launch-pin.test.ts b/src/main/daemon/daemon-runtime-launch-pin.test.ts new file mode 100644 index 000000000000..e452148cc373 --- /dev/null +++ b/src/main/daemon/daemon-runtime-launch-pin.test.ts @@ -0,0 +1,100 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +const mocks = vi.hoisted(() => ({ + resolve: vi.fn(), + launch: vi.fn(), + lease: vi.fn(), + replace: vi.fn(), + release: vi.fn(), + terminate: vi.fn() +})) +vi.mock('../../shared/app-environment', () => ({ + getAppEnvironment: () => ({ getPath: () => '/profile' }) +})) +vi.mock('./client', () => ({ + DaemonClient: class { + ensureConnectedWithin() { + return Promise.reject(new Error('no incumbent')) + } + disconnect() {} + } +})) +vi.mock('./daemon-endpoint-adoption', () => ({ + DaemonEndpointOwnershipError: class extends Error {}, + holdDaemonAdoptionLease: mocks.lease, + reconcileDaemonPidOwnership: vi.fn() +})) +vi.mock('./daemon-launched-child', () => ({ + DaemonEndpointUnavailableError: class extends Error {}, + launchDaemonChild: mocks.launch, + terminateLaunchedDaemonChild: mocks.terminate +})) +vi.mock('./daemon-launch-paths', () => ({ + getDaemonEntryPath: () => '/entry', + probeDaemonSocket: async () => false +})) +vi.mock('./daemon-bun-runtime', () => ({ resolveDesktopDaemonBunRuntime: mocks.resolve })) +vi.mock('./daemon-protocol-cleanup', () => ({ cleanupDaemonForProtocol: vi.fn() })) +vi.mock('./daemon-spawner', () => ({ + getDaemonPidPath: () => '/pid', + unlinkOwnedDaemonPidFile: vi.fn() +})) +vi.mock('./daemon-replacement-preflight', () => ({ prepareDaemonReplacement: mocks.replace })) +import { createOutOfProcessLauncher } from './daemon-out-of-process-launcher' + +beforeEach(() => { + vi.resetAllMocks() + mocks.replace.mockResolvedValue(null) + mocks.resolve.mockResolvedValue({ + execPath: '/bun', + entryPath: '/entry', + releaseLaunchPin: mocks.release + }) + mocks.launch.mockResolvedValue({ + child: { once: vi.fn(), off: vi.fn(), exitCode: null, signalCode: null }, + identity: { pid: 10 } + }) +}) +afterEach(() => { + vi.restoreAllMocks() +}) +describe('daemon runtime launch pin', () => { + it('holds the pin through launch and authenticated readiness', async () => { + mocks.launch.mockImplementation(async () => { + expect(mocks.release).not.toHaveBeenCalled() + return { child: {}, identity: { pid: 10 } } + }) + mocks.lease.mockImplementation(async () => { + expect(mocks.release).not.toHaveBeenCalled() + return { shutdown: vi.fn() } + }) + await createOutOfProcessLauncher('/profile')('/socket', '/token') + expect(mocks.release).toHaveBeenCalledTimes(1) + }) + it.each(['launch', 'lease'])('releases after %s failure', async (phase) => { + mocks[phase === 'launch' ? 'launch' : 'lease'].mockRejectedValue(new Error('failed')) + await expect(createOutOfProcessLauncher('/profile')('/socket', '/token')).rejects.toThrow( + 'failed' + ) + expect(mocks.release).toHaveBeenCalledTimes(1) + }) + it('releases the prepared runtime pin when replacement preflight fails', async () => { + mocks.replace.mockImplementation( + async (options: { prepareReplacementRuntime: () => Promise }) => { + await options.prepareReplacementRuntime() + throw new Error('replacement failed') + } + ) + await expect(createOutOfProcessLauncher('/profile')('/socket', '/token')).rejects.toThrow( + 'replacement failed' + ) + expect(mocks.resolve).toHaveBeenCalledTimes(1) + expect(mocks.launch).not.toHaveBeenCalled() + expect(mocks.release).toHaveBeenCalledTimes(1) + }) + it('never resolves or pins runtime files when adopting a live daemon', async () => { + const handle = { adopted: true, shutdown: vi.fn() } + mocks.replace.mockResolvedValue(handle) + expect(await createOutOfProcessLauncher('/profile')('/socket', '/token')).toBe(handle) + expect(mocks.resolve).not.toHaveBeenCalled() + }) +}) diff --git a/src/main/daemon/daemon-server-options.ts b/src/main/daemon/daemon-server-options.ts index 06f2cda6cb96..c122689e85f9 100644 --- a/src/main/daemon/daemon-server-options.ts +++ b/src/main/daemon/daemon-server-options.ts @@ -1,5 +1,5 @@ import type { DaemonFileLog } from './daemon-file-log' -import type { SubprocessHandle } from './session-subprocess-handle' +import type { TerminalHostOptions } from './terminal-host-options' export type DaemonServerOptions = { socketPath: string @@ -7,6 +7,8 @@ export type DaemonServerOptions = { pidPath?: string launchNonce?: string startedAtMs?: number + linuxStartTicks?: string + bootId?: string publishEndpointOwnership?: () => void entryPath?: string appVersion?: string @@ -27,14 +29,5 @@ export type DaemonServerOptions = { onPtySessionExit?: (sessionId: string) => void onAuthenticatedClientPair?: () => void log?: DaemonFileLog - spawnSubprocess: (opts: { - sessionId: string - cols: number - rows: number - cwd?: string - env?: Record - command?: string - shellOverride?: string - isCanceled?: () => boolean - }) => SubprocessHandle | Promise + spawnSubprocess: TerminalHostOptions['spawnSubprocess'] } diff --git a/src/main/daemon/daemon-server.ts b/src/main/daemon/daemon-server.ts index ab22af241001..e6b802b1e34a 100644 --- a/src/main/daemon/daemon-server.ts +++ b/src/main/daemon/daemon-server.ts @@ -1,3 +1,4 @@ +import { DaemonTransientPtys } from './daemon-transient-pty' import { randomUUID } from 'node:crypto' import type { Socket } from 'node:net' import { setImmediate as waitForImmediate } from 'node:timers/promises' @@ -33,6 +34,20 @@ export class DaemonServer { private static readonly INITIAL_ADOPTION_TIMEOUT_MS = 2 * 60 * 1000 private readonly log: DaemonFileLog + private readonly transientPtys = new DaemonTransientPtys((owner, event) => { + const socket = this.connections.get(owner)?.streamSocket + if (!socket || socket.destroyed || socket.writableLength > 256 * 1024) { + socket?.destroy() + return false + } + try { + socket.write(encodeNdjson(event)) + return true + } catch { + socket.destroy() + return false + } + }) private readonly host: TerminalHost private readonly transientFactRelay: BackgroundTransientFactRelay private readonly streamDataBatcher: DaemonStreamDataBatcher @@ -116,6 +131,8 @@ export class DaemonServer { identity: { launchNonce, startedAtMs, + linuxStartTicks: options.linuxStartTicks, + bootId: options.bootId, entryPath: options.entryPath ?? null, appVersion: options.appVersion ?? null, spawnerExecPath: options.spawnerExecPath ?? null @@ -134,17 +151,20 @@ export class DaemonServer { onControlRequest: (socket, clientId, request) => void this.handleRequest(socket, clientId, request), onControlReplaced: (clientId) => { + this.transientPtys.disconnect(clientId) this.preparations.cancelForClient(clientId) this.historySeedTransfers.clearOwner(clientId) this.streamDataBatcher.clear(clientId) }, onClientDisconnected: (clientId) => { + this.transientPtys.disconnect(clientId) this.preparations.cancelForClient(clientId) this.historySeedTransfers.clearOwner(clientId) this.streamDataBatcher.clear(clientId) this.attachments.detachClientSessions(clientId) }, onStreamDisconnected: (clientId) => { + this.transientPtys.disconnect(clientId) this.preparations.cancelForClient(clientId) this.streamDataBatcher.clear(clientId) this.attachments.detachClientSessions(clientId) @@ -197,6 +217,7 @@ export class DaemonServer { reevaluateIdleShutdown: () => this.lifecycle.reevaluateIdleShutdown() }) this.requestRouter = new DaemonRequestRouter({ + transientPtys: this.transientPtys, host: this.host, connections: this.connections, lifecycle: this.lifecycle, @@ -245,6 +266,7 @@ export class DaemonServer { private async disposeResources(): Promise { this.endpoint.stopOwnershipWatch() this.stopStreamBacklogProbe() + this.transientPtys.dispose() this.transientFactRelay.dispose() this.preparations.cancelAll() try { diff --git a/src/main/daemon/daemon-shared-owner-incarnation-retention.test.ts b/src/main/daemon/daemon-shared-owner-incarnation-retention.test.ts index 2fb980f18708..4590e8fc57f3 100644 --- a/src/main/daemon/daemon-shared-owner-incarnation-retention.test.ts +++ b/src/main/daemon/daemon-shared-owner-incarnation-retention.test.ts @@ -2,8 +2,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { join } from 'node:path' import { tmpdir } from 'node:os' import { DaemonPtyAdapter } from './daemon-pty-adapter' -import { DegradedDaemonPtyProvider } from './degraded-daemon-pty-provider' -import { LocalPtyProvider } from '../providers/local-pty-provider' +import { DaemonPtyRouter } from './daemon-pty-router' import type { PtyProcessInfo } from '../providers/types' import { TerminalSessionOwnerUnverifiedError } from './daemon-errors' @@ -59,17 +58,14 @@ function adapterFixture(label: string, pid: number) { async function fixture() { const current = adapterFixture('current', 999_999_997) const legacy = adapterFixture('legacy', 999_999_998) - const fallback = new LocalPtyProvider() - vi.spyOn(fallback, 'listProcesses').mockResolvedValue([]) - const provider = new DegradedDaemonPtyProvider({ + const provider = new DaemonPtyRouter({ current: current.adapter, - legacy: [legacy.adapter], - fallback + legacy: [legacy.adapter] }) cleanups.push(() => provider.dispose()) await current.publishIdentity(0) await legacy.publishIdentity(0) - const recovery = provider['ownerRecovery'] + const recovery = provider['ownerResolver'] return { current, legacy, provider, recovery } } @@ -78,44 +74,40 @@ function processInfo(id: string, incarnationId: string): PtyProcessInfo { } describe('shared daemon owner incarnation retirement', () => { - it('releases both private indexes after repeated authenticated daemon replacements', async () => { + it('releases the owner index after repeated authenticated daemon replacements', async () => { const { current, legacy, provider, recovery } = await fixture() legacy.setProcesses([processInfo('legacy-live', 'legacy-incarnation')]) for (let generation = 0; generation < 16; generation++) { const id = `retired-${generation}` current.setProcesses([processInfo(id, `incarnation-${generation}`)]) - await provider.discoverDaemonSessions() + await provider.discoverLegacySessions() await expect(provider.probePtyLiveness(`unmapped-${generation}`)).resolves.toBe(false) - expect(recovery['livenessResolver']['routeIncarnations'].get(id)).toBe( - `incarnation-${generation}` - ) + expect(recovery['routeIncarnations'].get(id)).toBe(`incarnation-${generation}`) current.setProcesses([]) await current.publishIdentity(generation + 1) - expect([...provider['sessionProviders'].keys()]).toEqual(['legacy-live']) - expect([...recovery['attachResolver']['routeIncarnations'].keys()]).toEqual(['legacy-live']) - expect([...recovery['livenessResolver']['routeIncarnations'].keys()]).toEqual(['legacy-live']) + expect([...provider['sessionAdapters'].keys()]).toEqual(['legacy-live']) + expect([...recovery['routeIncarnations'].keys()]).toEqual(['legacy-live']) } }) it('preserves the same session ID after another provider publishes its successor', async () => { const { current, legacy, provider, recovery } = await fixture() current.setProcesses([processInfo('same-id', 'old-incarnation')]) - await provider.discoverDaemonSessions() + await provider.discoverLegacySessions() await provider.probePtyLiveness('unmapped-old') current.setProcesses([]) legacy.setProcesses([processInfo('same-id', 'new-incarnation')]) - await provider.discoverDaemonSessions() + await provider.discoverLegacySessions() await provider.probePtyLiveness('unmapped-new') await current.publishIdentity(1) - expect(provider['sessionProviders'].get('same-id')).toBe(legacy.adapter) - expect(recovery['attachResolver']['routeIncarnations'].get('same-id')).toBe('new-incarnation') - expect(recovery['livenessResolver']['routeIncarnations'].get('same-id')).toBe('new-incarnation') + expect(provider['sessionAdapters'].get('same-id')).toBe(legacy.adapter) + expect(recovery['routeIncarnations'].get('same-id')).toBe('new-incarnation') }) it('keeps matching-incarnation direct attach without consulting another inventory', async () => { const { current, legacy, provider } = await fixture() legacy.setProcesses([processInfo('live', 'live-incarnation')]) - await provider.discoverDaemonSessions() + await provider.discoverLegacySessions() await provider.probePtyLiveness('unmapped') await current.publishIdentity(1) current.request.mockClear() @@ -139,7 +131,7 @@ describe('shared daemon owner incarnation retirement', () => { it('retains authoritative incarnation mismatch refusal after another daemon changes', async () => { const { current, legacy, provider } = await fixture() legacy.setProcesses([processInfo('live', 'current-incarnation')]) - await provider.discoverDaemonSessions() + await provider.discoverLegacySessions() await current.publishIdentity(1) await expect( provider.spawn({ diff --git a/src/main/daemon/daemon-spawner.ts b/src/main/daemon/daemon-spawner.ts index 717bef90d7aa..ef7baae531da 100644 --- a/src/main/daemon/daemon-spawner.ts +++ b/src/main/daemon/daemon-spawner.ts @@ -34,7 +34,7 @@ export type DaemonPidFile = { } export type DaemonProcessHandle = { - mode?: 'degraded-new-pty-fallback' + mode?: 'fresh-spawns-unavailable' /** Set when the launcher kept a daemon some earlier app launch forked, rather than forking one. */ adopted?: true releaseAdoptionLease?(): void diff --git a/src/main/daemon/daemon-startup-output-stream.test.ts b/src/main/daemon/daemon-startup-output-stream.test.ts new file mode 100644 index 000000000000..d78aa0ce1210 --- /dev/null +++ b/src/main/daemon/daemon-startup-output-stream.test.ts @@ -0,0 +1,67 @@ +import './mock-descendant-sweep' +import { rmSync } from 'node:fs' +import { expect, it } from 'vitest' +import { HeadlessEmulator } from './headless-emulator' +import { + createMockSubprocess, + startDaemonAdapterHarness, + waitFor +} from './daemon-pty-adapter-test-harness' + +it.each([1024 * 1024, 3 * 1024 * 1024])( + 'delivers %i startup characters through the public server and adapter', + async (length) => { + let confirm!: () => void + const receipt = new Promise((resolve) => { + confirm = resolve + }) + let subscribed = false + const subprocess = createMockSubprocess() + const harness = await startDaemonAdapterHarness(async (opts) => { + opts.onSpawnAttempt?.( + () => subprocess, + async () => { + subprocess.forceKill() + } + ) + subscribed = true + await receipt + return subprocess + }) + const observed: string[] = [] + harness.adapter.onData((event) => { + observed.push(event.data) + }) + const viewer = new HeadlessEmulator({ cols: 80, rows: 24 }) + try { + const pending = harness.adapter.spawn({ cols: 80, rows: 24, sessionId: 'startup-stream' }) + await waitFor(() => subscribed) + const startup = `\x1b[?2004h${'x'.repeat(length)}\r\nSTARTUP_END` + for (let offset = 0; offset < startup.length; offset += 16 * 1024) { + subprocess._simulateData(startup.slice(offset, offset + 16 * 1024)) + } + expect(observed).toEqual([]) + confirm() + const result = await pending + expect(result.isReattach).toBeUndefined() + expect(result.providerSequence).toEqual({ value: 0, generation: 'reset' }) + await waitFor(() => observed.join('').includes('STARTUP_END')) + subprocess._simulateData('\r\nLIVE_END') + await waitFor(() => observed.join('').includes('LIVE_END')) + for (const data of observed) { + await viewer.write(data) + } + expect(viewer.getSnapshot().modes.bracketedPaste).toBe(true) + expect(viewer.getSnapshot().snapshotAnsi).toContain('LIVE_END') + expect(observed.join('').match(/STARTUP_END/g)).toHaveLength(1) + if (length < 2 * 1024 * 1024) { + expect(observed.join('')).toBe(`${startup}\r\nLIVE_END`) + } + } finally { + viewer.dispose() + harness.adapter.dispose() + await harness.server.shutdown() + rmSync(harness.dir, { recursive: true, force: true }) + } + } +) diff --git a/src/main/daemon/daemon-terminal-admission-early-exit.test.ts b/src/main/daemon/daemon-terminal-admission-early-exit.test.ts new file mode 100644 index 000000000000..6f9f323d9be9 --- /dev/null +++ b/src/main/daemon/daemon-terminal-admission-early-exit.test.ts @@ -0,0 +1,45 @@ +import './mock-descendant-sweep' +import { rmSync } from 'node:fs' +import { expect, it, vi } from 'vitest' +import { DaemonSessionAttachments } from './daemon-session-attachments' +import { + createMockSubprocess, + startDaemonAdapterHarness, + waitFor +} from './daemon-pty-adapter-test-harness' + +it('does not recreate a released client attachment after an exit during spawn admission', async () => { + let confirm!: () => void + const receipt = new Promise((resolve) => { + confirm = resolve + }) + let subscribed = false + const subprocess = createMockSubprocess() + const attach = vi.spyOn(DaemonSessionAttachments.prototype, 'attach') + const release = vi.spyOn(DaemonSessionAttachments.prototype, 'release') + const harness = await startDaemonAdapterHarness(async (opts) => { + opts.onSpawnAttempt?.( + () => subprocess, + async () => { + subprocess.forceKill() + } + ) + subscribed = true + await receipt + return subprocess + }) + try { + const pending = harness.adapter.spawn({ cols: 80, rows: 24, sessionId: 'early-exit' }) + await waitFor(() => subscribed) + subprocess._simulateExit(7) + confirm() + await expect(pending).resolves.toMatchObject({ id: 'early-exit', exitedBeforeSpawnReply: true }) + expect(release).toHaveBeenCalledWith('early-exit') + expect(attach).not.toHaveBeenCalled() + } finally { + harness.adapter.dispose() + await harness.server.shutdown() + rmSync(harness.dir, { recursive: true, force: true }) + vi.restoreAllMocks() + } +}) diff --git a/src/main/daemon/daemon-terminal-admission.ts b/src/main/daemon/daemon-terminal-admission.ts index f02cbdb9205c..7b41bfd86e6d 100644 --- a/src/main/daemon/daemon-terminal-admission.ts +++ b/src/main/daemon/daemon-terminal-admission.ts @@ -65,6 +65,7 @@ export class DaemonTerminalAdmission { } this.createOrAttachInFlight++ let routedSessionId = payload.sessionId + let exitedDuringAdmission = false let result: CreateOrAttachResult let spawnPreparation: PendingPtySpawnPreparation | null = null try { @@ -120,7 +121,13 @@ export class DaemonTerminalAdmission { onSessionResolved: (sessionId) => { routedSessionId = sessionId }, - streamClient: this.createStreamClient(clientId, () => routedSessionId) + streamClient: this.createStreamClient( + clientId, + () => routedSessionId, + () => { + exitedDuringAdmission = true + } + ) }) } finally { if (spawnPreparation) { @@ -139,7 +146,9 @@ export class DaemonTerminalAdmission { this.options.host.detach(routedSessionId, result.attachToken) throw new TerminalAttachCanceledError(routedSessionId) } - this.options.attachments.attach(routedSessionId, clientId, result.attachToken) + if (!exitedDuringAdmission) { + this.options.attachments.attach(routedSessionId, clientId, result.attachToken) + } this.options.streamDataBatcher.refreshSessionDroppability(routedSessionId) if (this.options.transientFactRelay.isBackgrounded(routedSessionId)) { this.options.streamDataBatcher.enqueueControlEvent(clientId, routedSessionId, { @@ -171,7 +180,8 @@ export class DaemonTerminalAdmission { private createStreamClient( clientId: string, - sessionId: () => string + sessionId: () => string, + onExitObserved: () => void ): CreateOrAttachOptions['streamClient'] { return { onData: (data, rawLength = data.length, transformed = false, seq) => { @@ -191,6 +201,7 @@ export class DaemonTerminalAdmission { }) }, onExit: (code, incarnationId, cause) => { + onExitObserved() const routedSessionId = sessionId() this.options.log.log('session-exited', { sessionId: routedSessionId, diff --git a/src/main/daemon/daemon-transient-pty-protocol.ts b/src/main/daemon/daemon-transient-pty-protocol.ts new file mode 100644 index 000000000000..19ef50f58efb --- /dev/null +++ b/src/main/daemon/daemon-transient-pty-protocol.ts @@ -0,0 +1,9 @@ +import type { TransientPtyCommand } from './daemon-transient-pty' +export type TransientPtyRequest = + | { + id: string + type: 'createTransientPty' + payload: TransientPtyCommand + } + | { id: string; type: 'writeTransientPty'; payload: { id: string; data: string } } + | { id: string; type: 'closeTransientPty'; payload: { id: string } } diff --git a/src/main/daemon/daemon-transient-pty.test.ts b/src/main/daemon/daemon-transient-pty.test.ts new file mode 100644 index 000000000000..5d5eba737029 --- /dev/null +++ b/src/main/daemon/daemon-transient-pty.test.ts @@ -0,0 +1,190 @@ +import type { DaemonEvent } from './types' +import { randomUUID } from 'node:crypto' +import { afterEach, describe, expect, it, vi } from 'vitest' +import type { BunPtyProcess } from './pty-subprocess/bun-pty-process-contract' +import { DaemonTransientPtys } from './daemon-transient-pty' + +vi.mock('../pty/posix-pty-process-groups', () => ({ + forceKillPosixPtyProcessGroups: (_pid: number, fallback: () => void) => fallback() +})) + +function fixture(waitForSpawn?: () => Promise) { + let output: (data: string) => void = () => {} + let exit: (event: { exitCode: number }) => void = () => {} + const disposeData = vi.fn() + const disposeExit = vi.fn() + const process = { + pid: 123, + kill: vi.fn(() => exit({ exitCode: 137 })), + destroy: vi.fn(), + write: vi.fn(), + waitForSpawn, + onData: vi.fn((listener: typeof output) => { + output = listener + return { dispose: disposeData } + }), + onExit: vi.fn((listener: typeof exit) => { + exit = listener + return { dispose: disposeExit } + }) + } + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Registry only accesses the explicitly supplied process members. + const spawn = vi.fn(() => process as unknown as BunPtyProcess) + const publish = vi.fn((_owner: string, _event: DaemonEvent) => true) + const registry = new DaemonTransientPtys(publish, spawn, true) + const command = { + id: randomUUID(), + file: '/synthetic/tool', + args: ['a b', '$HOME'], + cwd: '/tmp', + env: { AUTH: 'fixture' }, + cols: 120, + rows: 40 + } + return { + registry, + command, + process, + spawn, + publish, + output: (data: string) => output(data), + exit: (code: number) => exit({ exitCode: code }), + disposeData, + disposeExit + } +} + +afterEach(() => { + vi.useRealTimers() + vi.unstubAllGlobals() +}) + +describe('transient daemon PTYs', () => { + it('rejects unsupported runtimes and unauthenticated pairs before spawning', async () => { + const f = fixture() + const unavailable = new DaemonTransientPtys(f.publish, f.spawn, false) + expect(unavailable.ping()).toEqual({ pong: true }) + await expect(unavailable.create('owner', f.command)).rejects.toThrow('unavailable') + expect(() => + f.registry.route( + 'owner', + { + id: 'request', + type: 'createTransientPty', + payload: f.command + }, + false + ) + ).toThrow('authenticated') + expect(f.spawn).not.toHaveBeenCalled() + }) + + it('passes exact argv and replacement environment, with owner-only input and cleanup', async () => { + const f = fixture() + await f.registry.create('owner', f.command) + expect(f.spawn).toHaveBeenCalledWith({ ...f.command, windowsJobKillOnClose: true }) + expect(() => f.registry.write('other', f.command.id, 'secret')).toThrow('not owned') + f.registry.write('owner', f.command.id, '/status') + expect(f.process.write).toHaveBeenCalledWith('/status') + f.registry.disconnect('other') + expect(f.process.destroy).not.toHaveBeenCalled() + f.registry.disconnect('owner') + expect(f.process.kill).toHaveBeenCalledOnce() + expect(f.process.destroy).toHaveBeenCalledOnce() + expect(f.disposeData).toHaveBeenCalledOnce() + expect(f.disposeExit).toHaveBeenCalledOnce() + }) + + it('kills a pending Windows spawn when its connection disappears', async () => { + let finish: () => void = () => {} + const f = fixture( + () => + new Promise((resolve) => { + finish = resolve + }) + ) + const creation = f.registry.create('owner', f.command) + f.registry.disconnect('owner') + finish() + await expect(creation).rejects.toThrow('canceled') + expect(f.process.kill).toHaveBeenCalledOnce() + expect(f.process.destroy).toHaveBeenCalledOnce() + }) + + it('orders output before exit and does not kill a naturally exited process', async () => { + const f = fixture() + await f.registry.create('owner', f.command) + f.output('hello') + f.exit(0) + expect(f.publish.mock.calls).toEqual([ + [ + 'owner', + { type: 'event', event: 'data', sessionId: f.command.id, payload: { data: 'hello' } } + ], + ['owner', { type: 'event', event: 'exit', sessionId: f.command.id, payload: { code: 0 } }] + ]) + expect(f.process.kill).not.toHaveBeenCalled() + expect(f.process.destroy).toHaveBeenCalledOnce() + }) + + it('retains ownership after failed signals until an actual exit is observed', async () => { + vi.useFakeTimers() + const warnings = vi.spyOn(console, 'warn').mockImplementation(() => {}) + const f = fixture() + f.process.kill.mockImplementation(() => { + throw new Error('EPERM') + }) + await f.registry.create('owner', f.command) + f.registry.disconnect('owner') + await vi.advanceTimersByTimeAsync(2_000) + expect(f.process.kill).toHaveBeenCalledTimes(3) + expect(f.process.destroy).not.toHaveBeenCalled() + expect(f.publish.mock.calls.some(([, event]) => event.event === 'exit')).toBe(false) + await expect(f.registry.create('owner', f.command)).rejects.toThrow('already exists') + f.exit(137) + expect(f.process.destroy).toHaveBeenCalledOnce() + expect(vi.getTimerCount()).toBe(0) + warnings.mockRestore() + }) + + it('terminates on overflow and bounds unattended lifetime', async () => { + vi.useFakeTimers() + const f = fixture() + await f.registry.create('owner', f.command) + f.publish.mockReturnValue(false) + f.output('flood') + expect(f.process.destroy).toHaveBeenCalledOnce() + const g = fixture() + await g.registry.create('owner', g.command) + await vi.advanceTimersByTimeAsync(30_000) + expect(g.process.destroy).toHaveBeenCalledOnce() + expect(vi.getTimerCount()).toBe(0) + }) + it('rejects new probes after shutdown starts', async () => { + const f = fixture() + f.registry.dispose() + await expect(f.registry.create('owner', f.command)).rejects.toThrow('shutting down') + expect(f.spawn).not.toHaveBeenCalled() + }) + + it('does not kill only the Windows gate after job termination fails', async () => { + vi.useFakeTimers() + vi.stubGlobal('process', { ...process, platform: 'win32' }) + const warning = vi.spyOn(console, 'warn').mockImplementation(() => {}) + try { + const f = fixture() + const terminateOwnedTree = vi.fn(() => 'unavailable') + Object.assign(f.process, { terminateOwnedTree }) + await f.registry.create('owner', f.command) + f.registry.disconnect('owner') + await vi.advanceTimersByTimeAsync(2_000) + expect(terminateOwnedTree).toHaveBeenCalledTimes(3) + expect(f.process.kill).not.toHaveBeenCalled() + expect(f.process.destroy).not.toHaveBeenCalled() + await expect(f.registry.create('owner', f.command)).rejects.toThrow('already exists') + f.exit(1) + } finally { + warning.mockRestore() + } + }) +}) diff --git a/src/main/daemon/daemon-transient-pty.ts b/src/main/daemon/daemon-transient-pty.ts new file mode 100644 index 000000000000..0907b07b06b5 --- /dev/null +++ b/src/main/daemon/daemon-transient-pty.ts @@ -0,0 +1,239 @@ +import type { TransientPtyRequest } from './daemon-transient-pty-protocol' +import { z } from 'zod' +import { spawnBunPty, canUseBunPty } from './pty-subprocess/bun-pty-process' +import type { BunPtyProcess } from './pty-subprocess/bun-pty-process-contract' +import type { DaemonEvent } from './types' +import { forceKillPosixPtyProcessGroups } from '../pty/posix-pty-process-groups' + +const text = z + .string() + .max(32_768) + .refine((value) => !value.includes('\0')) +export const transientPtyCommand = z.object({ + id: z.string().uuid(), + file: text.min(1), + args: z.array(text).max(256), + cwd: text.min(1), + env: z.record(z.string(), text), + cols: z.number().int().min(1).max(500), + rows: z.number().int().min(1).max(500) +}) +export type TransientPtyCommand = z.infer +type Entry = { + owner: string + process: BunPtyProcess | null + ended: boolean + closing: boolean + terminationAttempts: number + timer: ReturnType + listeners: { dispose(): void }[] +} + +/** Client-leased probes never enter terminal sessions, history or persistence. */ +export class DaemonTransientPtys { + private disposed = false + private readonly entries = new Map() + constructor( + private readonly publish: (owner: string, event: DaemonEvent) => boolean, + private readonly spawn = spawnBunPty, + readonly available = canUseBunPty() + ) {} + + ping(): { pong: true; capabilities?: { transientPty: 1 } } { + return this.available ? { pong: true, capabilities: { transientPty: 1 } } : { pong: true } + } + + route(owner: string, request: TransientPtyRequest, authenticated: boolean): unknown { + if (!authenticated) { + throw new Error('Transient PTY requires an authenticated stream') + } + switch (request.type) { + case 'createTransientPty': + return this.create(owner, request.payload) + case 'writeTransientPty': + this.write(owner, request.payload.id, request.payload.data) + return {} + case 'closeTransientPty': + this.close(owner, request.payload.id) + return {} + } + } + + async create(owner: string, input: unknown): Promise<{ pid: number }> { + if (this.disposed) { + throw new Error('Transient PTY service is shutting down') + } + if (!this.available) { + throw new Error('Transient PTY runtime unavailable') + } + const command = transientPtyCommand.parse(input) + if (this.entries.has(command.id) || this.entries.size >= 8) { + throw new Error('Transient PTY capacity exceeded or ID already exists') + } + const entry: Entry = { + owner, + process: null, + ended: false, + closing: false, + terminationAttempts: 0, + listeners: [], + timer: setTimeout(() => this.expire(command.id, entry), 30_000) + } + this.entries.set(command.id, entry) + try { + const proc = this.spawn({ ...command, windowsJobKillOnClose: true }) + entry.process = proc + // Register before waiting for Windows spawn receipt so early output remains ordered. + const data = proc.onData((output) => { + if (entry.ended || entry.closing) { + return + } + for (let offset = 0; offset < output.length; offset += 16_384) { + if ( + !this.publish(owner, { + type: 'event', + event: 'data', + sessionId: command.id, + payload: { data: output.slice(offset, offset + 16_384) } + }) + ) { + this.end(command.id, entry, true) + return + } + } + }) + entry.listeners.push(data) + const exit = proc.onExit(({ exitCode }) => { + if (!entry.ended) { + this.publish(owner, { + type: 'event', + event: 'exit', + sessionId: command.id, + payload: { code: exitCode } + }) + this.end(command.id, entry, false) + } + }) + entry.listeners.push(exit) + await proc.waitForSpawn?.() + if (entry.ended) { + data.dispose() + exit.dispose() + } + if (entry.closing) { + throw new Error('Transient PTY canceled during startup') + } + return { pid: proc.pid } + } catch (error) { + this.end(command.id, entry, true) + throw error + } + } + + write(owner: string, id: string, data: string): void { + const entry = this.requireOwned(owner, id) + if (data.length > 32_768) { + throw new Error('Transient PTY input too large') + } + entry.process?.write(data) + } + + close(owner: string, id: string): void { + const entry = this.entries.get(id) + if (!entry) { + return + } + this.requireOwned(owner, id) + this.end(id, entry, true) + } + + disconnect(owner: string): void { + for (const [id, entry] of this.entries) { + if (entry.owner === owner) { + this.end(id, entry, true) + } + } + } + + dispose(): void { + this.disposed = true + for (const [id, entry] of this.entries) { + this.end(id, entry, true) + } + } + + private expire(id: string, entry: Entry): void { + if (entry.ended) { + return + } + this.end(id, entry, true) + } + + private requireOwned(owner: string, id: string): Entry { + const entry = this.entries.get(id) + if (!entry || entry.owner !== owner || entry.ended || entry.closing) { + throw new Error('Transient PTY not owned') + } + return entry + } + + private end(id: string, entry: Entry, kill: boolean): void { + if (entry.ended) { + return + } + if (kill && entry.process) { + if (entry.closing) { + return + } + entry.closing = true + clearTimeout(entry.timer) + this.terminate(id, entry) + return + } + entry.ended = true + clearTimeout(entry.timer) + this.entries.delete(id) + for (const listener of entry.listeners.splice(0)) { + listener.dispose() + } + try { + entry.process?.destroy() + } catch (error) { + console.warn('[daemon] Exited transient PTY disposal failed', error) + } + } + + private terminate(id: string, entry: Entry): void { + const proc = entry.process + if (!proc || entry.ended) { + return + } + entry.terminationAttempts++ + try { + if (process.platform === 'win32') { + if (proc.terminateOwnedTree?.() !== 'terminated') { + throw new Error('Transient PTY job termination is unavailable') + } + } else { + forceKillPosixPtyProcessGroups(proc.pid, () => proc.kill('SIGKILL')) + } + } catch (error) { + console.warn('[daemon] Transient PTY termination failed', error) + } + if (entry.ended) { + return + } + if (entry.terminationAttempts < 3) { + entry.timer = setTimeout(() => this.terminate(id, entry), 1_000) + } else { + // Keep ownership until onExit proves termination; failed signals must not free capacity. + console.warn('[daemon] Transient PTY exit remains unverifiable', { id }) + this.publish(entry.owner, { + type: 'event', + event: 'terminalError', + sessionId: id, + payload: { message: 'Usage probe termination remains unverifiable' } + }) + } + } +} diff --git a/src/main/daemon/degraded-daemon-fallback-shutdown.ts b/src/main/daemon/degraded-daemon-fallback-shutdown.ts deleted file mode 100644 index 0d4932d6c20a..000000000000 --- a/src/main/daemon/degraded-daemon-fallback-shutdown.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { IPtyProvider } from '../providers/types' - -export async function shutdownDegradedFallbackSessions( - sessionProviders: Map, - fallback: T -): Promise { - const ids = [...sessionProviders] - .filter(([, provider]) => provider === fallback) - .map(([id]) => id) - const results = await Promise.allSettled( - ids.map(async (id) => { - await fallback.shutdown(id, { immediate: true }) - sessionProviders.delete(id) - }) - ) - // Why: fallback cleanup must not abort the user's daemon-restart recovery path. - const failed = results.filter((result) => result.status === 'rejected') - if (failed.length > 0) { - console.warn( - `[daemon] ${failed.length} local fallback PTY session(s) failed to shut down during daemon restart; continuing restart`, - ...failed.map((result) => (result as PromiseRejectedResult).reason) - ) - } - return results.length - failed.length -} diff --git a/src/main/daemon/degraded-daemon-fresh-spawn-routing.ts b/src/main/daemon/degraded-daemon-fresh-spawn-routing.ts deleted file mode 100644 index b1d4661dc93b..000000000000 --- a/src/main/daemon/degraded-daemon-fresh-spawn-routing.ts +++ /dev/null @@ -1,77 +0,0 @@ -import type { IPtyProvider, PtySpawnOptions, PtySpawnResult } from '../providers/types' - -export const DEGRADED_DAEMON_RECOVERY_RETRY_MS = 30_000 - -export class DegradedDaemonFreshSpawnRouter { - private target: IPtyProvider - private recovery: Promise | null = null - private retryAfterMs = 0 - - constructor( - private readonly current: IPtyProvider, - private readonly fallback: IPtyProvider, - private readonly sessionProviders: Map, - private readonly probeCurrent: (() => Promise) | null - ) { - this.target = fallback - } - - get routesToFallback(): true | undefined { - return this.target === this.fallback ? true : undefined - } - - supportsGitGuardHost(sessionId?: string): boolean { - const provider = (sessionId ? this.sessionProviders.get(sessionId) : undefined) ?? this.target - return provider.supportsGitCredentialGuardHost?.(sessionId) === true - } - - canProvideSnapshot(sessionId: string): boolean { - return ( - this.sessionProviders.get(sessionId)?.canProvideAuthoritativeBufferSnapshot?.(sessionId) === - true - ) - } - - async recover(): Promise { - if (this.target === this.current) { - return true - } - if (!this.probeCurrent) { - return false - } - if (Date.now() < this.retryAfterMs) { - return false - } - if (this.recovery) { - return this.recovery - } - const recovery = this.probeCurrent() - .catch(() => false) - .then((healthy) => { - if (healthy) { - this.target = this.current - console.info('[daemon] PTY spawn health recovered; fresh terminals are daemon-backed') - } else { - this.retryAfterMs = Date.now() + DEGRADED_DAEMON_RECOVERY_RETRY_MS - } - return healthy - }) - .finally(() => { - if (this.recovery === recovery) { - this.recovery = null - } - }) - this.recovery = recovery - return recovery - } - - async spawn(opts: PtySpawnOptions): Promise { - const mapped = opts.sessionId ? this.sessionProviders.get(opts.sessionId) : undefined - const target = mapped ?? this.target - const result = await target.spawn(opts) - if (!result.exitedBeforeSpawnReply) { - this.sessionProviders.set(result.id, target) - } - return result - } -} diff --git a/src/main/daemon/degraded-daemon-owner-recovery.ts b/src/main/daemon/degraded-daemon-owner-recovery.ts deleted file mode 100644 index ec8798a5ce35..000000000000 --- a/src/main/daemon/degraded-daemon-owner-recovery.ts +++ /dev/null @@ -1,82 +0,0 @@ -import type { IPtyProvider, PtySpawnOptions, PtySpawnResult } from '../providers/types' -import type { DaemonPtyAdapter } from './daemon-pty-adapter' -import { DaemonSessionOwnerResolver } from './daemon-session-owner-resolution' - -export class DegradedDaemonOwnerRecovery { - private readonly attachResolver: DaemonSessionOwnerResolver - private readonly livenessResolver: DaemonSessionOwnerResolver - - constructor( - providers: readonly IPtyProvider[], - private readonly daemonAdapters: readonly DaemonPtyAdapter[], - routes: Map, - private readonly spawnFresh: (opts: PtySpawnOptions) => Promise - ) { - this.attachResolver = new DaemonSessionOwnerResolver(providers, routes) - this.livenessResolver = new DaemonSessionOwnerResolver(daemonAdapters, routes) - } - - spawn = async (opts: PtySpawnOptions): Promise => { - return opts.attachOnly && opts.sessionId - ? await this.attachResolver.spawnAttachOnly({ ...opts, sessionId: opts.sessionId }) - : await this.spawnFresh(opts) - } - - probe = async (sessionId: string): Promise => - await this.livenessResolver.probe(sessionId) - - discoverRoutes = async (): Promise => await this.attachResolver.discoverRoutes() - - async reconcileOnStartup(validWorktreeIds: Set): Promise<{ - alive: string[] - killed: string[] - }> { - const alive: string[] = [] - const killed: string[] = [] - const aliveProviders = new Map>() - for (const adapter of this.daemonAdapters) { - const result = await adapter.reconcileOnStartup(validWorktreeIds) - for (const id of result.alive) { - alive.push(id) - const providers = aliveProviders.get(id) ?? new Set() - providers.add(adapter) - aliveProviders.set(id, providers) - } - killed.push(...result.killed) - } - for (const id of new Set([...alive, ...killed])) { - const providers = aliveProviders.get(id) - if (providers?.size === 1) { - this.recordRoute(id, providers.values().next().value!) - } else { - this.forgetRoute(id) - } - } - return { alive, killed } - } - - recordRoute(sessionId: string, provider: IPtyProvider): void { - this.attachResolver.recordRoute(sessionId, provider) - if (this.daemonAdapters.includes(provider as DaemonPtyAdapter)) { - this.livenessResolver.recordRoute(sessionId, provider as DaemonPtyAdapter) - } - } - - forgetRoute(sessionId: string): void { - this.attachResolver.forgetRoute(sessionId) - this.livenessResolver.forgetRoute(sessionId) - } - - subscribeIdentityChanges(): (() => void)[] { - return this.daemonAdapters.flatMap((adapter) => - typeof adapter.onDaemonIdentityChanged === 'function' - ? [ - adapter.onDaemonIdentityChanged(() => { - this.attachResolver.invalidateProvider(adapter) - this.livenessResolver.invalidateProvider(adapter) - }) - ] - : [] - ) - } -} diff --git a/src/main/daemon/degraded-daemon-pty-provider.test.ts b/src/main/daemon/degraded-daemon-pty-provider.test.ts deleted file mode 100644 index f2e86abab280..000000000000 --- a/src/main/daemon/degraded-daemon-pty-provider.test.ts +++ /dev/null @@ -1,686 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' -import { DegradedDaemonPtyProvider } from './degraded-daemon-pty-provider' -import { DEGRADED_DAEMON_RECOVERY_RETRY_MS } from './degraded-daemon-fresh-spawn-routing' -import type { DaemonPtyAdapter } from './daemon-pty-adapter' -import { settledWriteStub, stubWriteSettlement } from '../providers/settled-pty-write-stub' -import type { IPtyProvider, PtySpawnOptions, PtySpawnResult } from '../providers/types' -import type { PtyProcessInspection } from '../providers/pty-process-inspection' -import { SessionNotFoundError, TerminalSessionOwnerUnverifiedError } from './daemon-errors' - -type ProviderMock = IPtyProvider & { - probePtyLiveness: (id: string) => Promise - inspectProcess: (id: string) => Promise - emitData: (id: string, data: string, sequenceChars?: number) => void - emitReplay: (id: string, data: string) => void - emitExit: (id: string, code: number) => void - triggerWriteUnavailable: (id: string) => void - onWriteUnavailable: (callback: (payload: { id: string }) => void) => () => void -} - -function createProvider( - label: string, - sessions: string[] = [], - authoritativeOwnerListings = false -): ProviderMock { - const dataListeners: ((payload: { id: string; data: string; sequenceChars?: number }) => void)[] = - [] - const replayListeners: ((payload: { id: string; data: string }) => void)[] = [] - const exitListeners: ((payload: { id: string; code: number }) => void)[] = [] - const writeUnavailableListeners: ((payload: { id: string }) => void)[] = [] - return { - spawn: vi.fn(async (opts: PtySpawnOptions): Promise => { - const id = opts.sessionId ?? `${label}-new` - sessions.push(id) - return { id } - }), - attach: vi.fn(async () => {}), - hasPty: vi.fn((id: string) => sessions.includes(id)), - probePtyLiveness: vi.fn(async (id: string) => sessions.includes(id)), - providesAgentSessionOwnerListings: vi.fn(() => authoritativeOwnerListings), - write: vi.fn(), - writeWithSettlement: vi.fn(settledWriteStub()), - resize: vi.fn(), - shutdown: vi.fn(async (id: string) => { - const idx = sessions.indexOf(id) - if (idx !== -1) { - sessions.splice(idx, 1) - } - }), - sendSignal: vi.fn(async () => {}), - getCwd: vi.fn(async () => ''), - getInitialCwd: vi.fn(async () => ''), - clearBuffer: vi.fn(async () => {}), - acknowledgeDataEvent: vi.fn(), - hasChildProcesses: vi.fn(async () => false), - getForegroundProcess: vi.fn(async () => null), - inspectProcess: vi.fn(async () => ({ foregroundProcess: null, hasChildProcesses: false })), - confirmForegroundProcess: vi.fn(async () => `${label}-confirmed`), - serialize: vi.fn(async () => '{}'), - revive: vi.fn(async () => {}), - listProcesses: vi.fn(async () => sessions.map((id) => ({ id, cwd: '', title: label }))), - getDefaultShell: vi.fn(async () => '/bin/zsh'), - getProfiles: vi.fn(async () => []), - onData: vi.fn( - (callback: (payload: { id: string; data: string; sequenceChars?: number }) => void) => { - dataListeners.push(callback) - return () => { - const idx = dataListeners.indexOf(callback) - if (idx !== -1) { - dataListeners.splice(idx, 1) - } - } - } - ), - onReplay: vi.fn((callback: (payload: { id: string; data: string }) => void) => { - replayListeners.push(callback) - return () => { - const idx = replayListeners.indexOf(callback) - if (idx !== -1) { - replayListeners.splice(idx, 1) - } - } - }), - onExit: vi.fn((callback: (payload: { id: string; code: number }) => void) => { - exitListeners.push(callback) - return () => { - const idx = exitListeners.indexOf(callback) - if (idx !== -1) { - exitListeners.splice(idx, 1) - } - } - }), - emitData: (id: string, data: string, sequenceChars?: number) => { - for (const listener of dataListeners) { - listener({ id, data, ...(sequenceChars === undefined ? {} : { sequenceChars }) }) - } - }, - emitReplay: (id: string, data: string) => { - for (const listener of replayListeners) { - listener({ id, data }) - } - }, - emitExit: (id: string, code: number) => { - for (const listener of exitListeners) { - listener({ id, code }) - } - }, - onWriteUnavailable: vi.fn((callback: (payload: { id: string }) => void) => { - writeUnavailableListeners.push(callback) - return () => { - const idx = writeUnavailableListeners.indexOf(callback) - if (idx !== -1) { - writeUnavailableListeners.splice(idx, 1) - } - } - }), - triggerWriteUnavailable: (id: string) => { - for (const listener of writeUnavailableListeners) { - listener({ id }) - } - } - } -} - -function createDaemonAdapter( - label: string, - sessions: string[] = [] -): DaemonPtyAdapter & ProviderMock { - return { - ...createProvider(label, sessions, true), - protocolVersion: 13, - supportsGitCredentialGuardHost: vi.fn(() => true), - canProvideAuthoritativeBufferSnapshot: vi.fn(() => true), - listSessions: vi.fn(async () => []), - ackColdRestore: vi.fn(), - clearTombstone: vi.fn(), - reconcileOnStartup: vi.fn(async () => ({ alive: sessions, killed: [] })), - dispose: vi.fn(), - disconnectOnly: vi.fn(async () => {}), - getActiveSessionIds: vi.fn(() => []), - fanoutSyntheticExits: vi.fn() - } as unknown as DaemonPtyAdapter & ProviderMock -} - -it('forwards dead-endpoint write-unavailable signals from the daemon adapters', () => { - // Why revert-sensitive: this provider is the live localProvider in degraded launch - // mode and main subscribes on it, so without forwarding the STA-2373 fan-out reaches - // no listener and sibling panes stay frozen. - const current = createDaemonAdapter('daemon') - const legacy = createDaemonAdapter('legacy') - const fallback = createProvider('fallback') - const provider = new DegradedDaemonPtyProvider({ current, legacy: [legacy], fallback }) - const recovered: string[] = [] - - const unsubscribe = provider.onWriteUnavailable(({ id }) => recovered.push(id)) - current.triggerWriteUnavailable('daemon-pane') - legacy.triggerWriteUnavailable('legacy-pane') - expect(recovered).toEqual(['daemon-pane', 'legacy-pane']) - - unsubscribe() - current.triggerWriteUnavailable('after-unsubscribe') - expect(recovered).toEqual(['daemon-pane', 'legacy-pane']) -}) - -it('routes attach-only to a legacy session created after startup inventory', async () => { - const current = createDaemonAdapter('daemon') - const legacySessions = ['legacy-at-startup'] - const legacy = createDaemonAdapter('legacy', legacySessions) - const fallback = createProvider('fallback') - const provider = new DegradedDaemonPtyProvider({ - current, - legacy: [legacy], - fallback - }) - await provider.discoverDaemonSessions() - legacySessions.push('legacy-created-later') - - await provider.spawn({ - sessionId: 'legacy-created-later', - attachOnly: true, - cols: 80, - rows: 24 - }) - - expect(legacy.spawn).toHaveBeenCalledOnce() - expect(current.spawn).not.toHaveBeenCalled() - expect(fallback.spawn).not.toHaveBeenCalled() -}) - -it('keeps an attach unresolved when a legacy inventory listing fails', async () => { - const current = createDaemonAdapter('daemon') - const legacy = createDaemonAdapter('legacy') - vi.mocked(legacy.listProcesses).mockRejectedValue(new Error('wedged')) - const provider = new DegradedDaemonPtyProvider({ - current, - legacy: [legacy], - fallback: createProvider('fallback') - }) - - await expect( - provider.spawn({ sessionId: 'unknown-session', attachOnly: true, cols: 80, rows: 24 }) - ).rejects.toBeInstanceOf(TerminalSessionOwnerUnverifiedError) - expect(current.spawn).not.toHaveBeenCalled() -}) - -it('confirms repeated stale-binding absence without falling back or spawning', async () => { - const current = createDaemonAdapter('daemon-current') - const legacy = createDaemonAdapter('daemon-legacy') - const fallback = createProvider('local-fallback') - vi.mocked(fallback.spawn).mockImplementation(async (opts) => - opts.attachOnly - ? { - id: opts.sessionId!, - incarnationId: 'incarnation-fresh-local', - isReattach: true - } - : { id: 'pty-fresh-local', incarnationId: 'incarnation-fresh-local' } - ) - const provider = new DegradedDaemonPtyProvider({ current, legacy: [legacy], fallback }) - const attach = { - sessionId: 'pty-persisted-missing', - expectedIncarnationId: 'incarnation-persisted-missing', - attachOnly: true, - cols: 80, - rows: 24 - } as const - - await expect(provider.spawn(attach)).rejects.toBeInstanceOf(SessionNotFoundError) - await expect(provider.spawn(attach)).rejects.toBeInstanceOf(SessionNotFoundError) - - expect(fallback.listProcesses).toHaveBeenCalledTimes(2) - expect(current.listProcesses).toHaveBeenCalledTimes(2) - expect(legacy.listProcesses).toHaveBeenCalledTimes(2) - expect(fallback.spawn).not.toHaveBeenCalled() - expect(current.spawn).not.toHaveBeenCalled() - expect(legacy.spawn).not.toHaveBeenCalled() - - const fresh = await provider.spawn({ cols: 80, rows: 24 }) - await expect( - provider.spawn({ - sessionId: fresh.id, - expectedIncarnationId: fresh.incarnationId, - attachOnly: true, - cols: 80, - rows: 24 - }) - ).resolves.toMatchObject({ - id: 'pty-fresh-local', - incarnationId: 'incarnation-fresh-local', - isReattach: true - }) - - expect(fallback.spawn).toHaveBeenCalledTimes(2) - expect(vi.mocked(fallback.spawn).mock.calls[0]?.[0]).not.toHaveProperty('sessionId') - expect(vi.mocked(fallback.spawn).mock.calls[1]?.[0]).toMatchObject({ - sessionId: 'pty-fresh-local', - attachOnly: true - }) - expect(current.spawn).not.toHaveBeenCalled() - expect(legacy.spawn).not.toHaveBeenCalled() - expect(fallback.listProcesses).toHaveBeenCalledTimes(2) - expect(current.listProcesses).toHaveBeenCalledTimes(2) - expect(legacy.listProcesses).toHaveBeenCalledTimes(2) -}) - -it('rejects completion inspection instead of borrowing the fallback provider', async () => { - const provider = new DegradedDaemonPtyProvider({ - current: createDaemonAdapter('daemon'), - legacy: [], - fallback: createProvider('fallback') - }) - - await expect(provider.inspectProcess('unmapped-session')).rejects.toThrow('terminal_gone') -}) - -it('preserves client-only unverifiable inspection from an owning daemon', async () => { - const daemon = createDaemonAdapter('daemon', ['daemon-session']) - vi.mocked(daemon.inspectProcess).mockResolvedValue({ - foregroundProcess: null, - hasChildProcesses: false, - verdict: 'unverifiable', - reason: 'old_host' - }) - const provider = new DegradedDaemonPtyProvider({ - current: daemon, - legacy: [], - fallback: createProvider('fallback') - }) - await provider.discoverDaemonSessions() - - await expect(provider.inspectProcess('daemon-session')).resolves.toEqual({ - foregroundProcess: null, - hasChildProcesses: false, - verdict: 'unverifiable', - reason: 'old_host' - }) -}) - -describe('DegradedDaemonPtyProvider', () => { - it('refuses attach for ids no daemon adapter owns instead of the fallback no-op', async () => { - const daemonSessions: string[] = [] - const current = createDaemonAdapter('daemon', daemonSessions) - const fallback = createProvider('fallback', ['fresh-fallback-session']) - const provider = new DegradedDaemonPtyProvider({ current, legacy: [], fallback }) - - // Unknown id: the fallback's resolving no-op attach must never read as - // success — the runtime would pin a blank stream as attached. - await expect(provider.attach('wt-1@@unknown')).rejects.toThrow( - 'Session not found: wt-1@@unknown' - ) - // Fallback-owned fresh sessions stream in-process; attach is refused too. - await expect(provider.attach('fresh-fallback-session')).rejects.toThrow( - 'Session not found: fresh-fallback-session' - ) - expect(fallback.attach).not.toHaveBeenCalled() - expect(current.attach).not.toHaveBeenCalled() - - // Once a daemon adapter owns the id, attach routes to that adapter. - daemonSessions.push('wt-1@@learned') - await expect(provider.attach('wt-1@@learned')).resolves.toBeUndefined() - expect(current.attach).toHaveBeenCalledWith('wt-1@@learned') - expect(fallback.attach).not.toHaveBeenCalled() - }) - - it('forwards the owning daemon sequence from attach', async () => { - const legacy = createDaemonAdapter('legacy', ['daemon-session']) - const providerSequence = { value: 204, generation: 'continued' as const } - vi.mocked(legacy.attach).mockResolvedValueOnce({ providerSequence }) - const provider = new DegradedDaemonPtyProvider({ - current: createDaemonAdapter('current'), - legacy: [legacy], - fallback: createProvider('fallback') - }) - await provider.discoverDaemonSessions() - - await expect(provider.attach('daemon-session')).resolves.toEqual({ providerSequence }) - }) - - it('only delegates owner-listing authority to the provider that owns the id', async () => { - const current = createDaemonAdapter('daemon', ['daemon-session']) - const fallback = createProvider('fallback', [], true) - const provider = new DegradedDaemonPtyProvider({ current, legacy: [], fallback }) - await provider.discoverDaemonSessions() - - expect(provider.providesAgentSessionOwnerListings('daemon-session')).toBe(true) - expect(provider.providesAgentSessionOwnerListings('unknown-session')).toBe(false) - }) - - it('routes fresh foreground confirmation to the session owner', async () => { - const current = createDaemonAdapter('daemon', ['daemon-session']) - const fallback = createProvider('fallback') - const provider = new DegradedDaemonPtyProvider({ current, legacy: [], fallback }) - await provider.discoverDaemonSessions() - const fresh = await provider.spawn({ cols: 80, rows: 24 }) - - await expect(provider.confirmForegroundProcess('daemon-session')).resolves.toBe( - 'daemon-confirmed' - ) - await expect(provider.confirmForegroundProcess(fresh.id)).resolves.toBe('fallback-confirmed') - }) - - it('routes discovered daemon sessions to the daemon and fresh PTYs to the fallback', async () => { - const current = createDaemonAdapter('daemon', ['daemon-session']) - const fallback = createProvider('fallback') - const provider = new DegradedDaemonPtyProvider({ current, legacy: [], fallback }) - - await provider.discoverDaemonSessions() - - await provider.spawn({ sessionId: 'daemon-session', cols: 80, rows: 24 }) - const fresh = await provider.spawn({ cols: 80, rows: 24 }) - provider.write('daemon-session', 'old\n') - provider.write(fresh.id, 'new\n') - - expect(current.spawn).toHaveBeenCalledWith({ sessionId: 'daemon-session', cols: 80, rows: 24 }) - expect(fallback.spawn).toHaveBeenCalledWith({ cols: 80, rows: 24 }) - expect(current.write).toHaveBeenCalledWith('daemon-session', 'old\n') - expect(fallback.write).toHaveBeenCalledWith(fresh.id, 'new\n') - }) - - it('preserves settlement through daemon and fallback routes', async () => { - const current = createDaemonAdapter('daemon', ['daemon-session']) - const fallback = createProvider('fallback') - vi.mocked(current.writeWithSettlement).mockResolvedValue(stubWriteSettlement(false)) - const provider = new DegradedDaemonPtyProvider({ current, legacy: [], fallback }) - await provider.discoverDaemonSessions() - const fresh = await provider.spawn({ cols: 80, rows: 24 }) - - await expect(provider.writeWithSettlement('daemon-session', 'old')).resolves.toEqual( - stubWriteSettlement(false) - ) - await expect(provider.writeWithSettlement(fresh.id, 'new')).resolves.toEqual( - stubWriteSettlement(true) - ) - expect(current.writeWithSettlement).toHaveBeenCalledWith('daemon-session', 'old') - expect(fallback.writeWithSettlement).toHaveBeenCalledWith(fresh.id, 'new') - }) - - it('routes later fresh PTYs to the daemon after spawn health recovers', async () => { - const current = createDaemonAdapter('daemon') - const fallback = createProvider('fallback') - const probeCurrentDaemonSpawn = vi - .fn<() => Promise>() - .mockResolvedValueOnce(false) - .mockResolvedValueOnce(true) - const provider = new DegradedDaemonPtyProvider({ - current, - legacy: [], - fallback, - probeCurrentDaemonSpawn - }) - const now = vi.spyOn(Date, 'now').mockReturnValue(1_000) - - try { - await expect(provider.recoverFreshSpawnRouting()).resolves.toBe(false) - await provider.spawn({ cols: 80, rows: 24 }) - await expect(provider.recoverFreshSpawnRouting()).resolves.toBe(false) - expect(probeCurrentDaemonSpawn).toHaveBeenCalledOnce() - now.mockReturnValue(1_000 + DEGRADED_DAEMON_RECOVERY_RETRY_MS) - await expect(provider.recoverFreshSpawnRouting()).resolves.toBe(true) - const recovered = await provider.spawn({ cols: 80, rows: 24, worktreeId: 'wt-1' }) - - expect(provider.routesFreshSpawnsToLocalProvider).toBeUndefined() - expect(provider.isDegraded).toBe(true) - expect(provider.supportsGitCredentialGuardHost()).toBe(true) - expect(fallback.spawn).toHaveBeenCalledOnce() - expect(current.spawn).toHaveBeenCalledWith({ cols: 80, rows: 24, worktreeId: 'wt-1' }) - expect(recovered.id).toBe('daemon-new') - expect(provider.canProvideAuthoritativeBufferSnapshot(recovered.id)).toBe(true) - } finally { - now.mockRestore() - } - }) - - it('coalesces concurrent fresh-spawn recovery probes', async () => { - let resolveProbe: ((healthy: boolean) => void) | undefined - const probeCurrentDaemonSpawn = vi.fn( - () => - new Promise((resolve) => { - resolveProbe = resolve - }) - ) - const provider = new DegradedDaemonPtyProvider({ - current: createDaemonAdapter('daemon'), - legacy: [], - fallback: createProvider('fallback'), - probeCurrentDaemonSpawn - }) - - const first = provider.recoverFreshSpawnRouting() - const second = provider.recoverFreshSpawnRouting() - expect(probeCurrentDaemonSpawn).toHaveBeenCalledOnce() - resolveProbe?.(true) - - await expect(Promise.all([first, second])).resolves.toEqual([true, true]) - await expect(provider.recoverFreshSpawnRouting()).resolves.toBe(true) - expect(probeCurrentDaemonSpawn).toHaveBeenCalledOnce() - }) - - it('does not retain recovered daemon ownership after exit beats the spawn reply', async () => { - const current = createDaemonAdapter('daemon') - const provider = new DegradedDaemonPtyProvider({ - current, - legacy: [], - fallback: createProvider('fallback'), - probeCurrentDaemonSpawn: vi.fn(async () => true) - }) - vi.mocked(current.spawn).mockImplementation(async () => { - current.emitExit('daemon-fast-exit', 0) - return { id: 'daemon-fast-exit', exitedBeforeSpawnReply: true } - }) - - await provider.recoverFreshSpawnRouting() - await expect(provider.spawn({ cols: 80, rows: 24 })).resolves.toMatchObject({ - id: 'daemon-fast-exit', - exitedBeforeSpawnReply: true - }) - - expect(provider.getCurrentDaemonSessionIds()).toEqual([]) - }) - - it('routes a previously daemon-backed id to fallback after daemon exit removes the mapping', async () => { - const current = createDaemonAdapter('daemon', ['daemon-session']) - const fallback = createProvider('fallback') - const provider = new DegradedDaemonPtyProvider({ current, legacy: [], fallback }) - - await provider.discoverDaemonSessions() - current.emitExit('daemon-session', 0) - await provider.spawn({ sessionId: 'daemon-session', cols: 80, rows: 24 }) - - expect(fallback.spawn).toHaveBeenCalledWith({ - sessionId: 'daemon-session', - cols: 80, - rows: 24 - }) - }) - - it('caches a provider discovered by hasPty before routing later operations', () => { - const current = createDaemonAdapter('daemon', ['daemon-session']) - const fallback = createProvider('fallback') - const provider = new DegradedDaemonPtyProvider({ current, legacy: [], fallback }) - - expect(provider.hasPty('daemon-session')).toBe(true) - provider.write('daemon-session', 'kept-on-daemon\n') - - expect(current.write).toHaveBeenCalledWith('daemon-session', 'kept-on-daemon\n') - expect(fallback.write).not.toHaveBeenCalled() - }) - - it('probes daemon owners without borrowing fallback liveness', async () => { - const currentSessions: string[] = [] - const current = createDaemonAdapter('current', currentSessions) - const legacy = createDaemonAdapter('legacy') - const fallback = createProvider('fallback', ['unknown-session']) - const provider = new DegradedDaemonPtyProvider({ current, legacy: [legacy], fallback }) - vi.mocked(legacy.listProcesses).mockRejectedValue(new Error('legacy inventory unavailable')) - - await expect(provider.probePtyLiveness('unknown-session')).resolves.toBeNull() - expect(fallback.probePtyLiveness).not.toHaveBeenCalled() - - currentSessions.push('unknown-session') - await expect(provider.probePtyLiveness('unknown-session')).resolves.toBe(true) - }) - - it('reports a routed fallback PTY live before probing daemon absence', async () => { - const current = createDaemonAdapter('current') - const legacy = createDaemonAdapter('legacy') - const fallback = createProvider('fallback') - const provider = new DegradedDaemonPtyProvider({ current, legacy: [legacy], fallback }) - const fresh = await provider.spawn({ cols: 80, rows: 24 }) - - await expect(provider.probePtyLiveness(fresh.id)).resolves.toBe(true) - expect(fallback.hasPty).toHaveBeenCalledWith(fresh.id) - expect(current.listProcesses).not.toHaveBeenCalled() - expect(legacy.listProcesses).not.toHaveBeenCalled() - }) - - it('routes authoritative recovery snapshots to the owning daemon', async () => { - const current = createDaemonAdapter('daemon', ['daemon-session']) - const fallback = createProvider('fallback') - const snapshot = { - data: 'alt frame', - scrollbackAnsi: 'normal history', - cols: 80, - rows: 24, - seq: 42, - source: 'headless' as const - } - current.getBufferSnapshot = vi.fn(async () => snapshot) - const provider = new DegradedDaemonPtyProvider({ current, legacy: [], fallback }) - - await provider.discoverDaemonSessions() - - await expect( - provider.getBufferSnapshot('daemon-session', { scrollbackRows: 50_000 }) - ).resolves.toEqual(snapshot) - expect(current.getBufferSnapshot).toHaveBeenCalledWith('daemon-session', { - scrollbackRows: 50_000 - }) - }) - - it('forwards replay output from fallback and daemon providers', () => { - const current = createDaemonAdapter('daemon') - const fallback = createProvider('fallback') - const provider = new DegradedDaemonPtyProvider({ current, legacy: [], fallback }) - const replaySpy = vi.fn() - - const unsubscribe = provider.onReplay(replaySpy) - current.emitReplay('daemon-session', 'daemon replay') - fallback.emitReplay('fallback-session', 'fallback replay') - unsubscribe() - current.emitReplay('daemon-session', 'after unsubscribe') - - expect(replaySpy).toHaveBeenCalledTimes(2) - expect(replaySpy).toHaveBeenNthCalledWith(1, { - id: 'daemon-session', - data: 'daemon replay' - }) - expect(replaySpy).toHaveBeenNthCalledWith(2, { - id: 'fallback-session', - data: 'fallback replay' - }) - }) - - it('preserves explicit sequence accounting on daemon data events', () => { - const current = createDaemonAdapter('daemon') - const fallback = createProvider('fallback') - const provider = new DegradedDaemonPtyProvider({ current, legacy: [], fallback }) - const dataSpy = vi.fn() - provider.onData(dataSpy) - - current.emitData('daemon-session', '\x1b[6n', 0) - - expect(dataSpy).toHaveBeenCalledWith({ - id: 'daemon-session', - data: '\x1b[6n', - sequenceChars: 0 - }) - }) - - it('detaches provider subscriptions without disposing the underlying providers', () => { - const current = createDaemonAdapter('daemon') - const fallback = createProvider('fallback') - const provider = new DegradedDaemonPtyProvider({ current, legacy: [], fallback }) - const dataSpy = vi.fn() - const exitSpy = vi.fn() - provider.onData(dataSpy) - provider.onExit(exitSpy) - - provider.disposeProviderOnly() - current.emitData('daemon-session', 'data') - fallback.emitExit('fallback-session', 0) - - expect(dataSpy).not.toHaveBeenCalled() - expect(exitSpy).not.toHaveBeenCalled() - expect(current.dispose).not.toHaveBeenCalled() - }) - - it('shuts down fallback sessions before a daemon-provider swap', async () => { - const current = createDaemonAdapter('daemon') - const fallback = createProvider('fallback') - const provider = new DegradedDaemonPtyProvider({ current, legacy: [], fallback }) - - const fresh = await provider.spawn({ cols: 80, rows: 24 }) - const killedCount = await provider.shutdownFallbackSessions() - - expect(killedCount).toBe(1) - expect(fallback.shutdown).toHaveBeenCalledWith(fresh.id, { immediate: true }) - expect(provider.hasPty(fresh.id)).toBe(false) - }) - - it('is best-effort: counts only successful shutdowns and never throws (keeps restart alive)', async () => { - const current = createDaemonAdapter('daemon') - const fallback = createProvider('fallback') - const provider = new DegradedDaemonPtyProvider({ current, legacy: [], fallback }) - const stuck = await provider.spawn({ sessionId: 'stuck', cols: 80, rows: 24 }) - await provider.spawn({ sessionId: 'ok', cols: 80, rows: 24 }) - const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) - vi.mocked(fallback.shutdown).mockImplementation(async (id: string) => { - if (id === stuck.id) { - throw new Error('still alive') - } - }) - - // Why: a single un-killable local PTY must not abort the daemon restart. - const killedCount = await provider.shutdownFallbackSessions() - - // Best-effort: the one that shut down is counted, the stuck one is not, and - // crucially it does not throw — so the daemon restart sequence proceeds. - expect(killedCount).toBe(1) - expect(warn).toHaveBeenCalled() - expect(fallback.shutdown).toHaveBeenCalledWith('stuck', { immediate: true }) - expect(fallback.shutdown).toHaveBeenCalledWith('ok', { immediate: true }) - warn.mockRestore() - }) - - it('fans synthetic exits for discovered current-daemon sessions only', async () => { - const current = createDaemonAdapter('daemon', ['current-session']) - const legacy = createDaemonAdapter('legacy', ['legacy-session']) - const fallback = createProvider('fallback') - const provider = new DegradedDaemonPtyProvider({ current, legacy: [legacy], fallback }) - const exitSpy = vi.fn() - provider.onExit(exitSpy) - - await provider.discoverDaemonSessions() - provider.fanoutCurrentDaemonSyntheticExits(-1) - - expect(exitSpy).toHaveBeenCalledOnce() - expect(exitSpy).toHaveBeenCalledWith({ id: 'current-session', code: -1 }) - expect(provider.getCurrentDaemonSessionIds()).toEqual([]) - expect(provider.hasPty('legacy-session')).toBe(true) - }) - - it('keeps an exited legacy daemon poisoning listProcesses after construction', async () => { - const current = createDaemonAdapter('daemon', ['current-session']) - const legacy = createDaemonAdapter('legacy', ['legacy-session']) - const fallback = createProvider('fallback', ['fallback-session']) - const provider = new DegradedDaemonPtyProvider({ current, legacy: [legacy], fallback }) - await provider.discoverDaemonSessions() - vi.mocked(legacy.listProcesses).mockRejectedValue(new Error('legacy exited')) - - await expect(provider.listProcesses()).rejects.toThrow('legacy exited') - await expect(provider.listProcesses()).rejects.toThrow('legacy exited') - expect(provider.getLegacyAdapters()).toEqual([legacy]) - expect(current.listProcesses).toHaveBeenCalledTimes(3) - expect(fallback.listProcesses).toHaveBeenCalledTimes(3) - }) -}) diff --git a/src/main/daemon/degraded-daemon-pty-provider.ts b/src/main/daemon/degraded-daemon-pty-provider.ts deleted file mode 100644 index 2b50424ae830..000000000000 --- a/src/main/daemon/degraded-daemon-pty-provider.ts +++ /dev/null @@ -1,360 +0,0 @@ -import type { DaemonPtyAdapter } from './daemon-pty-adapter' -import { combineUnsubscribes } from './combine-unsubscribes' -import { shutdownDegradedFallbackSessions } from './degraded-daemon-fallback-shutdown' -import { inspectPtyProviderProcess } from '../providers/pty-process-inspection' -import type { - IPtyProvider, - PtyBackgroundStreamEvent, - PtyDataEvent, - PtyProviderBufferSnapshot, - PtyProcessInfo, - PtySpawnOptions, - PtySpawnResult -} from '../providers/types' -import { - adoptOwningProvider, - attachDaemonOwnedSession, - findDaemonAdapter, - listProviderSessionIds -} from './degraded-daemon-session-routing' -import { DegradedDaemonFreshSpawnRouter } from './degraded-daemon-fresh-spawn-routing' -import { DegradedDaemonOwnerRecovery } from './degraded-daemon-owner-recovery' -import type { WriteSettlement } from '../../shared/pty-write-settlement' - -export class DegradedDaemonPtyProvider implements IPtyProvider { - readonly isDegraded = true - - private current: DaemonPtyAdapter - private legacy: DaemonPtyAdapter[] - private fallback: IPtyProvider - private sessionProviders = new Map() - private freshSpawns: DegradedDaemonFreshSpawnRouter - private ownerRecovery: DegradedDaemonOwnerRecovery - private unsubscribers: (() => void)[] = [] - private dataListeners: ((payload: PtyDataEvent) => void)[] = [] - private exitListeners: ((payload: { id: string; code: number }) => void)[] = [] - - constructor(opts: { - current: DaemonPtyAdapter - legacy: DaemonPtyAdapter[] - fallback: IPtyProvider - probeCurrentDaemonSpawn?: () => Promise - }) { - this.current = opts.current - this.legacy = opts.legacy - this.fallback = opts.fallback - this.freshSpawns = new DegradedDaemonFreshSpawnRouter( - opts.current, - opts.fallback, - this.sessionProviders, - opts.probeCurrentDaemonSpawn ?? null - ) - this.ownerRecovery = new DegradedDaemonOwnerRecovery( - this.allProviders(), - this.allDaemonAdapters(), - this.sessionProviders, - (spawnOpts) => this.freshSpawns.spawn(spawnOpts) - ) - - for (const provider of this.allProviders()) { - this.unsubscribers.push( - provider.onData((payload) => this.dataListeners.forEach((listener) => listener(payload))), - provider.onExit((payload) => { - this.ownerRecovery.forgetRoute(payload.id) - this.exitListeners.forEach((listener) => listener(payload)) - }) - ) - } - this.unsubscribers.push(...this.ownerRecovery.subscribeIdentityChanges()) - } - - async discoverDaemonSessions(): Promise { - await this.ownerRecovery.discoverRoutes() - } - - get routesFreshSpawnsToLocalProvider(): true | undefined { - return this.freshSpawns.routesToFallback - } - - recoverFreshSpawnRouting = (): Promise => this.freshSpawns.recover() - - supportsGitCredentialGuardHost = (id?: string): boolean => - this.freshSpawns.supportsGitGuardHost(id) - - canProvideAuthoritativeBufferSnapshot = (id: string): boolean => - this.freshSpawns.canProvideSnapshot(id) - - spawn = (opts: PtySpawnOptions): Promise => this.ownerRecovery.spawn(opts) - - // Why refuse the fallback route (unknown ids resolve to it): see attachDaemonOwnedSession. - attach = (id: string): ReturnType => - attachDaemonOwnedSession(this.providerFor(id), this.fallback, id) - - hasPty(id: string): boolean { - const mapped = this.sessionProviders.get(id) - return mapped ? (mapped.hasPty?.(id) ?? true) : this.findProviderForExistingSession(id) !== null - } - - async probePtyLiveness(id: string): Promise { - const mapped = this.sessionProviders.get(id) - if (mapped && (mapped.hasPty?.(id) ?? true)) { - return true - } - return await this.ownerRecovery.probe(id) - } - - // Why: an unknown id cannot borrow listing authority from the fresh-spawn provider. - providesAgentSessionOwnerListings = (ptyId: string): boolean => - ( - this.sessionProviders.get(ptyId) ?? this.findProviderForExistingSession(ptyId) - )?.providesAgentSessionOwnerListings?.(ptyId) === true - - write(id: string, data: string): boolean | void { - return this.providerFor(id).write(id, data) - } - - async writeWithSettlement(id: string, data: string): Promise { - return await this.providerFor(id).writeWithSettlement(id, data) - } - - resize(id: string, cols: number, rows: number): void { - this.providerFor(id).resize(id, cols, rows) - } - - pauseProducer(id: string): void { - this.providerFor(id).pauseProducer?.(id) - } - - resumeProducer(id: string): void { - this.providerFor(id).resumeProducer?.(id) - } - - setPtyBackgrounded(id: string, background: boolean): void { - this.providerFor(id).setPtyBackgrounded?.(id, background) - } - - async shutdown( - id: string, - opts: { immediate?: boolean; keepHistory?: boolean; deadlineMs?: number } - ): Promise { - await this.providerFor(id).shutdown(id, opts) - if (!opts.keepHistory) { - this.sessionProviders.delete(id) - } - } - - async sendSignal(id: string, signal: string): Promise { - await this.providerFor(id).sendSignal(id, signal) - } - - async getCwd(id: string): Promise { - return this.providerFor(id).getCwd(id) - } - - async getInitialCwd(id: string): Promise { - return this.providerFor(id).getInitialCwd(id) - } - - async getAppliedSize(id: string): Promise<{ cols: number; rows: number } | null> { - return (await this.providerFor(id).getAppliedSize?.(id)) ?? null - } - - async getBufferSnapshot( - id: string, - opts?: { scrollbackRows?: number } - ): Promise { - // Why: recovery must reach the legacy adapter that owns the thinned session model. - return (await this.providerFor(id).getBufferSnapshot?.(id, opts)) ?? null - } - - clearBuffer = (id: string): Promise => this.providerFor(id).clearBuffer(id) - - async closeStartupQueryAuthority(id: string): Promise { - return (await this.providerFor(id).closeStartupQueryAuthority?.(id)) ?? 0 - } - - acknowledgeDataEvent(id: string, charCount: number): void { - this.providerFor(id).acknowledgeDataEvent(id, charCount) - } - - async hasChildProcesses(id: string): Promise { - return this.providerFor(id).hasChildProcesses(id) - } - - async getForegroundProcess(id: string): Promise { - return this.providerFor(id).getForegroundProcess(id) - } - inspectProcess(id: string) { - return this.hasPty(id) - ? inspectPtyProviderProcess(this.providerFor(id), id) - : Promise.reject(new Error('terminal_gone')) - } - async confirmForegroundProcess(id: string): Promise { - return this.providerFor(id).confirmForegroundProcess?.(id) ?? null - } - async confirmShellForeground(id: string): Promise { - return (await this.providerFor(id).confirmShellForeground?.(id)) ?? false - } - - async serialize(ids: string[]): Promise { - return this.fallback.serialize(ids) - } - - async revive(state: string): Promise { - await this.fallback.revive(state) - } - - async listProcesses(opts?: { deadlineMs?: number }): Promise { - const results = await Promise.all( - this.allProviders().map((provider) => provider.listProcesses(opts)) - ) - return results.flat() - } - - async getDefaultShell(): Promise { - return this.fallback.getDefaultShell() - } - - async getProfiles(): Promise<{ name: string; path: string }[]> { - return this.fallback.getProfiles() - } - - onData(callback: (payload: PtyDataEvent) => void): () => void { - this.dataListeners.push(callback) - return () => { - const idx = this.dataListeners.indexOf(callback) - if (idx !== -1) { - this.dataListeners.splice(idx, 1) - } - } - } - - onBackgroundStreamEvent(callback: (payload: PtyBackgroundStreamEvent) => void): () => void { - return combineUnsubscribes( - this.allProviders().flatMap((provider) => provider.onBackgroundStreamEvent?.(callback) ?? []) - ) - } - - // Why: main subscribes on the routed provider, so without this the dead-endpoint - // fan-out reaches no listener and only the written pane recovers (STA-2373). Daemon - // adapters only — the local fallback has no dead-socket problem. - onWriteUnavailable(callback: (payload: { id: string }) => void): () => void { - return combineUnsubscribes( - this.allDaemonAdapters().map((adapter) => adapter.onWriteUnavailable(callback)) - ) - } - - onReplay(callback: (payload: { id: string; data: string }) => void): () => void { - const unsubscribes = this.allProviders().map((provider) => provider.onReplay(callback)) - let active = true - const trackedUnsubscribe = (): void => { - if (!active) { - return - } - active = false - const idx = this.unsubscribers.indexOf(trackedUnsubscribe) - if (idx !== -1) { - this.unsubscribers.splice(idx, 1) - } - combineUnsubscribes(unsubscribes)() - } - this.unsubscribers.push(trackedUnsubscribe) - return trackedUnsubscribe - } - - onExit(callback: (payload: { id: string; code: number }) => void): () => void { - this.exitListeners.push(callback) - return () => { - const idx = this.exitListeners.indexOf(callback) - if (idx !== -1) { - this.exitListeners.splice(idx, 1) - } - } - } - - ackColdRestore(sessionId: string): void { - findDaemonAdapter(this.sessionProviders, this.allDaemonAdapters(), sessionId)?.ackColdRestore( - sessionId - ) - } - - clearTombstone(sessionId: string): void { - findDaemonAdapter(this.sessionProviders, this.allDaemonAdapters(), sessionId)?.clearTombstone( - sessionId - ) - } - - async reconcileOnStartup(validWorktreeIds: Set): Promise<{ - alive: string[] - killed: string[] - }> { - return await this.ownerRecovery.reconcileOnStartup(validWorktreeIds) - } - - dispose(): void { - this.disposeProviderOnly() - for (const adapter of this.allDaemonAdapters()) { - adapter.dispose() - } - } - - disposeProviderOnly(): void { - combineUnsubscribes(this.unsubscribers.splice(0))() - } - - async shutdownFallbackSessions(): Promise { - return shutdownDegradedFallbackSessions(this.sessionProviders, this.fallback) - } - - getCurrentDaemonSessionIds(): string[] { - return listProviderSessionIds(this.sessionProviders, this.current) - } - - fanoutCurrentDaemonSyntheticExits(code: number): void { - for (const id of this.getCurrentDaemonSessionIds()) { - this.sessionProviders.delete(id) - // Why: restart kills listed sessions even when the adapter did not track them active. - // oxlint-disable-next-line unicorn/no-useless-spread -- copy-safe: listeners may unsubscribe during iteration - for (const listener of [...this.exitListeners]) { - listener({ id, code }) - } - } - } - - async disconnectOnly(): Promise { - this.disposeProviderOnly() - await Promise.all(this.allDaemonAdapters().map((adapter) => adapter.disconnectOnly())) - } - - getCurrentAdapter(): DaemonPtyAdapter { - return this.current - } - - getLegacyAdapters(): readonly DaemonPtyAdapter[] { - return this.legacy - } - - getAllAdapters(): readonly DaemonPtyAdapter[] { - return this.allDaemonAdapters() - } - - private providerFor(sessionId: string): IPtyProvider { - return ( - this.sessionProviders.get(sessionId) ?? - this.findProviderForExistingSession(sessionId) ?? - this.fallback - ) - } - - private findProviderForExistingSession(sessionId: string): IPtyProvider | null { - return adoptOwningProvider(this.sessionProviders, this.allProviders(), sessionId) - } - - private allProviders(): IPtyProvider[] { - return [this.fallback, ...this.allDaemonAdapters()] - } - - private allDaemonAdapters(): DaemonPtyAdapter[] { - return [this.current, ...this.legacy] - } -} diff --git a/src/main/daemon/degraded-daemon-session-routing.ts b/src/main/daemon/degraded-daemon-session-routing.ts deleted file mode 100644 index 7fb5a9a00c8c..000000000000 --- a/src/main/daemon/degraded-daemon-session-routing.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { IPtyProvider } from '../providers/types' -import type { DaemonPtyAdapter } from './daemon-pty-adapter' -import { SessionNotFoundError } from './daemon-errors' - -export function listProviderSessionIds( - sessionProviders: ReadonlyMap, - provider: IPtyProvider -): string[] { - return [...sessionProviders] - .filter(([, mappedProvider]) => mappedProvider === provider) - .map(([id]) => id) -} - -/** Attach-only session adoption: refuses the in-process fallback route. A - * fallback pty cannot own a daemon-surviving session by definition, and its - * no-op attach resolving would pin a subscriber-driven attach as succeeded - * while the stream stays blank. */ -export async function attachDaemonOwnedSession( - owner: IPtyProvider, - fallback: IPtyProvider, - sessionId: string -): ReturnType { - if (owner === fallback) { - throw new SessionNotFoundError(sessionId) - } - return await owner.attach(sessionId) -} - -/** Probes providers for an id absent from the routing map and adopts the - * first proven owner into the map. */ -export function adoptOwningProvider( - sessionProviders: Map, - providers: readonly IPtyProvider[], - sessionId: string -): IPtyProvider | null { - for (const provider of providers) { - if (provider.hasPty?.(sessionId) === true) { - sessionProviders.set(sessionId, provider) - return provider - } - } - return null -} - -export function findDaemonAdapter( - sessionProviders: ReadonlyMap, - daemonAdapters: readonly DaemonPtyAdapter[], - sessionId: string -): DaemonPtyAdapter | null { - const provider = sessionProviders.get(sessionId) - return provider && daemonAdapters.includes(provider as DaemonPtyAdapter) - ? (provider as DaemonPtyAdapter) - : null -} diff --git a/src/main/daemon/headless-emulator-wide-char-repaint.test.ts b/src/main/daemon/headless-emulator-wide-char-repaint.test.ts index e183c8735f34..db6b25f8a7d8 100644 --- a/src/main/daemon/headless-emulator-wide-char-repaint.test.ts +++ b/src/main/daemon/headless-emulator-wide-char-repaint.test.ts @@ -5,7 +5,7 @@ * The reporter's decisive run had the command echo doubled while that same * command's output was clean, so the shell never received doubled bytes; and the * doubled text pasted doubled into Notepad, so it was really in the buffer. A - * stream-level assertion (windows-conpty-wide-char-duplication.node-pty.test.ts) + * stream-level assertion (windows-conpty-wide-char-duplication.bun.test.ts) * cannot see that: every byte of a bad repaint is legitimate — cursor * positioning plus text — and only the cells it lands on are wrong. * diff --git a/src/main/daemon/linux-packaged-terminal-floor-fixture.ts b/src/main/daemon/linux-packaged-terminal-floor-fixture.ts new file mode 100644 index 000000000000..e0363b321232 --- /dev/null +++ b/src/main/daemon/linux-packaged-terminal-floor-fixture.ts @@ -0,0 +1,154 @@ +import assert from 'node:assert/strict' +import { createHash, randomUUID } from 'node:crypto' +import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join, resolve } from 'node:path' +import { setTimeout as delay } from 'node:timers/promises' +export { runProcess } from '../../shared/child-process/run-process' +import { setAppEnvironment } from '../../shared/app-environment' +import { DaemonClient } from './client' +import { launchDaemonChild, terminateLaunchedDaemonChild } from './daemon-launched-child' +import { DaemonPtyAdapter } from './daemon-pty-adapter' +import { HistoryReader } from './history-reader' + +export async function qualifyPackagedTerminal(resourcesDirectory: string) { + const resources = resolve(resourcesDirectory) + const runtime = join(resources, 'cli-runtime', 'bun-runtime') + const entry = join(resources, 'terminal-daemon', 'daemon-entry.js') + for (const path of [runtime, entry]) { + assert(existsSync(path), `Missing packaged terminal artifact: ${path}`) + } + const root = mkdtempSync(join(tmpdir(), 'orca-terminal-floor-')) + setAppEnvironment({ + getPath: () => root, + getAppPath: () => resources, + getVersion: () => 'floor-qualification', + isPackaged: () => true, + onWillQuit: () => {}, + exit: () => {}, + getAppMetrics: () => [] + }) + process.env.ORCA_DIAGNOSTICS_DISABLED = '1' + process.env.ORCA_BACKGROUND_LAUNCH = '1' + const socketPath = join(root, 'daemon.sock') + const tokenPath = join(root, 'daemon.token') + const historyPath = join(root, 'history') + const options = { socketPath, tokenPath, historyPath, profileScope: root } + const control = new DaemonClient({ socketPath, tokenPath }) + let adapter: DaemonPtyAdapter | undefined + let launched: Awaited> | undefined + let daemonExited = false + let output = '' + const exits: { id: string; code: number }[] = [] + const waitFor = async (predicate: () => boolean, label: string) => { + const deadline = Date.now() + 15_000 + while (!predicate()) { + assert(Date.now() < deadline, `Timed out: ${label}; daemonExited=${daemonExited}; ${output}`) + await delay(20) + } + } + const connectAdapter = () => { + const next = new DaemonPtyAdapter(options) + next.onData(({ data }) => { + output = (output + data).slice(-64 * 1024) + }) + next.onExit(({ id, code }) => { + exits.push({ id, code }) + }) + return next + } + try { + launched = await launchDaemonChild({ + entryPath: entry, + forkEntryPath: entry, + relocatedExecPath: runtime, + bunRuntime: true, + userDataPath: root, + socketPath, + tokenPath, + pidPath: join(root, 'daemon.pid'), + launchNonce: randomUUID(), + macosLoginSessionWatch: false + }) + launched.child.on('exit', () => { + daemonExited = true + }) + await control.ensureConnected() + const identity = control.getDaemonIdentity() + adapter = connectAdapter() + const spawned = await adapter.spawn({ + cols: 80, + rows: 24, + cwd: root, + shellOverride: '/bin/sh', + env: { HOME: root, SHELL: '/bin/sh', PS1: '' } + }) + adapter.write( + spawned.id, + 'stty -echo; ORCA_FLOOR_MEMORY=retained; printf \'FLOOR_%s_PID_%s\\n\' ready "$$"\r' + ) + await waitFor(() => /FLOOR_ready_PID_([0-9]+)/.test(output), 'initial shell output') + const shellPid = output.match(/FLOOR_ready_PID_([0-9]+)/)?.[1] + assert(shellPid) + adapter.resize(spawned.id, 103, 37) + assert.equal(daemonExited, false) + await control.request('ping', {}) + // Read the kernel size repeatedly because adapter resize delivery is asynchronous. + for (let attempt = 0; !output.includes('FLOOR_SIZE_37 103') && attempt < 50; attempt++) { + adapter.write(spawned.id, "printf 'FLOOR_SIZE_'; stty size\r") + await delay(50) + } + assert(output.includes('FLOOR_SIZE_37 103'), 'PTY kernel did not receive resize') + const snapshot = await adapter.getBufferSnapshot(spawned.id) + assert(snapshot?.data.includes(`FLOOR_ready_PID_${shellPid}`)) + await adapter.disconnectOnly() + adapter = undefined + const history = await new HistoryReader(historyPath).detectColdRestore(spawned.id) + assert( + history && + (history.snapshotAnsi + history.scrollbackAnsi).includes(`FLOOR_ready_PID_${shellPid}`) + ) + adapter = connectAdapter() + const attached = await adapter.spawn({ + sessionId: spawned.id, + attachOnly: true, + cols: 103, + rows: 37 + }) + assert.equal(attached.id, spawned.id) + assert.deepEqual(adapter.getDaemonIdentity(), identity) + adapter.write(spawned.id, 'printf \'FLOOR_RECONNECT_%s_PID_%s\\n\' "$ORCA_FLOOR_MEMORY" "$$"\r') + await waitFor( + () => output.includes(`FLOOR_RECONNECT_retained_PID_${shellPid}`), + 'retained shell after reconnect' + ) + adapter.write(spawned.id, 'exit 17\r') + await waitFor(() => exits.some((event) => event.id === spawned.id), 'observed shell exit') + assert.deepEqual(exits, [{ id: spawned.id, code: 17 }]) + await adapter.disconnectOnly() + adapter = undefined + await control.request('shutdown', { killSessions: true }) + await waitFor(() => daemonExited, 'daemon graceful shutdown') + return { + passed: true, + platform: process.platform, + arch: process.arch, + clientRuntime: process.version, + electronVersion: process.versions.electron, + runtimeSha256: createHash('sha256').update(readFileSync(runtime)).digest('hex'), + entrySha256: createHash('sha256').update(readFileSync(entry)).digest('hex'), + shellPid: Number(shellPid), + resize: true, + reconnect: true, + history: true, + exitCode: 17 + } + } finally { + adapter?.dispose() + control.disconnect() + if (launched && !daemonExited) { + await terminateLaunchedDaemonChild(launched.child) + } + rmSync(root, { recursive: true, force: true }) + } +} diff --git a/src/main/daemon/node-pty-fd-leak.test.ts b/src/main/daemon/node-pty-fd-leak.test.ts deleted file mode 100644 index f021f7d48df4..000000000000 --- a/src/main/daemon/node-pty-fd-leak.test.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { execFileSync, spawn } from 'node:child_process' -import { once } from 'node:events' -import { existsSync, readdirSync, readFileSync, readlinkSync, renameSync } from 'node:fs' -import { setTimeout as delay } from 'node:timers/promises' -import * as pty from 'node-pty' -import { describe, expect, it } from 'vitest' -import { getNodePtySpawnHelperCandidates } from '../providers/local-pty-utils' - -function currentRevokedFdCount(): number { - return execFileSync('lsof', ['-p', String(process.pid)], { encoding: 'utf8' }) - .split('\n') - .filter((line) => line.includes('(revoked)')).length -} - -function currentOpenFdCount(): number { - return execFileSync('lsof', ['-p', String(process.pid)], { encoding: 'utf8' }) - .split('\n') - .filter((line) => line.trim().length > 0).length -} - -function getExistingSpawnHelper(): string { - const helperPath = getNodePtySpawnHelperCandidates().find((candidate) => existsSync(candidate)) - expect(helperPath).toBeTruthy() - return helperPath as string -} - -async function spawnExitingPty(index: number): Promise { - const proc = pty.spawn('/bin/sh', ['-c', 'exit 0'], { - name: 'xterm-256color', - cols: 80, - rows: 24, - cwd: process.cwd(), - env: { ...process.env, ORCA_FD_LEAK_TEST_INDEX: String(index) } - }) - - await new Promise((resolve) => { - proc.onExit(() => resolve()) - }) - ;(proc as unknown as { destroy?: () => void }).destroy?.() -} - -const describeOnDarwin = process.platform === 'darwin' ? describe : describe.skip - -describeOnDarwin('node-pty macOS spawn fd handling', () => { - it('does not leak revoked slave tty fds across exited pty spawns', async () => { - const before = currentRevokedFdCount() - - for (let i = 0; i < 50; i++) { - await spawnExitingPty(i) - } - - await delay(500) - const after = currentRevokedFdCount() - - expect(after - before).toBe(0) - }, 15000) - - it('does not leak fds when native posix_spawn setup fails', async () => { - const helperPath = getExistingSpawnHelper() - const hiddenHelperPath = `${helperPath}.orca-test-hidden` - expect(existsSync(hiddenHelperPath)).toBe(false) - - const before = currentOpenFdCount() - renameSync(helperPath, hiddenHelperPath) - const restoreHelper = (): void => { - if (existsSync(hiddenHelperPath) && !existsSync(helperPath)) { - renameSync(hiddenHelperPath, helperPath) - } - } - process.on('exit', restoreHelper) - try { - for (let i = 0; i < 20; i++) { - expect(() => - pty.spawn('/bin/sh', ['-c', 'exit 0'], { - name: 'xterm-256color', - cols: 80, - rows: 24, - cwd: process.cwd(), - env: { ...process.env, ORCA_FD_LEAK_TEST_INDEX: String(i) } - }) - ).toThrow(/node-pty: posix_spawn failed: ENOENT/) - } - } finally { - restoreHelper() - process.off('exit', restoreHelper) - } - - await delay(500) - const after = currentOpenFdCount() - - expect(after - before).toBe(0) - }, 15000) -}) - -// Linux is the only platform where node-pty takes the forkpty() path, which has no atomic -// O_CLOEXEC. /proc is what makes the inheritance observable, so the assertions live here. -const describeOnLinux = process.platform === 'linux' ? describe : describe.skip - -const O_CLOEXEC = 0o2000000 - -const LISTING_READY = '__fd_listing_ready__' - -function ptyMasterFd(term: pty.IPty): number { - return (term as unknown as { fd: number }).fd -} - -function isCloseOnExec(fd: number): boolean { - const flags = /flags:\s*(\d+)/.exec(readFileSync(`/proc/self/fdinfo/${fd}`, 'utf8')) - expect(flags).toBeTruthy() - return (Number.parseInt(flags![1]!, 8) & O_CLOEXEC) !== 0 -} - -function openFdTargets(pid: number): string[] { - return readdirSync(`/proc/${pid}/fd`).map((entry) => { - try { - return readlinkSync(`/proc/${pid}/fd/${entry}`) - } catch { - return '' - } - }) -} - -describeOnLinux('node-pty Linux forkpty fd handling', () => { - it('marks pty masters close-on-exec so later children cannot inherit them', async () => { - const terms: pty.IPty[] = [] - let child: ReturnType | null = null - try { - for (let i = 0; i < 3; i++) { - terms.push( - pty.spawn('/bin/sh', ['-c', 'sleep 30'], { - name: 'xterm-256color', - cols: 80, - rows: 24, - cwd: process.cwd(), - env: { ...process.env, ORCA_FD_LEAK_TEST_INDEX: String(i) } - }) - ) - } - - // The masters this process owns must not survive an exec in any child it forks later. - expect(terms.map((term) => isCloseOnExec(ptyMasterFd(term)))).toEqual([true, true, true]) - - child = spawn('/bin/sh', ['-c', 'sleep 5'], { stdio: 'ignore' }) - await once(child, 'spawn') - const inherited = openFdTargets(child.pid!).filter((target) => target.includes('ptmx')) - expect(inherited).toEqual([]) - } finally { - child?.kill() - for (const term of terms) { - term.kill() - } - } - }, 15000) - - it('does not hand an earlier pty master to a later pty child', async () => { - const first = pty.spawn('/bin/sh', ['-c', 'sleep 30'], { - name: 'xterm-256color', - cols: 80, - rows: 24, - cwd: process.cwd(), - env: { ...process.env } - }) - try { - // Why the read: the child must not be able to run its listing before onData is armed, or an - // empty capture would satisfy the negative assertion without inspecting a single fd. - const second = pty.spawn( - '/bin/sh', - ['-c', `IFS= read -r _; printf '${LISTING_READY}\\n'; ls -l /proc/self/fd; exit 0`], - { - name: 'xterm-256color', - cols: 200, - rows: 24, - cwd: process.cwd(), - env: { ...process.env } - } - ) - let output = '' - second.onData((data) => { - output += data - }) - second.write('go\n') - await new Promise((resolve) => { - second.onExit(() => resolve()) - }) - await delay(100) - - // The listing is the evidence; assert it arrived before reading anything into its absence. - expect(output).toContain(LISTING_READY) - expect(output).toMatch(/\d+ -> \/dev\/pts\//) - expect(output).not.toMatch(/ptmx/) - } finally { - first.kill() - } - }, 15000) -}) diff --git a/src/main/daemon/node-pty-windows-input-error.win32.test.ts b/src/main/daemon/node-pty-windows-input-error.win32.test.ts deleted file mode 100644 index dd7a0e798379..000000000000 --- a/src/main/daemon/node-pty-windows-input-error.win32.test.ts +++ /dev/null @@ -1,155 +0,0 @@ -import type { Socket } from 'node:net' -import { spawn, type IPty } from 'node-pty' -import { describe, expect, it } from 'vitest' - -type WindowsPtyInternals = IPty & { - _agent: { inSocket: Socket } - _socket: Socket -} - -function waitForOutput(terminal: IPty, marker: string): Promise { - return new Promise((resolve, reject) => { - let output = '' - const timeout = setTimeout( - () => reject(new Error(`Timed out waiting for ${marker}; got ${output}`)), - 10_000 - ) - terminal.onData((chunk) => { - output += chunk - if (output.includes(marker)) { - clearTimeout(timeout) - resolve() - } - }) - }) -} - -function waitForExit(terminal: IPty): Promise { - return new Promise((resolve, reject) => { - const timeout = setTimeout( - () => reject(new Error('Timed out waiting for the failed PTY to exit')), - 10_000 - ) - terminal.onExit(() => { - clearTimeout(timeout) - resolve() - }) - }) -} - -describe.skipIf(process.platform !== 'win32')('node-pty Windows input errors', () => { - it('retires only the failed PTY and keeps a witness writable after ConPTY EAGAIN', async () => { - const uncaught: unknown[] = [] - const uncaughtListener = (error: unknown): void => { - uncaught.push(error) - } - process.on('uncaughtException', uncaughtListener) - - let terminal: IPty | undefined - let witness: IPty | undefined - - try { - const options = { - cwd: process.cwd(), - env: process.env, - useConptyDll: false - } - terminal = spawn(process.env.ComSpec ?? 'cmd.exe', ['/d', '/q'], options) - witness = spawn(process.env.ComSpec ?? 'cmd.exe', ['/d', '/q'], options) - const input = (terminal as WindowsPtyInternals)._agent.inSocket - expect(input.listenerCount('error')).toBeGreaterThan(0) - expect(() => - input.emit('error', Object.assign(new Error('write EAGAIN'), { code: 'EAGAIN' })) - ).not.toThrow() - await waitForExit(terminal) - await new Promise((resolve) => setTimeout(resolve, 1_500)) - witness.write('echo ORCA_CONPTY_WITNESS\r') - await waitForOutput(witness, 'ORCA_CONPTY_WITNESS') - expect(uncaught).toEqual([]) - } finally { - try { - terminal?.kill() - } catch {} - try { - witness?.kill() - } catch {} - // ConPTY's worker drains asynchronously; keep the guard installed through - // the delayed close so cleanup cannot reintroduce an unhandled error. - await new Promise((resolve) => setTimeout(resolve, 1_500)) - process.off('uncaughtException', uncaughtListener) - } - }, 20_000) - - it('ignores a late output EPIPE after the PTY has closed', async () => { - const uncaught: unknown[] = [] - const uncaughtListener = (error: unknown): void => { - uncaught.push(error) - } - process.on('uncaughtException', uncaughtListener) - - let terminal: IPty | undefined - let exited = false - try { - terminal = spawn(process.env.ComSpec ?? 'cmd.exe', ['/d', '/q'], { - cwd: process.cwd(), - env: process.env, - useConptyDll: false - }) - const output = (terminal as WindowsPtyInternals)._socket - const exit = waitForExit(terminal).then(() => { - exited = true - }) - terminal.kill() - await exit - - expect(() => { - output.emit( - 'error', - Object.assign(new Error('This socket has been ended by the other party'), { - code: 'EPIPE' - }) - ) - }).not.toThrow() - expect(uncaught).toEqual([]) - } finally { - if (!exited) { - try { - terminal?.kill() - } catch {} - } - await new Promise((resolve) => setTimeout(resolve, 1_500)) - process.off('uncaughtException', uncaughtListener) - } - }, 20_000) - - it('contains an output EPIPE that races with PTY shutdown', async () => { - const uncaught: unknown[] = [] - const uncaughtListener = (error: unknown): void => { - uncaught.push(error) - } - process.on('uncaughtException', uncaughtListener) - - let terminal: IPty | undefined - try { - terminal = spawn(process.env.ComSpec ?? 'cmd.exe', ['/d', '/q'], { - cwd: process.cwd(), - env: process.env, - useConptyDll: false - }) - const output = (terminal as WindowsPtyInternals)._socket - const exit = waitForExit(terminal) - expect(() => { - output.emit('error', Object.assign(new Error('write EPIPE'), { code: 'EPIPE' })) - terminal?.kill() - }).not.toThrow() - await exit - expect(uncaught).toEqual([]) - } finally { - try { - terminal?.kill() - } catch {} - await new Promise((resolve) => setTimeout(resolve, 1_500)) - process.off('uncaughtException', uncaughtListener) - } - }, 20_000) -}) diff --git a/src/main/daemon/pty-subprocess-cwd-cancel-identity.test.ts b/src/main/daemon/pty-subprocess-cwd-cancel-identity.test.ts index 7f8131f7fcca..b66efe37b1ec 100644 --- a/src/main/daemon/pty-subprocess-cwd-cancel-identity.test.ts +++ b/src/main/daemon/pty-subprocess-cwd-cancel-identity.test.ts @@ -1,8 +1,9 @@ +import type { BunPtySpawnArgs } from './pty-subprocess/bun-pty-process-contract' // A canceled cwd probe must leave the daemon as the one cancellation identity // the wire carries. Clients key recovery off it, and an unrecognized message // takes the rollback branch that closes a terminal the user still has (#7718). import { describe, expect, it, vi } from 'vitest' -import type * as LocalPtyUtils from '../providers/local-pty-utils' +import type * as PtySpawnValidation from '../providers/pty-spawn-validation' const { spawnMock, @@ -20,11 +21,18 @@ const { validateWorkingDirectoryAsyncMock: vi.fn() })) -vi.mock('node-pty', () => ({ spawn: spawnMock })) -vi.mock('../pwsh', () => ({ isPwshAvailable: isPwshAvailableMock })) +vi.mock('./pty-subprocess/bun-pty-process', () => ({ + canUseBunPty: () => true, + spawnBunPty: ({ file, args, ...options }: BunPtySpawnArgs) => + spawnMock(file, args, { ...options, name: options.env.TERM ?? 'xterm-256color' }) +})) +vi.mock('../pwsh', () => ({ + isPwshAvailable: isPwshAvailableMock, + isPwshAvailableAsync: isPwshAvailableMock +})) -vi.mock('../providers/local-pty-utils', async (importOriginal) => { - const actual = await importOriginal() +vi.mock('../providers/pty-spawn-validation', async (importOriginal) => { + const actual = await importOriginal() return { ...actual, resolveUnixShellPath: resolveUnixShellPathMock, diff --git a/src/main/daemon/pty-subprocess-env-inheritance.test.ts b/src/main/daemon/pty-subprocess-env-inheritance.test.ts index 636b53d70a1f..075bfb3ff14b 100644 --- a/src/main/daemon/pty-subprocess-env-inheritance.test.ts +++ b/src/main/daemon/pty-subprocess-env-inheritance.test.ts @@ -1,7 +1,8 @@ +import type { BunPtySpawnArgs } from './pty-subprocess/bun-pty-process-contract' // Which daemon process env reaches the spawned shell, and how PATH is composed. import { describe, expect, it, vi } from 'vitest' import { delimiter } from 'node:path' -import type * as LocalPtyUtils from '../providers/local-pty-utils' +import type * as PtySpawnValidation from '../providers/pty-spawn-validation' const { spawnMock, @@ -23,12 +24,15 @@ const { }) })) -vi.mock('node-pty', () => ({ - spawn: spawnMock +vi.mock('./pty-subprocess/bun-pty-process', () => ({ + canUseBunPty: () => true, + spawnBunPty: ({ file, args, ...options }: BunPtySpawnArgs) => + spawnMock(file, args, { ...options, name: options.env.TERM ?? 'xterm-256color' }) })) vi.mock('../pwsh', () => ({ - isPwshAvailable: isPwshAvailableMock + isPwshAvailable: isPwshAvailableMock, + isPwshAvailableAsync: isPwshAvailableMock })) // Resolve PowerShell family names to deterministic absolute paths so these @@ -47,8 +51,8 @@ vi.mock('../providers/windows-powershell-executable', () => ({ getWindowsCmdPath: () => CMD_ABS })) -vi.mock('../providers/local-pty-utils', async (importOriginal) => { - const actual = await importOriginal() +vi.mock('../providers/pty-spawn-validation', async (importOriginal) => { + const actual = await importOriginal() return { ...actual, resolveUnixShellPath: resolveUnixShellPathMock, diff --git a/src/main/daemon/pty-subprocess-env-retention.test.ts b/src/main/daemon/pty-subprocess-env-retention.test.ts index c936fe28d308..e223641feb2f 100644 --- a/src/main/daemon/pty-subprocess-env-retention.test.ts +++ b/src/main/daemon/pty-subprocess-env-retention.test.ts @@ -1,6 +1,6 @@ import { EventEmitter } from 'node:events' import { describe, expect, it, vi } from 'vitest' -import type * as pty from 'node-pty' +import type { TerminalProcess } from '../../shared/terminal-process' import { createDaemonPtySubprocessHandle } from './pty-subprocess/subprocess-handle' vi.mock('../pty/posix-pty-process-groups', () => ({ @@ -22,7 +22,6 @@ function nativePort() { cols: 80, rows: 24, process: 'audit-shell', - handleFlowControl: false, onData(listener: (value: string) => void) { data.on('data', listener) return { dispose: () => data.off('data', listener) } @@ -38,7 +37,7 @@ function nativePort() { clear: vi.fn(), kill: vi.fn(), destroy: vi.fn() - } satisfies pty.IPty & { destroy: () => void } + } satisfies TerminalProcess & { destroy: () => void } return { process: process_, emitData: (value: string) => data.emit('data', value), diff --git a/src/main/daemon/pty-subprocess-foreground-degraded-scan.test.ts b/src/main/daemon/pty-subprocess-foreground-degraded-scan.test.ts index 8d37578bcf30..63356c439347 100644 --- a/src/main/daemon/pty-subprocess-foreground-degraded-scan.test.ts +++ b/src/main/daemon/pty-subprocess-foreground-degraded-scan.test.ts @@ -1,3 +1,4 @@ +import type { BunPtySpawnArgs } from './pty-subprocess/bun-pty-process-contract' // Guards the daemon foreground identity against Windows scan degradation: a // timed-out CIM scan (available:false) or an incomplete snapshot (available:true // with the agent row missing) must not retire a still-working agent and make @@ -22,8 +23,15 @@ const { jobReadableMock: vi.fn() })) -vi.mock('node-pty', () => ({ spawn: spawnMock })) -vi.mock('../pwsh', () => ({ isPwshAvailable: isPwshAvailableMock })) +vi.mock('./pty-subprocess/bun-pty-process', () => ({ + canUseBunPty: () => true, + spawnBunPty: ({ file, args, ...options }: BunPtySpawnArgs) => + spawnMock(file, args, { ...options, name: options.env.TERM ?? 'xterm-256color' }) +})) +vi.mock('../pwsh', () => ({ + isPwshAvailable: isPwshAvailableMock, + isPwshAvailableAsync: isPwshAvailableMock +})) const PWSH7_ABS = 'C:\\Program Files\\PowerShell\\7\\pwsh.exe' const WINDOWS_POWERSHELL_ABS = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' diff --git a/src/main/daemon/pty-subprocess-foreground-identity.test.ts b/src/main/daemon/pty-subprocess-foreground-identity.test.ts index 76793cdbab20..b10320c339f1 100644 --- a/src/main/daemon/pty-subprocess-foreground-identity.test.ts +++ b/src/main/daemon/pty-subprocess-foreground-identity.test.ts @@ -1,6 +1,7 @@ +import type { BunPtySpawnArgs } from './pty-subprocess/bun-pty-process-contract' // Foreground process reporting: node-pty names, agent enrichment and its cache. import { describe, expect, it, vi } from 'vitest' -import type * as LocalPtyUtils from '../providers/local-pty-utils' +import type * as PtySpawnValidation from '../providers/pty-spawn-validation' const { spawnMock, @@ -22,12 +23,15 @@ const { }) })) -vi.mock('node-pty', () => ({ - spawn: spawnMock +vi.mock('./pty-subprocess/bun-pty-process', () => ({ + canUseBunPty: () => true, + spawnBunPty: ({ file, args, ...options }: BunPtySpawnArgs) => + spawnMock(file, args, { ...options, name: options.env.TERM ?? 'xterm-256color' }) })) vi.mock('../pwsh', () => ({ - isPwshAvailable: isPwshAvailableMock + isPwshAvailable: isPwshAvailableMock, + isPwshAvailableAsync: isPwshAvailableMock })) // Resolve PowerShell family names to deterministic absolute paths so these @@ -46,8 +50,8 @@ vi.mock('../providers/windows-powershell-executable', () => ({ getWindowsCmdPath: () => CMD_ABS })) -vi.mock('../providers/local-pty-utils', async (importOriginal) => { - const actual = await importOriginal() +vi.mock('../providers/pty-spawn-validation', async (importOriginal) => { + const actual = await importOriginal() return { ...actual, getNodePtySpawnHelperCandidates: () => [import.meta.filename], diff --git a/src/main/daemon/pty-subprocess-foreground-scan-cadence.test.ts b/src/main/daemon/pty-subprocess-foreground-scan-cadence.test.ts index 47513702b7ac..b8c020313b4b 100644 --- a/src/main/daemon/pty-subprocess-foreground-scan-cadence.test.ts +++ b/src/main/daemon/pty-subprocess-foreground-scan-cadence.test.ts @@ -1,3 +1,4 @@ +import type { BunPtySpawnArgs } from './pty-subprocess/bun-pty-process-contract' // Regression guard: bound the volume of whole-process-table scans the daemon // schedules for persisted sessions. On Windows each agent-foreground refresh // forks a powershell.exe/CIM whole-table scan (the daemon-side analogue of @@ -10,19 +11,22 @@ import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' -import type * as LocalPtyUtils from '../providers/local-pty-utils' +import type * as PtySpawnValidation from '../providers/pty-spawn-validation' const { spawnMock, isPwshAvailableMock, resolveAgentForegroundProcessMock } = vi.hoisted(() => ({ spawnMock: vi.fn(), isPwshAvailableMock: vi.fn(), resolveAgentForegroundProcessMock: vi.fn() })) -vi.mock('node-pty', () => ({ - spawn: spawnMock +vi.mock('./pty-subprocess/bun-pty-process', () => ({ + canUseBunPty: () => true, + spawnBunPty: ({ file, args, ...options }: BunPtySpawnArgs) => + spawnMock(file, args, { ...options, name: options.env.TERM ?? 'xterm-256color' }) })) vi.mock('../pwsh', () => ({ - isPwshAvailable: isPwshAvailableMock + isPwshAvailable: isPwshAvailableMock, + isPwshAvailableAsync: isPwshAvailableMock })) // Resolve PowerShell family names to deterministic absolute paths so these @@ -40,8 +44,8 @@ vi.mock('../providers/windows-powershell-executable', () => ({ getWindowsCmdPath: () => CMD_ABS })) -vi.mock('../providers/local-pty-utils', async (importOriginal) => { - const actual = await importOriginal() +vi.mock('../providers/pty-spawn-validation', async (importOriginal) => { + const actual = await importOriginal() return { ...actual, getNodePtySpawnHelperCandidates: () => [import.meta.filename] diff --git a/src/main/daemon/pty-subprocess-git-credential-guard.test.ts b/src/main/daemon/pty-subprocess-git-credential-guard.test.ts index a073ee195093..8f0ec69bb956 100644 --- a/src/main/daemon/pty-subprocess-git-credential-guard.test.ts +++ b/src/main/daemon/pty-subprocess-git-credential-guard.test.ts @@ -1,6 +1,7 @@ +import type { BunPtySpawnArgs } from './pty-subprocess/bun-pty-process-contract' // Git prompt/credential guard env layered onto the inherited daemon Git config. import { describe, expect, it, vi } from 'vitest' -import type * as LocalPtyUtils from '../providers/local-pty-utils' +import type * as PtySpawnValidation from '../providers/pty-spawn-validation' const { spawnMock, @@ -22,12 +23,15 @@ const { }) })) -vi.mock('node-pty', () => ({ - spawn: spawnMock +vi.mock('./pty-subprocess/bun-pty-process', () => ({ + canUseBunPty: () => true, + spawnBunPty: ({ file, args, ...options }: BunPtySpawnArgs) => + spawnMock(file, args, { ...options, name: options.env.TERM ?? 'xterm-256color' }) })) vi.mock('../pwsh', () => ({ - isPwshAvailable: isPwshAvailableMock + isPwshAvailable: isPwshAvailableMock, + isPwshAvailableAsync: isPwshAvailableMock })) // Resolve PowerShell family names to deterministic absolute paths so these @@ -46,8 +50,8 @@ vi.mock('../providers/windows-powershell-executable', () => ({ getWindowsCmdPath: () => CMD_ABS })) -vi.mock('../providers/local-pty-utils', async (importOriginal) => { - const actual = await importOriginal() +vi.mock('../providers/pty-spawn-validation', async (importOriginal) => { + const actual = await importOriginal() return { ...actual, resolveUnixShellPath: resolveUnixShellPathMock, diff --git a/src/main/daemon/pty-subprocess-handle-lifecycle.test.ts b/src/main/daemon/pty-subprocess-handle-lifecycle.test.ts index 6ab328400102..bd10ad12f995 100644 --- a/src/main/daemon/pty-subprocess-handle-lifecycle.test.ts +++ b/src/main/daemon/pty-subprocess-handle-lifecycle.test.ts @@ -1,6 +1,7 @@ +import type { BunPtySpawnArgs } from './pty-subprocess/bun-pty-process-contract' // SubprocessHandle io forwarding plus kill/dispose neutralization contracts. import { describe, expect, it, vi } from 'vitest' -import type * as LocalPtyUtils from '../providers/local-pty-utils' +import type * as PtySpawnValidation from '../providers/pty-spawn-validation' const { spawnMock, @@ -22,12 +23,15 @@ const { }) })) -vi.mock('node-pty', () => ({ - spawn: spawnMock +vi.mock('./pty-subprocess/bun-pty-process', () => ({ + canUseBunPty: () => true, + spawnBunPty: ({ file, args, ...options }: BunPtySpawnArgs) => + spawnMock(file, args, { ...options, name: options.env.TERM ?? 'xterm-256color' }) })) vi.mock('../pwsh', () => ({ - isPwshAvailable: isPwshAvailableMock + isPwshAvailable: isPwshAvailableMock, + isPwshAvailableAsync: isPwshAvailableMock })) // Resolve PowerShell family names to deterministic absolute paths so these @@ -46,8 +50,8 @@ vi.mock('../providers/windows-powershell-executable', () => ({ getWindowsCmdPath: () => CMD_ABS })) -vi.mock('../providers/local-pty-utils', async (importOriginal) => { - const actual = await importOriginal() +vi.mock('../providers/pty-spawn-validation', async (importOriginal) => { + const actual = await importOriginal() return { ...actual, getNodePtySpawnHelperCandidates: () => [import.meta.filename], @@ -77,7 +81,6 @@ vi.mock('../providers/windows-pty-job-membership', () => ({ import { createPtySubprocess } from './pty-subprocess' import { mockPtyProcess, useDaemonPtySubprocessEnv } from './pty-subprocess-test-harness' -import { __setConptyJobNativeForTests } from '../windows/windows-pty-job' describe('createPtySubprocess', () => { useDaemonPtySubprocessEnv({ @@ -420,14 +423,8 @@ describe('createPtySubprocess', () => { // but it made forceKill a permanent no-op, so a wedged ConPTY -- which // never fires onExit -- could not be escalated at all (#9854). The job // terminates the tree without touching the handle node-pty owns. - const terminateJob = vi.fn().mockReturnValue(true) - __setConptyJobNativeForTests(() => ({ - terminateJob, - listJobProcessIds: vi.fn(), - assignCurrentProcessToJob: vi.fn().mockReturnValue(true) - })) - const proc = mockPtyProcess(123456) as ReturnType & { _pty: number } - proc._pty = 11 + const terminateJob = vi.fn(() => 'terminated' as const) + const proc = { ...mockPtyProcess(123456), terminateOwnedTree: terminateJob } spawnMock.mockReturnValue(proc) const origPlatform = Object.getOwnPropertyDescriptor(process, 'platform') Object.defineProperty(process, 'platform', { value: 'win32' }) @@ -436,11 +433,10 @@ describe('createPtySubprocess', () => { handle.kill() handle.forceKill() - expect(terminateJob).toHaveBeenCalledWith(11, 123456) + expect(terminateJob).toHaveBeenCalledOnce() // Still exactly one native kill: the escalation must not double-close. expect(proc.kill).toHaveBeenCalledOnce() } finally { - __setConptyJobNativeForTests() restorePlatform(origPlatform) } }) diff --git a/src/main/daemon/pty-subprocess-io-failure-bun-fixture.ts b/src/main/daemon/pty-subprocess-io-failure-bun-fixture.ts new file mode 100644 index 000000000000..2b2708b12dab --- /dev/null +++ b/src/main/daemon/pty-subprocess-io-failure-bun-fixture.ts @@ -0,0 +1,128 @@ +import assert from 'node:assert/strict' +import { readdirSync, readlinkSync } from 'node:fs' +import { runProcessSync } from '../../shared/child-process/run-process' +import { spawnBunPty } from './pty-subprocess/bun-pty-process' +import { createDaemonPtySubprocessHandle } from './pty-subprocess/subprocess-handle' +import { TerminalHost } from './terminal-host' + +function terminalDescriptors(): number { + if (process.platform === 'linux') { + return readdirSync('/proc/self/fd') + .flatMap((fd) => { + try { + return [readlinkSync(`/proc/self/fd/${fd}`)] + } catch { + return [] + } + }) + .filter((path) => /\/dev\/(ptmx|pts\/)/.test(path)).length + } + const result = runProcessSync({ + program: 'lsof', + args: ['-p', String(process.pid)], + timeoutMs: 5000 + }) + assert.equal(result.code, 0, result.stderr) + return result.stdout + .split('\n') + .filter((line) => /\/dev\/(ptmx|ttys[0-9])|\(revoked\)/.test(line)).length +} + +async function waitUntil( + predicate: () => boolean, + message = 'PTY lifecycle deadline exceeded' +): Promise { + const deadline = Date.now() + 3000 + while (!predicate()) { + assert.ok(Date.now() < deadline, message) + await new Promise((resolve) => setTimeout(resolve, 10)) + } +} + +export async function runNativeIoFailureFixture({ + operation, + immediate +}: { + operation: 'write' | 'resize' + immediate: boolean +}): Promise { + const before = terminalDescriptors() + for (let cycle = 0; cycle < 4; cycle++) { + const native = spawnBunPty({ + file: '/bin/sh', + args: [ + '-c', + 'printf "orca-cleanup-ready\\n"; while IFS= read -r line; do printf "reply:%s\\n" "$line"; done' + ], + cwd: process.cwd(), + cols: 80, + rows: 24, + env: { TERM: 'xterm-256color', PATH: '/usr/bin:/bin' } + }) + let exited = false + native.onExit(() => { + exited = true + }) + const handle = createDaemonPtySubprocessHandle({ + process: native, + shellPath: '/bin/sh', + spawnCwd: process.cwd(), + env: {}, + startupCommandDeliveredInShellArgs: false, + reportsChildExitStatus: true, + sessionId: 'native-io-failure', + startupAgentRecognition: null + }) + const host = new TerminalHost({ spawnSubprocess: () => handle }) + let output = '' + let exitEvents = 0 + try { + await host.createOrAttach({ + sessionId: 'native-io-failure', + cols: 80, + rows: 24, + streamClient: { + onData: (data) => { + output += data + }, + onExit: () => { + exitEvents++ + } + } + }) + await waitUntil(() => output.includes('orca-cleanup-ready')) + handle.resize(100, 30) + handle.write('roundtrip\n') + await waitUntil(() => output.includes('reply:roundtrip')) + host.pauseProducer('native-io-failure') + assert.equal(process.kill(native.pid, 0), true) + const originalWrite = native.write + const originalResize = native.resize + native[operation] = () => { + throw new Error('injected I/O failure') + } + if (operation === 'write') { + handle.write('ignored') + } else { + handle.resize(100, 30) + } + native.write = originalWrite + native.resize = originalResize + await host.kill('native-io-failure', { immediate }) + await waitUntil(() => exitEvents === 1) + assert.equal(host.listSessions().length, 0) + assert.throws(() => process.kill(native.pid, 0), { code: 'ESRCH' }) + } finally { + if (!exited) { + native.kill('SIGKILL') + } + await waitUntil(() => exited) + await host.dispose() + native.destroy() + } + // Bun closes its duplicated POSIX reader/writer fds off-thread after the exit callback. + await waitUntil(() => terminalDescriptors() === before, 'native terminal descriptor leaked') + assert.equal(terminalDescriptors(), before, 'native terminal descriptor leaked') + } + return 4 +} diff --git a/src/main/daemon/pty-subprocess-io-failure-cleanup.test.ts b/src/main/daemon/pty-subprocess-io-failure-cleanup.test.ts index b7ce586668b5..4f5bbbbf92d8 100644 --- a/src/main/daemon/pty-subprocess-io-failure-cleanup.test.ts +++ b/src/main/daemon/pty-subprocess-io-failure-cleanup.test.ts @@ -1,5 +1,4 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type * as pty from 'node-pty' import { createDaemonPtySubprocessHandle } from './pty-subprocess/subprocess-handle' import { mockPtyProcess } from './pty-subprocess-test-harness' import { TerminalHost } from './terminal-host' @@ -23,13 +22,15 @@ vi.mock('../pty-descendant-termination', () => ({ function createFixture() { const proc = { ...mockPtyProcess(4242), + cols: 80, + rows: 24, destroy: vi.fn(), pause: vi.fn(), resume: vi.fn(), clear: vi.fn() } const handle = createDaemonPtySubprocessHandle({ - process: proc as unknown as pty.IPty, + process: proc, shellPath: 'bash', spawnCwd: process.cwd(), env: {}, diff --git a/src/main/daemon/pty-subprocess-io-failure-native.test.ts b/src/main/daemon/pty-subprocess-io-failure-native.test.ts index b1835c068a79..56711de4b635 100644 --- a/src/main/daemon/pty-subprocess-io-failure-native.test.ts +++ b/src/main/daemon/pty-subprocess-io-failure-native.test.ts @@ -1,12 +1,10 @@ -import { fstatSync } from 'node:fs' -import * as pty from 'node-pty' -import { describe, expect, it, vi } from 'vitest' -import { createDaemonPtySubprocessHandle } from './pty-subprocess/subprocess-handle' -import { TerminalHost } from './terminal-host' +import { join } from 'node:path' +import { describe, expect, it } from 'vitest' +import { runBundledBunFixture } from '../bundled-bun-test-execution' const describePosix = process.platform === 'win32' ? describe.skip : describe -describePosix('failed-I/O teardown with a real native PTY', () => { +describePosix('failed-I/O teardown with a real bundled Bun PTY', () => { it.each([ ['write', false], ['write', true], @@ -15,83 +13,15 @@ describePosix('failed-I/O teardown with a real native PTY', () => { ] as const)( 'reaps real shells and master fds after %s failure (immediate=%s)', async (operation, immediate) => { - for (let cycle = 0; cycle < 4; cycle++) { - const native = pty.spawn( - '/bin/sh', - [ - '-c', - 'printf "orca-cleanup-ready\\n"; while IFS= read -r line; do printf "reply:%s\\n" "$line"; done' - ], - { - cwd: process.cwd(), - cols: 80, - rows: 24, - env: { TERM: 'xterm-256color', PATH: '/usr/bin:/bin' } - } + expect( + await runBundledBunFixture( + join(__dirname, 'pty-subprocess-io-failure-bun-fixture.ts'), + 'runNativeIoFailureFixture', + { operation, immediate }, + 15_000 ) - const fd = (native as pty.IPty & { fd: number }).fd - let exited = false - native.onExit(() => { - exited = true - }) - const handle = createDaemonPtySubprocessHandle({ - process: native, - shellPath: '/bin/sh', - spawnCwd: process.cwd(), - env: {}, - startupCommandDeliveredInShellArgs: false, - reportsChildExitStatus: true, - sessionId: 'native-io-failure', - startupAgentRecognition: null - }) - const host = new TerminalHost({ spawnSubprocess: () => handle }) - let output = '' - const onExit = vi.fn() - try { - await host.createOrAttach({ - sessionId: 'native-io-failure', - cols: 80, - rows: 24, - streamClient: { - onData: (data) => { - output += data - }, - onExit - } - }) - await vi.waitFor(() => expect(output).toContain('orca-cleanup-ready'), { timeout: 3000 }) - handle.resize(100, 30) - handle.write('roundtrip\n') - await vi.waitFor(() => expect(output).toContain('reply:roundtrip'), { timeout: 3000 }) - host.pauseProducer('native-io-failure') - expect(process.kill(native.pid, 0)).toBe(true) - const failure = vi.spyOn(native, operation).mockImplementation(() => { - throw new Error('injected I/O failure') - }) - if (operation === 'write') { - handle.write('ignored') - } else { - handle.resize(100, 30) - } - failure.mockRestore() - - await host.kill('native-io-failure', { immediate }) - await vi.waitFor(() => expect(onExit).toHaveBeenCalledOnce(), { timeout: 3000 }) - expect(host.listSessions()).toHaveLength(0) - expect(() => process.kill(native.pid, 0)).toThrow( - expect.objectContaining({ code: 'ESRCH' }) - ) - expect(() => fstatSync(fd)).toThrow(expect.objectContaining({ code: 'EBADF' })) - } finally { - // Only this test's still-owned native child is eligible for emergency cleanup. - if (!exited) { - native.kill('SIGKILL') - } - await vi.waitFor(() => expect(exited).toBe(true), { timeout: 3000 }) - await host.dispose() - } - } + ).toBe(4) }, - 15000 + 20_000 ) }) diff --git a/src/main/daemon/pty-subprocess-managed-agent-env.test.ts b/src/main/daemon/pty-subprocess-managed-agent-env.test.ts index d5dbc33246c2..419a88a02dee 100644 --- a/src/main/daemon/pty-subprocess-managed-agent-env.test.ts +++ b/src/main/daemon/pty-subprocess-managed-agent-env.test.ts @@ -1,6 +1,7 @@ +import type { BunPtySpawnArgs } from './pty-subprocess/bun-pty-process-contract' // Daemon-managed agent env: shell wrapper survival and overlay deletion. import { describe, expect, it, vi } from 'vitest' -import type * as LocalPtyUtils from '../providers/local-pty-utils' +import type * as PtySpawnValidation from '../providers/pty-spawn-validation' const { spawnMock, @@ -22,12 +23,15 @@ const { }) })) -vi.mock('node-pty', () => ({ - spawn: spawnMock +vi.mock('./pty-subprocess/bun-pty-process', () => ({ + canUseBunPty: () => true, + spawnBunPty: ({ file, args, ...options }: BunPtySpawnArgs) => + spawnMock(file, args, { ...options, name: options.env.TERM ?? 'xterm-256color' }) })) vi.mock('../pwsh', () => ({ - isPwshAvailable: isPwshAvailableMock + isPwshAvailable: isPwshAvailableMock, + isPwshAvailableAsync: isPwshAvailableMock })) // Resolve PowerShell family names to deterministic absolute paths so these @@ -46,8 +50,8 @@ vi.mock('../providers/windows-powershell-executable', () => ({ getWindowsCmdPath: () => CMD_ABS })) -vi.mock('../providers/local-pty-utils', async (importOriginal) => { - const actual = await importOriginal() +vi.mock('../providers/pty-spawn-validation', async (importOriginal) => { + const actual = await importOriginal() return { ...actual, resolveUnixShellPath: resolveUnixShellPathMock, diff --git a/src/main/daemon/pty-subprocess-spawn-file-foreground.test.ts b/src/main/daemon/pty-subprocess-spawn-file-foreground.test.ts index 94e352ce6954..1dca7b2176e1 100644 --- a/src/main/daemon/pty-subprocess-spawn-file-foreground.test.ts +++ b/src/main/daemon/pty-subprocess-spawn-file-foreground.test.ts @@ -1,4 +1,4 @@ -import type { IPty } from 'node-pty' +import type { TerminalProcess } from '../../shared/terminal-process' import { afterEach, describe, expect, it, vi } from 'vitest' import type { ProcessTableRow } from '../../shared/process-table-snapshot' import type * as SnapshotReader from '../../shared/process-table-snapshot-reader' @@ -68,11 +68,10 @@ function table(command: string | null, loginWrapper = false): ProcessTableRow[] } function createHandle(loginWrapper = false) { - const proc: IPty & { processNameIsSpawnFile: true } = { + const proc: TerminalProcess & { processNameIsSpawnFile: true } = { pid: 100, cols: 80, rows: 24, - handleFlowControl: false, process: loginWrapper ? '/Applications/Orca shell login' : '/bin/zsh', processNameIsSpawnFile: true, onData: () => ({ dispose() {} }), diff --git a/src/main/daemon/pty-subprocess-test-harness.ts b/src/main/daemon/pty-subprocess-test-harness.ts index 623dd20bf632..697362fc9687 100644 --- a/src/main/daemon/pty-subprocess-test-harness.ts +++ b/src/main/daemon/pty-subprocess-test-harness.ts @@ -1,4 +1,4 @@ -// Shared node-pty stub, PowerShell path stubs and daemon env lifecycle for the +// Shared terminal stub, PowerShell path stubs and daemon env lifecycle for the // pty-subprocess test files. `vi.mock` calls must stay in each test file. import { afterEach, beforeEach, vi } from 'vitest' import type { Mock } from 'vitest' diff --git a/src/main/daemon/pty-subprocess-windows-shell-launch.test.ts b/src/main/daemon/pty-subprocess-windows-shell-launch.test.ts index 5831274a9c6f..79601bb46f82 100644 --- a/src/main/daemon/pty-subprocess-windows-shell-launch.test.ts +++ b/src/main/daemon/pty-subprocess-windows-shell-launch.test.ts @@ -1,6 +1,8 @@ +import { TerminalAttachCanceledError } from './daemon-errors' +import type { BunPtySpawnArgs } from './pty-subprocess/bun-pty-process-contract' // Native Windows shell launch: PowerShell implementations, cmd.exe and Git Bash. import { describe, expect, it, vi } from 'vitest' -import type * as LocalPtyUtils from '../providers/local-pty-utils' +import type * as PtySpawnValidation from '../providers/pty-spawn-validation' const { spawnMock, @@ -22,12 +24,15 @@ const { }) })) -vi.mock('node-pty', () => ({ - spawn: spawnMock +vi.mock('./pty-subprocess/bun-pty-process', () => ({ + canUseBunPty: () => true, + spawnBunPty: ({ file, args, ...options }: BunPtySpawnArgs) => + spawnMock(file, args, { ...options, name: options.env.TERM ?? 'xterm-256color' }) })) vi.mock('../pwsh', () => ({ - isPwshAvailable: isPwshAvailableMock + isPwshAvailable: isPwshAvailableMock, + isPwshAvailableAsync: isPwshAvailableMock })) // Resolve PowerShell family names to deterministic absolute paths so these @@ -46,8 +51,8 @@ vi.mock('../providers/windows-powershell-executable', () => ({ getWindowsCmdPath: () => CMD_ABS })) -vi.mock('../providers/local-pty-utils', async (importOriginal) => { - const actual = await importOriginal() +vi.mock('../providers/pty-spawn-validation', async (importOriginal) => { + const actual = await importOriginal() return { ...actual, resolveUnixShellPath: resolveUnixShellPathMock, @@ -89,6 +94,40 @@ describe('createPtySubprocess', () => { validateWorkingDirectoryMock }) + it('preserves cancellation identity while a Windows shell receipt is pending', async () => { + const platform = Object.getOwnPropertyDescriptor(process, 'platform')! + const abort = new AbortController() + const destroy = vi.fn() + let awaitingReceipt = false + spawnMock.mockReturnValue({ + ...mockPtyProcess(), + destroy, + waitForSpawn: () => { + awaitingReceipt = true + return new Promise(() => {}) + } + }) + Object.defineProperty(process, 'platform', { value: 'win32' }) + try { + const pending = createPtySubprocess({ + sessionId: 'receipt-canceled', + cols: 80, + rows: 24, + env: { COMSPEC: CMD_ABS }, + cancelSignal: abort.signal + }) + const rejected = expect(pending).rejects.toThrow(TerminalAttachCanceledError) + await vi.waitFor(() => expect(awaitingReceipt).toBe(true)) + abort.abort() + await rejected + expect(destroy).toHaveBeenCalledOnce() + expect(spawnMock).toHaveBeenCalledOnce() + } finally { + abort.abort() + Object.defineProperty(process, 'platform', platform) + } + }) + it('keeps powershell.exe when the inbox PowerShell implementation is selected on Windows', async () => { const proc = mockPtyProcess() spawnMock.mockReturnValue(proc) diff --git a/src/main/daemon/pty-subprocess-windows-shell-probe.test.ts b/src/main/daemon/pty-subprocess-windows-shell-probe.test.ts new file mode 100644 index 000000000000..fe15e13ae2e7 --- /dev/null +++ b/src/main/daemon/pty-subprocess-windows-shell-probe.test.ts @@ -0,0 +1,165 @@ +import type { BunPtySpawnArgs } from './pty-subprocess/bun-pty-process-contract' +// Native Windows shell launch: PowerShell implementations, cmd.exe and Git Bash. +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type * as PtySpawnValidation from '../providers/pty-spawn-validation' + +const { + spawnMock, + isPwshAvailableMock, + validateWorkingDirectoryMock, + resolveUnixShellPathMock, + resolveAgentForegroundProcessMock +} = vi.hoisted(() => ({ + spawnMock: vi.fn(), + isPwshAvailableMock: vi.fn(), + resolveUnixShellPathMock: vi.fn((shellPath: string) => shellPath), + resolveAgentForegroundProcessMock: vi.fn(), + validateWorkingDirectoryMock: vi.fn((cwd: string) => { + if (cwd.includes('definitely-missing')) { + throw new Error( + `Working directory "${cwd}" does not exist. It may have been deleted or is on an unmounted volume.` + ) + } + }) +})) + +vi.mock('./pty-subprocess/bun-pty-process', () => ({ + canUseBunPty: () => true, + spawnBunPty: ({ file, args, ...options }: BunPtySpawnArgs) => + spawnMock(file, args, { ...options, name: options.env.TERM ?? 'xterm-256color' }) +})) + +vi.mock('../pwsh', () => ({ + isPwshAvailable: isPwshAvailableMock, + isPwshAvailableAsync: isPwshAvailableMock +})) + +// Resolve PowerShell family names to deterministic absolute paths so these +// tests run on non-Windows CI. The real resolver (which skips the Store App +// Execution Alias stub) is exercised in windows-powershell-executable.test.ts. +const PWSH7_ABS = 'C:\\Program Files\\PowerShell\\7\\pwsh.exe' +const WINDOWS_POWERSHELL_ABS = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' +const CMD_ABS = 'C:\\Windows\\System32\\cmd.exe' +vi.mock('../providers/windows-powershell-executable', () => ({ + resolveWindowsPowerShellExecutablePath: (family: 'pwsh.exe' | 'powershell.exe') => + family === 'pwsh.exe' ? PWSH7_ABS : WINDOWS_POWERSHELL_ABS, + resolveWindowsPowerShellSpawnChain: (family: 'pwsh.exe' | 'powershell.exe') => + family === 'pwsh.exe' + ? [PWSH7_ABS, WINDOWS_POWERSHELL_ABS, CMD_ABS] + : [WINDOWS_POWERSHELL_ABS, CMD_ABS], + getWindowsCmdPath: () => CMD_ABS +})) + +vi.mock('../providers/pty-spawn-validation', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + resolveUnixShellPath: resolveUnixShellPathMock, + validateWorkingDirectory: validateWorkingDirectoryMock, + validateWorkingDirectoryAsync: validateWorkingDirectoryMock + } +}) + +vi.mock('../providers/agent-foreground-process', () => ({ + resolveAgentForegroundProcessWithAvailability: async (...args: unknown[]) => { + const value = await resolveAgentForegroundProcessMock(...args) + return value && typeof value === 'object' && 'available' in value + ? value + : { available: true, processName: value } + } +})) + +// Console-membership reads run a real node-pty fork that never settles under +// fake timers; default to "shell-only" so the degraded-scan guard falls through +// to its existing retirement logic (the degraded-scan behavior itself is +// covered in pty-subprocess-foreground-degraded-scan.test.ts). +vi.mock('../providers/windows-pty-job-membership', () => ({ + readWindowsPtyJobProcessIds: () => new Set([12345]), + isWindowsPtyJobReadable: () => true +})) + +import { createPtySubprocess } from './pty-subprocess' +import { mockPtyProcess, useDaemonPtySubprocessEnv } from './pty-subprocess-test-harness' + +const POWERSHELL_OSC133_COMMAND_ARGS = ['-NoLogo', '-NoExit', '-EncodedCommand', expect.any(String)] + +describe('createPtySubprocess', () => { + useDaemonPtySubprocessEnv({ + spawnMock, + isPwshAvailableMock, + resolveUnixShellPathMock, + resolveAgentForegroundProcessMock, + validateWorkingDirectoryMock + }) + + const platform = Object.getOwnPropertyDescriptor(process, 'platform')! + beforeEach(() => + Object.defineProperty(process, 'platform', { value: 'win32', configurable: true }) + ) + afterEach(() => Object.defineProperty(process, 'platform', platform)) + + it('waits for the async automatic-shell probe before selecting PowerShell 7', async () => { + let release!: (available: boolean) => void + isPwshAvailableMock.mockReturnValue( + new Promise((resolve) => { + release = resolve + }) + ) + spawnMock.mockReturnValue(mockPtyProcess()) + const pending = createPtySubprocess({ + sessionId: 'automatic-shell', + cols: 80, + rows: 24, + shellOverride: 'powershell.exe' + }) + await vi.waitFor(() => expect(isPwshAvailableMock).toHaveBeenCalledOnce()) + expect(spawnMock).not.toHaveBeenCalled() + release(true) + await pending + expect(spawnMock).toHaveBeenCalledWith( + PWSH7_ABS, + POWERSHELL_OSC133_COMMAND_ARGS, + expect.any(Object) + ) + }) + + it('cancels the caller waiting for a shell probe without publishing a later native spawn', async () => { + let release!: (available: boolean) => void + isPwshAvailableMock.mockReturnValue( + new Promise((resolve) => { + release = resolve + }) + ) + const abort = new AbortController() + const pending = createPtySubprocess({ + sessionId: 'canceled-shell', + cols: 80, + rows: 24, + shellOverride: 'powershell.exe', + cancelSignal: abort.signal + }) + const rejected = expect(pending).rejects.toThrow('Attach canceled') + await vi.waitFor(() => expect(isPwshAvailableMock).toHaveBeenCalledOnce()) + abort.abort() + await rejected + release(true) + await new Promise((resolve) => setImmediate(resolve)) + expect(spawnMock).not.toHaveBeenCalled() + }) + + it('does not wait on availability for an explicit PowerShell 7 selection', async () => { + spawnMock.mockReturnValue(mockPtyProcess()) + await createPtySubprocess({ + sessionId: 'explicit-shell', + cols: 80, + rows: 24, + shellOverride: 'pwsh.exe' + }) + expect(isPwshAvailableMock).not.toHaveBeenCalled() + expect(spawnMock).toHaveBeenCalledWith( + PWSH7_ABS, + POWERSHELL_OSC133_COMMAND_ARGS, + expect.any(Object) + ) + }) +}) diff --git a/src/main/daemon/pty-subprocess-wsl-launch.test.ts b/src/main/daemon/pty-subprocess-wsl-launch.test.ts index 067dc729aadb..7161f1a7640b 100644 --- a/src/main/daemon/pty-subprocess-wsl-launch.test.ts +++ b/src/main/daemon/pty-subprocess-wsl-launch.test.ts @@ -1,9 +1,10 @@ +import type { BunPtySpawnArgs } from './pty-subprocess/bun-pty-process-contract' // WSL routing: distro selection, cwd translation and env import marking. import { describe, expect, it, vi } from 'vitest' import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' -import type * as LocalPtyUtils from '../providers/local-pty-utils' +import type * as PtySpawnValidation from '../providers/pty-spawn-validation' const { spawnMock, @@ -25,12 +26,15 @@ const { }) })) -vi.mock('node-pty', () => ({ - spawn: spawnMock +vi.mock('./pty-subprocess/bun-pty-process', () => ({ + canUseBunPty: () => true, + spawnBunPty: ({ file, args, ...options }: BunPtySpawnArgs) => + spawnMock(file, args, { ...options, name: options.env.TERM ?? 'xterm-256color' }) })) vi.mock('../pwsh', () => ({ - isPwshAvailable: isPwshAvailableMock + isPwshAvailable: isPwshAvailableMock, + isPwshAvailableAsync: isPwshAvailableMock })) // Resolve PowerShell family names to deterministic absolute paths so these @@ -49,8 +53,8 @@ vi.mock('../providers/windows-powershell-executable', () => ({ getWindowsCmdPath: () => CMD_ABS })) -vi.mock('../providers/local-pty-utils', async (importOriginal) => { - const actual = await importOriginal() +vi.mock('../providers/pty-spawn-validation', async (importOriginal) => { + const actual = await importOriginal() return { ...actual, resolveUnixShellPath: resolveUnixShellPathMock, diff --git a/src/main/daemon/pty-subprocess.test.ts b/src/main/daemon/pty-subprocess.test.ts index 100adaafaf58..cc4b4b8f9bb0 100644 --- a/src/main/daemon/pty-subprocess.test.ts +++ b/src/main/daemon/pty-subprocess.test.ts @@ -1,9 +1,10 @@ -// Spawn setup: node-pty launch options, Unix shell resolution and daemon cwd repair. +import type { BunPtySpawnArgs } from './pty-subprocess/bun-pty-process-contract' +// Spawn setup: Bun PTY launch options, Unix shell resolution and daemon cwd repair. import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' -import type * as LocalPtyUtils from '../providers/local-pty-utils' +import type * as PtySpawnValidation from '../providers/pty-spawn-validation' const { spawnMock, @@ -25,12 +26,15 @@ const { }) })) -vi.mock('node-pty', () => ({ - spawn: spawnMock +vi.mock('./pty-subprocess/bun-pty-process', () => ({ + canUseBunPty: () => true, + spawnBunPty: ({ file, args, ...options }: BunPtySpawnArgs) => + spawnMock(file, args, { ...options, name: options.env.TERM ?? 'xterm-256color' }) })) vi.mock('../pwsh', () => ({ - isPwshAvailable: isPwshAvailableMock + isPwshAvailable: isPwshAvailableMock, + isPwshAvailableAsync: isPwshAvailableMock })) // Resolve PowerShell family names to deterministic absolute paths so these @@ -49,8 +53,8 @@ vi.mock('../providers/windows-powershell-executable', () => ({ getWindowsCmdPath: () => CMD_ABS })) -vi.mock('../providers/local-pty-utils', async (importOriginal) => { - const actual = await importOriginal() +vi.mock('../providers/pty-spawn-validation', async (importOriginal) => { + const actual = await importOriginal() return { ...actual, resolveUnixShellPath: resolveUnixShellPathMock, @@ -68,7 +72,7 @@ vi.mock('../providers/agent-foreground-process', () => ({ } })) -// Console-membership reads run a real node-pty fork that never settles under +// Console-membership reads run a real Bun PTY fork that never settles under // fake timers; default to "shell-only" so the degraded-scan guard falls through // to its existing retirement logic (the degraded-scan behavior itself is // covered in pty-subprocess-foreground-degraded-scan.test.ts). @@ -98,7 +102,7 @@ describe('createPtySubprocess', () => { validateWorkingDirectoryMock }) - it('spawns node-pty with correct options', async () => { + it('spawns Bun PTY with correct options', async () => { const proc = mockPtyProcess() spawnMock.mockReturnValue(proc) const onMacosTccSpawnStrategy = vi.fn() @@ -156,7 +160,7 @@ describe('createPtySubprocess', () => { expect(spawnMock).not.toHaveBeenCalled() }) - it('does not report a spawn strategy when node-pty fails before launch', async () => { + it('does not report a spawn strategy when Bun PTY fails before launch', async () => { spawnMock.mockImplementationOnce(() => { throw new Error('spawn failed') }) @@ -193,7 +197,7 @@ describe('createPtySubprocess', () => { expect(PREVIOUS_DAEMON_PROTOCOL_VERSIONS).toContain(22) }) - it('resolves a missing Unix default before spawning node-pty', async () => { + it('resolves a missing Unix default before spawning Bun PTY', async () => { const proc = mockPtyProcess() spawnMock.mockReturnValue(proc) resolveUnixShellPathMock.mockReturnValue('/bin/sh') @@ -285,7 +289,7 @@ describe('createPtySubprocess', () => { } }) - it('surfaces the no-executable-shell error before node-pty forks', async () => { + it('surfaces the no-executable-shell error before Bun PTY forks', async () => { resolveUnixShellPathMock.mockImplementation(() => { throw new Error('No executable Unix shell found (tried: /bin/zsh, /bin/bash, /bin/sh)') }) @@ -304,7 +308,7 @@ describe('createPtySubprocess', () => { } }) - it('uses bundled ConPTY for native Windows daemon terminals', async () => { + it('passes Windows dimensions to the Bun terminal backend', async () => { const proc = mockPtyProcess() spawnMock.mockReturnValue(proc) const platform = Object.getOwnPropertyDescriptor(process, 'platform') @@ -327,7 +331,7 @@ describe('createPtySubprocess', () => { expect(spawnMock).toHaveBeenCalledWith( expect.any(String), expect.any(Array), - expect.objectContaining({ useConptyDll: true }) + expect.objectContaining({ cols: 80, rows: 24 }) ) }) @@ -354,7 +358,7 @@ describe('createPtySubprocess', () => { expect(spawnCall[2].env[POWERLEVEL10K_WIZARD_DISABLE_ENV]).toBe('true') }) - itOnMacHost('repairs a deleted macOS daemon cwd before spawning node-pty', async () => { + itOnMacHost('repairs a deleted macOS daemon cwd before spawning Bun PTY', async () => { const proc = mockPtyProcess() spawnMock.mockReturnValue(proc) const platform = Object.getOwnPropertyDescriptor(process, 'platform') @@ -388,7 +392,7 @@ describe('createPtySubprocess', () => { ) }) - itOnPosixHost('repairs a deleted POSIX daemon cwd before Linux node-pty spawn', async () => { + itOnPosixHost('repairs a deleted POSIX daemon cwd before Linux Bun PTY spawn', async () => { const proc = mockPtyProcess() spawnMock.mockReturnValue(proc) const platform = Object.getOwnPropertyDescriptor(process, 'platform') @@ -515,7 +519,7 @@ describe('createPtySubprocess', () => { expect(spawnMock).not.toHaveBeenCalled() }) - it('rejects a missing explicit POSIX cwd before node-pty spawn', async () => { + it('rejects a missing explicit POSIX cwd before Bun PTY spawn', async () => { const platform = Object.getOwnPropertyDescriptor(process, 'platform') Object.defineProperty(process, 'platform', { value: 'linux' }) spawnMock.mockClear() diff --git a/src/main/daemon/pty-subprocess.ts b/src/main/daemon/pty-subprocess.ts index 1b5bc3d11021..b4aad30254ee 100644 --- a/src/main/daemon/pty-subprocess.ts +++ b/src/main/daemon/pty-subprocess.ts @@ -1,9 +1,15 @@ +import type { ObserveTerminalSpawnAttempt } from './terminal-spawn-attempt' import type { SubprocessHandle } from './session-subprocess-handle' import { normalizePtySize } from './daemon-pty-size' import { TerminalAttachCanceledError } from './daemon-errors' +import { waitForTerminalAttachOperation } from './terminal-attach-cancellation' import { createDaemonPtyEnvironment } from './pty-subprocess/spawn-environment' import { createPtyShellLaunchPlan } from './pty-subprocess/shell-launch-plan' -import { spawnNativeDaemonPty, type SpawnedDaemonPty } from './pty-subprocess/native-pty-spawn' +import { + spawnNativeDaemonPty, + PtySpawnCleanupError, + type SpawnedDaemonPty +} from './pty-subprocess/native-pty-spawn' import { formatPtySpawnError, preflightPtySpawn, @@ -35,6 +41,7 @@ export type PtySubprocessOptions = { isCanceled?: () => boolean /** Aborts in-progress cwd validation; `isCanceled` is only polled between steps. */ cancelSignal?: AbortSignal + onSpawnAttempt?: ObserveTerminalSpawnAttempt onMacosTccSpawnStrategy?: (strategy: 'wrapped' | 'direct') => void } @@ -70,7 +77,11 @@ export async function checkPtySpawnHealth(): Promise { export async function createPtySubprocess(opts: PtySubprocessOptions): Promise { const size = normalizePtySize(opts.cols, opts.rows) const env = createDaemonPtyEnvironment(opts) - const launch = createPtyShellLaunchPlan(opts, env) + const launch = await waitForTerminalAttachOperation( + createPtyShellLaunchPlan(opts, env), + opts.cancelSignal, + opts.sessionId + ) await preflightPtySpawn({ validationCwd: launch.validationCwd, @@ -82,6 +93,20 @@ export async function createPtySubprocess(opts: PtySubprocessOptions): Promise + createDaemonPtySubprocessHandle({ + process: spawned.process, + shellPath: spawned.shellPath, + spawnCwd: spawned.spawnCwd, + env, + startupCommandDeliveredInShellArgs: + spawned.startupCommandDeliveredInShellArgs ?? launch.startupCommandDeliveredInShellArgs, + reportsChildExitStatus: spawned.reportsChildExitStatus, + requestedCwd: opts.cwd, + sessionId: opts.sessionId, + startupAgentRecognition: launch.startupAgentRecognition + }) + let attemptHandle: SubprocessHandle | undefined let spawned: SpawnedDaemonPty try { spawned = await spawnNativeDaemonPty({ @@ -92,25 +117,28 @@ export async function createPtySubprocess(opts: PtySubprocessOptions): Promise Promise) => { + return opts.onSpawnAttempt!(() => { + attemptHandle = makeHandle(attempt) + return attemptHandle + }, discardNative) + } + } + : {}) }) } catch (error) { + if (opts.cancelSignal?.aborted && !(error instanceof PtySpawnCleanupError)) { + throw new TerminalAttachCanceledError(opts.sessionId) + } if (process.platform === 'win32') { throw formatPtySpawnError(error, launch.shellPath, launch.spawnCwd) } throw error } - return createDaemonPtySubprocessHandle({ - process: spawned.process, - shellPath: spawned.shellPath, - spawnCwd: spawned.spawnCwd, - env, - startupCommandDeliveredInShellArgs: - spawned.startupCommandDeliveredInShellArgs ?? launch.startupCommandDeliveredInShellArgs, - reportsChildExitStatus: spawned.reportsChildExitStatus, - requestedCwd: opts.cwd, - sessionId: opts.sessionId, - startupAgentRecognition: launch.startupAgentRecognition - }) + return attemptHandle ?? makeHandle(spawned) } diff --git a/src/main/daemon/pty-subprocess/bun-pty-process-contract.ts b/src/main/daemon/pty-subprocess/bun-pty-process-contract.ts index b43dd57a659d..b20d52a9a3f7 100644 --- a/src/main/daemon/pty-subprocess/bun-pty-process-contract.ts +++ b/src/main/daemon/pty-subprocess/bun-pty-process-contract.ts @@ -1,5 +1,6 @@ -import type * as pty from 'node-pty' +import type { TerminalProcess } from '../../../shared/terminal-process' import type { JobTerminationOutcome } from '../../windows/windows-pty-job' +import type { WindowsBunPtyJobNative } from './windows-bun-pty-native' import type { WindowsBunPtyJob } from './windows-bun-pty-job' import type { createWindowsBunPtyLaunch } from './windows-bun-pty-launch' @@ -41,7 +42,8 @@ export type BunRuntime = { ): BunSubprocess } -export type BunPtyProcess = pty.IPty & { +export type BunPtyProcess = TerminalProcess & { + handleFlowControl: boolean destroy(): void processNameIsSpawnFile?: true jobRootProcessIsWrapper?: true @@ -59,13 +61,18 @@ export type BunPtySpawnArgs = { env: Record cols: number rows: number + windowsJobKillOnClose?: boolean } export type SpawnBunPtyDeps = { platform?: NodeJS.Platform runtime?: BunRuntime assignHostJob?: () => boolean - createJob?: (pid: number) => WindowsBunPtyJob | null + createJob?: ( + pid: number, + native?: WindowsBunPtyJobNative | null, + killOnClose?: boolean + ) => WindowsBunPtyJob | null createWindowsLaunch?: typeof createWindowsBunPtyLaunch readProcessTable?: () => string signalProcessGroup?: (pgid: number, signal: NodeJS.Signals) => void diff --git a/src/main/daemon/pty-subprocess/bun-pty-process-flow-control.ts b/src/main/daemon/pty-subprocess/bun-pty-process-flow-control.ts index 5b7dfd7a3133..f3ca7c538c54 100644 --- a/src/main/daemon/pty-subprocess/bun-pty-process-flow-control.ts +++ b/src/main/daemon/pty-subprocess/bun-pty-process-flow-control.ts @@ -117,7 +117,7 @@ export function createBunPtyProducerFlowControl( // Partial signals require a fresh transition even if the requested state changes again. state = 'uncertain' if (nextPaused) { - // Signal delivery is asynchronous; prove the shell stopped before suspending its jobs. + // Confirm root signal delivery; a login wrapper may still have a separately running shell. if (!isPosixPtyRootStopped(table, options.processHandle.pid)) { retryTransition() return diff --git a/src/main/daemon/pty-subprocess/bun-pty-process-runtime.ts b/src/main/daemon/pty-subprocess/bun-pty-process-runtime.ts index b64097950433..bf9b0dacdc69 100644 --- a/src/main/daemon/pty-subprocess/bun-pty-process-runtime.ts +++ b/src/main/daemon/pty-subprocess/bun-pty-process-runtime.ts @@ -1,4 +1,5 @@ import { constants } from 'node:os' +import { createBunPtyTerminalIo } from './bun-pty-terminal-io' import { assignCurrentProcessToBunPtyHostJob, createWindowsBunPtyJob, @@ -31,17 +32,22 @@ export function spawnBunPty(args: BunPtySpawnArgs, deps: SpawnBunPtyDeps = {}): const exitListeners = new Set<(event: { exitCode: number; signal?: number }) => void>() const decoder = new TextDecoder() let pendingData = '' + let pendingDataError: Error | undefined let exited = false let exitCode = 0 let exitSignal: number | undefined - // Keep a closed Bun native handle from escaping as a daemon RPC failure. - let terminalUnavailable = false - let appliedCols = args.cols - let appliedRows = args.rows const emitData = (data: string): void => { if (dataListeners.size === 0) { - pendingData = (pendingData + data).slice(-512 * 1024) + if (pendingDataError) { + return + } + if (pendingData.length + data.length > 512 * 1024) { + pendingDataError = new Error('Terminal output arrived before its consumer was installed') + pendingData = '' + return + } + pendingData += data return } for (const listener of dataListeners) { @@ -138,7 +144,11 @@ export function spawnBunPty(args: BunPtySpawnArgs, deps: SpawnBunPtyDeps = {}): } if (windowsLaunch) { try { - windowsJob = (deps.createJob ?? createWindowsBunPtyJob)(processHandle.pid) + windowsJob = (deps.createJob ?? createWindowsBunPtyJob)( + processHandle.pid, + undefined, + args.windowsJobKillOnClose === true + ) if (!windowsJob) { throw new Error('Windows Bun PTY job ownership is unavailable') } @@ -233,6 +243,13 @@ export function spawnBunPty(args: BunPtySpawnArgs, deps: SpawnBunPtyDeps = {}): } } + const terminalIo = createBunPtyTerminalIo( + processHandle.terminal, + args, + () => exited, + () => terminate('SIGKILL') + ) + return { pid: processHandle.pid, get shellProcessId() { @@ -243,12 +260,17 @@ export function spawnBunPty(args: BunPtySpawnArgs, deps: SpawnBunPtyDeps = {}): clear() {}, process: args.file, get cols() { - return appliedCols + return terminalIo.cols }, get rows() { - return appliedRows + return terminalIo.rows }, + write: terminalIo.write, + resize: terminalIo.resize, onData(listener) { + if (pendingDataError) { + throw pendingDataError + } if (pendingData) { const data = pendingData pendingData = '' @@ -268,28 +290,6 @@ export function spawnBunPty(args: BunPtySpawnArgs, deps: SpawnBunPtyDeps = {}): exitListeners.add(listener) return { dispose: () => exitListeners.delete(listener) } }, - write(data) { - if (exited || terminalUnavailable || processHandle.terminal.closed) { - return - } - try { - processHandle.terminal.write(data) - } catch { - terminalUnavailable = true - } - }, - resize(cols, rows) { - if (exited || terminalUnavailable || processHandle.terminal.closed) { - return - } - try { - processHandle.terminal.resize(cols, rows) - appliedCols = cols - appliedRows = rows - } catch { - terminalUnavailable = true - } - }, ...clearCapability, ...producerFlowControl, ...windowsCapabilities, diff --git a/src/main/daemon/pty-subprocess/bun-pty-process.integration.test.ts b/src/main/daemon/pty-subprocess/bun-pty-process.integration.test.ts index 1c4f0436d6f6..929d5894a009 100644 --- a/src/main/daemon/pty-subprocess/bun-pty-process.integration.test.ts +++ b/src/main/daemon/pty-subprocess/bun-pty-process.integration.test.ts @@ -4,7 +4,7 @@ import { join, resolve } from 'node:path' import { describe, expect, it } from 'vitest' import { runProcess, runProcessSync } from '../../../shared/child-process/run-process' import { orcadBunRuntimeFilename } from '../../../shared/orcad-artifacts' -import { ORCAD_BUN_VERSION } from '../../../shared/orcad-bun-runtime' +import { ORCAD_BUN_VERSION, orcadBunVersionProbeArgs } from '../../../shared/orcad-bun-runtime' import { removeTreeSync } from '../../../shared/windows-transient-lock-removal' const runtimePath = @@ -12,9 +12,9 @@ const runtimePath = resolve(__dirname, '../../../../out/orcad', orcadBunRuntimeFilename(process.platform)) async function runTerminalScript(script: string): Promise { - expect(runProcessSync({ program: runtimePath, args: ['--version'] }).stdout.trim()).toBe( - ORCAD_BUN_VERSION - ) + expect( + runProcessSync({ program: runtimePath, args: [...orcadBunVersionProbeArgs()] }).stdout.trim() + ).toBe(ORCAD_BUN_VERSION) const directory = mkdtempSync(join(tmpdir(), 'orca-bun-terminal-')) try { const entry = join(directory, 'terminal.cjs') diff --git a/src/main/daemon/pty-subprocess/bun-pty-process.test.ts b/src/main/daemon/pty-subprocess/bun-pty-process.test.ts index 6726524f53d3..61018ba15753 100644 --- a/src/main/daemon/pty-subprocess/bun-pty-process.test.ts +++ b/src/main/daemon/pty-subprocess/bun-pty-process.test.ts @@ -72,9 +72,10 @@ function createBunHarness({ closeImmediately = true } = {}) { } } -function spawn(deps?: Parameters[1]) { +function spawn(deps?: Parameters[1], windowsJobKillOnClose = false) { return spawnBunPty( { + windowsJobKillOnClose, file: '/bin/sh', args: ['-l'], cwd: '/tmp', @@ -284,30 +285,129 @@ describe('Bun.Terminal PTY adapter', () => { expect(harness.terminal.close).not.toHaveBeenCalled() }) - it('contains a native terminal write failure and suppresses later writes', () => { - const harness = createBunHarness() - harness.terminal.write = vi.fn(() => { - throw new Error('terminal closed') + it('retires only the failed writer and waits for observed termination', async () => { + const failed = createBunHarness() + failed.terminal.write = vi.fn(() => { + throw new Error('native write failed') }) const proc = spawn() + const onExit = vi.fn() + proc.onExit(onExit) + const witness = createBunHarness() + const witnessProc = spawn() expect(() => proc.write('first')).not.toThrow() proc.write('second') + proc.resize(100, 30) + witnessProc.write('still live') + witnessProc.resize(90, 30) - expect(harness.terminal.write).toHaveBeenCalledOnce() + expect(failed.terminal.write).toHaveBeenCalledOnce() + expect(failed.processHandle.kill).toHaveBeenCalledExactlyOnceWith('SIGKILL') + expect(failed.terminal.resize).not.toHaveBeenCalled() + expect(onExit).not.toHaveBeenCalled() + expect(witness.processHandle.kill).not.toHaveBeenCalled() + expect(witness.terminal.write).toHaveBeenCalledWith('still live') + expect(witness.terminal.resize).toHaveBeenCalledWith(90, 30) + + failed.resolveExit(137) + await failed.processHandle.exited + expect(onExit).toHaveBeenCalledExactlyOnceWith({ exitCode: 137 }) }) - it('contains a native terminal resize failure and suppresses later resizes', () => { + it('keeps input and future resize usable after a failed resize on an open terminal', () => { const harness = createBunHarness() - harness.terminal.resize = vi.fn(() => { - throw new Error('terminal closed') + vi.mocked(harness.terminal.resize).mockImplementationOnce(() => { + throw new Error('Failed to resize terminal') }) const proc = spawn() expect(() => proc.resize(120, 40)).not.toThrow() + expect({ cols: proc.cols, rows: proc.rows }).toEqual({ cols: 80, rows: 24 }) + proc.write('still live') proc.resize(100, 30) - expect(harness.terminal.resize).toHaveBeenCalledOnce() + expect(harness.terminal.resize).toHaveBeenCalledTimes(2) + expect(harness.terminal.write).toHaveBeenCalledWith('still live') + expect({ cols: proc.cols, rows: proc.rows }).toEqual({ cols: 100, rows: 30 }) + expect(harness.processHandle.kill).not.toHaveBeenCalled() + }) + + it('retires a terminal that closes during resize without inventing its exit', async () => { + const harness = createBunHarness() + vi.mocked(harness.terminal.resize).mockImplementationOnce(() => { + harness.terminal.closed = true + throw new Error('Terminal is closed') + }) + const proc = spawn() + const onExit = vi.fn() + proc.onExit(onExit) + expect(() => proc.resize(120, 40)).not.toThrow() + expect(harness.processHandle.kill).toHaveBeenCalledExactlyOnceWith('SIGKILL') + expect(onExit).not.toHaveBeenCalled() + harness.resolveExit(137) + await harness.processHandle.exited + expect(onExit).toHaveBeenCalledExactlyOnceWith({ exitCode: 137 }) + }) + + it('retries a failed kill even when the native terminal closed during the write', () => { + const harness = createBunHarness() + harness.terminal.write = vi.fn(() => { + harness.terminal.closed = true + throw new Error('native write failed') + }) + harness.processHandle.kill.mockImplementationOnce(() => { + throw new Error('kill failed') + }) + const proc = spawn() + expect(() => proc.write('first')).not.toThrow() + expect(() => proc.write('second')).not.toThrow() + expect(harness.processHandle.kill).toHaveBeenCalledTimes(2) + expect(harness.terminal.write).toHaveBeenCalledOnce() + }) + + it('retires the owned Windows job after write failure and drains output before exit', async () => { + const harness = createBunHarness({ closeImmediately: false }) + harness.terminal.write = vi.fn(() => { + throw new Error('broken pipe') + }) + const job = { + listProcessIds: () => [4321], + pause: () => true, + resume: () => true, + terminate: vi.fn(() => 'terminated' as const), + close: vi.fn() + } + const proc = spawn({ + platform: 'win32', + assignHostJob: () => true, + createJob: () => job, + createWindowsLaunch: () => ({ + command: ['cmd.exe'], + clearCommand: ['cmd.exe'], + env: {}, + windowsVerbatimArguments: true, + release() {}, + dispose() {}, + async waitForSpawn() {}, + readShellProcessId: () => 4321 + }) + }) + const onExit = vi.fn() + const onData = vi.fn() + proc.onExit(onExit) + proc.onData(onData) + proc.write('failed') + expect(job.terminate).toHaveBeenCalledOnce() + expect(onExit).not.toHaveBeenCalled() + harness.resolveExit(1) + await harness.processHandle.exited + expect(onExit).not.toHaveBeenCalled() + harness.emitData(new TextEncoder().encode('last output')) + harness.finishTerminal() + expect(onData).toHaveBeenCalledWith('last output') + expect(onExit).toHaveBeenCalledExactlyOnceWith({ exitCode: 1 }) + expect(job.close).toHaveBeenCalledOnce() }) it('pauses and resumes the POSIX producer process group once per transition', async () => { @@ -376,111 +476,114 @@ describe('Bun.Terminal PTY adapter', () => { ]) }) - it('gates a Windows shell behind exact job ownership and exposes owned capabilities', async () => { - const harness = createBunHarness({ closeImmediately: false }) - const assignHostJob = vi.fn(() => true) - const release = vi.fn() - const dispose = vi.fn() - const waitForSpawn = vi.fn(async () => {}) - let reportedShellPid: number | undefined - const job = { - listProcessIds: vi.fn(() => [4321, 4322]), - pause: vi.fn(() => true), - resume: vi.fn(() => true), - terminate: vi.fn(() => 'terminated' as const), - close: vi.fn() - } - const createJob = vi.fn(() => job) - const createWindowsLaunch = vi.fn(() => ({ - command: ['cmd.exe', '/d /c launch.cmd'], - clearCommand: ['cmd.exe', '/d /c clear.cmd'], - env: { TERM: 'xterm-256color', ORCA_BUN_PTY_JOB_GATE: 'gate' }, - windowsVerbatimArguments: true as const, - release, - dispose, - waitForSpawn, - readShellProcessId: () => reportedShellPid - })) - const proc = spawn({ - platform: 'win32', - assignHostJob, - createJob, - createWindowsLaunch - }) - - expect(harness.spawn.mock.calls[0]?.[0]).toEqual(['cmd.exe', '/d /c launch.cmd']) - expect(harness.spawn.mock.calls[0]?.[1]).toMatchObject({ - windowsVerbatimArguments: true, - env: { ORCA_BUN_PTY_JOB_GATE: 'gate' }, - terminal: harness.terminal - }) - expect(assignHostJob.mock.invocationCallOrder[0]).toBeLessThan( - harness.spawn.mock.invocationCallOrder[0] - ) - expect(createJob).toHaveBeenCalledWith(4321) - expect(createJob.mock.invocationCallOrder[0]).toBeLessThan(release.mock.invocationCallOrder[0]) - await proc.waitForSpawn?.() - expect(waitForSpawn).toHaveBeenCalledWith(harness.processHandle.exited) - - proc.pause() - proc.pause() - proc.resume() - proc.resume() - expect(job.pause).toHaveBeenCalledOnce() - expect(job.resume).toHaveBeenCalledOnce() - expect(proc.jobRootProcessIsWrapper).toBe(true) - expect(readWindowsPtyJobProcessIds(proc)).toBeNull() - expect(harness.spawn.mock.calls[0]?.[1]).not.toHaveProperty('ipc') - reportedShellPid = 4322 - expect(proc.shellProcessId).toBe(4322) - expect(readWindowsPtyJobProcessIds(proc)).toEqual(new Set([4322])) - job.listProcessIds.mockReturnValueOnce([4321, 4323]) - expect(readWindowsPtyJobProcessIds(proc)).toBeNull() - expect(proc.shellProcessId).toBe(4322) - expect(proc.listOwnedProcessIds?.()).toEqual([4321, 4322]) - expect(proc.terminateOwnedTree?.()).toBe('terminated') - - job.terminate.mockClear() - proc.signalProcess?.('SIGINT') - expect(job.terminate).toHaveBeenCalledOnce() - expect(harness.processHandle.kill).not.toHaveBeenCalled() - - proc.clear() - proc.clear() - expect(harness.spawn.mock.calls[1]?.[0]).toEqual(['cmd.exe', '/d /c clear.cmd']) - expect(harness.spawn.mock.calls[1]?.[1]).toMatchObject({ - terminal: harness.terminal, - windowsVerbatimArguments: true - }) - expect(harness.spawn).toHaveBeenCalledTimes(2) + it.each([false, true])( + 'gates Windows job ownership with kill-on-close=%s', + async (killOnClose) => { + const harness = createBunHarness({ closeImmediately: false }) + const assignHostJob = vi.fn(() => true) + const release = vi.fn() + const dispose = vi.fn() + const waitForSpawn = vi.fn(async () => {}) + let reportedShellPid: number | undefined + const job = { + listProcessIds: vi.fn(() => [4321, 4322]), + pause: vi.fn(() => true), + resume: vi.fn(() => true), + terminate: vi.fn(() => 'terminated' as const), + close: vi.fn() + } + const createJob = vi.fn(() => job) + const createWindowsLaunch = vi.fn(() => ({ + command: ['cmd.exe', '/d /c launch.cmd'], + clearCommand: ['cmd.exe', '/d /c clear.cmd'], + env: { TERM: 'xterm-256color', ORCA_BUN_PTY_JOB_GATE: 'gate' }, + windowsVerbatimArguments: true as const, + release, + dispose, + waitForSpawn, + readShellProcessId: () => reportedShellPid + })) + const proc = spawn( + { platform: 'win32', assignHostJob, createJob, createWindowsLaunch }, + killOnClose + ) + + expect(harness.spawn.mock.calls[0]?.[0]).toEqual(['cmd.exe', '/d /c launch.cmd']) + expect(harness.spawn.mock.calls[0]?.[1]).toMatchObject({ + windowsVerbatimArguments: true, + env: { ORCA_BUN_PTY_JOB_GATE: 'gate' }, + terminal: harness.terminal + }) + expect(assignHostJob.mock.invocationCallOrder[0]).toBeLessThan( + harness.spawn.mock.invocationCallOrder[0] + ) + expect(createJob).toHaveBeenCalledWith(4321, undefined, killOnClose) + expect(createJob.mock.invocationCallOrder[0]).toBeLessThan( + release.mock.invocationCallOrder[0] + ) + await proc.waitForSpawn?.() + expect(waitForSpawn).toHaveBeenCalledWith(harness.processHandle.exited) - const lastOutput = vi.fn() - const onExit = vi.fn() - proc.onData(lastOutput) - proc.onExit(onExit) - harness.resolveExit(0) - await harness.processHandle.exited - await Promise.resolve() - expect(onExit).not.toHaveBeenCalled() - expect(job.close).not.toHaveBeenCalled() - harness.emitData(new TextEncoder().encode('final ConPTY frame')) - harness.finishTerminal() - expect(lastOutput).toHaveBeenCalledWith('final ConPTY frame') - expect(onExit).toHaveBeenCalledOnce() - expect(job.close).toHaveBeenCalledOnce() - expect(dispose).toHaveBeenCalledOnce() - expect(job.resume).toHaveBeenCalledOnce() - job.resume.mockImplementation(() => { - throw new Error('job already closed') - }) - expect(() => { + proc.pause() proc.pause() proc.resume() - proc.kill() - proc.destroy() - }).not.toThrow() - expect(job.resume).toHaveBeenCalledOnce() - }) + proc.resume() + expect(job.pause).toHaveBeenCalledOnce() + expect(job.resume).toHaveBeenCalledOnce() + expect(proc.jobRootProcessIsWrapper).toBe(true) + expect(readWindowsPtyJobProcessIds(proc)).toBeNull() + expect(harness.spawn.mock.calls[0]?.[1]).not.toHaveProperty('ipc') + reportedShellPid = 4322 + expect(proc.shellProcessId).toBe(4322) + expect(readWindowsPtyJobProcessIds(proc)).toEqual(new Set([4322])) + job.listProcessIds.mockReturnValueOnce([4321, 4323]) + expect(readWindowsPtyJobProcessIds(proc)).toBeNull() + expect(proc.shellProcessId).toBe(4322) + expect(proc.listOwnedProcessIds?.()).toEqual([4321, 4322]) + expect(proc.terminateOwnedTree?.()).toBe('terminated') + + job.terminate.mockClear() + proc.signalProcess?.('SIGINT') + expect(job.terminate).toHaveBeenCalledOnce() + expect(harness.processHandle.kill).not.toHaveBeenCalled() + + proc.clear() + proc.clear() + expect(harness.spawn.mock.calls[1]?.[0]).toEqual(['cmd.exe', '/d /c clear.cmd']) + expect(harness.spawn.mock.calls[1]?.[1]).toMatchObject({ + terminal: harness.terminal, + windowsVerbatimArguments: true + }) + expect(harness.spawn).toHaveBeenCalledTimes(2) + + const lastOutput = vi.fn() + const onExit = vi.fn() + proc.onData(lastOutput) + proc.onExit(onExit) + harness.resolveExit(0) + await harness.processHandle.exited + await Promise.resolve() + expect(onExit).not.toHaveBeenCalled() + expect(job.close).not.toHaveBeenCalled() + harness.emitData(new TextEncoder().encode('final ConPTY frame')) + harness.finishTerminal() + expect(lastOutput).toHaveBeenCalledWith('final ConPTY frame') + expect(onExit).toHaveBeenCalledOnce() + expect(job.close).toHaveBeenCalledOnce() + expect(dispose).toHaveBeenCalledOnce() + expect(job.resume).toHaveBeenCalledOnce() + job.resume.mockImplementation(() => { + throw new Error('job already closed') + }) + expect(() => { + proc.pause() + proc.resume() + proc.kill() + proc.destroy() + }).not.toThrow() + expect(job.resume).toHaveBeenCalledOnce() + } + ) it('does not release a Windows gate without exact job ownership', async () => { const harness = createBunHarness() diff --git a/src/main/daemon/pty-subprocess/bun-pty-terminal-io.ts b/src/main/daemon/pty-subprocess/bun-pty-terminal-io.ts new file mode 100644 index 000000000000..139c1fcf7f98 --- /dev/null +++ b/src/main/daemon/pty-subprocess/bun-pty-terminal-io.ts @@ -0,0 +1,71 @@ +import type { BunTerminal } from './bun-pty-process-contract' + +export function createBunPtyTerminalIo( + terminal: BunTerminal, + dimensions: { cols: number; rows: number }, + isExited: () => boolean, + terminate: () => void +) { + let cols = dimensions.cols + let rows = dimensions.rows + let state: 'active' | 'failed' | 'retiring' = 'active' + + const retire = (): void => { + try { + terminate() + state = 'retiring' + } catch (error) { + console.warn('[daemon/pty] Failed to retire PTY:', error) + } + } + const canUseTerminal = (): boolean => { + if (isExited()) { + return false + } + if (state === 'failed') { + retire() + } + return state === 'active' && !terminal.closed + } + const fail = (error: unknown): void => { + console.warn('[daemon/pty] Native terminal I/O failed; retiring PTY:', error) + state = 'failed' + retire() + } + + return { + get cols() { + return cols + }, + get rows() { + return rows + }, + write(data: string | Buffer) { + if (!canUseTerminal()) { + return + } + try { + terminal.write(data) + } catch (error) { + // Bun buffers backpressure internally; replaying a failed write can duplicate its prefix. + fail(error) + } + }, + resize(nextCols: number, nextRows: number) { + if (!canUseTerminal()) { + return + } + try { + terminal.resize(nextCols, nextRows) + cols = nextCols + rows = nextRows + } catch (error) { + if (terminal.closed) { + fail(error) + } else { + console.warn('[daemon/pty] Native terminal resize failed:', error) + } + } + } + } +} diff --git a/src/main/daemon/pty-subprocess/bun-tests/bash-reply-echo-bun-fixture.ts b/src/main/daemon/pty-subprocess/bun-tests/bash-reply-echo-bun-fixture.ts new file mode 100644 index 000000000000..94e1291c1d0a --- /dev/null +++ b/src/main/daemon/pty-subprocess/bun-tests/bash-reply-echo-bun-fixture.ts @@ -0,0 +1,61 @@ +import { spawnBunPty } from '../bun-pty-process' +const sleep = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)) + +async function waitFor(predicate: () => boolean, timeoutMs: number): Promise { + const deadline = Date.now() + timeoutMs + while (Date.now() < deadline && !predicate()) { + await sleep(25) + } +} + +/** + * Writes `reply` to the master once bash is settled, and returns what came back. + * `discipline: 'cooked'` parks bash in `read` first, which restores ICANON+ECHO. + */ +export async function captureBashReplyEcho({ + reply, + discipline +}: { + reply: string + discipline: 'readline' | 'cooked' +}): Promise { + let output = '' + const pty = spawnBunPty({ + file: '/bin/bash', + args: ['--norc', '--noprofile', '-i'], + cwd: process.cwd(), + cols: 80, + rows: 24, + env: { + ...Object.fromEntries( + Object.entries(process.env).filter( + (entry): entry is [string, string] => entry[1] !== undefined + ) + ), + PS1: 'ORCA16542> ', + TERM: 'xterm-256color' + } + }) + const exited = new Promise((resolve) => pty.onExit(() => resolve())) + try { + pty.onData((data) => { + output += data + }) + + await waitFor(() => output.includes('ORCA16542> '), 10_000) + if (discipline === 'cooked') { + pty.write('read -r ORCA_LINE\r') + await sleep(400) + } + output = '' + pty.write(reply) + // No marker to wait on: the echo is all this produces, so settle instead. + await waitFor(() => output.length > 0, 5_000) + await sleep(250) + return output + } finally { + pty.kill() + await exited + pty.destroy() + } +} diff --git a/src/main/daemon/pty-subprocess/bun-tests/bundled-bun-pty-fixture.ts b/src/main/daemon/pty-subprocess/bun-tests/bundled-bun-pty-fixture.ts new file mode 100644 index 000000000000..8e0a4203cd8f --- /dev/null +++ b/src/main/daemon/pty-subprocess/bun-tests/bundled-bun-pty-fixture.ts @@ -0,0 +1,59 @@ +import { createServer, type Socket } from 'node:net' +import { readFileSync } from 'node:fs' +import { join } from 'node:path' +import { runBundledBunFixture } from '../../../bundled-bun-test-execution' +import type { BunPtySpawnArgs } from '../bun-pty-process-contract' + +/** Keeps live observers in Vitest while their POSIX PTY runs under pinned Bun. */ +export async function startBundledBunPty(args: BunPtySpawnArgs) { + const socketPath = join(args.cwd, 'terminal.sock') + const server = createServer() + const connected = new Promise((resolve) => server.once('connection', resolve)) + await new Promise((resolve, reject) => { + server.once('error', reject) + server.listen(socketPath, resolve) + }) + const fixture = runBundledBunFixture( + join(__dirname, 'bundled-bun-pty-host-fixture.ts'), + 'runBundledBunPty', + { socketPath, args }, + 35_000 + ) + try { + const socket = await Promise.race([ + connected, + fixture.then(() => { + throw new Error('Bun PTY fixture exited before connecting') + }) + ]) + const pid = Number(readFileSync(`${socketPath}.pid`, 'utf8')) + if (!Number.isSafeInteger(pid) || pid <= 0) { + socket.destroy() + throw new Error('Invalid Bun PTY fixture process ID') + } + socket.setEncoding('utf8') + // Retain asynchronous fixture failures for dispose(), without an unhandled rejection. + void fixture.catch(() => {}) + return { + pid, + kill: () => { + socket.destroy() + }, + write: (data: string) => { + socket.write(data) + }, + onData: (listener: (data: string) => void) => { + socket.on('data', listener) + }, + onExit: (listener: () => void) => { + socket.on('end', listener) + }, + dispose: async () => { + socket.destroy() + await fixture + } + } + } finally { + server.close() + } +} diff --git a/src/main/daemon/pty-subprocess/bun-tests/bundled-bun-pty-host-fixture.ts b/src/main/daemon/pty-subprocess/bun-tests/bundled-bun-pty-host-fixture.ts new file mode 100644 index 000000000000..95981bc3a5b6 --- /dev/null +++ b/src/main/daemon/pty-subprocess/bun-tests/bundled-bun-pty-host-fixture.ts @@ -0,0 +1,39 @@ +import { createConnection } from 'node:net' +import { writeFileSync } from 'node:fs' +import { spawnBunPty } from '../bun-pty-process' +import type { BunPtySpawnArgs } from '../bun-pty-process-contract' + +export async function runBundledBunPty({ + socketPath, + args +}: { + socketPath: string + args: BunPtySpawnArgs +}): Promise { + const term = spawnBunPty(args) + writeFileSync(`${socketPath}.pid`, String(term.pid), { mode: 0o600 }) + const socket = createConnection(socketPath) + await new Promise((resolve, reject) => { + socket.once('connect', resolve) + socket.once('error', (error) => { + term.destroy() + reject(error) + }) + }) + socket.setEncoding('utf8') + const exited = new Promise((resolve) => term.onExit(({ exitCode }) => resolve(exitCode))) + socket.on('data', (data: string) => term.write(data)) + socket.on('close', () => term.kill()) + socket.on('error', () => term.kill()) + term.onData((data) => { + if (!socket.destroyed) { + socket.write(data) + } + }) + try { + return await exited + } finally { + term.destroy() + socket.end() + } +} diff --git a/src/renderer/src/components/terminal-pane/fish-color-scheme-child-stdin.node-pty.test.ts b/src/main/daemon/pty-subprocess/bun-tests/fish-color-scheme-child-stdin.bun.test.ts similarity index 93% rename from src/renderer/src/components/terminal-pane/fish-color-scheme-child-stdin.node-pty.test.ts rename to src/main/daemon/pty-subprocess/bun-tests/fish-color-scheme-child-stdin.bun.test.ts index 36768745bd61..7fffb886f743 100644 --- a/src/renderer/src/components/terminal-pane/fish-color-scheme-child-stdin.node-pty.test.ts +++ b/src/main/daemon/pty-subprocess/bun-tests/fish-color-scheme-child-stdin.bun.test.ts @@ -21,6 +21,8 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import path from 'node:path' import type * as React from 'react' +import { z } from 'zod' +import { startBundledBunPty } from './bundled-bun-pty-fixture' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { fishRequirementViolation, @@ -60,12 +62,14 @@ let storeSubscribers: ((state: MutableState) => void)[] = [] let transportFactoryQueue: unknown[] = [] vi.mock('@/runtime/sync-runtime-graph', () => ({ scheduleRuntimeGraphSync: vi.fn() })) -vi.mock('./terminal-webgl-atlas-recovery', () => ({ +vi.mock('@/components/terminal-pane/terminal-webgl-atlas-recovery', () => ({ scheduleTerminalWebglAtlasRecovery: vi.fn() })) vi.mock('@/lib/codex-stale-pane-sweep', () => ({ notifyCodexPaneBoundForStaleSweep: vi.fn() })) vi.mock('sonner', () => ({ toast: { info: vi.fn() } })) -vi.mock('./cache-timer-seeding', () => ({ shouldSeedCacheTimerOnInitialTitle: vi.fn(() => false) })) +vi.mock('@/components/terminal-pane/cache-timer-seeding', () => ({ + shouldSeedCacheTimerOnInitialTitle: vi.fn(() => false) +})) vi.mock('@/store', () => ({ useAppStore: { getState: () => mockStoreState, @@ -82,7 +86,7 @@ vi.mock('react', async (importOriginal) => ({ ...(await importOriginal()), useCallback: (fn: T): T => fn })) -vi.mock('./pty-transport', () => ({ +vi.mock('@/components/terminal-pane/pty-transport', () => ({ createIpcPtyTransport: vi.fn(() => { const next = transportFactoryQueue.shift() if (!next) { @@ -161,7 +165,7 @@ function createPane(paneId: number): Record { } } -function createDeps(): Record { +function createDeps() { return { tabId: 'tab-1', worktreeId: 'wt-1', @@ -409,7 +413,7 @@ describe('fish never receives a color-scheme report it did not query (#9993)', ( markTerminalPaneUnread: vi.fn(), markAgentCompletionPaneUnread: vi.fn() } - ;(globalThis as unknown as { window: unknown }).window = { + vi.stubGlobal('window', { api: { pty: { kill: vi.fn(), @@ -447,18 +451,19 @@ describe('fish never receives a color-scheme report it did not query (#9993)', ( dispatchEvent: vi.fn(), addEventListener: vi.fn(), removeEventListener: vi.fn() - } - globalThis.requestAnimationFrame = vi.fn((callback: FrameRequestCallback) => { - callback(0) - return 1 }) - globalThis.cancelAnimationFrame = vi.fn() + vi.stubGlobal( + 'requestAnimationFrame', + vi.fn((callback: FrameRequestCallback) => { + callback(0) + return 1 + }) + ) + vi.stubGlobal('cancelAnimationFrame', vi.fn()) }) afterEach(() => { - delete (globalThis as { requestAnimationFrame?: unknown }).requestAnimationFrame - delete (globalThis as { cancelAnimationFrame?: unknown }).cancelAnimationFrame - delete (globalThis as { window?: unknown }).window + vi.unstubAllGlobals() if (configHome) { rmSync(configHome, { recursive: true, force: true }) configHome = null @@ -468,8 +473,15 @@ describe('fish never receives a color-scheme report it did not query (#9993)', ( itWithFish( 'writes no CSI 997 while fish arms mode 2031 around its prompt, so a child reads clean stdin', async () => { - const { connectPanePty } = await import('./pty-connection') - const nodePty = await import('node-pty') + // The renderer module is typechecked by tc:web, separately from this native PTY harness. + const rendererModule = '../../../../renderer/src/components/terminal-pane/pty-connection' + const { connectPanePty } = z + .object({ + connectPanePty: z.custom< + (pane: never, manager: never, deps: never) => { dispose(): void } + >((value) => typeof value === 'function') + }) + .parse(await import(rendererModule)) configHome = mkdtempSync(path.join(tmpdir(), 'orca-fish-2031-')) mkdirSync(path.join(configHome, 'fish'), { recursive: true }) @@ -500,8 +512,12 @@ describe('fish never receives a color-scheme report it did not query (#9993)', ( "process.stdout.write('CHILD-READY\\n')\n" ) - const term = nodePty.spawn(FISH_BIN as string, ['-l', '-i'], { - name: 'xterm-256color', + if (!FISH_BIN) { + throw new Error('Fish binary missing') + } + const term = await startBundledBunPty({ + file: FISH_BIN, + args: ['-l', '-i'], cols: 120, rows: 30, cwd: configHome, @@ -527,7 +543,7 @@ describe('fish never receives a color-scheme report it did not query (#9993)', ( // on DA1 and re-probes every prompt. These are harness bytes, never renderer output. term.onData((chunk) => { rendered += chunk - // Maximal fragmentation keeps query handling independent of node-pty chunk boundaries. + // Maximal fragmentation keeps query handling independent of PTY chunk boundaries. for (const fragment of chunk) { answerTerminalQueries.accept(fragment) } @@ -542,16 +558,16 @@ describe('fish never receives a color-scheme report it did not query (#9993)', ( try { const pane = createPane(1) const deps = createDeps() + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The fixture implements the pane, manager and dependency members used by this real PTY binding. const binding = connectPanePty(pane as never, createManager(pane) as never, deps as never) - expect(await waitUntil(() => rendered.includes(PROMPT_MARK), 15_000)).toBe(true) + expect(await waitUntil(() => rendered.includes(PROMPT_MARK), 15_000), rendered).toBe(true) // The arm is the settle signal. - expect(await waitUntil(() => rendered.includes(ARM_2031), 5_000)).toBe(true) + expect(await waitUntil(() => rendered.includes(ARM_2031), 5_000), rendered).toBe(true) // Vacuity guard: the renderer observer must actually have seen the subscribe, // otherwise "sent nothing" is trivially true. Checked here and not at the end, // because the last decision races between fish's subscribe and its withdrawal. - const paneMode2031 = (deps as { paneMode2031Ref: { current: Map } }) - .paneMode2031Ref.current + const paneMode2031 = deps.paneMode2031Ref.current expect(await waitUntil(() => paneMode2031.get(1) === true, 5_000)).toBe(true) // Type-ahead is the deterministic leak shape: queue the child command while an @@ -595,11 +611,7 @@ describe('fish never receives a color-scheme report it did not query (#9993)', ( } finally { term.write('exit\r') await waitUntil(() => exited, 3_000) - try { - term.kill() - } catch { - // already gone - } + await term.dispose() } }, 30_000 diff --git a/src/main/daemon/pty-subprocess/bun-tests/fish-query-reply-child-stdin-bun-fixture.ts b/src/main/daemon/pty-subprocess/bun-tests/fish-query-reply-child-stdin-bun-fixture.ts new file mode 100644 index 000000000000..0c861c26229a --- /dev/null +++ b/src/main/daemon/pty-subprocess/bun-tests/fish-query-reply-child-stdin-bun-fixture.ts @@ -0,0 +1,166 @@ +import assert from 'node:assert/strict' +import path from 'node:path' +import { spawnBunPty } from '../bun-pty-process' +import { PtyStartupIngress } from '../../../../shared/pty-startup-ingress' +const PROMPT_MARK = 'ORCA13892> ' + +/* oxlint-disable no-control-regex -- terminal query grammars are control sequences */ +/** Anchored at an ESC, first match wins; reply values match xterm.js's. */ +const QUERY_GRAMMARS = [ + { + re: /^\x1b\]1[012];\?(\x07|\x1b\\)/, + reply: (m: RegExpExecArray) => `\x1b]11;rgb:1e1e/1e1e/1e1e${m[1]}` + }, + { re: /^\x1b\[\?6n/, reply: () => '\x1b[?1;1;1R' }, + { re: /^\x1b\[6n/, reply: () => '\x1b[1;1R' }, + { re: /^\x1b\[\?996n/, reply: () => '\x1b[?997;1n' }, + { re: /^\x1b\[>0?c/, reply: () => '\x1b[>0;276;0c' }, + { re: /^\x1b\[0?c/, reply: () => '\x1b[?1;2c' }, + { re: /^\x1b\[>0?q/, reply: () => '\x1bP>|Orca\x1b\\' }, + { re: /^\x1b\[\?u/, reply: () => '\x1b[?0u' } +] as const +/** Still accumulating: no CSI final byte and no OSC/DCS terminator yet. */ +const PARTIAL_QUERY_RE = + /^(?:\x1b|\x1b\[[?>=]?[0-9;]*|\x1b\][0-9]*(?:;[^\x07\x1b]*)?\x1b?|\x1bP[^\x1b]*\x1b?)$/ +/* oxlint-enable no-control-regex */ + +const sleep = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)) + +async function waitUntil(predicate: () => boolean, timeoutMs: number): Promise { + const deadline = Date.now() + timeoutMs + while (Date.now() < deadline) { + if (predicate()) { + return true + } + await sleep(10) + } + return false +} + +export async function runFishQueryReplyFixture({ + binary, + configHome, + childScript, + nodeBinary +}: { + binary: string + configHome: string + childScript: string + nodeBinary: string +}): Promise { + const term = spawnBunPty({ + file: binary, + args: ['-l', '-i'], + cols: 120, + rows: 30, + cwd: configHome, + env: { + PATH: process.env.PATH ?? '/usr/bin:/bin', + HOME: configHome, + TERM: 'xterm-256color', + COLORTERM: 'truecolor', + LANG: 'en_US.UTF-8', + XDG_CONFIG_HOME: configHome, + XDG_DATA_HOME: path.join(configHome, 'data'), + ORCA_NODE_BIN: nodeBinary, + ORCA_CHILD_SCRIPT: childScript + } + }) + + let rendered = '' + const ingress = new PtyStartupIngress({ + ownerBackend: 'posix-pty', + write: (data) => term.write(data), + onEmission: (emission) => { + rendered += emission.data + answerQueriesInOrder(emission.data) + } + }) + + // The host gate: cooked-echo-risk replies are written by the ingress with their + // echo shapes armed; DA1/CPR stay on the host's own path, in call order. + const hostWrite = (data: string): void => { + if (ingress.answerLiveQueryReply(data)) { + return + } + term.write(data) + } + + let tail = '' + let oscQueryCount = 0 + function answerQueriesInOrder(chunk: string): void { + let buffer = tail + chunk + tail = '' + let index = 0 + while (index < buffer.length) { + const at = buffer.indexOf('\x1b', index) + if (at === -1) { + return + } + const rest = buffer.slice(at) + const grammar = QUERY_GRAMMARS.map((candidate) => ({ + candidate, + match: candidate.re.exec(rest) + })).find((entry) => entry.match) + if (grammar?.match) { + if (grammar.candidate === QUERY_GRAMMARS[0]) { + oscQueryCount += 1 + } + hostWrite(grammar.candidate.reply(grammar.match)) + index = at + grammar.match[0].length + continue + } + if (PARTIAL_QUERY_RE.test(rest)) { + tail = rest + return + } + index = at + 1 + } + } + + let exited = false + term.onExit(() => { + exited = true + }) + term.onData((data) => ingress.accept(data)) + + try { + assert.equal(await waitUntil(() => rendered.includes(PROMPT_MARK), 15_000), true) + await sleep(500) + const oscQueriesBeforeHandoff = oscQueryCount + + // Type-ahead is the deterministic shape: queue the child's command while an + // external command still owns the tty, so fish repaints its prompt (re-querying + // OSC 11) and hands the tty over in the same breath. + term.write('sleep 0.4\r') + await sleep(150) + term.write('"$ORCA_NODE_BIN" "$ORCA_CHILD_SCRIPT"\r') + await sleep(1_500) + assert.ok(oscQueryCount > oscQueriesBeforeHandoff) + + const renderedBeforeChildInput = rendered.length + term.write('hello\r') + assert.equal( + await waitUntil( + () => rendered.slice(renderedBeforeChildInput).includes('CHILD-READ:'), + 10_000 + ), + true + ) + + const childRead = + rendered.slice(renderedBeforeChildInput).match(/CHILD-READ:[^\r\n]*/)?.[0] ?? '' + // The merge-blocking assertion: the child's first LINE is what the user typed, + // with no escape byte in front of it. Pre-fix this reads + // `\u001b]11;rgb:1e1e/1e1e/1e1e\u001b\\hello\n`. + assert.equal(childRead, 'CHILD-READ:"hello\\n"') + } finally { + term.write('exit\r') + await waitUntil(() => exited, 3_000) + if (!exited) { + term.kill() + } + assert.equal(await waitUntil(() => exited, 3_000), true) + term.destroy() + } +} diff --git a/src/main/daemon/pty-subprocess/bun-tests/fish-query-reply-child-stdin.bun.test.ts b/src/main/daemon/pty-subprocess/bun-tests/fish-query-reply-child-stdin.bun.test.ts new file mode 100644 index 000000000000..88b7daf468d8 --- /dev/null +++ b/src/main/daemon/pty-subprocess/bun-tests/fish-query-reply-child-stdin.bun.test.ts @@ -0,0 +1,83 @@ +/** + * Real-fish regression for #13892: a terminal query reply Orca held back is overtaken + * by the DA1 answer written later in the same turn, so fish's read sentinel hands the + * tty to the child while the OSC 11 reply is still queued — and the CHILD READS IT. + * + * What is real here: bundled Bun running fish, `PtyStartupIngress`, `PtyStartupReplyDelivery` + * and both echo probes, plus the host's own write gate + * (`answerLiveQueryReply` models the historical write gate that deferred replies). Only the renderer is modelled: it + * answers queries strictly in the order they appear in the stream, so any inversion the + * child sees was produced by the delivery split and nothing else. + * + * The assertion is about a CHILD PROCESS'S STDIN, not the screen: a rendered-output check + * passes while the bytes are still being eaten by the next `npx` / `brew` confirm prompt. + * The child reads a full LINE because a leaked reply carries no newline, so a + * once('data') child would report it alone whenever it happened to land in its own read. + */ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import path from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { fishRequirementViolation, resolveFishBinary } from '../../../../shared/fish-binary-requirement' +import { runBundledBunFixture } from '../../../bundled-bun-test-execution' + +// Why fish 4: the DA1-sentinel handoff this measures lives in the 4.0 Rust tty_handoff. +const FISH = resolveFishBinary(4) +const itWithFish = FISH.available ? it : it.skip + +const PROMPT_MARK = 'ORCA13892> ' + +describe('a held query reply never reaches the next child process (#13892)', () => { + let configHome: string | null = null + + // Always runs, so the CI lane cannot report green with the regression below skipped. + it('has the fish this suite needs when CI requires one', () => { + expect(fishRequirementViolation(FISH)).toBeNull() + }) + + afterEach(() => { + if (configHome) { + rmSync(configHome, { recursive: true, force: true }) + configHome = null + } + }) + + itWithFish( + 'answers OSC 11 in the query turn so the reply cannot land in the child’s stdin', + async () => { + configHome = mkdtempSync(path.join(tmpdir(), 'orca-fish-13892-')) + mkdirSync(path.join(configHome, 'fish'), { recursive: true }) + writeFileSync( + path.join(configHome, 'fish/config.fish'), + [ + 'set -g fish_greeting ""', + `function fish_prompt; printf '${PROMPT_MARK}'; end`, + 'function fish_right_prompt; end', + '' + ].join('\n') + ) + const childScript = path.join(configHome, 'read-stdin.mjs') + writeFileSync( + childScript, + "let buffered = ''\n" + + "process.stdin.on('data', (d) => {\n" + + " buffered += d.toString('utf8')\n" + + " if (!buffered.includes('\\n')) return\n" + + " process.stdout.write('CHILD-READ:' + JSON.stringify(buffered) + '\\n')\n" + + ' process.exit(0)\n' + + '})\n' + ) + + if (!FISH.path) { + throw new Error('Fish binary missing') + } + await runBundledBunFixture( + path.join(__dirname, 'fish-query-reply-child-stdin-bun-fixture.ts'), + 'runFishQueryReplyFixture', + { binary: FISH.path, configHome, childScript, nodeBinary: process.execPath }, + 40_000 + ) + }, + 45_000 + ) +}) diff --git a/src/shared/pty-reply-echo-shapes.node-pty.test.ts b/src/main/daemon/pty-subprocess/bun-tests/pty-reply-echo-shapes.bun.test.ts similarity index 65% rename from src/shared/pty-reply-echo-shapes.node-pty.test.ts rename to src/main/daemon/pty-subprocess/bun-tests/pty-reply-echo-shapes.bun.test.ts index 0fe06c7cae5f..96a93a6d27e2 100644 --- a/src/shared/pty-reply-echo-shapes.node-pty.test.ts +++ b/src/main/daemon/pty-subprocess/bun-tests/pty-reply-echo-shapes.bun.test.ts @@ -1,3 +1,6 @@ +import { z } from 'zod' +import { join } from 'node:path' +import { runBundledBunFixture } from '../../../bundled-bun-test-execution' /** * Keeps the transcript in pty-startup-reply-echo-shapes.test.ts honest. * @@ -12,9 +15,9 @@ * cooked — under `read`, the kernel echoes via ECHOCTL */ import { existsSync } from 'node:fs' -import { afterEach, describe, expect, it } from 'vitest' -import { locateEcho, replyEchoProjections } from './pty-startup-reply-echo-shapes' -import { mode2031SequenceFor } from './terminal-color-scheme-protocol' +import { describe, expect, it } from 'vitest' +import { locateEcho, replyEchoProjections } from '../../../../shared/pty-startup-reply-echo-shapes' +import { mode2031SequenceFor } from '../../../../shared/terminal-color-scheme-protocol' const BASH = '/bin/bash' const itWithBash = process.platform !== 'win32' && existsSync(BASH) ? it : it.skip @@ -22,53 +25,17 @@ const itWithBash = process.platform !== 'win32' && existsSync(BASH) ? it : it.sk const COLOR_SCHEME_REPLY = mode2031SequenceFor('dark') const OSC_COLOR_REPLY_ST = '\x1b]11;rgb:2e2e/3434/3434\x1b\\' -type Pty = { write: (data: string) => void; kill: () => void } - -let live: Pty | null = null - -afterEach(() => { - live?.kill() - live = null -}) - -const sleep = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)) - -async function waitFor(predicate: () => boolean, timeoutMs: number): Promise { - const deadline = Date.now() + timeoutMs - while (Date.now() < deadline && !predicate()) { - await sleep(25) - } -} - -/** - * Writes `reply` to the master once bash is settled, and returns what came back. - * `discipline: 'cooked'` parks bash in `read` first, which restores ICANON+ECHO. - */ async function echoOf(reply: string, discipline: 'readline' | 'cooked'): Promise { - const { spawn } = await import('node-pty') - let output = '' - const pty = spawn(BASH, ['--norc', '--noprofile', '-i'], { - name: 'xterm-256color', - cols: 80, - rows: 24, - env: { ...process.env, PS1: 'ORCA16542> ', TERM: 'xterm-256color' } - }) - live = { write: (data) => pty.write(data), kill: () => pty.kill() } - pty.onData((data) => { - output += data - }) - - await waitFor(() => output.includes('ORCA16542> '), 10_000) - if (discipline === 'cooked') { - pty.write('read -r ORCA_LINE\r') - await sleep(400) - } - output = '' - pty.write(reply) - // No marker to wait on: the echo is all this produces, so settle instead. - await waitFor(() => output.length > 0, 5_000) - await sleep(250) - return output + return z + .string() + .parse( + await runBundledBunFixture( + join(__dirname, 'bash-reply-echo-bun-fixture.ts'), + 'captureBashReplyEcho', + { reply, discipline }, + 20_000 + ) + ) } describe('replyEchoProjections against a real bash pty', () => { diff --git a/src/main/daemon/pty-subprocess/foreground-process-tracker.ts b/src/main/daemon/pty-subprocess/foreground-process-tracker.ts index 7a96877c204f..b955608b2171 100644 --- a/src/main/daemon/pty-subprocess/foreground-process-tracker.ts +++ b/src/main/daemon/pty-subprocess/foreground-process-tracker.ts @@ -1,4 +1,4 @@ -import type * as pty from 'node-pty' +import type { TerminalProcess } from '../../../shared/terminal-process' import { ptyShellProcessId } from '../../windows/windows-pty-job' import { getAgentForegroundContextPaths } from '../../providers/agent-foreground-context-paths' import { confirmPtyShellForeground } from './pty-shell-foreground-confirmation' @@ -49,7 +49,7 @@ export type PtyForegroundProcessTracker = { } export function createPtyForegroundProcessTracker(args: { - process: pty.IPty + process: TerminalProcess shellPath: string cwd?: string sessionId: string diff --git a/src/main/daemon/pty-subprocess/native-pty-spawn-bun.test.ts b/src/main/daemon/pty-subprocess/native-pty-spawn-bun.test.ts index 26a3fcda7dc7..8f033bab73ba 100644 --- a/src/main/daemon/pty-subprocess/native-pty-spawn-bun.test.ts +++ b/src/main/daemon/pty-subprocess/native-pty-spawn-bun.test.ts @@ -1,11 +1,9 @@ import { describe, expect, it, vi } from 'vitest' -const { nodePtyFactory, wrapShellSpawnMock } = vi.hoisted(() => ({ - nodePtyFactory: vi.fn(() => ({ spawn: vi.fn() })), +const { wrapShellSpawnMock } = vi.hoisted(() => ({ wrapShellSpawnMock: vi.fn((file: string, args: string[]) => ({ file, args })) })) -vi.mock('node-pty', nodePtyFactory) vi.mock('../../providers/macos-tcc-login-shell', () => ({ hostReportsChildExitStatus: (file: string) => file !== '/usr/bin/login', wrapShellSpawnForMacosTccAttribution: wrapShellSpawnMock @@ -14,6 +12,25 @@ vi.mock('../../providers/macos-tcc-login-shell', () => ({ import { spawnNativeDaemonPty } from './native-pty-spawn' describe('native PTY runtime selection', () => { + it('refuses an unsupported runtime before attempting any shell', async () => { + const spawnBunPty = vi.fn() + await expect( + spawnNativeDaemonPty( + { + shellPath: '/bin/sh', + shellArgs: [], + spawnCwd: '/tmp', + env: {}, + cols: 80, + rows: 24, + windowsFallbackAttempts: [] + }, + { canUseBunPty: () => false, spawnBunPty } + ) + ).rejects.toThrow('requires the bundled Bun') + expect(spawnBunPty).not.toHaveBeenCalled() + }) + it('spawns with Bun.Terminal without loading node-pty', async () => { const dispose = vi.fn() const spawnBunPty = vi.fn(() => ({ @@ -48,7 +65,6 @@ describe('native PTY runtime selection', () => { expect(result.process.pid).toBe(9876) expect(spawnBunPty).toHaveBeenCalledOnce() - expect(nodePtyFactory).not.toHaveBeenCalled() }) it('applies the macOS login wrapper before selecting the Bun PTY runtime', async () => { diff --git a/src/main/daemon/pty-subprocess/native-pty-spawn-windows.test.ts b/src/main/daemon/pty-subprocess/native-pty-spawn-windows.test.ts index 8d6f7117d081..2442d9cd09d2 100644 --- a/src/main/daemon/pty-subprocess/native-pty-spawn-windows.test.ts +++ b/src/main/daemon/pty-subprocess/native-pty-spawn-windows.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { spawnNativeDaemonPty } from './native-pty-spawn' +import { PtySpawnCleanupError, spawnNativeDaemonPty } from './native-pty-spawn' import { WindowsBunPtySpawnUnconfirmedError } from './windows-bun-pty-spawn-receipt' const attempts = ['pwsh.exe', 'powershell.exe', 'cmd.exe'].map((shellPath) => ({ @@ -103,6 +103,40 @@ describe('Windows Bun shell fallback after gated spawn', () => { expect(spawnBunPty).toHaveBeenCalledOnce() }) + it.each([0, 1])( + 'preserves failed cleanup after cancellation of attempt %s', + async (canceledIndex) => { + const controller = new AbortController() + const cleanupError = new Error('physical process still live') + const spawnBunPty = vi.fn(({ file }: { file: string }) => + createProcess(async () => { + await Promise.resolve() + if (file === attempts[canceledIndex]!.shellPath) { + controller.abort() + } + throw new Error('spawn failed') + }) + ) + const result = spawnNativeDaemonPty( + { + ...args, + signal: controller.signal, + onSpawnAttempt: ({ shellPath }) => ({ + discard: async () => { + if (shellPath === attempts[canceledIndex]!.shellPath) { + throw cleanupError + } + } + }) + }, + { canUseBunPty: () => true, spawnBunPty } + ) + await expect(result).rejects.toBeInstanceOf(PtySpawnCleanupError) + await expect(result).rejects.toHaveProperty('cause', cleanupError) + expect(spawnBunPty).toHaveBeenCalledTimes(canceledIndex + 1) + } + ) + it.each([0, 1])( 'stops at an ambiguous attempt %s to avoid running its startup command twice', async (ambiguousIndex) => { diff --git a/src/main/daemon/pty-subprocess/native-pty-spawn.ts b/src/main/daemon/pty-subprocess/native-pty-spawn.ts index e89e8a5aac42..2565c16df392 100644 --- a/src/main/daemon/pty-subprocess/native-pty-spawn.ts +++ b/src/main/daemon/pty-subprocess/native-pty-spawn.ts @@ -1,20 +1,19 @@ -import type * as pty from 'node-pty' +import { PhysicalExitTracker } from '../../../shared/physical-exit-tracker' +import { IMMEDIATE_KILL_PHYSICAL_EXIT_TIMEOUT_MS } from '../immediate-kill-reply-budget' +import type { TerminalSpawnAttempt } from '../terminal-spawn-attempt' +import type { TerminalProcess } from '../../../shared/terminal-process' import { waitForPromiseWithSignal } from '../../../shared/abort-signal-reason' import { hostReportsChildExitStatus, wrapShellSpawnForMacosTccAttribution } from '../../providers/macos-tcc-login-shell' import type { WindowsShellSpawnAttempt } from '../../providers/windows-shell-fallback-chain' -import { assignHostProcessToKillOnCloseJob } from '../../windows/windows-pty-job' import { canUseBunPty, spawnBunPty } from './bun-pty-process' import { WindowsBunPtySpawnUnconfirmedError } from './windows-bun-pty-spawn-receipt' -async function loadNodePty(): Promise { - return import('node-pty') -} export type SpawnedDaemonPty = { - process: pty.IPty + process: TerminalProcess shellPath: string spawnCwd: string startupCommandDeliveredInShellArgs?: boolean @@ -22,6 +21,8 @@ export type SpawnedDaemonPty = { reportsChildExitStatus: boolean } +export class PtySpawnCleanupError extends Error {} + type NativePtyRuntime = { canUseBunPty: typeof canUseBunPty spawnBunPty: typeof spawnBunPty @@ -38,59 +39,85 @@ export async function spawnNativeDaemonPty( rows: number windowsFallbackAttempts: WindowsShellSpawnAttempt[] signal?: AbortSignal + onSpawnAttempt?: ( + spawned: SpawnedDaemonPty, + discardNative: () => Promise + ) => TerminalSpawnAttempt onMacosTccSpawnStrategy?: (strategy: 'wrapped' | 'direct') => void }, runtime: NativePtyRuntime = { canUseBunPty, spawnBunPty } ): Promise { + args.signal?.throwIfAborted() + if (!runtime.canUseBunPty()) { + throw new Error('Terminal service requires the bundled Bun runtime') + } let reportsChildExitStatus = true const spawnAt = async ( shellPath: string, shellArgs: string[], - cwd: string - ): Promise => { + cwd: string, + startupCommandDeliveredInShellArgs?: boolean + ): Promise => { args.signal?.throwIfAborted() const wrapped = wrapShellSpawnForMacosTccAttribution(shellPath, shellArgs, args.env) reportsChildExitStatus = hostReportsChildExitStatus(wrapped.file) - if (runtime.canUseBunPty()) { - const proc = runtime.spawnBunPty({ - file: wrapped.file, - args: wrapped.args, - cwd, - env: args.env, - cols: args.cols, - rows: args.rows - }) + const proc = runtime.spawnBunPty({ + file: wrapped.file, + args: wrapped.args, + cwd, + env: args.env, + cols: args.cols, + rows: args.rows + }) + let attempt: TerminalSpawnAttempt | undefined + const physicalExit = new PhysicalExitTracker() + const exitSubscription = args.onSpawnAttempt + ? proc.onExit(() => physicalExit.markExited()) + : undefined + const discardNative = async (): Promise => { + proc.destroy() + await physicalExit.waitForExit( + IMMEDIATE_KILL_PHYSICAL_EXIT_TIMEOUT_MS, + () => new Error('Failed terminal spawn has not exited') + ) + exitSubscription?.dispose() + } + try { + attempt = args.onSpawnAttempt?.( + { + process: proc, + shellPath, + spawnCwd: cwd, + reportsChildExitStatus, + ...(startupCommandDeliveredInShellArgs === undefined + ? {} + : { startupCommandDeliveredInShellArgs }) + }, + discardNative + ) + if (attempt?.failure) { + throw attempt.failure.error + } + if (proc.waitForSpawn) { + await waitForPromiseWithSignal(proc.waitForSpawn(), args.signal) + } + args.signal?.throwIfAborted() + } catch (error) { try { - if (proc.waitForSpawn) { - await waitForPromiseWithSignal(proc.waitForSpawn(), args.signal) - } - args.signal?.throwIfAborted() - } catch (error) { - try { + if (attempt) { + await attempt.discard() + exitSubscription?.dispose() + } else { proc.destroy() - } catch (cleanupError) { - console.warn('[daemon/pty] Failed shell launch cleanup failed:', cleanupError) } - throw error + } catch (cleanupError) { + throw new PtySpawnCleanupError('Failed shell launch still owns a process', { + cause: cleanupError + }) } - args.onMacosTccSpawnStrategy?.(wrapped.file === shellPath ? 'direct' : 'wrapped') - return proc - } - const nodePty = await loadNodePty() - // Why: children inherit job membership, so the host job must exist before the first Windows PTY. - if (process.platform === 'win32') { - assignHostProcessToKillOnCloseJob() + throw error } - const proc = nodePty.spawn(wrapped.file, wrapped.args, { - name: args.env.TERM ?? 'xterm-256color', - cols: args.cols, - rows: args.rows, - cwd, - env: args.env, - // Why: bundled ConPTY has the wrap-marker behavior xterm expects. - ...(process.platform === 'win32' ? { useConptyDll: true } : {}) - }) - reportsChildExitStatus = hostReportsChildExitStatus(wrapped.file) + exitSubscription?.dispose() args.onMacosTccSpawnStrategy?.(wrapped.file === shellPath ? 'direct' : 'wrapped') return proc } @@ -104,13 +131,21 @@ export async function spawnNativeDaemonPty( reportsChildExitStatus } } catch (primaryErr) { + if (primaryErr instanceof PtySpawnCleanupError) { + throw primaryErr + } args.signal?.throwIfAborted() if (process.platform !== 'win32' || primaryErr instanceof WindowsBunPtySpawnUnconfirmedError) { throw primaryErr } for (const attempt of args.windowsFallbackAttempts.slice(1)) { try { - const process = await spawnAt(attempt.shellPath, attempt.shellArgs, attempt.effectiveCwd) + const process = await spawnAt( + attempt.shellPath, + attempt.shellArgs, + attempt.effectiveCwd, + attempt.startupCommandDeliveredInShellArgs + ) const message = primaryErr instanceof Error ? primaryErr.message : String(primaryErr) console.warn( `[daemon/pty] Primary shell "${args.shellPath}" failed (${message}), fell back to "${attempt.shellPath}"` @@ -123,6 +158,9 @@ export async function spawnNativeDaemonPty( reportsChildExitStatus } } catch (error) { + if (error instanceof PtySpawnCleanupError) { + throw error + } args.signal?.throwIfAborted() if (error instanceof WindowsBunPtySpawnUnconfirmedError) { throw error diff --git a/src/main/daemon/pty-subprocess/pre-listener-events.ts b/src/main/daemon/pty-subprocess/pre-listener-events.ts index f7eb7216e58d..6d4abef7620e 100644 --- a/src/main/daemon/pty-subprocess/pre-listener-events.ts +++ b/src/main/daemon/pty-subprocess/pre-listener-events.ts @@ -14,6 +14,7 @@ export class PtyPreListenerEvents { private onExitCb: ExitListener | null = null private pendingData: string[] = [] private pendingDataChars = 0 + private pendingDataError: Error | undefined private pendingExitCode: number | null = null private pendingExitCause: TerminalExitCause | null = null @@ -22,16 +23,17 @@ export class PtyPreListenerEvents { this.onDataCb(data) return } + if (this.pendingDataError) { + return + } + if (this.pendingDataChars + data.length > PENDING_PRE_LISTENER_DATA_MAX_CHARS) { + this.pendingDataError = new Error('Terminal output arrived before its consumer was installed') + this.pendingData = [] + this.pendingDataChars = 0 + return + } this.pendingData.push(data) this.pendingDataChars += data.length - while (this.pendingDataChars > PENDING_PRE_LISTENER_DATA_MAX_CHARS) { - const removed = this.pendingData.shift() - if (removed === undefined) { - this.pendingDataChars = 0 - return - } - this.pendingDataChars -= removed.length - } } acceptExit(args: { @@ -50,6 +52,9 @@ export class PtyPreListenerEvents { } onData(cb: DataListener): void { + if (this.pendingDataError) { + throw this.pendingDataError + } this.onDataCb = cb this.flushData() } diff --git a/src/main/daemon/pty-subprocess/pty-shell-foreground-confirmation.ts b/src/main/daemon/pty-subprocess/pty-shell-foreground-confirmation.ts index b4f6aa502f80..92558be92b1a 100644 --- a/src/main/daemon/pty-subprocess/pty-shell-foreground-confirmation.ts +++ b/src/main/daemon/pty-subprocess/pty-shell-foreground-confirmation.ts @@ -1,4 +1,4 @@ -import type * as pty from 'node-pty' +import type { TerminalProcess } from '../../../shared/terminal-process' import { ptyShellProcessId } from '../../windows/windows-pty-job' import { confirmShellForegroundProcess } from '../../providers/agent-foreground-process' import { readWindowsPtyJobProcessIds } from '../../providers/windows-pty-job-membership' @@ -7,7 +7,7 @@ import { readWindowsPtyJobProcessIds } from '../../providers/windows-pty-job-mem * a post-request process inspection (POSIX `ps`, Windows job membership), * never cached state. */ export async function confirmPtyShellForeground(args: { - process: pty.IPty + process: TerminalProcess shellPath: string isDead: () => boolean }): Promise { diff --git a/src/main/daemon/pty-subprocess/shell-launch-plan.ts b/src/main/daemon/pty-subprocess/shell-launch-plan.ts index b50ef12baac2..1d87fa61acaf 100644 --- a/src/main/daemon/pty-subprocess/shell-launch-plan.ts +++ b/src/main/daemon/pty-subprocess/shell-launch-plan.ts @@ -1,7 +1,7 @@ import { shouldUseShellReadyStartupDelivery } from '../../../shared/codex-startup-delivery' import { win32 as pathWin32 } from 'node:path' import { isWindowsGitBashShellPath, resolveWindowsGitBashShellPath } from '../../git-bash' -import { isPwshAvailable } from '../../pwsh' +import { isPwshAvailableAsync } from '../../pwsh' import { isHostCodexHomeForWsl, isWslCodexHomeForHost } from '../../pty/codex-home-wsl-env' import { addOrcaWslInteropEnv } from '../../pty/wsl-orca-env' import { @@ -25,7 +25,7 @@ import { ORCA_CODEX_LAUNCH_PREFLIGHT_CMD_QUOTE_ENV, resolveWindowsShellLaunchArgs } from '../../providers/windows-shell-args' -import { resolveUnixShellPath } from '../../providers/local-pty-utils' +import { resolveUnixShellPath } from '../../providers/pty-spawn-validation' import { selectShellStartupFeatures } from '../../shell-startup-features' import { parseWslPath } from '../../wsl' import { addWslEnvKeys } from '../../wsl-env' @@ -50,10 +50,10 @@ export type PtyShellLaunchPlan = { startupAgentRecognition: RecognizedAgentProcess | null } -export function createPtyShellLaunchPlan( +export async function createPtyShellLaunchPlan( opts: PtySubprocessOptions, env: Record -): PtyShellLaunchPlan { +): Promise { const resolvedWslContext = resolveWslSessionContext(opts) let shellPath = resolvedWslContext ? 'wsl.exe' : opts.shellOverride || resolvePtyShellPath(env) let shellArgs: string[] @@ -92,7 +92,7 @@ export function createPtyShellLaunchPlan( ? (resolveEffectiveWindowsPowerShell({ shellFamily: resolvedShellFamily, implementation: opts.terminalWindowsPowerShellImplementation, - pwshAvailable: shouldProbePwsh ? isPwshAvailable() : false + pwshAvailable: shouldProbePwsh ? await isPwshAvailableAsync() : false }) ?? shellPath) : shellPath } diff --git a/src/main/daemon/pty-subprocess/shell-launch-profile.test.ts b/src/main/daemon/pty-subprocess/shell-launch-profile.test.ts new file mode 100644 index 000000000000..01738d1c5e2e --- /dev/null +++ b/src/main/daemon/pty-subprocess/shell-launch-profile.test.ts @@ -0,0 +1,55 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { createPtyShellLaunchPlan } from './shell-launch-plan' + +vi.mock('../../providers/pty-spawn-validation', () => ({ + resolveUnixShellPath: (shell: string) => shell +})) +vi.mock('../shell-ready', () => ({ + resolvePtyShellPath: (env: Record) => env.SHELL || '/bin/zsh', + getShellLaunchConfig: () => ({ args: ['-l'], env: {} }) +})) +afterEach(() => vi.restoreAllMocks()) + +const base = { sessionId: 'profile-shell', cwd: '/tmp', cols: 80, rows: 24 } +describe.skipIf(process.platform === 'win32')('daemon Unix shell profiles', () => { + it.each(['/bin/bash', '/bin/zsh', '/usr/bin/fish', '/usr/bin/nu'])( + 'uses the configured executable %s ahead of the ambient shell', + async (shell) => { + expect( + await createPtyShellLaunchPlan({ ...base, shellOverride: shell }, { SHELL: '/bin/sh' }) + ).toMatchObject({ shellPath: shell, shellArgs: ['-l'] }) + } + ) + it('uses the environment shell without a configured override', async () => { + expect(await createPtyShellLaunchPlan(base, { SHELL: '/bin/zsh' })).toMatchObject({ + shellPath: '/bin/zsh' + }) + }) + it.each([{ args: [] }, { args: ['--rcfile', '/tmp/orca rc'] }])( + 'preserves configured argument vector %j', + async ({ args }) => { + expect( + await createPtyShellLaunchPlan( + { ...base, shellOverride: '/bin/bash', terminalShellArgs: args }, + {} + ) + ).toMatchObject({ shellPath: '/bin/bash', shellArgs: args }) + } + ) + it.each([{ command: 'codex' }, { launchAgent: 'codex' as const }])( + 'keeps controlled login arguments for agent launches %o', + async (overrides) => { + expect( + await createPtyShellLaunchPlan( + { + ...base, + shellOverride: '/bin/zsh', + terminalShellArgs: ['--rcfile', '/tmp/orca'], + ...overrides + }, + {} + ) + ).toMatchObject({ shellArgs: ['-l'] }) + } + ) +}) diff --git a/src/main/daemon/pty-subprocess/spawn-environment-client-overrides.test.ts b/src/main/daemon/pty-subprocess/spawn-environment-client-overrides.test.ts new file mode 100644 index 000000000000..26c61e7110fc --- /dev/null +++ b/src/main/daemon/pty-subprocess/spawn-environment-client-overrides.test.ts @@ -0,0 +1,65 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { createDaemonPtyEnvironment, finalizeDaemonPtyEnvironment } from './spawn-environment' + +const spawn = { sessionId: 'override-test', cols: 80, rows: 24 } +afterEach(() => vi.unstubAllEnvs()) + +describe('daemon client environment authority', () => { + it('preserves an explicit wizard preference and honors deletion of its default', () => { + expect( + createDaemonPtyEnvironment({ + ...spawn, + env: { POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD: 'already-set' } + }).POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD + ).toBe('already-set') + expect( + createDaemonPtyEnvironment({ + ...spawn, + envToDelete: ['POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD'] + }).POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD + ).toBeUndefined() + }) + + it('does not resurrect stale inherited Git config slots behind an explicit smaller count', () => { + vi.stubEnv('GIT_CONFIG_COUNT', '2') + vi.stubEnv('GIT_CONFIG_KEY_0', 'base.zero') + vi.stubEnv('GIT_CONFIG_VALUE_0', 'zero') + vi.stubEnv('GIT_CONFIG_KEY_1', 'base.one') + vi.stubEnv('GIT_CONFIG_VALUE_1', 'one') + const env = createDaemonPtyEnvironment({ + ...spawn, + env: { + GIT_CONFIG_COUNT: '1', + GIT_CONFIG_KEY_0: 'override.zero', + GIT_CONFIG_VALUE_0: 'override' + } + }) + expect(env.GIT_CONFIG_COUNT).toBe('1') + expect(env.GIT_CONFIG_KEY_0).toBe('override.zero') + expect(env.GIT_CONFIG_VALUE_0).toBe('override') + expect(env.GIT_CONFIG_KEY_1).toBeUndefined() + expect(env.GIT_CONFIG_VALUE_1).toBeUndefined() + }) + + it('drops the activation sentinel after the client removes CONDA_PREFIX', () => { + vi.stubEnv('CONDA_SHLVL', '1') + vi.stubEnv('CONDA_PREFIX', '/opt/miniconda3') + vi.stubEnv('CONDA_DEFAULT_ENV', 'base') + const env = createDaemonPtyEnvironment({ ...spawn, envToDelete: ['CONDA_PREFIX'] }) + finalizeDaemonPtyEnvironment(env, undefined) + expect(env.CONDA_PREFIX).toBeUndefined() + expect(env.CONDA_SHLVL).toBeUndefined() + expect(env.CONDA_DEFAULT_ENV).toBeUndefined() + }) + + it('does not re-promote a legacy attribution path for Agent Teams', () => { + const env = createDaemonPtyEnvironment({ + ...spawn, + env: { + PATH: '/tmp/orca-terminal-attribution/posix:/usr/bin', + ORCA_AGENT_TEAMS_TEAM_ID: 'team-test' + } + }) + expect(env.PATH).toBe('/usr/bin') + }) +}) diff --git a/src/main/daemon/pty-subprocess/spawn-environment-pi-owner.test.ts b/src/main/daemon/pty-subprocess/spawn-environment-pi-owner.test.ts new file mode 100644 index 000000000000..a1b70509de43 --- /dev/null +++ b/src/main/daemon/pty-subprocess/spawn-environment-pi-owner.test.ts @@ -0,0 +1,43 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { createDaemonPtyEnvironment, finalizeDaemonPtyEnvironment } from './spawn-environment' + +const owners = { + ORCA_PI_STATUS_OWNED: '1234', + ORCA_PRIME_AGENT_STATUS_OWNED: '1234', + ORCA_PI_TITLE_MARKER_OWNED: '1234' +} +const terminal = { + ORCA_PANE_KEY: 'new-tab:new-leaf', + ORCA_AGENT_LAUNCH_TOKEN: 'new-launch', + ORCA_AGENT_HOOK_TOKEN: 'receiver-token', + KEEP_ME: 'terminal-value' +} +afterEach(() => vi.unstubAllEnvs()) + +describe('independent daemon terminal Pi ownership', () => { + it.each(['host', 'request'] as const)('does not inherit the %s process owner', (source) => { + if (source === 'host') { + for (const [key, value] of Object.entries(owners)) { + vi.stubEnv(key, value) + } + } + const env = createDaemonPtyEnvironment({ + sessionId: 'new-terminal', + cols: 80, + rows: 24, + env: { ...terminal, ...(source === 'request' ? owners : {}) } + }) + for (const key of Object.keys(owners)) { + expect(env[key]).toBeUndefined() + } + expect(env).toMatchObject(terminal) + }) + it('scrubs owners reintroduced by shell launch preparation', () => { + const env: Record = { ...terminal, ...owners, APP_ENV: 'kept' } + finalizeDaemonPtyEnvironment(env, terminal) + for (const key of Object.keys(owners)) { + expect(env[key]).toBeUndefined() + } + expect(env).toMatchObject({ ...terminal, APP_ENV: 'kept' }) + }) +}) diff --git a/src/main/daemon/pty-subprocess/spawn-environment-preflight.test.ts b/src/main/daemon/pty-subprocess/spawn-environment-preflight.test.ts new file mode 100644 index 000000000000..b69b60801824 --- /dev/null +++ b/src/main/daemon/pty-subprocess/spawn-environment-preflight.test.ts @@ -0,0 +1,23 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { createDaemonPtyEnvironment } from './spawn-environment' + +afterEach(() => vi.unstubAllEnvs()) + +describe('daemon Codex preflight environment', () => { + it('does not resurrect a launcher omitted by the current client', () => { + vi.stubEnv('ORCA_CODEX_LAUNCH_PREFLIGHT', '/old-build/bin/orca') + const env = createDaemonPtyEnvironment({ sessionId: 'test', cols: 80, rows: 24, env: {} }) + expect(env.ORCA_CODEX_LAUNCH_PREFLIGHT).toBeUndefined() + }) + + it('preserves the launcher explicitly verified by the current client', () => { + vi.stubEnv('ORCA_CODEX_LAUNCH_PREFLIGHT', '/old-build/bin/orca') + const env = createDaemonPtyEnvironment({ + sessionId: 'test', + cols: 80, + rows: 24, + env: { ORCA_CODEX_LAUNCH_PREFLIGHT: '/current-build/bin/orca' } + }) + expect(env.ORCA_CODEX_LAUNCH_PREFLIGHT).toBe('/current-build/bin/orca') + }) +}) diff --git a/src/main/daemon/pty-subprocess/spawn-environment.ts b/src/main/daemon/pty-subprocess/spawn-environment.ts index 32281f386187..5673d841c071 100644 --- a/src/main/daemon/pty-subprocess/spawn-environment.ts +++ b/src/main/daemon/pty-subprocess/spawn-environment.ts @@ -30,7 +30,8 @@ const PANE_IDENTITY_ENV_KEYS = [ 'ORCA_WORKTREE_ID', 'ORCA_AGENT_LAUNCH_TOKEN', // Not identity but equally per-spawn: an inherited copy names another launch's CLI. - 'ORCA_WSL_CLI_DIR' + 'ORCA_WSL_CLI_DIR', + 'ORCA_CODEX_LAUNCH_PREFLIGHT' ] as const const WINDOWS_PATH_ENV_KEY_RE = /^path$/i @@ -172,6 +173,9 @@ export function createDaemonPtyEnvironment(opts: PtySubprocessOptions): Record 1 ? 'children' : 'no-children' } diff --git a/src/main/daemon/pty-subprocess/spawn-file-foreground-process.ts b/src/main/daemon/pty-subprocess/spawn-file-foreground-process.ts index 37e748677955..caa83357ecdb 100644 --- a/src/main/daemon/pty-subprocess/spawn-file-foreground-process.ts +++ b/src/main/daemon/pty-subprocess/spawn-file-foreground-process.ts @@ -1,4 +1,4 @@ -import type { IPty } from 'node-pty' +import type { TerminalProcess } from '../../../shared/terminal-process' import { isShellProcess } from '../../../shared/shell-process-detection' import { getCommandTokenPathBasename, @@ -28,12 +28,12 @@ import { readWindowsProcessIdentityTableFresh } from '../../windows/windows-process-table' -export function ptyProcessNameIsSpawnFile(proc: IPty): boolean { +export function ptyProcessNameIsSpawnFile(proc: TerminalProcess): boolean { return 'processNameIsSpawnFile' in proc && proc.processNameIsSpawnFile === true } export function createPtyForegroundResolver( - proc: IPty + proc: TerminalProcess ): typeof resolveAgentForegroundProcessWithAvailability { return ptyProcessNameIsSpawnFile(proc) ? (_pid, fallback, options) => resolveSpawnFileForegroundProcess(proc, fallback, options) @@ -79,7 +79,7 @@ export function resolveSpawnFileForegroundFromRows( } export async function resolveSpawnFileForegroundProcess( - proc: IPty, + proc: TerminalProcess, fallbackProcess: string | null, options: AgentForegroundResolutionOptions = {} ): Promise { diff --git a/src/main/daemon/pty-subprocess/spawn-file-foreground-rejected-agents.test.ts b/src/main/daemon/pty-subprocess/spawn-file-foreground-rejected-agents.test.ts index 3ec912300aba..8ef6ded22f27 100644 --- a/src/main/daemon/pty-subprocess/spawn-file-foreground-rejected-agents.test.ts +++ b/src/main/daemon/pty-subprocess/spawn-file-foreground-rejected-agents.test.ts @@ -1,4 +1,4 @@ -import type { IPty } from 'node-pty' +import type { TerminalProcess } from '../../../shared/terminal-process' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { ProcessTableRow } from '../../../shared/process-table-snapshot' import { __setWindowsProcessTreeLoaderForTests } from '../../windows/windows-process-table' @@ -12,11 +12,10 @@ vi.mock('../../providers/windows-pty-job-membership', () => ({ readWindowsPtyJobProcessIds: members })) -const proc: IPty = { +const proc: TerminalProcess = { pid: 100, cols: 80, rows: 24, - handleFlowControl: false, process: 'powershell.exe', onData: () => ({ dispose() {} }), onExit: () => ({ dispose() {} }), diff --git a/src/main/daemon/pty-subprocess/spawn-preflight-bun.test.ts b/src/main/daemon/pty-subprocess/spawn-preflight-bun.test.ts index d40b30e2b324..20035701d6ec 100644 --- a/src/main/daemon/pty-subprocess/spawn-preflight-bun.test.ts +++ b/src/main/daemon/pty-subprocess/spawn-preflight-bun.test.ts @@ -1,21 +1,68 @@ import { afterEach, beforeEach, expect, it, vi } from 'vitest' -const fixture = vi.hoisted((): { shellPid?: number } => ({})) +const fixture = vi.hoisted( + (): { + shellPid?: number + available: boolean + immediateExit: boolean + dispose: ReturnType + dataDispose: ReturnType + write: ReturnType + waitForDa1: boolean + writeFails: boolean + hang: boolean + kill: ReturnType + } => ({ + shellPid: Number.NaN, + available: true, + immediateExit: false, + dispose: vi.fn(), + dataDispose: vi.fn(), + write: vi.fn(), + waitForDa1: false, + writeFails: false, + hang: false, + kill: vi.fn() + }) +) vi.mock('./bun-pty-process', () => ({ - canUseBunPty: () => true, + canUseBunPty: () => fixture.available, spawnBunPty: () => ({ pid: 41, get shellProcessId() { return fixture.shellPid }, + write: fixture.write, + kill: fixture.kill, + onData(callback: (data: string) => void) { + if (fixture.waitForDa1) { + callback('\x1b[') + callback('0c') + } + return { dispose: fixture.dataDispose } + }, onExit(callback: (event: { exitCode: number }) => void) { - queueMicrotask(() => callback({ exitCode: 0 })) - return { dispose() {} } + if (fixture.hang) { + return { dispose: fixture.dispose } + } + if (fixture.waitForDa1) { + fixture.write.mockImplementation(() => { + if (fixture.writeFails) { + throw new Error('probe write failed') + } + callback({ exitCode: 0 }) + }) + } else if (fixture.immediateExit) { + callback({ exitCode: 0 }) + } else { + queueMicrotask(() => callback({ exitCode: 0 })) + } + return { dispose: fixture.dispose } } }) })) -import { runPtySpawnHealthProbe } from './spawn-preflight' +import { preflightPtySpawnHealth, runPtySpawnHealthProbe } from './spawn-preflight' beforeEach(() => vi.stubGlobal( @@ -37,5 +84,82 @@ it.each([undefined, 41, 0])( it('accepts successful exit with the separate original shell identity', async () => { fixture.shellPid = 42 + fixture.dispose.mockClear() await expect(runPtySpawnHealthProbe()).resolves.toBeUndefined() + expect(fixture.dispose).toHaveBeenCalledOnce() +}) + +it('rejects health preflight outside the bundled runtime', async () => { + fixture.available = false + try { + expect(() => preflightPtySpawnHealth()).toThrow('requires the bundled Bun') + await expect(runPtySpawnHealthProbe()).rejects.toThrow('requires the bundled Bun') + } finally { + fixture.available = true + } +}) + +it('disposes the subscription when a probe has already exited before registration', async () => { + fixture.shellPid = 42 + fixture.immediateExit = true + fixture.dispose.mockClear() + try { + await expect(runPtySpawnHealthProbe()).resolves.toBeUndefined() + expect(fixture.dispose).toHaveBeenCalledOnce() + } finally { + fixture.immediateExit = false + } +}) + +it('answers split DA1 queries and releases probe resources', async () => { + fixture.shellPid = 42 + fixture.waitForDa1 = true + fixture.write.mockReset() + fixture.dataDispose.mockClear() + try { + await expect(runPtySpawnHealthProbe()).resolves.toBeUndefined() + expect(fixture.write).toHaveBeenCalledExactlyOnceWith('\x1b[?1;2c') + expect(fixture.dataDispose).toHaveBeenCalledOnce() + } finally { + fixture.waitForDa1 = false + fixture.write.mockReset() + } +}) + +it('kills the probe and releases subscriptions if the DA1 reply fails', async () => { + fixture.shellPid = 42 + fixture.waitForDa1 = true + fixture.writeFails = true + fixture.kill.mockClear() + fixture.dispose.mockClear() + fixture.dataDispose.mockClear() + try { + await expect(runPtySpawnHealthProbe()).rejects.toThrow('probe write failed') + expect(fixture.kill).toHaveBeenCalledOnce() + expect(fixture.dispose).toHaveBeenCalledOnce() + expect(fixture.dataDispose).toHaveBeenCalledOnce() + } finally { + fixture.waitForDa1 = false + fixture.writeFails = false + fixture.write.mockReset() + } +}) + +it('kills a timed-out probe and releases both subscriptions', async () => { + vi.useFakeTimers() + fixture.hang = true + fixture.kill.mockClear() + fixture.dispose.mockClear() + fixture.dataDispose.mockClear() + try { + const rejected = expect(runPtySpawnHealthProbe()).rejects.toThrow('timed out') + await vi.advanceTimersByTimeAsync(4_000) + await rejected + expect(fixture.kill).toHaveBeenCalledOnce() + expect(fixture.dispose).toHaveBeenCalledOnce() + expect(fixture.dataDispose).toHaveBeenCalledOnce() + } finally { + fixture.hang = false + vi.useRealTimers() + } }) diff --git a/src/main/daemon/pty-subprocess/spawn-preflight.ts b/src/main/daemon/pty-subprocess/spawn-preflight.ts index 404f5b258f04..1109eaae5f6d 100644 --- a/src/main/daemon/pty-subprocess/spawn-preflight.ts +++ b/src/main/daemon/pty-subprocess/spawn-preflight.ts @@ -1,13 +1,18 @@ -import type * as pty from 'node-pty' +import '../xterm-env-polyfill' +import { Terminal } from '@xterm/headless' +import { + installDeviceAttributesResponder, + STARTUP_DA1_RESPONSE +} from '../startup-device-attributes-responder' +import type { BunPtyProcess } from './bun-pty-process-contract' +import { isWindowsAbsolutePathLike } from '../../../shared/cross-platform-path' import { statSync } from 'node:fs' import { release } from 'node:os' import { getCmdExePath } from '../../../shared/windows-batch-spawn' import { - ensureNodePtySpawnHelperExecutable, - getNodePtySpawnHelperCandidates, validateWorkingDirectoryAsync, WorkingDirectoryValidationAbortedError -} from '../../providers/local-pty-utils' +} from '../../providers/pty-spawn-validation' import { resolveSafePtyDefaultCwd } from '../../providers/pty-default-cwd' import { TerminalAttachCanceledError } from '../daemon-errors' import { DaemonProtocolError } from '../types' @@ -15,10 +20,6 @@ import { canUseBunPty, spawnBunPty } from './bun-pty-process' const PTY_SPAWN_HEALTH_TIMEOUT_MS = 4_000 -async function loadNodePty(): Promise { - return import('node-pty') -} - function daemonEnvironmentDiagSuffix(): string { const orca = process.env.ORCA_APP_VERSION?.trim() || '0.0.0-dev' const systemVersion = @@ -27,15 +28,6 @@ function daemonEnvironmentDiagSuffix(): string { return ` (orca: ${orca}, arch: ${process.arch}, platform: ${process.platform} ${systemVersion})` } -function formatMissingDaemonPathError(kind: 'helper' | 'cwd', path: string): DaemonProtocolError { - const detailName = kind === 'helper' ? 'helper' : 'cwd' - const step = kind === 'helper' ? 'posix_spawn' : 'daemon_cwd' - const missingTarget = kind === 'helper' ? 'node-pty install' : 'working directory' - return new DaemonProtocolError( - `Daemon's ${missingTarget} is gone (worktree deleted?). Restart Orca. node-pty: ${step} failed: ENOENT (errno 2, No such file or directory) - ${detailName}='${path}'${daemonEnvironmentDiagSuffix()}` - ) -} - function isExistingDirectory(path: string | undefined): path is string { if (!path) { return false @@ -81,29 +73,9 @@ function preflightDaemonCwd(): void { if (repairDaemonCwd()) { return } - throw formatMissingDaemonPathError('cwd', daemonCwd) -} - -function preflightMacNodePtySpawnEnvironment(): void { - if (process.platform !== 'darwin' || canUseBunPty()) { - return - } - let candidates: string[] - try { - candidates = getNodePtySpawnHelperCandidates() - } catch { - throw formatMissingDaemonPathError('helper', '') - } - for (const candidate of candidates) { - try { - if (statSync(candidate).isFile()) { - return - } - } catch { - // Try the next node-pty native location. - } - } - throw formatMissingDaemonPathError('helper', candidates[0] ?? '') + throw new DaemonProtocolError( + `Daemon working directory is unavailable: '${daemonCwd}'. Restart Orca.${daemonEnvironmentDiagSuffix()}` + ) } function preflightUnixPtySpawnEnvironment(): void { @@ -112,11 +84,6 @@ function preflightUnixPtySpawnEnvironment(): void { } // Why: detached daemons can outlive their launch cwd; repair before every spawn. preflightDaemonCwd() - preflightMacNodePtySpawnEnvironment() -} - -function isNativeWindowsPath(path: string): boolean { - return /^[A-Za-z]:[\\/]/.test(path) || path.startsWith('\\\\') } export async function preflightPtySpawn(args: { @@ -126,12 +93,12 @@ export async function preflightPtySpawn(args: { signal?: AbortSignal }): Promise { if (!canUseBunPty()) { - ensureNodePtySpawnHelperExecutable() + throw new DaemonProtocolError('Terminal service requires the bundled Bun runtime') } preflightUnixPtySpawnEnvironment() try { if (process.platform === 'win32') { - if (args.cwdWasExplicit && isNativeWindowsPath(args.validationCwd)) { + if (args.cwdWasExplicit && isWindowsAbsolutePathLike(args.validationCwd)) { await validateWorkingDirectoryAsync( args.validationCwd, args.signal ? { signal: args.signal } : {} @@ -163,7 +130,10 @@ export function formatPtySpawnError(err: unknown, shellPath: string, spawnCwd: s } export async function runPtySpawnHealthProbe(): Promise { - const requiresShellIdentity = process.platform === 'win32' && canUseBunPty() + if (!canUseBunPty()) { + throw new DaemonProtocolError('Terminal service requires the bundled Bun runtime') + } + const requiresShellIdentity = process.platform === 'win32' const cwd = isExistingDirectory(process.env.ORCA_USER_DATA_PATH) ? process.env.ORCA_USER_DATA_PATH : resolveSafePtyDefaultCwd() @@ -171,7 +141,7 @@ export async function runPtySpawnHealthProbe(): Promise { process.platform === 'win32' ? { file: getCmdExePath(), args: ['/d', '/c', 'exit', '0'] } : { file: '/bin/sh', args: ['-c', 'exit 0'] } - let proc: pty.IPty + let proc: BunPtyProcess try { const env: Record = { TERM: 'xterm-256color' } for (const [key, value] of Object.entries(process.env)) { @@ -179,21 +149,29 @@ export async function runPtySpawnHealthProbe(): Promise { env[key] = value } } - proc = canUseBunPty() - ? spawnBunPty({ ...command, cols: 2, rows: 1, cwd, env }) - : (await loadNodePty()).spawn(command.file, command.args, { - name: 'xterm-256color', - cols: 2, - rows: 1, - cwd, - env - }) + proc = spawnBunPty({ ...command, cols: 2, rows: 1, cwd, env, windowsJobKillOnClose: true }) } catch (err) { throw formatPtySpawnError(err, command.file, cwd) } return new Promise((resolve, reject) => { let settled = false + const terminal = new Terminal({ cols: 2, rows: 1, scrollback: 0 }) + const releaseResponder = installDeviceAttributesResponder({ + parser: terminal.parser, + response: STARTUP_DA1_RESPONSE, + reply: (data) => { + if (settled) { + return + } + try { + proc.write(data) + } catch (error) { + finish(formatPtySpawnError(error, command.file, cwd), { kill: true }) + } + } + }) + let dataDisposable: { dispose(): void } | undefined let exitDisposable: { dispose(): void } | undefined const finish = (error?: Error, opts?: { kill?: boolean }): void => { if (settled) { @@ -202,6 +180,9 @@ export async function runPtySpawnHealthProbe(): Promise { settled = true clearTimeout(timer) exitDisposable?.dispose() + dataDisposable?.dispose() + releaseResponder() + terminal.dispose() if (opts?.kill) { try { proc.kill() @@ -220,9 +201,15 @@ export async function runPtySpawnHealthProbe(): Promise { kill: true }) }, PTY_SPAWN_HEALTH_TIMEOUT_MS) + // ConPTY waits for DA1 even when the probe shell immediately exits. + dataDisposable = proc.onData((data) => { + if (!settled) { + terminal.write(data) + } + }) exitDisposable = proc.onExit(({ exitCode }) => { if (exitCode === 0) { - const shellPid = 'shellProcessId' in proc ? proc.shellProcessId : undefined + const shellPid = proc.shellProcessId if ( requiresShellIdentity && (typeof shellPid !== 'number' || @@ -238,15 +225,15 @@ export async function runPtySpawnHealthProbe(): Promise { finish(new Error(`PTY spawn health check exited with code ${exitCode}`)) } }) + if (settled) { + exitDisposable.dispose() + } }) } export function preflightPtySpawnHealth(): boolean { - if (process.platform === 'win32' && !canUseBunPty()) { - return false - } if (!canUseBunPty()) { - ensureNodePtySpawnHelperExecutable() + throw new DaemonProtocolError('Terminal service requires the bundled Bun runtime') } preflightUnixPtySpawnEnvironment() return true diff --git a/src/main/daemon/pty-subprocess/subprocess-handle-subscriptions.test.ts b/src/main/daemon/pty-subprocess/subprocess-handle-subscriptions.test.ts new file mode 100644 index 000000000000..f903466dfed8 --- /dev/null +++ b/src/main/daemon/pty-subprocess/subprocess-handle-subscriptions.test.ts @@ -0,0 +1,91 @@ +import { describe, expect, it, vi } from 'vitest' +import type { TerminalProcess } from '../../../shared/terminal-process' +import { createDaemonPtySubprocessHandle } from './subprocess-handle' + +function createFixture(options: { synchronousExit?: boolean; synchronousData?: string } = {}) { + let physicalExit: (event: { exitCode: number; signal?: number }) => void = () => {} + const dataDispose = vi.fn() + const exitDispose = vi.fn() + const process: TerminalProcess = { + pid: 999_999_999, + process: 'zsh', + cols: 80, + rows: 24, + clear: vi.fn(), + write: vi.fn(), + resize: vi.fn(), + kill: vi.fn(), + pause: vi.fn(), + resume: vi.fn(), + onData: (listener) => { + if (options.synchronousData) { + listener(options.synchronousData) + } + return { dispose: dataDispose } + }, + onExit: (listener) => { + physicalExit = listener + if (options.synchronousExit) { + listener({ exitCode: 0 }) + } + return { dispose: exitDispose } + } + } + const handle = createDaemonPtySubprocessHandle({ + process, + shellPath: '/bin/zsh', + spawnCwd: '/tmp', + env: {}, + startupCommandDeliveredInShellArgs: false, + reportsChildExitStatus: true, + sessionId: 'subscription-test', + startupAgentRecognition: null + }) + return { + handle, + physicalExit: (event: { exitCode: number; signal?: number } = { exitCode: 0 }) => + physicalExit(event), + dataDispose, + exitDispose + } +} + +describe('daemon native PTY subscription ownership', () => { + it('retains native exit observation until physical exit, even when disposal was requested', () => { + const fixture = createFixture() + fixture.handle.dispose() + expect(fixture.exitDispose).not.toHaveBeenCalled() + fixture.physicalExit() + expect(fixture.dataDispose).toHaveBeenCalledOnce() + expect(fixture.exitDispose).toHaveBeenCalledOnce() + fixture.handle.dispose() + expect(fixture.exitDispose).toHaveBeenCalledOnce() + }) + + it('releases both native subscriptions when an exit listener disposes reentrantly', () => { + const fixture = createFixture() + fixture.handle.onExit(() => fixture.handle.dispose()) + fixture.physicalExit() + expect(fixture.dataDispose).toHaveBeenCalledOnce() + expect(fixture.exitDispose).toHaveBeenCalledOnce() + }) + + it('buffers synchronous registration events and still releases each subscription once', () => { + const fixture = createFixture({ synchronousData: 'last output', synchronousExit: true }) + const data = vi.fn() + const exit = vi.fn(() => fixture.handle.dispose()) + fixture.handle.onData(data) + fixture.handle.onExit(exit) + expect(data).toHaveBeenCalledWith('last output') + expect(exit).toHaveBeenCalledOnce() + expect(fixture.dataDispose).toHaveBeenCalledOnce() + expect(fixture.exitDispose).toHaveBeenCalledOnce() + }) + it('preserves signal provenance when a native exit also carries a zero exit code', () => { + const fixture = createFixture() + const onExit = vi.fn(() => fixture.handle.dispose()) + fixture.handle.onExit(onExit) + fixture.physicalExit({ exitCode: 0, signal: 9 }) + expect(onExit).toHaveBeenCalledWith(0, { kind: 'signaled', signal: 9 }) + }) +}) diff --git a/src/main/daemon/pty-subprocess/subprocess-handle.ts b/src/main/daemon/pty-subprocess/subprocess-handle.ts index 56b739bfccc0..aaa36dc34241 100644 --- a/src/main/daemon/pty-subprocess/subprocess-handle.ts +++ b/src/main/daemon/pty-subprocess/subprocess-handle.ts @@ -1,10 +1,10 @@ -import type * as pty from 'node-pty' +import type { TerminalProcess } from '../../../shared/terminal-process' import type { RecognizedAgentProcess } from '../../../shared/agent-process-recognition' import { readPtySlavePath } from '../../../shared/pty-slave-line-discipline-echo' import { forceKillPosixPtyProcessGroups } from '../../pty/posix-pty-process-groups' import { signalPosixPtyForegroundGroup } from '../../pty/posix-pty-foreground-group' -import { readPtsName } from '../../pty/node-pty-pts-name' -import { terminatePtyJob } from '../../windows/windows-pty-job' +import { readPtsName } from '../../pty/terminal-slave-device' +import { ptyShellProcessId, terminatePtyJob } from '../../windows/windows-pty-job' import { isValidPtySize } from '../daemon-pty-size' import type { SubprocessHandle } from '../session-subprocess-handle' import { createPtyForegroundProcessTracker } from './foreground-process-tracker' @@ -12,13 +12,13 @@ import { PtyPreListenerEvents } from './pre-listener-events' import { ptyProcessNameIsSpawnFile } from './spawn-file-foreground-process' import { inspectSpawnFileWindowsChildProcesses } from './spawn-file-child-processes' -type DisposableNativePty = pty.IPty & { +type DisposableNativePty = TerminalProcess & { destroy?: () => void signalProcess?: (signal: string) => void } export function createDaemonPtySubprocessHandle(args: { - process: pty.IPty + process: TerminalProcess shellPath: string spawnCwd: string env: Record @@ -38,6 +38,23 @@ export function createDaemonPtySubprocessHandle(args: { let ioFailed = false let disposed = false let nodePtyKillIssued = false + let physicalExitObserved = false + let dataSubscription: { dispose(): void } | null = null + let exitSubscription: { dispose(): void } | null = null + const releaseNativeSubscriptions = (): void => { + if (!disposed || !physicalExitObserved) { + return + } + const data = dataSubscription + const exit = exitSubscription + dataSubscription = null + exitSubscription = null + try { + data?.dispose() + } finally { + exit?.dispose() + } + } const foreground = createPtyForegroundProcessTracker({ process: proc, shellPath: args.shellPath, @@ -47,11 +64,12 @@ export function createDaemonPtySubprocessHandle(args: { isDead: () => dead }) - proc.onData((data) => { + dataSubscription = proc.onData((data) => { foreground.recordOutput(data) events.acceptData(data) }) - proc.onExit(({ exitCode, signal }) => { + exitSubscription = proc.onExit(({ exitCode, signal }) => { + physicalExitObserved = true // Exit listeners may re-enter cleanup; retire signal authority before notifying them. dead = true foreground.markDead() @@ -59,16 +77,22 @@ export function createDaemonPtySubprocessHandle(args: { if (process.platform !== 'win32') { nativeProc.kill = () => {} } - events.acceptExit({ - exitCode, - signal, - hostReportsChildExitStatus: reportsChildExitStatus - }) + try { + events.acceptExit({ + exitCode, + signal, + hostReportsChildExitStatus: reportsChildExitStatus + }) + } finally { + releaseNativeSubscriptions() + } }) + releaseNativeSubscriptions() const slavePath = readPtySlavePath(proc) return { pid: proc.pid, + getShellProcessId: () => ptyShellProcessId(proc), processNameIsSpawnFile: ptyProcessNameIsSpawnFile(proc), ...(process.platform === 'win32' ? { inspectChildProcesses: () => inspectSpawnFileWindowsChildProcesses(proc) } @@ -206,6 +230,7 @@ export function createDaemonPtySubprocessHandle(args: { disposed = true dead = true events.clear() + releaseNativeSubscriptions() // POSIX destroy() can asynchronously signal a recycled pid; Windows needs kill() to close ConPTY. if (process.platform !== 'win32') { nativeProc.kill = () => {} diff --git a/src/main/daemon/pty-subprocess/subprocess-windows-shell-identity.test.ts b/src/main/daemon/pty-subprocess/subprocess-windows-shell-identity.test.ts new file mode 100644 index 000000000000..72bb8c8483db --- /dev/null +++ b/src/main/daemon/pty-subprocess/subprocess-windows-shell-identity.test.ts @@ -0,0 +1,99 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import type { TerminalProcess } from '../../../shared/terminal-process' +import { Session } from '../session' +import { resolveTerminalHostSessionCwd } from '../terminal-host-session-cwd' +import { createDaemonPtySubprocessHandle } from './subprocess-handle' + +const { cwd, confirm, membership } = vi.hoisted(() => ({ + cwd: vi.fn(), + confirm: vi.fn(), + membership: vi.fn() +})) +vi.mock('../../providers/process-cwd', () => ({ resolveProcessCwd: cwd })) +vi.mock('../../providers/agent-foreground-process', () => ({ + confirmShellForegroundProcess: confirm, + resolveAgentForegroundProcessWithAvailability: vi.fn() +})) +vi.mock('../../providers/windows-pty-job-membership', () => ({ + readWindowsPtyJobProcessIds: membership, + isWindowsPtyJobReadable: () => true +})) +const platform = Object.getOwnPropertyDescriptor(process, 'platform')! +afterEach(() => { + Object.defineProperty(process, 'platform', platform) + vi.restoreAllMocks() +}) + +describe('daemon gated Windows shell identity', () => { + it('resolves the child shell cwd without changing the owned process root', async () => { + Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) + cwd.mockResolvedValue('C:\\work') + let exit: (event: { exitCode: number }) => void = () => {} + const proc: TerminalProcess & { + shellProcessId?: number + jobRootProcessIsWrapper: true + signalProcess: ReturnType + } = { + pid: 999_999_999, + shellProcessId: 999_999_998, + jobRootProcessIsWrapper: true, + signalProcess: vi.fn(), + process: 'cmd.exe', + cols: 80, + rows: 24, + clear: vi.fn(), + write: vi.fn(), + resize: vi.fn(), + kill: vi.fn(), + pause: vi.fn(), + resume: vi.fn(), + onData: () => ({ dispose() {} }), + onExit: (listener) => { + exit = listener + return { dispose() {} } + } + } + const subprocess = createDaemonPtySubprocessHandle({ + process: proc, + shellPath: 'cmd.exe', + spawnCwd: '', + env: {}, + startupCommandDeliveredInShellArgs: false, + reportsChildExitStatus: true, + sessionId: 'gated-shell', + startupAgentRecognition: null + }) + const session = new Session({ + sessionId: 'gated-shell', + cols: 80, + rows: 24, + subprocess, + shellReadySupported: false + }) + try { + expect(session.pid).toBe(999_999_999) + expect(await resolveTerminalHostSessionCwd(session)).toBe('C:\\work') + expect(cwd).toHaveBeenCalledWith(999_999_998) + confirm.mockResolvedValue(true) + expect(await subprocess.confirmShellForeground?.()).toBe(true) + expect(confirm).toHaveBeenCalledWith(999_999_998, 'cmd.exe', expect.any(Object)) + const kill = vi.spyOn(process, 'kill') + subprocess.signal('SIGTERM') + expect(proc.signalProcess).toHaveBeenCalledWith('SIGTERM') + expect(kill).not.toHaveBeenCalled() + membership.mockReturnValue(new Set([999_999_998])) + expect(session.inspectChildProcesses()).toBe('no-children') + membership.mockReturnValue(new Set([999_999_998, 999_999_997])) + expect(session.inspectChildProcesses()).toBe('children') + membership.mockReturnValue(null) + expect(session.inspectChildProcesses()).toBe('unverifiable') + delete proc.shellProcessId + cwd.mockClear() + expect(await resolveTerminalHostSessionCwd(session)).toBeNull() + expect(cwd).not.toHaveBeenCalled() + } finally { + exit({ exitCode: 0 }) + session.dispose() + } + }) +}) diff --git a/src/main/daemon/pty-subprocess/windows-bun-pty-gate.integration.test.ts b/src/main/daemon/pty-subprocess/windows-bun-pty-gate.integration.test.ts index bb1c3d88a478..22a62508d5e9 100644 --- a/src/main/daemon/pty-subprocess/windows-bun-pty-gate.integration.test.ts +++ b/src/main/daemon/pty-subprocess/windows-bun-pty-gate.integration.test.ts @@ -5,7 +5,7 @@ import { join, resolve } from 'node:path' import { describe, expect, it } from 'vitest' import { runProcess, runProcessSync } from '../../../shared/child-process/run-process' import { orcadBunRuntimeFilename } from '../../../shared/orcad-artifacts' -import { ORCAD_BUN_VERSION } from '../../../shared/orcad-bun-runtime' +import { ORCAD_BUN_VERSION, orcadBunVersionProbeArgs } from '../../../shared/orcad-bun-runtime' import { createWindowsBunPtyLaunch } from './windows-bun-pty-launch' const runtimePath = @@ -15,9 +15,12 @@ const available = existsSync(runtimePath) describe.skipIf(!available)('bundled Windows job gate under Bun', () => { it('executes the worker with real Bun flags and preserves long executable argv', async () => { - expect(runProcessSync({ program: runtimePath, args: ['--version'] }).stdout.trim()).toBe( - ORCAD_BUN_VERSION - ) + expect( + runProcessSync({ + program: runtimePath, + args: [...orcadBunVersionProbeArgs()] + }).stdout.trim() + ).toBe(ORCAD_BUN_VERSION) const directory = mkdtempSync(join(tmpdir(), 'orca-gate-contract-')) const workerPath = join(directory, 'windows-bun-pty-gate-entry.js') try { diff --git a/src/main/daemon/pty-subprocess/windows-bun-pty-job.test.ts b/src/main/daemon/pty-subprocess/windows-bun-pty-job.test.ts index ccc3ebc6d1ad..56c95a63cf42 100644 --- a/src/main/daemon/pty-subprocess/windows-bun-pty-job.test.ts +++ b/src/main/daemon/pty-subprocess/windows-bun-pty-job.test.ts @@ -156,3 +156,12 @@ describe('Windows Bun PTY job ownership', () => { expect(warn).toHaveBeenCalledOnce() }) }) + +it('arms kill-on-close only for explicitly transient PTY jobs', () => { + const native = createNative() + const job = createWindowsBunPtyJob(11, native, true) + expect(job).not.toBeNull() + expect(native.configureJob).toHaveBeenCalledWith(7, 0x2000) + job?.close() + expect(native.closeHandle).toHaveBeenCalledWith(7) +}) diff --git a/src/main/daemon/pty-subprocess/windows-bun-pty-job.ts b/src/main/daemon/pty-subprocess/windows-bun-pty-job.ts index 5f0a7065c85e..cf7229a08010 100644 --- a/src/main/daemon/pty-subprocess/windows-bun-pty-job.ts +++ b/src/main/daemon/pty-subprocess/windows-bun-pty-job.ts @@ -180,13 +180,17 @@ class BunPtyJob implements WindowsBunPtyJob { export function createWindowsBunPtyJob( rootPid: number, - native: WindowsBunPtyJobNative | null = loadWindowsBunPtyJobNative() + native: WindowsBunPtyJobNative | null = loadWindowsBunPtyJobNative(), + killOnClose = false ): WindowsBunPtyJob | null { if (!native || !Number.isInteger(rootPid) || rootPid <= 0) { return null } const job = native.createJob() - if (job === null || !native.configureJob(job, 0)) { + if ( + job === null || + !native.configureJob(job, killOnClose ? JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE : 0) + ) { if (job !== null) { native.closeHandle(job) } diff --git a/src/main/daemon/pty-subprocess/windows-bun-pty-launch.test.ts b/src/main/daemon/pty-subprocess/windows-bun-pty-launch.test.ts index 1f4df4ef749f..5f4d23a03681 100644 --- a/src/main/daemon/pty-subprocess/windows-bun-pty-launch.test.ts +++ b/src/main/daemon/pty-subprocess/windows-bun-pty-launch.test.ts @@ -69,6 +69,7 @@ describe('Windows Bun PTY gated launch', () => { expect(child.env).not.toHaveProperty('ORCA_BUN_PTY_JOB_GATE') expect(launch.windowsVerbatimArguments).toBe(false) expect(launch.command).toContain('--no-env-file') + expect(launch.command).toContain('--no-install') expect(launch.command).toContain(`--config=${join(directory, 'bunfig.toml')}`) expect(launch.command).toContain(`--cwd=${directory}`) expect(launch.command.join(' ').length).toBeLessThan(8191) diff --git a/src/main/daemon/session-options.ts b/src/main/daemon/session-options.ts index 1e2d38814d42..065acf1defb6 100644 --- a/src/main/daemon/session-options.ts +++ b/src/main/daemon/session-options.ts @@ -11,6 +11,7 @@ export type SessionOptions = { launchAgent?: TuiAgent subprocess: SubprocessHandle shellReadySupported: boolean + requireSynchronousOutput?: boolean shellReadyTimeoutMs?: number /** Reports a readiness outcome worth diagnosing to the daemon's file log. * Why not console: the detached daemon runs with stdio 'ignore'. */ diff --git a/src/main/daemon/session-output-observation.ts b/src/main/daemon/session-output-observation.ts new file mode 100644 index 000000000000..99234dac7653 --- /dev/null +++ b/src/main/daemon/session-output-observation.ts @@ -0,0 +1,38 @@ +import type { SubprocessHandle } from './session-subprocess-handle' +import type { SessionShellReadyBarrier } from './session-shell-ready-barrier' +import type { SessionOutputPlane } from './session-output-plane' +import type { TerminalShellRecoveryBarrier } from './terminal-shell-recovery-barrier' +import type { PtyStartupIngress } from '../../shared/pty-startup-ingress' + +/** A rejected subscription must release the parser and readiness resources it already created. */ +export function installSessionOutputObservation(opts: { + subprocess: SubprocessHandle + shellReady: SessionShellReadyBarrier + ingress: PtyStartupIngress + recovery: TerminalShellRecoveryBarrier + output: SessionOutputPlane + isDisposed(): boolean + onFailure(): void + onExit: Parameters[0] +}): void { + try { + opts.shellReady.startPromptReadinessProbe() + opts.subprocess.onData((data) => { + if (!opts.isDisposed()) { + opts.shellReady.ingestSubprocessData(data) + } + }) + opts.subprocess.onExit(opts.onExit) + } catch (error) { + opts.onFailure() + opts.shellReady.releaseDeviceAttributes() + opts.shellReady.dispose() + try { + opts.ingress.drainAndClose() + } finally { + opts.recovery.dispose() + opts.output.disposeEmulator() + } + throw error + } +} diff --git a/src/main/daemon/session-output-pipeline.ts b/src/main/daemon/session-output-pipeline.ts index f67d2e15b927..2a6f2e9487ef 100644 --- a/src/main/daemon/session-output-pipeline.ts +++ b/src/main/daemon/session-output-pipeline.ts @@ -11,12 +11,14 @@ export function createSessionOutputPipeline(opts: { scrollback?: number | undefined wslDistro?: string | undefined historySeedChunks?: readonly string[] | undefined + requireSynchronousOutput?: boolean subprocess: SubprocessHandle isAlive: () => boolean }): { output: SessionOutputPlane; recoveryBarrier: TerminalShellRecoveryBarrier } { const { subprocess, isAlive } = opts let barrier: TerminalShellRecoveryBarrier | null = null const output = new SessionOutputPlane({ + requireSynchronousOutput: opts.requireSynchronousOutput, cols: opts.cols, rows: opts.rows, scrollback: opts.scrollback, diff --git a/src/main/daemon/session-output-plane.ts b/src/main/daemon/session-output-plane.ts index 507ca01a927e..27523a91a073 100644 --- a/src/main/daemon/session-output-plane.ts +++ b/src/main/daemon/session-output-plane.ts @@ -24,6 +24,7 @@ export type SessionOutputPlaneOptions = { scrollback?: number | undefined wslDistro?: string | undefined historySeedChunks?: readonly string[] | undefined + requireSynchronousOutput?: boolean /** Read from the recovery barrier at snapshot time; the barrier scans bytes * before this plane receives them, so its owner never lags the emulator. */ getTerminalOwner?: (() => TerminalOwner | undefined) | undefined @@ -43,6 +44,7 @@ export class SessionOutputPlane { private _outputSequence = 0 private deviceAttributesQueryFilter: StartupDeviceAttributesQueryFilter | null = null private disposed = false + private exit: { code: number; incarnationId: string; cause?: TerminalExitCause } | undefined constructor(opts: SessionOutputPlaneOptions) { const size = normalizePtySize(opts.cols, opts.rows) @@ -63,6 +65,10 @@ export class SessionOutputPlane { opts.historySeedChunks === undefined ? undefined : opts.historySeedChunks.every((chunk) => this.emulator.writeSync(chunk)) + if (opts.requireSynchronousOutput && !this.emulator.writeSync('')) { + this.emulator.dispose() + throw new Error('Startup terminal output requires synchronous parsing') + } this.readTerminalOwner = opts.getTerminalOwner } @@ -74,9 +80,40 @@ export class SessionOutputPlane { return this.attachedClients.length > 0 } - attachClient(client: Omit): symbol { + attachClient(client: Omit, replayStartup = false): symbol { const token = Symbol('attach') this.attachedClients.push({ token, ...client }) + try { + if (replayStartup && this._outputSequence > 0) { + if (this.pendingOutputOverflowed) { + const snapshot = this.getSnapshot() + if (!snapshot) { + throw new Error('Startup terminal snapshot is unavailable') + } + { + client.onData( + `\x1b[3J\x1b[2J\x1b[H${snapshot.scrollbackAnsi}${snapshot.rehydrateSequences}${ + snapshot.snapshotAnsi + }${snapshot.pendingEscapeTailAnsi ?? ''}`, + this._outputSequence, + true, + this._outputSequence + ) + } + } else { + const data = this.pendingOutputRecords + .flatMap((record) => (record.kind === 'output' ? [record.data] : [])) + .join('') + client.onData(data, this._outputSequence, true, this._outputSequence) + } + } + } catch (error) { + this.detachClient(token) + throw error + } + if (replayStartup && this.exit) { + client.onExit(this.exit.code, this.exit.incarnationId, this.exit.cause) + } return token } @@ -100,6 +137,7 @@ export class SessionOutputPlane { // Why the fallback here: a handle that predates exit causes still reports a // code, and every client deserves the same shape. const resolved = cause ?? resolveProcessExitCause({ exitCode: code }) + this.exit = { code, incarnationId, cause: resolved } for (const client of this.attachedClients) { client.onExit(code, incarnationId, resolved) } diff --git a/src/main/daemon/session-output-snapshot-fidelity.test.ts b/src/main/daemon/session-output-snapshot-fidelity.test.ts new file mode 100644 index 000000000000..1f44d515f0c9 --- /dev/null +++ b/src/main/daemon/session-output-snapshot-fidelity.test.ts @@ -0,0 +1,50 @@ +import { expect, it } from 'vitest' +import { HeadlessEmulator } from './headless-emulator' +import { SessionOutputPlane } from './session-output-plane' +import { RecentPtyOutputBuffer } from '../runtime/recent-pty-output-buffer' + +it('retains a persistent header after redraw traffic outgrows the relay replay tail', async () => { + const dimensions = { cols: 80, rows: 24 } + const plane = new SessionOutputPlane(dimensions) + const rawTail = new RecentPtyOutputBuffer({ limit: 100 * 1024 }) + const fromTail = new HeadlessEmulator(dimensions) + const fromSnapshot = new HeadlessEmulator(dimensions) + let sequence = 0 + const emit = (data: string): void => { + const start = sequence + sequence += data.length + rawTail.append(data) + plane.emit({ data, rawStartSeq: start, rawEndSeq: sequence, transformed: false }) + } + try { + emit('\x1b[2J\x1b[HIMPORTANT HEADER\x1b[2;1H') + for (let frame = 0; frame < 12_000; frame++) { + emit(`\r\x1b[2Kprogress ${frame}`) + } + expect(sequence).toBeGreaterThan(100 * 1024) + await plane.flushParsedWrites() + const snapshot = plane.getSnapshot() + expect(snapshot).not.toBeNull() + if (!snapshot) { + throw new Error('Expected an owner snapshot') + } + await fromTail.write(rawTail.read()) + await fromSnapshot.write( + snapshot.scrollbackAnsi + snapshot.rehydrateSequences + snapshot.snapshotAnsi + ) + expect(fromTail.getVisibleLines().join('\n')).not.toContain('IMPORTANT HEADER') + expect(fromSnapshot.getVisibleLines()[0]).toContain('IMPORTANT HEADER') + expect(fromSnapshot.getVisibleLines()[1]).toContain('progress 11999') + expect(snapshot.outputSequence).toBe(sequence) + emit(' LIVE') + await plane.flushParsedWrites() + await fromSnapshot.write(' LIVE') + expect(fromSnapshot.getVisibleLines()[1]).toContain('progress 11999 LIVE') + expect(plane.getSnapshot()?.outputSequence).toBe(sequence) + } finally { + plane.markDisposed() + plane.disposeEmulator() + fromTail.dispose() + fromSnapshot.dispose() + } +}) diff --git a/src/main/daemon/session-subprocess-handle.ts b/src/main/daemon/session-subprocess-handle.ts index be336c998f2a..9ee09374e232 100644 --- a/src/main/daemon/session-subprocess-handle.ts +++ b/src/main/daemon/session-subprocess-handle.ts @@ -5,6 +5,7 @@ import type { JobTerminationOutcome } from '../windows/windows-pty-job' export type SubprocessHandle = { pid: number + getShellProcessId?(): number | undefined processNameIsSpawnFile?: boolean inspectChildProcesses?(): PtyChildProcessVerdict /** Live foreground process name of the PTY (node-pty's `.process`), e.g. diff --git a/src/main/daemon/session-termination-controller.ts b/src/main/daemon/session-termination-controller.ts index 9ccee58af650..5169822b10d6 100644 --- a/src/main/daemon/session-termination-controller.ts +++ b/src/main/daemon/session-termination-controller.ts @@ -38,6 +38,15 @@ export class SessionTerminationController { this.physicalExit.markExited() } + forceKillForDisposal(): void { + try { + this.deps.subprocess.forceKill() + } catch { + /* child may already be gone */ + } + this.clearTerminating() + } + clearTerminating(): void { this._isTerminating = false } diff --git a/src/main/daemon/session.ts b/src/main/daemon/session.ts index e69bec7c5b1c..33f11fcb344f 100644 --- a/src/main/daemon/session.ts +++ b/src/main/daemon/session.ts @@ -1,3 +1,4 @@ +import { installSessionOutputObservation } from './session-output-observation' import { isValidPtySize } from './daemon-pty-size' import type { SessionOutputPlane, AttachedClient } from './session-output-plane' import { createSessionOutputPipeline } from './session-output-pipeline' @@ -49,12 +50,7 @@ export class Session { this.processNameIsSpawnFile = opts.subprocess.processNameIsSpawnFile === true this.onSessionExit = opts.onExit const pipeline = createSessionOutputPipeline({ - cols: opts.cols, - rows: opts.rows, - scrollback: opts.scrollback, - wslDistro: opts.wslDistro, - historySeedChunks: opts.historySeedChunks, - subprocess: this.subprocess, + ...opts, isAlive: () => !this._disposed && this._state !== 'exited' }) this.output = pipeline.output @@ -86,13 +82,18 @@ export class Session { write: (data) => this.subprocess.write(data), onEmission: (emission) => this.recoveryBarrier.accept(emission) }) - this.shellReady.startPromptReadinessProbe() - this.subprocess.onData((data) => { - if (!this._disposed) { - this.shellReady.ingestSubprocessData(data) - } + installSessionOutputObservation({ + subprocess: this.subprocess, + shellReady: this.shellReady, + ingress: this.startupIngress, + recovery: this.recoveryBarrier, + output: this.output, + isDisposed: () => this._disposed, + onFailure: () => { + this._disposed = true + }, + onExit: (code, cause) => this.handleSubprocessExit(code, cause) }) - this.subprocess.onExit((code, cause) => this.handleSubprocessExit(code, cause)) } get state(): SessionState { @@ -130,6 +131,12 @@ export class Session { return this.termination.beginTermination() } + get shellProcessId(): number | undefined { + return this.subprocess.getShellProcessId + ? this.subprocess.getShellProcessId() + : this.subprocess.pid + } + get pid(): number { return this.subprocess.pid } @@ -144,15 +151,8 @@ export class Session { return } - // Daemon POSIX PTYs need the local provider's cooked-echo containment (#13137). - // DA1/CPR stay immediate unless an echo-risk reply is already held (#13892, #15559). - if (this.startupIngress.answerLiveQueryReply(data)) { - return - } - - // Why: keep queuing during the post-ready flush-gate window ('ready' but not yet flushed); a - // direct write would race fresh input ahead of the buffered startup command. - if (this.shellReady.tryEnqueue(data)) { + // Query replies bypass queued startup input so prompt negotiation cannot deadlock. + if (this.startupIngress.answerLiveQueryReply(data) || this.shellReady.tryEnqueue(data)) { return } @@ -203,8 +203,8 @@ export class Session { this.termination.signal(sig) } - attachClient(client: Omit): symbol { - return this.output.attachClient(client) + attachClient(client: Omit, replayStartup = false): symbol { + return this.output.attachClient(client, replayStartup) } detachClient(token: symbol): void { @@ -303,13 +303,8 @@ export class Session { const wasTerminating = this.termination.isTerminating && this._state !== 'exited' const clientsToNotify = wasTerminating ? this.output.snapshotClients() : [] if (wasTerminating) { - try { - this.subprocess.forceKill() - } catch { - /* child may already be gone */ - } + this.termination.forceKillForDisposal() this._exitCode = -1 - this.termination.clearTerminating() } this.#teardownSubprocess() @@ -382,13 +377,11 @@ export class Session { this.shellReady.clearReadyTimer() this.shellReady.clearFlushGate() - // Why: release the ptmx fd here or node-pty's _socket leaks the master fd until GC (docs/fix-pty-fd-leak.md). - // Not via #teardownSubprocess: it flips `_disposed`, short-circuiting the later Session.dispose() reaper. + // Keep the emulator available until the owner reaps the exited session. this.termination.disposeSubprocessHandle() this.output.broadcastExit(code, this.incarnationId, cause) - // Why: hand off to the owner's reaper (disposes emulator, drops session from host map); else dead sessions accumulate. this.onSessionExit?.(code) } diff --git a/src/main/daemon/shell-ready-bun-fixture.ts b/src/main/daemon/shell-ready-bun-fixture.ts new file mode 100644 index 000000000000..b9a2a08a4c80 --- /dev/null +++ b/src/main/daemon/shell-ready-bun-fixture.ts @@ -0,0 +1,196 @@ +import { existsSync } from 'node:fs' +import { spawnBunPty } from './pty-subprocess/bun-pty-process' +import { + createShellStartupOutputScanState, + drainShellStartupOutputScanState, + scanShellStartupOutput +} from '../shell-startup-output-scanner' +const SHELL_READY_MARKER_OUTPUT = '\x1b]777;orca-shell-ready\x07' +/** Minimal xterm.js-shaped answers to the capability queries fish emits at startup + * and again around every prompt. */ +const TERMINAL_QUERY_REPLIES: readonly (readonly [string, string])[] = [ + ['\x1b[0c', '\x1b[?6c'], // primary device attributes + ['\x1b[?u', '\x1b[?0u'], // kitty keyboard flags + ['\x1b[6n', '\x1b[1;1R'], // cursor position report + ['\x1b]11;?', '\x1b]11;rgb:0000/0000/0000\x1b\\'], // background colour + ['\x1bP+q', '\x1bP0+r\x1b\\'] // XTGETTCAP (unsupported) +] + +/** Derived, not hardcoded: a shorter carry than the longest query would silently + * stop matching sequences split across two PTY chunks. */ +const QUERY_CARRY_LEN = Math.max(...TERMINAL_QUERY_REPLIES.map(([query]) => query.length)) + +// Why: the shell-ready marker fires from zle-line-init only on a real TTY, so spawn through a terminal rather than a pipe. +export async function runInteractiveZshLogin(args: { + tempHome: string + wrapperZdotdir: string + expected: string[] +}): Promise { + // Why: -o noglobalrcs skips /etc/zsh/*, whose insecure fpath dirs make compinit block on a [y/n] prompt before the marker fires. + const proc = spawnBunPty({ + file: 'zsh', + args: ['-o', 'noglobalrcs', '-l'], + cols: 80, + rows: 24, + cwd: args.tempHome, + env: { + PATH: process.env.PATH ?? '/usr/bin:/bin', + HOME: args.tempHome, + TERM: 'xterm-256color', + ZDOTDIR: args.wrapperZdotdir, + ORCA_ORIG_ZDOTDIR: args.tempHome, + ORCA_ZSHENV_SOURCE_DIR: args.tempHome, + ORCA_SHELL_FEATURES: 'ready' + } + }) + let output = '' + let settle = (): void => {} + const done = new Promise((resolve) => { + settle = resolve + }) + const deadline = setTimeout(settle, 10_000) + proc.onData((chunk) => { + output += chunk + if (args.expected.every((value) => output.includes(value))) { + settle() + } + }) + await done + clearTimeout(deadline) + const exited = new Promise((resolve) => proc.onExit(() => resolve())) + proc.kill() + await exited + proc.destroy() + return output +} + +// Why: exercise an arbitrary interactive zsh rc (own ZDOTDIR, no wrapper) so a test can source the marker block directly. +export async function runInteractiveZshRc(args: { + zdotdir: string + expected: string[] +}): Promise { + // Why: -o noglobalrcs skips /etc/zsh/* so the CI runner's global compinit can't block on an insecure-directory [y/n] prompt. + const proc = spawnBunPty({ + file: 'zsh', + args: ['-o', 'noglobalrcs', '-i'], + cols: 80, + rows: 24, + cwd: args.zdotdir, + env: { + PATH: process.env.PATH ?? '/usr/bin:/bin', + HOME: args.zdotdir, + TERM: 'xterm-256color', + ZDOTDIR: args.zdotdir, + ORCA_SHELL_FEATURES: 'ready' + } + }) + let output = '' + let settle = (): void => {} + const done = new Promise((resolve) => { + settle = resolve + }) + const deadline = setTimeout(settle, 10_000) + proc.onData((chunk) => { + output += chunk + if (args.expected.every((value) => output.includes(value))) { + settle() + } + }) + await done + clearTimeout(deadline) + const exited = new Promise((resolve) => proc.onExit(() => resolve())) + proc.kill() + await exited + proc.destroy() + return output +} + +export async function runFishShellReadyFixture({ + binary, + tempHome, + config, + sentinel, + erased, + stillRegistered +}: { + binary: string + tempHome: string + config: { args: string[] | null; env: Record } + sentinel: string + erased: string + stillRegistered: string +}): Promise<{ output: string; scannedOutput: string }> { + const proc = spawnBunPty({ + file: binary, + args: config.args ?? [], + cols: 80, + rows: 24, + cwd: tempHome, + env: { + PATH: process.env.PATH ?? '/usr/bin:/bin', + HOME: tempHome, + TERM: 'xterm-256color', + ...config.env + } + }) + let output = '' + let scannedOutput = '' + const startupScanState = createShellStartupOutputScanState() + let commandWritten = false + let erasureProbeWritten = false + let queryCarry = '' + let settle = (): void => {} + const done = new Promise((resolve) => { + settle = resolve + }) + const deadline = setTimeout(settle, 10_000) + // Why: settling on the first sentinel observes only one post-marker prompt, + // so a marker that never erased itself still looks single. Drive a second + // command and settle on its result, which also probes the erase directly. + const sentinelPoll = setInterval(() => { + if (commandWritten && !erasureProbeWritten && existsSync(sentinel)) { + erasureProbeWritten = true + proc.write( + `functions -q __orca_shell_ready_marker; and touch ${stillRegistered}; or touch ${erased}\n` + ) + return + } + if (erasureProbeWritten && (existsSync(erased) || existsSync(stillRegistered))) { + settle() + } + }, 50) + proc.onData((chunk) => { + output += chunk + scannedOutput += scanShellStartupOutput(startupScanState, chunk).output + // Why: fish stalls its first prompt 10s waiting on these and re-queries + // each prompt, so answer every occurrence — an unanswered query makes + // fish swallow the post-marker command as its reply. + const carriedLength = queryCarry.length + const scan = queryCarry + chunk + queryCarry = scan.slice(-QUERY_CARRY_LEN) + for (const [query, reply] of TERMINAL_QUERY_REPLIES) { + for (let at = scan.indexOf(query); at !== -1; at = scan.indexOf(query, at + query.length)) { + // Why: a query wholly inside the carry was answered on the previous + // chunk; replying again would land in fish's stdin as typed input. + if (at + query.length > carriedLength) { + proc.write(reply) + } + } + } + if (!commandWritten && output.includes(SHELL_READY_MARKER_OUTPUT)) { + commandWritten = true + // Why: mirror PostReadyFlushGate — flush shortly after the post-marker prompt draw. + setTimeout(() => proc.write(`touch ${sentinel}\n`), 50) + } + }) + await done + clearTimeout(deadline) + clearInterval(sentinelPoll) + const exited = new Promise((resolve) => proc.onExit(() => resolve())) + proc.kill() + await exited + proc.destroy() + scannedOutput += drainShellStartupOutputScanState(startupScanState) + + return { output, scannedOutput } +} diff --git a/src/main/daemon/shell-ready.test.ts b/src/main/daemon/shell-ready.test.ts index 01cdad10be92..e4ffa307ea2c 100644 --- a/src/main/daemon/shell-ready.test.ts +++ b/src/main/daemon/shell-ready.test.ts @@ -1,3 +1,5 @@ +import { z } from 'zod' +import { runBundledBunFixture } from '../bundled-bun-test-execution' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { spawnSync } from 'node:child_process' import { tmpdir } from 'node:os' @@ -10,11 +12,6 @@ import { import { makeUserZdotdir } from '../zsh-user-config-dir-fixture' import { getZshShellReadyMarkerRegistrationBlock } from '../shell-templates' import { fishRequirementViolation, resolveFishBinary } from '../../shared/fish-binary-requirement' -import { - createShellStartupOutputScanState, - drainShellStartupOutputScanState, - scanShellStartupOutput -} from '../shell-startup-output-scanner' import { HeadlessEmulator } from './headless-emulator' // Why resolved rather than hardcoded: the wrapper tree is content-addressed. import { getShellReadyWrapperRoot } from './shell-ready' @@ -39,97 +36,33 @@ const itWithFish = FISH.available ? it : it.skip const SHELL_READY_MARKER_OUTPUT = '\x1b]777;orca-shell-ready\x07' -/** Minimal xterm.js-shaped answers to the capability queries fish emits at startup - * and again around every prompt. */ -const TERMINAL_QUERY_REPLIES: readonly (readonly [string, string])[] = [ - ['\x1b[0c', '\x1b[?6c'], // primary device attributes - ['\x1b[?u', '\x1b[?0u'], // kitty keyboard flags - ['\x1b[6n', '\x1b[1;1R'], // cursor position report - ['\x1b]11;?', '\x1b]11;rgb:0000/0000/0000\x1b\\'], // background colour - ['\x1bP+q', '\x1bP0+r\x1b\\'] // XTGETTCAP (unsupported) -] - -/** Derived, not hardcoded: a shorter carry than the longest query would silently - * stop matching sequences split across two PTY chunks. */ -const QUERY_CARRY_LEN = Math.max(...TERMINAL_QUERY_REPLIES.map(([query]) => query.length)) - -// Why: the shell-ready marker fires from zle-line-init only on a real TTY, so spawn through node-pty not spawnSync. async function runInteractiveZshLogin(args: { tempHome: string wrapperZdotdir: string - isDone: (output: string) => boolean + expected: string[] }): Promise { - const pty = await import('node-pty') - // Why: -o noglobalrcs skips /etc/zsh/*, whose insecure fpath dirs make compinit block on a [y/n] prompt before the marker fires. - const proc = pty.spawn('zsh', ['-o', 'noglobalrcs', '-l'], { - name: 'xterm-256color', - cols: 80, - rows: 24, - cwd: args.tempHome, - env: { - PATH: process.env.PATH ?? '/usr/bin:/bin', - HOME: args.tempHome, - TERM: 'xterm-256color', - ZDOTDIR: args.wrapperZdotdir, - ORCA_ORIG_ZDOTDIR: args.tempHome, - ORCA_ZSHENV_SOURCE_DIR: args.tempHome, - ORCA_SHELL_FEATURES: 'ready' - } - }) - let output = '' - let settle = (): void => {} - const done = new Promise((resolve) => { - settle = resolve - }) - const deadline = setTimeout(settle, 10_000) - proc.onData((chunk) => { - output += chunk - if (args.isDone(output)) { - settle() - } - }) - await done - clearTimeout(deadline) - proc.kill() - return output + return z + .string() + .parse( + await runBundledBunFixture( + join(__dirname, 'shell-ready-bun-fixture.ts'), + 'runInteractiveZshLogin', + args, + 12_000 + ) + ) } - -// Why: exercise an arbitrary interactive zsh rc (own ZDOTDIR, no wrapper) so a test can source the marker block directly. -async function runInteractiveZshRc(args: { - zdotdir: string - isDone: (output: string) => boolean -}): Promise { - const pty = await import('node-pty') - // Why: -o noglobalrcs skips /etc/zsh/* so the CI runner's global compinit can't block on an insecure-directory [y/n] prompt. - const proc = pty.spawn('zsh', ['-o', 'noglobalrcs', '-i'], { - name: 'xterm-256color', - cols: 80, - rows: 24, - cwd: args.zdotdir, - env: { - PATH: process.env.PATH ?? '/usr/bin:/bin', - HOME: args.zdotdir, - TERM: 'xterm-256color', - ZDOTDIR: args.zdotdir, - ORCA_SHELL_FEATURES: 'ready' - } - }) - let output = '' - let settle = (): void => {} - const done = new Promise((resolve) => { - settle = resolve - }) - const deadline = setTimeout(settle, 10_000) - proc.onData((chunk) => { - output += chunk - if (args.isDone(output)) { - settle() - } - }) - await done - clearTimeout(deadline) - proc.kill() - return output +async function runInteractiveZshRc(args: { zdotdir: string; expected: string[] }): Promise { + return z + .string() + .parse( + await runBundledBunFixture( + join(__dirname, 'shell-ready-bun-fixture.ts'), + 'runInteractiveZshRc', + args, + 12_000 + ) + ) } describePosix('daemon shell-ready launch config', () => { @@ -248,78 +181,15 @@ describePosix('daemon shell-ready launch config', () => { join(tempHome, '.config', 'fish', 'config.fish'), 'command sleep 0.2\nfunction fish_prompt\n printf "> "\nend\n' ) - const pty = await import('node-pty') - const proc = pty.spawn('fish', config.args ?? [], { - name: 'xterm-256color', - cols: 80, - rows: 24, - cwd: tempHome, - env: { - PATH: process.env.PATH ?? '/usr/bin:/bin', - HOME: tempHome, - TERM: 'xterm-256color', - ...config.env - } - }) - let output = '' - let scannedOutput = '' - const startupScanState = createShellStartupOutputScanState() - let commandWritten = false - let erasureProbeWritten = false - let queryCarry = '' - let settle = (): void => {} - const done = new Promise((resolve) => { - settle = resolve - }) - const deadline = setTimeout(settle, 10_000) - // Why: settling on the first sentinel observes only one post-marker prompt, - // so a marker that never erased itself still looks single. Drive a second - // command and settle on its result, which also probes the erase directly. - const sentinelPoll = setInterval(() => { - if (commandWritten && !erasureProbeWritten && existsSync(sentinel)) { - erasureProbeWritten = true - proc.write( - `functions -q __orca_shell_ready_marker; and touch ${stillRegistered}; or touch ${erased}\n` - ) - return - } - if (erasureProbeWritten && (existsSync(erased) || existsSync(stillRegistered))) { - settle() - } - }, 50) - proc.onData((chunk) => { - output += chunk - scannedOutput += scanShellStartupOutput(startupScanState, chunk).output - // Why: fish stalls its first prompt 10s waiting on these and re-queries - // each prompt, so answer every occurrence — an unanswered query makes - // fish swallow the post-marker command as its reply. - const carriedLength = queryCarry.length - const scan = queryCarry + chunk - queryCarry = scan.slice(-QUERY_CARRY_LEN) - for (const [query, reply] of TERMINAL_QUERY_REPLIES) { - for ( - let at = scan.indexOf(query); - at !== -1; - at = scan.indexOf(query, at + query.length) - ) { - // Why: a query wholly inside the carry was answered on the previous - // chunk; replying again would land in fish's stdin as typed input. - if (at + query.length > carriedLength) { - proc.write(reply) - } - } - } - if (!commandWritten && output.includes(SHELL_READY_MARKER_OUTPUT)) { - commandWritten = true - // Why: mirror PostReadyFlushGate — flush shortly after the post-marker prompt draw. - setTimeout(() => proc.write(`touch ${sentinel}\n`), 50) - } - }) - await done - clearTimeout(deadline) - clearInterval(sentinelPoll) - proc.kill() - scannedOutput += drainShellStartupOutputScanState(startupScanState) + const result = await runBundledBunFixture( + join(__dirname, 'shell-ready-bun-fixture.ts'), + 'runFishShellReadyFixture', + { binary: FISH.path, tempHome, config, sentinel, erased, stillRegistered }, + 12_000 + ) + const { output, scannedOutput } = z + .object({ output: z.string(), scannedOutput: z.string() }) + .parse(result) expect(output).toContain(SHELL_READY_MARKER_OUTPUT) expect(output.split(SHELL_READY_MARKER_OUTPUT)).toHaveLength(2) @@ -482,7 +352,7 @@ describePosix('daemon shell-ready launch config', () => { const output = await runInteractiveZshLogin({ tempHome, wrapperZdotdir: config.env.ZDOTDIR, - isDone: (current) => current.includes(SHELL_READY_MARKER_OUTPUT) + expected: [SHELL_READY_MARKER_OUTPUT] }) expect(output).toContain(SHELL_READY_MARKER_OUTPUT) } finally { @@ -513,8 +383,7 @@ describePosix('daemon shell-ready launch config', () => { const output = await runInteractiveZshLogin({ tempHome, wrapperZdotdir: config.env.ZDOTDIR, - isDone: (current) => - current.includes(SHELL_READY_MARKER_OUTPUT) && current.includes(userHookOutput) + expected: [SHELL_READY_MARKER_OUTPUT, userHookOutput] }) // Why: the marker widget chains to the prior widget, so a user-registered azhw dispatcher must keep dispatching. expect(output).toContain(SHELL_READY_MARKER_OUTPUT) @@ -551,8 +420,7 @@ describePosix('daemon shell-ready launch config', () => { try { const output = await runInteractiveZshRc({ zdotdir, - isDone: (current) => - current.includes(SHELL_READY_MARKER_OUTPUT) && current.includes(userHookOutput) + expected: [SHELL_READY_MARKER_OUTPUT, userHookOutput] }) expect(output).toContain(SHELL_READY_MARKER_OUTPUT) expect(output).toContain(userHookOutput) diff --git a/src/main/daemon/terminal-host-bun-posix-inspection.test.ts b/src/main/daemon/terminal-host-bun-posix-inspection.test.ts new file mode 100644 index 000000000000..4f81b20aa5a2 --- /dev/null +++ b/src/main/daemon/terminal-host-bun-posix-inspection.test.ts @@ -0,0 +1,178 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type * as ProcessTableReader from '../../shared/process-table-snapshot-reader' +import type { ProcessTableRow } from '../../shared/process-table-snapshot' +import type { TerminalProcess } from '../../shared/terminal-process' +import { TerminalHost } from './terminal-host' +import { createDaemonPtySubprocessHandle } from './pty-subprocess/subprocess-handle' + +const scans = vi.hoisted(() => ({ full: vi.fn(), fresh: vi.fn(), strict: vi.fn() })) +vi.mock('../../shared/process-table-snapshot-reader', async (importOriginal) => ({ + ...(await importOriginal()), + getProcessTableSnapshot: scans.full, + getFreshProcessTableSnapshot: scans.fresh, + getStrictProcessTableSnapshotWithAge: scans.strict +})) +const platform = Object.getOwnPropertyDescriptor(process, 'platform')! +const id = 'bun-pane' +let rows: ProcessTableRow[] +let host: TerminalHost | undefined +let physicalExit: () => void + +function row(pid: number, ppid: number, command: string, foregroundPid: number): ProcessTableRow { + return { + pid, + ppid, + command, + pgid: pid, + tpgid: foregroundPid, + stat: pid === foregroundPid ? 'Ss+' : 'Ss', + tty: 'ttys002' + } +} + +async function createPane(name = '/bin/zsh', pid = 100) { + host ??= new TerminalHost({ + spawnSubprocess: () => { + const proc: TerminalProcess & { processNameIsSpawnFile: true } = { + pid, + process: name, + processNameIsSpawnFile: true, + cols: 80, + rows: 24, + onData: () => ({ dispose() {} }), + onExit: (listener) => { + physicalExit = () => listener({ exitCode: 0 }) + return { dispose() {} } + }, + resize() {}, + clear() {}, + write() {}, + kill() {}, + pause() {}, + resume() {} + } + return createDaemonPtySubprocessHandle({ + process: proc, + shellPath: '/bin/zsh', + spawnCwd: '/tmp', + env: {}, + startupCommandDeliveredInShellArgs: false, + reportsChildExitStatus: true, + sessionId: id, + startupAgentRecognition: null + }) + } + }) + return host.createOrAttach({ + sessionId: id, + cols: 80, + rows: 24, + streamClient: { onData() {}, onExit() {} } + }) +} + +beforeEach(() => { + vi.resetAllMocks() + scans.full.mockImplementation(async () => rows) + scans.fresh.mockImplementation(async () => rows) + scans.strict.mockImplementation(async () => ({ rows, capturedAgeMs: 0 })) +}) +afterEach(async () => { + physicalExit?.() + await host?.dispose() + // Each test owns a distinct host incarnation. + host = undefined + Object.defineProperty(process, 'platform', platform) + vi.restoreAllMocks() +}) + +describe.each(['darwin', 'linux'])('Bun daemon %s inspection', (hostPlatform) => { + beforeEach(() => + Object.defineProperty(process, 'platform', { value: hostPlatform, configurable: true }) + ) + + it('reports the actual foreground command and warns before closing a busy pane', async () => { + rows = [row(100, 1, '-zsh', 101), row(101, 100, 'vim notes.md', 101)] + await createPane() + expect(await host!.inspectProcess(id)).toMatchObject({ + foregroundProcess: 'vim', + hasChildProcesses: true, + childProcessEvidence: 'children' + }) + expect(await host!.confirmForegroundProcess(id)).toBe('vim') + expect(scans.fresh).toHaveBeenCalledOnce() + }) + + it('proves an idle shell has no children', async () => { + rows = [row(100, 1, '-zsh', 100)] + await createPane() + expect(await host!.inspectProcess(id)).toMatchObject({ + hasChildProcesses: false, + childProcessEvidence: 'no-children' + }) + }) + + it('does not mistake the login wrapper for a running user job', async () => { + rows = [row(100, 1, '/usr/bin/login -fp test', 101), row(101, 100, '-zsh', 101)] + await createPane('/usr/bin/login') + expect(await host!.inspectProcess(id)).toMatchObject({ + hasChildProcesses: false, + childProcessEvidence: 'no-children' + }) + }) + + it('preserves a confirmed agent when a later foreground scan cannot verify it', async () => { + rows = [row(100, 1, '-zsh', 101), row(101, 100, 'node /usr/local/bin/claude', 101)] + await createPane() + expect(await host!.confirmForegroundProcess(id)).toBe('claude') + scans.full.mockRejectedValue(new Error('process table unavailable')) + scans.strict.mockRejectedValue(new Error('process table unavailable')) + scans.fresh.mockRejectedValue(new Error('process table unavailable')) + expect(await host!.inspectProcess(id)).toMatchObject({ + foregroundProcess: 'claude', + hasChildProcesses: true, + childProcessEvidence: 'unverifiable', + foregroundProcessEvidence: { verdict: 'unverifiable' } + }) + expect(await host!.confirmForegroundProcess(id)).toBeNull() + }) + + it('returns uncertainty when the process table has no pane root', async () => { + rows = [row(900, 1, '-zsh', 900)] + await createPane() + expect(await host!.inspectProcess(id)).toMatchObject({ + hasChildProcesses: true, + childProcessEvidence: 'unverifiable', + foregroundProcessEvidence: { verdict: 'unverifiable' } + }) + }) + + it('does not publish live evidence after physical exit during a child scan', async () => { + rows = [row(100, 1, '-zsh', 101), row(101, 100, 'vim notes.md', 101)] + await createPane() + scans.strict.mockImplementationOnce(async () => { + physicalExit() + return { rows, capturedAgeMs: 0 } + }) + expect(await host!.inspectProcess(id)).toMatchObject({ + foregroundProcess: null, + hasChildProcesses: true, + childProcessEvidence: 'unverifiable', + foregroundProcessEvidence: { verdict: 'unverifiable' } + }) + }) + it('does not cache an agent in a replacement incarnation after a delayed confirmation', async () => { + rows = [row(100, 1, '-zsh', 101), row(101, 100, 'node /usr/local/bin/claude', 101)] + const original = await createPane() + scans.fresh.mockImplementationOnce(async () => { + const observed = rows + physicalExit() + rows = [row(100, 1, '-zsh', 100)] + const replacement = await createPane() + expect(replacement.incarnationId).not.toBe(original.incarnationId) + return observed + }) + expect(await host!.confirmForegroundProcess(id)).toBeNull() + expect(await host!.confirmForegroundProcess(id)).toBe('zsh') + }) +}) diff --git a/src/main/daemon/terminal-host-concurrent-create.test.ts b/src/main/daemon/terminal-host-concurrent-create.test.ts index aa7b9fda956e..421edd1f8c82 100644 --- a/src/main/daemon/terminal-host-concurrent-create.test.ts +++ b/src/main/daemon/terminal-host-concurrent-create.test.ts @@ -165,7 +165,7 @@ describe('concurrent createOrAttach across the async spawn', () => { await host.dispose() }) - it('waits for an in-flight spawn before disposing its session', async () => { + it('cleans up an in-flight spawn without publishing after shutdown starts', async () => { let releaseSpawn: () => void = () => {} const spawnGate = new Promise((resolve) => { releaseSpawn = resolve @@ -179,6 +179,7 @@ describe('concurrent createOrAttach across the async spawn', () => { }) const creation = host.createOrAttach(createOptions('shutdown-race')) + const rejected = expect(creation).rejects.toThrow('Terminal host is shutting down') const disposal = host.dispose() let disposed = false void disposal.then(() => { @@ -189,7 +190,7 @@ describe('concurrent createOrAttach across the async spawn', () => { expect(disposed).toBe(false) releaseSpawn() - await creation + await rejected await disposal expect(subprocess.forceKill).toHaveBeenCalledOnce() diff --git a/src/main/daemon/terminal-host-disposal.ts b/src/main/daemon/terminal-host-disposal.ts index baa432c0f165..1a411e3f939c 100644 --- a/src/main/daemon/terminal-host-disposal.ts +++ b/src/main/daemon/terminal-host-disposal.ts @@ -1,3 +1,4 @@ +import type { TerminalStartupSession } from './terminal-startup-session' import { shutdownTerminalHostSessions } from './terminal-host-session-shutdown' import type { TerminalSessionTeardown } from './terminal-session-teardown' import type { Session } from './session' @@ -13,6 +14,7 @@ type TerminalHostDisposalOptions = { pendingCreations: ReadonlyMap> sessionTeardown: TerminalSessionTeardown sessions: Map + startupSessions?: Map onFinalCheckpoint?: FinalCheckpoint killedTombstones: { clear: () => void } } @@ -21,6 +23,7 @@ export async function disposeTerminalHostSessions({ pendingCreations, sessionTeardown, sessions, + startupSessions, onFinalCheckpoint, killedTombstones }: TerminalHostDisposalOptions): Promise { @@ -30,6 +33,7 @@ export async function disposeTerminalHostSessions({ } const existingTeardowns = sessionTeardown.requestImmediateAll() await Promise.all([ + ...[...(startupSessions?.values() ?? [])].map((attempt) => attempt.discard()), shutdownTerminalHostSessions( sessions, onFinalCheckpoint, diff --git a/src/main/daemon/terminal-host-options.ts b/src/main/daemon/terminal-host-options.ts index 918628b322b5..047519e8fbb7 100644 --- a/src/main/daemon/terminal-host-options.ts +++ b/src/main/daemon/terminal-host-options.ts @@ -1,3 +1,4 @@ +import type { ObserveTerminalSpawnAttempt } from './terminal-spawn-attempt' import type { StartupCommandDelivery } from '../../shared/codex-startup-delivery' import type { TuiAgent } from '../../shared/tui-agent' import type { SubprocessHandle } from './session-subprocess-handle' @@ -20,6 +21,7 @@ export type TerminalHostOptions = { terminalWindowsPowerShellImplementation?: 'auto' | 'powershell.exe' | 'pwsh.exe' isCanceled?: () => boolean cancelSignal?: AbortSignal + onSpawnAttempt?: ObserveTerminalSpawnAttempt // Async production spawns and sync test stubs share this boundary. }) => SubprocessHandle | Promise // Why: login-session death detection (#7936) needs subprocess exits even when no client is attached. diff --git a/src/main/daemon/terminal-host-process-inspection.ts b/src/main/daemon/terminal-host-process-inspection.ts index e149179c1aa8..c937cbc1da9e 100644 --- a/src/main/daemon/terminal-host-process-inspection.ts +++ b/src/main/daemon/terminal-host-process-inspection.ts @@ -71,7 +71,7 @@ export async function inspectTerminalHostProcess(args: { !expectedIncarnationId || expectedIncarnationId === session.incarnationId if (args.steadyState === true && incarnationMatches) { const anchored = await readAnchoredForeground(session) - if (anchored !== null) { + if (anchored !== null && session.isAlive) { args.onTier?.('cheap') // No evidence member on purpose: a tty-less capture cannot fence anything, and a // fabricated fence would be read by remote/restore consumers as an observation. @@ -124,6 +124,19 @@ export async function inspectTerminalHostProcess(args: { clearSteadyStateAnchor(session) } } + if (!session.isAlive) { + clearSteadyStateAnchor(session) + return { + foregroundProcess: null, + hasChildProcesses: true, + childProcessEvidence: 'unverifiable', + foregroundProcessEvidence: unverifiableEvidence( + args, + session, + 'session_exited_during_inspection' + ) + } + } const nonShellForeground = foregroundProcess !== null && !isShellProcess(foregroundProcess) // Evidence names recognized agents only, so its null must not erase an ordinary command (#18078). const ordinaryForeground = diff --git a/src/main/daemon/terminal-host-session-create.ts b/src/main/daemon/terminal-host-session-create.ts index 15e86a70a72c..bdbe7d3915a2 100644 --- a/src/main/daemon/terminal-host-session-create.ts +++ b/src/main/daemon/terminal-host-session-create.ts @@ -1,9 +1,12 @@ +import { TerminalAttachCanceledError } from './daemon-errors' import { buildStartupCommandSubmission } from '../../shared/startup-command-submission' import { resolvePtyOwnerBackend } from '../../shared/pty-owner-backend' import { getDaemonSessionResultMetadata } from './daemon-create-or-attach-result' import { enumerateDirectoryOnce } from './directory-enumeration-probe' import { normalizePtySize } from './daemon-pty-size' import { Session } from './session' +import { TerminalStartupSession } from './terminal-startup-session' +import type { SubprocessHandle } from './session-subprocess-handle' import { shellPathSupportsPtyStartupBarrier } from './shell-ready' import type { InternalCreateOrAttachOptions } from './terminal-host-agent-session-claim' import type { CreateOrAttachResult } from './terminal-host-create-contract' @@ -11,13 +14,13 @@ import type { TerminalHostOptions } from './terminal-host-options' import type { TerminalHostTombstones } from './terminal-host-tombstones' import type { TerminalSessionTeardown } from './terminal-session-teardown' import { resolveDaemonSessionScrollbackRows } from './daemon-session-scrollback-window' -import { TerminalAttachCanceledError } from './daemon-errors' import { waitForTerminalAttachOperation } from './terminal-attach-cancellation' import { SessionNotFoundError } from './types' import { resolveWslSessionContext } from './wsl-session-context' type TerminalHostSessionCreateDependencies = { sessions: Map + startupSessions: Map /** Re-checks the host's shutdown fence and this request's cancellation after any await. */ assertCreateAllowed: () => void sessionTeardown: TerminalSessionTeardown @@ -113,8 +116,25 @@ async function spawnAndPublishSession( // Why before the fork: the shell's own cwd may already have fallen back, so probe the requested path. const cwdReadableByDaemon = opts.cwd && !wslDistro ? await isCwdReadableByThisProcess(opts.cwd) : null + const startup = new TerminalStartupSession( + opts.sessionId, + deps.startupSessions, + (subprocess, onExit, requireSynchronousOutput) => + createSession(opts, deps, ctx, subprocess, onExit, requireSynchronousOutput), + createSessionExitHandler(deps.onSessionExit, opts.sessionId, opts.agentSessionGeneration) + ) + await startup.clearPreviousAttempt() const subprocess = await deps.spawnSubprocess({ sessionId: opts.sessionId, + onSpawnAttempt: (createHandle, discardNative) => { + const discard = () => startup.discard() + try { + startup.prepare(createHandle, true, discardNative) + return { discard } + } catch (error) { + return { discard, failure: { error } } + } + }, cols: size.cols, rows: size.rows, cwd: opts.cwd, @@ -131,54 +151,22 @@ async function spawnAndPublishSession( ...(opts.cancelSignal ? { cancelSignal: opts.cancelSignal } : {}) }) - // Why: a fallback shell does not emit the preferred shell's ready marker; - // retaining the stale capability would indefinitely queue its first command. - const shellReadySupported = - (opts.shellReadySupported ?? false) && - (subprocess.shellPath === undefined || shellPathSupportsPtyStartupBarrier(subprocess.shellPath)) - const session = new Session({ - sessionId: opts.sessionId, - cols: size.cols, - rows: size.rows, - terminalHandle: opts.env?.ORCA_TERMINAL_HANDLE, - launchAgent: opts.launchAgent, - subprocess, - ownerBackend: resolvePtyOwnerBackend({ - platform: process.platform, - shellPath: subprocess.shellPath, - wslDistro - }), - shellReadySupported, - scrollback: resolveDaemonSessionScrollbackRows(), - historySeedChunks: opts.historySeedChunks, - ...(opts.startupIngress ? { startupIngress: opts.startupIngress } : {}), - wslDistro, - onExit: createSessionExitHandler( - deps.onSessionExit, - opts.sessionId, - opts.agentSessionGeneration - ), - ...(deps.reportReadinessEvent ? { reportReadinessEvent: deps.reportReadinessEvent } : {}), - ...(opts.shellReadyTimeoutMs !== undefined - ? { shellReadyTimeoutMs: opts.shellReadyTimeoutMs } - : {}) - }) - - if (opts.isCanceled?.()) { - // Retain cleanup ownership if the native child refuses to exit. - deps.sessions.set(opts.sessionId, session) - await session.forceKillAndDisposeSubprocess() - if (deps.sessions.get(opts.sessionId) === session) { - session.dispose() - deps.sessions.delete(opts.sessionId) - deps.onDeadSessionRemoved(opts.sessionId) + const session = startup.current ?? startup.prepare(() => subprocess, false) + const shellReadySupported = supportsStartupBarrier(opts, subprocess) + try { + deps.assertCreateAllowed() + opts.cancelSignal?.throwIfAborted() + if (opts.isCanceled?.()) { + throw new TerminalAttachCanceledError(opts.sessionId) } - throw new TerminalAttachCanceledError(opts.sessionId) + } catch (error) { + await startup.discard() + throw error } deps.sessions.set(opts.sessionId, session) deps.onSessionCreated(opts.sessionId, opts.agentSessionGeneration, session.isAlive) - const token = session.attachClient(opts.streamClient) + const token = session.attachClient(opts.streamClient, startup.observedBeforeConfirmation) const startupCommandWritten = Boolean(opts.command) && !subprocess.startupCommandDeliveredInShellArgs @@ -207,7 +195,7 @@ async function spawnAndPublishSession( ) } - return { + const result: CreateOrAttachResult = { isNew: true, snapshot: null, pid: subprocess.pid, @@ -217,14 +205,45 @@ async function spawnAndPublishSession( ...(cwdReadableByDaemon !== null ? { cwdReadableByDaemon } : {}), attachToken: token } + startup.publish() + return result } -function createSessionExitHandler( - onSessionExit: TerminalHostSessionCreateDependencies['onSessionExit'], - sessionId: string, - generation: string | undefined -): () => void { - return () => onSessionExit(sessionId, generation) +function createSession( + opts: InternalCreateOrAttachOptions, + deps: TerminalHostSessionCreateDependencies, + { size, wslDistro }: { size: { cols: number; rows: number }; wslDistro: string | undefined }, + subprocess: SubprocessHandle, + onExit: () => void, + requireSynchronousOutput: boolean +): Session { + // Why: a fallback shell does not emit the preferred shell's ready marker; + // retaining the stale capability would indefinitely queue its first command. + const shellReadySupported = supportsStartupBarrier(opts, subprocess) + return new Session({ + sessionId: opts.sessionId, + cols: size.cols, + rows: size.rows, + terminalHandle: opts.env?.ORCA_TERMINAL_HANDLE, + launchAgent: opts.launchAgent, + subprocess, + ownerBackend: resolvePtyOwnerBackend({ + platform: process.platform, + shellPath: subprocess.shellPath, + wslDistro + }), + shellReadySupported, + scrollback: resolveDaemonSessionScrollbackRows(), + historySeedChunks: opts.historySeedChunks, + ...(opts.startupIngress ? { startupIngress: opts.startupIngress } : {}), + wslDistro, + onExit, + requireSynchronousOutput, + ...(deps.reportReadinessEvent ? { reportReadinessEvent: deps.reportReadinessEvent } : {}), + ...(opts.shellReadyTimeoutMs !== undefined + ? { shellReadyTimeoutMs: opts.shellReadyTimeoutMs } + : {}) + }) } // Why enumeration: a shell's cwd listing is what TCC withholds, and it can withhold it while @@ -233,3 +252,22 @@ function createSessionExitHandler( async function isCwdReadableByThisProcess(cwd: string): Promise { return (await enumerateDirectoryOnce(cwd)) !== 'denied' } + +function supportsStartupBarrier( + opts: InternalCreateOrAttachOptions, + subprocess: SubprocessHandle +): boolean { + return ( + (opts.shellReadySupported ?? false) && + (subprocess.shellPath === undefined || shellPathSupportsPtyStartupBarrier(subprocess.shellPath)) + ) +} + +// Keep request buffers out of the live session's exit closure. +function createSessionExitHandler( + onSessionExit: TerminalHostSessionCreateDependencies['onSessionExit'], + sessionId: string, + generation: string | undefined +): () => void { + return () => onSessionExit(sessionId, generation) +} diff --git a/src/main/daemon/terminal-host-session-cwd.ts b/src/main/daemon/terminal-host-session-cwd.ts index 6de0df3a8154..4b5b54c1e0cb 100644 --- a/src/main/daemon/terminal-host-session-cwd.ts +++ b/src/main/daemon/terminal-host-session-cwd.ts @@ -6,6 +6,10 @@ export async function resolveTerminalHostSessionCwd(session: Session): Promise { } }) + it('releases observer-path spawn inputs and native cleanup captures after the shell receipt', async () => { + const receipt = Promise.withResolvers() + const observed = Promise.withResolvers() + let cleanupCapture: WeakRef | undefined + const host = new TerminalHost({ + spawnSubprocess: async (options) => { + const handle = subprocess() + const nativeCleanupState = { handle, env: options.env } + cleanupCapture = new WeakRef(nativeCleanupState) + const attempt = options.onSpawnAttempt?.( + () => handle, + async () => nativeCleanupState.handle.forceKill() + ) + if (!attempt) { + throw new Error('Production spawn observer was not installed') + } + if (attempt.failure) { + throw attempt.failure.error + } + handle.emitData('BEFORE-RECEIPT\r\n') + observed.resolve() + await receipt.promise + return handle + } + }) + const created = startWithInputs(host, 'observed-pending') + try { + await observed.promise + await collect() + expect(created.refs.map((ref) => ref.deref() !== undefined)).toEqual(Array(4).fill(true)) + expect(cleanupCapture?.deref()).toBeDefined() + expect(host.listSessions()).toHaveLength(0) + receipt.resolve() + expect((await created.creation).historySeeded).toBe(true) + await collect() + expect(created.refs.map((ref) => ref.deref() === undefined)).toEqual(Array(4).fill(true)) + expect(cleanupCapture?.deref()).toBeUndefined() + expect(host.listSessions()).toHaveLength(1) + expect(host.getSnapshot('observed-pending')?.snapshotAnsi).toContain('BEFORE-RECEIPT') + } finally { + receipt.resolve() + await created.creation + await host.dispose() + } + }) + it('reaps exited sessions, preserves exit evidence and releases claimed generations', async () => { const handles: ReturnType[] = [] const reaped: string[] = [] diff --git a/src/main/daemon/terminal-host.ts b/src/main/daemon/terminal-host.ts index 6e8d5ceee816..93d95082656f 100644 --- a/src/main/daemon/terminal-host.ts +++ b/src/main/daemon/terminal-host.ts @@ -1,3 +1,4 @@ +import type { TerminalStartupSession } from './terminal-startup-session' import type { Session } from './session' import { SessionNotFoundError, @@ -47,6 +48,7 @@ const REMOTE_FOREGROUND_TOMBSTONE_RETENTION_MS = 2_000 export class TerminalHost { private sessions = new Map() + private startupSessions = new Map() // Serializes creates for one id across async spawn validation. private pendingCreations = new Map>() private sessionTeardown = new TerminalSessionTeardown(this.sessions) @@ -112,6 +114,7 @@ export class TerminalHost { } return await createOrAttachTerminalSession(options, { sessions: this.sessions, + startupSessions: this.startupSessions, assertCreateAllowed: () => this.assertCreateOrAttachAllowed(options), sessionTeardown: this.sessionTeardown, killedTombstones: this.killedTombstones, @@ -321,7 +324,14 @@ export class TerminalHost { if (this.disposePromise) { return this.disposePromise } - const disposePromise = this.disposeSessions() + const disposePromise = disposeTerminalHostSessions({ + pendingCreations: this.pendingCreations, + sessionTeardown: this.sessionTeardown, + sessions: this.sessions, + startupSessions: this.startupSessions, + onFinalCheckpoint: this.onFinalCheckpoint, + killedTombstones: this.killedTombstones + }) this.disposePromise = disposePromise void disposePromise.catch(() => { // Why: keep failed native owners retryable on a later shutdown request. @@ -331,14 +341,4 @@ export class TerminalHost { }) return disposePromise } - - private disposeSessions(): Promise { - return disposeTerminalHostSessions({ - pendingCreations: this.pendingCreations, - sessionTeardown: this.sessionTeardown, - sessions: this.sessions, - onFinalCheckpoint: this.onFinalCheckpoint, - killedTombstones: this.killedTombstones - }) - } } diff --git a/src/main/daemon/terminal-spawn-attempt.ts b/src/main/daemon/terminal-spawn-attempt.ts new file mode 100644 index 000000000000..b58f2625b191 --- /dev/null +++ b/src/main/daemon/terminal-spawn-attempt.ts @@ -0,0 +1,10 @@ +import type { SubprocessHandle } from './session-subprocess-handle' +export type TerminalSpawnAttempt = { + discard(): Promise + failure?: { error: unknown } +} + +export type ObserveTerminalSpawnAttempt = ( + createHandle: () => SubprocessHandle, + discardNative: () => Promise +) => TerminalSpawnAttempt diff --git a/src/main/daemon/terminal-startup-fallback.test.ts b/src/main/daemon/terminal-startup-fallback.test.ts new file mode 100644 index 000000000000..a3a37c57b6a7 --- /dev/null +++ b/src/main/daemon/terminal-startup-fallback.test.ts @@ -0,0 +1,146 @@ +import './mock-descendant-sweep' +import { expect, it, vi } from 'vitest' +import { TerminalHost } from './terminal-host' +import { spawnNativeDaemonPty } from './pty-subprocess/native-pty-spawn' +import { createMockSubprocess, waitFor } from './daemon-pty-adapter-test-harness' +import type { BunPtyProcess } from './pty-subprocess/bun-pty-process-contract' + +it.each([false, true])( + 'isolates failed Windows shell output and retains failed cleanup (%s)', + async (cleanupFails) => { + const platform = Object.getOwnPropertyDescriptor(process, 'platform')! + Object.defineProperty(process, 'platform', { value: 'win32' }) + const attempts: ReturnType[] = [] + const onData = vi.fn() + const onExit = vi.fn() + const host = new TerminalHost({ + spawnSubprocess: async (opts) => { + let selected = createMockSubprocess() + await spawnNativeDaemonPty( + { + shellPath: 'pwsh.exe', + shellArgs: [], + spawnCwd: '.', + env: {}, + cols: 80, + rows: 24, + windowsFallbackAttempts: [ + { + shellPath: 'pwsh.exe', + shellArgs: [], + effectiveCwd: '.', + validationCwd: '.', + startupCommandDeliveredInShellArgs: false + }, + { + shellPath: 'cmd.exe', + shellArgs: [], + effectiveCwd: '.', + validationCwd: '.', + startupCommandDeliveredInShellArgs: false + } + ], + onSpawnAttempt: (_spawned, discardNative) => { + selected = attempts.at(-1)!.handle + return opts.onSpawnAttempt!(() => selected, discardNative) + } + }, + { + canUseBunPty: () => true, + spawnBunPty: () => { + const attempt = makeAttempt() + attempts.push(attempt) + return attempt.process + } + } + ) + return selected + } + }) + try { + const pending = host.createOrAttach({ + sessionId: 'fallback', + cols: 80, + rows: 24, + streamClient: { onData, onExit } + }) + await waitFor(() => attempts.length === 1) + attempts[0]!.handle._simulateData('PRIVATE_PRIMARY') + if (cleanupFails) { + attempts[0]!.forceKill.mockImplementation(() => { + throw new Error('still live') + }) + const rejected = expect(pending).rejects.toThrow('still owns a process') + attempts[0]!.reject(new Error('primary spawn rejected')) + await rejected + expect(attempts).toHaveLength(1) + expect(host.listSessions()).toEqual([]) + expect(onData).not.toHaveBeenCalled() + expect(onExit).not.toHaveBeenCalled() + await expect( + host.createOrAttach({ + sessionId: 'fallback', + cols: 80, + rows: 24, + streamClient: { onData, onExit } + }) + ).rejects.toThrow('still live') + expect(attempts).toHaveLength(1) + attempts[0]!.forceKill.mockImplementation(attempts[0]!.finish) + return + } + attempts[0]!.reject(new Error('primary spawn rejected')) + await waitFor(() => attempts.length === 2) + expect(onData).not.toHaveBeenCalled() + expect(onExit).not.toHaveBeenCalled() + attempts[1]!.handle._simulateData('PUBLIC_FALLBACK') + attempts[1]!.confirm() + await pending + expect(onData.mock.calls.map((call) => call[0]).join('')).toBe('PUBLIC_FALLBACK') + expect(attempts[0]!.handle.forceKill).toHaveBeenCalledOnce() + } finally { + await host.dispose() + Object.defineProperty(process, 'platform', platform) + } + } +) + +function makeAttempt() { + const handle = createMockSubprocess() + let confirm!: () => void + let reject!: (error: Error) => void + const receipt = new Promise((yes, no) => { + confirm = yes + reject = no + }) + const exits = new Set<(event: { exitCode: number }) => void>() + const finish = () => { + handle._simulateExit(1) + for (const listener of exits) { + listener({ exitCode: 1 }) + } + } + const forceKill = vi.fn(finish) + handle.forceKill = forceKill + const process: BunPtyProcess = { + pid: handle.pid, + process: 'shell', + cols: 80, + rows: 24, + handleFlowControl: false, + write() {}, + resize() {}, + clear() {}, + pause() {}, + resume() {}, + kill: finish, + destroy: finish, + waitForSpawn: () => receipt, + onData: () => ({ dispose() {} }), + onExit(listener) { + exits.add(listener) + return { dispose: () => exits.delete(listener) } + } + } + return { handle, process, confirm, reject, finish, forceKill } +} diff --git a/src/main/daemon/terminal-startup-output.test.ts b/src/main/daemon/terminal-startup-output.test.ts new file mode 100644 index 000000000000..fd419f9c5b26 --- /dev/null +++ b/src/main/daemon/terminal-startup-output.test.ts @@ -0,0 +1,275 @@ +import './mock-descendant-sweep' +import { afterEach, expect, it, vi } from 'vitest' +import { TerminalHost } from './terminal-host' +import { HeadlessEmulator } from './headless-emulator' +import { spawnNativeDaemonPty } from './pty-subprocess/native-pty-spawn' +import { spawnBunPty } from './pty-subprocess/bun-pty-process-runtime' +import { createDaemonPtySubprocessHandle } from './pty-subprocess/subprocess-handle' +import type { + BunTerminal, + BunTerminalOptions, + BunRuntime +} from './pty-subprocess/bun-pty-process-contract' +import type { SubprocessHandle } from './session-subprocess-handle' + +vi.mock('../pty/posix-pty-process-groups', () => ({ + forceKillPosixPtyProcessGroups() { + throw new Error('Use the fixture process kill') + } +})) +vi.mock('./pty-subprocess/foreground-process-tracker', () => ({ + createPtyForegroundProcessTracker: () => ({ + recordOutput() {}, + markDead() {}, + getForegroundProcess: () => null + }) +})) + +function deferred() { + let resolve!: (value: T) => void + let reject!: (error: Error) => void + const promise = new Promise((yes, no) => { + resolve = yes + reject = no + }) + return { promise, resolve, reject } +} + +const hosts: TerminalHost[] = [] +afterEach(async () => { + await Promise.all(hosts.splice(0).map((host) => host.dispose())) + vi.restoreAllMocks() +}) + +function fixture(historySeedChunks?: readonly string[]) { + const receipt = deferred() + const started = deferred() + const exited = deferred() + let callbacks!: BunTerminalOptions + const terminal: BunTerminal = { + closed: false, + write: () => 0, + resize() {}, + close() { + this.closed = true + callbacks.exit?.(this, 0, null) + } + } + const process = { + pid: 4321, + terminal, + exited: exited.promise, + kill: vi.fn(() => exited.resolve(1)) + } + const runtime: BunRuntime = { + Terminal: class { + closed = false + write = terminal.write + resize = terminal.resize + close = terminal.close + constructor(options: BunTerminalOptions) { + callbacks = options + return terminal + } + }, + spawn: () => process + } + const pause = vi.fn(() => true) + const host = new TerminalHost({ + spawnSubprocess: async (opts) => { + let handle!: SubprocessHandle + const spawned = await spawnNativeDaemonPty( + { + shellPath: '/bin/sh', + shellArgs: [], + spawnCwd: '/tmp', + env: {}, + cols: opts.cols, + rows: opts.rows, + windowsFallbackAttempts: [], + signal: opts.cancelSignal, + onSpawnAttempt: (attempt, discardNative) => { + handle = createDaemonPtySubprocessHandle({ + process: attempt.process, + shellPath: attempt.shellPath, + spawnCwd: '/tmp', + env: {}, + startupCommandDeliveredInShellArgs: false, + reportsChildExitStatus: true, + sessionId: opts.sessionId, + startupAgentRecognition: null + }) + const discard = opts.onSpawnAttempt?.(() => handle, discardNative) + started.resolve() + return discard ?? { discard: async () => {} } + } + }, + { + canUseBunPty: () => true, + spawnBunPty: (args) => + spawnBunPty(args, { + platform: 'win32', + runtime, + assignHostJob: () => true, + createJob: () => ({ + terminate: () => { + exited.resolve(1) + return 'terminated' + }, + close() {}, + pause, + resume: () => true, + listProcessIds: () => [] + }), + createWindowsLaunch: () => ({ + command: [], + env: {}, + clearCommand: [], + windowsVerbatimArguments: false, + release() {}, + dispose() {}, + readShellProcessId: () => 4322, + waitForSpawn: () => receipt.promise + }) + }) + } + ) + expect(spawned.process.pid).toBe(4321) + return handle + } + }) + hosts.push(host) + const onData = vi.fn() + const onExit = vi.fn() + const controller = new AbortController() + const pending = host.createOrAttach({ + historySeedChunks, + sessionId: 'startup', + cols: 80, + rows: 24, + command: 'after-confirmation', + streamClient: { onData, onExit }, + cancelSignal: controller.signal, + isCanceled: () => controller.signal.aborted + }) + return { + host, + pending, + onData, + onExit, + receipt, + started, + exited, + terminal, + pause, + controller, + emit(data: string) { + const encoded = new TextEncoder().encode(data) + for (let offset = 0; offset < encoded.length; offset += 16 * 1024) { + callbacks.data(terminal, encoded.slice(offset, offset + 16 * 1024)) + } + } + } +} + +it('parses and retains a startup prefix while the Windows receipt is pending', async () => { + const f = fixture() + await f.started.promise + const output = `\x1b[?2004hSTART\r\n${'x'.repeat(1024 * 1024)}\r\nEND日本語🙂` + f.emit(output) + expect(f.onData).not.toHaveBeenCalled() + expect(f.pause).not.toHaveBeenCalled() + expect(f.host.listSessions()).toEqual([]) + f.receipt.resolve() + const result = await f.pending + expect(result).toMatchObject({ isNew: true, snapshot: null }) + expect(f.onData).toHaveBeenCalledWith(output, output.length, true, output.length) + f.emit('NEXT') + expect(f.onData.mock.calls.at(-1)?.[0]).toBe('NEXT') + expect(f.host.getSnapshot('startup')?.modes.bracketedPaste).toBe(true) +}) + +it('bounds retained output while preserving early modes in the initial stream snapshot', async () => { + const f = fixture() + await f.started.promise + const output = `\x1b[?2004h${'x'.repeat(3 * 1024 * 1024)}\r\nFINAL` + f.emit(output) + expect(f.onData).not.toHaveBeenCalled() + f.receipt.resolve() + await f.pending + const [data, rawLength, transformed, seq] = f.onData.mock.calls[0]! + expect(data.length).toBeLessThan(2 * 1024 * 1024) + expect([rawLength, transformed, seq]).toEqual([output.length, true, output.length]) + const viewer = new HeadlessEmulator({ cols: 80, rows: 24 }) + try { + await viewer.write(data) + expect(viewer.getSnapshot().modes.bracketedPaste).toBe(true) + expect(viewer.getSnapshot().snapshotAnsi).toContain('FINAL') + } finally { + viewer.dispose() + } + f.emit('NEXT') + expect(f.onData.mock.calls.at(-1)?.[0]).toBe('NEXT') + expect(f.host.takePendingOutput('startup', false)?.overflowed).toBe(true) +}) + +it('delivers final output before one exit when the shell exits before confirmation', async () => { + const f = fixture() + await f.started.promise + f.emit('FINAL') + f.exited.resolve(7) + await Promise.resolve() + expect(f.onExit).not.toHaveBeenCalled() + f.receipt.resolve() + await f.pending + expect(f.onData.mock.calls[0]?.[0]).toBe('FINAL') + expect(f.onExit).toHaveBeenCalledTimes(1) + expect(f.onExit.mock.calls[0]?.[0]).toBe(7) + expect(f.onData.mock.invocationCallOrder[0]).toBeLessThan(f.onExit.mock.invocationCallOrder[0]!) +}) + +it('cancels an unconfirmed attempt without publishing output or leaking ownership', async () => { + const f = fixture() + await f.started.promise + f.emit('PRIVATE') + const rejected = expect(f.pending).rejects.toThrow() + f.controller.abort(new Error('canceled')) + await rejected + expect(f.onData).not.toHaveBeenCalled() + expect(f.onExit).not.toHaveBeenCalled() + expect(f.host.listSessions()).toEqual([]) + expect(f.terminal.closed).toBe(true) +}) + +it('cleans up after synchronous parser construction fails before receipt confirmation', async () => { + vi.spyOn(HeadlessEmulator.prototype, 'writeSync').mockReturnValue(false) + const f = fixture() + await expect(f.pending).rejects.toThrow('requires synchronous parsing') + expect(f.terminal.closed).toBe(true) + expect(f.onData).not.toHaveBeenCalled() + expect(f.onExit).not.toHaveBeenCalled() + expect(f.host.listSessions()).toEqual([]) +}) + +it('replays overflowed alternate screen, history seed, cursor and an incomplete escape before the live tail', async () => { + const f = fixture(['SEEDED_HISTORY\r\n']) + await f.started.promise + f.emit(`\x1b[?1049h\x1b[?2004h${'x'.repeat(3 * 1024 * 1024)}\x1b[2;4HFINAL\x1b[3`) + f.receipt.resolve() + await f.pending + const viewer = new HeadlessEmulator({ cols: 80, rows: 24 }) + try { + await viewer.write(f.onData.mock.calls[0]![0]) + expect(viewer.getSnapshot().modes.alternateScreen).toBe(true) + expect(viewer.getSnapshot().modes.bracketedPaste).toBe(true) + f.emit('1mNEXT') + await viewer.write(f.onData.mock.calls.at(-1)![0]) + expect(viewer.getSnapshot().snapshotAnsi).toContain('FINAL') + expect(viewer.getSnapshot().snapshotAnsi).toContain('NEXT') + expect(viewer.getSnapshot().snapshotAnsi).toContain('\x1b[31m') + await viewer.write('\x1b[?1049l') + expect(viewer.getSnapshot().snapshotAnsi).toContain('SEEDED_HISTORY') + } finally { + viewer.dispose() + } +}) diff --git a/src/main/daemon/terminal-startup-session.test.ts b/src/main/daemon/terminal-startup-session.test.ts new file mode 100644 index 000000000000..01a4a4e4b2f6 --- /dev/null +++ b/src/main/daemon/terminal-startup-session.test.ts @@ -0,0 +1,91 @@ +import './mock-descendant-sweep' +import { expect, it, vi } from 'vitest' +import { TerminalStartupSession } from './terminal-startup-session' +import { Session } from './session' +import { createMockSubprocess } from './daemon-pty-adapter-test-harness' + +function ownership() { + const pending = new Map() + const onExit = vi.fn() + const attempt = new TerminalStartupSession( + 'attempt', + pending, + (subprocess, notify, synchronous) => + new Session({ + sessionId: 'attempt', + cols: 80, + rows: 24, + subprocess, + shellReadySupported: false, + requireSynchronousOutput: synchronous, + onExit: notify + }), + onExit + ) + return { attempt, pending, onExit } +} + +it('discards failed-shell output before constructing the fallback session', async () => { + const { attempt, pending, onExit } = ownership() + const first = createMockSubprocess() + attempt.prepare(() => first, true) + first._simulateData('\x1b[?2004hPRIMARY') + await attempt.discard() + expect(pending.size).toBe(0) + expect(onExit).not.toHaveBeenCalled() + const fallback = createMockSubprocess() + const session = attempt.prepare(() => fallback, true) + fallback._simulateData('FALLBACK') + const onData = vi.fn() + session.attachClient({ onData, onExit: vi.fn() }, true) + expect(onData.mock.calls[0]?.[0]).toBe('FALLBACK') + expect(session.getSnapshot()?.modes.bracketedPaste).toBe(false) + await attempt.discard() +}) + +it('retains a failed native cleanup and retries it before another attempt', async () => { + const { attempt, pending } = ownership() + const discardNative = vi + .fn() + .mockRejectedValueOnce(new Error('still live')) + .mockResolvedValue(undefined) + expect(() => + attempt.prepare( + () => { + throw new Error('handle failed') + }, + true, + discardNative + ) + ).toThrow('handle failed') + await expect(attempt.discard()).rejects.toThrow('still live') + expect(pending.get('attempt')).toBe(attempt) + await attempt.clearPreviousAttempt() + expect(discardNative).toHaveBeenCalledTimes(2) + expect(pending.size).toBe(0) +}) + +it('cleans up Session parser resources when listener installation fails', () => { + const handle = createMockSubprocess() + handle.onData = () => { + throw new Error('subscribe failed') + } + const timers = vi.getTimerCount + vi.useFakeTimers() + try { + const before = timers() + expect( + () => + new Session({ + sessionId: 'failed', + cols: 80, + rows: 24, + subprocess: handle, + shellReadySupported: true + }) + ).toThrow('subscribe failed') + expect(timers()).toBe(before) + } finally { + vi.useRealTimers() + } +}) diff --git a/src/main/daemon/terminal-startup-session.ts b/src/main/daemon/terminal-startup-session.ts new file mode 100644 index 000000000000..2685dc2e2f00 --- /dev/null +++ b/src/main/daemon/terminal-startup-session.ts @@ -0,0 +1,138 @@ +import { PhysicalExitTracker } from '../../shared/physical-exit-tracker' +import { IMMEDIATE_KILL_PHYSICAL_EXIT_TIMEOUT_MS } from './immediate-kill-reply-budget' +import type { Session } from './session' +import type { SubprocessHandle } from './session-subprocess-handle' + +/** Owns unconfirmed attempts without exposing them through public session lookup. */ +export class TerminalStartupSession { + current: Session | undefined + observedBeforeConfirmation = false + private published = false + private discarding = false + private exited = false + private handle: SubprocessHandle | undefined + private discardNative: (() => Promise) | undefined + private physicalExit = new PhysicalExitTracker() + + constructor( + private readonly id: string, + private readonly pending: Map, + private create: + | ((handle: SubprocessHandle, onExit: () => void, synchronous: boolean) => Session) + | undefined, + private readonly onExit: () => void + ) {} + + async clearPreviousAttempt(): Promise { + const previous = this.pending.get(this.id) + if (previous) { + await previous.discard() + if (this.pending.get(this.id) === previous) { + this.pending.delete(this.id) + } + } + } + + prepare( + createHandle: () => SubprocessHandle, + synchronous: boolean, + discardNative?: () => Promise + ): Session { + if (!this.create) { + throw new Error('Terminal startup attempt is already published') + } + if (this.current) { + throw new Error('Previous terminal spawn attempt still owns its process') + } + this.observedBeforeConfirmation = synchronous + this.exited = false + this.discarding = false + this.discardNative = discardNative + this.physicalExit = new PhysicalExitTracker() + this.pending.set(this.id, this) + let session: Session + try { + const handle = createHandle() + this.handle = handle + session = this.create( + handle, + () => { + this.physicalExit.markExited() + this.exited = true + if (this.published) { + this.onExit() + } else if (this.discarding) { + queueMicrotask(() => this.releaseExitedAttempt()) + } + }, + synchronous + ) + } catch (error) { + this.handle?.onExit(() => this.physicalExit.markExited()) + throw error + } + this.current = session + return session + } + + async discard(): Promise { + const session = this.current + this.discarding = true + if (session) { + await session.forceKillAndDisposeSubprocess() + } else if (this.discardNative) { + await this.discardNative() + this.handle?.dispose() + this.handle = undefined + this.discardNative = undefined + if (this.pending.get(this.id) === this) { + this.pending.delete(this.id) + } + return + } else if (this.handle) { + this.handle.forceKill() + await this.physicalExit.waitForExit( + IMMEDIATE_KILL_PHYSICAL_EXIT_TIMEOUT_MS, + () => new Error('Unconfirmed terminal process has not exited') + ) + this.handle.dispose() + this.handle = undefined + if (this.pending.get(this.id) === this) { + this.pending.delete(this.id) + } + return + } + this.releaseExitedAttempt() + } + + private releaseExitedAttempt(): void { + const session = this.current + if (!session || session.isAlive) { + return + } + session.dispose() + if (this.pending.get(this.id) === this) { + this.pending.delete(this.id) + } + this.current = undefined + this.handle = undefined + this.discardNative = undefined + } + + publish(): void { + const session = this.current + if (!session) { + throw new Error('Terminal startup attempt is unavailable') + } + if (this.pending.get(this.id) === this) { + this.pending.delete(this.id) + } + this.create = undefined + this.handle = undefined + this.discardNative = undefined + this.published = true + if (this.exited) { + this.onExit() + } + } +} diff --git a/src/main/daemon/tui-idle-agent-bun-fixture.ts b/src/main/daemon/tui-idle-agent-bun-fixture.ts new file mode 100644 index 000000000000..727120476763 --- /dev/null +++ b/src/main/daemon/tui-idle-agent-bun-fixture.ts @@ -0,0 +1,142 @@ +import { fileURLToPath } from 'node:url' +import { spawnBunPty } from './pty-subprocess/bun-pty-process' +import type { BunPtyProcess } from './pty-subprocess/bun-pty-process-contract' +import type { OrcaRuntimeService } from '../runtime/orca-runtime' +import { makeTuiIdleRuntime } from '../runtime/tui-idle-wait-test-harness' +import type { RuntimeSyncWindowGraph } from '../../shared/runtime-types' +import { TERMINAL_LIFECYCLE_METHODS } from '../runtime/rpc/methods/terminal/terminal-lifecycle-methods' +import { getForegroundProcessName } from '../../relay/pty-shell-utils' + +const FIXTURE = fileURLToPath(new URL('../runtime/tui-idle-agent-fixture.mjs', import.meta.url)) +const WORKTREE_ID = 'repo-1::/tmp/tui-idle-real-pty' +const TAB_ID = '55555555-5555-4555-8555-555555555555' +const LEAF_ID = '66666666-6666-4666-8666-666666666666' +const PTY_ID = 'pty-tui-idle-real' + +const waitMethod = TERMINAL_LIFECYCLE_METHODS.find((method) => method.name === 'terminal.wait')! + +const running: BunPtyProcess[] = [] + +async function cleanup() { + while (running.length > 0) { + try { + const child = running.pop() + if (child) { + const exited = new Promise((resolve) => child.onExit(() => resolve())) + child.kill() + await exited + child.destroy() + } + } catch { + // The fixture may already be gone. + } + } +} + +async function startRealAgentPane(mode: 'explicit-idle' | 'quiet', workMs: number) { + const child = spawnBunPty({ + file: process.execPath, + args: [FIXTURE, mode, String(workMs)], + env: Object.fromEntries( + Object.entries(process.env).filter( + (entry): entry is [string, string] => entry[1] !== undefined + ) + ), + cols: 120, + rows: 30, + cwd: '/tmp' + }) + running.push(child) + + // Real foreground read against the real pty: the same helper the relay serves + // `pty.getForegroundProcess` with, so corroboration is host-produced here too. + const runtime = makeTuiIdleRuntime({ + repoPath: '/tmp/tui-idle-real-pty', + getForegroundProcess: () => getForegroundProcessName(child.pid, child.process || null) + }) + runtime.attachWindow(1) + const graph: RuntimeSyncWindowGraph = { + tabs: [ + { + tabId: TAB_ID, + worktreeId: WORKTREE_ID, + title: 'Agent', + activeLeafId: LEAF_ID, + layout: null + } + ], + leaves: [ + { + tabId: TAB_ID, + worktreeId: WORKTREE_ID, + leafId: LEAF_ID, + paneRuntimeId: 1, + ptyId: PTY_ID, + paneTitle: null, + title: '' + } + ] + } + runtime.syncWindowGraph(1, graph) + + const transcript: string[] = [] + child.onData((data) => { + transcript.push(data) + runtime.onPtyData(PTY_ID, data, Date.now()) + }) + + const { terminals } = await runtime.listTerminals(`id:${WORKTREE_ID}`) + return { runtime, transcript, handle: terminals[0].handle } +} + +/** Exactly what `orca terminal wait --terminal --for tui-idle` reaches over RPC. */ +async function terminalWait( + runtime: OrcaRuntimeService, + terminal: string, + timeoutMs: number +): Promise<{ satisfied: boolean; elapsedMs: number }> { + const startedAt = Date.now() + try { + const result = await waitMethod.handler( + { terminal, for: 'tui-idle', timeoutMs }, + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: terminal.wait reads only `runtime` off its context; the rest is request plumbing this fixture has no use for. + { runtime } as Parameters[1] + ) + return { satisfied: result.wait.satisfied === true, elapsedMs: Date.now() - startedAt } + } catch (error) { + // Why only `timeout`: an unsatisfied wait is the outcome under test, but any other + // failure means the harness broke and must not read as a passing refusal. + if ((error instanceof Error ? error.message : String(error)) !== 'timeout') { + throw error + } + return { satisfied: false, elapsedMs: Date.now() - startedAt } + } +} + +export async function runTuiIdleAgentWait(options: { + mode: 'explicit-idle' | 'quiet' + workMs: number + timeoutMs: number + waitForTitle: boolean +}): Promise<{ satisfied: boolean; elapsedMs: number; transcript: string }> { + try { + const { runtime, transcript, handle } = await startRealAgentPane(options.mode, options.workMs) + if (options.waitForTitle) { + const deadline = Date.now() + 5_000 + while (!transcript.join('').includes('\x1b]0;Codex\x07')) { + if (Date.now() >= deadline) { + throw new Error('Agent did not emit its name-only title') + } + await new Promise((resolve) => setTimeout(resolve, 20)) + } + } else { + await new Promise((resolve) => setTimeout(resolve, 500)) + } + return { + ...(await terminalWait(runtime, handle, options.timeoutMs)), + transcript: transcript.join('') + } + } finally { + await cleanup() + } +} diff --git a/src/main/daemon/types.ts b/src/main/daemon/types.ts index d8d88fab0333..8ff74dda5ae2 100644 --- a/src/main/daemon/types.ts +++ b/src/main/daemon/types.ts @@ -1,3 +1,5 @@ +import type { PingRequest } from './daemon-hello-protocol' +import type { TransientPtyRequest } from './daemon-transient-pty-protocol' import type { ConfirmForegroundProcessRequest, ConfirmShellForegroundRequest, @@ -54,7 +56,7 @@ export type { TerminalCheckpointFile } from './daemon-checkpoint-file' // ─── NDJSON Protocol Messages ─────────────────────────────────────── // Hello handshake (first message on each socket) -export type { DaemonEndpointIdentity, HelloMessage, HelloResponse } from './daemon-hello-protocol' +export type { DaemonEndpointIdentity, HelloMessage, HelloResponse, PingRequest } from './daemon-hello-protocol' // ─── RPC Requests (Client → Daemon, on control socket) ───────────── @@ -222,11 +224,6 @@ export type ShutdownRequest = { } } -export type PingRequest = { - id: string - type: 'ping' -} - export type SystemResolverHealthRequest = { id: string type: 'systemResolverHealth' @@ -303,6 +300,7 @@ export type TakePendingOutputResult = { } export type DaemonRequest = + | TransientPtyRequest | CreateOrAttachRequest | HistorySeedProtocol.TerminalHistorySeedTransferRequest | CancelCreateOrAttachRequest diff --git a/src/main/daemon/windows-conpty-warmup.test.ts b/src/main/daemon/windows-conpty-warmup.test.ts index a10d054926cb..b306159c4f7e 100644 --- a/src/main/daemon/windows-conpty-warmup.test.ts +++ b/src/main/daemon/windows-conpty-warmup.test.ts @@ -1,11 +1,6 @@ +import type { BunPtySpawnArgs } from './pty-subprocess/bun-pty-process-contract' import { afterEach, describe, expect, it, vi } from 'vitest' -import type * as pty from 'node-pty' import { warmWindowsConptyOnce } from './windows-conpty-warmup' -import { assignHostProcessToKillOnCloseJob } from '../windows/windows-pty-job' - -vi.mock('../windows/windows-pty-job', () => ({ - assignHostProcessToKillOnCloseJob: vi.fn(() => true) -})) function setPlatform(platform: NodeJS.Platform): () => void { const original = process.platform @@ -25,48 +20,48 @@ afterEach(() => { vi.clearAllMocks() }) -function makeFakePty(): { proc: pty.IPty; fireExit: () => void } { +function makeFakePty() { let exitListener: (() => void) | null = null + const dispose = vi.fn() const proc = { pid: 4321, kill: vi.fn(), onExit: vi.fn((listener: () => void) => { exitListener = listener - return { dispose: () => undefined } + return { dispose } }) - } as unknown as pty.IPty - return { proc, fireExit: () => exitListener?.() } + } + return { proc, dispose, fireExit: () => exitListener?.() } } describe('warmWindowsConptyOnce', () => { it('is a no-op off Windows', async () => { restorePlatform = setPlatform('darwin') - const spawnPty = vi.fn() as unknown as typeof pty.spawn + const spawnPty = vi.fn() warmWindowsConptyOnce(spawnPty) await flushImmediates() expect(spawnPty).not.toHaveBeenCalled() - expect(assignHostProcessToKillOnCloseJob).not.toHaveBeenCalled() }) it('spawns a short-lived cmd.exe with the bundled ConPTY on Windows', async () => { restorePlatform = setPlatform('win32') - const { proc, fireExit } = makeFakePty() - const spawnPty = vi.fn(() => proc) as unknown as typeof pty.spawn + const { proc, fireExit, dispose } = makeFakePty() + const spawnPty = vi.fn((_args: BunPtySpawnArgs) => proc) warmWindowsConptyOnce(spawnPty) await flushImmediates() expect(spawnPty).toHaveBeenCalledTimes(1) - expect(assignHostProcessToKillOnCloseJob).toHaveBeenCalledBefore(vi.mocked(spawnPty)) - const [file, args, options] = vi.mocked(spawnPty).mock.calls[0] - expect(String(file).toLowerCase()).toContain('cmd') - expect(args).toEqual(['/c', 'exit']) - expect(options).toMatchObject({ useConptyDll: true, cols: 2, rows: 1 }) + const options = vi.mocked(spawnPty).mock.calls[0][0] + expect(String(options.file).toLowerCase()).toContain('cmd') + expect(options.args).toEqual(['/d', '/c', 'exit', '0']) + expect(options).toMatchObject({ windowsJobKillOnClose: true, cols: 2, rows: 1 }) // A clean exit must not leave the kill timer to fire later. fireExit() + expect(dispose).toHaveBeenCalledOnce() expect(proc.kill).not.toHaveBeenCalled() }) @@ -75,7 +70,7 @@ describe('warmWindowsConptyOnce', () => { vi.useFakeTimers() try { const { proc } = makeFakePty() - const spawnPty = vi.fn(() => proc) as unknown as typeof pty.spawn + const spawnPty = vi.fn((_args: BunPtySpawnArgs) => proc) warmWindowsConptyOnce(spawnPty) await vi.runOnlyPendingTimersAsync() @@ -91,9 +86,31 @@ describe('warmWindowsConptyOnce', () => { restorePlatform = setPlatform('win32') const spawnPty = vi.fn(() => { throw new Error('conpty unavailable') - }) as unknown as typeof pty.spawn + }) expect(() => warmWindowsConptyOnce(spawnPty)).not.toThrow() await flushImmediates() }) + + it('disposes an already-exited probe and clears its kill timer', async () => { + restorePlatform = setPlatform('win32') + vi.useFakeTimers() + const dispose = vi.fn() + const kill = vi.fn() + try { + warmWindowsConptyOnce(() => ({ + kill, + onExit(listener) { + listener({ exitCode: 0 }) + return { dispose } + } + })) + await vi.runOnlyPendingTimersAsync() + expect(dispose).toHaveBeenCalledOnce() + expect(vi.getTimerCount()).toBe(0) + expect(kill).not.toHaveBeenCalled() + } finally { + vi.useRealTimers() + } + }) }) diff --git a/src/main/daemon/windows-conpty-warmup.ts b/src/main/daemon/windows-conpty-warmup.ts index 98c3144380ce..7f09372bcf90 100644 --- a/src/main/daemon/windows-conpty-warmup.ts +++ b/src/main/daemon/windows-conpty-warmup.ts @@ -1,78 +1,54 @@ import os from 'node:os' -import type * as pty from 'node-pty' -import { createRequire } from 'node:module' -import { canUseBunPty, spawnBunPty } from './pty-subprocess/bun-pty-process' -import { assignHostProcessToKillOnCloseJob } from '../windows/windows-pty-job' +import type { TerminalProcess } from '../../shared/terminal-process' +import type { BunPtySpawnArgs } from './pty-subprocess/bun-pty-process-contract' +import { spawnBunPty } from './pty-subprocess/bun-pty-process' +import { getCmdExePath } from '../../shared/windows-batch-spawn' const WARMUP_KILL_TIMEOUT_MS = 10_000 -const requireFromMain = createRequire(__filename) +type WarmupSpawn = (args: BunPtySpawnArgs) => Pick -const spawnWarmupPty: typeof pty.spawn = (file, args, options) => { - if (canUseBunPty()) { - if (!Array.isArray(args)) { - throw new Error('Bun PTY requires argument arrays') - } - const env: Record = {} - for (const [key, value] of Object.entries(options.env ?? process.env)) { - if (value !== undefined) { - env[key] = value - } - } - return spawnBunPty({ - file, - args, - cwd: options.cwd ?? os.homedir(), - env, - cols: options.cols ?? 2, - rows: options.rows ?? 1 - }) - } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: node-pty's installed package implements the declared spawn contract. - const nodePty = requireFromMain('node-pty') as typeof pty - return nodePty.spawn(file, args, options) -} - -/** - * Pays the one-time cost of the first ConPTY spawn (conpty native module - * load, bundled conpty.dll + OpenConsole.exe first launch, Defender scans of - * those binaries) at daemon boot instead of on the user's first terminal. - * Measured ~2.7s on a Windows dev profile for the first spawn vs ~70ms after. - */ -export function warmWindowsConptyOnce(spawnPty: typeof pty.spawn = spawnWarmupPty): void { +/** Pays first ConPTY startup costs without delaying the daemon handshake. */ +export function warmWindowsConptyOnce(spawnPty: WarmupSpawn = spawnBunPty): void { if (process.platform !== 'win32') { return } - // Why: setImmediate keeps the ready/handshake path ahead of the warm-up; a - // real spawn arriving first simply does the warming itself. setImmediate(() => { try { - // Warm-up children must die with the daemon, even before its first real terminal. - if (!canUseBunPty()) { - assignHostProcessToKillOnCloseJob() + const env: Record = {} + for (const [key, value] of Object.entries(process.env)) { + if (value !== undefined) { + env[key] = value + } } - const proc = spawnPty(process.env.COMSPEC || 'cmd.exe', ['/c', 'exit'], { - name: 'xterm-256color', + const proc = spawnPty({ + file: getCmdExePath(), + args: ['/d', '/c', 'exit', '0'], cols: 2, rows: 1, cwd: os.homedir(), - env: process.env as Record, - // Match real terminal spawns so the bundled ConPTY binaries are the - // ones warmed, not the legacy system ConPTY. - useConptyDll: true + env, + windowsJobKillOnClose: true }) const killTimer = setTimeout(() => { try { proc.kill() } catch { - /* best-effort cleanup of a stuck warm-up shell */ + // Best-effort cleanup of a stuck warm-up shell. } }, WARMUP_KILL_TIMEOUT_MS) killTimer.unref?.() - proc.onExit(() => { + let exited = false + let subscription: { dispose(): void } | undefined + subscription = proc.onExit(() => { + exited = true clearTimeout(killTimer) + subscription?.dispose() }) + if (exited) { + subscription.dispose() + } } catch { - /* warm-up is best-effort; real spawns surface their own errors */ + // Warm-up is best-effort; real spawns surface their own errors. } }) } diff --git a/src/main/daemon/windows-shell-preflight-bun-fixture.ts b/src/main/daemon/windows-shell-preflight-bun-fixture.ts new file mode 100644 index 000000000000..c7e335a1fddc --- /dev/null +++ b/src/main/daemon/windows-shell-preflight-bun-fixture.ts @@ -0,0 +1,63 @@ +import { join } from 'node:path' +import { createWindowsBunPtyLaunch } from './pty-subprocess/windows-bun-pty-launch' +import { spawnBunPty } from './pty-subprocess/bun-pty-process' + +export async function runWindowsShellPreflight(options: { + shellPath: string + shellArgs: string[] + cwd: string + env: NodeJS.ProcessEnv + input?: string + timeoutMs?: number +}): Promise<{ output: string; exitCode: number }> { + const env: Record = {} + for (const [key, value] of Object.entries(options.env)) { + if (value !== undefined) { + env[key] = value + } + } + const proc = spawnBunPty( + { + file: options.shellPath, + args: options.shellArgs, + cols: 100, + rows: 30, + cwd: options.cwd, + env + }, + { + createWindowsLaunch: (args) => + createWindowsBunPtyLaunch(args, { + workerPath: join(process.cwd(), 'out', 'terminal-daemon', 'windows-bun-pty-gate-entry.js') + }) + } + ) + let output = '' + proc.onData((data) => { + output += data + }) + const exited = new Promise((resolve) => { + proc.onExit(({ exitCode }) => resolve(exitCode)) + }) + let timeout: ReturnType | undefined + try { + if (options.input) { + proc.write(options.input.replaceAll('\n', '\r')) + } + const exitCode = await Promise.race([ + exited, + new Promise((_resolve, reject) => { + timeout = setTimeout( + () => reject(new Error(`timed out waiting for Windows shell PTY:\n${output}`)), + options.timeoutMs ?? 10_000 + ) + }) + ]) + return { output, exitCode } + } finally { + clearTimeout(timeout) + proc.kill() + await exited + proc.destroy() + } +} diff --git a/src/main/daemon/wsl-cold-restore-cwd.test.ts b/src/main/daemon/wsl-cold-restore-cwd.test.ts index d9c5cf54770b..2346dbca1ff2 100644 --- a/src/main/daemon/wsl-cold-restore-cwd.test.ts +++ b/src/main/daemon/wsl-cold-restore-cwd.test.ts @@ -38,3 +38,24 @@ describe('normalizeWslColdRestoreCwd', () => { ).toBe('\\\\server\\x') }) }) + +describe('guest-owned history paths', () => { + it.each([ + ['/home/jin', '/home/jin'], + ['\\\\wsl.localhost\\Ubuntu\\home\\jin', '/home/jin'], + ['\\\\wsl$\\ubuntu\\home\\jin', '/home/jin'], + ['\\\\wsl.localhost\\Debian\\home\\jin', '/home/guest'], + ['C:\\Users\\jin', '/home/guest'], + ['relative/path', '/home/guest'], + ['//foreign/share', '/home/guest'] + ])('keeps %s inside the selected Linux guest', (recoveredCwd, expected) => { + expect( + normalizeWslColdRestoreCwd({ + ...base, + guestExecution: true, + requestedCwd: '/home/guest', + recoveredCwd + }) + ).toBe(expected) + }) +}) diff --git a/src/main/daemon/wsl-cold-restore-cwd.ts b/src/main/daemon/wsl-cold-restore-cwd.ts index b3f921106dc3..10fcd3f18cf3 100644 --- a/src/main/daemon/wsl-cold-restore-cwd.ts +++ b/src/main/daemon/wsl-cold-restore-cwd.ts @@ -7,7 +7,17 @@ export function normalizeWslColdRestoreCwd(args: { wslDistro?: string platform?: NodeJS.Platform hostname?: string + guestExecution?: boolean }): string | undefined { + if (args.guestExecution) { + const path = parseWslUncPath(args.recoveredCwd) + if (path) { + return path.distro.toLowerCase() === args.wslDistro?.toLowerCase() + ? path.linuxPath + : args.requestedCwd + } + return /^\/(?!\/)/.test(args.recoveredCwd) ? args.recoveredCwd : args.requestedCwd + } if ((args.platform ?? process.platform) !== 'win32' || !args.wslDistro) { return args.recoveredCwd } diff --git a/src/main/ipc/pty-activation-inventory-scope.test.ts b/src/main/ipc/pty-activation-inventory-scope.test.ts index a6cb5dabad58..74a753559089 100644 --- a/src/main/ipc/pty-activation-inventory-scope.test.ts +++ b/src/main/ipc/pty-activation-inventory-scope.test.ts @@ -1,3 +1,5 @@ +import { registerWslPtyProvider } from './pty/provider/registry' +import { toAppWslPtyId } from '../../shared/wsl-pty-id' import { describe, expect, it, vi } from 'vitest' import { setupPtyIpcSuite } from './pty-ipc-test-harness' import { registerSshPtyProvider, getLocalPtyProvider } from './pty' @@ -6,7 +8,6 @@ import { ptyOwnership } from './pty/provider/ownership-state' vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) @@ -76,6 +77,45 @@ describe('scoped activation PTY inventory', () => { return { localList, remoteLists, startup, list } } + it('includes both guest owners in local scope and refuses partial guest inventory', async () => { + const { list, remoteLists } = install() + const firstOwner = { distro: 'Ubuntu', relayBuildId: 'build-a' } + const secondOwner = { distro: 'Debian', relayBuildId: 'build-b' } + const firstId = toAppWslPtyId(firstOwner, 'pty2:guest:1') + const secondId = toAppWslPtyId(secondOwner, 'pty2:guest:1') + const first = vi.fn(async () => [ + { id: firstId, cwd: '/folder one', title: 'guest', worktreeId: 'folder-project' } + ]) + const second = vi.fn(async () => [{ id: secondId, cwd: '/folder two', title: 'guest' }]) + const releaseFirst = registerWslPtyProvider(firstOwner, { + ...getLocalPtyProvider(), + listProcesses: first + }) + const releaseSecond = registerWslPtyProvider(secondOwner, { + ...getLocalPtyProvider(), + listProcesses: second + }) + try { + const rows = await list({ connectionId: null }) + if (!Array.isArray(rows)) { + throw new Error('Expected session inventory') + } + expect(rows.map((row: { id: string }) => row.id)).toEqual(['local', firstId, secondId]) + expect(rows[1]).toMatchObject({ cwd: '/folder one', worktreeId: 'folder-project' }) + expect(remoteLists.every((remote) => remote.mock.calls.length === 0)).toBe(true) + second.mockRejectedValue(new Error('disconnected')) + await expect(list({ connectionId: null })).rejects.toThrow('unverifiable') + releaseSecond() + await expect(list({ connectionId: null })).rejects.toThrow('unverifiable') + expect(await list({ connectionId: 'host-17' })).toHaveLength(1) + } finally { + releaseFirst() + releaseSecond() + ptyOwnership.delete(firstId) + ptyOwnership.delete(secondId) + } + }) + it('queries only the chosen SSH provider and preserves workspace and ownership evidence', async () => { const { list, localList, remoteLists, startup } = install() expect(await list({ connectionId: 'host-17' })).toEqual([ diff --git a/src/main/ipc/pty-applied-size-reporting.test.ts b/src/main/ipc/pty-applied-size-reporting.test.ts index 87b8c5e086f2..1f5e04a47016 100644 --- a/src/main/ipc/pty-applied-size-reporting.test.ts +++ b/src/main/ipc/pty-applied-size-reporting.test.ts @@ -5,7 +5,6 @@ import { registerPtyHandlers, setLocalPtyProvider } from './pty' vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) diff --git a/src/main/ipc/pty-buffer-snapshot-dispatch.test.ts b/src/main/ipc/pty-buffer-snapshot-dispatch.test.ts index 5be4194bc76c..9366bf0f2eb0 100644 --- a/src/main/ipc/pty-buffer-snapshot-dispatch.test.ts +++ b/src/main/ipc/pty-buffer-snapshot-dispatch.test.ts @@ -5,7 +5,6 @@ import { registerPtyHandlers, setLocalPtyProvider } from './pty' vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) @@ -147,6 +146,9 @@ describe('registerPtyHandlers', () => { function setup(): { listener: SerializeListener; controller: SerializeController } { const runtime = { + createPreAllocatedTerminalHandle: vi.fn(() => 'term_test'), + registerPreAllocatedHandleForPty: vi.fn(), + registerPty: vi.fn(), setPtyController: vi.fn(), onPtySpawned: vi.fn(), onPtyData: vi.fn(), @@ -305,6 +307,9 @@ describe('registerPtyHandlers', () => { describe('main buffer snapshot dispatch', () => { it('returns a hidden-output recovery snapshot with clamped scrollback', async () => { const runtime = { + createPreAllocatedTerminalHandle: vi.fn(() => 'term_test'), + registerPreAllocatedHandleForPty: vi.fn(), + registerPty: vi.fn(), setPtyController: vi.fn(), getPtyOutputSequence: vi.fn(() => 42), serializeHiddenOutputRecoveryBuffer: vi.fn().mockResolvedValue({ @@ -348,6 +353,9 @@ describe('registerPtyHandlers', () => { source: 'headless' }) const runtime = { + createPreAllocatedTerminalHandle: vi.fn(() => 'term_test'), + registerPreAllocatedHandleForPty: vi.fn(), + registerPty: vi.fn(), setPtyController: vi.fn(), getPtyOutputSequence: vi.fn(() => 640), notePtyDataGap: vi.fn(), @@ -389,6 +397,9 @@ describe('registerPtyHandlers', () => { const provider = installObservableDaemonTestProvider() provider.getBufferSnapshot.mockResolvedValue(null) const runtime = { + createPreAllocatedTerminalHandle: vi.fn(() => 'term_test'), + registerPreAllocatedHandleForPty: vi.fn(), + registerPty: vi.fn(), setPtyController: vi.fn(), getPtyOutputSequence: vi.fn(() => 640), notePtyDataGap: vi.fn(), @@ -422,12 +433,14 @@ describe('registerPtyHandlers', () => { const mockProc = createMockProc() spawnMock.mockReturnValue(mockProc.proc) const runtime = { + createPreAllocatedTerminalHandle: vi.fn(() => null), + registerPreAllocatedHandleForPty: vi.fn(), setPtyController: vi.fn(), registerPty: vi.fn(), noteTerminalSpawnCommand: vi.fn(), onPtySpawned: vi.fn(), onPtyExit: vi.fn(), - onPtyData: vi.fn(), + onPtyData: vi.fn(() => 2_472), preAllocateHandleForPty: vi.fn(() => null), getPtyOutputSequence: vi.fn(() => 2_472), hasRemoteTerminalViewSubscriber: vi.fn(() => false), diff --git a/src/main/ipc/pty-codex-account-attribution.test.ts b/src/main/ipc/pty-codex-account-attribution.test.ts index 353be0f59095..abd78e83fc4d 100644 --- a/src/main/ipc/pty-codex-account-attribution.test.ts +++ b/src/main/ipc/pty-codex-account-attribution.test.ts @@ -11,7 +11,6 @@ import { registerPtyHandlers, setLocalPtyProvider } from './pty' vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) diff --git a/src/main/ipc/pty-controller-owner-recovery.test.ts b/src/main/ipc/pty-controller-owner-recovery.test.ts index 23cca2fb45f1..379a142765b3 100644 --- a/src/main/ipc/pty-controller-owner-recovery.test.ts +++ b/src/main/ipc/pty-controller-owner-recovery.test.ts @@ -11,7 +11,6 @@ import { vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) diff --git a/src/main/ipc/pty-controller-ownership-routing.test.ts b/src/main/ipc/pty-controller-ownership-routing.test.ts index cf61b71c9e55..4e21148647f2 100644 --- a/src/main/ipc/pty-controller-ownership-routing.test.ts +++ b/src/main/ipc/pty-controller-ownership-routing.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from 'vitest' import { setupPtyIpcSuite } from './pty-ipc-test-harness' import type { AgentSessionOwnerBinding } from '../../shared/agent-session-host-authority' -import { LocalPtyProvider } from '../providers/local-pty-provider' +import { createUnavailablePtyProvider } from '../providers/unavailable-pty-provider' import { registerPtyHandlers, registerSshPtyProvider, @@ -24,7 +24,6 @@ type SettledControllerDouble = { vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) @@ -159,8 +158,7 @@ describe('registerPtyHandlers', () => { expect(provider.write).toHaveBeenCalledWith('pty-refused', 'input') }) describe('controller probePtyLiveness routing', () => { - it('proves absence for an id the in-process local provider never owned', async () => { - setLocalPtyProvider(new LocalPtyProvider()) + it('proves absence only after the daemon answers for a previously unseen id', async () => { const controller = registerAgentClaimController() await expect(controller.probePtyLiveness('pty-from-prior-run')).resolves.toBe(false) @@ -190,7 +188,6 @@ describe('registerPtyHandlers', () => { it('answers unknown for SSH-owned ids whose provider has no probe', async () => { const connectionId = 'ssh-probe-1' const ptyId = `ssh:${connectionId}@@remote-pty` - setLocalPtyProvider(new LocalPtyProvider()) registerSshPtyProvider(connectionId, createAgentClaimProvider({}) as never) setPtyOwnership(ptyId, connectionId) const controller = registerAgentClaimController() @@ -210,7 +207,6 @@ describe('registerPtyHandlers', () => { it('answers unknown for remote-scoped ids without consulting local providers', async () => { // Why: a locally routed provider would answer confidently — and wrongly — // for a PTY that lives on a remote Orca host. - setLocalPtyProvider(new LocalPtyProvider()) const controller = registerAgentClaimController() await expect(controller.probePtyLiveness('remote:some-remote-pty')).resolves.toBeNull() @@ -252,12 +248,12 @@ describe('registerPtyHandlers', () => { localProvider.attach.mockRejectedValueOnce(new Error('Session not found')) await expect(controller.attach(daemonPtyId)).resolves.toBe(false) - // The in-process local provider streams without attach; never called. - const inProcess = new LocalPtyProvider() - const inProcessAttach = vi.spyOn(inProcess, 'attach') - setLocalPtyProvider(inProcess) + const unavailable = createUnavailablePtyProvider() + const unavailableAttach = vi.spyOn(unavailable, 'attach') + setLocalPtyProvider(unavailable) await expect(controller.attach(daemonPtyId)).resolves.toBe(false) - expect(inProcessAttach).not.toHaveBeenCalled() + expect(unavailableAttach).toHaveBeenCalledWith(daemonPtyId) + await expect(controller.probePtyLiveness(daemonPtyId)).resolves.toBeNull() } finally { unregisterSshPtyProvider('ssh-attach') clearPtyOwnershipForConnection('ssh-attach') diff --git a/src/main/ipc/pty-controller-process-inventory.test.ts b/src/main/ipc/pty-controller-process-inventory.test.ts index e89d888978a5..e03203b31b2b 100644 --- a/src/main/ipc/pty-controller-process-inventory.test.ts +++ b/src/main/ipc/pty-controller-process-inventory.test.ts @@ -34,18 +34,6 @@ vi.mock('fs', () => ({ constants: { X_OK: 1 } })) -vi.mock('node-pty', () => ({ - spawn: vi.fn().mockReturnValue({ - onData: vi.fn(), - onExit: vi.fn(), - write: vi.fn(), - resize: vi.fn(), - kill: vi.fn(), - process: 'zsh', - pid: 12345 - }) -})) - vi.mock('../opencode/hook-service', () => ({ openCodeHookService: { buildPtyEnv: () => ({}), clearPty: vi.fn() } })) diff --git a/src/main/ipc/pty-controller-spawn-admission.test.ts b/src/main/ipc/pty-controller-spawn-admission.test.ts index 65f5ba565c9e..8d363a2f7445 100644 --- a/src/main/ipc/pty-controller-spawn-admission.test.ts +++ b/src/main/ipc/pty-controller-spawn-admission.test.ts @@ -1,4 +1,6 @@ -import { describe, expect, it, vi } from 'vitest' +import { DaemonPtyAdapter } from '../daemon/daemon-pty-adapter' +import { DaemonPtyRouter } from '../daemon/daemon-pty-router' +import { describe, expect, it, onTestFinished, vi } from 'vitest' import { openCodeBuildPtyEnvMock, openCodeClearPtyMock, @@ -17,7 +19,6 @@ import { vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) @@ -115,7 +116,7 @@ describe('registerPtyHandlers', () => { expect(internals.pendingPtyRegistrationIncarnations.size).toBe(0) clearProviderPtyState(ptyId) }) - it('adopts a live controller-owned local fallback when listings cannot serialize claims', async () => { + it('adopts a live controller-owned terminal when listings cannot serialize claims', async () => { const sessions: { id: string incarnationId: string @@ -132,7 +133,6 @@ describe('registerPtyHandlers', () => { spawn: physicalSpawn, authoritativeOwnerListings: false }) - Object.assign(provider, { routesFreshSpawnsToLocalProvider: true }) setLocalPtyProvider(provider as never) const controller = registerAgentClaimController() const request = { @@ -156,25 +156,23 @@ describe('registerPtyHandlers', () => { clearProviderPtyState('pty-local-claim') }) it.each(['runtime controller', 'renderer IPC'] as const)( - 'recovers degraded fresh-spawn routing before %s chooses daemon host semantics', + '%s preserves daemon identity while the router recovers fresh spawning', async (entryPoint) => { - let degraded = true - const daemonSpawn = vi.fn(async (options: { sessionId?: string }) => ({ + const adapter = new DaemonPtyAdapter({ + socketPath: '/test/recovery.sock', + tokenPath: '/test/recovery.token' + }) + const daemonSpawn = vi.spyOn(adapter, 'spawn').mockImplementation(async (options) => ({ id: options.sessionId ?? 'unexpected-fallback-id' })) - const provider = createAgentClaimProvider({ spawn: daemonSpawn }) - const recoverFreshSpawnRouting = vi.fn(async () => { - degraded = false - return true - }) - Object.defineProperties(provider, { - routesFreshSpawnsToLocalProvider: { - configurable: true, - get: () => (degraded ? true : undefined) - }, - recoverFreshSpawnRouting: { value: recoverFreshSpawnRouting } + const recoverFreshSpawnRouting = vi.fn(async () => true) + const provider = new DaemonPtyRouter({ + current: adapter, + legacy: [], + probeFreshSpawn: recoverFreshSpawnRouting }) - setLocalPtyProvider(provider as never) + onTestFinished(() => provider.dispose()) + setLocalPtyProvider(provider) const controller = registerAgentClaimController() const worktreeId = 'repo::/tmp/recovered-daemon-routing' const spawnArgs = { @@ -199,23 +197,21 @@ describe('registerPtyHandlers', () => { } ) it('recovers degraded routing for a fresh runtime session with a stable id', async () => { - let degraded = true - const daemonSpawn = vi.fn(async (options: { sessionId?: string; isNewSession?: boolean }) => ({ + const adapter = new DaemonPtyAdapter({ + socketPath: '/test/recovery.sock', + tokenPath: '/test/recovery.token' + }) + const daemonSpawn = vi.spyOn(adapter, 'spawn').mockImplementation(async (options) => ({ id: options.sessionId ?? 'unexpected-fallback-id' })) - const provider = createAgentClaimProvider({ spawn: daemonSpawn }) - const recoverFreshSpawnRouting = vi.fn(async () => { - degraded = false - return true + const recoverFreshSpawnRouting = vi.fn(async () => true) + const provider = new DaemonPtyRouter({ + current: adapter, + legacy: [], + probeFreshSpawn: recoverFreshSpawnRouting }) - Object.defineProperties(provider, { - routesFreshSpawnsToLocalProvider: { - configurable: true, - get: () => (degraded ? true : undefined) - }, - recoverFreshSpawnRouting: { value: recoverFreshSpawnRouting } - }) - setLocalPtyProvider(provider as never) + onTestFinished(() => provider.dispose()) + setLocalPtyProvider(provider) const controller = registerAgentClaimController() await controller.spawn({ diff --git a/src/main/ipc/pty-cumulative-ack-accounting.test.ts b/src/main/ipc/pty-cumulative-ack-accounting.test.ts index 4761588606b6..0f633ed55814 100644 --- a/src/main/ipc/pty-cumulative-ack-accounting.test.ts +++ b/src/main/ipc/pty-cumulative-ack-accounting.test.ts @@ -9,7 +9,6 @@ import { vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) @@ -86,7 +85,7 @@ describe('registerPtyHandlers', () => { rendererInFlightPtyCount: 0 }) - vi.runOnlyPendingTimers() + vi.advanceTimersByTime(0) expect(getPtyDataSendCalls()).toHaveLength(33) } finally { vi.useRealTimers() @@ -131,22 +130,22 @@ describe('registerPtyHandlers', () => { const spawnResult = await spawnAndSaturateRendererDeliveryGate(mockProc) const ackData = getPtyAckDataListener() expect(getPtyDataSendCalls()).toHaveLength(32) - expect(vi.getTimerCount()).toBe(0) + expect(vi.getTimerCount()).toBe(1) ackData(null, { id: spawnResult.id, processedChars: 0 }) expect(getPtyRendererDeliveryDebugSnapshot().flushScheduled).toBe(true) - expect(vi.getTimerCount()).toBe(1) + expect(vi.getTimerCount()).toBe(2) ackData(null, { id: spawnResult.id, processedChars: 0 }) ackData(null, { id: spawnResult.id, processedChars: -1 }) - expect(vi.getTimerCount()).toBe(1) + expect(vi.getTimerCount()).toBe(2) - vi.runOnlyPendingTimers() + vi.advanceTimersByTime(1) expect(getPtyDataSendCalls()).toHaveLength(32) expect(getPtyRendererDeliveryDebugSnapshot().flushScheduled).toBe(false) - expect(vi.getTimerCount()).toBe(0) + expect(vi.getTimerCount()).toBe(1) ackData(null, { id: spawnResult.id, processedChars: 16 * 1024 }) - vi.runOnlyPendingTimers() + vi.advanceTimersByTime(1) expect(getPtyDataSendCalls()).toHaveLength(33) } finally { vi.useRealTimers() diff --git a/src/main/ipc/pty-daemon-controller-teardown.test.ts b/src/main/ipc/pty-daemon-controller-teardown.test.ts index f43ed69f0cc5..5f693fbb480c 100644 --- a/src/main/ipc/pty-daemon-controller-teardown.test.ts +++ b/src/main/ipc/pty-daemon-controller-teardown.test.ts @@ -11,7 +11,6 @@ import { vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) diff --git a/src/main/ipc/pty-daemon-spawn-agent-home-env.test.ts b/src/main/ipc/pty-daemon-spawn-agent-home-env.test.ts index f73f11e7f932..9c219ab37351 100644 --- a/src/main/ipc/pty-daemon-spawn-agent-home-env.test.ts +++ b/src/main/ipc/pty-daemon-spawn-agent-home-env.test.ts @@ -13,7 +13,6 @@ import { registerPtyHandlers } from './pty' vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) diff --git a/src/main/ipc/pty-daemon-spawn-codex-auth.test.ts b/src/main/ipc/pty-daemon-spawn-codex-auth.test.ts index cb60984c50cf..2bd9fb3cadb5 100644 --- a/src/main/ipc/pty-daemon-spawn-codex-auth.test.ts +++ b/src/main/ipc/pty-daemon-spawn-codex-auth.test.ts @@ -17,7 +17,6 @@ import { registerPtyHandlers, resolveCodexHomeAfterManagedAuthReadiness } from ' vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) diff --git a/src/main/ipc/pty-daemon-spawn-session-identity.test.ts b/src/main/ipc/pty-daemon-spawn-session-identity.test.ts index 766decf1c9c0..888792547dc1 100644 --- a/src/main/ipc/pty-daemon-spawn-session-identity.test.ts +++ b/src/main/ipc/pty-daemon-spawn-session-identity.test.ts @@ -19,7 +19,6 @@ import { vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) diff --git a/src/main/ipc/pty-daemon-spawn-wsl-runtime.test.ts b/src/main/ipc/pty-daemon-spawn-wsl-runtime.test.ts index cbeddb333cd7..3c9a05e90d5e 100644 --- a/src/main/ipc/pty-daemon-spawn-wsl-runtime.test.ts +++ b/src/main/ipc/pty-daemon-spawn-wsl-runtime.test.ts @@ -11,7 +11,6 @@ import { registerPtyHandlers } from './pty' vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) diff --git a/src/main/ipc/pty-daemon-ssh-lease-lifecycle.test.ts b/src/main/ipc/pty-daemon-ssh-lease-lifecycle.test.ts index 128f163c5b34..49c7ff39ade0 100644 --- a/src/main/ipc/pty-daemon-ssh-lease-lifecycle.test.ts +++ b/src/main/ipc/pty-daemon-ssh-lease-lifecycle.test.ts @@ -15,7 +15,6 @@ import { vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) diff --git a/src/main/ipc/pty-dead-owner-respawn.test.ts b/src/main/ipc/pty-dead-owner-respawn.test.ts index 7a72c0d53b49..6adf1f267d5b 100644 --- a/src/main/ipc/pty-dead-owner-respawn.test.ts +++ b/src/main/ipc/pty-dead-owner-respawn.test.ts @@ -7,7 +7,6 @@ import { registerPtyHandlers, setLocalPtyProvider } from './pty' vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) diff --git a/src/main/ipc/pty-delivery-health-heal.test.ts b/src/main/ipc/pty-delivery-health-heal.test.ts index 934a5bcdd374..9a76c2208176 100644 --- a/src/main/ipc/pty-delivery-health-heal.test.ts +++ b/src/main/ipc/pty-delivery-health-heal.test.ts @@ -5,7 +5,6 @@ import { registerPtyHandlers, getPtyRendererDeliveryDebugSnapshot } from './pty' vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) @@ -86,8 +85,8 @@ describe('registerPtyHandlers', () => { vi.advanceTimersByTime(1) } mockProc.emitData('stuck-output') - // Only the probe's hygiene timeout remains; the dispatcher-ready handshake already drained the pending flush. - expect(vi.getTimerCount()).toBe(1) + // The daemon's pause failsafe and the delivery probe have independent lifetimes. + expect(vi.getTimerCount()).toBe(2) destroyed = true mockProc.emitData('post-destroy output') diff --git a/src/main/ipc/pty-dispatcher-handshake-osc-answers.test.ts b/src/main/ipc/pty-dispatcher-handshake-osc-answers.test.ts index cdb744b6d534..c7e800534b8a 100644 --- a/src/main/ipc/pty-dispatcher-handshake-osc-answers.test.ts +++ b/src/main/ipc/pty-dispatcher-handshake-osc-answers.test.ts @@ -5,7 +5,6 @@ import { registerPtyHandlers, getPtyRendererDeliveryDebugSnapshot } from './pty' vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) diff --git a/src/main/ipc/pty-global-renderer-credit.test.ts b/src/main/ipc/pty-global-renderer-credit.test.ts index caeccfc572ed..43c894c99c80 100644 --- a/src/main/ipc/pty-global-renderer-credit.test.ts +++ b/src/main/ipc/pty-global-renderer-credit.test.ts @@ -5,7 +5,6 @@ import { registerPtyHandlers, getPtyRendererDeliveryDebugSnapshot } from './pty' vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) @@ -227,7 +226,7 @@ describe('registerPtyHandlers', () => { pendingPtyCount: 1, flushScheduled: false }) - expect(vi.getTimerCount()).toBe(0) + expect(vi.getTimerCount()).toBe(1) proc.emitExit(0) diff --git a/src/main/ipc/pty-hidden-at-spawn-mark.test.ts b/src/main/ipc/pty-hidden-at-spawn-mark.test.ts index 240793eda1d0..ba2c38d7af5b 100644 --- a/src/main/ipc/pty-hidden-at-spawn-mark.test.ts +++ b/src/main/ipc/pty-hidden-at-spawn-mark.test.ts @@ -7,11 +7,10 @@ import { OrcaRuntimeService } from '../runtime/orca-runtime' import { registerPtyHandlers } from './pty' import { join } from 'node:path' // Why resolved rather than hardcoded: the wrapper tree is content-addressed. -import { getShellReadyWrapperRoot } from '../providers/local-pty-shell-ready-wrapper-root' +import { getShellReadyWrapperRoot } from '../daemon/shell-ready' vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) @@ -365,7 +364,10 @@ describe('registerPtyHandlers', () => { expect(result).toEqual({ id: expect.any(String), - pid: 12345, + pid: 999_999_999, + providerSequence: { generation: 'reset', value: 0 }, + snapshotKittyKeyboardFlags: undefined, + wslDistro: null, incarnationId: expect.any(String) }) expect(spawnMock).toHaveBeenCalledTimes(1) @@ -375,7 +377,7 @@ describe('registerPtyHandlers', () => { expect.objectContaining({ cwd: '/tmp' }) ) expect(warnSpy).toHaveBeenCalledWith( - expect.stringContaining('Primary shell "/opt/homebrew/bin/bash" failed') + expect.stringContaining('Preferred shell "/opt/homebrew/bin/bash" is unavailable') ) } finally { warnSpy.mockRestore() @@ -422,7 +424,7 @@ describe('registerPtyHandlers', () => { }) ) expect(warnSpy).toHaveBeenCalledWith( - expect.stringContaining('Shell "/opt/homebrew/bin/bash" is not executable') + expect.stringContaining('Preferred shell "/opt/homebrew/bin/bash" is unavailable') ) } finally { warnSpy.mockRestore() diff --git a/src/main/ipc/pty-ipc-daemon-test-provider.ts b/src/main/ipc/pty-ipc-daemon-test-provider.ts new file mode 100644 index 000000000000..fac0343eff53 --- /dev/null +++ b/src/main/ipc/pty-ipc-daemon-test-provider.ts @@ -0,0 +1,151 @@ +import { DaemonPtyAdapter } from '../daemon/daemon-pty-adapter' +import { TerminalHost } from '../daemon/terminal-host' +import { prepareMacosTccLoginShell } from '../providers/macos-tcc-login-shell' +import { createPtySubprocess } from '../daemon/pty-subprocess' +import type { DaemonEvent, DaemonRequest } from '../daemon/types' +import { WRITE_ACCEPTED } from '../../shared/pty-write-settlement' + +/** Replaces transport only; IPC tests exercise real daemon launch, session and exit behavior. */ +export class PtyIpcDaemonTestProvider extends DaemonPtyAdapter { + private readonly host = new TerminalHost({ + spawnSubprocess: async (options) => { + await prepareMacosTccLoginShell() + return createPtySubprocess(options) + } + }) + private readonly eventHandlers = new Set<(event: unknown) => void>() + private readonly attachments = new Map() + + constructor() { + super({ socketPath: '/test/pty-ipc.sock', tokenPath: '/test/pty-ipc.token' }) + this.client.ensureConnected = async () => {} + this.client.ensureConnectedWithin = async () => {} + this.client.isConnected = () => true + this.client.onEvent = (handler) => { + this.eventHandlers.add(handler) + return () => this.eventHandlers.delete(handler) + } + this.client.request = async (type: string, payload: unknown): Promise => { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Only the real adapter's typed requests enter this test transport. + const request = { id: 'ipc-test', type, payload } as DaemonRequest + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Each route returns the host result expected by the real adapter's RPC call. + return (await this.route(request)) as T + } + this.client.notify = (type, payload) => { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Notifications originate from the real adapter, with its protocol payloads. + this.route({ id: 'ipc-test-notify', type, payload } as DaemonRequest) + return true + } + this.client.notifyWithSettlement = async (type, payload) => { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Notifications originate from the real adapter, with its protocol payloads. + await this.route({ id: 'ipc-test-notify', type, payload } as DaemonRequest) + return WRITE_ACCEPTED + } + } + + private emit(event: DaemonEvent): void { + for (const handler of this.eventHandlers) { + handler(event) + } + } + + private route(request: DaemonRequest): unknown { + switch (request.type) { + case 'createOrAttach': { + const { historySeed, ...payload } = request.payload + return this.host + .createOrAttach({ + ...payload, + ...(historySeed ? { historySeedChunks: [historySeed] } : {}), + streamClient: { + onData: (data, rawLength, transformed, seq) => + this.emit({ + type: 'event', + event: 'data', + sessionId: payload.sessionId, + payload: { data, rawLength, transformed, seq } + }), + onExit: (code, incarnationId, cause) => + this.emit({ + type: 'event', + event: 'exit', + sessionId: payload.sessionId, + payload: { code, incarnationId, ...(cause ? { cause } : {}) } + }) + } + }) + .then((result) => { + this.attachments.set(payload.sessionId, result.attachToken) + return result + }) + } + case 'listSessions': + return { sessions: this.host.listSessions() } + case 'getSize': + return { size: this.host.getAppliedSize(request.payload.sessionId) } + case 'write': + return this.host.write(request.payload.sessionId, request.payload.data) + case 'resize': + return this.host.resize( + request.payload.sessionId, + request.payload.cols, + request.payload.rows + ) + case 'pausePty': + return this.host.pauseProducer(request.payload.sessionId) + case 'resumePty': + return this.host.resumeProducer(request.payload.sessionId) + case 'kill': + return this.host.kill(request.payload.sessionId, { immediate: request.payload.immediate }) + case 'signal': + return this.host.signal(request.payload.sessionId, request.payload.signal) + case 'getCwd': + return this.host.getCwd(request.payload.sessionId).then((cwd) => ({ cwd })) + case 'getForegroundProcess': + return { foregroundProcess: this.host.getForegroundProcess(request.payload.sessionId) } + case 'getSnapshot': + return { snapshot: this.host.getSnapshot(request.payload.sessionId) } + case 'takePendingOutput': + return this.host.takePendingOutput( + request.payload.sessionId, + request.payload.includeSnapshot ?? false + ) + case 'clearScrollback': + return this.host.clearScrollback(request.payload.sessionId) + case 'closeStartupQueryAuthority': + return { appliedSeq: this.host.closeStartupQueryAuthority(request.payload.sessionId) } + case 'detach': { + const token = this.attachments.get(request.payload.sessionId) + if (token) { + this.host.detach(request.payload.sessionId, token) + } + this.attachments.delete(request.payload.sessionId) + return {} + } + case 'setSessionBackground': + return {} + case 'abortHistorySeedTransfer': + case 'appendHistorySeedTransfer': + case 'cancelCreateOrAttach': + case 'closeTransientPty': + case 'confirmForegroundProcess': + case 'confirmShellForeground': + case 'createTransientPty': + case 'finishHistorySeedTransfer': + case 'inspectProcess': + case 'ping': + case 'ptySpawnHealth': + case 'shutdown': + case 'shutdownIfIdle': + case 'startHistorySeedTransfer': + case 'systemResolverHealth': + case 'writeTransientPty': + throw new Error(`IPC daemon test transport does not implement ${request.type}`) + } + } + + async disposeHost(): Promise { + this.dispose() + await this.host.dispose() + } +} diff --git a/src/main/ipc/pty-ipc-hidden-delivery-gate.test.ts b/src/main/ipc/pty-ipc-hidden-delivery-gate.test.ts index bfacfc969352..84f41b61f249 100644 --- a/src/main/ipc/pty-ipc-hidden-delivery-gate.test.ts +++ b/src/main/ipc/pty-ipc-hidden-delivery-gate.test.ts @@ -6,7 +6,6 @@ import { registerPtyHandlers, getPtyRendererDeliveryDebugSnapshot } from './pty' vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) diff --git a/src/main/ipc/pty-ipc-mock-registry.ts b/src/main/ipc/pty-ipc-mock-registry.ts index 71b77621b755..777daf9d13b7 100644 --- a/src/main/ipc/pty-ipc-mock-registry.ts +++ b/src/main/ipc/pty-ipc-mock-registry.ts @@ -95,10 +95,6 @@ export const fsModuleMock = () => ({ } }) -export const nodePtyModuleMock = () => ({ - spawn: spawnMock -}) - // Why: these suites force darwin on non-macOS hosts; isolate the PAM probe while preserving other child_process APIs. export const childProcessModuleMock = (original: Record) => ({ ...original, @@ -142,6 +138,7 @@ export const piTitlebarExtensionModuleMock = () => ({ }) export const pwshModuleMock = () => ({ + isPwshAvailable: isPwshAvailableMock, isPwshAvailableAsync: isPwshAvailableMock }) diff --git a/src/main/ipc/pty-ipc-native-runtime-mock.ts b/src/main/ipc/pty-ipc-native-runtime-mock.ts new file mode 100644 index 000000000000..f2c6d2943914 --- /dev/null +++ b/src/main/ipc/pty-ipc-native-runtime-mock.ts @@ -0,0 +1,43 @@ +import { vi } from 'vitest' +import type * as FsPromises from 'node:fs/promises' +import { existsSyncMock, statSyncMock, spawnMock } from './pty-ipc-mock-registry' + +vi.mock('node:fs/promises', async (importOriginal) => ({ + ...(await importOriginal()), + stat: async (...args: unknown[]) => { + if (!existsSyncMock(args[0])) { + throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' }) + } + return statSyncMock(...args) + } +})) + +vi.mock('../daemon/pty-subprocess/bun-pty-process', () => ({ + canUseBunPty: () => true, + spawnBunPty: (args: { + file: string + args: string[] + cwd: string + env: Record + cols: number + rows: number + }) => + spawnMock(args.file, args.args, { + cwd: args.cwd, + env: args.env, + cols: args.cols, + rows: args.rows, + name: args.env.TERM + }) +})) + +export function finishMockNativePtyProcesses(): void { + for (const result of spawnMock.mock.results) { + if (result.type !== 'return') { + continue + } + for (const [onExit] of result.value?.onExit?.mock?.calls ?? []) { + onExit({ exitCode: 0 }) + } + } +} diff --git a/src/main/ipc/pty-ipc-process-env-scope.ts b/src/main/ipc/pty-ipc-process-env-scope.ts index 2f6e6bd7c409..63a489ff3c37 100644 --- a/src/main/ipc/pty-ipc-process-env-scope.ts +++ b/src/main/ipc/pty-ipc-process-env-scope.ts @@ -7,6 +7,7 @@ export function createPtyIpcProcessEnvScope() { const savedPiAgentDir = process.env.PI_CODING_AGENT_DIR const savedOrcaPiAgentDir = process.env.ORCA_PI_CODING_AGENT_DIR const savedOrcaPiSourceAgentDir = process.env.ORCA_PI_SOURCE_AGENT_DIR + const savedCodexHome = process.env.CODEX_HOME const savedOrcaCodexHome = process.env.ORCA_CODEX_HOME const savedOrcaOmpAgentDir = process.env.ORCA_OMP_CODING_AGENT_DIR const savedOrcaOmpSourceAgentDir = process.env.ORCA_OMP_SOURCE_AGENT_DIR @@ -35,6 +36,7 @@ export function createPtyIpcProcessEnvScope() { delete process.env.PI_CODING_AGENT_DIR delete process.env.ORCA_PI_SOURCE_AGENT_DIR delete process.env.ORCA_PI_CODING_AGENT_DIR + delete process.env.CODEX_HOME delete process.env.ORCA_CODEX_HOME delete process.env.ORCA_OMP_SOURCE_AGENT_DIR delete process.env.ORCA_OMP_CODING_AGENT_DIR @@ -45,6 +47,11 @@ export function createPtyIpcProcessEnvScope() { } function restoreProcessEnv() { + if (savedCodexHome === undefined) { + delete process.env.CODEX_HOME + } else { + process.env.CODEX_HOME = savedCodexHome + } if (savedProcessPlatform) { Object.defineProperty(process, 'platform', savedProcessPlatform) } diff --git a/src/main/ipc/pty-ipc-producer-flow-control.test.ts b/src/main/ipc/pty-ipc-producer-flow-control.test.ts index 8b97751dba74..af4b88b19f44 100644 --- a/src/main/ipc/pty-ipc-producer-flow-control.test.ts +++ b/src/main/ipc/pty-ipc-producer-flow-control.test.ts @@ -15,7 +15,6 @@ import { vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) diff --git a/src/main/ipc/pty-ipc-suite-environment.ts b/src/main/ipc/pty-ipc-suite-environment.ts index c7256a57f3e8..f1bb2ac61965 100644 --- a/src/main/ipc/pty-ipc-suite-environment.ts +++ b/src/main/ipc/pty-ipc-suite-environment.ts @@ -1,3 +1,4 @@ +import { finishMockNativePtyProcesses } from './pty-ipc-native-runtime-mock' import { afterEach, beforeEach, vi } from 'vitest' import * as electron from 'electron' import { join } from 'node:path' @@ -47,10 +48,8 @@ import { } from './pty-ipc-mock-registry' import { makeDisposable } from './pty-ipc-test-constants' import { createPtyIpcProcessEnvScope } from './pty-ipc-process-env-scope' -import { - LocalPtyProvider, - _resetLocalPtyProviderStateForTest -} from '../providers/local-pty-provider' +import { PtyIpcDaemonTestProvider } from './pty-ipc-daemon-test-provider' +import { createUnavailablePtyProvider } from '../providers/unavailable-pty-provider' import { setLocalPtyProvider, unregisterSshPtyProvider } from './pty' import { _resetHiddenRendererPtyDeliveryGateForTest } from './pty-hidden-delivery-gate' import { __resetShellStartupEnvCache } from '../pty/shell-startup-env' @@ -108,6 +107,7 @@ export function createPtyIpcSuiteEnvironment(): PtyIpcSuiteEnvironment { } } const envScope = createPtyIpcProcessEnvScope() + let provider: PtyIpcDaemonTestProvider beforeEach(() => { // Why here: pty.ts registers against injected surfaces now, so the mocked ipcMain @@ -280,12 +280,18 @@ export function createPtyIpcSuiteEnvironment(): PtyIpcSuiteEnvironment { resize: vi.fn(), kill: vi.fn(), process: 'zsh', - pid: 12345 + pid: 999_999_999, + pause: vi.fn(), + resume: vi.fn(), + destroy: vi.fn() }) + provider = new PtyIpcDaemonTestProvider() + setLocalPtyProvider(provider) }) - afterEach(() => { - _resetLocalPtyProviderStateForTest() + afterEach(async () => { + finishMockNativePtyProcesses() + await provider.disposeHost() _resetWslCachesForTests() vi.useRealTimers() // Why: sshProviders is module-level state; a leftover id leaks into later tests (pty:listSessions sweeps every provider). @@ -303,7 +309,7 @@ export function createPtyIpcSuiteEnvironment(): PtyIpcSuiteEnvironment { ]) { unregisterSshPtyProvider(leakedConnectionId) } - setLocalPtyProvider(new LocalPtyProvider()) + setLocalPtyProvider(createUnavailablePtyProvider()) envScope.restoreProcessEnv() }) diff --git a/src/main/ipc/pty-listener-teardown-and-orphans.test.ts b/src/main/ipc/pty-listener-teardown-and-orphans.test.ts index 7e4178f35d3e..a7e4ff39cfdc 100644 --- a/src/main/ipc/pty-listener-teardown-and-orphans.test.ts +++ b/src/main/ipc/pty-listener-teardown-and-orphans.test.ts @@ -12,11 +12,10 @@ import * as livePtyGate from '../claude-accounts/live-pty-gate' import { registerPtyHandlers, setLocalPtyProvider, getLocalPtyProvider } from './pty' import { join } from 'node:path' // Why resolved rather than hardcoded: the wrapper tree is content-addressed. -import { getShellReadyWrapperRoot } from '../providers/local-pty-shell-ready-wrapper-root' +import { getShellReadyWrapperRoot } from '../daemon/shell-ready' vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) @@ -58,6 +57,22 @@ vi.mock('../codex/codex-state-db-backfill-recovery', () => import('./pty-ipc-mock-registry').then((m) => m.codexBackfillRecoveryModuleMock()) ) +function createExitListeners() { + const listeners = new Set<(info: { exitCode: number }) => void>() + return { + subscribe: vi.fn((listener: (info: { exitCode: number }) => void) => { + listeners.add(listener) + return { dispose: vi.fn(() => listeners.delete(listener)) } + }), + emit: (info: { exitCode: number }) => { + for (const listener of listeners) { + listener(info) + } + }, + activeCount: () => listeners.size + } +} + describe('registerPtyHandlers', () => { const { handlers, mainWindow } = setupPtyIpcSuite() @@ -96,7 +111,7 @@ describe('registerPtyHandlers', () => { }) ) expect(warnSpy).toHaveBeenCalledWith( - expect.stringContaining('Primary shell "/opt/homebrew/bin/bash" failed') + expect.stringContaining('Preferred shell "/opt/homebrew/bin/bash" is unavailable') ) } finally { warnSpy.mockRestore() @@ -139,16 +154,12 @@ describe('registerPtyHandlers', () => { }) it('retains PTY listeners until physical exit after manual kill IPC', async () => { const onDataDisposable = makeDisposable() - const onExitDisposable = makeDisposable() - let exitCb: ((info: { exitCode: number }) => void) | undefined + const exitListeners = createExitListeners() // Why: hold a stable ref to the kill spy — destroyPtyProcess reassigns proc.kill to a no-op (docs/fix-pty-fd-leak.md), so reading proc.kill.mock later would crash. const killSpy = vi.fn() const proc = { onData: vi.fn(() => onDataDisposable), - onExit: vi.fn((cb: (info: { exitCode: number }) => void) => { - exitCb = cb - return onExitDisposable - }), + onExit: exitListeners.subscribe, write: vi.fn(), resize: vi.fn(), kill: killSpy, @@ -179,29 +190,25 @@ describe('registerPtyHandlers', () => { await vi.waitFor(() => expect(finishSnapshot).toBeTypeOf('function')) expect(killSpy).not.toHaveBeenCalled() expect(onDataDisposable.dispose).not.toHaveBeenCalled() - expect(onExitDisposable.dispose).not.toHaveBeenCalled() + expect(exitListeners.activeCount()).toBe(1) finishSnapshot?.() await vi.waitFor(() => expect(killSpy).toHaveBeenCalledTimes(1)) expect(onDataDisposable.dispose).not.toHaveBeenCalled() - expect(onExitDisposable.dispose).not.toHaveBeenCalled() + expect(exitListeners.activeCount()).toBe(1) - exitCb?.({ exitCode: -1 }) + exitListeners.emit({ exitCode: -1 }) await killPromise expect(onDataDisposable.dispose).toHaveBeenCalledTimes(1) - expect(onExitDisposable.dispose).toHaveBeenCalledTimes(1) + expect(exitListeners.activeCount()).toBe(0) }) it('retains PTY listeners until physical exit after runtime controller kill', async () => { const onDataDisposable = makeDisposable() - const onExitDisposable = makeDisposable() - let exitCb: ((info: { exitCode: number }) => void) | undefined + const exitListeners = createExitListeners() const killSpy = vi.fn() const proc = { onData: vi.fn(() => onDataDisposable), - onExit: vi.fn((cb: (info: { exitCode: number }) => void) => { - exitCb = cb - return onExitDisposable - }), + onExit: exitListeners.subscribe, write: vi.fn(), resize: vi.fn(), kill: killSpy, @@ -209,6 +216,10 @@ describe('registerPtyHandlers', () => { pid: 12345 } const runtime = { + createPreAllocatedTerminalHandle: vi.fn(() => 'term_test'), + registerPreAllocatedHandleForPty: vi.fn(), + registerPty: vi.fn(), + markPtyStopRequested: vi.fn(), setPtyController: vi.fn(), noteTerminalSpawnCommand: vi.fn(), onPtySpawned: vi.fn(), @@ -230,23 +241,19 @@ describe('registerPtyHandlers', () => { expect(runtimeController.kill(spawnResult.id)).toBe(true) await vi.waitFor(() => expect(killSpy).toHaveBeenCalledTimes(1)) expect(onDataDisposable.dispose).not.toHaveBeenCalled() - expect(onExitDisposable.dispose).not.toHaveBeenCalled() + expect(exitListeners.activeCount()).toBe(1) - exitCb?.({ exitCode: -1 }) - await vi.waitFor(() => expect(onExitDisposable.dispose).toHaveBeenCalledTimes(1)) + exitListeners.emit({ exitCode: -1 }) + await vi.waitFor(() => expect(exitListeners.activeCount()).toBe(0)) expect(onDataDisposable.dispose).toHaveBeenCalledTimes(1) }) - it('retains the PTY exit listener through did-finish-load orphan cleanup', async () => { + it('retains daemon PTY listeners across renderer reload until physical exit', async () => { const onDataDisposable = makeDisposable() - const onExitDisposable = makeDisposable() - let exitCb: ((info: { exitCode: number }) => void) | undefined + const exitListeners = createExitListeners() const killSpy = vi.fn() const proc = { onData: vi.fn(() => onDataDisposable), - onExit: vi.fn((cb: (info: { exitCode: number }) => void) => { - exitCb = cb - return onExitDisposable - }), + onExit: exitListeners.subscribe, write: vi.fn(), resize: vi.fn(), kill: killSpy, @@ -254,6 +261,10 @@ describe('registerPtyHandlers', () => { pid: 12345 } const runtime = { + createPreAllocatedTerminalHandle: vi.fn(() => 'term_test'), + registerPreAllocatedHandleForPty: vi.fn(), + registerPty: vi.fn(), + markPtyStopRequested: vi.fn(), setPtyController: vi.fn(), noteTerminalSpawnCommand: vi.fn(), onPtySpawned: vi.fn(), @@ -276,19 +287,18 @@ describe('registerPtyHandlers', () => { } await handlers.get('pty:spawn')!(null, { cols: 80, rows: 24 }) - // First load after spawn only advances generation; the second sees this PTY as from a prior load and kills it as orphaned. + // Renderer reload does not transfer termination authority from the daemon. didFinishLoad() didFinishLoad() - expect(onDataDisposable.dispose.mock.invocationCallOrder[0]).toBeLessThan( - killSpy.mock.invocationCallOrder[0] - ) - expect(onExitDisposable.dispose).not.toHaveBeenCalled() + expect(killSpy).not.toHaveBeenCalled() + expect(onDataDisposable.dispose).not.toHaveBeenCalled() + expect(exitListeners.activeCount()).toBe(1) - exitCb?.({ exitCode: -1 }) - expect(onExitDisposable.dispose).toHaveBeenCalledTimes(1) + exitListeners.emit({ exitCode: -1 }) + expect(exitListeners.activeCount()).toBe(0) }) - it('removes the previous orphan-cleanup listener from its original webContents', () => { + it('removes the previous renderer-reset listener from its original webContents', () => { const firstWindow = { isDestroyed: () => false, isFocused: () => true, @@ -313,11 +323,11 @@ describe('registerPtyHandlers', () => { } registerPtyHandlers(firstWindow as never) - // Two listeners on the first (LocalPtyProvider) window: the renderer-gate reset and the orphan cleanup. + // The daemon owns terminal lifetime; only renderer delivery resets on load. const firstWindowLoadHandlers = firstWindow.webContents.on.mock.calls.filter( ([eventName]) => eventName === 'did-finish-load' ) - expect(firstWindowLoadHandlers).toHaveLength(2) + expect(firstWindowLoadHandlers).toHaveLength(1) setLocalPtyProvider({ spawn: vi.fn(), @@ -339,7 +349,7 @@ describe('registerPtyHandlers', () => { handler ) } - // The non-Local provider keeps orphan cleanup off the second window — only the renderer-gate reset listener remains. + // Re-registering keeps exactly one delivery-reset listener. expect( secondWindow.webContents.on.mock.calls.filter( ([eventName]) => eventName === 'did-finish-load' @@ -359,6 +369,10 @@ describe('registerPtyHandlers', () => { pid: 12345 } const runtime = { + createPreAllocatedTerminalHandle: vi.fn(() => 'term_test'), + registerPreAllocatedHandleForPty: vi.fn(), + registerPty: vi.fn(), + markPtyStopRequested: vi.fn(), setPtyController: vi.fn(), onPtySpawned: vi.fn(), onPtyData: vi.fn(), @@ -402,8 +416,7 @@ describe('registerPtyHandlers', () => { markClaudePtyExitedSpy.mockRestore() }) - // Why: guard against over-suppression — with no recovery reload in flight the sweep MUST still reclaim genuinely orphaned local PTYs. - it('still sweeps orphaned local PTYs when no recovery reload is in flight', async () => { + it('preserves daemon terminals across ordinary reload and reports subsequent physical exit', async () => { let exitCb: ((info: { exitCode: number }) => void) | undefined const killSpy = vi.fn(() => { queueMicrotask(() => exitCb?.({ exitCode: -1 })) @@ -421,6 +434,10 @@ describe('registerPtyHandlers', () => { pid: 12345 } const runtime = { + createPreAllocatedTerminalHandle: vi.fn(() => 'term_test'), + registerPreAllocatedHandleForPty: vi.fn(), + registerPty: vi.fn(), + markPtyStopRequested: vi.fn(), setPtyController: vi.fn(), onPtySpawned: vi.fn(), onPtyData: vi.fn(), @@ -455,7 +472,12 @@ describe('registerPtyHandlers', () => { didFinishLoad() await Promise.resolve() - expect(killSpy).toHaveBeenCalled() + expect(killSpy).not.toHaveBeenCalled() + expect(runtime.onPtyExit).not.toHaveBeenCalled() + expect( + (await getLocalPtyProvider().listProcesses()).some((info) => info.id === spawnResult.id) + ).toBe(true) + exitCb?.({ exitCode: -1 }) expect(runtime.onPtyExit).toHaveBeenCalledWith(spawnResult.id, -1, spawnResult.incarnationId, { providerExitObserved: true, cause: { kind: 'unknown', reason: 'stop_unverified' } @@ -468,6 +490,10 @@ describe('registerPtyHandlers', () => { const killSpyA = vi.fn() const killSpyB = vi.fn() const runtime = { + createPreAllocatedTerminalHandle: vi.fn(() => 'term_test'), + registerPreAllocatedHandleForPty: vi.fn(), + registerPty: vi.fn(), + markPtyStopRequested: vi.fn(), setPtyController: vi.fn(), onPtySpawned: vi.fn(), onPtyData: vi.fn(), @@ -549,7 +575,7 @@ describe('registerPtyHandlers', () => { })) as { id: string } await expect(handlers.get('pty:kill')!(null, { id: spawnResult.id })).rejects.toThrow( - 'already dead' + 'kill ESRCH' ) expect((await getLocalPtyProvider().listProcesses()).map(({ id }) => id)).toContain( diff --git a/src/main/ipc/pty-login-shell-startup-commands.test.ts b/src/main/ipc/pty-login-shell-startup-commands.test.ts index 7ccb0386b9fe..17297c43688b 100644 --- a/src/main/ipc/pty-login-shell-startup-commands.test.ts +++ b/src/main/ipc/pty-login-shell-startup-commands.test.ts @@ -12,11 +12,10 @@ import { registerPtyHandlers } from './pty' import { join } from 'node:path' import { POSIX_SHELL_STARTUP_COMMAND_ENV } from '../pty/posix-shell-startup-command' // Why resolved rather than hardcoded: the wrapper tree is content-addressed. -import { getShellReadyWrapperRoot } from '../providers/local-pty-shell-ready-wrapper-root' +import { getShellReadyWrapperRoot } from '../daemon/shell-ready' vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) @@ -227,10 +226,10 @@ describe('registerPtyHandlers', () => { expect(mockProc.proc.write).not.toHaveBeenCalled() mockProc.emitData('last login: today\r\n') - vi.runOnlyPendingTimers() + vi.advanceTimersByTime(1) expect(mockProc.proc.write).not.toHaveBeenCalled() - mockProc.emitData('\x1b]133;A\x07% ') + mockProc.emitData('\x1b]777;orca-shell-ready\x07% ') await Promise.resolve() vi.runAllTimers() expect(mockProc.proc.write).toHaveBeenCalledWith('claude\n') @@ -240,7 +239,7 @@ describe('registerPtyHandlers', () => { } ) posixOnlyIt( - 'uses the no-marker wrapper for Codex startup commands without a PTY write', + 'delivers Codex once after the daemon observes the zsh line-editor marker', async () => { vi.useFakeTimers() const mockProc = createMockProc() @@ -256,18 +255,18 @@ describe('registerPtyHandlers', () => { }) const [, , options] = spawnMock.mock.calls[0]! - expect(options.env.ORCA_SHELL_FEATURES).not.toContain('ready') - expect(options.env[POSIX_SHELL_STARTUP_COMMAND_ENV]).toBe('codex') + expect(options.env.ORCA_SHELL_FEATURES).toContain('ready') + expect(options.env[POSIX_SHELL_STARTUP_COMMAND_ENV]).toBeUndefined() await Promise.resolve() vi.advanceTimersByTime(49) await Promise.resolve() expect(mockProc.proc.write).not.toHaveBeenCalled() - vi.advanceTimersByTime(1) - await Promise.resolve() + mockProc.emitData('\x1b]777;orca-shell-ready\x07% ') + expect(mockProc.proc.write).toHaveBeenCalledExactlyOnceWith('codex\n') vi.runAllTimers() - expect(mockProc.proc.write).not.toHaveBeenCalled() + expect(mockProc.proc.write).toHaveBeenCalledOnce() } finally { vi.useRealTimers() } @@ -290,7 +289,7 @@ describe('registerPtyHandlers', () => { const [, , options] = spawnMock.mock.calls[0]! expect(options.env.ORCA_SHELL_FEATURES).toContain('ready') - expect(options.env[POSIX_SHELL_STARTUP_COMMAND_ENV]).toBe("codex 'linked issue context'") + expect(options.env[POSIX_SHELL_STARTUP_COMMAND_ENV]).toBeUndefined() expect(mockProc.proc.write).not.toHaveBeenCalled() mockProc.emitData('last login: today\r\n') @@ -302,17 +301,17 @@ describe('registerPtyHandlers', () => { await Promise.resolve() vi.advanceTimersByTime(50) await Promise.resolve() - expect(mockProc.proc.write).not.toHaveBeenCalled() + expect(mockProc.proc.write).toHaveBeenCalledExactlyOnceWith("codex 'linked issue context'\n") vi.advanceTimersByTime(150) await Promise.resolve() - expect(mockProc.proc.write).not.toHaveBeenCalled() + expect(mockProc.proc.write).toHaveBeenCalledExactlyOnceWith("codex 'linked issue context'\n") } finally { vi.useRealTimers() } }) posixOnlyIt( - 'uses the short settle path for delivery-hinted Codex when prompt follows the marker', + 'delivers delivery-hinted Codex once when the zsh marker and prompt arrive together', async () => { vi.useFakeTimers() const mockProc = createMockProc() @@ -329,17 +328,21 @@ describe('registerPtyHandlers', () => { }) const [, , options] = spawnMock.mock.calls[0]! - expect(options.env[POSIX_SHELL_STARTUP_COMMAND_ENV]).toBe("codex 'linked issue context'") + expect(options.env[POSIX_SHELL_STARTUP_COMMAND_ENV]).toBeUndefined() mockProc.emitData('\x1b]777;orca-shell-ready\x07\r\nuser@host % ') await Promise.resolve() vi.advanceTimersByTime(29) await Promise.resolve() - expect(mockProc.proc.write).not.toHaveBeenCalled() + expect(mockProc.proc.write).toHaveBeenCalledExactlyOnceWith( + "codex 'linked issue context'\n" + ) vi.advanceTimersByTime(1) await Promise.resolve() - expect(mockProc.proc.write).not.toHaveBeenCalled() + expect(mockProc.proc.write).toHaveBeenCalledExactlyOnceWith( + "codex 'linked issue context'\n" + ) } finally { vi.useRealTimers() } @@ -361,16 +364,16 @@ describe('registerPtyHandlers', () => { const [, , options] = spawnMock.mock.calls[0]! expect(options.env.ORCA_SHELL_FEATURES).toContain('ready') - expect(options.env[POSIX_SHELL_STARTUP_COMMAND_ENV]).toBe( - "codex --prefill 'linked issue context'" - ) + expect(options.env[POSIX_SHELL_STARTUP_COMMAND_ENV]).toBeUndefined() expect(mockProc.proc.write).not.toHaveBeenCalled() mockProc.emitData('\x1b]777;orca-shell-ready\x07') await Promise.resolve() vi.runAllTimers() await Promise.resolve() - expect(mockProc.proc.write).not.toHaveBeenCalled() + expect(mockProc.proc.write).toHaveBeenCalledExactlyOnceWith( + "codex --prefill 'linked issue context'\n" + ) } finally { vi.useRealTimers() } diff --git a/src/main/ipc/pty-management.test.ts b/src/main/ipc/pty-management.test.ts index c279a239f2ad..f8d7453b8a0d 100644 --- a/src/main/ipc/pty-management.test.ts +++ b/src/main/ipc/pty-management.test.ts @@ -13,6 +13,7 @@ const { removeHandlerMock, getDaemonProviderMock, restartDaemonMock, + retryDaemonPtyProviderMock, getCurrentDaemonMacTccAttributionHealthMock, getDaemonFolderAccessMismatchMock, refreshDaemonFolderAccessProbeMock, @@ -22,6 +23,7 @@ const { removeHandlerMock: vi.fn(), getDaemonProviderMock: vi.fn(), restartDaemonMock: vi.fn(), + retryDaemonPtyProviderMock: vi.fn(), getCurrentDaemonMacTccAttributionHealthMock: vi.fn(async () => 'unknown'), getDaemonFolderAccessMismatchMock: vi.fn< () => { daemonScope: string; cwdClass: string; freshDaemonAccess: string } | null @@ -48,6 +50,7 @@ vi.mock('../daemon/daemon-folder-access-reset', () => ({ vi.mock('../daemon/daemon-init', () => ({ getDaemonProvider: getDaemonProviderMock, restartDaemon: restartDaemonMock, + retryDaemonPtyProvider: retryDaemonPtyProviderMock, getCurrentDaemonMacTccAttributionHealth: getCurrentDaemonMacTccAttributionHealthMock })) @@ -62,8 +65,14 @@ vi.mock('../daemon/daemon-pty-router', () => { class DaemonPtyRouter { private allAdapters: unknown[] private current: unknown - constructor(opts: { current: unknown; legacy: unknown[] }) { + freshSpawnsUnavailable = false + async recoverFreshSpawnRouting(): Promise { + this.freshSpawnsUnavailable = false + return true + } + constructor(opts: { current: unknown; legacy: unknown[]; probeFreshSpawn?: unknown }) { this.current = opts.current + this.freshSpawnsUnavailable = Boolean(opts.probeFreshSpawn) this.allAdapters = [opts.current, ...opts.legacy] } getAllAdapters() { @@ -80,38 +89,6 @@ vi.mock('../daemon/daemon-pty-router', () => { return { DaemonPtyRouter } }) -// Why: the handler also branches on `provider instanceof DegradedDaemonPtyProvider` -// (for getAllAdapters) and reports `degraded` from it. The real constructor -// subscribes to adapter events, so keep only the accessors pty-management uses. -vi.mock('../daemon/degraded-daemon-pty-provider', () => { - class DegradedDaemonPtyProvider { - private allAdapters: unknown[] - private current: unknown - private routesFreshToFallback = true - constructor(opts: { current: unknown; legacy: unknown[] }) { - this.current = opts.current - this.allAdapters = [opts.current, ...opts.legacy] - } - getCurrentAdapter() { - return this.current - } - getLegacyAdapters() { - return this.allAdapters.slice(1) - } - get routesFreshSpawnsToLocalProvider(): true | undefined { - return this.routesFreshToFallback ? true : undefined - } - async recoverFreshSpawnRouting(): Promise { - this.routesFreshToFallback = false - return true - } - getAllAdapters() { - return this.allAdapters - } - } - return { DegradedDaemonPtyProvider } -}) - type HandlerMap = Record unknown> function buildHandlerMap(): HandlerMap { @@ -179,11 +156,11 @@ async function makeRouter(current: MockAdapter, legacy: MockAdapter[] = []) { } async function makeDegradedProvider(current: MockAdapter, legacy: MockAdapter[] = []) { - const { DegradedDaemonPtyProvider } = await import('../daemon/degraded-daemon-pty-provider') - return new DegradedDaemonPtyProvider({ + const { DaemonPtyRouter } = await import('../daemon/daemon-pty-router') + return new DaemonPtyRouter({ current: current as never, legacy: legacy as never, - fallback: undefined as never + probeFreshSpawn: async () => false }) } @@ -191,6 +168,7 @@ describe('pty:management IPC handlers', () => { beforeEach(() => { getDaemonProviderMock.mockReset() restartDaemonMock.mockReset() + retryDaemonPtyProviderMock.mockReset() getCurrentDaemonMacTccAttributionHealthMock.mockReset() getCurrentDaemonMacTccAttributionHealthMock.mockResolvedValue('unknown') getDaemonFolderAccessMismatchMock.mockReset().mockReturnValue(null) @@ -638,6 +616,28 @@ describe('pty:management IPC handlers', () => { }) }) + describe('retry', () => { + it('retries initialization without stopping sessions', async () => { + retryDaemonPtyProviderMock.mockResolvedValue(undefined) + const { registerDaemonManagementHandlers } = await importFresh() + registerDaemonManagementHandlers() + await expect(buildHandlerMap()['pty:management:retry']({})).resolves.toEqual({ + success: true + }) + expect(retryDaemonPtyProviderMock).toHaveBeenCalledOnce() + expect(restartDaemonMock).not.toHaveBeenCalled() + }) + it('reports failure without invoking destructive restart', async () => { + retryDaemonPtyProviderMock.mockRejectedValue(new Error('service unavailable')) + const { registerDaemonManagementHandlers } = await importFresh() + registerDaemonManagementHandlers() + await expect(buildHandlerMap()['pty:management:retry']({})).resolves.toEqual({ + success: false + }) + expect(restartDaemonMock).not.toHaveBeenCalled() + }) + }) + describe('restart', () => { it('delegates to restartDaemon and reports success', async () => { restartDaemonMock.mockResolvedValue({ killedCount: 2 }) diff --git a/src/main/ipc/pty-management.ts b/src/main/ipc/pty-management.ts index 34f990ac1617..d4cacdca2fc0 100644 --- a/src/main/ipc/pty-management.ts +++ b/src/main/ipc/pty-management.ts @@ -1,11 +1,11 @@ import { ipcMain } from 'electron' import { DaemonPtyRouter } from '../daemon/daemon-pty-router' -import { DegradedDaemonPtyProvider } from '../daemon/degraded-daemon-pty-provider' import type { DaemonPtyAdapter } from '../daemon/daemon-pty-adapter' import { getCurrentDaemonMacTccAttributionHealth, getDaemonProvider, - restartDaemon + restartDaemon, + retryDaemonPtyProvider } from '../daemon/daemon-init' import { getCurrentDaemonAdapter } from '../daemon/daemon-provider-routing' import { @@ -34,7 +34,7 @@ function getDaemonAdapters(): DaemonPtyAdapter[] { if (!provider) { return [] } - if (provider instanceof DaemonPtyRouter || provider instanceof DegradedDaemonPtyProvider) { + if (provider instanceof DaemonPtyRouter) { return [...provider.getAllAdapters()] } return [provider] @@ -43,10 +43,7 @@ function getDaemonAdapters(): DaemonPtyAdapter[] { // Why: surface degraded mode (daemon alive but cannot spawn fresh PTYs) so the UI can warn new terminals lack persistence. function isDaemonDegraded(): boolean { const provider = getDaemonProvider() - return ( - provider instanceof DegradedDaemonPtyProvider && - provider.routesFreshSpawnsToLocalProvider === true - ) + return provider instanceof DaemonPtyRouter && provider.freshSpawnsUnavailable } // Why the current adapter only: evidence is keyed to the daemon now spawning terminals, so a @@ -74,6 +71,7 @@ export function registerDaemonManagementHandlers(): void { ipcMain.removeHandler('pty:management:killAll') ipcMain.removeHandler('pty:management:killOne') ipcMain.removeHandler('pty:management:restart') + ipcMain.removeHandler('pty:management:retry') ipcMain.removeHandler('pty:management:macTccAttribution') ipcMain.removeHandler('pty:management:resetFolderAccess') @@ -113,9 +111,13 @@ export function registerDaemonManagementHandlers(): void { ipcMain.handle( 'pty:management:listSessions', - async (): Promise<{ sessions: DaemonSessionInfo[]; degraded: boolean }> => { + async (): Promise<{ + sessions: DaemonSessionInfo[] + degraded: boolean + unavailable: boolean + }> => { const sessions = await collectSessions(getDaemonAdapters()) - return { sessions, degraded: isDaemonDegraded() } + return { sessions, degraded: isDaemonDegraded(), unavailable: getDaemonProvider() === null } } ) @@ -203,6 +205,16 @@ export function registerDaemonManagementHandlers(): void { } ) + ipcMain.handle('pty:management:retry', async (): Promise<{ success: boolean }> => { + try { + await retryDaemonPtyProvider() + return { success: true } + } catch (error) { + console.warn('[pty:management] retry failed', error) + return { success: false } + } + }) + ipcMain.handle('pty:management:restart', async (): Promise<{ success: boolean }> => { try { await restartDaemon() diff --git a/src/main/ipc/pty-output-batching-drain.test.ts b/src/main/ipc/pty-output-batching-drain.test.ts index 2a515e2634b8..b7eeaa063eb5 100644 --- a/src/main/ipc/pty-output-batching-drain.test.ts +++ b/src/main/ipc/pty-output-batching-drain.test.ts @@ -5,7 +5,6 @@ import { registerPtyHandlers, setLocalPtyProvider } from './pty' vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) diff --git a/src/main/ipc/pty-output-drain-rounds.test.ts b/src/main/ipc/pty-output-drain-rounds.test.ts index 29e0a7ffeba2..0636f45d7b9b 100644 --- a/src/main/ipc/pty-output-drain-rounds.test.ts +++ b/src/main/ipc/pty-output-drain-rounds.test.ts @@ -9,7 +9,6 @@ import { vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) @@ -305,7 +304,8 @@ describe('registerPtyHandlers', () => { expect(mainWindow.webContents.send).toHaveBeenCalledTimes(32) vi.advanceTimersByTime(1) expect(mainWindow.webContents.send).toHaveBeenCalledTimes(32) - expect(vi.getTimerCount()).toBe(0) + // The daemon retains its producer-pause failsafe. + expect(vi.getTimerCount()).toBe(1) expect(getPtyRendererDeliveryDebugSnapshot()).toMatchObject({ pendingPtyCount: 1, pendingChars: 88 * 1024, diff --git a/src/main/ipc/pty-pane-claim-arbitration.test.ts b/src/main/ipc/pty-pane-claim-arbitration.test.ts index 86fe3381f2bf..e43dc6143e3c 100644 --- a/src/main/ipc/pty-pane-claim-arbitration.test.ts +++ b/src/main/ipc/pty-pane-claim-arbitration.test.ts @@ -12,7 +12,6 @@ import { vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) @@ -70,17 +69,6 @@ describe('registerPtyHandlers', () => { getSize(ptyId: string): { cols: number; rows: number } | null } let controller: RuntimeResizeController | null = null - const proc = { - onData: vi.fn(), - onExit: vi.fn(), - write: vi.fn(), - resize: vi.fn(() => { - throw new Error('resize failed') - }), - kill: vi.fn(), - process: 'zsh', - pid: 12345 - } const runtime = { setPtyController: vi.fn((value) => { controller = value @@ -94,7 +82,11 @@ describe('registerPtyHandlers', () => { onPtyExit: vi.fn(), onPtyData: vi.fn() } - spawnMock.mockReturnValue(proc) + const provider = createAgentClaimProvider({ spawn: vi.fn(async () => ({ id: 'resize-pty' })) }) + provider.resize.mockImplementation(() => { + throw new Error('resize failed') + }) + setLocalPtyProvider(provider as never) registerPtyHandlers(mainWindow as never, runtime as never) const resizeController = controller as unknown as RuntimeResizeController @@ -378,7 +370,6 @@ describe('registerPtyHandlers', () => { spawn: physicalSpawn, authoritativeOwnerListings: false }) - Object.assign(provider, { routesFreshSpawnsToLocalProvider: true }) setLocalPtyProvider(provider as never) registerPtyHandlers(mainWindow as never, runtime) const controller = ( diff --git a/src/main/ipc/pty-pane-materialization-race.test.ts b/src/main/ipc/pty-pane-materialization-race.test.ts index bdfd41181823..5d22ab0518fb 100644 --- a/src/main/ipc/pty-pane-materialization-race.test.ts +++ b/src/main/ipc/pty-pane-materialization-race.test.ts @@ -10,7 +10,6 @@ import { registerPtyHandlers, getPtyIdForPaneKey, setLocalPtyProvider } from './ vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) diff --git a/src/main/ipc/pty-pane-reservation-settlement.test.ts b/src/main/ipc/pty-pane-reservation-settlement.test.ts index 30ef457a81d3..f06c4942b714 100644 --- a/src/main/ipc/pty-pane-reservation-settlement.test.ts +++ b/src/main/ipc/pty-pane-reservation-settlement.test.ts @@ -16,7 +16,6 @@ import { vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) diff --git a/src/main/ipc/pty-pane-restart-replace.test.ts b/src/main/ipc/pty-pane-restart-replace.test.ts index a1ced7904f8e..1d5e8276e193 100644 --- a/src/main/ipc/pty-pane-restart-replace.test.ts +++ b/src/main/ipc/pty-pane-restart-replace.test.ts @@ -6,7 +6,6 @@ import { registerPtyHandlers, setLocalPtyProvider } from './pty' vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) diff --git a/src/main/ipc/pty-pending-output-cap.test.ts b/src/main/ipc/pty-pending-output-cap.test.ts index 236293b01b6f..c15e023bd658 100644 --- a/src/main/ipc/pty-pending-output-cap.test.ts +++ b/src/main/ipc/pty-pending-output-cap.test.ts @@ -6,7 +6,6 @@ import { registerPtyHandlers, getPtyRendererDeliveryDebugSnapshot } from './pty' vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) diff --git a/src/main/ipc/pty-persisted-incarnation-repair.test.ts b/src/main/ipc/pty-persisted-incarnation-repair.test.ts index c057d6ec5d21..dd436df3d72e 100644 --- a/src/main/ipc/pty-persisted-incarnation-repair.test.ts +++ b/src/main/ipc/pty-persisted-incarnation-repair.test.ts @@ -8,7 +8,6 @@ import { registerPtyHandlers, clearProviderPtyState, setLocalPtyProvider } from vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) diff --git a/src/main/ipc/pty-renderer-inflight-credit.test.ts b/src/main/ipc/pty-renderer-inflight-credit.test.ts index 6629b7d3a09a..179de43a704c 100644 --- a/src/main/ipc/pty-renderer-inflight-credit.test.ts +++ b/src/main/ipc/pty-renderer-inflight-credit.test.ts @@ -5,7 +5,6 @@ import { registerPtyHandlers, setLocalPtyProvider } from './pty' vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) @@ -150,7 +149,7 @@ describe('registerPtyHandlers', () => { vi.advanceTimersByTime(1) } expect(mainWindow.webContents.send).toHaveBeenCalledTimes(512) - expect(vi.getTimerCount()).toBe(0) + expect(vi.getTimerCount()).toBe(16) writeListener(mainWindowIpcEvent, { id: interactiveSpawn.id, @@ -254,7 +253,7 @@ describe('registerPtyHandlers', () => { vi.advanceTimersByTime(1) } expect(getPtyDataSendCalls()).toHaveLength(512) - expect(vi.getTimerCount()).toBe(0) + expect(vi.getTimerCount()).toBe(17) mainWindow.webContents.send.mockClear() procs[0]!.emitExit(0) diff --git a/src/main/ipc/pty-renderer-lifecycle-delivery-reset.test.ts b/src/main/ipc/pty-renderer-lifecycle-delivery-reset.test.ts index 544795e1f67f..fed11a81466b 100644 --- a/src/main/ipc/pty-renderer-lifecycle-delivery-reset.test.ts +++ b/src/main/ipc/pty-renderer-lifecycle-delivery-reset.test.ts @@ -5,7 +5,6 @@ import { registerPtyHandlers, getPtyRendererDeliveryDebugSnapshot } from './pty' vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) @@ -199,7 +198,8 @@ describe('registerPtyHandlers', () => { expect(mainWindow.webContents.send).toHaveBeenCalledTimes(32) vi.advanceTimersByTime(1) expect(mainWindow.webContents.send).toHaveBeenCalledTimes(32) - expect(vi.getTimerCount()).toBe(0) + // The daemon retains its producer-pause failsafe independently of renderer scheduling. + expect(vi.getTimerCount()).toBe(1) expect(getPtyRendererDeliveryDebugSnapshot()).toMatchObject({ rendererInFlightChars: 512 * 1024, pendingChars: 88 * 1024, diff --git a/src/main/ipc/pty-renderer-liveness-guard.test.ts b/src/main/ipc/pty-renderer-liveness-guard.test.ts index 08a925067d87..89a56c53e118 100644 --- a/src/main/ipc/pty-renderer-liveness-guard.test.ts +++ b/src/main/ipc/pty-renderer-liveness-guard.test.ts @@ -6,7 +6,6 @@ import { registerPtyHandlers } from './pty' vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) diff --git a/src/main/ipc/pty-renderer-send-failure-recovery.test.ts b/src/main/ipc/pty-renderer-send-failure-recovery.test.ts index 423288446f9a..a0c7a12e4970 100644 --- a/src/main/ipc/pty-renderer-send-failure-recovery.test.ts +++ b/src/main/ipc/pty-renderer-send-failure-recovery.test.ts @@ -5,7 +5,6 @@ import { registerPtyHandlers, getPtyRendererDeliveryDebugSnapshot } from './pty' vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) diff --git a/src/main/ipc/pty-restore-record-seeding.test.ts b/src/main/ipc/pty-restore-record-seeding.test.ts index 0f1646e7812d..1470dc544b8e 100644 --- a/src/main/ipc/pty-restore-record-seeding.test.ts +++ b/src/main/ipc/pty-restore-record-seeding.test.ts @@ -24,7 +24,6 @@ import { vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) diff --git a/src/main/ipc/pty-restored-appimage-cli-shim-refresh.test.ts b/src/main/ipc/pty-restored-appimage-cli-shim-refresh.test.ts index d42103dc5182..6e3ad9de0409 100644 --- a/src/main/ipc/pty-restored-appimage-cli-shim-refresh.test.ts +++ b/src/main/ipc/pty-restored-appimage-cli-shim-refresh.test.ts @@ -4,7 +4,6 @@ import { linuxCliShimMock } from './pty-ipc-mock-registry' vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) diff --git a/src/main/ipc/pty-runtime-hidden-at-spawn-mark.test.ts b/src/main/ipc/pty-runtime-hidden-at-spawn-mark.test.ts index b4deb243b0b3..71d2231d651b 100644 --- a/src/main/ipc/pty-runtime-hidden-at-spawn-mark.test.ts +++ b/src/main/ipc/pty-runtime-hidden-at-spawn-mark.test.ts @@ -7,7 +7,6 @@ import { registerPtyHandlers } from './pty' vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) diff --git a/src/main/ipc/pty-runtime-kill-and-exit.test.ts b/src/main/ipc/pty-runtime-kill-and-exit.test.ts index 05a6d67e2d47..224d85796947 100644 --- a/src/main/ipc/pty-runtime-kill-and-exit.test.ts +++ b/src/main/ipc/pty-runtime-kill-and-exit.test.ts @@ -15,7 +15,6 @@ import { vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) diff --git a/src/main/ipc/pty-runtime-ssh-binding-persistence.test.ts b/src/main/ipc/pty-runtime-ssh-binding-persistence.test.ts index b66cd30e71fe..8b0184937554 100644 --- a/src/main/ipc/pty-runtime-ssh-binding-persistence.test.ts +++ b/src/main/ipc/pty-runtime-ssh-binding-persistence.test.ts @@ -13,7 +13,6 @@ import { vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) diff --git a/src/main/ipc/pty-serializer-settlement-mapping.test.ts b/src/main/ipc/pty-serializer-settlement-mapping.test.ts index 70e358a80140..0404ccfad6a5 100644 --- a/src/main/ipc/pty-serializer-settlement-mapping.test.ts +++ b/src/main/ipc/pty-serializer-settlement-mapping.test.ts @@ -21,7 +21,6 @@ import { vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) @@ -287,6 +286,7 @@ describe('registerPtyHandlers', () => { } let controller: RuntimeSpawnController | null = null const runtime = { + createPreAllocatedTerminalHandle: vi.fn(() => 'term_trusted'), setPtyController: vi.fn((value) => { controller = value }), @@ -353,6 +353,8 @@ describe('registerPtyHandlers', () => { } as never) let controller: RuntimeSpawnController | null = null const runtime = { + createPreAllocatedTerminalHandle: vi.fn(() => null), + registerPreAllocatedHandleForPty: vi.fn(), setPtyController: vi.fn((value) => { controller = value }), @@ -401,6 +403,9 @@ describe('registerPtyHandlers', () => { } let controller: RuntimeSpawnController | null = null const runtime = { + createPreAllocatedTerminalHandle: vi.fn(() => 'term_test'), + registerPreAllocatedHandleForPty: vi.fn(), + registerPty: vi.fn(), setPtyController: vi.fn((value) => { controller = value }) @@ -454,6 +459,9 @@ describe('registerPtyHandlers', () => { }) it('ignores renderer-provided ORCA_TERMINAL_HANDLE for local PTY spawns', async () => { const runtime = { + createPreAllocatedTerminalHandle: vi.fn(() => 'term_trusted'), + registerPreAllocatedHandleForPty: vi.fn(), + registerPty: vi.fn(), setPtyController: vi.fn(), noteTerminalSpawnCommand: vi.fn(), preAllocateHandleForPty: vi.fn(() => 'term_trusted'), @@ -473,7 +481,11 @@ describe('registerPtyHandlers', () => { const env = spawnCall[2].env as Record expect(env.ORCA_TERMINAL_HANDLE).toBe('term_trusted') expect(env.ORCA_WSL_CLI_DIR).toBeUndefined() - expect(runtime.preAllocateHandleForPty).toHaveBeenCalledWith(expect.any(String)) + expect(runtime.createPreAllocatedTerminalHandle).toHaveBeenCalledOnce() + expect(runtime.registerPreAllocatedHandleForPty).toHaveBeenCalledWith( + expect.any(String), + 'term_trusted' + ) }) it('forwards the trusted Orca terminal handle into managed WSL terminals', async () => { const platform = Object.getOwnPropertyDescriptor(process, 'platform') @@ -482,6 +494,9 @@ describe('registerPtyHandlers', () => { value: 'win32' }) const runtime = { + createPreAllocatedTerminalHandle: vi.fn(() => 'term_wsl'), + registerPreAllocatedHandleForPty: vi.fn(), + registerPty: vi.fn(), setPtyController: vi.fn(), noteTerminalSpawnCommand: vi.fn(), preAllocateHandleForPty: vi.fn(() => 'term_wsl'), @@ -535,6 +550,9 @@ describe('registerPtyHandlers', () => { value: 'win32' }) const runtime = { + createPreAllocatedTerminalHandle: vi.fn(() => 'term_wsl'), + registerPreAllocatedHandleForPty: vi.fn(), + registerPty: vi.fn(), setPtyController: vi.fn(), preAllocateHandleForPty: vi.fn(() => 'term_wsl'), onPtySpawned: vi.fn(), diff --git a/src/main/ipc/pty-session-liveness-and-ownership.test.ts b/src/main/ipc/pty-session-liveness-and-ownership.test.ts index 574e83c12208..aba12e1009e5 100644 --- a/src/main/ipc/pty-session-liveness-and-ownership.test.ts +++ b/src/main/ipc/pty-session-liveness-and-ownership.test.ts @@ -12,7 +12,6 @@ import { vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) diff --git a/src/main/ipc/pty-spawn-codex-home-unavailable.test.ts b/src/main/ipc/pty-spawn-codex-home-unavailable.test.ts index e039f92ff872..4857dec9bd71 100644 --- a/src/main/ipc/pty-spawn-codex-home-unavailable.test.ts +++ b/src/main/ipc/pty-spawn-codex-home-unavailable.test.ts @@ -8,7 +8,6 @@ import { registerPtyHandlers } from './pty' vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) diff --git a/src/main/ipc/pty-spawn-cwd-fallback.test.ts b/src/main/ipc/pty-spawn-cwd-fallback.test.ts index 97ca4cc86c47..914bb19ab196 100644 --- a/src/main/ipc/pty-spawn-cwd-fallback.test.ts +++ b/src/main/ipc/pty-spawn-cwd-fallback.test.ts @@ -8,7 +8,6 @@ import { registerPtyHandlers, registerSshPtyProvider } from './pty' vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) diff --git a/src/main/ipc/pty-spawn-env-agent-overlays.test.ts b/src/main/ipc/pty-spawn-env-agent-overlays.test.ts index 1595835801f3..ee16b5f33ea1 100644 --- a/src/main/ipc/pty-spawn-env-agent-overlays.test.ts +++ b/src/main/ipc/pty-spawn-env-agent-overlays.test.ts @@ -14,7 +14,6 @@ import { SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV } from '../../shared/setup-age vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) diff --git a/src/main/ipc/pty-spawn-env-codex-home-routing.test.ts b/src/main/ipc/pty-spawn-env-codex-home-routing.test.ts index 1c2f4ebc465c..1fa3b740322d 100644 --- a/src/main/ipc/pty-spawn-env-codex-home-routing.test.ts +++ b/src/main/ipc/pty-spawn-env-codex-home-routing.test.ts @@ -12,7 +12,6 @@ import { registerPtyHandlers } from './pty' vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) diff --git a/src/main/ipc/pty-spawn-env-codex-resume-provenance.test.ts b/src/main/ipc/pty-spawn-env-codex-resume-provenance.test.ts index a2a6153e99a1..097f407d91ef 100644 --- a/src/main/ipc/pty-spawn-env-codex-resume-provenance.test.ts +++ b/src/main/ipc/pty-spawn-env-codex-resume-provenance.test.ts @@ -9,7 +9,6 @@ import { registerPtyHandlers, setLocalPtyProvider, type PrepareCodexSessionResum vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) @@ -169,7 +168,7 @@ describe('registerPtyHandlers', () => { await Promise.resolve() vi.runAllTimers() - expect(mockProc.proc.write).not.toHaveBeenCalled() + expect(mockProc.proc.write).toHaveBeenCalledExactlyOnceWith('codex\n') expect(mockProc.proc.write).not.toHaveBeenCalledWith( expect.stringContaining(RESUME_SESSION_ID) ) @@ -177,7 +176,7 @@ describe('registerPtyHandlers', () => { // The pane still runs under the selected account — but with nothing to resume. const env = spawnMock.mock.calls.at(-1)![2].env as Record expect(env.CODEX_HOME).toBe(OTHER_HOME) - expect(env[POSIX_SHELL_STARTUP_COMMAND_ENV]).toBe('codex') + expect(env[POSIX_SHELL_STARTUP_COMMAND_ENV]).toBeUndefined() expect(selectedHome).toHaveBeenCalled() } finally { vi.useRealTimers() @@ -213,11 +212,13 @@ describe('registerPtyHandlers', () => { await Promise.resolve() vi.runAllTimers() - expect(mockProc.proc.write).not.toHaveBeenCalled() + expect(mockProc.proc.write).toHaveBeenCalledExactlyOnceWith( + `codex 'resume' '${RESUME_SESSION_ID}'\n` + ) expect(spawned.agentResumeUnavailable).toBeUndefined() const env = spawnMock.mock.calls.at(-1)![2].env as Record expect(env.CODEX_HOME).toBe(ORIGIN_HOME) - expect(env[POSIX_SHELL_STARTUP_COMMAND_ENV]).toBe(`codex 'resume' '${RESUME_SESSION_ID}'`) + expect(env[POSIX_SHELL_STARTUP_COMMAND_ENV]).toBeUndefined() expect(selectedHome).not.toHaveBeenCalled() } finally { vi.useRealTimers() @@ -239,9 +240,9 @@ describe('registerPtyHandlers', () => { await Promise.resolve() vi.runAllTimers() - expect(mockProc.proc.write).not.toHaveBeenCalled() + expect(mockProc.proc.write).toHaveBeenCalledExactlyOnceWith('codex\n') const env = spawnMock.mock.calls.at(-1)![2].env as Record - expect(env[POSIX_SHELL_STARTUP_COMMAND_ENV]).toBe('codex') + expect(env[POSIX_SHELL_STARTUP_COMMAND_ENV]).toBeUndefined() expect(spawned.agentResumeUnavailable).toBe(true) } finally { vi.useRealTimers() diff --git a/src/main/ipc/pty-spawn-env-terminal-basics.test.ts b/src/main/ipc/pty-spawn-env-terminal-basics.test.ts index c36fa3e40f5e..0270eb77382c 100644 --- a/src/main/ipc/pty-spawn-env-terminal-basics.test.ts +++ b/src/main/ipc/pty-spawn-env-terminal-basics.test.ts @@ -1,10 +1,10 @@ +import { createGlobalSettingsFixture } from '../../shared/global-settings-test-fixture' import { withFreshOmpLaunch } from '../../shared/omp-fresh-launch' import { describe, expect, it, vi } from 'vitest' import { piBuildPtyEnvMock, spawnMock } from './pty-ipc-mock-registry' import { BUNDLED_CLI_PATH, TEST_CODEX_HOME, makeDisposable } from './pty-ipc-test-constants' import { setupPtyIpcSuite } from './pty-ipc-test-harness' import { delimiter } from 'node:path' -import { LocalPtyProvider } from '../providers/local-pty-provider' import { __resetPersistedWindowsPathCacheForTests } from '../pty/windows-environment-path' import { __setWindowsPathRegistryLoaderForTests } from '../pty/windows-path-registry-reader' import { hasLiveClaudePtys, markClaudePtySpawned } from '../claude-accounts/live-pty-gate' @@ -13,7 +13,6 @@ import { registerPtyHandlers, buildPtyHostEnv, clearProviderPtyState } from './p vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) @@ -237,18 +236,11 @@ describe('registerPtyHandlers', () => { __resetPersistedWindowsPathCacheForTests() try { - const provider = new LocalPtyProvider({ - buildSpawnEnv: (id, baseEnv, context) => - buildPtyHostEnv(id, baseEnv, { - isPackaged: true, - userDataPath: '/tmp/orca-user-data', - selectedCodexHomePath: null, - agentStatusHooksEnabled: false, - isWsl: context?.isWsl, - wslDistro: context?.wslDistro - }) - }) - await provider.spawn({ + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: This IPC fixture supplies the BrowserWindow methods used during handler registration. + registerPtyHandlers(mainWindow as never, undefined, undefined, () => + createGlobalSettingsFixture({ agentStatusHooksEnabled: false }) + ) + await handlers.get('pty:spawn')!(null, { cols: 80, rows: 24, cwd: '\\\\wsl.localhost\\Ubuntu\\home\\me\\repo', diff --git a/src/main/ipc/pty-spawn-pi-owner-env.test.ts b/src/main/ipc/pty-spawn-pi-owner-env.test.ts index 168d253851d4..d939e2cf952e 100644 --- a/src/main/ipc/pty-spawn-pi-owner-env.test.ts +++ b/src/main/ipc/pty-spawn-pi-owner-env.test.ts @@ -5,7 +5,6 @@ import { registerPtyHandlers } from './pty' vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) diff --git a/src/main/ipc/pty-spawn-runtime-handle-binding.test.ts b/src/main/ipc/pty-spawn-runtime-handle-binding.test.ts index 73c6e906d5e3..5b220985f4a8 100644 --- a/src/main/ipc/pty-spawn-runtime-handle-binding.test.ts +++ b/src/main/ipc/pty-spawn-runtime-handle-binding.test.ts @@ -15,7 +15,6 @@ import { registerPtyHandlers, registerSshPtyProvider } from './pty' vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) @@ -84,6 +83,7 @@ describe('registerPtyHandlers', () => { acknowledgeDataEvent: vi.fn() } as never) const runtime = { + registerPty: vi.fn(), setPtyController: vi.fn(), noteTerminalSpawnCommand: vi.fn(), createPreAllocatedTerminalHandle: vi.fn(() => 'term_remote'), @@ -200,6 +200,7 @@ describe('registerPtyHandlers', () => { it('threads the validated pane identity into registerPty for a renderer PTY spawn (#7587)', async () => { const leafId = '88888888-8888-4888-8888-888888888888' const runtime = { + createPreAllocatedTerminalHandle: vi.fn(() => 'term_seam'), setPtyController: vi.fn(), preAllocateHandleForPty: vi.fn(() => 'term_seam'), registerPreAllocatedHandleForPty: vi.fn(), @@ -226,7 +227,7 @@ describe('registerPtyHandlers', () => { expect.any(String), 'wt-1', null, - { tabId: 'tab-1', leafId, incarnationId: expect.any(String) }, + { tabId: 'tab-1', leafId, terminalHandle: 'term_seam', incarnationId: expect.any(String) }, false ) }) @@ -339,6 +340,7 @@ describe('registerPtyHandlers', () => { }) it('omits the pane identity from registerPty when the leafId is not a terminal leaf (#7587)', async () => { const runtime = { + createPreAllocatedTerminalHandle: vi.fn(() => 'term_seam'), setPtyController: vi.fn(), preAllocateHandleForPty: vi.fn(() => 'term_seam'), registerPreAllocatedHandleForPty: vi.fn(), @@ -502,6 +504,7 @@ describe('registerPtyHandlers', () => { } let controller: RuntimeSpawnController | null = null const runtime = { + createPreAllocatedTerminalHandle: vi.fn(() => 'term_wrong'), setPtyController: vi.fn((value) => { controller = value }), diff --git a/src/main/ipc/pty-ssh-stop-verdict.test.ts b/src/main/ipc/pty-ssh-stop-verdict.test.ts index 8001028ea1db..8356995cde6d 100644 --- a/src/main/ipc/pty-ssh-stop-verdict.test.ts +++ b/src/main/ipc/pty-ssh-stop-verdict.test.ts @@ -12,7 +12,6 @@ import { vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) diff --git a/src/main/ipc/pty-ssh-undelivered-kill.test.ts b/src/main/ipc/pty-ssh-undelivered-kill.test.ts index d59d7d8543a1..3f108d11e5e5 100644 --- a/src/main/ipc/pty-ssh-undelivered-kill.test.ts +++ b/src/main/ipc/pty-ssh-undelivered-kill.test.ts @@ -12,7 +12,6 @@ import { vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) diff --git a/src/main/ipc/pty-startup-barrier-and-listing.test.ts b/src/main/ipc/pty-startup-barrier-and-listing.test.ts index a4463d9d8654..6ab033958b31 100644 --- a/src/main/ipc/pty-startup-barrier-and-listing.test.ts +++ b/src/main/ipc/pty-startup-barrier-and-listing.test.ts @@ -12,7 +12,6 @@ import { vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) diff --git a/src/main/ipc/pty-startup-swap-window-presence.test.ts b/src/main/ipc/pty-startup-swap-window-presence.test.ts index 05fcf27a1d2e..a7eda5d420ff 100644 --- a/src/main/ipc/pty-startup-swap-window-presence.test.ts +++ b/src/main/ipc/pty-startup-swap-window-presence.test.ts @@ -6,7 +6,6 @@ import { ptyOwnership } from './pty/provider/ownership-state' vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) diff --git a/src/main/ipc/pty-windows-shell-selection.test.ts b/src/main/ipc/pty-windows-shell-selection.test.ts index 6d42f7d0bed3..b6b2b6a1539d 100644 --- a/src/main/ipc/pty-windows-shell-selection.test.ts +++ b/src/main/ipc/pty-windows-shell-selection.test.ts @@ -15,7 +15,6 @@ import { registerPtyHandlers } from './pty' vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) @@ -118,7 +117,11 @@ describe('registerPtyHandlers', () => { process.env.COMSPEC = 'C:\\Windows\\system32\\cmd.exe' registerPtyHandlers(mainWindow as never) - await handlers.get('pty:spawn')!(null, { cols: 80, rows: 24 }) + await handlers.get('pty:spawn')!(null, { + cols: 80, + rows: 24, + shellOverride: process.env.COMSPEC + }) expect(spawnMock).toHaveBeenCalledWith( 'C:\\Windows\\system32\\cmd.exe', @@ -130,7 +133,11 @@ describe('registerPtyHandlers', () => { process.env.COMSPEC = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' registerPtyHandlers(mainWindow as never) - await handlers.get('pty:spawn')!(null, { cols: 80, rows: 24 }) + await handlers.get('pty:spawn')!(null, { + cols: 80, + rows: 24, + shellOverride: process.env.COMSPEC + }) expect(spawnMock).toHaveBeenCalledWith( 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe', @@ -142,7 +149,11 @@ describe('registerPtyHandlers', () => { process.env.COMSPEC = 'C:\\Program Files\\PowerShell\\7\\pwsh.exe' registerPtyHandlers(mainWindow as never) - await handlers.get('pty:spawn')!(null, { cols: 80, rows: 24 }) + await handlers.get('pty:spawn')!(null, { + cols: 80, + rows: 24, + shellOverride: process.env.COMSPEC + }) expect(spawnMock).toHaveBeenCalledWith( 'C:\\Program Files\\PowerShell\\7\\pwsh.exe', @@ -154,7 +165,11 @@ describe('registerPtyHandlers', () => { process.env.COMSPEC = 'C:\\Windows\\system32\\cmd.exe' registerPtyHandlers(mainWindow as never) - await handlers.get('pty:spawn')!(null, { cols: 80, rows: 24 }) + await handlers.get('pty:spawn')!(null, { + cols: 80, + rows: 24, + shellOverride: process.env.COMSPEC + }) const spawnCall = spawnMock.mock.calls.at(-1)! const env = spawnCall[2].env as Record @@ -165,17 +180,25 @@ describe('registerPtyHandlers', () => { process.env.PYTHONUTF8 = '0' registerPtyHandlers(mainWindow as never) - await handlers.get('pty:spawn')!(null, { cols: 80, rows: 24 }) + await handlers.get('pty:spawn')!(null, { + cols: 80, + rows: 24, + shellOverride: process.env.COMSPEC + }) const spawnCall = spawnMock.mock.calls.at(-1)! const env = spawnCall[2].env as Record expect(env.PYTHONUTF8).toBe('0') }) - it('launches Git Bash from COMSPEC as an interactive login shell', async () => { + it('launches explicitly selected Git Bash as an interactive login shell', async () => { process.env.COMSPEC = 'C:\\Program Files\\Git\\bin\\bash.exe' registerPtyHandlers(mainWindow as never) - await handlers.get('pty:spawn')!(null, { cols: 80, rows: 24 }) + await handlers.get('pty:spawn')!(null, { + cols: 80, + rows: 24, + shellOverride: process.env.COMSPEC + }) expect(spawnMock).toHaveBeenCalledWith( 'C:\\Program Files\\Git\\bin\\bash.exe', diff --git a/src/main/ipc/pty-write-ipc-validation.test.ts b/src/main/ipc/pty-write-ipc-validation.test.ts index ee05fc62c608..4daa4334b6e3 100644 --- a/src/main/ipc/pty-write-ipc-validation.test.ts +++ b/src/main/ipc/pty-write-ipc-validation.test.ts @@ -13,7 +13,6 @@ import { registerPtyHandlers, deletePtyOwnership, setLocalPtyProvider } from './ vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) diff --git a/src/main/ipc/pty-wsl-cwd-validation.test.ts b/src/main/ipc/pty-wsl-cwd-validation.test.ts index cfdb693d228b..77d4deeb8611 100644 --- a/src/main/ipc/pty-wsl-cwd-validation.test.ts +++ b/src/main/ipc/pty-wsl-cwd-validation.test.ts @@ -10,11 +10,10 @@ import { _setWslCachesForTests } from '../wsl' import { registerPtyHandlers } from './pty' import { join } from 'node:path' // Why resolved rather than hardcoded: the wrapper tree is content-addressed. -import { getShellReadyWrapperRoot } from '../providers/local-pty-shell-ready-wrapper-root' +import { getShellReadyWrapperRoot } from '../daemon/shell-ready' vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) @@ -298,7 +297,7 @@ describe('registerPtyHandlers', () => { // Why: the startup-cwd guard normalizes separators, so the provider sees the forward-slash UNC form. existsSyncMock.mockImplementation((targetPath: string) => { - if (targetPath === '//wsl.localhost/Ubuntu/home/jin/missing') { + if (targetPath.replaceAll('\\', '/') === '//wsl.localhost/Ubuntu/home/jin/missing') { return false } return true diff --git a/src/main/ipc/pty-wsl-stable-adoption.test.ts b/src/main/ipc/pty-wsl-stable-adoption.test.ts new file mode 100644 index 000000000000..8f8a9966234d --- /dev/null +++ b/src/main/ipc/pty-wsl-stable-adoption.test.ts @@ -0,0 +1,90 @@ +import type * as StableOwner from './pty/pane/stable-owner' +import { afterEach, expect, it, vi } from 'vitest' +import { registerPtyHandlers } from './pty/register-handlers' +import { installPtySpawnIpcHandler } from './pty/ipc/spawn' +import { resolveStablePaneOwner } from './pty/pane/stable-owner' +import { registerWslPtyProvider } from './pty/provider/registry' +import { WslDaemonSessions } from '../wsl/wsl-daemon-sessions' +import { WslDaemonPtyProvider } from '../wsl/wsl-daemon-pty-provider' +import { DaemonPtyAdapter } from '../daemon/daemon-pty-adapter' +import { toAppWslPtyId } from '../../shared/wsl-pty-id' + +vi.mock('./pty/ipc/spawn', () => ({ installPtySpawnIpcHandler: vi.fn() })) +vi.mock('./pty/pane/stable-owner', async (original) => ({ + ...(await original()), + resolveStablePaneOwner: vi.fn() +})) +const releases: (() => void)[] = [] +afterEach(() => { + releases.splice(0).forEach((release) => release()) + vi.restoreAllMocks() +}) + +it('reconnects the persisted guest before the bound renderer adoption consults its registry', async () => { + const owner = { distro: 'Ubuntu', relayBuildId: 'daemon+profile-user' } + const id = toAppWslPtyId(owner, 'existing-shell') + const binding = { + ptyId: id, + tabId: 'tab-restore', + leafId: '44444444-4444-4444-8444-444444444444' + } + vi.mocked(resolveStablePaneOwner).mockReturnValue(binding) + const endpoint = { + distro: 'Ubuntu', + distributionId: 'registration', + userName: 'alice', + userId: '1000', + home: '/home/alice', + runtime: '/bun', + entry: '/daemon.js', + envBinary: '/usr/bin/env', + socket: '/socket', + tokenPath: '/token', + serverBuildId: 'artifact' + } + const adapter = new DaemonPtyAdapter({ + guest: { + distro: 'Ubuntu', + defaultShell: '/bin/bash', + defaultCwd: '/home/alice', + profiles: [], + transport: { + readToken: () => 'unused', + connect: async () => { + throw new Error('unexpected connector') + } + } + } + }) + const provider = new WslDaemonPtyProvider(owner, adapter) + const spawn = vi.spyOn(provider, 'spawn').mockResolvedValue({ id, isReattach: true }) + const sessions = new WslDaemonSessions({ + profileScope: '/profile', + historyRoot: '/history', + store: { + getWslDaemonRecovery: () => null, + upsertWslDaemonRecovery: async () => {} + } + }) + const reconnect = vi.spyOn(sessions, 'reconnect').mockImplementation(async () => { + releases.push(registerWslPtyProvider(owner, provider)) + return { owner, endpoint, provider } + }) + registerPtyHandlers(undefined, undefined, undefined, undefined, undefined, undefined, { + wslDaemonSessions: sessions + }) + const deps = vi.mocked(installPtySpawnIpcHandler).mock.calls.at(-1)?.[0] + expect(deps).toBeDefined() + const result = await deps?.adoptStablePane({ + ...binding, + worktreeId: 'repo::/workspace', + cols: 91, + rows: 31, + ownsPaneSpawnReservation: true + }) + expect(reconnect).toHaveBeenCalledWith(expect.objectContaining(owner)) + expect(spawn).toHaveBeenCalledWith(expect.objectContaining({ sessionId: id, attachOnly: true })) + expect(reconnect.mock.invocationCallOrder[0]).toBeLessThan(spawn.mock.invocationCallOrder[0]) + expect(result?.result.id).toBe(id) + adapter.dispose() +}) diff --git a/src/main/ipc/pty/delivery/ssh-intake.ts b/src/main/ipc/pty/delivery/ssh-intake.ts index 2f86608fb82f..3f68eed16a76 100644 --- a/src/main/ipc/pty/delivery/ssh-intake.ts +++ b/src/main/ipc/pty/delivery/ssh-intake.ts @@ -5,7 +5,7 @@ import { installSshPtyOutputIntake, publishSshPtySourceAck } from '../../ssh-pty-output-intake-registry' -import { sshProvidersByGeneration } from '../provider/registry' +import { relayProvidersByGeneration } from '../../../providers/relay-pty-generation-registry' import { setSshOutputIntakeCleanup, sshOutputIntakeCleanup } from '../provider/listener-lifecycle' import type { PtyIpcSession } from '../session' @@ -51,9 +51,7 @@ export function installSessionSshOutputIntake(session: PtyIpcSession): void { session.finalizePtyExitForRenderer(event) }, pauseProvider: (generation, id) => { - const provider = sshProvidersByGeneration.get(generation) as - | (IPtyProvider & { hasPtyDeliveryPauseAdapter?: () => boolean }) - | undefined + const provider = relayProvidersByGeneration.get(generation) if (!provider?.hasPtyDeliveryPauseAdapter?.()) { return false } @@ -61,9 +59,9 @@ export function installSessionSshOutputIntake(session: PtyIpcSession): void { return true }, resumeProvider: (generation, id) => - sshProvidersByGeneration.get(generation)?.resumeProducer?.(id), + relayProvidersByGeneration.get(generation)?.resumeProducer?.(id), closeProvider: (generation, reason) => { - const provider = sshProvidersByGeneration.get(generation) + const provider = relayProvidersByGeneration.get(generation) ;( provider as (IPtyProvider & { closeOutputIntake?: (reason: string) => void }) | undefined )?.closeOutputIntake?.(reason) @@ -85,7 +83,7 @@ export function installSessionSshOutputIntake(session: PtyIpcSession): void { session.pendingOverflowMarkedPtys.delete(id) } } - sshProvidersByGeneration.delete(providerGeneration) + relayProvidersByGeneration.delete(providerGeneration) }, publishSourceAck: publishSshPtySourceAck, cancelSourceDelivery: cancelSshPtySourceDelivery diff --git a/src/main/ipc/pty/host-env/assembly.ts b/src/main/ipc/pty/host-env/assembly.ts index b5daa5bed7f0..d6dcb0e88056 100644 --- a/src/main/ipc/pty/host-env/assembly.ts +++ b/src/main/ipc/pty/host-env/assembly.ts @@ -142,13 +142,25 @@ export function buildPtyHostEnv( explicitPiAgentKind === 'pi' || explicitPiAgentKind === 'omp' ? explicitPiAgentKind : undefined - wslHookRelayManager.ensureForDistro(distro, opts.selectedCodexHomePath, wslLaunchKind) - const guestEndpoint = wslHookRelayManager.getGuestEndpointFilePath(distro) + wslHookRelayManager.ensureForDistro( + distro, + opts.selectedCodexHomePath, + wslLaunchKind, + ...(opts.wslUser ? [opts.wslUser] : []) + ) + const guestEndpoint = wslHookRelayManager.getGuestEndpointFilePath( + distro, + ...(opts.wslUser ? [opts.wslUser] : []) + ) if (guestEndpoint) { baseEnv.ORCA_AGENT_HOOK_ENDPOINT = guestEndpoint } // Why: OpenCode loads its status plugin from a guest config overlay, so point OPENCODE_CONFIG_DIR at the guest dir the relay materialized. - const opencodeOverlayDir = wslHookRelayManager.getOpenCodeOverlayDir(distro, openCodeAgent) + const opencodeOverlayDir = wslHookRelayManager.getOpenCodeOverlayDir( + distro, + openCodeAgent, + ...(opts.wslUser ? [opts.wslUser] : []) + ) if (opencodeOverlayDir) { baseEnv.OPENCODE_CONFIG_DIR = opencodeOverlayDir baseEnv.ORCA_OPENCODE_CONFIG_DIR = opencodeOverlayDir @@ -231,12 +243,20 @@ export function buildPtyHostEnv( if (opts.isWsl && opts.agentStatusHooksEnabled) { const distro = opts.wslDistro ?? null if (explicitPiAgentKind === 'pi') { - const guestPiDir = wslHookRelayManager.getGuestAgentPath(distro, 'pi') + const guestPiDir = wslHookRelayManager.getGuestAgentPath( + distro, + 'pi', + ...(opts.wslUser ? [opts.wslUser] : []) + ) if (guestPiDir) { baseEnv.ORCA_PI_SOURCE_AGENT_DIR = guestPiDir } } else if (explicitPiAgentKind === 'omp') { - const guestOmpExtension = wslHookRelayManager.getGuestAgentPath(distro, 'omp') + const guestOmpExtension = wslHookRelayManager.getGuestAgentPath( + distro, + 'omp', + ...(opts.wslUser ? [opts.wslUser] : []) + ) if (guestOmpExtension) { baseEnv.ORCA_OMP_STATUS_EXTENSION = guestOmpExtension } diff --git a/src/main/ipc/pty/host-env/codex-resume.ts b/src/main/ipc/pty/host-env/codex-resume.ts index 62e199e09c3f..ff23b92d74da 100644 --- a/src/main/ipc/pty/host-env/codex-resume.ts +++ b/src/main/ipc/pty/host-env/codex-resume.ts @@ -1,3 +1,4 @@ +import type { WslAccountExecutionContext } from '../../../wsl/wsl-account-execution-context' import type { TuiAgent } from '../../../../shared/tui-agent' import { normalizeAgentProviderSession, @@ -25,6 +26,7 @@ export type PreparedCodexResumeHome = { } export type PrepareCodexResumeHomeArgs = { + wslExecution?: WslAccountExecutionContext connectionId?: string | null launchAgent?: TuiAgent providerSession?: AgentProviderSessionMetadata @@ -50,7 +52,8 @@ export function prepareCodexResumeHome( providerSession, target: args.target, launchEnv: args.launchEnv, - workspacePath: args.workspacePath + workspacePath: args.workspacePath, + ...(args.wslExecution ? { wslExecution: args.wslExecution } : {}) }) } } diff --git a/src/main/ipc/pty/host-env/fresh-spawn-routing.ts b/src/main/ipc/pty/host-env/fresh-spawn-routing.ts index e03c29e79bc8..e731b0afcb35 100644 --- a/src/main/ipc/pty/host-env/fresh-spawn-routing.ts +++ b/src/main/ipc/pty/host-env/fresh-spawn-routing.ts @@ -1,6 +1,5 @@ import { normalizeRuntimePathForComparison } from '../../../../shared/cross-platform-path' import { splitWorktreeIdForFilesystem } from '../../../../shared/worktree/id' -import type { IPtyProvider } from '../../../providers/types' import { beginTerminalInstall } from '../../watcher-removal-gate' export function isClaudeLaunchCommand(command: string | undefined): boolean { @@ -12,22 +11,6 @@ export function isClaudeLaunchCommand(command: string | undefined): boolean { ) } -export function routesFreshSpawnsToLocalProvider(provider: IPtyProvider): boolean { - return provider.routesFreshSpawnsToLocalProvider === true -} - -export function recoverFreshSpawnProviderRouting( - provider: IPtyProvider, - connectionId: string | null | undefined, - sessionId: string | undefined, - isNewSession = sessionId === undefined -): Promise | undefined { - if (connectionId || (!isNewSession && sessionId) || !routesFreshSpawnsToLocalProvider(provider)) { - return - } - return provider.recoverFreshSpawnRouting?.() -} - export function beginPtySpawnForWorktree( worktreeId: string | undefined, cwd: string | undefined, diff --git a/src/main/ipc/pty/host-env/types.ts b/src/main/ipc/pty/host-env/types.ts index 37ef0614a504..2e37437c1839 100644 --- a/src/main/ipc/pty/host-env/types.ts +++ b/src/main/ipc/pty/host-env/types.ts @@ -1,3 +1,4 @@ +import type { WslAccountExecutionContext } from '../../../wsl/wsl-account-execution-context' import type { TuiAgent } from '../../../../shared/tui-agent' import type { AgentProviderSessionMetadata } from '../../../../shared/agent-session-resume' import type { NetworkProxySettings } from '../../../../shared/network-proxy' @@ -26,6 +27,8 @@ export type BuildPtyHostEnvOptions = { isWsl?: boolean /** Distro for WSL spawns (null = Windows default distro); drives the WSL hook relay + endpoint repoint. Only read when isWsl. */ wslDistro?: string | null + /** Persisted guest owner; omitted for legacy Windows-owned WSL panes. */ + wslUser?: string agentStatusHooksEnabled: boolean /** Per-agent opt-out; disabled agents must not receive managed extensions. */ disabledTuiAgents?: Iterable | null @@ -39,6 +42,7 @@ export type BuildPtyHostEnvOptions = { export type CodexHomeLaunchContext = { workspacePath?: string + wslExecution?: WslAccountExecutionContext launchAgent?: TuiAgent unavailableManagedHomePath?: string } @@ -53,6 +57,7 @@ export type GetSelectedCodexHomePath = ( ) => string | null | Promise export type PrepareCodexSessionResume = (args: { + wslExecution?: WslAccountExecutionContext providerSession: AgentProviderSessionMetadata target: CodexAccountSelectionTarget launchEnv?: NodeJS.ProcessEnv @@ -76,5 +81,6 @@ export function allocatePtyLifecycleSequence(): number { } export type PrepareClaudeAuth = ( - target?: ClaudeAccountSelectionTarget + target?: ClaudeAccountSelectionTarget, + execution?: WslAccountExecutionContext ) => Promise diff --git a/src/main/ipc/pty/ipc/inspect.ts b/src/main/ipc/pty/ipc/inspect.ts index 37226017b8f3..ae715410a23d 100644 --- a/src/main/ipc/pty/ipc/inspect.ts +++ b/src/main/ipc/pty/ipc/inspect.ts @@ -1,3 +1,7 @@ +import { + createLocalInventoryAuthority, + type LocalInventoryAuthorityDeps +} from '../provider/local-inventory-authority' import { getPtyIpc } from '../../pty-host-bindings' import { parseAppSshPtyId } from '../../../providers/ssh-pty-id' import { inspectPtyProviderProcessForRenderer } from '../../../providers/pty-process-inspection' @@ -13,6 +17,7 @@ import { getProvider, hasPtyProviderForInspection, registeredPtyProviders, + localProvider, sshProviders, tryGetProviderForPty } from '../provider/registry' @@ -25,9 +30,11 @@ import { settlePendingPaneSerializer } from '../pane/serializer-state' -export function installPtyInspectIpcHandlers(deps: { - getLocalPtyProviderStartupPromise: (connectionId?: string | null) => Promise | undefined -}): void { +export function installPtyInspectIpcHandlers( + deps: LocalInventoryAuthorityDeps & { + getLocalPtyProviderStartupPromise: (connectionId?: string | null) => Promise | undefined + } +): void { const ipcMain = getPtyIpc() const { getLocalPtyProviderStartupPromise } = deps @@ -57,16 +64,31 @@ export function installPtyInspectIpcHandlers(deps: { await getLocalPtyProviderStartupPromise() } } + const localAuthority = createLocalInventoryAuthority( + scope === undefined || scope.connectionId === null ? deps : {} + ) const deduped = new Map() const admission = new PtyProcessListAdmission() await visitPtyProcessListingsInBatches( - scope === undefined - ? registeredPtyProviders() + scope === undefined || scope.connectionId === null + ? registeredPtyProviders().filter( + ({ connectionId }) => scope === undefined || connectionId === null + ) : [{ provider: getProvider(scope.connectionId), connectionId: scope.connectionId }], - ({ provider, connectionId }) => - connectionId === null || scope !== undefined - ? provider.listProcesses() - : provider.listProcesses().catch(() => []), + async ({ provider, connectionId }) => { + try { + return await provider.listProcesses() + } catch (error) { + if (connectionId === null && provider !== localProvider) { + localAuthority.failed(provider, error) + return [] + } + if (connectionId !== null && scope === undefined) { + return [] + } + throw error + } + }, ({ provider, connectionId }, sessions) => { for (const rawSession of sessions) { const session = admission.admit(rawSession) @@ -90,6 +112,9 @@ export function installPtyInspectIpcHandlers(deps: { } } ) + if (scope === undefined || scope.connectionId === null) { + localAuthority.assertComplete() + } return Array.from(deduped.values()) } ) diff --git a/src/main/ipc/pty/ipc/spawn-env-codex.ts b/src/main/ipc/pty/ipc/spawn-env-codex.ts index 50173f93c3d2..226c704ee651 100644 --- a/src/main/ipc/pty/ipc/spawn-env-codex.ts +++ b/src/main/ipc/pty/ipc/spawn-env-codex.ts @@ -1,3 +1,4 @@ +import type { BuildPtyHostEnvOptions } from '../host-env/types' import { inheritOmpLaunchEnvironment } from '../host-env/omp-launch-environment' import { getAppEnvironment } from '../../../../shared/app-environment' import { isTuiAgent } from '../../../../shared/tui-agent-config' @@ -23,16 +24,20 @@ import type { PtyIpcSpawnState } from './spawn-state' export async function assemblePtyIpcSpawnCodexEnv(ctx: PtyIpcSpawnState): Promise { const args = ctx.args ctx.effectiveShellOverride = ctx.terminalRuntimeOptions.shellOverride - ctx.nativeWindowsConptySpawn = isNativeWindowsLocalPtySpawn({ - connectionId: args.connectionId, - cwd: args.cwd, - shellOverride: ctx.effectiveShellOverride - }) - ctx.codexSelectionTarget = getCodexSelectionTargetForPty( - ctx.effectiveShellOverride, - ctx.cwd, - ctx.expectedWslDistro - ) + ctx.nativeWindowsConptySpawn = + !ctx.wslGuest && + isNativeWindowsLocalPtySpawn({ + connectionId: args.connectionId, + cwd: args.cwd, + shellOverride: ctx.effectiveShellOverride + }) + ctx.codexSelectionTarget = ctx.wslGuest + ? { runtime: 'wsl', wslDistro: ctx.wslGuest.execution.distro } + : getCodexSelectionTargetForPty(ctx.effectiveShellOverride, ctx.cwd, ctx.expectedWslDistro) + if (ctx.wslGuest && !ctx.wslGuest.fresh && !ctx.wslGuest.coldRestore) { + ctx.env = ctx.baseEnv + return + } const codexResumePreparation = ctx.preAdoptedStablePane ? null : ctx.deps.prepareCodexResumeHome({ @@ -41,7 +46,8 @@ export async function assemblePtyIpcSpawnCodexEnv(ctx: PtyIpcSpawnState): Promis providerSession: args.resumeProviderSession, target: ctx.codexSelectionTarget, launchEnv: ctx.baseEnv, - workspacePath: ctx.cwd + workspacePath: ctx.cwd, + ...(ctx.wslGuest ? { wslExecution: ctx.wslGuest.execution } : {}) }) ctx.codexResumeLaunch = codexResumePreparation ? await ctx.deps.resolveCodexResumeLaunch(args.command, codexResumePreparation) @@ -59,6 +65,7 @@ export async function assemblePtyIpcSpawnCodexEnv(ctx: PtyIpcSpawnState): Promis const selectLaunchCodexHome = async (): Promise => (await ctx.deps.getSelectedCodexHomePath?.(ctx.codexSelectionTarget, ctx.baseEnv, { workspacePath: ctx.cwd, + ...(ctx.wslGuest ? { wslExecution: ctx.wslGuest.execution } : {}), launchAgent: isTuiAgent(args.launchAgent) ? args.launchAgent : undefined })) ?? null ctx.selectedCodexHomePath = @@ -86,6 +93,7 @@ export async function assemblePtyIpcSpawnCodexEnv(ctx: PtyIpcSpawnState): Promis ctx.codexSelectionTarget, (await ctx.deps.getSelectedCodexHomePath?.(ctx.codexSelectionTarget, ctx.baseEnv, { workspacePath: ctx.cwd, + ...(ctx.wslGuest ? { wslExecution: ctx.wslGuest.execution } : {}), launchAgent: 'codex' })) ?? null ), @@ -94,6 +102,7 @@ export async function assemblePtyIpcSpawnCodexEnv(ctx: PtyIpcSpawnState): Promis ctx.codexSelectionTarget, (await ctx.deps.getSelectedCodexHomePath?.(ctx.codexSelectionTarget, ctx.baseEnv, { workspacePath: ctx.cwd, + ...(ctx.wslGuest ? { wslExecution: ctx.wslGuest.execution } : {}), launchAgent: 'codex', unavailableManagedHomePath })) ?? null @@ -101,7 +110,7 @@ export async function assemblePtyIpcSpawnCodexEnv(ctx: PtyIpcSpawnState): Promis }) ctx.selectedCodexHomePath = resolution instanceof Promise ? await resolution : resolution } - if (args.launchAgent === 'codex' && ctx.selectedCodexHomePath) { + if (!ctx.wslGuest && args.launchAgent === 'codex' && ctx.selectedCodexHomePath) { await ensureCodexStateDbBackfillRecoveryStarted(ctx.selectedCodexHomePath) } ctx.spawnTiming.mark('codex_home') @@ -135,11 +144,13 @@ export async function assemblePtyIpcSpawnCodexEnv(ctx: PtyIpcSpawnState): Promis ctx.env = { ...ctx.baseEnv } try { await inheritOmpLaunchEnvironment(ctx.env, { - isWsl: shouldSkipCodexHomeEnvForWindowsShell(ctx.effectiveShellOverride, ctx.cwd), + isWsl: + Boolean(ctx.wslGuest) || + shouldSkipCodexHomeEnvForWindowsShell(ctx.effectiveShellOverride, ctx.cwd), launchAgent: args.launchAgent, launchCommand: ctx.launchCommand }) - buildPtyHostEnv(sessionIdForEnv, ctx.env, { + const policy: BuildPtyHostEnvOptions = { isPackaged: getAppEnvironment().isPackaged(), resourcesPath: process.resourcesPath, userDataPath: getAppEnvironment().getPath('userData'), @@ -148,7 +159,9 @@ export async function assemblePtyIpcSpawnCodexEnv(ctx: PtyIpcSpawnState): Promis stripInheritedOrcaCodexHome: ctx.stripInheritedOrcaCodexHome, launchCommand: ctx.launchCommand, launchAgent: isTuiAgent(args.launchAgent) ? args.launchAgent : undefined, - isWsl: shouldSkipCodexHomeEnvForWindowsShell(ctx.effectiveShellOverride, ctx.cwd), + isWsl: + Boolean(ctx.wslGuest) || + shouldSkipCodexHomeEnvForWindowsShell(ctx.effectiveShellOverride, ctx.cwd), wslDistro: ctx.codexSelectionTarget.runtime === 'wsl' ? ctx.expectedWslDistro : null, agentStatusHooksEnabled: isAgentStatusHooksEnabled(ptySettings), disabledTuiAgents: ptySettings?.disabledTuiAgents, @@ -157,7 +170,12 @@ export async function assemblePtyIpcSpawnCodexEnv(ctx: PtyIpcSpawnState): Promis routeBrowserOpensToClient: ctx.deps.runtime?.shouldRelayTerminalBrowserOpens?.(), deferGitConfigGuardToDaemon: ctx.provider.supportsGitCredentialGuardHost?.(ctx.effectiveSessionId) === true - }) + } + if (ctx.wslGuest) { + ctx.guestHostEnvPolicy = policy + } else { + buildPtyHostEnv(sessionIdForEnv, ctx.env, policy) + } stampWslOrchestrationCompatibilityHost( ctx.env, ctx.deps.runtime?.getOrchestrationCompatibilityHostId?.(), diff --git a/src/main/ipc/pty/ipc/spawn-env.ts b/src/main/ipc/pty/ipc/spawn-env.ts index 94f1acf363e1..38222262c3af 100644 --- a/src/main/ipc/pty/ipc/spawn-env.ts +++ b/src/main/ipc/pty/ipc/spawn-env.ts @@ -12,9 +12,7 @@ import { CLAUDE_AUTH_SWITCH_IN_PROGRESS_MESSAGE, hasClaudeAuthEnvConflict } from '../../../claude-accounts/environment' -import { LocalPtyProvider } from '../../../providers/local-pty-provider' import { resolvePathEnvKey } from '../../../pty/windows-environment-path' -import { routesFreshSpawnsToLocalProvider } from '../host-env/fresh-spawn-routing' import { stripRemotePaneEnvWhenHooksDisabled } from '../provider/liveness' import { parseValidPaneKey } from '../pane/key-state' import { shouldRefreshNativeClaudeAgentTeamsEnv } from '../pane/launch-authority' @@ -72,6 +70,7 @@ export async function assemblePtyIpcSpawnEnv(ctx: PtyIpcSpawnState): Promise { args.worktreeId, args.connectionId ) + const stableGuest = stablePaneOwnerCandidate && parseAppWslPtyId(stablePaneOwnerCandidate.ptyId) + if (stableGuest) { + const sessions = ctx.deps.options?.wslDaemonSessions + if (!sessions) { + throw new Error('WSL terminal owner is unavailable on this host') + } + await sessions.reconnect(stableGuest) + } const expectedPtyId = stablePaneOwnerCandidate?.ptyId ?? ctx.effectiveSessionAppId ?? ctx.effectiveSessionId if (expectedPtyId) { @@ -101,7 +112,14 @@ export async function executePtyIpcSpawn(ctx: PtyIpcSpawnState): Promise { ctx.snapshotKittyFlagsCoverReconciledSeq = false } } - ensureWslHookRelayForReattach(ctx.result, args.connectionId) + const guestHooks = await prepareWslDaemonReattachHooks({ + result: ctx.result, + sessions: ctx.deps.options?.wslDaemonSessions, + hooksEnabled: isAgentStatusHooksEnabled(ctx.deps.getSettings?.()) + }) + if (!guestHooks) { + ensureWslHookRelayForReattach(ctx.result, args.connectionId) + } ctx.deps.runtime?.preparePtyExecutionContext?.( ctx.result.id, args.connectionId diff --git a/src/main/ipc/pty/ipc/spawn-options.ts b/src/main/ipc/pty/ipc/spawn-options.ts index a2adf48771b6..d4f0adabfe60 100644 --- a/src/main/ipc/pty/ipc/spawn-options.ts +++ b/src/main/ipc/pty/ipc/spawn-options.ts @@ -1,3 +1,4 @@ +import { preparePtyIpcWslOptions } from './spawn-wsl-options' import { isTuiAgent } from '../../../../shared/tui-agent-config' import { CLAUDE_AUTH_ENV_VARS } from '../../../claude-accounts/environment' import { LEGACY_TERMINAL_SHIM_REMOTE_ENV_KEYS } from '../../../pty/legacy-terminal-shim-dir' @@ -42,7 +43,9 @@ export async function buildPtyIpcSpawnOptions( ctx.agentTeamsEnvToDelete ?? [], // Why: disable old hosts without removing ORCA_REAL_* while their Windows shim remains on PATH. ctx.isDaemonHostSpawn || args.connectionId ? LEGACY_TERMINAL_SHIM_REMOTE_ENV_KEYS : [], - ctx.isDaemonHostSpawn ? getInheritedAgentHookEnvKeysToDelete(ctx.spawnEnv) : [], + ctx.isDaemonHostSpawn && !ctx.wslGuest + ? getInheritedAgentHookEnvKeysToDelete(ctx.spawnEnv) + : [], getInheritedClaudeSessionStampEnvKeysToDelete(ctx.spawnEnv), ctx.skipCodexHomeEnv ? CODEX_HOME_ENV_KEYS : [], // Why: the persistent daemon compares its own merged CODEX_HOME pair; @@ -193,5 +196,6 @@ export async function buildPtyIpcSpawnOptions( ctx.releaseWorktreeSpawn = acquireWorktreeSpawn ? await acquireWorktreeSpawn.call(runtime, args.worktreeId) : undefined + await preparePtyIpcWslOptions(ctx) return null } diff --git a/src/main/ipc/pty/ipc/spawn-preflight.ts b/src/main/ipc/pty/ipc/spawn-preflight.ts index 07fbbf942b87..d936e9b8f32d 100644 --- a/src/main/ipc/pty/ipc/spawn-preflight.ts +++ b/src/main/ipc/pty/ipc/spawn-preflight.ts @@ -1,3 +1,4 @@ +import { preparePtyIpcWslRoute } from './spawn-wsl-route' import { isWslShellName, resolveLocalWindowsTerminalRuntimeOptions @@ -7,15 +8,10 @@ import { isClaudeAuthSwitchInProgress } from '../../../claude-accounts/live-pty- import { CLAUDE_AUTH_SWITCH_IN_PROGRESS_MESSAGE } from '../../../claude-accounts/environment' import { mintPtySessionId } from '../../../daemon/pty-session-id' import { resolveWslSessionContext } from '../../../daemon/wsl-session-context' -import { LocalPtyProvider } from '../../../providers/local-pty-provider' import { normalizeWindowsTerminalCwd } from '../../../providers/windows-shell-args' import { wslUncDirectoryExistsAsync } from '../../../wsl' import { getCodexSelectionTargetForPty } from '../host-env/codex-home' -import { - isClaudeLaunchCommand, - recoverFreshSpawnProviderRouting, - routesFreshSpawnsToLocalProvider -} from '../host-env/fresh-spawn-routing' +import { isClaudeLaunchCommand } from '../host-env/fresh-spawn-routing' import { getAppPtyId, getProvider, getRelayPtyId } from '../provider/registry' import type { PtyIpcSpawnState } from './spawn-state' @@ -23,10 +19,7 @@ export async function preparePtyIpcSpawnPreflight(ctx: PtyIpcSpawnState): Promis const args = ctx.args // Establish daemon identity before the first await so hidden delivery is gated before byte zero. ctx.provider = getProvider(args.connectionId) - ctx.isDaemonHostSpawn = - !args.connectionId && - !(ctx.provider instanceof LocalPtyProvider) && - !routesFreshSpawnsToLocalProvider(ctx.provider) + ctx.isDaemonHostSpawn = !args.connectionId ctx.isMintedSessionId = args.sessionId === undefined && ctx.isDaemonHostSpawn ctx.effectiveSessionId = args.sessionId ?? (ctx.isDaemonHostSpawn ? mintPtySessionId(args.worktreeId) : undefined) @@ -156,46 +149,6 @@ export async function preparePtyIpcSpawnPreflight(ctx: PtyIpcSpawnState): Promis ctx.startupCwdFallback = didFallbackToWorkspaceRootCwd && ctx.cwd ? { kind: 'worktree', cwd: ctx.cwd } : undefined } - ctx.spawnTiming.mark('preflight') - const freshSpawnRecovery = ctx.preAdoptedStablePane - ? undefined - : recoverFreshSpawnProviderRouting(ctx.provider, args.connectionId, args.sessionId) - if (freshSpawnRecovery) { - await freshSpawnRecovery - const previousHiddenMarkId = ctx.preSpawnHiddenMarkId - ctx.isDaemonHostSpawn = - !args.connectionId && - !(ctx.provider instanceof LocalPtyProvider) && - !routesFreshSpawnsToLocalProvider(ctx.provider) - ctx.isMintedSessionId = args.sessionId === undefined && ctx.isDaemonHostSpawn - ctx.effectiveSessionId = - args.sessionId ?? (ctx.isDaemonHostSpawn ? mintPtySessionId(args.worktreeId) : undefined) - ctx.effectiveSessionAppId = - ctx.effectiveSessionId !== undefined - ? getAppPtyId(args.connectionId, ctx.effectiveSessionId) - : undefined - ctx.effectiveSessionRelayId = - ctx.effectiveSessionId !== undefined - ? getRelayPtyId(args.connectionId, ctx.effectiveSessionId) - : undefined - ctx.preSpawnHiddenMarkId = - ctx.initiallyHidden && ctx.isDaemonHostSpawn && ctx.effectiveSessionAppId !== undefined - ? ctx.effectiveSessionAppId - : null - if (previousHiddenMarkId !== ctx.preSpawnHiddenMarkId) { - if (previousHiddenMarkId !== null) { - ctx.deps.transitionSpawnHiddenRendererPtyDeliveryState(previousHiddenMarkId, false) - } - if (ctx.preSpawnHiddenMarkId !== null) { - ctx.deps.transitionSpawnHiddenRendererPtyDeliveryState(ctx.preSpawnHiddenMarkId, true) - } - } - } - ctx.isClaudeLaunch = - !ctx.preAdoptedStablePane && !args.connectionId && isClaudeLaunchCommand(args.command) - if (ctx.isClaudeLaunch && isClaudeAuthSwitchInProgress()) { - throw new Error(CLAUDE_AUTH_SWITCH_IN_PROGRESS_MESSAGE) - } ctx.terminalRuntimeOptions = process.platform === 'win32' && !args.connectionId ? resolveLocalWindowsTerminalRuntimeOptions({ @@ -210,7 +163,6 @@ export async function preparePtyIpcSpawnPreflight(ctx: PtyIpcSpawnState): Promis (ctx.deps.getSettings?.()?.terminalDefaultShell?.trim() || undefined), terminalWindowsWslDistro: null } - const initialShellOverride = ctx.terminalRuntimeOptions.shellOverride // Why: daemon host-env setup needs a stable id BEFORE provider.spawn so buildPtyHostEnv hooks/Pi cleanup can run; daemon still honors opts.sessionId ?? mint(). // Note: sessionId is STABLE across daemon restarts by design — do NOT simplify to a fresh UUID per spawn; that orphans reconnectable state. // Why: only clear ids minted in THIS request on failure — a caller-supplied args.sessionId may name an existing PTY we must not clobber. @@ -222,14 +174,28 @@ export async function preparePtyIpcSpawnPreflight(ctx: PtyIpcSpawnState): Promis terminalWindowsWslDistro: ctx.terminalRuntimeOptions.terminalWindowsWslDistro })?.distro ?? null) : null - const initialSelectionTarget = getCodexSelectionTargetForPty( - initialShellOverride, - ctx.cwd, - ctx.expectedWslDistro - ) + await preparePtyIpcWslRoute(ctx) + ctx.spawnTiming.mark('preflight') + ctx.isClaudeLaunch = + !ctx.preAdoptedStablePane && + !args.connectionId && + (!ctx.wslGuest || ctx.wslGuest.fresh || ctx.wslGuest.coldRestore === true) && + isClaudeLaunchCommand(args.command) + if (ctx.isClaudeLaunch && isClaudeAuthSwitchInProgress()) { + throw new Error(CLAUDE_AUTH_SWITCH_IN_PROGRESS_MESSAGE) + } + const initialSelectionTarget = ctx.wslGuest + ? { runtime: 'wsl' as const, wslDistro: ctx.wslGuest.execution.distro } + : getCodexSelectionTargetForPty( + ctx.terminalRuntimeOptions.shellOverride, + ctx.cwd, + ctx.expectedWslDistro + ) ctx.claudeAuth = ctx.isClaudeLaunch && ctx.deps.prepareClaudeAuth - ? await ctx.deps.prepareClaudeAuth(initialSelectionTarget) + ? ctx.wslGuest + ? await ctx.deps.prepareClaudeAuth(initialSelectionTarget, ctx.wslGuest.execution) + : await ctx.deps.prepareClaudeAuth(initialSelectionTarget) : null ctx.spawnTiming.mark('auth') } diff --git a/src/main/ipc/pty/ipc/spawn-state.ts b/src/main/ipc/pty/ipc/spawn-state.ts index b1240c4ec6e7..a0a61d5adf0e 100644 --- a/src/main/ipc/pty/ipc/spawn-state.ts +++ b/src/main/ipc/pty/ipc/spawn-state.ts @@ -1,4 +1,6 @@ import type { IPtyProvider, PtySpawnOptions, PtySpawnResult } from '../../../providers/types' +import type { WslDaemonSpawnRoute } from '../../../wsl/wsl-daemon-spawn-route' +import type { BuildPtyHostEnvOptions } from '../host-env/types' import type { CodexPaneHomeRoute } from '../../../codex/codex-pane-account-registry' import type { CodexAccountSelectionTarget } from '../../../codex-accounts/runtime-selection' import type { SleepingAgentLaunchConfig } from '../../../../shared/agent-session-resume' @@ -36,6 +38,8 @@ export type PtyIpcSpawnState = { preparedProvisionalExecutionContext: boolean releaseWorktreeSpawn: (() => void) | undefined provider: IPtyProvider + wslGuest: WslDaemonSpawnRoute | null + guestHostEnvPolicy: BuildPtyHostEnvOptions | null preAdoptedStablePane: AdoptStablePaneResult | null isClaudeLaunch: boolean claudeAuth: ClaudeRuntimeAuthPreparation | null @@ -114,6 +118,8 @@ export function createPtyIpcSpawnState( preparedProvisionalExecutionContext: false, releaseWorktreeSpawn: undefined, provider: localProvider, + wslGuest: null, + guestHostEnvPolicy: null, preAdoptedStablePane: null, isClaudeLaunch: false, claudeAuth: null, diff --git a/src/main/ipc/pty/ipc/spawn-types.ts b/src/main/ipc/pty/ipc/spawn-types.ts index e14b469d1fb0..61dba036bb96 100644 --- a/src/main/ipc/pty/ipc/spawn-types.ts +++ b/src/main/ipc/pty/ipc/spawn-types.ts @@ -1,3 +1,5 @@ +import type { WslAccountExecutionContext } from '../../../wsl/wsl-account-execution-context' +import type { WslDaemonSessions } from '../../../wsl/wsl-daemon-sessions' import type { TuiAgent } from '../../../../shared/tui-agent' import type { AgentProviderSessionMetadata, @@ -86,6 +88,7 @@ export type PtySpawnIpcDeps = { getSelectedCodexHomePath?: GetSelectedCodexHomePath prepareClaudeAuth?: PrepareClaudeAuth options?: { + wslDaemonSessions?: WslDaemonSessions prepareCodexSessionResume?: PrepareCodexSessionResume onCodexHomePtySpawned?: (args: CodexHomePtySpawnedLifecycleArgs) => void } @@ -105,6 +108,7 @@ export type PtySpawnIpcDeps = { target: CodexAccountSelectionTarget launchEnv?: NodeJS.ProcessEnv workspacePath?: string + wslExecution?: WslAccountExecutionContext }) => PreparedCodexResumeHome | null noCodexResumeLaunch: (command: string | undefined) => CodexResumeLaunch resolveCodexResumeLaunch: ( diff --git a/src/main/ipc/pty/ipc/spawn-wsl-options.ts b/src/main/ipc/pty/ipc/spawn-wsl-options.ts new file mode 100644 index 000000000000..6408ac2bdde4 --- /dev/null +++ b/src/main/ipc/pty/ipc/spawn-wsl-options.ts @@ -0,0 +1,36 @@ +import { prepareWslGuestTerminalSpawn } from '../../../wsl/wsl-guest-terminal-preparation' +import { clearProviderPtyState } from '../provider/state-cleanup' +import type { PtyIpcSpawnState } from './spawn-state' + +export async function preparePtyIpcWslOptions(ctx: PtyIpcSpawnState): Promise { + if (!ctx.wslGuest || ctx.preAdoptedStablePane) { + return + } + if (!ctx.wslGuest.fresh && !ctx.wslGuest.coldRestore) { + ctx.spawnOptions.sessionId = ctx.effectiveSessionAppId + ctx.spawnOptions.attachOnly = true + return + } + if (!ctx.guestHostEnvPolicy || !ctx.effectiveSessionId) { + throw new Error('Guest terminal launch policy is unavailable') + } + if (ctx.wslGuest.coldRestore) { + ctx.spawnOptions.sessionId = ctx.effectiveSessionAppId + ctx.spawnOptions.isNewSession = false + ctx.spawnOptions.attachOnly = false + } + try { + ctx.spawnOptions = await prepareWslGuestTerminalSpawn( + ctx.wslGuest.prepared, + ctx.spawnOptions, + ctx.guestHostEnvPolicy, + undefined, + ctx.wslGuest.coldRestore ? 'confirmed-exited' : undefined + ) + } catch (error) { + if (ctx.isMintedSessionId) { + clearProviderPtyState(ctx.effectiveSessionId) + } + throw error + } +} diff --git a/src/main/ipc/pty/ipc/spawn-wsl-route.ts b/src/main/ipc/pty/ipc/spawn-wsl-route.ts new file mode 100644 index 000000000000..5a7bd8512de3 --- /dev/null +++ b/src/main/ipc/pty/ipc/spawn-wsl-route.ts @@ -0,0 +1,39 @@ +import { toAppWslPtyId, toRelayWslPtyId } from '../../../../shared/wsl-pty-id' +import { mintPtySessionId } from '../../../daemon/pty-session-id' +import { prepareWslDaemonSpawnRoute } from '../../../wsl/wsl-daemon-spawn-route' +import type { PtyIpcSpawnState } from './spawn-state' + +export async function preparePtyIpcWslRoute(ctx: PtyIpcSpawnState): Promise { + if (ctx.preAdoptedStablePane) { + return + } + const route = await prepareWslDaemonSpawnRoute({ + sessions: ctx.deps.options?.wslDaemonSessions, + connectionId: ctx.args.connectionId, + sessionId: ctx.args.sessionId, + distro: ctx.expectedWslDistro + }) + if (!route) { + return + } + ctx.wslGuest = route + ctx.provider = route.connection.provider + ctx.expectedWslDistro = route.connection.owner.distro + ctx.isDaemonHostSpawn = true + ctx.isMintedSessionId = route.fresh + ctx.effectiveSessionId = route.fresh + ? (ctx.effectiveSessionId ?? mintPtySessionId(ctx.args.worktreeId)) + : toRelayWslPtyId(route.connection.owner, ctx.args.sessionId!) + ctx.effectiveSessionAppId = toAppWslPtyId(route.connection.owner, ctx.effectiveSessionId) + ctx.effectiveSessionRelayId = ctx.effectiveSessionId + const previousHidden = ctx.preSpawnHiddenMarkId + ctx.preSpawnHiddenMarkId = ctx.initiallyHidden ? ctx.effectiveSessionAppId : null + if (previousHidden !== ctx.preSpawnHiddenMarkId) { + if (previousHidden) { + ctx.deps.transitionSpawnHiddenRendererPtyDeliveryState(previousHidden, false) + } + if (ctx.preSpawnHiddenMarkId) { + ctx.deps.transitionSpawnHiddenRendererPtyDeliveryState(ctx.preSpawnHiddenMarkId, true) + } + } +} diff --git a/src/main/ipc/pty/ipc/spawn-wsl-routing.test.ts b/src/main/ipc/pty/ipc/spawn-wsl-routing.test.ts new file mode 100644 index 000000000000..0c9a984cae31 --- /dev/null +++ b/src/main/ipc/pty/ipc/spawn-wsl-routing.test.ts @@ -0,0 +1,360 @@ +import * as ProviderStateCleanup from '../provider/state-cleanup' +import { ensureCodexStateDbBackfillRecoveryStarted } from '../../../codex/codex-state-db-backfill-recovery' +import type * as GuestSpawnOptions from '../../../wsl/wsl-guest-spawn-options' +import type * as GuestTerminalPreparation from '../../../wsl/wsl-guest-terminal-preparation' +import { prepareWslDaemonSpawnRoute } from '../../../wsl/wsl-daemon-spawn-route' +import { afterEach, beforeEach, expect, it, vi } from 'vitest' +import { getDefaultSettings } from '../../../../shared/constants' +import { toAppWslPtyId } from '../../../../shared/wsl-pty-id' +import { DaemonPtyAdapter } from '../../../daemon/daemon-pty-adapter' +import { WslDaemonPtyProvider } from '../../../wsl/wsl-daemon-pty-provider' +import { WslDaemonSessions } from '../../../wsl/wsl-daemon-sessions' +import { prepareWslGuestTerminalSpawn } from '../../../wsl/wsl-guest-terminal-preparation' +import { buildPtyHostEnv } from '../host-env/assembly' +import { + noCodexResumeLaunch, + resolveCodexResumeLaunch, + stripSequencedStartupResumeArgv +} from '../host-env/codex-resume' +import { createPtyIpcSpawnState } from './spawn-state' +import type { PtySpawnIpcArgs, PtySpawnIpcDeps } from './spawn-types' +import { preparePtyIpcSpawnPreflight } from './spawn-preflight' +import { assemblePtyIpcSpawnEnv } from './spawn-env' +import { buildPtyIpcSpawnOptions } from './spawn-options' +import { executePtyIpcSpawn } from './spawn-execute' +import { ptySizes } from '../delivery/visibility-state' +import { wslHookRelayManager } from '../../../agent-hooks/wsl-hook-relay-manager' + +vi.mock('../../../codex/codex-state-db-backfill-recovery', () => ({ + ensureCodexStateDbBackfillRecoveryStarted: vi.fn() +})) +vi.mock('../host-env/assembly', () => ({ buildPtyHostEnv: vi.fn((_id, env) => env) })) +vi.mock('../../../wsl/wsl-guest-spawn-options', async (original) => ({ + ...(await original()), + prepareWslGuestSpawnOptions: vi.fn(async (_owner, options) => options) +})) +vi.mock('../../../agent-hooks/wsl-hook-relay-manager', () => ({ + wslHookRelayManager: { + ensureForDistro: vi.fn(async () => {}), + getGuestEndpointFilePath: vi.fn(() => '/home/alice/.orca-wsl/agent-hooks/endpoint') + } +})) +vi.mock('../../../wsl', () => ({ getDefaultWslDistro: () => 'Ubuntu', parseWslPath: () => null })) +vi.mock('../../../wsl/wsl-guest-terminal-preparation', async (original) => { + const actual = await original() + return { ...actual, prepareWslGuestTerminalSpawn: vi.fn(actual.prepareWslGuestTerminalSpawn) } +}) +const platform = process.platform +const owner = { distro: 'Ubuntu', relayBuildId: 'daemon+artifact+alice' } +const endpoint = { + distro: 'Ubuntu', + distributionId: 'registration', + userName: 'alice', + userId: '1000', + home: '/home/alice', + runtime: '/bin/bun', + entry: '/daemon.js', + envBinary: '/usr/bin/env', + socket: '/socket', + tokenPath: '/token', + serverBuildId: 'artifact' +} +const execution = { distro: 'Ubuntu', userName: 'alice', userId: '1000', home: '/home/alice' } +const prepared = { owner, endpoint, entry: '/daemon.js', path: '/bin', artifactId: 'artifact' } + +function fixture(args: Partial = {}) { + const adapter = new DaemonPtyAdapter({ + guest: { + distro: 'Ubuntu', + defaultShell: '/bin/bash', + defaultCwd: '/home/alice', + profiles: [], + transport: { + readToken: () => 'unused', + connect: async () => { + throw new Error('unexpected connection') + } + } + } + }) + const provider = new WslDaemonPtyProvider(owner, adapter) + vi.spyOn(provider, 'probePtyLiveness').mockResolvedValue(true) + const connection = { owner, endpoint, provider } + const sessions = new WslDaemonSessions({ + profileScope: '/profile', + historyRoot: '/history', + store: { + getWslDaemonRecovery: () => null, + upsertWslDaemonRecovery: async () => {} + } + }) + const fresh = vi + .spyOn(sessions, 'prepareFresh') + .mockResolvedValue({ prepared, execution, connection }) + const reconnect = vi.spyOn(sessions, 'reconnect').mockResolvedValue(connection) + const auth = vi.fn(async () => ({ + envPatch: {}, + stripAuthEnv: false, + configDir: '/home/alice/.claude', + provenance: 'captured-user' + })) + const selected = vi.fn>(async () => null) + const prepareResume = vi.fn(() => null) + const deps: PtySpawnIpcDeps = { + options: { wslDaemonSessions: sessions }, + getSettings: () => ({ ...getDefaultSettings('/profile'), terminalWindowsDefaultShell: 'wsl' }), + prepareClaudeAuth: auth, + getSelectedCodexHomePath: selected, + getLocalPtyStartupPromise: () => undefined, + adoptStablePane: async () => null, + assertFolderWorkspacePtyPathUsable: () => {}, + resolvePtySpawnStartupCwd: (_id, cwd) => cwd, + localStartupCwdDirectoryExists: () => true, + prepareCodexResumeHome: prepareResume, + noCodexResumeLaunch, + resolveCodexResumeLaunch, + stripSequencedStartupResumeArgv, + reconcileSharedRuntimeResumeHome: async (_home, resolve) => (await resolve()) ?? '', + transitionSpawnHiddenRendererPtyDeliveryState: vi.fn(), + trustedTerminalHandleEnv: new Set(), + sendPtySpawnedToRenderer: () => {}, + syncPtyBackgroundedDelivery: () => {}, + stopReplacedPty: async () => {} + } + const ctx = createPtyIpcSpawnState(deps, { + cols: 91, + rows: 31, + shellOverride: 'wsl.exe', + cwd: '/home/alice', + initiallyHidden: true, + ...args + }) + const spawn = vi.spyOn(provider, 'spawn').mockImplementation(async (options) => ({ + id: options.sessionId?.startsWith('wsl:') + ? options.sessionId + : toAppWslPtyId(owner, options.sessionId!), + wslDistro: 'Ubuntu', + ...(options.attachOnly ? { isReattach: true } : {}) + })) + return { ctx, deps, provider, fresh, reconnect, auth, selected, prepareResume, spawn, adapter } +} +beforeEach(() => { + Object.defineProperty(process, 'platform', { value: 'win32' }) + vi.clearAllMocks() +}) +afterEach(() => { + Object.defineProperty(process, 'platform', { value: platform }) + ptySizes.clear() + vi.restoreAllMocks() +}) + +it('captures the owner before auth and builds guest host-env exactly once with raw ID', async () => { + const { ctx, deps, fresh, auth, selected, prepareResume, spawn, adapter } = fixture({ + command: 'claude' + }) + await preparePtyIpcSpawnPreflight(ctx) + expect(fresh).toHaveBeenCalledWith('Ubuntu', undefined) + expect(auth).toHaveBeenCalledWith({ runtime: 'wsl', wslDistro: 'Ubuntu' }, execution) + expect(fresh.mock.invocationCallOrder[0]).toBeLessThan(auth.mock.invocationCallOrder[0]) + expect(ctx.effectiveSessionId).not.toMatch(/^wsl:/) + expect(ctx.effectiveSessionAppId).toBe(toAppWslPtyId(owner, ctx.effectiveSessionId!)) + await assemblePtyIpcSpawnEnv(ctx) + expect(selected).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + expect.objectContaining({ wslExecution: execution }) + ) + expect(prepareResume).toHaveBeenCalledWith(expect.objectContaining({ wslExecution: execution })) + expect(buildPtyHostEnv).not.toHaveBeenCalled() + await buildPtyIpcSpawnOptions(ctx) + expect(buildPtyHostEnv).toHaveBeenCalledOnce() + expect(buildPtyHostEnv).toHaveBeenCalledWith( + ctx.effectiveSessionId, + expect.anything(), + expect.objectContaining({ isWsl: true, wslUser: 'alice' }) + ) + expect(prepareWslGuestTerminalSpawn).toHaveBeenCalledOnce() + expect(ptySizes.get(ctx.effectiveSessionAppId!)).toEqual({ cols: 91, rows: 31 }) + expect(ptySizes.has(ctx.effectiveSessionId!)).toBe(false) + expect(deps.transitionSpawnHiddenRendererPtyDeliveryState).toHaveBeenLastCalledWith( + ctx.effectiveSessionAppId, + true + ) + await executePtyIpcSpawn(ctx) + expect(spawn).toHaveBeenCalledWith( + expect.objectContaining({ sessionId: ctx.effectiveSessionId, isNewSession: true }) + ) + expect(ctx.result.id).toBe(ctx.effectiveSessionAppId) + adapter.dispose() +}) + +it('reattaches the persisted encoded owner without auth, host-env or default-user hooks', async () => { + const id = toAppWslPtyId(owner, 'guest-pty') + const { ctx, fresh, reconnect, auth, selected, spawn, adapter } = fixture({ + sessionId: id, + command: 'claude' + }) + await preparePtyIpcSpawnPreflight(ctx) + await assemblePtyIpcSpawnEnv(ctx) + await buildPtyIpcSpawnOptions(ctx) + expect(fresh).not.toHaveBeenCalled() + expect(reconnect).toHaveBeenCalledWith(expect.objectContaining(owner), undefined) + expect(auth).not.toHaveBeenCalled() + expect(selected).not.toHaveBeenCalled() + expect(buildPtyHostEnv).not.toHaveBeenCalled() + await executePtyIpcSpawn(ctx) + expect(spawn).toHaveBeenCalledWith(expect.objectContaining({ sessionId: id, attachOnly: true })) + expect(wslHookRelayManager.ensureForDistro).toHaveBeenCalledWith( + 'Ubuntu', + undefined, + undefined, + 'alice' + ) + adapter.dispose() +}) + +it('keeps legacy raw session IDs on their original provider', async () => { + const { ctx, provider, fresh, reconnect, adapter } = fixture({ sessionId: 'legacy-session' }) + await preparePtyIpcSpawnPreflight(ctx) + expect(ctx.wslGuest).toBeNull() + expect(ctx.provider).not.toBe(provider) + expect(fresh).not.toHaveBeenCalled() + expect(reconnect).not.toHaveBeenCalled() + adapter.dispose() +}) + +it('limits fresh routing to Windows WSL and distinguishes explicitly fresh runtime IDs', async () => { + const { fresh, reconnect, adapter } = fixture() + const sessions = { prepareFresh: fresh, reconnect } + await expect( + prepareWslDaemonSpawnRoute({ sessions, distro: 'Ubuntu', platform: 'linux' }) + ).resolves.toBeNull() + await expect( + prepareWslDaemonSpawnRoute({ + sessions, + distro: 'Ubuntu', + connectionId: 'ssh', + platform: 'win32' + }) + ).resolves.toBeNull() + await expect( + prepareWslDaemonSpawnRoute({ + sessions, + distro: 'Ubuntu', + sessionId: 'raw-existing', + platform: 'win32' + }) + ).resolves.toBeNull() + expect(fresh).not.toHaveBeenCalled() + await expect( + prepareWslDaemonSpawnRoute({ + sessions, + distro: 'Ubuntu', + sessionId: 'raw-fresh', + isNewSession: true, + platform: 'win32' + }) + ).resolves.toMatchObject({ fresh: true }) + expect(fresh).toHaveBeenCalledOnce() + adapter.dispose() +}) + +it('fails closed when an encoded owner has no profile service', async () => { + await expect( + prepareWslDaemonSpawnRoute({ + sessionId: toAppWslPtyId(owner, 'existing'), + platform: 'win32' + }) + ).rejects.toThrow('unavailable') +}) + +it('passes captured user to Codex selection and avoids desktop backfill probes of guest paths', async () => { + const { ctx, selected, prepareResume, adapter } = fixture({ + command: 'codex', + launchAgent: 'codex' + }) + selected.mockResolvedValue('/home/alice/.codex') + await preparePtyIpcSpawnPreflight(ctx) + await assemblePtyIpcSpawnEnv(ctx) + expect(selected).toHaveBeenCalledWith( + { runtime: 'wsl', wslDistro: 'Ubuntu' }, + undefined, + expect.objectContaining({ wslExecution: execution, launchAgent: 'codex' }) + ) + expect(prepareResume).toHaveBeenCalledWith(expect.objectContaining({ wslExecution: execution })) + expect(ensureCodexStateDbBackfillRecoveryStarted).not.toHaveBeenCalled() + expect(buildPtyHostEnv).not.toHaveBeenCalled() + adapter.dispose() +}) + +it('cold restores the same absent guest ID with captured-user auth and guest environment preparation', async () => { + const id = toAppWslPtyId(owner, 'guest-pty') + const { ctx, provider, fresh, auth, selected, spawn, adapter } = fixture({ + sessionId: id, + command: 'claude --resume current' + }) + vi.mocked(provider.probePtyLiveness).mockResolvedValue(false) + await preparePtyIpcSpawnPreflight(ctx) + await assemblePtyIpcSpawnEnv(ctx) + await buildPtyIpcSpawnOptions(ctx) + expect(ctx.wslGuest).toMatchObject({ fresh: false, coldRestore: true }) + expect(fresh).not.toHaveBeenCalled() + expect(auth).toHaveBeenCalledWith(expect.anything(), execution) + expect(selected).toHaveBeenCalled() + expect(prepareWslGuestTerminalSpawn).toHaveBeenCalledWith( + expect.objectContaining({ owner, endpoint }), + expect.objectContaining({ sessionId: id, isNewSession: false, attachOnly: false }), + expect.anything(), + undefined, + 'confirmed-exited' + ) + await executePtyIpcSpawn(ctx) + expect(spawn).toHaveBeenCalledWith( + expect.objectContaining({ + sessionId: id, + isNewSession: false, + attachOnly: false, + command: 'claude --resume current' + }) + ) + adapter.dispose() +}) +it('refuses retained guest restore when the admitted owner cannot prove session absence', async () => { + const { ctx, provider, fresh, auth, spawn, adapter } = fixture({ + sessionId: toAppWslPtyId(owner, 'guest-pty') + }) + vi.mocked(provider.probePtyLiveness).mockResolvedValue(null) + await expect(preparePtyIpcSpawnPreflight(ctx)).rejects.toThrow('unverifiable') + expect(fresh).not.toHaveBeenCalled() + expect(auth).not.toHaveBeenCalled() + expect(spawn).not.toHaveBeenCalled() + adapter.dispose() +}) + +it.each([false, true])( + 'preserves retained provider state on preparation failure (fresh=%s)', + async (freshSession) => { + const id = toAppWslPtyId(owner, 'retained-after-failure') + const { ctx, provider, spawn, adapter } = fixture(freshSession ? {} : { sessionId: id }) + vi.mocked(provider.probePtyLiveness).mockResolvedValue(false) + const clear = vi.spyOn(ProviderStateCleanup, 'clearProviderPtyState') + try { + await preparePtyIpcSpawnPreflight(ctx) + await assemblePtyIpcSpawnEnv(ctx) + vi.mocked(prepareWslGuestTerminalSpawn).mockRejectedValueOnce( + new Error('guest preparation failed') + ) + await expect(buildPtyIpcSpawnOptions(ctx)).rejects.toThrow('guest preparation failed') + expect(spawn).not.toHaveBeenCalled() + if (freshSession) { + expect(clear).toHaveBeenCalledExactlyOnceWith(ctx.effectiveSessionId) + } else { + expect(clear).not.toHaveBeenCalled() + expect(ctx.effectiveSessionAppId).toBe(id) + expect(ctx.isMintedSessionId).toBe(false) + } + } finally { + adapter.dispose() + } + } +) diff --git a/src/main/ipc/pty/kill-all.ts b/src/main/ipc/pty/kill-all.ts index 307bfaeabb23..097643f271b7 100644 --- a/src/main/ipc/pty/kill-all.ts +++ b/src/main/ipc/pty/kill-all.ts @@ -1,11 +1,2 @@ -import { LocalPtyProvider } from '../../providers/local-pty-provider' -import { localProvider } from './provider/registry' - -/** - * Kill in-process local PTYs. Daemon-backed PTYs are preserved by daemon disconnect. - */ -export function killAllPty(): void { - if (localProvider instanceof LocalPtyProvider) { - localProvider.killAll() - } -} +/** Daemon-owned terminals survive app quit; shutdown is explicit through their owner. */ +export function killAllPty(): void {} diff --git a/src/main/ipc/pty/pane/adopt-stable-wsl.test.ts b/src/main/ipc/pty/pane/adopt-stable-wsl.test.ts new file mode 100644 index 000000000000..db30acde9383 --- /dev/null +++ b/src/main/ipc/pty/pane/adopt-stable-wsl.test.ts @@ -0,0 +1,37 @@ +import { beforeEach, expect, it, vi } from 'vitest' +import { adoptStablePane } from './adopt-stable' +import { getProvider, getProviderForPty } from '../provider/registry' +import { attachStablePaneOwner, resolveStablePaneOwner } from './stable-owner' + +vi.mock('../provider/registry', () => ({ getProvider: vi.fn(), getProviderForPty: vi.fn() })) +vi.mock('./stable-owner', () => ({ + attachStablePaneOwner: vi.fn(), + resolveStablePaneOwner: vi.fn(), + stablePaneAdoptionsByOwnerKey: new Map() +})) +const args = { + cols: 80, + rows: 24, + worktreeId: 'folder', + tabId: 'tab', + leafId: '1b3f2c4d-5e6a-4b7c-8d9e-0f1a2b3c4d5e' +} +beforeEach(() => vi.clearAllMocks()) + +it('refuses an unavailable guest owner without attempting a host-native attach', async () => { + const id = 'wsl:Ubuntu@@build@@terminal' + vi.mocked(resolveStablePaneOwner).mockReturnValue({ + tabId: args.tabId, + leafId: args.leafId, + ptyId: id, + hasPersistedBinding: true + }) + vi.mocked(getProviderForPty).mockImplementation(() => { + throw new Error('WSL terminal owner is not connected') + }) + + await expect(adoptStablePane(undefined, undefined, args)).rejects.toThrow('not connected') + expect(getProviderForPty).toHaveBeenCalledWith(id) + expect(getProvider).not.toHaveBeenCalled() + expect(attachStablePaneOwner).not.toHaveBeenCalled() +}) diff --git a/src/main/ipc/pty/pane/adopt-stable.ts b/src/main/ipc/pty/pane/adopt-stable.ts index 7c1a7d5c8185..89381b33879f 100644 --- a/src/main/ipc/pty/pane/adopt-stable.ts +++ b/src/main/ipc/pty/pane/adopt-stable.ts @@ -1,7 +1,7 @@ import type { OrcaRuntimeService } from '../../../runtime/orca-runtime' import type { Store } from '../../../persistence' import { makePaneKey } from '../../../../shared/stable-pane-id' -import { getProvider } from '../provider/registry' +import { getProvider, getProviderForPty } from '../provider/registry' import { makePaneSpawnReservationKey, paneSpawnReservationsByOwnerKey } from './spawn-reservation' import { attachStablePaneOwner, @@ -60,7 +60,9 @@ export async function adoptStablePane( const adoption = attachStablePaneOwner({ runtime, store, - provider: getProvider(args.connectionId), + provider: owner.ptyId.startsWith('wsl:') + ? getProviderForPty(owner.ptyId) + : getProvider(args.connectionId), spawnOptions: { cols: args.cols, rows: args.rows, diff --git a/src/main/ipc/pty/pane/stable-owner-wsl-routing.test.ts b/src/main/ipc/pty/pane/stable-owner-wsl-routing.test.ts new file mode 100644 index 000000000000..f1de98662a56 --- /dev/null +++ b/src/main/ipc/pty/pane/stable-owner-wsl-routing.test.ts @@ -0,0 +1,70 @@ +import { afterEach, expect, it, vi } from 'vitest' +import { spawnForStablePane } from './stable-owner' +import { registerWslPtyProvider } from '../provider/registry' +import { createUnavailablePtyProvider } from '../../../providers/unavailable-pty-provider' +import { toAppWslPtyId } from '../../../../shared/wsl-pty-id' +import type { PtySpawnResult } from '../../../providers/types' + +const guestOwner = { distro: 'Ubuntu', relayBuildId: 'build+user' } +const id = toAppWslPtyId(guestOwner, 'pty2:guest:1') +const owner = { tabId: 'tab', leafId: 'leaf', ptyId: id } +const releases: (() => void)[] = [] +afterEach(() => releases.splice(0).forEach((release) => release())) +const native = () => ({ ...createUnavailablePtyProvider(), spawn: vi.fn() }) + +it('uses the late guest owner instead of the provider selected for a fresh native spawn', async () => { + const host = native() + const spawn = vi.fn(async () => ({ id, isReattach: true })) + releases.push(registerWslPtyProvider(guestOwner, { ...native(), spawn })) + const result = await spawnForStablePane({ + runtime: undefined, + provider: host, + owner, + spawnOptions: { cols: 80, rows: 24 } + }) + expect(result.result.id).toBe(id) + expect(spawn).toHaveBeenCalledWith(expect.objectContaining({ sessionId: id, attachOnly: true })) + expect(host.spawn).not.toHaveBeenCalled() +}) + +it('does not replace a disconnected guest with a fresh host terminal', async () => { + const host = native() + await expect( + spawnForStablePane({ + runtime: undefined, + provider: host, + owner, + spawnOptions: { cols: 80, rows: 24 } + }) + ).rejects.toThrow('not connected') + expect(host.spawn).not.toHaveBeenCalled() +}) + +it.each(['reply', 'failure'] as const)( + 'refuses a retired provider whose pending attach returns %s', + async (outcome) => { + const host = native() + let finish = () => {} + const spawn = vi.fn( + () => + new Promise((resolve, reject) => { + finish = () => + outcome === 'reply' + ? resolve({ id, isReattach: true }) + : reject(new Error('retired connection failure')) + }) + ) + const release = registerWslPtyProvider(guestOwner, { ...native(), spawn }) + releases.push(release) + const pending = spawnForStablePane({ + runtime: undefined, + provider: host, + owner, + spawnOptions: { cols: 80, rows: 24 } + }) + release() + finish() + await expect(pending).rejects.toThrow('owner changed during attach') + expect(host.spawn).not.toHaveBeenCalled() + } +) diff --git a/src/main/ipc/pty/pane/stable-owner.ts b/src/main/ipc/pty/pane/stable-owner.ts index 83ebdd50364d..0b78a727725c 100644 --- a/src/main/ipc/pty/pane/stable-owner.ts +++ b/src/main/ipc/pty/pane/stable-owner.ts @@ -13,10 +13,14 @@ import { TerminalHostGoneError, TerminalSessionOwnerUnverifiedError } from '../../../daemon/daemon-errors' +import { stablePaneAttachProvider } from './stable-pane-attach-provider' import { ptyIncarnationById, ptyOwnership } from '../provider/ownership-state' import { isHostReportedPtyAbsenceError, isObservedPtyExitEvidence } from '../provider/liveness' import { clearProviderPtyState } from '../provider/state-cleanup' -import { spawnCommitBindingOrigin } from '../../../persistence/loading-store/pty-binding-span' +export { + stablePanePersistenceFence, + persistAdmittedStablePaneBinding +} from './stable-pane-binding-persistence' export type StablePaneOwner = { handle?: string @@ -180,52 +184,11 @@ export type StablePaneSpawnContext = { onFreshSpawn?: (result: PtySpawnResult) => void } -export function stablePanePersistenceFence( - owner: StablePaneOwner | null -): { ptyId: string; incarnationId?: string } | undefined { - return owner?.hasPersistedBinding - ? { - ptyId: owner.ptyId, - ...(owner.persistedIncarnationId ? { incarnationId: owner.persistedIncarnationId } : {}) - } - : undefined -} - -export async function persistAdmittedStablePaneBinding(args: { - store: Store | undefined - owner: StablePaneOwner | null - result: PtySpawnResult - worktreeId: string | undefined - startupCwd: string | undefined - connectionId: string | null | undefined -}): Promise { - const expectedBinding = stablePanePersistenceFence(args.owner) - if (!args.store || !args.owner || !args.worktreeId || !expectedBinding) { - return false - } - const persisted = await args.store.persistPtyBinding( - { - worktreeId: args.worktreeId, - tabId: args.owner.tabId, - leafId: args.owner.leafId, - ptyId: args.result.id, - ...(args.result.incarnationId ? { incarnationId: args.result.incarnationId } : {}), - ...(args.startupCwd ? { startupCwd: args.startupCwd } : {}), - expectedBinding, - origin: spawnCommitBindingOrigin(args.result) - }, - args.connectionId ? toSshExecutionHostId(args.connectionId) : undefined - ) - if (persisted === false) { - throw new Error('terminal_pane_owner_changed') - } - return true -} - export async function attachStablePaneOwner( args: StablePaneSpawnContext & { owner: StablePaneOwner } ): Promise<{ result: PtySpawnResult; owner: StablePaneOwner } | null> { - const { owner, provider, runtime, spawnOptions } = args + const { owner, runtime, spawnOptions } = args + const { provider, assertOwnerConnected } = stablePaneAttachProvider(owner.ptyId, args.provider) let result: PtySpawnResult try { result = await provider.spawn({ @@ -245,6 +208,7 @@ export async function attachStablePaneOwner( onPtySpawnCommitted: undefined }) } catch (error) { + assertOwnerConnected() if (error instanceof TerminalSessionOwnerUnverifiedError) { throw new Error('terminal_pane_owner_unverified') } @@ -265,11 +229,7 @@ export async function attachStablePaneOwner( ) { throw new Error('terminal_pane_owner_changed') } - // `pty.attach` answers absent both for a pid the relay probed and found gone and for an id its - // session map never had — every id minted before a relay restart, checked against nothing. Only - // the marked half observed the process, so only it may certify a death; the rest publishes the - // stop sentinel its sibling handlePtyReattachFailure publishes, which every reader resolves to - // `stop_unverified` (docs/reference/ssh-execution-boundary.md). + // Only observed exit evidence certifies death; relay-map absence remains unverifiable. runtime?.onPtyExit( owner.ptyId, UNVERIFIED_PROCESS_EXIT_CODE, @@ -289,6 +249,7 @@ export async function attachStablePaneOwner( } return null } + assertOwnerConnected() if ( result.id !== owner.ptyId || result.isReattach !== true || diff --git a/src/main/ipc/pty/pane/stable-pane-attach-provider.ts b/src/main/ipc/pty/pane/stable-pane-attach-provider.ts new file mode 100644 index 000000000000..c3576b299b1b --- /dev/null +++ b/src/main/ipc/pty/pane/stable-pane-attach-provider.ts @@ -0,0 +1,16 @@ +import type { IPtyProvider } from '../../../providers/types' +import { getProviderForPty, tryGetProviderForPty } from '../provider/registry' + +/** A late guest owner overrides the fresh-spawn provider and fences asynchronous replies. */ +export function stablePaneAttachProvider(id: string, freshProvider: IPtyProvider) { + const guestOwner = id.startsWith('wsl:') + const provider = guestOwner ? getProviderForPty(id) : freshProvider + return { + provider, + assertOwnerConnected() { + if (guestOwner && tryGetProviderForPty(id) !== provider) { + throw new Error('WSL terminal owner changed during attach') + } + } + } +} diff --git a/src/main/ipc/pty/pane/stable-pane-binding-persistence.ts b/src/main/ipc/pty/pane/stable-pane-binding-persistence.ts new file mode 100644 index 000000000000..00560eecc7db --- /dev/null +++ b/src/main/ipc/pty/pane/stable-pane-binding-persistence.ts @@ -0,0 +1,47 @@ +import type { Store } from '../../../persistence' +import type { PtySpawnResult } from '../../../providers/types' +import { toSshExecutionHostId } from '../../../../shared/execution-host' +import { spawnCommitBindingOrigin } from '../../../persistence/loading-store/pty-binding-span' +import type { StablePaneOwner } from './stable-owner' + +export function stablePanePersistenceFence( + owner: StablePaneOwner | null +): { ptyId: string; incarnationId?: string } | undefined { + return owner?.hasPersistedBinding + ? { + ptyId: owner.ptyId, + ...(owner.persistedIncarnationId ? { incarnationId: owner.persistedIncarnationId } : {}) + } + : undefined +} + +export async function persistAdmittedStablePaneBinding(args: { + store: Store | undefined + owner: StablePaneOwner | null + result: PtySpawnResult + worktreeId: string | undefined + startupCwd: string | undefined + connectionId: string | null | undefined +}): Promise { + const expectedBinding = stablePanePersistenceFence(args.owner) + if (!args.store || !args.owner || !args.worktreeId || !expectedBinding) { + return false + } + const persisted = await args.store.persistPtyBinding( + { + worktreeId: args.worktreeId, + tabId: args.owner.tabId, + leafId: args.owner.leafId, + ptyId: args.result.id, + ...(args.result.incarnationId ? { incarnationId: args.result.incarnationId } : {}), + ...(args.startupCwd ? { startupCwd: args.startupCwd } : {}), + expectedBinding, + origin: spawnCommitBindingOrigin(args.result) + }, + args.connectionId ? toSshExecutionHostId(args.connectionId) : undefined + ) + if (persisted === false) { + throw new Error('terminal_pane_owner_changed') + } + return true +} diff --git a/src/main/ipc/pty/provider/bind-listeners-wsl.test.ts b/src/main/ipc/pty/provider/bind-listeners-wsl.test.ts new file mode 100644 index 000000000000..5c36e6ddea80 --- /dev/null +++ b/src/main/ipc/pty/provider/bind-listeners-wsl.test.ts @@ -0,0 +1,197 @@ +import { + isDaemonRestartInFlight, + runCoalescedDaemonRestart +} from '../../../daemon/daemon-restart-state' +import { afterEach, expect, it, vi } from 'vitest' +import { bindProviderListeners } from './bind-listeners' +import { setRebindProviderListeners, unbindLocalProviderListeners } from './listener-lifecycle' +import { + getLocalPtyProvider, + setLocalPtyProvider, + registerWslPtyProvider, + registerSshPtyProvider, + unregisterSshPtyProvider +} from './registry' +import { createUnavailablePtyProvider } from '../../../providers/unavailable-pty-provider' +import type { IPtyProvider } from '../../../providers/types' + +const original = getLocalPtyProvider() +const releases: (() => void)[] = [] +afterEach(() => { + setRebindProviderListeners(null) + unbindLocalProviderListeners() + releases.splice(0).forEach((release) => release()) + unregisterSshPtyProvider('remote') + setLocalPtyProvider(original) +}) +function provider() { + const listeners = new Set[0]>() + const exits = new Set[0]>() + return { + value: { + ...createUnavailablePtyProvider(), + onExit(listener: Parameters[0]) { + exits.add(listener) + return () => { + exits.delete(listener) + } + }, + onData(listener: Parameters[0]) { + listeners.add(listener) + return () => { + listeners.delete(listener) + } + } + }, + listeners, + exits, + exit: (id: string) => exits.forEach((listener) => listener({ id, code: 7 })), + emit: (id: string) => listeners.forEach((listener) => listener({ id, data: 'output' })) + } +} +it('binds added guests once, excludes SSH and removes listeners across reload and disconnect', () => { + const native = provider(), + guest = provider(), + remote = provider() + setLocalPtyProvider(native.value) + registerSshPtyProvider('remote', remote.value) + const session = { + acceptPtyDataForRenderer: vi.fn(), + sendModelRestoreNeededMarker: vi.fn(), + consumeSyntheticKillExit: vi.fn(), + sendPtyExitToRenderer: vi.fn() + } + setRebindProviderListeners(() => bindProviderListeners(session)) + bindProviderListeners(session) + const release = registerWslPtyProvider({ distro: 'Ubuntu', relayBuildId: 'owner' }, guest.value) + releases.push(release) + bindProviderListeners(session) + expect(native.listeners.size).toBe(1) + expect(guest.listeners.size).toBe(1) + expect(remote.listeners.size).toBe(0) + native.emit('native') + guest.emit('guest') + expect(session.acceptPtyDataForRenderer).toHaveBeenCalledTimes(2) + release() + expect(guest.listeners.size).toBe(0) + expect(native.listeners.size).toBe(1) + guest.emit('retired') + expect(session.acceptPtyDataForRenderer).toHaveBeenCalledTimes(2) + unbindLocalProviderListeners() + expect(native.listeners.size).toBe(0) +}) + +it('delivers new guest data and exit before a pending restart settles', async () => { + const native = provider(), + guest = provider() + setLocalPtyProvider(native.value) + const session = { + acceptPtyDataForRenderer: vi.fn(), + sendModelRestoreNeededMarker: vi.fn(), + consumeSyntheticKillExit: vi.fn(), + sendPtyExitToRenderer: vi.fn() + } + setRebindProviderListeners(() => bindProviderListeners(session)) + bindProviderListeners(session) + let finish!: () => void + const pending = runCoalescedDaemonRestart(async () => { + await new Promise((resolve) => { + finish = resolve + }) + return { killedCount: 0 } + }) + try { + const release = registerWslPtyProvider( + { distro: 'Ubuntu', relayBuildId: 'during-restart' }, + guest.value + ) + releases.push(release) + expect(isDaemonRestartInFlight()).toBe(true) + native.emit('native') + guest.emit('guest') + guest.exit('guest') + expect(session.acceptPtyDataForRenderer).toHaveBeenCalledTimes(2) + expect(session.sendPtyExitToRenderer).toHaveBeenCalledWith({ id: 'guest', code: 7 }) + expect(native.listeners.size).toBe(1) + release() + expect(guest.listeners.size).toBe(0) + expect(guest.exits.size).toBe(0) + guest.emit('retired') + guest.exit('retired') + expect(session.acceptPtyDataForRenderer).toHaveBeenCalledTimes(2) + expect(session.sendPtyExitToRenderer).toHaveBeenCalledTimes(1) + } finally { + finish() + await pending + } +}) + +it.each([false, true])( + 'binds a guest arriving after the final rebind but before restart settles (failed=%s)', + async (failed) => { + const native = provider(), + guest = provider() + setLocalPtyProvider(native.value) + const session = { + acceptPtyDataForRenderer: vi.fn(), + sendModelRestoreNeededMarker: vi.fn(), + consumeSyntheticKillExit: vi.fn(), + sendPtyExitToRenderer: vi.fn() + } + const rebind = vi.fn(() => bindProviderListeners(session)) + setRebindProviderListeners(rebind) + let lateRegistered = false + const pending = runCoalescedDaemonRestart(async () => { + await Promise.resolve() + rebind() + queueMicrotask(() => { + expect(isDaemonRestartInFlight()).toBe(true) + releases.push( + registerWslPtyProvider({ distro: 'Ubuntu', relayBuildId: 'late' }, guest.value) + ) + expect(guest.listeners.size).toBe(1) + guest.emit('during-settlement') + guest.exit('during-settlement') + lateRegistered = true + }) + if (failed) { + throw new Error('restart failed') + } + return { killedCount: 0 } + }) + await (failed ? expect(pending).rejects.toThrow('restart failed') : pending) + await vi.waitFor(() => expect(guest.listeners.size).toBe(1)) + expect(lateRegistered).toBe(true) + expect(native.listeners.size).toBe(1) + guest.emit('guest') + expect(session.acceptPtyDataForRenderer).toHaveBeenCalledTimes(2) + expect(session.sendPtyExitToRenderer).toHaveBeenCalledWith({ id: 'during-settlement', code: 7 }) + expect(rebind).toHaveBeenCalledTimes(2) + } +) + +it('updates guest subscriptions immediately despite a later failed restart', async () => { + const rebind = vi.fn() + setRebindProviderListeners(rebind) + let fail!: (error: Error) => void + const pending = runCoalescedDaemonRestart( + () => + new Promise((_resolve, reject) => { + fail = reject + }) + ) + const rejection = expect(pending).rejects.toThrow('failed') + const release = registerWslPtyProvider( + { distro: 'Ubuntu', relayBuildId: 'failed' }, + provider().value + ) + releases.push(release) + release() + releases.push( + registerWslPtyProvider({ distro: 'Ubuntu', relayBuildId: 'survivor' }, provider().value) + ) + expect(rebind).toHaveBeenCalledTimes(3) + fail(new Error('failed')) + await rejection + expect(rebind).toHaveBeenCalledTimes(3) +}) diff --git a/src/main/ipc/pty/provider/bind-listeners.ts b/src/main/ipc/pty/provider/bind-listeners.ts index 31bacec82f7f..03d3bc2b1299 100644 --- a/src/main/ipc/pty/provider/bind-listeners.ts +++ b/src/main/ipc/pty/provider/bind-listeners.ts @@ -1,5 +1,4 @@ import { markClaudePtyExited } from '../../../claude-accounts/live-pty-gate' -import { LocalPtyProvider } from '../../../providers/local-pty-provider' import { isCurrentPtyExit, ptyOwnership } from './ownership-state' import { localBackgroundStreamUnsub, @@ -11,12 +10,30 @@ import { setLocalExitUnsub, setLocalWriteUnavailableUnsub } from './listener-lifecycle' -import { localProvider } from './registry' +import { registeredPtyProviders } from './registry' +import type { IPtyProvider } from '../../../providers/types' import { clearProviderPtyState } from './state-cleanup' import { providerSnapshotRequiredPtys } from '../delivery/visibility-state' import type { PtyIpcSession } from '../session' -export function bindProviderListeners(session: PtyIpcSession): void { +function subscribeLocalProviders(subscribe: (provider: IPtyProvider) => (() => void) | undefined) { + const cleanups = registeredPtyProviders() + .filter(({ connectionId }) => connectionId === null) + .map(({ provider }) => subscribe(provider)) + return () => cleanups.forEach((cleanup) => cleanup?.()) +} + +export function bindProviderListeners( + session: Pick< + PtyIpcSession, + | 'mainWindow' + | 'runtime' + | 'sendModelRestoreNeededMarker' + | 'acceptPtyDataForRenderer' + | 'consumeSyntheticKillExit' + | 'sendPtyExitToRenderer' + > +): void { localDataUnsub?.() localExitUnsub?.() localBackgroundStreamUnsub?.() @@ -26,92 +43,97 @@ export function bindProviderListeners(session: PtyIpcSession): void { // each affected pane here so background panes remount + re-attach too, not // just the pane whose write happened to detect the dead endpoint (STA-2373). setLocalWriteUnavailableUnsub( - localProvider.onWriteUnavailable?.((payload) => { - if ( - !session.mainWindow || - session.mainWindow.isDestroyed() || - (typeof session.mainWindow.webContents.isDestroyed === 'function' && - session.mainWindow.webContents.isDestroyed()) - ) { - return - } - session.mainWindow.webContents.send('pty:writeUnavailable', { id: payload.id }) - }) ?? null + subscribeLocalProviders((provider) => + provider.onWriteUnavailable?.((payload) => { + if ( + !session.mainWindow || + session.mainWindow.isDestroyed() || + (typeof session.mainWindow.webContents.isDestroyed === 'function' && + session.mainWindow.webContents.isDestroyed()) + ) { + return + } + session.mainWindow.webContents.send('pty:writeUnavailable', { id: payload.id }) + }) + ) ) // Daemon keep-tail thinning facts, in byte order with onData: markers flip transient-fact scan authority; a gap forces renderer restore from the snapshot. setLocalBackgroundStreamUnsub( - localProvider.onBackgroundStreamEvent?.((payload) => { - if (payload.kind === 'backgroundMarker') { - session.runtime?.setPtyTransientFactDelegation( - payload.id, - payload.background, - payload.scanSeedAnsi, - payload.mode2031PendingSubscribe - ) - return - } - if (payload.kind === 'dataGap') { - providerSnapshotRequiredPtys.add(payload.id) - session.runtime?.notePtyDataGap(payload.id, payload.sequenceChars ?? payload.droppedChars) - session.sendModelRestoreNeededMarker( - payload.id, - 'hidden-drop', - session.runtime?.getPtyOutputSequence(payload.id) - ) - return - } - session.runtime?.emitDaemonPtyTransientFact(payload.id, payload.fact) - }) ?? null + subscribeLocalProviders((provider) => + provider.onBackgroundStreamEvent?.((payload) => { + if (payload.kind === 'backgroundMarker') { + session.runtime?.setPtyTransientFactDelegation( + payload.id, + payload.background, + payload.scanSeedAnsi, + payload.mode2031PendingSubscribe + ) + return + } + if (payload.kind === 'dataGap') { + providerSnapshotRequiredPtys.add(payload.id) + session.runtime?.notePtyDataGap(payload.id, payload.sequenceChars ?? payload.droppedChars) + session.sendModelRestoreNeededMarker( + payload.id, + 'hidden-drop', + session.runtime?.getPtyOutputSequence(payload.id) + ) + return + } + session.runtime?.emitDaemonPtyTransientFact(payload.id, payload.fact) + }) + ) ) // Why: daemon providers lack configure().onData, so feed the runtime here or their tail buffer (terminal.read, agent-detection, mobile stream) stays empty. - const isLocalProvider = localProvider instanceof LocalPtyProvider setLocalDataUnsub( - localProvider.onData((payload) => { - const rawLength = payload.sequenceChars ?? payload.data.length - const outputSeq = isLocalProvider - ? session.runtime?.getPtyOutputSequence(payload.id) - : session.runtime?.onPtyData( - payload.id, - payload.data, - Date.now(), - rawLength, - payload.transformed - ) - session.acceptPtyDataForRenderer(payload, outputSeq) - }) + subscribeLocalProviders((provider) => + provider.onData((payload) => { + const rawLength = payload.sequenceChars ?? payload.data.length + const outputSeq = session.runtime?.onPtyData( + payload.id, + payload.data, + Date.now(), + rawLength, + payload.transformed + ) + session.acceptPtyDataForRenderer(payload, outputSeq) + }) + ) ) setLocalExitUnsub( - localProvider.onExit((payload) => { - if (!isCurrentPtyExit(payload)) { - return - } - const syntheticExit = session.consumeSyntheticKillExit(payload.id, payload.incarnationId) - if (!isLocalProvider) { - clearProviderPtyState(payload.id) - ptyOwnership.delete(payload.id) - markClaudePtyExited(payload.id) + subscribeLocalProviders((provider) => + provider.onExit((payload) => { + if (!isCurrentPtyExit(payload)) { + return + } + const syntheticExit = session.consumeSyntheticKillExit(payload.id, payload.incarnationId) + { + clearProviderPtyState(payload.id) + ptyOwnership.delete(payload.id) + markClaudePtyExited(payload.id) + if (syntheticExit) { + session.runtime?.markPtyStopRequested(payload.id) + } + session.runtime?.onPtyExit(payload.id, payload.code, payload.incarnationId, { + providerExitObserved: true, + ...(payload.cause ? { cause: payload.cause } : {}) + }) + } + // The control reply can overtake stream data; the physical exit must retire that late output. if (syntheticExit) { - session.runtime?.markPtyStopRequested(payload.id) + return } - session.runtime?.onPtyExit(payload.id, payload.code, payload.incarnationId, { - providerExitObserved: true, - ...(payload.cause ? { cause: payload.cause } : {}) + // Why not the whole payload: the exit cause is a main-process fact for the + // runtime's records; the renderer's pty:exit contract stays as it was. + session.sendPtyExitToRenderer({ + id: payload.id, + code: payload.code, + ...(payload.incarnationId ? { incarnationId: payload.incarnationId } : {}) }) - } - // The control reply can overtake stream data; the physical exit must retire that late output. - if (syntheticExit) { - return - } - // Why not the whole payload: the exit cause is a main-process fact for the - // runtime's records; the renderer's pty:exit contract stays as it was. - session.sendPtyExitToRenderer({ - id: payload.id, - code: payload.code, - ...(payload.incarnationId ? { incarnationId: payload.incarnationId } : {}) }) - }) + ) ) } diff --git a/src/main/ipc/pty/provider/local-configure.ts b/src/main/ipc/pty/provider/local-configure.ts deleted file mode 100644 index bf07eddf0bbb..000000000000 --- a/src/main/ipc/pty/provider/local-configure.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { inheritOmpLaunchEnvironment } from '../host-env/omp-launch-environment' -import { getAppEnvironment } from '../../../../shared/app-environment' -import type { OrcaRuntimeService } from '../../../runtime/orca-runtime' -import type { GlobalSettings } from '../../../../shared/global-settings-types' -import { isAgentStatusHooksEnabled } from '../../../agent-hooks/managed-agent-hook-controls' -import { isPwshAvailableAsync } from '../../../pwsh' -import { LocalPtyProvider } from '../../../providers/local-pty-provider' -import { - addOrcaWslInteropEnv, - stampWslOrchestrationCompatibilityHost -} from '../../../pty/wsl-orca-env' -import type { CodexAccountSelectionTarget } from '../../../codex-accounts/runtime-selection' -import { markClaudePtyExited } from '../../../claude-accounts/live-pty-gate' -import { buildPtyHostEnv } from '../host-env/assembly' -import { - getCompatibleSelectedCodexHomePath, - isCodexStatusHooksEnabled, - shouldStripInheritedOrcaCodexHome -} from '../host-env/codex-home' -import type { GetSelectedCodexHomePath } from '../host-env/types' -import { isCurrentPtyExit, ptyOwnership } from './ownership-state' -import { localProvider } from './registry' -import { clearProviderPtyState } from './state-cleanup' -import { awaitExplicitPiOmpGuestReadiness } from '../../../agent-hooks/wsl-pi-omp-guest-readiness' - -export function configureLocalPtyProvider(args: { - runtime?: OrcaRuntimeService - getSettings?: () => GlobalSettings - getSelectedCodexHomePath?: GetSelectedCodexHomePath - trustedTerminalHandleEnv: Set -}): void { - // Why: only LocalPtyProvider needs main-process hook injection; daemon-backed providers spawn subprocesses internally. - if (!(localProvider instanceof LocalPtyProvider)) { - return - } - const { runtime, getSettings, getSelectedCodexHomePath, trustedTerminalHandleEnv } = args - localProvider.configure({ - isHistoryEnabled: () => getSettings?.()?.terminalScopeHistoryByWorktree ?? true, - getWindowsShell: () => getSettings?.()?.terminalWindowsShell, - getDefaultShell: () => getSettings?.()?.terminalDefaultShell, - getWindowsPowerShellImplementation: () => - getSettings ? (getSettings()?.terminalWindowsPowerShellImplementation ?? 'auto') : undefined, - pwshAvailable: () => isPwshAvailableAsync(), - buildSpawnEnv: async (id, baseEnv, ctx) => { - const codexSelectionTarget: CodexAccountSelectionTarget = - ctx?.isWsl === true - ? { runtime: 'wsl', wslDistro: ctx.wslDistro ?? null } - : { runtime: 'host' } - const selectedCodexHomePath = getCompatibleSelectedCodexHomePath( - codexSelectionTarget, - ctx?.codexHomePathOverride - ? ctx.codexHomePathOverride.value - : ((await getSelectedCodexHomePath?.(codexSelectionTarget, baseEnv, { - workspacePath: ctx?.cwd, - launchAgent: ctx?.launchAgent - })) ?? null) - ) - const skipCodexHomeEnv = ctx?.isWsl === true && !selectedCodexHomePath - const ptySettings = getSettings?.() - await inheritOmpLaunchEnvironment(baseEnv, { - shellPath: ctx?.shellPath, - explicitEnv: ctx?.explicitEnv, - isWsl: ctx?.isWsl, - launchAgent: ctx?.launchAgent, - launchCommand: ctx?.command - }) - await awaitExplicitPiOmpGuestReadiness({ - isWsl: ctx?.isWsl === true, - distro: ctx?.wslDistro, - codexHomePath: selectedCodexHomePath, - launchAgent: ctx?.launchAgent, - launchCommand: ctx?.command - }) - const env = buildPtyHostEnv(id, baseEnv, { - isPackaged: getAppEnvironment().isPackaged(), - resourcesPath: process.resourcesPath, - userDataPath: getAppEnvironment().getPath('userData'), - selectedCodexHomePath, - skipCodexHomeEnv, - stripInheritedOrcaCodexHome: shouldStripInheritedOrcaCodexHome({ - target: codexSelectionTarget, - selectedCodexHomePath, - skipCodexHomeEnv, - settings: ptySettings - }), - launchCommand: ctx?.command, - launchAgent: ctx?.launchAgent, - isWsl: ctx?.isWsl, - wslDistro: ctx?.wslDistro ?? null, - agentStatusHooksEnabled: isAgentStatusHooksEnabled(ptySettings), - disabledTuiAgents: ptySettings?.disabledTuiAgents, - codexStatusHooksEnabled: isCodexStatusHooksEnabled(ptySettings), - networkProxySettings: ptySettings, - routeBrowserOpensToClient: runtime?.shouldRelayTerminalBrowserOpens?.() - }) - // Why: agents need their terminal handle at process start to self-identify in orchestration messages without an extra RPC. - const requestedHandle = baseEnv.ORCA_TERMINAL_HANDLE - const preAllocatedHandle = - requestedHandle && trustedTerminalHandleEnv.has(requestedHandle) - ? requestedHandle - : runtime?.preAllocateHandleForPty(id) - if (requestedHandle && requestedHandle !== preAllocatedHandle) { - delete env.ORCA_TERMINAL_HANDLE - } - if (preAllocatedHandle) { - env.ORCA_TERMINAL_HANDLE = preAllocatedHandle - } - stampWslOrchestrationCompatibilityHost( - env, - runtime?.getOrchestrationCompatibilityHostId?.(), - ctx?.isWsl === true ? ctx.wslDistro : null - ) - if (ctx?.isWsl === true) { - addOrcaWslInteropEnv(env) - } - return env - }, - onSpawned: (id, incarnationId) => runtime?.onPtySpawned(id, incarnationId), - onExit: (id, code, incarnationId, cause) => { - if (!isCurrentPtyExit({ id, incarnationId })) { - return - } - clearProviderPtyState(id) - ptyOwnership.delete(id) - markClaudePtyExited(id) - runtime?.onPtyExit(id, code, incarnationId, { - providerExitObserved: true, - ...(cause ? { cause } : {}) - }) - }, - onData: (id, data, timestamp, sequenceChars, transformed) => - runtime?.onPtyData(id, data, timestamp, sequenceChars ?? data.length, transformed) - }) -} diff --git a/src/main/ipc/pty/provider/local-inventory-authority.ts b/src/main/ipc/pty/provider/local-inventory-authority.ts new file mode 100644 index 000000000000..02b03e5ff3a4 --- /dev/null +++ b/src/main/ipc/pty/provider/local-inventory-authority.ts @@ -0,0 +1,86 @@ +import type { Store } from '../../../persistence' +import type { OrcaRuntimeService } from '../../../runtime/orca-runtime' +import type { IPtyProvider } from '../../../providers/types' +import { ptyOwnership } from './ownership-state' +import { registeredPtyProviders, tryGetProviderForPty } from './registry' + +export type LocalInventoryAuthorityDeps = { + store?: Pick + runtime?: Pick +} + +/** Persisted bindings still name an owner when its connector has left the registry. */ +export function createLocalInventoryAuthority(deps: LocalInventoryAuthorityDeps) { + const known = new Set(ptyOwnership.keys()) + const session = deps.store?.getWorkspaceSession() + for (const tabs of Object.values(session?.tabsByWorktree ?? {})) { + for (const tab of tabs) { + if (tab.ptyId) { + known.add(tab.ptyId) + } + } + } + for (const id of Object.values(session?.remoteSessionIdsByTabId ?? {})) { + if (id) { + known.add(id) + } + } + for (const layout of Object.values(session?.terminalLayoutsByTabId ?? {})) { + for (const id of Object.values(layout.ptyIdsByLeafId ?? {})) { + if (id) { + known.add(id) + } + } + } + const guestIds = [...known].filter((id) => id.startsWith('wsl:')) + const owners = new Map(guestIds.map((id) => [id, tryGetProviderForPty(id)])) + const localProviders = () => + new Set( + registeredPtyProviders() + .filter((entry) => entry.connectionId === null) + .map((entry) => entry.provider) + ) + const providers = localProviders() + let complete = true + const mark = (id: string, reason: string) => { + complete = false + deps.runtime?.markPtyLivenessUnverifiable?.(id, reason) + } + for (const [id, provider] of owners) { + if (!provider) { + mark(id, 'WSL terminal owner is not connected') + } + } + const isComplete = () => { + const current = localProviders() + if ( + current.size !== providers.size || + [...providers].some((provider) => !current.has(provider)) + ) { + complete = false + } + for (const [id, provider] of owners) { + if (provider !== tryGetProviderForPty(id)) { + mark(id, 'WSL terminal owner changed during inventory') + } + } + return complete + } + return { + failed(provider: IPtyProvider, error: unknown) { + complete = false + const reason = error instanceof Error ? error.message : String(error) + for (const id of guestIds) { + if (owners.get(id) === provider) { + mark(id, reason) + } + } + }, + isComplete, + assertComplete() { + if (!isComplete()) { + throw new Error('Local terminal inventory is unverifiable: a WSL owner is unavailable') + } + } + } +} diff --git a/src/main/ipc/pty/provider/registry.ts b/src/main/ipc/pty/provider/registry.ts index 2a3d3b162fd4..1f0d783cd698 100644 --- a/src/main/ipc/pty/provider/registry.ts +++ b/src/main/ipc/pty/provider/registry.ts @@ -1,14 +1,18 @@ -import { LocalPtyProvider } from '../../../providers/local-pty-provider' +import { parseAppWslPtyId, type WslPtyOwner } from '../../../../shared/wsl-pty-id' +import { wslPtyOwnerKey } from '../../../../shared/wsl-pty-consumer-recovery' +import { relayProvidersByGeneration } from '../../../providers/relay-pty-generation-registry' +import { createUnavailablePtyProvider } from '../../../providers/unavailable-pty-provider' import type { IPtyProvider } from '../../../providers/types' import { parseAppSshPtyId, toAppSshPtyId, toRelaySshPtyId } from '../../../providers/ssh-pty-id' import { ptyOwnership } from './ownership-state' +import { rebindLocalProviderListeners } from './listener-lifecycle' // ─── Provider Registry ────────────────────────────────────────────── // Routes PTY operations by connectionId (null = local provider). -export let localProvider: IPtyProvider = new LocalPtyProvider() +export let localProvider: IPtyProvider = createUnavailablePtyProvider() export const sshProviders = new Map() -export const sshProvidersByGeneration = new Map() +const wslProviders = new Map() export type RegisteredPtyProvider = { provider: IPtyProvider @@ -18,7 +22,8 @@ export type RegisteredPtyProvider = { export function registeredPtyProviders(): RegisteredPtyProvider[] { return [ { provider: localProvider, connectionId: null }, - ...Array.from(sshProviders, ([connectionId, provider]) => ({ provider, connectionId })) + ...Array.from(sshProviders, ([connectionId, provider]) => ({ provider, connectionId })), + ...Array.from(wslProviders.values(), (provider) => ({ provider, connectionId: null })) ] } @@ -38,7 +43,34 @@ export function getProvider(connectionId: string | null | undefined): IPtyProvid return provider } +export function registerWslPtyProvider(owner: WslPtyOwner, provider: IPtyProvider): () => void { + const key = wslPtyOwnerKey(owner) + const previous = wslProviders.get(key) + if (previous && previous !== provider) { + throw new Error('WSL terminal owner already registered') + } + wslProviders.set(key, provider) + rebindLocalProviderListeners() + return () => { + if (wslProviders.get(key) === provider) { + wslProviders.delete(key) + rebindLocalProviderListeners() + } + } +} + export function getProviderForPty(ptyId: string): IPtyProvider { + const wslOwner = parseAppWslPtyId(ptyId) + if (wslOwner) { + const provider = wslProviders.get(wslPtyOwnerKey(wslOwner)) + if (!provider) { + throw new Error('WSL terminal owner is not connected; reconnect its owning distro') + } + return provider + } + if (ptyId.startsWith('wsl:')) { + throw new Error('Invalid WSL terminal identity') + } const connectionId = ptyOwnership.get(ptyId) if (connectionId === undefined) { const parsedSshId = parseAppSshPtyId(ptyId) @@ -52,6 +84,13 @@ export function getProviderForPty(ptyId: string): IPtyProvider { } export function hasPtyProviderForInspection(ptyId: string): boolean { + const wslOwner = parseAppWslPtyId(ptyId) + if (wslOwner) { + return wslProviders.has(wslPtyOwnerKey(wslOwner)) + } + if (ptyId.startsWith('wsl:')) { + return false + } // Why: process inspection is background polling; disconnected SSH hosts should read as idle, not raise repeated IPC errors. const connectionId = ptyOwnership.get(ptyId) if (connectionId === undefined) { @@ -89,6 +128,9 @@ export function closeStartupQueryAuthorityForPty(ptyId: string): void { } export function tryGetProviderForAgentSessionOwner(ptyId: string): IPtyProvider | undefined { + if (ptyId.startsWith('wsl:')) { + return tryGetProviderForPty(ptyId) + } const ownedConnectionId = ptyOwnership.get(ptyId) const parsedSshId = ownedConnectionId === undefined ? parseAppSshPtyId(ptyId) : null try { @@ -103,7 +145,7 @@ export function registerSshPtyProvider(connectionId: string, provider: IPtyProvi sshProviders.set(connectionId, provider) const generation = (provider as { providerGeneration?: number }).providerGeneration if (Number.isSafeInteger(generation) && generation! > 0) { - sshProvidersByGeneration.set(generation!, provider) + relayProvidersByGeneration.set(generation!, provider) } } @@ -111,8 +153,8 @@ export function registerSshPtyProvider(connectionId: string, provider: IPtyProvi export function unregisterSshPtyProvider(connectionId: string): void { const provider = sshProviders.get(connectionId) const generation = (provider as { providerGeneration?: number } | undefined)?.providerGeneration - if (generation !== undefined && sshProvidersByGeneration.get(generation) === provider) { - sshProvidersByGeneration.delete(generation) + if (generation !== undefined && relayProvidersByGeneration.get(generation) === provider) { + relayProvidersByGeneration.delete(generation) } sshProviders.delete(connectionId) } @@ -122,9 +164,7 @@ export function getSshPtyProvider(connectionId: string): IPtyProvider | undefine return sshProviders.get(connectionId) } -/** Get the installed PTY provider (for direct access in tests/runtime). - * After daemon init this may be a DaemonPtyAdapter/DaemonPtyRouter, not LocalPtyProvider; - * callers needing LocalPtyProvider-specific methods must type-narrow or import the class. */ +/** Get the installed daemon provider for runtime operations and tests. */ export function getLocalPtyProvider(): IPtyProvider { return localProvider } diff --git a/src/main/ipc/pty/provider/wsl-registry.test.ts b/src/main/ipc/pty/provider/wsl-registry.test.ts new file mode 100644 index 000000000000..89d33e6c7c35 --- /dev/null +++ b/src/main/ipc/pty/provider/wsl-registry.test.ts @@ -0,0 +1,36 @@ +import { afterEach, expect, it } from 'vitest' +import { createUnavailablePtyProvider } from '../../../providers/unavailable-pty-provider' +import { toAppWslPtyId } from '../../../../shared/wsl-pty-id' +import { + getLocalPtyProvider, + getProviderForPty, + hasPtyProviderForInspection, + registerWslPtyProvider, + tryGetProviderForAgentSessionOwner +} from './registry' +const releases: (() => void)[] = [] +afterEach(() => { + for (const release of releases.splice(0)) { + release() + } +}) +it('routes exact guest identity while retaining legacy Windows daemon IDs', () => { + const owner = { distro: 'Ubuntu', relayBuildId: 'version+hash' } + const provider = createUnavailablePtyProvider() + releases.push(registerWslPtyProvider(owner, provider)) + const id = toAppWslPtyId(owner, 'pty2:guest:1') + expect(getProviderForPty(id)).toBe(provider) + expect(tryGetProviderForAgentSessionOwner(id)).toBe(provider) + expect(hasPtyProviderForInspection(id)).toBe(true) + expect(getProviderForPty('pty2:windows:1')).toBe(getLocalPtyProvider()) + releases.pop()?.() + expect(() => getProviderForPty(id)).toThrow('not connected') + expect(tryGetProviderForAgentSessionOwner(id)).toBeUndefined() + expect(hasPtyProviderForInspection(id)).toBe(false) +}) +it('never falls through to Windows for malformed or foreign guest identity', () => { + expect(() => getProviderForPty('wsl:broken')).toThrow('Invalid WSL') + expect(() => + getProviderForPty(toAppWslPtyId({ distro: 'Debian', relayBuildId: 'old' }, 'pty2:guest:1')) + ).toThrow('not connected') +}) diff --git a/src/main/ipc/pty/register-handlers.ts b/src/main/ipc/pty/register-handlers.ts index aaace9fac370..a9ada6b96368 100644 --- a/src/main/ipc/pty/register-handlers.ts +++ b/src/main/ipc/pty/register-handlers.ts @@ -1,14 +1,15 @@ +import { parseAppWslPtyId } from '../../../shared/wsl-pty-id' +import { makePaneKey } from '../../../shared/stable-pane-id' +import { resolveStablePaneOwner } from './pane/stable-owner' import { getAppEnvironment } from '../../../shared/app-environment' import type { OrcaRuntimeService } from '../../runtime/orca-runtime' import type { Store } from '../../persistence' import type { GlobalSettings } from '../../../shared/global-settings-types' -import { LocalPtyProvider } from '../../providers/local-pty-provider' import type { TerminalStartupCwdMissingDirFallback } from '../../../shared/terminal-startup-cwd' import { getHiddenRendererPtyDeliveryDebug, resetRendererScopedHiddenPtyDeliveryState } from '../pty-hidden-delivery-gate' -import { localProvider } from './provider/registry' import { finishPtyShutdown } from './provider/liveness' import type { GetSelectedCodexHomePath, PrepareClaudeAuth } from './host-env/types' import { installPtyInspectIpcHandlers } from './ipc/inspect' @@ -34,7 +35,6 @@ import { } from './delivery/visibility-state' import { setRebindProviderListeners, - setDidFinishLoadHandler, setRendererGateResetState } from './provider/listener-lifecycle' import { @@ -44,12 +44,10 @@ import { } from './delivery/debug' import { registerRendererLifecycleResetHandlers, - clearRendererGateResetHandlers, - clearDidFinishLoadHandler + clearRendererGateResetHandlers } from './delivery/lifecycle-reset' import { createPtyIpcSession, type PtyIpcSessionOptions, type PtyRendererDelivery } from './session' import { wirePtyIpcSession } from './delivery/wire-session' -import { configureLocalPtyProvider } from './provider/local-configure' import { bindProviderListeners } from './provider/bind-listeners' import { installSessionSshOutputIntake } from './delivery/ssh-intake' import { installPtySerializeBufferIpc } from './ipc/serialize-buffer' @@ -146,12 +144,6 @@ export function registerPtyHandlers( options }) wirePtyIpcSession(session) - configureLocalPtyProvider({ - runtime, - getSettings, - getSelectedCodexHomePath, - trustedTerminalHandleEnv: session.trustedTerminalHandleEnv - }) installSessionSshOutputIntake(session) bindProviderListeners(session) setRebindProviderListeners(() => bindProviderListeners(session)) @@ -178,23 +170,6 @@ export function registerPtyHandlers( mainWindow.webContents.on('render-process-gone', resetRendererPtyDeliveryGateState) } - // Why: only LocalPtyProvider PTYs (main-process) can be orphaned on reload; daemon sessions survive by design and cleanup would kill them. - clearDidFinishLoadHandler() - if (mainWindow && localProvider instanceof LocalPtyProvider) { - const lp = localProvider - const finishLoadHandler = () => { - // Why: always advance to keep the generation monotonic, but skip the sweep on crash/freeze-recovery reload — it would kill live local PTYs before session restore (#5787). - const generation = lp.advanceGeneration() - if (options?.isRecoveryReloadInFlight?.(mainWindow.webContents.id)) { - return - } - // Why: the retained provider onExit callback is the only physical-exit proof; it clears ownership after the OS reaps it. - lp.killOrphanedPtys(generation - 1) - } - setDidFinishLoadHandler(finishLoadHandler, mainWindow.webContents) - mainWindow.webContents.on('did-finish-load', finishLoadHandler) - } - const assertFolderWorkspacePtyPathUsable = ( worktreeId: string | undefined ): Promise | void => assertFolderWorkspacePtyPathUsableImpl(store, worktreeId) @@ -207,8 +182,22 @@ export function registerPtyHandlers( args: Parameters[1] ): ReturnType => prepareCodexResumeHome(options?.prepareCodexSessionResume, args) - const adoptStablePaneBound = (args: Parameters[2]) => - adoptStablePane(runtime, store, args) + const adoptStablePaneBound = async (args: Parameters[2]) => { + if (!args.connectionId && options?.wslDaemonSessions) { + const binding = resolveStablePaneOwner( + runtime, + store, + makePaneKey(args.tabId, args.leafId), + args.worktreeId, + args.connectionId + ) + const owner = binding?.ptyId ? parseAppWslPtyId(binding.ptyId) : null + if (owner) { + await options.wslDaemonSessions.reconnect(owner) + } + } + return adoptStablePane(runtime, store, args) + } // Why: route through getProviderForPty() so CLI commands work for remote PTYs too; localProvider would silently fail for them. installPtyRuntimeController({ @@ -280,6 +269,6 @@ export function registerPtyHandlers( }) installPtyWriteIpcHandlers({ mainWindow, runtime }) installPtyResizeVisibilityIpc(session) - installPtyInspectIpcHandlers({ getLocalPtyProviderStartupPromise }) + installPtyInspectIpcHandlers({ getLocalPtyProviderStartupPromise, store, runtime }) installPtyKillIpcHandler(killDeps) } diff --git a/src/main/ipc/pty/register-headless-runtime.ts b/src/main/ipc/pty/register-headless-runtime.ts index 424cb40facdb..8ba180547ca3 100644 --- a/src/main/ipc/pty/register-headless-runtime.ts +++ b/src/main/ipc/pty/register-headless-runtime.ts @@ -1,3 +1,4 @@ +import type { WslDaemonSessions } from '../../wsl/wsl-daemon-sessions' import type { OrcaRuntimeService } from '../../runtime/orca-runtime' import type { Store } from '../../persistence' import type { GlobalSettings } from '../../../shared/global-settings-types' @@ -18,6 +19,7 @@ export function registerHeadlessPtyRuntime( store?: Store, prepareCodexSessionResume?: PrepareCodexSessionResume, lifecycle?: { + wslDaemonSessions?: WslDaemonSessions onCodexHomePtySpawned?: (args: CodexHomePtySpawnedLifecycleArgs) => void onPtyExit?: (id: string, exitSequence: number) => void } diff --git a/src/main/ipc/pty/register-without-renderer.test.ts b/src/main/ipc/pty/register-without-renderer.test.ts index 513d969f1108..71b712e42731 100644 --- a/src/main/ipc/pty/register-without-renderer.test.ts +++ b/src/main/ipc/pty/register-without-renderer.test.ts @@ -15,7 +15,6 @@ import { onMock } from '../pty-ipc-mock-registry' vi.mock('electron', () => import('../pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('../pty-ipc-mock-registry').then((m) => m.fsModuleMock())) -vi.mock('node-pty', () => import('../pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) vi.mock('node:child_process', async (importOriginal) => (await import('../pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal()) ) diff --git a/src/main/ipc/pty/runtime/controller-deps.ts b/src/main/ipc/pty/runtime/controller-deps.ts index 8f20245505ca..f79765ead498 100644 --- a/src/main/ipc/pty/runtime/controller-deps.ts +++ b/src/main/ipc/pty/runtime/controller-deps.ts @@ -1,3 +1,5 @@ +import type { WslAccountExecutionContext } from '../../../wsl/wsl-account-execution-context' +import type { WslDaemonSessions } from '../../../wsl/wsl-daemon-sessions' import type { PtyRendererDelivery } from '../session' import type { OrcaRuntimeService } from '../../../runtime/orca-runtime' import type { Store } from '../../../persistence' @@ -26,6 +28,7 @@ export type PtyRuntimeControllerDeps = { getLocalPtyStartupPromise: (connectionId?: string | null) => Promise | undefined getLocalPtyProviderStartupPromise: (connectionId?: string | null) => Promise | undefined prepareCodexResumeHome: (args: { + wslExecution?: WslAccountExecutionContext connectionId?: string | null launchAgent?: TuiAgent providerSession?: AgentProviderSessionMetadata @@ -77,6 +80,7 @@ export type PtyRuntimeControllerDeps = { getSelectedCodexHomePath?: GetSelectedCodexHomePath prepareClaudeAuth?: PrepareClaudeAuth options?: { + wslDaemonSessions?: WslDaemonSessions onCodexHomePtySpawned?: (args: CodexHomePtySpawnedLifecycleArgs) => void prepareCodexSessionResume?: PrepareCodexSessionResume } diff --git a/src/main/ipc/pty/runtime/foreground-process-evidence-capability.ts b/src/main/ipc/pty/runtime/foreground-process-evidence-capability.ts index 0cbf4377d0d1..a2bd0b60aee2 100644 --- a/src/main/ipc/pty/runtime/foreground-process-evidence-capability.ts +++ b/src/main/ipc/pty/runtime/foreground-process-evidence-capability.ts @@ -1,19 +1,20 @@ -import { getProvider, registeredPtyProviders } from '../provider/registry' +import { getProvider, registeredPtyProviders, localProvider } from '../provider/registry' /** Probe the owning provider before opting into the no-process-table inventory projection. */ export async function supportsForegroundProcessEvidenceFromRuntimeController( connectionId?: string | null ): Promise { - if (connectionId === null) { - return true - } - if (connectionId === undefined) { - const providers = registeredPtyProviders() + if (connectionId === undefined || connectionId === null) { + const providers = registeredPtyProviders().filter( + (entry) => connectionId === undefined || entry.connectionId === null + ) const supported = await Promise.all( - providers.map(async ({ provider, connectionId: providerConnectionId }) => - providerConnectionId === null + providers.map(async ({ provider }) => + provider === localProvider ? true - : ((await provider.supportsForegroundProcessEvidence?.()) ?? false) + : ((await Promise.resolve() + .then(() => provider.supportsForegroundProcessEvidence?.()) + .catch(() => false)) ?? false) ) ) return supported.every(Boolean) diff --git a/src/main/ipc/pty/runtime/inventory-operations.ts b/src/main/ipc/pty/runtime/inventory-operations.ts index 882908a926e1..9c09602ef170 100644 --- a/src/main/ipc/pty/runtime/inventory-operations.ts +++ b/src/main/ipc/pty/runtime/inventory-operations.ts @@ -1,3 +1,7 @@ +import { + createLocalInventoryAuthority, + type LocalInventoryAuthorityDeps +} from '../provider/local-inventory-authority' import { ptyOwnership } from '../provider/ownership-state' import { getProvider, localProvider, registeredPtyProviders } from '../provider/registry' import { @@ -6,10 +10,9 @@ import { type ExecutionHostId } from '../../../../shared/execution-host' import type { PtyProcessInfo } from '../../../providers/pty-process-info' -import type { PtyRuntimeControllerDeps } from './controller-deps' function markSshInventoryUnverifiable( - runtime: PtyRuntimeControllerDeps['runtime'], + runtime: LocalInventoryAuthorityDeps['runtime'], connectionId: string, error: unknown ): void { @@ -22,9 +25,10 @@ function markSshInventoryUnverifiable( } export async function listProcessesWithHostScopeFromRuntimeController( - deps: PtyRuntimeControllerDeps, + deps: LocalInventoryAuthorityDeps, opts?: { deadlineMs?: number; includeForegroundProcessEvidence?: boolean } ): Promise<{ processes: PtyProcessInfo[]; hostIds: ExecutionHostId[] }> { + const localAuthority = createLocalInventoryAuthority(deps) const providerSessions = await Promise.all( registeredPtyProviders().map(async ({ provider, connectionId }) => { const hostId: ExecutionHostId = connectionId @@ -32,12 +36,18 @@ export async function listProcessesWithHostScopeFromRuntimeController( : LOCAL_EXECUTION_HOST_ID try { return { - processes: await (connectionId ? provider.listProcesses(opts) : provider.listProcesses()), + processes: await (provider === localProvider + ? provider.listProcesses() + : provider.listProcesses(opts)), hostId } } catch (error) { if (!connectionId) { - throw error + if (provider === localProvider) { + throw error + } + localAuthority.failed(provider, error) + return null } markSshInventoryUnverifiable(deps.runtime, connectionId, error) return null @@ -47,17 +57,38 @@ export async function listProcessesWithHostScopeFromRuntimeController( const respondingSessions = providerSessions.filter((session) => session !== null) return { processes: respondingSessions.flatMap((session) => session.processes), - hostIds: respondingSessions.map((session) => session.hostId) + hostIds: [...new Set(respondingSessions.map((session) => session.hostId))].filter( + (hostId) => hostId !== LOCAL_EXECUTION_HOST_ID || localAuthority.isComplete() + ) } } export async function listProcessesFromRuntimeController( - deps: PtyRuntimeControllerDeps, + deps: LocalInventoryAuthorityDeps, connectionId?: string | null, opts?: { deadlineMs?: number; includeForegroundProcessEvidence?: boolean } ) { if (connectionId === null) { - return localProvider.listProcesses() + const localAuthority = createLocalInventoryAuthority(deps) + const rows = await Promise.all( + registeredPtyProviders() + .filter(({ connectionId }) => connectionId === null) + .map(async ({ provider }) => { + try { + return await (provider === localProvider + ? provider.listProcesses() + : provider.listProcesses(opts)) + } catch (error) { + if (provider === localProvider) { + throw error + } + localAuthority.failed(provider, error) + return [] + } + }) + ) + localAuthority.assertComplete() + return rows.flat() } if (connectionId !== undefined) { try { diff --git a/src/main/ipc/pty/runtime/local-guest-inventory.test.ts b/src/main/ipc/pty/runtime/local-guest-inventory.test.ts new file mode 100644 index 000000000000..d284de34beb5 --- /dev/null +++ b/src/main/ipc/pty/runtime/local-guest-inventory.test.ts @@ -0,0 +1,171 @@ +import { afterEach, expect, it, vi } from 'vitest' +import { createUnavailablePtyProvider } from '../../../providers/unavailable-pty-provider' +import { getDefaultWorkspaceSession } from '../../../../shared/constants' +import { toAppWslPtyId } from '../../../../shared/wsl-pty-id' +import { + getLocalPtyProvider, + registerSshPtyProvider, + registerWslPtyProvider, + setLocalPtyProvider, + unregisterSshPtyProvider +} from '../provider/registry' +import { ptyOwnership } from '../provider/ownership-state' +import { + listProcessesFromRuntimeController, + listProcessesWithHostScopeFromRuntimeController +} from './inventory-operations' +import { supportsForegroundProcessEvidenceFromRuntimeController } from './foreground-process-evidence-capability' + +const releases: (() => void)[] = [] +const original = getLocalPtyProvider() +afterEach(() => { + for (const release of releases.splice(0)) { + release() + } + unregisterSshPtyProvider('remote') + setLocalPtyProvider(original) + ptyOwnership.clear() +}) +const ubuntu = { distro: 'Ubuntu', relayBuildId: 'build+user1' } +const debian = { distro: 'Debian', relayBuildId: 'build+user2' } +const row = (id: string) => ({ + id, + cwd: '/folder with spaces', + title: 'shell', + worktreeId: 'folder-project' +}) +function setup() { + const nativeList = vi.fn(async () => [row('pty2:windows:1')]) + const firstList = vi.fn(async () => [row(toAppWslPtyId(ubuntu, 'pty2:guest:1'))]) + const secondList = vi.fn(async () => [row(toAppWslPtyId(debian, 'pty2:guest:1'))]) + const provider = (listProcesses: typeof nativeList) => ({ + ...createUnavailablePtyProvider(), + listProcesses, + supportsForegroundProcessEvidence: vi.fn(async () => true) + }) + setLocalPtyProvider(provider(nativeList)) + releases.push(registerWslPtyProvider(ubuntu, provider(firstList))) + const second = provider(secondList) + releases.push(registerWslPtyProvider(debian, second)) + const sshList = vi.fn(async () => [row('ssh:remote@@pty2:remote:1')]) + registerSshPtyProvider('remote', provider(sshList)) + return { nativeList, firstList, secondList, sshList, second } +} + +it('local scope includes native and both guest owners without touching SSH or rewriting folder identity', async () => { + const { sshList } = setup() + const rows = await listProcessesFromRuntimeController({}, null) + expect(rows.map((entry) => entry.id)).toEqual([ + 'pty2:windows:1', + toAppWslPtyId(ubuntu, 'pty2:guest:1'), + toAppWslPtyId(debian, 'pty2:guest:1') + ]) + expect( + rows.every( + (entry) => entry.cwd === '/folder with spaces' && entry.worktreeId === 'folder-project' + ) + ).toBe(true) + expect(sshList).not.toHaveBeenCalled() +}) + +it('failed guest removes local absence authority while retaining successful rows and SSH authority', async () => { + const { secondList } = setup() + secondList.mockRejectedValue(new Error('guest disconnected')) + const id = toAppWslPtyId(debian, 'pty2:guest:1') + ptyOwnership.set(id, null) + const runtime = { markPtyLivenessUnverifiable: vi.fn() } + const snapshot = await listProcessesWithHostScopeFromRuntimeController({ runtime }) + expect(snapshot.processes.map((entry) => entry.id)).toEqual([ + 'pty2:windows:1', + 'ssh:remote@@pty2:remote:1', + toAppWslPtyId(ubuntu, 'pty2:guest:1') + ]) + expect(snapshot.hostIds).toEqual(['ssh:remote']) + expect(runtime.markPtyLivenessUnverifiable).toHaveBeenCalledWith(id, 'guest disconnected') + await expect(listProcessesFromRuntimeController({ runtime }, null)).rejects.toThrow( + 'unverifiable' + ) +}) + +it('persisted split binding fences local authority even after its disconnected provider leaves the registry', async () => { + setup() + releases.pop()?.() + const id = toAppWslPtyId(debian, 'pty2:guest:1') + const session = getDefaultWorkspaceSession() + session.terminalLayoutsByTabId.tab = { + root: { type: 'leaf', leafId: 'leaf' }, + activeLeafId: 'leaf', + expandedLeafId: null, + ptyIdsByLeafId: { leaf: id } + } + const store = { getWorkspaceSession: () => session } + const runtime = { markPtyLivenessUnverifiable: vi.fn() } + const snapshot = await listProcessesWithHostScopeFromRuntimeController({ store, runtime }) + expect(snapshot.hostIds).not.toContain('local') + expect(snapshot.processes).toHaveLength(3) + expect(runtime.markPtyLivenessUnverifiable).toHaveBeenCalledWith( + id, + expect.stringContaining('not connected') + ) + await expect(listProcessesFromRuntimeController({ store, runtime }, null)).rejects.toThrow( + 'unverifiable' + ) + expect(await listProcessesFromRuntimeController({ store, runtime }, 'remote')).toHaveLength(1) +}) + +it('guest capabilities participate in local projection and failures return false', async () => { + const { second } = setup() + expect(await supportsForegroundProcessEvidenceFromRuntimeController(null)).toBe(true) + second.supportsForegroundProcessEvidence.mockRejectedValue(new Error('offline')) + expect(await supportsForegroundProcessEvidenceFromRuntimeController(null)).toBe(false) + expect(await supportsForegroundProcessEvidenceFromRuntimeController()).toBe(false) +}) + +it.each(['disconnect', 'replace', 'reject'] as const)( + 'does not grant local absence authority when a guest changes during inventory: %s', + async (transition) => { + const { secondList } = setup() + const id = toAppWslPtyId(debian, 'pty2:guest:1') + ptyOwnership.set(id, null) + const runtime = { markPtyLivenessUnverifiable: vi.fn() } + let finish = () => {} + secondList.mockImplementation( + () => + new Promise((resolve, reject) => { + finish = () => + transition === 'reject' + ? reject(new Error('query lost after disconnect')) + : resolve([row(id)]) + }) + ) + const pending = listProcessesWithHostScopeFromRuntimeController({ runtime }) + releases.pop()?.() + if (transition === 'replace') { + releases.push(registerWslPtyProvider(debian, createUnavailablePtyProvider())) + } + finish() + const snapshot = await pending + expect(snapshot.hostIds).toEqual(['ssh:remote']) + expect(runtime.markPtyLivenessUnverifiable).toHaveBeenCalledWith(id, expect.any(String)) + } +) + +it('rejects array-only local inventory if a new guest registers while queries are pending', async () => { + const { secondList } = setup() + let finish = () => {} + secondList.mockImplementation( + () => + new Promise((resolve) => { + finish = () => resolve([]) + }) + ) + const pending = listProcessesFromRuntimeController({}, null) + releases.push( + registerWslPtyProvider( + { distro: 'Fedora', relayBuildId: 'build+user3' }, + createUnavailablePtyProvider() + ) + ) + finish() + await expect(pending).rejects.toThrow('unverifiable') +}) diff --git a/src/main/ipc/pty/runtime/operations.ts b/src/main/ipc/pty/runtime/operations.ts index f03892a6e31d..72cb1c84af59 100644 --- a/src/main/ipc/pty/runtime/operations.ts +++ b/src/main/ipc/pty/runtime/operations.ts @@ -1,5 +1,4 @@ import type { IPtyProvider } from '../../../providers/types' -import { LocalPtyProvider } from '../../../providers/local-pty-provider' import { parseAppSshPtyId } from '../../../providers/ssh-pty-id' import { ptyOwnership } from '../provider/ownership-state' import { ptySizes } from '../delivery/visibility-state' @@ -71,11 +70,6 @@ export async function probePtyLivenessFromRuntimeController( if (provider.probePtyLiveness) { return await provider.probePtyLiveness(ptyId) } - // Why: the in-process provider is its own sole owner (#12393), so its - // refusal is authoritative; every other probe-less provider is doubt. - if (provider instanceof LocalPtyProvider) { - return provider.hasPty(ptyId) - } return null } catch { return null @@ -95,7 +89,7 @@ export async function attachPtyFromRuntimeController( } catch { return false } - if (provider !== localProvider || provider instanceof LocalPtyProvider) { + if (provider !== localProvider) { return false } try { diff --git a/src/main/ipc/pty/runtime/queried-host-kinds.test.ts b/src/main/ipc/pty/runtime/queried-host-kinds.test.ts index 1f7ce2459d88..6e6e1b33db1b 100644 --- a/src/main/ipc/pty/runtime/queried-host-kinds.test.ts +++ b/src/main/ipc/pty/runtime/queried-host-kinds.test.ts @@ -1,7 +1,8 @@ -import { afterEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { parseExecutionHostId } from '../../../../shared/execution-host' -import { sshProviders } from '../provider/registry' +import { localProvider, setLocalPtyProvider, sshProviders } from '../provider/registry' import { listProcessesWithHostScopeFromRuntimeController } from './inventory-operations' +import { createUnavailablePtyProvider } from '../../../providers/unavailable-pty-provider' import type { PtyRuntimeControllerDeps } from './controller-deps' /** @@ -21,7 +22,12 @@ import type { PtyRuntimeControllerDeps } from './controller-deps' * `kind === 'ssh'` in `orca-runtime-refresh-pty-worktree-records-with-controller-inventory.ts`. */ describe('the hosts a PTY inventory can report having queried', () => { + const originalProvider = localProvider + beforeEach(() => { + setLocalPtyProvider({ ...createUnavailablePtyProvider(), listProcesses: async () => [] }) + }) afterEach(() => { + setLocalPtyProvider(originalProvider) sshProviders.clear() }) diff --git a/src/main/ipc/pty/runtime/spawn-early.ts b/src/main/ipc/pty/runtime/spawn-early.ts index 575f89b22e79..57853c988696 100644 --- a/src/main/ipc/pty/runtime/spawn-early.ts +++ b/src/main/ipc/pty/runtime/spawn-early.ts @@ -1,3 +1,6 @@ +import { parseAppWslPtyId } from '../../../../shared/wsl-pty-id' +import { prepareWslDaemonReattachHooks } from '../../../wsl/wsl-daemon-reattach-hooks' +import { isAgentStatusHooksEnabled } from '../../../agent-hooks/managed-agent-hook-controls' import type { PtySpawnResult } from '../../../providers/types' import { getAppPtyId } from '../provider/registry' import { allocatePtyLifecycleSequence } from '../host-env/types' @@ -54,7 +57,7 @@ export function adoptMaterializedRuntimePtySpawn( } } ensureWslHookRelayForReattach( - { isReattach: true, wslDistro: ctx.preAdoptedStablePane.result.wslDistro }, + { id: ctx.result.id, isReattach: true, wslDistro: ctx.preAdoptedStablePane.result.wslDistro }, args.connectionId ) if (!args.connectionId) { @@ -67,5 +70,12 @@ export function adoptMaterializedRuntimePtySpawn( ...codexReattachedHomeRouteField(ctx.reattachedCodexHomeRoutes, ctx.result.id, true) }) } + if (parseAppWslPtyId(ctx.result.id)) { + return prepareWslDaemonReattachHooks({ + result: { ...ctx.result, isReattach: true }, + sessions: ctx.deps.options?.wslDaemonSessions, + hooksEnabled: isAgentStatusHooksEnabled(ctx.deps.getSettings?.()) + }).then(() => ctx.result) + } return ctx.result } diff --git a/src/main/ipc/pty/runtime/spawn-execute.ts b/src/main/ipc/pty/runtime/spawn-execute.ts index 8228175a2cf1..cf18a932b933 100644 --- a/src/main/ipc/pty/runtime/spawn-execute.ts +++ b/src/main/ipc/pty/runtime/spawn-execute.ts @@ -1,3 +1,6 @@ +import { prepareWslDaemonReattachHooks } from '../../../wsl/wsl-daemon-reattach-hooks' +import { isAgentStatusHooksEnabled } from '../../../agent-hooks/managed-agent-hook-controls' +import { parseAppWslPtyId } from '../../../../shared/wsl-pty-id' import type { PtySpawnResult } from '../../../providers/types' import { ptyIncarnationById, deletePtyOwnership } from '../provider/ownership-state' import { ptySizes } from '../delivery/visibility-state' @@ -164,6 +167,13 @@ export async function executeRuntimePtySpawn(ctx: RuntimePtySpawnState): Promise ctx.rejectedRegistrationCandidate = ctx.result assertSpawnReplyWasLive(ctx.result) } + if (parseAppWslPtyId(ctx.result.id)) { + const actualOwner = tryGetProviderForAgentSessionOwner(ctx.result.id) + if (!actualOwner) { + throw new Error('execution_owner_unavailable') + } + ctx.provider = actualOwner + } ctx.rejectedRegistrationCandidate ??= ctx.result if (ctx.pendingRegistrationPtyId !== ctx.result.id) { if (ctx.pendingRegistrationPtyId) { @@ -190,7 +200,14 @@ export async function executeRuntimePtySpawn(ctx: RuntimePtySpawnState): Promise ctx.snapshotKittyFlagsCoverReconciledSeq = false } } - ensureWslHookRelayForReattach(ctx.result, args.connectionId) + const guestHooks = await prepareWslDaemonReattachHooks({ + result: ctx.result, + sessions: ctx.deps.options?.wslDaemonSessions, + hooksEnabled: isAgentStatusHooksEnabled(ctx.deps.getSettings?.()) + }) + if (!guestHooks) { + ensureWslHookRelayForReattach(ctx.result, args.connectionId) + } ctx.deps.runtime?.preparePtyExecutionContext?.( ctx.result.id, args.connectionId @@ -269,7 +286,7 @@ export async function executeRuntimePtySpawn(ctx: RuntimePtySpawnState): Promise } } if (ctx.isNewDaemonSession && ctx.sessionId !== undefined) { - clearProviderPtyState(ctx.sessionId) + clearProviderPtyState(ctx.effectiveSessionAppId ?? ctx.sessionId) } throw spawnError } finally { diff --git a/src/main/ipc/pty/runtime/spawn-guest-environment.ts b/src/main/ipc/pty/runtime/spawn-guest-environment.ts new file mode 100644 index 000000000000..5943a9a54c50 --- /dev/null +++ b/src/main/ipc/pty/runtime/spawn-guest-environment.ts @@ -0,0 +1,54 @@ +import { getAppEnvironment } from '../../../../shared/app-environment' +import { isTuiAgent } from '../../../../shared/tui-agent-config' +import { prepareWslGuestTerminalSpawn } from '../../../wsl/wsl-guest-terminal-preparation' +import { isAgentStatusHooksEnabled } from '../../../agent-hooks/managed-agent-hook-controls' +import { isCodexStatusHooksEnabled } from '../host-env/codex-home' +import { stampWslOrchestrationCompatibilityHost } from '../../../pty/wsl-orca-env' +import type { RuntimePtySpawnState } from './spawn-state' + +export async function prepareRuntimeGuestSpawnOptions(ctx: RuntimePtySpawnState): Promise { + if (!ctx.wslGuest) { + return + } + if (!ctx.wslGuest.fresh && !ctx.wslGuest.coldRestore) { + ctx.spawnOptions.attachOnly = true + return + } + if (ctx.wslGuest.coldRestore) { + ctx.spawnOptions.sessionId = ctx.effectiveSessionAppId + ctx.spawnOptions.isNewSession = false + ctx.spawnOptions.attachOnly = false + } + const settings = ctx.deps.getSettings?.() + const environment = getAppEnvironment() + ctx.spawnOptions = await prepareWslGuestTerminalSpawn( + ctx.wslGuest.prepared, + ctx.spawnOptions, + { + isPackaged: environment.isPackaged(), + resourcesPath: process.resourcesPath, + userDataPath: environment.getPath('userData'), + selectedCodexHomePath: ctx.selectedCodexHomePath, + skipCodexHomeEnv: ctx.skipCodexHomeEnv, + stripInheritedOrcaCodexHome: ctx.stripInheritedOrcaCodexHome, + launchCommand: ctx.launchCommand, + launchAgent: isTuiAgent(ctx.args.launchAgent) ? ctx.args.launchAgent : undefined, + agentStatusHooksEnabled: isAgentStatusHooksEnabled(settings), + disabledTuiAgents: settings?.disabledTuiAgents, + codexStatusHooksEnabled: isCodexStatusHooksEnabled(settings), + networkProxySettings: settings, + routeBrowserOpensToClient: ctx.deps.runtime?.shouldRelayTerminalBrowserOpens?.(), + deferGitConfigGuardToDaemon: true + }, + ctx.args.signal, + ctx.wslGuest.coldRestore ? 'confirmed-exited' : undefined + ) + ctx.env = ctx.spawnOptions.env + if (ctx.env) { + stampWslOrchestrationCompatibilityHost( + ctx.env, + ctx.deps.runtime?.getOrchestrationCompatibilityHostId?.(), + ctx.expectedWslDistro + ) + } +} diff --git a/src/main/ipc/pty/runtime/spawn-host-environment.ts b/src/main/ipc/pty/runtime/spawn-host-environment.ts new file mode 100644 index 000000000000..dd9d70790167 --- /dev/null +++ b/src/main/ipc/pty/runtime/spawn-host-environment.ts @@ -0,0 +1,71 @@ +import { getAppEnvironment } from '../../../../shared/app-environment' +import { inheritOmpLaunchEnvironment } from '../host-env/omp-launch-environment' +import { buildPtyHostEnv } from '../host-env/assembly' +import { isSafePtySessionId } from '../../../daemon/pty-session-id' +import { + shouldSkipCodexHomeEnvForWindowsShell, + isCodexStatusHooksEnabled +} from '../host-env/codex-home' +import { isTuiAgent } from '../../../../shared/tui-agent-config' +import { isAgentStatusHooksEnabled } from '../../../agent-hooks/managed-agent-hook-controls' +import { stampWslOrchestrationCompatibilityHost } from '../../../pty/wsl-orca-env' +import { clearProviderPtyState } from '../provider/state-cleanup' +import { awaitExplicitPiOmpGuestReadiness } from '../../../agent-hooks/wsl-pi-omp-guest-readiness' +import { promoteAgentTeamsShimPath } from '../host-env/path' +import type { RuntimePtySpawnState } from './spawn-state' + +export async function prepareRuntimeHostSpawnEnvironment(ctx: RuntimePtySpawnState): Promise { + const args = ctx.args + const ptySettings = ctx.deps.getSettings?.() + if (!ctx.wslGuest && ctx.isDaemonHostSpawn && ctx.sessionId && !ctx.preAdoptedStablePane) { + if (!isSafePtySessionId(ctx.sessionId, getAppEnvironment().getPath('userData'))) { + throw new Error('Invalid PTY session id') + } + try { + ctx.env ??= {} + await inheritOmpLaunchEnvironment(ctx.env, { + isWsl: shouldSkipCodexHomeEnvForWindowsShell(ctx.daemonShellOverride, ctx.cwd), + launchAgent: args.launchAgent, + launchCommand: ctx.launchCommand + }) + await awaitExplicitPiOmpGuestReadiness({ + isWsl: shouldSkipCodexHomeEnvForWindowsShell(ctx.daemonShellOverride, ctx.cwd), + distro: ctx.codexSelectionTarget.runtime === 'wsl' ? ctx.expectedWslDistro : null, + codexHomePath: ctx.selectedCodexHomePath, + launchAgent: isTuiAgent(args.launchAgent) ? args.launchAgent : undefined, + launchCommand: ctx.launchCommand + }) + ctx.env = buildPtyHostEnv(ctx.sessionId, ctx.env ?? {}, { + isPackaged: getAppEnvironment().isPackaged(), + resourcesPath: process.resourcesPath, + userDataPath: getAppEnvironment().getPath('userData'), + selectedCodexHomePath: ctx.selectedCodexHomePath, + skipCodexHomeEnv: ctx.skipCodexHomeEnv, + stripInheritedOrcaCodexHome: ctx.stripInheritedOrcaCodexHome, + launchCommand: ctx.launchCommand, + launchAgent: isTuiAgent(args.launchAgent) ? args.launchAgent : undefined, + isWsl: shouldSkipCodexHomeEnvForWindowsShell(ctx.daemonShellOverride, ctx.cwd), + wslDistro: ctx.codexSelectionTarget.runtime === 'wsl' ? ctx.expectedWslDistro : null, + agentStatusHooksEnabled: isAgentStatusHooksEnabled(ptySettings), + disabledTuiAgents: ptySettings?.disabledTuiAgents, + codexStatusHooksEnabled: isCodexStatusHooksEnabled(ptySettings), + networkProxySettings: ptySettings, + routeBrowserOpensToClient: ctx.deps.runtime?.shouldRelayTerminalBrowserOpens?.(), + deferGitConfigGuardToDaemon: + ctx.provider.supportsGitCredentialGuardHost?.(ctx.sessionId) === true + }) + stampWslOrchestrationCompatibilityHost( + ctx.env, + ctx.deps.runtime?.getOrchestrationCompatibilityHostId?.(), + ctx.codexSelectionTarget.runtime === 'wsl' ? ctx.expectedWslDistro : null + ) + promoteAgentTeamsShimPath(ctx.env, ctx.requestedAgentTeamsPath) + } catch (error) { + // Why: host-env setup can materialize agent hooks/extensions before failing. + if (ctx.requestedSessionId === undefined) { + clearProviderPtyState(ctx.sessionId) + } + throw error + } + } +} diff --git a/src/main/ipc/pty/runtime/spawn-options.ts b/src/main/ipc/pty/runtime/spawn-options.ts index 67c633560e97..529a37ffeaeb 100644 --- a/src/main/ipc/pty/runtime/spawn-options.ts +++ b/src/main/ipc/pty/runtime/spawn-options.ts @@ -1,5 +1,5 @@ +import { prepareRuntimeGuestSpawnOptions } from './spawn-guest-environment' import type { IPtyProvider, PtySpawnResult } from '../../../providers/types' -import { LocalPtyProvider } from '../../../providers/local-pty-provider' import { makePaneKey, isTerminalLeafId } from '../../../../shared/stable-pane-id' import { isValidTerminalTabId } from '../../../../shared/terminal-tab-id' import { ptySizes } from '../delivery/visibility-state' @@ -12,10 +12,7 @@ import { getInheritedClaudeSessionStampEnvKeysToDelete } from '../host-env/pi-agent' import { promoteAgentTeamsShimPath, deleteRequestedEnvKeys } from '../host-env/path' -import { - routesFreshSpawnsToLocalProvider, - beginPtySpawnForWorktree -} from '../host-env/fresh-spawn-routing' +import { beginPtySpawnForWorktree } from '../host-env/fresh-spawn-routing' import { isTuiAgent } from '../../../../shared/tui-agent-config' import { CLAUDE_AUTH_ENV_VARS } from '../../../claude-accounts/environment' import { LEGACY_TERMINAL_SHIM_REMOTE_ENV_KEYS } from '../../../pty/legacy-terminal-shim-dir' @@ -71,7 +68,7 @@ export async function buildRuntimePtySpawnOptions( PI_PROCESS_OWNER_ENV_KEYS, // Why: disable old hosts without removing ORCA_REAL_* while their Windows shim remains on PATH. ctx.isDaemonHostSpawn || args.connectionId ? LEGACY_TERMINAL_SHIM_REMOTE_ENV_KEYS : [], - ctx.isDaemonHostSpawn ? getInheritedAgentHookEnvKeysToDelete(ctx.env) : [], + ctx.isDaemonHostSpawn && !ctx.wslGuest ? getInheritedAgentHookEnvKeysToDelete(ctx.env) : [], // Why: ungated, unlike the agent-hook keys — the local provider and the relay host also spread their own process.env into every spawn. getInheritedClaudeSessionStampEnvKeysToDelete(ctx.env) ) @@ -181,13 +178,8 @@ export async function buildRuntimePtySpawnOptions( if (args.signal) { ctx.spawnOptions.signal = args.signal } - if ( - args.onPtySpawnCommitted && - (ctx.provider instanceof LocalPtyProvider || routesFreshSpawnsToLocalProvider(ctx.provider)) - ) { - // Why: local fallback has no lower operation ledger, so commit must be reported at native spawn. - ctx.spawnOptions.onPtySpawnCommitted = ctx.reportPtySpawnCommitted - } + + await prepareRuntimeGuestSpawnOptions(ctx) const resolvedPaneSpawnReservationKey = makePaneSpawnReservationKey( args.worktreeId, diff --git a/src/main/ipc/pty/runtime/spawn-preflight.ts b/src/main/ipc/pty/runtime/spawn-preflight.ts index 2bd10feaab63..441c8fb7eefe 100644 --- a/src/main/ipc/pty/runtime/spawn-preflight.ts +++ b/src/main/ipc/pty/runtime/spawn-preflight.ts @@ -1,26 +1,19 @@ -import { inheritOmpLaunchEnvironment } from '../host-env/omp-launch-environment' -import { getAppEnvironment } from '../../../../shared/app-environment' +import { prepareRuntimeHostSpawnEnvironment } from './spawn-host-environment' +import { prepareWslDaemonSpawnRoute } from '../../../wsl/wsl-daemon-spawn-route' +import { toAppWslPtyId } from '../../../../shared/wsl-pty-id' import type { PtySpawnResult } from '../../../providers/types' -import { LocalPtyProvider } from '../../../providers/local-pty-provider' import { isValidTerminalTabId } from '../../../../shared/terminal-tab-id' import { isTerminalLeafId } from '../../../../shared/stable-pane-id' import { getAppPtyId, getProvider, getRelayPtyId } from '../provider/registry' -import { buildPtyHostEnv } from '../host-env/assembly' import { getCompatibleSelectedCodexHomePath, getCodexSelectionTargetForPty, resolveCodexHomeAfterManagedAuthReadiness, shouldSkipCodexHomeEnvForWindowsShell, shouldStripInheritedOrcaCodexHome, - isCodexStatusHooksEnabled, codexHomePathsEqual } from '../host-env/codex-home' -import { promoteAgentTeamsShimPath } from '../host-env/path' -import { - isClaudeLaunchCommand, - recoverFreshSpawnProviderRouting, - routesFreshSpawnsToLocalProvider -} from '../host-env/fresh-spawn-routing' +import { isClaudeLaunchCommand } from '../host-env/fresh-spawn-routing' import { stripRemotePaneEnvWhenHooksDisabled } from '../provider/liveness' import { isTuiAgent } from '../../../../shared/tui-agent-config' import { isClaudeAuthSwitchInProgress } from '../../../claude-accounts/live-pty-gate' @@ -30,44 +23,22 @@ import { hasClaudeAuthEnvConflict } from '../../../claude-accounts/environment' import { - isSafePtySessionId, mintPtySessionId, ptySessionIdForAgentCreateOperation } from '../../../daemon/pty-session-id' import { resolveWslSessionContext } from '../../../daemon/wsl-session-context' -import { isAgentStatusHooksEnabled } from '../../../agent-hooks/managed-agent-hook-controls' import { resolveLocalWindowsTerminalRuntimeOptions } from '../../../../shared/local-windows-terminal-runtime' import { resolveLocalProjectRuntimeForWorktreeId } from '../../../local-project-runtime-resolution' import { resolvePathEnvKey } from '../../../pty/windows-environment-path' -import { stampWslOrchestrationCompatibilityHost } from '../../../pty/wsl-orca-env' import { ensureCodexStateDbBackfillRecoveryStarted } from '../../../codex/codex-state-db-backfill-recovery' -import { clearProviderPtyState } from '../provider/state-cleanup' -import { awaitExplicitPiOmpGuestReadiness } from '../../../agent-hooks/wsl-pi-omp-guest-readiness' import type { RuntimePtySpawnState } from './spawn-state' export async function prepareRuntimePtySpawn( ctx: RuntimePtySpawnState ): Promise { const args = ctx.args - if (!ctx.preAdoptedStablePane) { - const pathUsable = ctx.deps.assertFolderWorkspacePtyPathUsable(args.worktreeId) - if (pathUsable) { - await pathUsable - } - } ctx.cwd = ctx.deps.resolvePtySpawnStartupCwd(args.worktreeId, args.cwd) ctx.provider = getProvider(args.connectionId) - const freshSpawnRecovery = ctx.preAdoptedStablePane - ? undefined - : recoverFreshSpawnProviderRouting( - ctx.provider, - args.connectionId, - args.sessionId, - args.isNewSession - ) - if (freshSpawnRecovery) { - await freshSpawnRecovery - } ctx.isClaudeLaunch = !ctx.preAdoptedStablePane && !args.connectionId && isClaudeLaunchCommand(args.command) if (ctx.isClaudeLaunch && isClaudeAuthSwitchInProgress()) { @@ -96,10 +67,7 @@ export async function prepareRuntimePtySpawn( terminalWindowsWslDistro: null } ctx.daemonShellOverride = ctx.terminalRuntimeOptions.shellOverride - ctx.isDaemonHostSpawn = - !args.connectionId && - !(ctx.provider instanceof LocalPtyProvider) && - !routesFreshSpawnsToLocalProvider(ctx.provider) + ctx.isDaemonHostSpawn = !args.connectionId ctx.callerRequestedSessionId = args.sessionId?.trim() ctx.requestedSessionId = ctx.callerRequestedSessionId ?? @@ -125,21 +93,52 @@ export async function prepareRuntimePtySpawn( terminalWindowsWslDistro: ctx.terminalRuntimeOptions.terminalWindowsWslDistro })?.distro ?? null) : null + if (!ctx.preAdoptedStablePane) { + ctx.wslGuest = await prepareWslDaemonSpawnRoute({ + sessions: ctx.deps.options?.wslDaemonSessions, + connectionId: args.connectionId, + sessionId: args.sessionId, + isNewSession: args.isNewSession, + distro: ctx.expectedWslDistro, + signal: args.signal + }) + } + if (ctx.wslGuest) { + ctx.provider = ctx.wslGuest.connection.provider + ctx.expectedWslDistro = ctx.wslGuest.connection.owner.distro + ctx.isDaemonHostSpawn = true + ctx.isNewDaemonSession = ctx.wslGuest.fresh + ctx.isClaudeLaunch &&= ctx.wslGuest.fresh || ctx.wslGuest.coldRestore === true + ctx.sessionId ??= args.agentSessionCreateOperationId + ? ptySessionIdForAgentCreateOperation(args.worktreeId, args.agentSessionCreateOperationId) + : mintPtySessionId(args.worktreeId) + ctx.effectiveSessionAppId = toAppWslPtyId(ctx.wslGuest.connection.owner, ctx.sessionId) + ctx.effectiveSessionRelayId = ctx.sessionId + } else { + if (!ctx.preAdoptedStablePane) { + const pathUsable = ctx.deps.assertFolderWorkspacePtyPathUsable(args.worktreeId) + if (pathUsable) { + await pathUsable + } + } + } ctx.codexSelectionTarget = getCodexSelectionTargetForPty( ctx.daemonShellOverride, ctx.cwd, ctx.expectedWslDistro ) - const codexResumePreparation = ctx.preAdoptedStablePane - ? null - : ctx.deps.prepareCodexResumeHome({ - connectionId: args.connectionId, - launchAgent: args.launchAgent, - providerSession: args.resumeProviderSession, - target: ctx.codexSelectionTarget, - launchEnv: args.env, - workspacePath: ctx.cwd - }) + const codexResumePreparation = + ctx.preAdoptedStablePane || (ctx.wslGuest && !ctx.wslGuest.fresh && !ctx.wslGuest.coldRestore) + ? null + : ctx.deps.prepareCodexResumeHome({ + connectionId: args.connectionId, + launchAgent: args.launchAgent, + providerSession: args.resumeProviderSession, + target: ctx.codexSelectionTarget, + launchEnv: args.env, + workspacePath: ctx.cwd, + wslExecution: ctx.wslGuest?.execution + }) const codexResumeLaunch = codexResumePreparation ? await ctx.deps.resolveCodexResumeLaunch(args.command, codexResumePreparation) : ctx.deps.noCodexResumeLaunch(ctx.preAdoptedStablePane ? undefined : args.command) @@ -149,7 +148,7 @@ export async function prepareRuntimePtySpawn( ctx.launchCommand = codexResumeLaunch.command ctx.claudeAuth = ctx.isClaudeLaunch && ctx.deps.prepareClaudeAuth - ? await ctx.deps.prepareClaudeAuth(ctx.codexSelectionTarget) + ? await ctx.deps.prepareClaudeAuth(ctx.codexSelectionTarget, ctx.wslGuest?.execution) : null if (ctx.isClaudeLaunch && isClaudeAuthSwitchInProgress()) { throw new Error(CLAUDE_AUTH_SWITCH_IN_PROGRESS_MESSAGE) @@ -190,10 +189,13 @@ export async function prepareRuntimePtySpawn( const selectLaunchCodexHome = async (): Promise => (await ctx.deps.getSelectedCodexHomePath?.(ctx.codexSelectionTarget, ctx.env, { workspacePath: ctx.cwd, + wslExecution: ctx.wslGuest?.execution, launchAgent: isTuiAgent(args.launchAgent) ? args.launchAgent : undefined })) ?? null ctx.selectedCodexHomePath = - !ctx.preAdoptedStablePane && !args.connectionId + !ctx.preAdoptedStablePane && + !args.connectionId && + (!ctx.wslGuest || ctx.wslGuest.fresh || ctx.wslGuest.coldRestore) ? getCompatibleSelectedCodexHomePath( ctx.codexSelectionTarget, codexResumeHome @@ -221,6 +223,7 @@ export async function prepareRuntimePtySpawn( ctx.codexSelectionTarget, (await ctx.deps.getSelectedCodexHomePath?.(ctx.codexSelectionTarget, ctx.env, { workspacePath: ctx.cwd, + wslExecution: ctx.wslGuest?.execution, launchAgent: 'codex' })) ?? null ), @@ -229,6 +232,7 @@ export async function prepareRuntimePtySpawn( ctx.codexSelectionTarget, (await ctx.deps.getSelectedCodexHomePath?.(ctx.codexSelectionTarget, ctx.env, { workspacePath: ctx.cwd, + wslExecution: ctx.wslGuest?.execution, launchAgent: 'codex', unavailableManagedHomePath })) ?? null @@ -236,7 +240,7 @@ export async function prepareRuntimePtySpawn( }) ctx.selectedCodexHomePath = resolution instanceof Promise ? await resolution : resolution } - if (args.launchAgent === 'codex' && ctx.selectedCodexHomePath) { + if (!ctx.wslGuest && args.launchAgent === 'codex' && ctx.selectedCodexHomePath) { await ensureCodexStateDbBackfillRecoveryStarted(ctx.selectedCodexHomePath) } ctx.codexResumeHomeSelected = Boolean( @@ -255,57 +259,7 @@ export async function prepareRuntimePtySpawn( skipCodexHomeEnv: ctx.skipCodexHomeEnv, settings: ptySettings }) - if (ctx.isDaemonHostSpawn && ctx.sessionId && !ctx.preAdoptedStablePane) { - if (!isSafePtySessionId(ctx.sessionId, getAppEnvironment().getPath('userData'))) { - throw new Error('Invalid PTY session id') - } - try { - ctx.env ??= {} - await inheritOmpLaunchEnvironment(ctx.env, { - isWsl: shouldSkipCodexHomeEnvForWindowsShell(ctx.daemonShellOverride, ctx.cwd), - launchAgent: args.launchAgent, - launchCommand: ctx.launchCommand - }) - await awaitExplicitPiOmpGuestReadiness({ - isWsl: shouldSkipCodexHomeEnvForWindowsShell(ctx.daemonShellOverride, ctx.cwd), - distro: ctx.codexSelectionTarget.runtime === 'wsl' ? ctx.expectedWslDistro : null, - codexHomePath: ctx.selectedCodexHomePath, - launchAgent: isTuiAgent(args.launchAgent) ? args.launchAgent : undefined, - launchCommand: ctx.launchCommand - }) - ctx.env = buildPtyHostEnv(ctx.sessionId, ctx.env ?? {}, { - isPackaged: getAppEnvironment().isPackaged(), - resourcesPath: process.resourcesPath, - userDataPath: getAppEnvironment().getPath('userData'), - selectedCodexHomePath: ctx.selectedCodexHomePath, - skipCodexHomeEnv: ctx.skipCodexHomeEnv, - stripInheritedOrcaCodexHome: ctx.stripInheritedOrcaCodexHome, - launchCommand: ctx.launchCommand, - launchAgent: isTuiAgent(args.launchAgent) ? args.launchAgent : undefined, - isWsl: shouldSkipCodexHomeEnvForWindowsShell(ctx.daemonShellOverride, ctx.cwd), - wslDistro: ctx.codexSelectionTarget.runtime === 'wsl' ? ctx.expectedWslDistro : null, - agentStatusHooksEnabled: isAgentStatusHooksEnabled(ptySettings), - disabledTuiAgents: ptySettings?.disabledTuiAgents, - codexStatusHooksEnabled: isCodexStatusHooksEnabled(ptySettings), - networkProxySettings: ptySettings, - routeBrowserOpensToClient: ctx.deps.runtime?.shouldRelayTerminalBrowserOpens?.(), - deferGitConfigGuardToDaemon: - ctx.provider.supportsGitCredentialGuardHost?.(ctx.sessionId) === true - }) - stampWslOrchestrationCompatibilityHost( - ctx.env, - ctx.deps.runtime?.getOrchestrationCompatibilityHostId?.(), - ctx.codexSelectionTarget.runtime === 'wsl' ? ctx.expectedWslDistro : null - ) - promoteAgentTeamsShimPath(ctx.env, ctx.requestedAgentTeamsPath) - } catch (error) { - // Why: host-env setup can materialize agent hooks/extensions before failing. - if (ctx.requestedSessionId === undefined) { - clearProviderPtyState(ctx.sessionId) - } - throw error - } - } + await prepareRuntimeHostSpawnEnvironment(ctx) return null } diff --git a/src/main/ipc/pty/runtime/spawn-state.ts b/src/main/ipc/pty/runtime/spawn-state.ts index 581d335ecdbc..82bc2c533b9a 100644 --- a/src/main/ipc/pty/runtime/spawn-state.ts +++ b/src/main/ipc/pty/runtime/spawn-state.ts @@ -1,3 +1,4 @@ +import type { WslDaemonSpawnRoute } from '../../../wsl/wsl-daemon-spawn-route' import type { IPtyProvider, PtySpawnOptions, PtySpawnResult } from '../../../providers/types' import type { CodexPaneHomeRoute } from '../../../codex/codex-pane-account-registry' import type { CodexAccountSelectionTarget } from '../../../codex-accounts/runtime-selection' @@ -24,6 +25,7 @@ export type RuntimePtySpawnState = { preAdoptedStablePane: AdoptStablePaneResult | null reattachedCodexHomeRoutes: Map cwd: string | undefined + wslGuest: WslDaemonSpawnRoute | null provider: IPtyProvider isClaudeLaunch: boolean terminalRuntimeOptions: { shellOverride?: string; terminalWindowsWslDistro?: string | null } @@ -144,6 +146,7 @@ export function createRuntimePtySpawnState( reattachedCodexHomeRoutes: new Map(), cwd: undefined, provider: localProvider, + wslGuest: null, isClaudeLaunch: false, terminalRuntimeOptions: {}, daemonShellOverride: undefined, diff --git a/src/main/ipc/pty/runtime/spawn-wsl-routing.test.ts b/src/main/ipc/pty/runtime/spawn-wsl-routing.test.ts new file mode 100644 index 000000000000..b84079cf7a56 --- /dev/null +++ b/src/main/ipc/pty/runtime/spawn-wsl-routing.test.ts @@ -0,0 +1,338 @@ +import { adoptMaterializedRuntimePtySpawn } from './spawn-early' +import { prepareWslDaemonReattachHooks } from '../../../wsl/wsl-daemon-reattach-hooks' +import type { OrcaRuntimeService } from '../../../runtime/orca-runtime' +import * as Registry from '../provider/registry' +import { executeRuntimePtySpawn } from './spawn-execute' +import { ensureWslHookRelayForReattach } from '../../../agent-hooks/wsl-hook-relay-reattach' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { BrowserWindow } from 'electron' +import { getDefaultSettings } from '../../../../shared/constants' +import { finishPtyShutdown } from '../provider/liveness' +import { localProvider } from '../provider/registry' +import { prepareRuntimePtySpawn } from './spawn-preflight' +import { buildRuntimePtySpawnOptions } from './spawn-options' +import { createRuntimePtySpawnState } from './spawn-state' +import type { PtyRuntimeControllerDeps } from './controller-deps' +import { + prepareWslDaemonSpawnRoute, + type WslDaemonSpawnRoute +} from '../../../wsl/wsl-daemon-spawn-route' +import { prepareWslGuestTerminalSpawn } from '../../../wsl/wsl-guest-terminal-preparation' +import { buildPtyHostEnv } from '../host-env/assembly' +import { ensureCodexStateDbBackfillRecoveryStarted } from '../../../codex/codex-state-db-backfill-recovery' +import { toAppWslPtyId } from '../../../../shared/wsl-pty-id' +import { ptySizes } from '../delivery/visibility-state' + +vi.mock('../../../wsl/wsl-daemon-reattach-hooks', () => ({ + prepareWslDaemonReattachHooks: vi.fn(async () => true) +})) +vi.mock('../../../agent-hooks/wsl-hook-relay-reattach', () => ({ + ensureWslHookRelayForReattach: vi.fn() +})) +vi.mock('../../../wsl/wsl-daemon-spawn-route', () => ({ prepareWslDaemonSpawnRoute: vi.fn() })) +vi.mock('../../../wsl/wsl-guest-terminal-preparation', () => ({ + prepareWslGuestTerminalSpawn: vi.fn() +})) +vi.mock('../host-env/assembly', () => ({ + buildPtyHostEnv: vi.fn(() => { + throw new Error('host env must not run for guest') + }) +})) +vi.mock('../../../codex/codex-state-db-backfill-recovery', () => ({ + ensureCodexStateDbBackfillRecoveryStarted: vi.fn() +})) +const HOST_DEFAULT_SHELL = 'wsl.exe' +const hostPlatform = process.platform +function makeDeps(): PtyRuntimeControllerDeps { + const noCodexResumeLaunch: PtyRuntimeControllerDeps['noCodexResumeLaunch'] = (command) => ({ + codexResumeHome: null, + command, + notifyResumeUnavailable: false, + droppedResumeArgv: false, + providerSession: null + }) + return { + store: undefined, + getSettings: () => ({ + ...getDefaultSettings('/tmp'), + terminalWindowsShell: HOST_DEFAULT_SHELL + }), + adoptStablePane: async () => null, + getLocalPtyStartupPromise: () => undefined, + getLocalPtyProviderStartupPromise: () => undefined, + prepareCodexResumeHome: () => null, + resolveCodexResumeLaunch: async (command) => noCodexResumeLaunch(command), + noCodexResumeLaunch, + reconcileSharedRuntimeResumeHome: async (resumeHome) => resumeHome.codexHomePath, + stripSequencedStartupResumeArgv: (env) => env, + assertFolderWorkspacePtyPathUsable: () => undefined, + resolvePtySpawnStartupCwd: (_worktreeId, cwd) => cwd, + requestSerializedBuffer: async () => null, + shutdownProviderAndDetectExit: async () => false, + rememberSyntheticKillExit: () => {}, + rememberRetiredRejectedPty: () => {}, + sendPtyExitToRenderer: () => {}, + sendPtySpawnedToRenderer: () => {}, + finishPtyShutdown, + trustedTerminalHandleEnv: new Set(), + retiredRejectedPtyIds: new Map(), + reversibleStopOwnersByPtyId: new Map(), + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: only `operations.ts` (write/clearBuffer) reads `mainWindow`; the spawn preflight and option build never touch it, and a real BrowserWindow cannot exist in vitest. + mainWindow: {} as BrowserWindow + } +} + +const endpoint = { + distro: 'Ubuntu', + distributionId: 'registration', + userName: 'alice', + userId: '1000', + home: '/home/alice', + runtime: '/bun', + entry: '/daemon', + envBinary: '/usr/bin/env', + socket: '/socket', + tokenPath: '/token', + serverBuildId: 'build' +} +const owner = { distro: 'Ubuntu', relayBuildId: 'owner' } +function route(fresh = true): WslDaemonSpawnRoute { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: These preflight tests never invoke provider operations; localProvider supplies the IPtyProvider surface consumed here. + const provider = localProvider as WslDaemonSpawnRoute['connection']['provider'] + return { + fresh, + connection: { owner, endpoint, provider }, + prepared: { owner, endpoint }, + execution: endpoint + } +} +beforeEach(() => { + vi.clearAllMocks() + Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) + vi.mocked(prepareWslDaemonSpawnRoute).mockResolvedValue(route()) + vi.mocked(prepareWslGuestTerminalSpawn).mockImplementation(async (_prepared, options) => ({ + ...options, + env: { ...options.env, GUEST_PREPARED: '1' } + })) +}) +afterEach(() => { + Object.defineProperty(process, 'platform', { configurable: true, value: hostPlatform }) + ptySizes.clear() + vi.restoreAllMocks() +}) + +describe('runtime guest daemon routing', () => { + it('captures guest identity before Claude/Codex preparation and builds guest env once', async () => { + const deps = makeDeps() + const order: string[] = [] + vi.mocked(prepareWslDaemonSpawnRoute).mockImplementation(async () => { + order.push('owner') + return route() + }) + deps.prepareClaudeAuth = vi.fn(async () => { + order.push('claude') + return { + envPatch: {}, + stripAuthEnv: false, + configDir: '/home/alice/.claude', + provenance: 'captured' + } + }) + deps.getSelectedCodexHomePath = vi.fn(async () => { + order.push('codex') + return '/home/alice/.codex' + }) + deps.prepareCodexResumeHome = vi.fn(() => null) + deps.assertFolderWorkspacePtyPathUsable = vi.fn(() => { + throw new Error('host path probe') + }) + const ctx = createRuntimePtySpawnState(deps, { + cols: 80, + rows: 24, + command: 'claude', + initiallyHidden: true + }) + await prepareRuntimePtySpawn(ctx) + expect(order).toEqual(['owner', 'claude', 'codex']) + expect(deps.prepareClaudeAuth).toHaveBeenCalledWith( + { runtime: 'wsl', wslDistro: 'Ubuntu' }, + endpoint + ) + expect(deps.prepareCodexResumeHome).toHaveBeenCalledWith( + expect.objectContaining({ wslExecution: endpoint }) + ) + expect(deps.getSelectedCodexHomePath).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + expect.objectContaining({ wslExecution: endpoint }) + ) + const raw = ctx.sessionId! + expect(raw).not.toMatch(/^wsl:/) + expect(ctx.effectiveSessionAppId).toBe(toAppWslPtyId(owner, raw)) + await buildRuntimePtySpawnOptions(ctx) + expect(ctx.spawnOptions.sessionId).toBe(raw) + expect(ctx.spawnOptions.isNewSession).toBe(true) + expect(ptySizes.get(ctx.effectiveSessionAppId!)).toEqual({ cols: 80, rows: 24 }) + expect(prepareWslGuestTerminalSpawn).toHaveBeenCalledOnce() + expect(buildPtyHostEnv).not.toHaveBeenCalled() + expect(ensureCodexStateDbBackfillRecoveryStarted).not.toHaveBeenCalled() + ctx.finishTerminalInstall() + }) + + it('retains encoded attach identity and skips account prep for a live guest owner', async () => { + vi.mocked(prepareWslDaemonSpawnRoute).mockResolvedValue(route(false)) + const deps = makeDeps() + deps.getSelectedCodexHomePath = vi.fn() + deps.prepareClaudeAuth = vi.fn() + deps.prepareCodexResumeHome = vi.fn(() => null) + const id = toAppWslPtyId(owner, 'pty2:kept:1') + const ctx = createRuntimePtySpawnState(deps, { + cols: 80, + rows: 24, + sessionId: id, + command: 'claude' + }) + await prepareRuntimePtySpawn(ctx) + await buildRuntimePtySpawnOptions(ctx) + expect(ctx.spawnOptions.sessionId).toBe(id) + expect(ctx.spawnOptions.isNewSession).toBeUndefined() + expect(ctx.spawnOptions.attachOnly).toBe(true) + expect(prepareWslGuestTerminalSpawn).not.toHaveBeenCalled() + expect(buildPtyHostEnv).not.toHaveBeenCalled() + expect(ctx.effectiveSessionAppId).toBe(id) + expect(deps.prepareClaudeAuth).not.toHaveBeenCalled() + expect(deps.getSelectedCodexHomePath).not.toHaveBeenCalled() + expect(deps.prepareCodexResumeHome).not.toHaveBeenCalled() + ctx.finishTerminalInstall() + }) + + it('passes explicit fresh raw identities to route selection without changing their daemon ID', async () => { + const deps = makeDeps() + const ctx = createRuntimePtySpawnState(deps, { + cols: 80, + rows: 24, + sessionId: 'pty2:allocated:1', + isNewSession: true + }) + await prepareRuntimePtySpawn(ctx) + expect(prepareWslDaemonSpawnRoute).toHaveBeenCalledWith( + expect.objectContaining({ sessionId: 'pty2:allocated:1', isNewSession: true }) + ) + expect(ctx.sessionId).toBe('pty2:allocated:1') + expect(ctx.effectiveSessionAppId).toBe(toAppWslPtyId(owner, 'pty2:allocated:1')) + }) +}) + +it('marks and registers the encoded guest ID before provider output can begin', async () => { + const deps = makeDeps() + const order: string[] = [] + deps.transitionSpawnHiddenRendererPtyDeliveryState = vi.fn((id) => { + order.push(`hidden:${id}`) + }) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: With no pane/workspace metadata, execute only invokes these optional runtime hooks. + deps.runtime = { + beginPtyRegistration: (id: string) => { + order.push(`register:${id}`) + }, + preparePtyExecutionContext: () => false, + assertPtyRegistrationAllowed: () => {} + } as unknown as OrcaRuntimeService + const ctx = createRuntimePtySpawnState(deps, { cols: 80, rows: 24, initiallyHidden: true }) + await prepareRuntimePtySpawn(ctx) + await buildRuntimePtySpawnOptions(ctx) + const appId = ctx.effectiveSessionAppId! + vi.spyOn(Registry, 'tryGetProviderForAgentSessionOwner').mockReturnValue(ctx.provider) + vi.spyOn(ctx.provider, 'spawn').mockImplementation(async (options) => { + expect(options.sessionId).toBe(ctx.sessionId) + expect(order.slice(0, 2)).toEqual([`hidden:${appId}`, `register:${appId}`]) + order.push('output-can-start') + return { id: appId, wslDistro: 'Ubuntu' } + }) + try { + await executeRuntimePtySpawn(ctx) + expect(ctx.result.id).toBe(appId) + expect(ensureWslHookRelayForReattach).not.toHaveBeenCalled() + } finally { + ctx.finishTerminalInstall() + } +}) + +it('prepares captured-user hooks on runtime guest reattach', async () => { + const deps = makeDeps() + vi.mocked(prepareWslDaemonSpawnRoute).mockResolvedValue(route(false)) + const id = toAppWslPtyId(owner, 'retained') + const ctx = createRuntimePtySpawnState(deps, { cols: 80, rows: 24, sessionId: id }) + await prepareRuntimePtySpawn(ctx) + await buildRuntimePtySpawnOptions(ctx) + vi.spyOn(Registry, 'tryGetProviderForAgentSessionOwner').mockReturnValue(ctx.provider) + vi.spyOn(ctx.provider, 'spawn').mockResolvedValue({ id, isReattach: true, wslDistro: 'Ubuntu' }) + try { + await executeRuntimePtySpawn(ctx) + expect(prepareWslDaemonReattachHooks).toHaveBeenCalledWith({ + result: expect.objectContaining({ id, isReattach: true }), + sessions: deps.options?.wslDaemonSessions, + hooksEnabled: true + }) + expect(ensureWslHookRelayForReattach).not.toHaveBeenCalled() + } finally { + ctx.finishTerminalInstall() + } +}) + +it('prepares captured-user hooks for an already-materialized runtime guest pane', async () => { + const deps = makeDeps() + const id = toAppWslPtyId(owner, 'materialized') + const ctx = createRuntimePtySpawnState(deps, { + cols: 80, + rows: 24, + adoptedStablePane: { + materialized: true, + owner: { ptyId: id, handle: 'handle', tabId: 'tab', leafId: 'leaf' }, + result: { id, isReattach: true, wslDistro: 'Ubuntu' } + } + }) + await adoptMaterializedRuntimePtySpawn(ctx) + expect(prepareWslDaemonReattachHooks).toHaveBeenCalledWith({ + result: expect.objectContaining({ id, isReattach: true }), + sessions: deps.options?.wslDaemonSessions, + hooksEnabled: true + }) +}) + +it('prepares same-ID runtime cold restore with captured guest account context', async () => { + vi.mocked(prepareWslDaemonSpawnRoute).mockResolvedValue({ ...route(false), coldRestore: true }) + const deps = makeDeps() + deps.getSelectedCodexHomePath = vi.fn(async () => null) + deps.prepareClaudeAuth = vi.fn(async () => ({ + configDir: '/home/alice/.claude', + envPatch: {}, + stripAuthEnv: false, + provenance: 'test' + })) + deps.prepareCodexResumeHome = vi.fn(() => null) + const id = toAppWslPtyId(owner, 'pty2:kept:1') + const ctx = createRuntimePtySpawnState(deps, { + cols: 80, + rows: 24, + sessionId: id, + command: 'claude --resume current' + }) + await prepareRuntimePtySpawn(ctx) + await buildRuntimePtySpawnOptions(ctx) + expect(ctx.spawnOptions).toMatchObject({ + sessionId: id, + isNewSession: false, + attachOnly: false, + command: 'claude --resume current' + }) + expect(deps.prepareClaudeAuth).toHaveBeenCalledWith(expect.anything(), endpoint) + expect(deps.prepareCodexResumeHome).toHaveBeenCalled() + expect(prepareWslGuestTerminalSpawn).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ sessionId: id }), + expect.anything(), + undefined, + 'confirmed-exited' + ) + ctx.finishTerminalInstall() +}) diff --git a/src/main/ipc/pty/session.ts b/src/main/ipc/pty/session.ts index e5c59d9b81a1..373b33a88693 100644 --- a/src/main/ipc/pty/session.ts +++ b/src/main/ipc/pty/session.ts @@ -1,3 +1,4 @@ +import type { WslDaemonSessions } from '../../wsl/wsl-daemon-sessions' import type { BrowserWindow, WebContents } from 'electron' import type { OrcaRuntimeService } from '../../runtime/orca-runtime' import type { Store } from '../../persistence' @@ -49,6 +50,7 @@ export type SerializeResult = { } | null export type PtyIpcSessionOptions = { + wslDaemonSessions?: WslDaemonSessions prepareCodexSessionResume?: PrepareCodexSessionResume awaitLocalPtyStartup?: () => Promise awaitLocalPtyProviderStartup?: () => Promise diff --git a/src/main/local-builds/local-build-compatibility.ts b/src/main/local-builds/local-build-compatibility.ts index 015377f9d5d6..422adb930e8e 100644 --- a/src/main/local-builds/local-build-compatibility.ts +++ b/src/main/local-builds/local-build-compatibility.ts @@ -5,10 +5,7 @@ import { } from '../../shared/local-build-compatibility' import type { DaemonPtyAdapter } from '../daemon/daemon-pty-adapter' import { DaemonPtyRouter } from '../daemon/daemon-pty-router' -import { DegradedDaemonPtyProvider } from '../daemon/degraded-daemon-pty-provider' import { getDaemonProvider } from '../daemon/daemon-init' -import { getLocalPtyProvider } from '../ipc/pty' -import { LocalPtyProvider } from '../providers/local-pty-provider' export type LocalBuildCompatibilityResult = { liveTerminalCount: number @@ -21,23 +18,11 @@ async function getLiveDaemonProtocols(): Promise<{ }> { const provider = getDaemonProvider() if (!provider) { - const localProvider = getLocalPtyProvider() - if (!(localProvider instanceof LocalPtyProvider)) { - throw new Error('Could not verify terminal preservation. Restart Orca and try again.') - } - const localProcesses = await localProvider.listProcesses() - if (localProcesses.length > 0) { - throw new Error( - 'Local build switching is blocked while non-persistent fallback terminals are running.' - ) - } - throw new Error('The terminal service is still starting. Try again in a moment.') - } - if (provider instanceof DegradedDaemonPtyProvider) { throw new Error( - 'Local build switching is blocked while the terminal service is in fallback mode. Restart Orca first.' + 'The terminal service is unavailable. Retry starting it before switching builds.' ) } + const adapters = provider instanceof DaemonPtyRouter ? provider.getAllAdapters() : [provider as DaemonPtyAdapter] const sessions = await Promise.all( diff --git a/src/main/memory/hydrate-local-pty-registry.ts b/src/main/memory/hydrate-local-pty-registry.ts index 4e5063b5fc14..66f205cdf0be 100644 --- a/src/main/memory/hydrate-local-pty-registry.ts +++ b/src/main/memory/hydrate-local-pty-registry.ts @@ -9,7 +9,6 @@ import { splitWorktreeId, worktreeIdComparisonKey } from '../../shared/worktree/ import { getDaemonProvider } from '../daemon/daemon-init' import type { DaemonPtyAdapter } from '../daemon/daemon-pty-adapter' import type { DaemonPtyRouter } from '../daemon/daemon-pty-router' -import type { DegradedDaemonPtyProvider } from '../daemon/degraded-daemon-pty-provider' import type { SessionInfo } from '../daemon/types' import { isFolderWorkspaceIdForRepo } from '../ipc/worktrees/folder-workspace-model' import type { Store } from '../persistence' @@ -251,7 +250,7 @@ function getVerifiedFolderWorktreeIds( } async function collectSessionInfos( - provider: DaemonPtyRouter | DaemonPtyAdapter | DegradedDaemonPtyProvider, + provider: DaemonPtyRouter | DaemonPtyAdapter, signal: AbortSignal ): Promise { const adapters = diff --git a/src/main/memory/pty-registry.test.ts b/src/main/memory/pty-registry.test.ts new file mode 100644 index 000000000000..8a5a47e34121 --- /dev/null +++ b/src/main/memory/pty-registry.test.ts @@ -0,0 +1,19 @@ +import { afterEach, expect, it } from 'vitest' +import { listRegisteredPtys, registerPty, unregisterPty } from './pty-registry' +import { toAppWslPtyId } from '../../shared/wsl-pty-id' + +const nativeId = 'native-memory-owner' +const guestId = toAppWslPtyId({ distro: 'Ubuntu', relayBuildId: 'build' }, 'guest-owner') +afterEach(() => { + unregisterPty(nativeId) + unregisterPty(guestId) +}) + +it('does not attribute a guest PID collision to the Windows host process', () => { + const entry = { pid: 1234, worktreeId: 'folder', sessionId: null, paneKey: null } + registerPty({ ...entry, ptyId: nativeId }) + registerPty({ ...entry, ptyId: guestId }) + + const rows = listRegisteredPtys().filter((row) => row.pid === 1234) + expect(rows).toEqual([{ ...entry, ptyId: nativeId }]) +}) diff --git a/src/main/memory/pty-registry.ts b/src/main/memory/pty-registry.ts index bb92081258f6..a70083cfd8b6 100644 --- a/src/main/memory/pty-registry.ts +++ b/src/main/memory/pty-registry.ts @@ -9,7 +9,7 @@ * handler. The write-side is just two calls — `register` on spawn, * `unregister` on teardown. * - * Scope: local PTYs only. SSH-backed PTYs execute on a remote host, so + * Scope: host-native PTYs only. SSH and guest WSL PTYs execute elsewhere, so * their memory does not contribute to Orca's process footprint and * cannot be queried with our local `ps` tree. */ @@ -21,7 +21,7 @@ export type PtyRegistration = { paneKey: string | null // Why number | null: captured at spawn time so the collector does not have // to reach back into the IPC module on every snapshot to resolve it. It is - // nullable because node-pty can return a process whose pid is briefly + // nullable because a PTY backend can return a process whose pid is briefly // unavailable (spawn succeeded but the OS hasn't published the pid yet); // storing null lets the collector render a zero-attribution row for that // PTY instead of throwing and dropping the whole snapshot. @@ -31,6 +31,10 @@ export type PtyRegistration = { const registry = new Map() export function registerPty(entry: PtyRegistration): void { + // Guest PIDs can collide with unrelated Windows processes. + if (entry.ptyId.startsWith('wsl:')) { + return + } registry.set(entry.ptyId, entry) } diff --git a/src/main/orcad/main-preflight-order.test.ts b/src/main/orcad/main-preflight-order.test.ts index 567267a113b7..873ff38212b6 100644 --- a/src/main/orcad/main-preflight-order.test.ts +++ b/src/main/orcad/main-preflight-order.test.ts @@ -1,23 +1,26 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { ORCAD_PROFILE_PREFLIGHT_FLAG, + ORCAD_NATIVE_PREFLIGHT_CHILD_FLAG, ORCAD_STARTUP_PREFLIGHT_FLAG } from '../../shared/orcad-profile-preflight' -const { order, profileProbe } = vi.hoisted(() => { +const { order, profileProbe, windowsProbe } = vi.hoisted(() => { const order: string[] = [] - return { order, profileProbe: vi.fn(async () => {}) } + return { order, profileProbe: vi.fn(async () => {}), windowsProbe: vi.fn(async () => {}) } }) vi.mock('./orcad-profile-preflight', () => ({ preflightBundledOrcadStartup: async () => { order.push('profile-admission') }, - runOrcadProfilePreflight: profileProbe + runOrcadProfilePreflight: profileProbe, + runBundledWindowsProfilePreflight: windowsProbe })) beforeEach(() => { vi.resetModules() + vi.spyOn(process, 'platform', 'get').mockReturnValue('linux') vi.spyOn(process, 'versions', 'get').mockReturnValue({ ...process.versions, bun: 'test' }) order.length = 0 }) @@ -35,6 +38,7 @@ vi.mock('./orcad-entry', () => ({ describe('orcad entry', () => { it.each([ { flag: ORCAD_PROFILE_PREFLIGHT_FLAG, nativeFeatures: true }, + { flag: ORCAD_NATIVE_PREFLIGHT_CHILD_FLAG, nativeFeatures: true }, { flag: ORCAD_STARTUP_PREFLIGHT_FLAG, nativeFeatures: false } ])( 'runs the selected disposable probe without starting a server: $flag', @@ -46,6 +50,23 @@ describe('orcad entry', () => { } ) + it.each([ + { flag: ORCAD_PROFILE_PREFLIGHT_FLAG, child: true }, + { flag: ORCAD_NATIVE_PREFLIGHT_CHILD_FLAG, child: false } + ])('routes Windows native probe without recursion: $flag', async ({ flag, child }) => { + vi.spyOn(process, 'platform', 'get').mockReturnValue('win32') + vi.spyOn(process, 'argv', 'get').mockReturnValue(['runtime', 'orcad.js', flag, 'nonce']) + await import('./orcad-app') + if (child) { + expect(windowsProbe).toHaveBeenCalledExactlyOnceWith('nonce') + expect(profileProbe).not.toHaveBeenCalled() + } else { + expect(profileProbe).toHaveBeenCalledExactlyOnceWith('nonce', { nativeFeatures: true }) + expect(windowsProbe).not.toHaveBeenCalled() + } + expect(order).toEqual([]) + }) + it('checks bundled runtime readiness before starting the server', async () => { await import('./orcad-app') await vi.waitFor(() => expect(order).toContain('main')) diff --git a/src/main/orcad/orcad-app.ts b/src/main/orcad/orcad-app.ts index 747d29307e3d..a8df1f8ab0e0 100644 --- a/src/main/orcad/orcad-app.ts +++ b/src/main/orcad/orcad-app.ts @@ -3,9 +3,14 @@ import process from 'node:process' import { main, resolveOrcadExitCode } from './orcad-entry' import { ORCAD_PROFILE_PREFLIGHT_FLAG, + ORCAD_NATIVE_PREFLIGHT_CHILD_FLAG, ORCAD_STARTUP_PREFLIGHT_FLAG } from '../../shared/orcad-profile-preflight' -import { preflightBundledOrcadStartup, runOrcadProfilePreflight } from './orcad-profile-preflight' +import { + preflightBundledOrcadStartup, + runOrcadProfilePreflight, + runBundledWindowsProfilePreflight +} from './orcad-profile-preflight' function failStartup(error: unknown): void { console.error('orcad: failed to start:', error) @@ -25,11 +30,19 @@ try { } const flag = process.argv[2] if ( - (flag === ORCAD_PROFILE_PREFLIGHT_FLAG || flag === ORCAD_STARTUP_PREFLIGHT_FLAG) && + flag === ORCAD_PROFILE_PREFLIGHT_FLAG && + process.platform === 'win32' && + process.argv.length === 4 + ) { + void runBundledWindowsProfilePreflight(process.argv[3]).catch(failStartup) + } else if ( + (flag === ORCAD_PROFILE_PREFLIGHT_FLAG || + flag === ORCAD_STARTUP_PREFLIGHT_FLAG || + flag === ORCAD_NATIVE_PREFLIGHT_CHILD_FLAG) && process.argv.length === 4 ) { void runOrcadProfilePreflight(process.argv[3], { - nativeFeatures: flag === ORCAD_PROFILE_PREFLIGHT_FLAG + nativeFeatures: flag !== ORCAD_STARTUP_PREFLIGHT_FLAG }).catch(failStartup) } else { void preflightBundledOrcadStartup() diff --git a/src/main/orcad/orcad-bundled-runtime.test.ts b/src/main/orcad/orcad-bundled-runtime.test.ts index ff6212289d93..b8100f3afc88 100644 --- a/src/main/orcad/orcad-bundled-runtime.test.ts +++ b/src/main/orcad/orcad-bundled-runtime.test.ts @@ -6,7 +6,11 @@ import { ORCAD_VERSION_FILENAME } from '../../shared/orcad-artifacts' const fixture = vi.hoisted(() => ({ exists: vi.fn<(path: string) => boolean>(), realpath: vi.fn<(path: string) => string>(), - spawn: vi.fn() + spawn: vi.fn(), + provider: vi.fn() +})) +vi.mock('../windows/windows-conpty-provider', () => ({ + resolveWindowsConptyProvider: fixture.provider })) vi.mock('node:fs', () => ({ existsSync: fixture.exists, realpathSync: fixture.realpath })) vi.mock('../../shared/child-process/run-process', () => ({ spawnProcess: fixture.spawn })) @@ -16,6 +20,7 @@ beforeEach(() => { ...process.versions, bun: ORCAD_BUN_VERSION }) + fixture.provider.mockReturnValue('C:\\slot\\conpty\\conpty.dll') fixture.exists.mockReturnValue(true) fixture.realpath.mockImplementation((path) => path) vi.spyOn(process, 'exit').mockImplementation(() => { @@ -27,6 +32,8 @@ beforeEach(() => { }) afterEach(() => { + vi.unstubAllGlobals() + vi.unstubAllEnvs() vi.restoreAllMocks() vi.clearAllMocks() }) @@ -85,3 +92,21 @@ describe('bundled Orca runtime validation', () => { expect(fixture.spawn).not.toHaveBeenCalled() }) }) + +it('selects the verified packaged Windows provider before application imports', () => { + vi.stubEnv('BUN_CONPTY_LIBRARY', 'C:\\foreign\\provider.dll') + vi.stubGlobal('process', { ...process, platform: 'win32', arch: 'arm64' }) + fixture.realpath.mockReturnValue('/real/runtime') + assertBundledOrcadRuntime() + expect(fixture.provider).toHaveBeenCalledWith(expect.stringContaining('conpty'), 'arm64') + expect(process.env.BUN_CONPTY_LIBRARY).toBe('C:\\slot\\conpty\\conpty.dll') +}) + +it('reports a damaged Windows provider as a permanent install fault', () => { + vi.stubGlobal('process', { ...process, platform: 'win32' }) + fixture.realpath.mockReturnValue('/real/runtime') + fixture.provider.mockImplementationOnce(() => { + throw new Error('bad hash') + }) + expect(() => assertBundledOrcadRuntime()).toThrow('bundled Windows terminal provider is invalid') +}) diff --git a/src/main/orcad/orcad-bundled-runtime.ts b/src/main/orcad/orcad-bundled-runtime.ts index 196f9866fba0..881c4ea7cd1c 100644 --- a/src/main/orcad/orcad-bundled-runtime.ts +++ b/src/main/orcad/orcad-bundled-runtime.ts @@ -6,6 +6,7 @@ import { orcadBunRuntimeFilename } from '../../shared/orcad-artifacts' import { ORCAD_BUN_VERSION } from '../../shared/orcad-bun-runtime' +import { resolveWindowsConptyProvider } from '../windows/windows-conpty-provider' export class OrcadBundledRuntimeError extends Error {} @@ -38,6 +39,19 @@ export function assertBundledOrcadRuntime(): void { `The bundled Orca runtime must be Bun ${ORCAD_BUN_VERSION}` ) } + if (process.platform === 'win32') { + try { + // Only child launches consume this: Bun snapshots native environment before JS starts. + process.env.BUN_CONPTY_LIBRARY = resolveWindowsConptyProvider( + join(directory, 'conpty'), + process.arch + ) + } catch (cause) { + throw new OrcadBundledRuntimeError('The bundled Windows terminal provider is invalid', { + cause + }) + } + } return } throw new OrcadBundledRuntimeError(`Start orcad with its bundled runtime: ${runtime}`) diff --git a/src/main/orcad/orcad-daemon-supervision.test.ts b/src/main/orcad/orcad-daemon-supervision.test.ts index 19616c9d5efa..e5337cd7a0d2 100644 --- a/src/main/orcad/orcad-daemon-supervision.test.ts +++ b/src/main/orcad/orcad-daemon-supervision.test.ts @@ -56,20 +56,19 @@ describe('startOrcadDaemon', () => { }) }) - it('reports degraded when fresh terminals would fall back to the local provider', async () => { + it('reports degraded when new terminals are unavailable', async () => { daemonOwnsFreshPersistentPtysMock.mockReturnValue(false) const result = await startOrcadDaemon() expect(result.state).toBe('degraded') }) it('fails open when the daemon cannot start at all', async () => { - initDaemonPtyProviderMock.mockRejectedValue(new Error('node-pty is missing')) + initDaemonPtyProviderMock.mockRejectedValue(new Error('Bun runtime is missing')) daemonOwnsFreshPersistentPtysMock.mockReturnValue(false) - // Fail-open, like the desktop: git, worktrees and non-persistent terminals must still - // serve. What must not happen is a thrown startup or a claim of persistence. + // Fail-open, like the desktop: git and worktrees must still serve. What must not happen is a thrown startup or a claim of persistence. await expect(startOrcadDaemon()).resolves.toEqual({ state: 'unavailable', - reason: 'node-pty is missing' + reason: 'Bun runtime is missing' }) }) }) diff --git a/src/main/orcad/orcad-daemon-supervision.ts b/src/main/orcad/orcad-daemon-supervision.ts index d35b03048d32..9f5ec30d3aa8 100644 --- a/src/main/orcad/orcad-daemon-supervision.ts +++ b/src/main/orcad/orcad-daemon-supervision.ts @@ -27,7 +27,7 @@ export type OrcadDaemonStartup = * Bring the daemon up and install it as the local PTY provider. * * Fail-open, like the desktop: a host that cannot start a daemon must still serve git, - * worktrees and non-persistent terminals. What it must NOT do is keep claiming persistence + * worktrees and other non-terminal services. What it must NOT do is keep claiming persistence * — `daemonOwnsFreshPersistentPtys()` is what the runtime reads for that, and it answers * false here without any extra bookkeeping. */ @@ -40,15 +40,14 @@ export async function startOrcadDaemon(): Promise { const reason = error instanceof Error ? error.message : String(error) console.error( `[orcad] The terminal daemon did not start: ${reason}\n` + - '[orcad] Terminals will run in-process and WILL NOT survive an orcad restart.' + '[orcad] New terminals are unavailable until the terminal service recovers.' ) return { state: 'unavailable', reason } } if (!daemonOwnsFreshPersistentPtys()) { - const reason = 'daemon adopted in degraded mode; fresh terminals run on the local provider' + const reason = 'daemon adopted in degraded mode; new terminals are unavailable' console.warn( - `[orcad] ${reason}. Existing daemon sessions keep working, but new terminals will not ` + - 'survive an orcad restart until the daemon is restarted.' + `[orcad] ${reason}. Existing daemon sessions retain their owners; retry the terminal service to create new terminals.` ) return { state: 'degraded', reason } } diff --git a/src/main/orcad/orcad-profile-preflight.test.ts b/src/main/orcad/orcad-profile-preflight.test.ts index 1b6952226e16..7fd07d0f9744 100644 --- a/src/main/orcad/orcad-profile-preflight.test.ts +++ b/src/main/orcad/orcad-profile-preflight.test.ts @@ -2,10 +2,17 @@ import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { ProcessResult, ProcessSpec } from '../../shared/child-process/run-process' import { ORCAD_BUN_VERSION } from '../../shared/orcad-bun-runtime' -import { ORCAD_STARTUP_PREFLIGHT_FLAG } from '../../shared/orcad-profile-preflight' +import { + ORCAD_STARTUP_PREFLIGHT_FLAG, + ORCAD_NATIVE_PREFLIGHT_CHILD_FLAG +} from '../../shared/orcad-profile-preflight' import { OrcadBundledRuntimeError } from './orcad-bundled-runtime' import { resolveOrcadExitCode } from './orcad-exit-code' -import { preflightBundledOrcadStartup, runOrcadProfilePreflight } from './orcad-profile-preflight' +import { + preflightBundledOrcadStartup, + runOrcadProfilePreflight, + runBundledWindowsProfilePreflight +} from './orcad-profile-preflight' const fixture = vi.hoisted(() => ({ identity: vi.fn(), @@ -205,3 +212,39 @@ describe('bundled Orca startup readiness', () => { expect(output).toHaveBeenCalledWith(readyResult(nonce).stdout) }) }) + +it('runs explicit Windows native checks in a child with the selected initial environment', async () => { + vi.stubEnv('BUN_CONPTY_LIBRARY', 'C:\\slot\\conpty\\conpty.dll') + const output = vi.spyOn(console, 'log').mockImplementation(() => {}) + try { + await runBundledWindowsProfilePreflight(nonce) + expect(fixture.run).toHaveBeenCalledWith( + expect.objectContaining({ + program: join('/slot', 'bun-runtime.exe'), + args: [ + '--no-env-file', + '--config=NUL', + '--no-install', + join('/slot', 'orcad.js'), + ORCAD_NATIVE_PREFLIGHT_CHILD_FLAG, + nonce + ], + env: expect.objectContaining({ BUN_CONPTY_LIBRARY: 'C:\\slot\\conpty\\conpty.dll' }), + terminationBarrier: true + }) + ) + expect(fixture.native).not.toHaveBeenCalled() + expect(fixture.sql).not.toHaveBeenCalled() + expect(output).toHaveBeenCalledWith(readyResult(nonce).stdout) + } finally { + vi.unstubAllEnvs() + } +}) + +it.each([{ code: 78 }, { timedOut: true }, { outputTruncated: true }])( + 'rejects a failed Windows native child: %j', + async (failure) => { + fixture.run.mockResolvedValue({ ...readyResult(nonce), ...failure }) + await expect(runBundledWindowsProfilePreflight(nonce)).rejects.toThrow('failed readiness') + } +) diff --git a/src/main/orcad/orcad-profile-preflight.ts b/src/main/orcad/orcad-profile-preflight.ts index 4a1bf1da68ae..31c1cbb5c357 100644 --- a/src/main/orcad/orcad-profile-preflight.ts +++ b/src/main/orcad/orcad-profile-preflight.ts @@ -6,6 +6,7 @@ import { join } from 'node:path' import { preflightProfileStateRuntime } from '../persistence/profile-state/profile-state-runtime-preflight' import { ORCAD_STARTUP_PREFLIGHT_FLAG, + ORCAD_NATIVE_PREFLIGHT_CHILD_FLAG, ORCAD_PROFILE_PREFLIGHT_TIMEOUT_MS, parseOrcadProfilePreflight, orcadProfilePreflightResponseSchema, @@ -24,18 +25,20 @@ export async function preflightBundledOrcadStartup(): Promise { if (!process.versions.bun) { return } + await runBundledPreflightChild(ORCAD_STARTUP_PREFLIGHT_FLAG, randomUUID(), process.platform) +} + +async function runBundledPreflightChild( + flag: string, + nonce: string, + platform: NodeJS.Platform +): Promise { const directory = resolveOrcadInstallRoot() const identity = await readInstalledVersion(directory) - const nonce = randomUUID() // Keep disposable SQLite ownership and native state out of the serving process. const result = await runProcess({ - program: join(directory, orcadBunRuntimeFilename(process.platform)), - args: [ - ...bunOwnedRuntimeArgs(), - join(directory, 'orcad.js'), - ORCAD_STARTUP_PREFLIGHT_FLAG, - nonce - ], + program: join(directory, orcadBunRuntimeFilename(platform)), + args: [...bunOwnedRuntimeArgs(platform), join(directory, 'orcad.js'), flag, nonce], env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1' }, timeoutMs: ORCAD_PROFILE_PREFLIGHT_TIMEOUT_MS, maxOutputBytes: 64 * 1024, @@ -46,7 +49,7 @@ export async function preflightBundledOrcadStartup(): Promise { throw new Failure(`The bundled Orca runtime failed readiness: ${result.stderr}`) } try { - parseOrcadProfilePreflight(result.stdout, nonce, ORCAD_BUN_VERSION, identity) + return parseOrcadProfilePreflight(result.stdout, nonce, ORCAD_BUN_VERSION, identity) } catch (cause) { throw new OrcadBundledRuntimeError('The bundled runtime returned invalid readiness identity', { cause @@ -97,3 +100,14 @@ async function readInstalledVersion(directory: string): Promise { ) } } + +/** Bun reads the native selector from its initial environment, not later JS assignments. */ +export async function runBundledWindowsProfilePreflight(nonce: string | undefined): Promise { + const checkedNonce = z.string().uuid().parse(nonce) + const response = await runBundledPreflightChild( + ORCAD_NATIVE_PREFLIGHT_CHILD_FLAG, + checkedNonce, + 'win32' + ) + console.log(JSON.stringify(response)) +} diff --git a/src/main/persistence/loading-store/loaded-state-parsing.ts b/src/main/persistence/loading-store/loaded-state-parsing.ts index b93dc2515001..9c418f46daf5 100644 --- a/src/main/persistence/loading-store/loaded-state-parsing.ts +++ b/src/main/persistence/loading-store/loaded-state-parsing.ts @@ -1,3 +1,4 @@ +import { decryptWslConsumerRecoveries } from './wsl-consumer-recovery-secrets' import { homedir } from 'node:os' import { normalizeProxyUrl } from '../../../shared/network-proxy' import { normalizeKagiSessionLink } from '../../../shared/browser-url' @@ -152,6 +153,12 @@ export class LoadedStateParsingOperations { (value) => normalizeKagiSessionLink(value) !== null ) } + if (parsed.wslPtyConsumerRecoveries !== undefined) { + parsed.wslPtyConsumerRecoveries = decryptWslConsumerRecoveries( + parsed.wslPtyConsumerRecoveries, + this.runtime.protectedSecrets + ) + } parsed.sshPtyConsumerRecoveries = ( Array.isArray(parsed.sshPtyConsumerRecoveries) ? parsed.sshPtyConsumerRecoveries : [] ) diff --git a/src/main/persistence/loading-store/ssh-lease-durable-mutation.ts b/src/main/persistence/loading-store/relay-lease-durable-mutation.ts similarity index 83% rename from src/main/persistence/loading-store/ssh-lease-durable-mutation.ts rename to src/main/persistence/loading-store/relay-lease-durable-mutation.ts index 28f361a12cd3..dbf51f5bf384 100644 --- a/src/main/persistence/loading-store/ssh-lease-durable-mutation.ts +++ b/src/main/persistence/loading-store/relay-lease-durable-mutation.ts @@ -4,7 +4,7 @@ import { type WriteFlushBarrierOperations } from './write-flush-barriers' -export type SshLeaseDurableMutationRuntime = Pick< +export type RelayLeaseDurableMutationRuntime = Pick< StoreRuntimeState, | 'dirtyProfileStateDomains' | 'profileMaintenancePending' @@ -14,10 +14,10 @@ export type SshLeaseDurableMutationRuntime = Pick< | 'writesFrozen' > -export async function runSshLeaseDurableMutation( - runtime: SshLeaseDurableMutationRuntime, +export async function runRelayLeaseDurableMutation( + runtime: RelayLeaseDurableMutationRuntime, barriers: WriteFlushBarrierOperations, - domain: 'sshPtyConsumerRecoveries' | 'sshRemotePtyLeases', + domain: 'sshPtyConsumerRecoveries' | 'sshRemotePtyLeases' | 'wslPtyConsumerRecoveries', mutate: () => DurableProfileStateMutation ): Promise { const writeMutation = (): DurableProfileStateMutation => { diff --git a/src/main/persistence/loading-store/ssh-lease-recovery-operations.ts b/src/main/persistence/loading-store/ssh-lease-recovery-operations.ts index 7188b4efb664..20389f82df64 100644 --- a/src/main/persistence/loading-store/ssh-lease-recovery-operations.ts +++ b/src/main/persistence/loading-store/ssh-lease-recovery-operations.ts @@ -45,11 +45,11 @@ import type { TerminalBindingRecoveryOperations } from './terminal-binding-recov import type { WriteSchedulingOperations } from './write-scheduling' import { scheduleSave } from './write-scheduling' import { - runSshLeaseDurableMutation, - type SshLeaseDurableMutationRuntime -} from './ssh-lease-durable-mutation' + runRelayLeaseDurableMutation, + type RelayLeaseDurableMutationRuntime +} from './relay-lease-durable-mutation' -type SshLeaseRecoveryOperationsRuntime = SshLeaseDurableMutationRuntime & +type SshLeaseRecoveryOperationsRuntime = RelayLeaseDurableMutationRuntime & Pick const sshLeaseRecoveryOperationsContext = Symbol('SshLeaseRecoveryOperations') @@ -210,7 +210,7 @@ export function getSshPtyConsumerRecoveryOperations( state: owner[sshLeaseRecoveryOperationsContext].runtime.state, protectedSecrets: owner[sshLeaseRecoveryOperationsContext].runtime.protectedSecrets, runDurableMutation: (mutate) => - runSshLeaseDurableMutation( + runRelayLeaseDurableMutation( owner[sshLeaseRecoveryOperationsContext].runtime, owner[sshLeaseRecoveryOperationsContext].flushBarriers, 'sshPtyConsumerRecoveries', @@ -264,7 +264,7 @@ export function getSshPtyLeaseOperations(owner: SshLeaseRecoveryOperations): Ssh owner[sshLeaseRecoveryOperationsContext].flushBarriers.flush() }, runDurableMutation: (mutate) => - runSshLeaseDurableMutation( + runRelayLeaseDurableMutation( owner[sshLeaseRecoveryOperationsContext].runtime, owner[sshLeaseRecoveryOperationsContext].flushBarriers, 'sshRemotePtyLeases', diff --git a/src/main/persistence/loading-store/state-serialization-secret-handling.ts b/src/main/persistence/loading-store/state-serialization-secret-handling.ts index 40d40818343c..dd2109f939b4 100644 --- a/src/main/persistence/loading-store/state-serialization-secret-handling.ts +++ b/src/main/persistence/loading-store/state-serialization-secret-handling.ts @@ -1,3 +1,4 @@ +import { isWslDaemonRecovery } from '../../../shared/wsl-pty-consumer-recovery' import { serializeCompleteProfileStateDomains, serializeSelectiveProfileStateDomains @@ -9,6 +10,7 @@ import { collectFolderWorkspaceDiffComments } from '../../folder-workspace-diff- import { PROTECTED_SECRET_SLOT, sshPtyOwnerLeaseSecretSlot, + wslPtyOwnerLeaseSecretSlot, type ProtectedSecretRetentionUpdate } from '../../protected-secret-persistence' import { stripRetiredGlobalSettings } from '../applying-settings/terminal-settings-migrations' @@ -106,6 +108,16 @@ export class StateSerializationSecretHandlingOperations { ) } break + case 'wslPtyConsumerRecoveries': + stateToSave[domain] = (this.runtime.state.wslPtyConsumerRecoveries ?? []).map((record) => + isWslDaemonRecovery(record) + ? record + : { + ...record, + ownerLease: encrypt(wslPtyOwnerLeaseSecretSlot(record), record.ownerLease) + } + ) + break case 'sshRemotePtyLeases': stateToSave[domain] = this.runtime.state.sshRemotePtyLeases break @@ -203,6 +215,21 @@ export class StateSerializationSecretHandlingOperations { folderWorkspaceDiffComments: collectFolderWorkspaceDiffComments( this.runtime.state.folderWorkspaces ), + ...(this.runtime.state.wslPtyConsumerRecoveries !== undefined + ? { + wslPtyConsumerRecoveries: this.runtime.state.wslPtyConsumerRecoveries.map((record) => + isWslDaemonRecovery(record) + ? record + : { + ...record, + ownerLease: encryptToSentinel( + wslPtyOwnerLeaseSecretSlot(record), + record.ownerLease + ) + } + ) + } + : {}), sshPtyConsumerRecoveries: (this.runtime.state.sshPtyConsumerRecoveries ?? []).map( (record) => ({ ...record, diff --git a/src/main/persistence/loading-store/store-domain-composition.ts b/src/main/persistence/loading-store/store-domain-composition.ts index 70f22e90c5c7..3bdf05efc8ed 100644 --- a/src/main/persistence/loading-store/store-domain-composition.ts +++ b/src/main/persistence/loading-store/store-domain-composition.ts @@ -1,3 +1,7 @@ +import { + WslConsumerRecoveryOperations, + installWslConsumerRecoveryOperationsContext +} from './wsl-consumer-recovery-operations' import type { StoreRuntimeState } from './store-runtime-state' import type { Store } from './store' import { LoadedStateAdaptationOperations } from './loaded-state-adaptation' @@ -79,6 +83,7 @@ export type StoreDomainOperations = WriteSchedulingOperations & SshProfileOperations & RetiredWorktreeNamePersistence & SshLeaseRecoveryOperations & + WslConsumerRecoveryOperations & WriteFlushBarrierOperations export type StoreDomains = { @@ -103,6 +108,7 @@ export type StoreDomains = { sshProfiles: SshProfileOperations retiredWorktreeNames: RetiredWorktreeNamePersistence sshLeases: SshLeaseRecoveryOperations + wslConsumers: WslConsumerRecoveryOperations } export const STORE_DOMAIN_OPERATION_CLASSES = [ @@ -121,6 +127,7 @@ export const STORE_DOMAIN_OPERATION_CLASSES = [ SshProfileOperations, RetiredWorktreeNamePersistence, SshLeaseRecoveryOperations, + WslConsumerRecoveryOperations, WriteFlushBarrierOperations ] as const @@ -140,6 +147,7 @@ export function installStoreDomainContexts(target: Store, domains: StoreDomains) installSshProfileOperationsContext(target, domains.sshProfiles) installRetiredWorktreeNamePersistenceContext(target, domains.retiredWorktreeNames) installSshLeaseRecoveryOperationsContext(target, domains.sshLeases) + installWslConsumerRecoveryOperationsContext(target, domains.wslConsumers) installWriteFlushBarrierOperationsContext(target, domains.flushBarriers) } @@ -175,6 +183,7 @@ export function createStoreDomains(runtime: StoreRuntimeState): StoreDomains { bindingRecovery, scheduling ) + const wslConsumers = new WslConsumerRecoveryOperations(runtime, flushBarriers) return { adaptation, cohorts, @@ -196,6 +205,7 @@ export function createStoreDomains(runtime: StoreRuntimeState): StoreDomains { ptyBindings, sshProfiles, retiredWorktreeNames, - sshLeases + sshLeases, + wslConsumers } } diff --git a/src/main/persistence/loading-store/wsl-consumer-recovery-operations.test.ts b/src/main/persistence/loading-store/wsl-consumer-recovery-operations.test.ts new file mode 100644 index 000000000000..4df093cda4e3 --- /dev/null +++ b/src/main/persistence/loading-store/wsl-consumer-recovery-operations.test.ts @@ -0,0 +1,111 @@ +import { describe, expect, it, vi } from 'vitest' +import { fixture } from './profile-state-delayed-authority-fixture' +import { Store } from './store' +import { setSecretStore } from '../../../shared/secret-store' + +vi.mock('../../telemetry/client', () => ({ track: vi.fn() })) +vi.mock('../../telemetry/cohort-classifier', () => ({ + getCohortAtEmit: () => ({ nth_repo_added: 2 }) +})) +vi.mock('../../ssh/ssh-config-parser', () => ({ + loadUserSshConfig: () => ({ hosts: [] }), + sshConfigHostsToTargets: () => [] +})) + +const recovery = { + distro: 'Ubuntu', + relayBuildId: '0.1.0+build', + clientInstanceId: 'client', + clientGeneration: 1, + ownerGeneration: 1, + ownerLease: 'c5f48e88-0dbe-4894-90a2-876c0a3c0d8b' +} + +function installSecretStore(available = true) { + setSecretStore({ + isEncryptionAvailable: () => available, + encryptString: (value) => Buffer.from(`sealed:${value}`), + decryptString: (value) => value.toString().slice('sealed:'.length), + describeProtectionGap: () => null + }) +} + +describe('durable guest terminal owner recovery', () => { + it('reserves writes before mutation and fences removal by consumer identity', async () => { + installSecretStore() + const { store, authority, readState } = await fixture() + const gate = authority.pause() + store.updateSettings({ theme: 'dark' }) + const flush = store.flushPendingOrThrowAsync() + await gate.started.promise + const first = store.upsertWslPtyConsumerRecovery(recovery) + const second = store.upsertWslPtyConsumerRecovery({ + ...recovery, + clientInstanceId: 'new-owner' + }) + const staleRemoval = store.removeWslPtyConsumerRecovery(recovery, 'client') + expect(store.getWslPtyConsumerRecovery(recovery)).toBeNull() + gate.finish.resolve() + await Promise.all([flush, first, second, staleRemoval]) + expect(store.getWslPtyConsumerRecovery(recovery)?.clientInstanceId).toBe('new-owner') + expect(readState().wslPtyConsumerRecoveries).toHaveLength(1) + expect( + authority.captures.flat().filter((row) => row.domain === 'wslPtyConsumerRecoveries') + ).toHaveLength(2) + }) + + it('retains separate leases for old builds and other distros', async () => { + installSecretStore() + const { store, readState } = await fixture() + const newer = { ...recovery, relayBuildId: 'new-build' } + const debian = { ...recovery, distro: 'Debian' } + await store.upsertWslPtyConsumerRecovery(recovery) + await store.upsertWslPtyConsumerRecovery(newer) + await store.upsertWslPtyConsumerRecovery(debian) + await store.removeWslPtyConsumerRecovery(newer, 'client') + expect(store.getWslPtyConsumerRecovery(recovery)).toEqual(recovery) + expect(store.getWslPtyConsumerRecovery(debian)).toEqual(debian) + expect(store.getWslPtyConsumerRecovery(newer)).toBeNull() + expect(readState().wslPtyConsumerRecoveries).toHaveLength(2) + }) + + it('encrypts both selective and final writes and restores the lease after restart', async () => { + installSecretStore() + const { store, readState } = await fixture() + await store.upsertWslPtyConsumerRecovery(recovery) + expect(JSON.stringify(readState())).not.toContain(recovery.ownerLease) + await store.flushPendingOrThrowAsync() + const serialized = JSON.stringify(readState()) + expect(serialized).not.toContain(recovery.ownerLease) + const loaded = new Store({ serializedState: serialized }) + expect(loaded.getWslPtyConsumerRecovery(recovery)).toEqual(recovery) + loaded.freezeWrites() + }) + + it('refuses to replace an unreadable owner lease with a new identity', async () => { + installSecretStore() + const { store, readState } = await fixture() + await store.upsertWslPtyConsumerRecovery(recovery) + installSecretStore(false) + const loaded = new Store({ serializedState: JSON.stringify(readState()) }) + expect(() => loaded.getWslPtyConsumerRecovery(recovery)).toThrow('secrets are locked') + loaded.freezeWrites() + installSecretStore() + }) + + it('does not acknowledge a write that the authority refused', async () => { + installSecretStore() + const { store, authority, readState } = await fixture() + await store.upsertWslPtyConsumerRecovery(recovery) + const gate = authority.pause() + const removal = expect(store.removeWslPtyConsumerRecovery(recovery, 'client')).rejects.toThrow( + 'disk refused' + ) + await gate.started.promise + gate.finish.reject(new Error('disk refused')) + await removal + expect(readState().wslPtyConsumerRecoveries).toHaveLength(1) + await store.removeWslPtyConsumerRecovery(recovery, 'client') + expect(readState().wslPtyConsumerRecoveries).toEqual([]) + }) +}) diff --git a/src/main/persistence/loading-store/wsl-consumer-recovery-operations.ts b/src/main/persistence/loading-store/wsl-consumer-recovery-operations.ts new file mode 100644 index 000000000000..00f3dab0b46f --- /dev/null +++ b/src/main/persistence/loading-store/wsl-consumer-recovery-operations.ts @@ -0,0 +1,183 @@ +import { + normalizeWslDaemonRecovery, + sameWslDaemonEndpoint, + type WslDaemonIncarnation, + type WslDaemonRecovery +} from '../../../shared/wsl-daemon-recovery' +import type { StoreRuntimeState } from './store-runtime-state' +import type { WriteFlushBarrierOperations } from './write-flush-barriers' +import type { WslPtyOwner } from '../../../shared/wsl-pty-id' +import { + normalizeWslPtyConsumerRecovery, + isWslDaemonRecovery, + wslPtyOwnerKey, + type WslPtyConsumerRecovery +} from '../../../shared/wsl-pty-consumer-recovery' +import { wslPtyOwnerLeaseSecretSlot } from '../../protected-secret-persistence' +import { runRelayLeaseDurableMutation } from './relay-lease-durable-mutation' + +const context = Symbol('WslConsumerRecoveryOperations') +type Context = { runtime: StoreRuntimeState; barriers: WriteFlushBarrierOperations } + +export class WslConsumerRecoveryOperations { + readonly [context]: Context + + constructor(runtime: StoreRuntimeState, barriers: WriteFlushBarrierOperations) { + this[context] = { runtime, barriers } + } + + getWslPtyConsumerRecovery(owner: WslPtyOwner): WslPtyConsumerRecovery | null { + const { runtime } = this[context] + const key = wslPtyOwnerKey(owner) + const record = runtime.state.wslPtyConsumerRecoveries?.find( + (row) => wslPtyOwnerKey(row) === key + ) + if (!record || isWslDaemonRecovery(record)) { + return null + } + if (runtime.protectedSecrets.isSealed(wslPtyOwnerLeaseSecretSlot(owner), record.ownerLease)) { + throw new Error('WSL terminal owner lease is unavailable while profile secrets are locked') + } + return structuredClone(record) + } + + getWslDaemonRecovery(owner: WslPtyOwner): WslDaemonRecovery | null { + const record = this[context].runtime.state.wslPtyConsumerRecoveries?.find( + (row) => wslPtyOwnerKey(row) === wslPtyOwnerKey(owner) && isWslDaemonRecovery(row) + ) + return record && isWslDaemonRecovery(record) ? structuredClone(record) : null + } + + listWslDaemonRecoveries(): WslDaemonRecovery[] { + return structuredClone( + (this[context].runtime.state.wslPtyConsumerRecoveries ?? []).filter(isWslDaemonRecovery) + ) + } + + async upsertWslDaemonRecovery( + record: WslDaemonRecovery, + expectedIncarnation?: WslDaemonIncarnation | null + ): Promise { + const normalized = normalizeWslDaemonRecovery(record) + if (!normalized) { + throw new Error('Invalid WSL daemon recovery record') + } + const { runtime, barriers } = this[context] + const key = wslPtyOwnerKey(normalized) + const refusal = await runRelayLeaseDurableMutation( + runtime, + barriers, + 'wslPtyConsumerRecoveries', + () => { + const previous = runtime.state.wslPtyConsumerRecoveries + const rows = previous ?? [] + if (rows.some((row) => wslPtyOwnerKey(row) === key && !isWslDaemonRecovery(row))) { + return { + value: new Error('WSL terminal identity already belongs to a relay consumer'), + persist: false + } + } + const current = rows.find((row) => wslPtyOwnerKey(row) === key) + if (current && isWslDaemonRecovery(current)) { + if (!sameWslDaemonEndpoint(current.endpoint, normalized.endpoint)) { + return { value: new Error('WSL daemon owner identity cannot change'), persist: false } + } + if (JSON.stringify(current.incarnation) === JSON.stringify(normalized.incarnation)) { + return { value: undefined, persist: false } + } + if ( + !normalized.incarnation || + expectedIncarnation === undefined || + JSON.stringify(current.incarnation ?? null) !== JSON.stringify(expectedIncarnation) + ) { + return { value: new Error('WSL daemon incarnation admission changed'), persist: false } + } + } else if (expectedIncarnation) { + return { + value: new Error('WSL daemon incarnation admission disappeared'), + persist: false + } + } + runtime.state.wslPtyConsumerRecoveries = [ + ...rows.filter((row) => wslPtyOwnerKey(row) !== key), + normalized + ] + return { + value: undefined, + rollback: () => { + runtime.state.wslPtyConsumerRecoveries = previous + } + } + } + ) + if (refusal) { + throw refusal + } + } + + async upsertWslPtyConsumerRecovery(record: WslPtyConsumerRecovery): Promise { + const normalized = normalizeWslPtyConsumerRecovery(record) + if (!normalized) { + throw new Error('Invalid WSL terminal consumer recovery record') + } + const { runtime, barriers } = this[context] + const key = wslPtyOwnerKey(normalized) + const refusal = await runRelayLeaseDurableMutation( + runtime, + barriers, + 'wslPtyConsumerRecoveries', + () => { + if ( + runtime.state.wslPtyConsumerRecoveries?.some( + (row) => wslPtyOwnerKey(row) === key && isWslDaemonRecovery(row) + ) + ) { + return { + value: new Error('WSL terminal identity already belongs to a daemon'), + persist: false + } + } + runtime.state.wslPtyConsumerRecoveries = [ + ...(runtime.state.wslPtyConsumerRecoveries ?? []).filter( + (row) => wslPtyOwnerKey(row) !== key + ), + normalized + ] + return { value: undefined } + } + ) + if (refusal) { + throw refusal + } + } + + async removeWslPtyConsumerRecovery( + owner: WslPtyOwner, + expectedClientInstanceId: string + ): Promise { + const { runtime, barriers } = this[context] + const key = wslPtyOwnerKey(owner) + await runRelayLeaseDurableMutation(runtime, barriers, 'wslPtyConsumerRecoveries', () => { + const records = runtime.state.wslPtyConsumerRecoveries ?? [] + const current = records.find((row) => wslPtyOwnerKey(row) === key) + if ( + current && + (isWslDaemonRecovery(current) || current.clientInstanceId !== expectedClientInstanceId) + ) { + return { value: undefined, persist: false } + } + runtime.state.wslPtyConsumerRecoveries = records.filter((row) => wslPtyOwnerKey(row) !== key) + return { value: undefined } + }) + if (!runtime.state.wslPtyConsumerRecoveries?.some((row) => wslPtyOwnerKey(row) === key)) { + runtime.protectedSecrets.removeRetainedBlob(wslPtyOwnerLeaseSecretSlot(owner)) + } + } +} + +export function installWslConsumerRecoveryOperationsContext( + target: WslConsumerRecoveryOperations, + source: WslConsumerRecoveryOperations +): void { + Object.defineProperty(target, context, { value: source[context] }) +} diff --git a/src/main/persistence/loading-store/wsl-consumer-recovery-secrets.ts b/src/main/persistence/loading-store/wsl-consumer-recovery-secrets.ts new file mode 100644 index 000000000000..50b9291748fa --- /dev/null +++ b/src/main/persistence/loading-store/wsl-consumer-recovery-secrets.ts @@ -0,0 +1,39 @@ +import { normalizeWslDaemonRecovery } from '../../../shared/wsl-daemon-recovery' +import { + normalizeWslPtyConsumerRecovery, + type WslPtyRecoveryRecord +} from '../../../shared/wsl-pty-consumer-recovery' +import { + wslPtyOwnerLeaseSecretSlot, + type ProtectedSecretPersistence +} from '../../protected-secret-persistence' +import { isLegacySshPtyOwnerLease } from '../leasing-ssh-ptys/secret-validation' + +export function decryptWslConsumerRecoveries( + value: unknown, + secrets: ProtectedSecretPersistence +): WslPtyRecoveryRecord[] { + if (!Array.isArray(value)) { + return [] + } + return value.flatMap((raw: unknown): WslPtyRecoveryRecord[] => { + if (raw && typeof raw === 'object' && 'kind' in raw) { + const daemon = normalizeWslDaemonRecovery(raw) + return daemon ? [daemon] : [] + } + const record = normalizeWslPtyConsumerRecovery(raw, true) + if (!record) { + return [] + } + const slot = wslPtyOwnerLeaseSecretSlot(record) + const decrypted = secrets.decryptWithStatus(slot, record.ownerLease, isLegacySshPtyOwnerLease) + const normalized = + decrypted.status === 'unavailable' || (decrypted.status === 'failed' && !decrypted.plaintext) + ? record + : normalizeWslPtyConsumerRecovery({ ...record, ownerLease: decrypted.plaintext }) + if (!normalized) { + secrets.removeRetainedBlob(slot) + } + return normalized ? [normalized] : [] + }) +} diff --git a/src/main/persistence/loading-store/wsl-daemon-recovery.test.ts b/src/main/persistence/loading-store/wsl-daemon-recovery.test.ts new file mode 100644 index 000000000000..ae3ecb37bfd6 --- /dev/null +++ b/src/main/persistence/loading-store/wsl-daemon-recovery.test.ts @@ -0,0 +1,215 @@ +import { describe, expect, it, vi } from 'vitest' +import { fixture } from './profile-state-delayed-authority-fixture' +import { Store } from './store' +import { + normalizeWslDaemonRecovery, + type WslDaemonRecovery +} from '../../../shared/wsl-daemon-recovery' + +vi.mock('../../telemetry/client', () => ({ track: vi.fn() })) +vi.mock('../../telemetry/cohort-classifier', () => ({ + getCohortAtEmit: () => ({ nth_repo_added: 2 }) +})) +vi.mock('../../ssh/ssh-config-parser', () => ({ + loadUserSshConfig: () => ({ hosts: [] }), + sshConfigHostsToTargets: () => [] +})) + +const recovery: WslDaemonRecovery = { + kind: 'daemon', + distro: 'Ubuntu', + relayBuildId: 'profile-user-runtime-daemon-hash', + endpoint: { + distro: 'Ubuntu', + distributionId: 'registration-id', + userName: 'alice', + userId: '1000', + home: '/home/alice', + runtime: '/home/alice/.orca/runtime/bun', + entry: '/home/alice/.orca/daemon/daemon.js', + envBinary: '/usr/bin/env', + socket: '/home/alice/.orca/profile/control.sock', + tokenPath: '/home/alice/.orca/profile/control.token', + serverBuildId: 'daemon-build' + } +} + +describe('persisted guest daemon endpoint identity', () => { + it('roundtrips optional owner protocol metadata while preserving strict endpoint validation', () => { + const versioned = { ...recovery, endpoint: { ...recovery.endpoint, protocolVersion: 36 } } + const loaded = new Store({ + serializedState: JSON.stringify({ wslPtyConsumerRecoveries: [versioned] }) + }) + expect(loaded.getWslDaemonRecovery(recovery)).toEqual(versioned) + expect(normalizeWslDaemonRecovery(recovery)).toEqual(recovery) + for (const protocolVersion of [0, -1, 1.5, '36', null, Number.MAX_SAFE_INTEGER + 1]) { + expect( + normalizeWslDaemonRecovery({ + ...recovery, + endpoint: { ...recovery.endpoint, protocolVersion } + }) + ).toBeNull() + } + expect( + normalizeWslDaemonRecovery({ + ...versioned, + endpoint: { ...versioned.endpoint, token: 'secret' } + }) + ).toBeNull() + loaded.freezeWrites() + }) + + it('roundtrips JSON without copying any guest authentication token', () => { + const loaded = new Store({ + serializedState: JSON.stringify({ wslPtyConsumerRecoveries: [recovery] }) + }) + expect(loaded.getWslDaemonRecovery(recovery)).toEqual(recovery) + expect(loaded.getWslPtyConsumerRecovery(recovery)).toBeNull() + const records = loaded.listWslDaemonRecoveries() + records[0]!.endpoint.userName = 'mutated-copy' + expect(loaded.getWslDaemonRecovery(recovery)?.endpoint.userName).toBe('alice') + loaded.freezeWrites() + }) + + it('selectively persists and reloads the confirmed endpoint with real SQLite', async () => { + const { store, authority, readState } = await fixture() + const gate = authority.pause() + let acknowledged = false + const write = store.upsertWslDaemonRecovery(recovery).then(() => { + acknowledged = true + }) + await gate.started.promise + expect(acknowledged).toBe(false) + expect(readState().wslPtyConsumerRecoveries ?? []).toEqual([]) + gate.finish.resolve() + await write + expect(authority.captures.flat().map((row) => row.domain)).toEqual(['wslPtyConsumerRecoveries']) + expect(readState().wslPtyConsumerRecoveries).toEqual([recovery]) + const loaded = new Store({ serializedState: JSON.stringify(readState()) }) + expect(loaded.listWslDaemonRecoveries()).toEqual([recovery]) + loaded.freezeWrites() + }) + + it('isolates profiles and preserves distinct captured users/build owners', async () => { + const first = await fixture() + const second = await fixture() + const bob: WslDaemonRecovery = { + ...recovery, + relayBuildId: 'bob-owner', + endpoint: { ...recovery.endpoint, userName: 'bob', userId: '1001', home: '/home/bob' } + } + await first.store.upsertWslDaemonRecovery(recovery) + await first.store.upsertWslDaemonRecovery(bob) + expect(first.store.listWslDaemonRecoveries()).toHaveLength(2) + expect(second.store.listWslDaemonRecoveries()).toEqual([]) + expect(second.readState().wslPtyConsumerRecoveries ?? []).toEqual([]) + }) + + it.each([ + { ...recovery, endpoint: { ...recovery.endpoint, token: 'must-not-persist' } }, + { ...recovery, endpoint: { ...recovery.endpoint, userId: 'unknown' } }, + { ...recovery, endpoint: { ...recovery.endpoint, socket: '/home/alice/../bob/socket' } }, + { ...recovery, endpoint: { ...recovery.endpoint, tokenPath: 'relative' } }, + { ...recovery, endpoint: { ...recovery.endpoint, distro: 'Debian' } }, + { ...recovery, endpoint: { ...recovery.endpoint, entry: undefined } } + ])('rejects an invalid or secret-bearing record %#', (invalid) => { + expect(normalizeWslDaemonRecovery(invalid)).toBeNull() + const loaded = new Store({ + serializedState: JSON.stringify({ wslPtyConsumerRecoveries: [invalid] }) + }) + expect(loaded.listWslDaemonRecoveries()).toEqual([]) + loaded.freezeWrites() + }) + + it('refuses to retarget a retained owner to another user', async () => { + const { store, readState } = await fixture() + await store.upsertWslDaemonRecovery(recovery) + await expect( + store.upsertWslDaemonRecovery({ + ...recovery, + endpoint: { ...recovery.endpoint, userName: 'bob' } + }) + ).rejects.toThrow('owner identity cannot change') + expect(readState().wslPtyConsumerRecoveries).toEqual([recovery]) + await store.upsertWslDaemonRecovery({ ...recovery, relayBuildId: 'another-owner' }) + expect(store.listWslDaemonRecoveries()).toHaveLength(2) + }) + + it('does not acknowledge a refused disk write', async () => { + const { store, authority, readState } = await fixture() + const gate = authority.pause() + const write = expect(store.upsertWslDaemonRecovery(recovery)).rejects.toThrow('disk refused') + await gate.started.promise + gate.finish.reject(new Error('disk refused')) + await write + expect(store.getWslDaemonRecovery(recovery)).toBeNull() + expect(readState().wslPtyConsumerRecoveries ?? []).toEqual([]) + }) +}) + +const oldIncarnation = { + pid: 42, + startedAtMs: 100, + launchNonce: 'old', + linuxStartTicks: '123', + bootId: 'boot' +} +const newIncarnation = { ...oldIncarnation, pid: 43, launchNonce: 'new', linuxStartTicks: '456' } + +it('admits incarnation changes only against the expected prior while keeping endpoint immutable', async () => { + const { store, readState } = await fixture() + await store.upsertWslDaemonRecovery(recovery) + await store.upsertWslDaemonRecovery({ ...recovery, incarnation: oldIncarnation }, null) + await expect( + store.upsertWslDaemonRecovery({ ...recovery, incarnation: newIncarnation }, null) + ).rejects.toThrow('admission changed') + await expect( + store.upsertWslDaemonRecovery( + { + ...recovery, + endpoint: { ...recovery.endpoint, socket: '/other' }, + incarnation: newIncarnation + }, + oldIncarnation + ) + ).rejects.toThrow('owner identity cannot change') + await store.upsertWslDaemonRecovery({ ...recovery, incarnation: newIncarnation }, oldIncarnation) + expect(readState().wslPtyConsumerRecoveries).toEqual([ + { ...recovery, incarnation: newIncarnation } + ]) +}) +it('rolls back refused incarnation admission without losing the prior durable owner', async () => { + const { store, authority, readState } = await fixture() + await store.upsertWslDaemonRecovery({ ...recovery, incarnation: oldIncarnation }) + const gate = authority.pause() + const rejected = expect( + store.upsertWslDaemonRecovery({ ...recovery, incarnation: newIncarnation }, oldIncarnation) + ).rejects.toThrow('disk refused') + await gate.started.promise + expect(readState().wslPtyConsumerRecoveries).toEqual([ + { ...recovery, incarnation: oldIncarnation } + ]) + gate.finish.reject(new Error('disk refused')) + await rejected + expect(store.getWslDaemonRecovery(recovery)?.incarnation).toEqual(oldIncarnation) +}) + +it('admits equivalent protocol metadata without changing the persisted owner identity', async () => { + const { store } = await fixture() + await store.upsertWslDaemonRecovery(recovery) + const versioned = { ...recovery, endpoint: { ...recovery.endpoint, protocolVersion: 36 } } + await store.upsertWslDaemonRecovery(versioned) + expect(store.getWslDaemonRecovery(recovery)).toEqual(recovery) + await expect( + store.upsertWslDaemonRecovery({ + ...versioned, + endpoint: { ...versioned.endpoint, protocolVersion: 37 } + }) + ).rejects.toThrow('cannot change') + await expect( + store.upsertWslDaemonRecovery({ + ...versioned, + endpoint: { ...versioned.endpoint, userName: 'bob' } + }) + ).rejects.toThrow('cannot change') +}) diff --git a/src/main/persistence/profile-state/profile-state-live-store-factory.test.ts b/src/main/persistence/profile-state/profile-state-live-store-factory.test.ts index b96d09d44f36..c35bd8058042 100644 --- a/src/main/persistence/profile-state/profile-state-live-store-factory.test.ts +++ b/src/main/persistence/profile-state/profile-state-live-store-factory.test.ts @@ -6,6 +6,7 @@ import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest import { agentHookServer } from '../../agent-hooks/server' import { buildProfileStateCutoverFixture } from '../profile-state-cutover-fixture' import type { Store } from '../loading-store/store' +import type { WslDaemonRecovery } from '../../../shared/wsl-daemon-recovery' import { ProfileStateSqliteAuthority } from './profile-state-sqlite-authority' import { createLiveProfileStateStore } from './profile-state-live-store-factory' import { @@ -81,6 +82,44 @@ function readState(input: ReturnType) { } describe('live profile authority admission', () => { + it('recovers the exact guest daemon owner after the SQLite writer is closed and reopened', async () => { + const input = options() + const recovery: WslDaemonRecovery = { + kind: 'daemon', + distro: 'Ubuntu', + relayBuildId: 'captured-user-daemon', + endpoint: { + distro: 'Ubuntu', + distributionId: 'guest-registration', + userName: 'alice', + userId: '1000', + home: '/home/alice', + runtime: '/home/alice/.orca/bun', + entry: '/home/alice/.orca/daemon.js', + envBinary: '/usr/bin/env', + socket: '/home/alice/.orca/control.sock', + tokenPath: '/home/alice/.orca/control.token', + serverBuildId: 'captured-build' + } + } + const first = await open(input) + await first.store.upsertWslDaemonRecovery(recovery) + expect(readState(input).wslPtyConsumerRecoveries).toEqual([recovery]) + await first.store.freezeWritesAsync() + + const reopened = await open(input) + expect(reopened.store.getWslDaemonRecovery(recovery)).toEqual(recovery) + await expect( + reopened.store.upsertWslDaemonRecovery({ + ...recovery, + endpoint: { ...recovery.endpoint, userName: 'bob', userId: '1001' } + }) + ).rejects.toThrow('owner identity cannot change') + expect(readState(input).wslPtyConsumerRecoveries).toEqual([recovery]) + const otherProfile = await open(options()) + expect(otherProfile.store.getWslDaemonRecovery(recovery)).toBeNull() + }) + it.each([false, true])( 'hands unbound aliases to admitted startup only (worker refused=%s)', async (refused) => { diff --git a/src/main/protected-secret-persistence.ts b/src/main/protected-secret-persistence.ts index c72af0966197..fd5d6b090a6c 100644 --- a/src/main/protected-secret-persistence.ts +++ b/src/main/protected-secret-persistence.ts @@ -1,3 +1,5 @@ +import { wslPtyOwnerKey } from '../shared/wsl-pty-consumer-recovery' +import type { WslPtyOwner } from '../shared/wsl-pty-id' import { getSecretStore } from '../shared/secret-store' export const PROTECTED_SECRET_SLOT = { @@ -11,6 +13,10 @@ export function sshPtyOwnerLeaseSecretSlot(targetId: string): string { return `sshPtyConsumerRecoveries.ownerLease:${targetId}` } +export function wslPtyOwnerLeaseSecretSlot(owner: WslPtyOwner): string { + return `wslPtyConsumerRecoveries.ownerLease:${wslPtyOwnerKey(owner)}` +} + export type ProtectedSecretDecryption = { plaintext: string status: 'decrypted' | 'failed' | 'unavailable' diff --git a/src/main/providers/agent-foreground-process-git-bash.win32.test.ts b/src/main/providers/agent-foreground-process-git-bash.win32.test.ts index f8fe5f2ac79d..60616dee9197 100644 --- a/src/main/providers/agent-foreground-process-git-bash.win32.test.ts +++ b/src/main/providers/agent-foreground-process-git-bash.win32.test.ts @@ -1,125 +1,23 @@ -import { mkdtempSync } from 'node:fs' -import { tmpdir } from 'node:os' import { join } from 'node:path' -import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' -import { removeTreeSync } from '../../shared/windows-transient-lock-removal' -import { WINDOWS_GIT_BASH_SHELL } from '../../shared/windows-terminal-shell' -import { confirmPtyShellForeground } from '../daemon/pty-subprocess/pty-shell-foreground-confirmation' -import { createPtyShellLaunchPlan } from '../daemon/pty-subprocess/shell-launch-plan' -import { spawnNativeDaemonPty } from '../daemon/pty-subprocess/native-pty-spawn' -import { canUseBunPty, spawnBunPty } from '../daemon/pty-subprocess/bun-pty-process' -import { createWindowsBunPtyLaunch } from '../daemon/pty-subprocess/windows-bun-pty-launch' -import { createDaemonPtyEnvironment } from '../daemon/pty-subprocess/spawn-environment' -import type { PtySubprocessOptions } from '../daemon/pty-subprocess' -import { isGitForWindowsBashLauncherPath } from '../git-bash' -import { readWindowsPtyJobProcessIds } from './windows-pty-job-membership' +import { describe, it } from 'vitest' +import { runBundledBunFixture } from '../bundled-bun-test-execution' const describeOnWindows = process.platform === 'win32' ? describe : describe.skip describeOnWindows("Git Bash launcher shell proof with Orca's real launch", () => { - let userData: string - const previousUserData = process.env.ORCA_USER_DATA_PATH - - beforeAll(() => { - // The launch plan writes shell-ready wrappers under the user-data root. - userData = mkdtempSync(join(tmpdir(), 'orca-git-bash-proof-')) - process.env.ORCA_USER_DATA_PATH = userData - }) - - afterAll(() => { - if (previousUserData === undefined) { - delete process.env.ORCA_USER_DATA_PATH - } else { - process.env.ORCA_USER_DATA_PATH = previousUserData - } - removeTreeSync(userData) - }) - it.each([ ['login shell', {}], - // Why: with the Codex preflight set, the exec'd shell runs Orca's --rcfile wrapper. ['rcfile wrapper', { ORCA_CODEX_LAUNCH_PREFLIGHT: 'C:\\orca-missing-preflight.exe' }] ])( 'confirms an idle %s prompt, refutes a running command, and confirms again', async (_label, extraEnv) => { - const opts: PtySubprocessOptions = { - sessionId: 'git-bash-shell-proof', - cols: 120, - rows: 30, - cwd: tmpdir(), - shellOverride: WINDOWS_GIT_BASH_SHELL, - env: extraEnv - } - const env = createDaemonPtyEnvironment(opts) - const plan = createPtyShellLaunchPlan(opts, env) - expect(isGitForWindowsBashLauncherPath(plan.shellPath)).toBe(true) - expect(plan.shellArgs.join(' ')).toContain('exec "$BASH"') - const spawned = await spawnNativeDaemonPty( - { ...plan, env, cols: opts.cols, rows: opts.rows }, - { - canUseBunPty, - spawnBunPty: (args) => - spawnBunPty(args, { - // Source tests use the TS worker; packaged hosts resolve their adjacent JS worker. - createWindowsLaunch: (launch) => - createWindowsBunPtyLaunch(launch, { - workerPath: join( - __dirname, - '../daemon/pty-subprocess/windows-bun-pty-gate-entry.ts' - ) - }) - }) - } + await runBundledBunFixture( + join(__dirname, 'git-bash-foreground-bun-fixture.ts'), + 'verifyGitBashForeground', + extraEnv, + 60_000 ) - const proc = spawned.process - let output = '' - let dead = false - proc.onData((chunk) => { - output += chunk - }) - proc.onExit(() => { - dead = true - }) - const confirm = (): Promise => - confirmPtyShellForeground({ - process: proc, - shellPath: spawned.shellPath, - isDead: () => dead - }) - try { - await vi.waitFor(() => expect(output).toContain('$'), { timeout: 20_000 }) - // Launcher, exec stub, interactive bash: the shape that a size-1 or size-2 rule never matches. - await vi.waitFor(() => expect(readWindowsPtyJobProcessIds(proc)?.size).toBe(3), { - timeout: 5_000 - }) - await vi.waitFor(async () => expect(await confirm(), 'initial prompt').toBe(true), { - timeout: 5_000 - }) - - // Interrupt only after the child is ready, not during a transient shell fork. - proc.write( - "node -e \"console.log(['ORCA','FOREGROUND_READY'].join('_')); setInterval(() => {}, 1000)\"\r" - ) - await vi.waitFor(() => expect(output).toContain('ORCA_FOREGROUND_READY'), { - timeout: 10_000 - }) - await vi.waitFor(async () => expect(await confirm()).toBe(false), { timeout: 10_000 }) - - proc.write('\x03') - await vi.waitFor( - async () => { - expect(dead, 'terminal survived foreground interrupt').toBe(false) - expect(await confirm(), 'prompt after interrupt').toBe(true) - }, - { timeout: 10_000 } - ) - - proc.write('sleep 60 &\r') - await vi.waitFor(async () => expect(await confirm()).toBe(false), { timeout: 10_000 }) - } finally { - proc.kill() - } }, - 60_000 + 65_000 ) }) diff --git a/src/main/providers/bun-pty-transcript-fixture.ts b/src/main/providers/bun-pty-transcript-fixture.ts new file mode 100644 index 000000000000..14266478a546 --- /dev/null +++ b/src/main/providers/bun-pty-transcript-fixture.ts @@ -0,0 +1,87 @@ +import { join } from 'node:path' +import { z } from 'zod' +import { spawnBunPty } from '../daemon/pty-subprocess/bun-pty-process' +import { createWindowsBunPtyLaunch } from '../daemon/pty-subprocess/windows-bun-pty-launch' +import { runBundledBunFixture } from '../bundled-bun-test-execution' + +const transcript = z.array( + z.union([z.object({ data: z.string() }), z.object({ resizeTo: z.number() })]) +) +export type PtyTranscriptEvent = z.infer[number] +type TranscriptOptions = { + script: string + cols: number + rows: number + resizes?: { atMs: number; cols: number }[] +} + +/** Captures real output and resize ordering for replay into the production emulator. */ +export async function recordBunPtyTranscript( + options: TranscriptOptions +): Promise { + if (!process.versions.bun) { + return transcript.parse( + await runBundledBunFixture(__filename, 'recordBunPtyTranscript', options, 25_000) + ) + } + const proc = spawnBunPty( + { + file: process.execPath, + args: ['-e', options.script], + cols: options.cols, + rows: options.rows, + cwd: process.cwd(), + env: Object.fromEntries( + Object.entries(process.env).filter( + (entry): entry is [string, string] => entry[1] !== undefined + ) + ) + }, + { + createWindowsLaunch: (args) => + createWindowsBunPtyLaunch(args, { + workerPath: join(__dirname, '../daemon/pty-subprocess/windows-bun-pty-gate-entry.ts') + }) + } + ) + const events: PtyTranscriptEvent[] = [] + const output = proc.onData((data) => events.push({ data })) + let exitSubscription: { dispose(): void } | undefined + const exited = new Promise((resolve, reject) => { + exitSubscription = proc.onExit(({ exitCode }) => + exitCode === 0 ? resolve() : reject(new Error(`Fixture exited with ${exitCode}`)) + ) + }) + void exited.catch(() => {}) + const timers: ReturnType[] = [] + try { + await proc.waitForSpawn?.() + for (const resize of options.resizes ?? []) { + timers.push( + setTimeout(() => { + events.push({ resizeTo: resize.cols }) + proc.resize(resize.cols, options.rows) + }, resize.atMs) + ) + } + let timeout: ReturnType | undefined + try { + await Promise.race([ + exited, + new Promise((_resolve, reject) => { + timeout = setTimeout(() => reject(new Error('PTY transcript fixture timed out')), 20_000) + }) + ]) + } finally { + clearTimeout(timeout) + } + return events + } finally { + for (const timer of timers) { + clearTimeout(timer) + } + output.dispose() + exitSubscription?.dispose() + proc.destroy() + } +} diff --git a/src/main/providers/git-bash-foreground-bun-fixture.ts b/src/main/providers/git-bash-foreground-bun-fixture.ts new file mode 100644 index 000000000000..17b20e0cf232 --- /dev/null +++ b/src/main/providers/git-bash-foreground-bun-fixture.ts @@ -0,0 +1,134 @@ +import { mkdtempSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import assert from 'node:assert/strict' +import { setTimeout as delay } from 'node:timers/promises' +import { removeTreeSync } from '../../shared/windows-transient-lock-removal' +import { WINDOWS_GIT_BASH_SHELL } from '../../shared/windows-terminal-shell' +import { confirmPtyShellForeground } from '../daemon/pty-subprocess/pty-shell-foreground-confirmation' +import { createPtyShellLaunchPlan } from '../daemon/pty-subprocess/shell-launch-plan' +import { spawnNativeDaemonPty } from '../daemon/pty-subprocess/native-pty-spawn' +import { canUseBunPty, spawnBunPty } from '../daemon/pty-subprocess/bun-pty-process' +import { createWindowsBunPtyLaunch } from '../daemon/pty-subprocess/windows-bun-pty-launch' +import { createDaemonPtyEnvironment } from '../daemon/pty-subprocess/spawn-environment' +import type { PtySubprocessOptions } from '../daemon/pty-subprocess' +import { isGitForWindowsBashLauncherPath } from '../git-bash' +import { readWindowsPtyJobProcessIds } from './windows-pty-job-membership' + +async function waitForAssertion(check: () => unknown, options: { timeout: number }): Promise { + const deadline = Date.now() + options.timeout + for (;;) { + try { + await check() + return + } catch (error) { + if (Date.now() >= deadline) { + throw error + } + await delay(50) + } + } +} + +export async function verifyGitBashForeground(extraEnv: Record): Promise { + assert.equal(process.platform, 'win32') + assert(process.versions.bun) + const previousUserData = process.env.ORCA_USER_DATA_PATH + const userData = mkdtempSync(join(tmpdir(), 'orca-git-bash-proof-')) + process.env.ORCA_USER_DATA_PATH = userData + try { + const opts: PtySubprocessOptions = { + sessionId: 'git-bash-shell-proof', + cols: 120, + rows: 30, + cwd: tmpdir(), + shellOverride: WINDOWS_GIT_BASH_SHELL, + env: extraEnv + } + const env = createDaemonPtyEnvironment(opts) + const plan = await createPtyShellLaunchPlan(opts, env) + assert(isGitForWindowsBashLauncherPath(plan.shellPath)) + assert(plan.shellArgs.join(' ').includes('exec "$BASH"')) + const spawned = await spawnNativeDaemonPty( + { ...plan, env, cols: opts.cols, rows: opts.rows }, + { + canUseBunPty, + spawnBunPty: (args) => + spawnBunPty(args, { + // Source tests use the TS worker; packaged hosts resolve their adjacent JS worker. + createWindowsLaunch: (launch) => + createWindowsBunPtyLaunch(launch, { + workerPath: join( + __dirname, + '../daemon/pty-subprocess/windows-bun-pty-gate-entry.ts' + ) + }) + }) + } + ) + const proc = spawned.process + let output = '' + let dead = false + proc.onData((chunk) => { + output += chunk + }) + const exited = new Promise((resolve) => { + proc.onExit(() => { + dead = true + resolve() + }) + }) + const confirm = (): Promise => + confirmPtyShellForeground({ + process: proc, + shellPath: spawned.shellPath, + isDead: () => dead + }) + try { + await waitForAssertion(() => assert(output.includes('$')), { timeout: 20_000 }) + // Launcher, exec stub, interactive bash: the shape that a size-1 or size-2 rule never matches. + await waitForAssertion(() => assert.equal(readWindowsPtyJobProcessIds(proc)?.size, 3), { + timeout: 5_000 + }) + await waitForAssertion(async () => assert.equal(await confirm(), true, 'initial prompt'), { + timeout: 5_000 + }) + + // Interrupt only after the child is ready, not during a transient shell fork. + proc.write( + "node -e \"console.log(['ORCA','FOREGROUND_READY'].join('_')); setInterval(() => {}, 1000)\"\r" + ) + await waitForAssertion(() => assert(output.includes('ORCA_FOREGROUND_READY')), { + timeout: 10_000 + }) + await waitForAssertion(async () => assert.equal(await confirm(), false), { timeout: 10_000 }) + + proc.write('\x03') + await waitForAssertion( + async () => { + assert.equal(dead, false, 'terminal survived foreground interrupt') + assert.equal(await confirm(), true, 'prompt after interrupt') + }, + { timeout: 10_000 } + ) + + proc.write('sleep 60 &\r') + await waitForAssertion(async () => assert.equal(await confirm(), false), { timeout: 10_000 }) + } finally { + proc.kill() + await Promise.race([ + exited, + delay(5_000, undefined, { ref: false }).then(() => { + throw new Error('Git Bash fixture did not exit') + }) + ]) + } + } finally { + if (previousUserData === undefined) { + delete process.env.ORCA_USER_DATA_PATH + } else { + process.env.ORCA_USER_DATA_PATH = previousUserData + } + removeTreeSync(userData) + } +} diff --git a/src/main/providers/local-pty-bun-posix-inspection.test.ts b/src/main/providers/local-pty-bun-posix-inspection.test.ts deleted file mode 100644 index c71e4a3fa6c4..000000000000 --- a/src/main/providers/local-pty-bun-posix-inspection.test.ts +++ /dev/null @@ -1,153 +0,0 @@ -import type { IPty } from 'node-pty' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type * as ProcessTableReader from '../../shared/process-table-snapshot-reader' -import type { ProcessTableRow } from '../../shared/process-table-snapshot' -import { - confirmLocalPtyForegroundProcess, - getLocalPtyForegroundProcess, - hasLocalPtyChildProcesses, - inspectLocalPtyChildProcesses -} from './local-pty-foreground-inspection' -import { LocalPtyProvider } from './local-pty-provider' -import { ptyLastRecognizedForeground, ptyProcesses, ptyShellPath } from './local-pty-provider-state' - -const scans = vi.hoisted(() => ({ full: vi.fn(), fresh: vi.fn(), strict: vi.fn() })) -vi.mock('../../shared/process-table-snapshot-reader', async (importOriginal) => ({ - ...(await importOriginal()), - getProcessTableSnapshot: scans.full, - getFreshProcessTableSnapshot: scans.fresh, - getStrictProcessTableSnapshotWithAge: scans.strict -})) -const platform = Object.getOwnPropertyDescriptor(process, 'platform')! -const id = 'bun-pane' -const provider = new LocalPtyProvider() -let rows: ProcessTableRow[] - -function row(pid: number, ppid: number, command: string, foregroundPid: number): ProcessTableRow { - return { - pid, - ppid, - command, - pgid: pid, - tpgid: foregroundPid, - stat: pid === foregroundPid ? 'Ss+' : 'Ss', - tty: 'ttys002' - } -} - -function pane(): IPty & { processNameIsSpawnFile: true } { - return { - pid: 100, - process: '/bin/zsh', - processNameIsSpawnFile: true, - cols: 80, - rows: 24, - handleFlowControl: false, - onData: () => ({ dispose() {} }), - onExit: () => ({ dispose() {} }), - resize() {}, - clear() {}, - write() {}, - kill() {}, - pause() {}, - resume() {} - } -} - -beforeEach(() => { - vi.resetAllMocks() - scans.full.mockImplementation(async () => rows) - scans.fresh.mockImplementation(async () => rows) - scans.strict.mockImplementation(async () => ({ rows, capturedAgeMs: 0 })) - ptyProcesses.set(id, pane()) - ptyShellPath.set(id, '/bin/zsh') -}) -afterEach(() => { - ptyProcesses.clear() - ptyShellPath.clear() - ptyLastRecognizedForeground.clear() - Object.defineProperty(process, 'platform', platform) - vi.restoreAllMocks() -}) - -describe.each(['darwin', 'linux'])('Bun in-process %s inspection', (host) => { - beforeEach(() => Object.defineProperty(process, 'platform', { value: host, configurable: true })) - - it('reports the actual foreground command and warns before closing a busy pane', async () => { - rows = [row(100, 1, '-zsh', 101), row(101, 100, 'vim notes.md', 101)] - expect(await provider.inspectProcess(id)).toEqual({ - foregroundProcess: 'vim', - hasChildProcesses: true, - childProcessEvidence: 'children' - }) - expect(await hasLocalPtyChildProcesses(id)).toBe(true) - expect(await confirmLocalPtyForegroundProcess(id)).toBe('vim') - expect(scans.fresh).toHaveBeenCalledOnce() - }) - - it('proves an idle shell has no children', async () => { - rows = [row(100, 1, '-zsh', 100)] - expect(await provider.inspectProcess(id)).toEqual({ - foregroundProcess: 'zsh', - hasChildProcesses: false, - childProcessEvidence: 'no-children' - }) - expect(await hasLocalPtyChildProcesses(id)).toBe(false) - }) - - it('does not mistake the login wrapper for a running user job', async () => { - ptyProcesses.set(id, { ...pane(), process: '/usr/bin/login' }) - rows = [row(100, 1, '/usr/bin/login -fp test', 101), row(101, 100, '-zsh', 101)] - expect(await provider.inspectProcess(id)).toEqual({ - foregroundProcess: 'zsh', - hasChildProcesses: false, - childProcessEvidence: 'no-children' - }) - }) - - it('preserves a cached agent when the foreground scan cannot verify it', async () => { - rows = [] - scans.full.mockRejectedValue(new Error('process table unavailable')) - scans.strict.mockRejectedValue(new Error('process table unavailable')) - ptyLastRecognizedForeground.set(id, { name: 'claude', pid: 101, at: Date.now() }) - expect(await provider.inspectProcess(id)).toEqual({ - foregroundProcess: 'claude', - hasChildProcesses: true, - childProcessEvidence: 'unverifiable' - }) - expect(await hasLocalPtyChildProcesses(id)).toBe(true) - expect(await confirmLocalPtyForegroundProcess(id)).toBeNull() - }) - - it('returns uncertainty when the process table has no pane root', async () => { - rows = [row(900, 1, '-zsh', 900)] - expect(await getLocalPtyForegroundProcess(id)).toBeNull() - expect(await inspectLocalPtyChildProcesses(id)).toBe('unverifiable') - expect(await hasLocalPtyChildProcesses(id)).toBe(true) - }) - - it('does not resurrect an agent cache after replacement during fingerprint capture', async () => { - rows = [row(100, 1, '-zsh', 101), row(101, 100, 'node /usr/local/bin/claude', 101)] - scans.full - .mockImplementationOnce(async () => rows) - .mockImplementationOnce(async () => { - ptyProcesses.set(id, { ...pane(), pid: 999 }) - return rows - }) - expect(await getLocalPtyForegroundProcess(id)).toBeNull() - expect(ptyLastRecognizedForeground.has(id)).toBe(false) - }) - - it('does not combine an old foreground with a replacement pane during a child scan', async () => { - rows = [row(100, 1, '-zsh', 101), row(101, 100, 'vim notes.md', 101)] - scans.strict.mockImplementationOnce(async () => { - ptyProcesses.set(id, { ...pane(), pid: 999 }) - return { rows, capturedAgeMs: 0 } - }) - expect(await provider.inspectProcess(id)).toEqual({ - foregroundProcess: null, - hasChildProcesses: true, - childProcessEvidence: 'unverifiable' - }) - }) -}) diff --git a/src/main/providers/local-pty-bun-windows-identity.test.ts b/src/main/providers/local-pty-bun-windows-identity.test.ts deleted file mode 100644 index 434445790462..000000000000 --- a/src/main/providers/local-pty-bun-windows-identity.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { ptyProcesses, ptyShellPath } from './local-pty-provider-state' -import { - confirmLocalPtyShellForeground, - inspectLocalPtyChildProcesses -} from './local-pty-foreground-inspection' -import { getLocalPtyCwd, sendLocalPtySignal } from './local-pty-session-operations' - -const { confirm, cwd, membership } = vi.hoisted(() => ({ - confirm: vi.fn(), - cwd: vi.fn(), - membership: vi.fn() -})) -vi.mock('./agent-foreground-process', () => ({ - confirmShellForegroundProcess: confirm, - resolveAgentForegroundProcessWithAvailability: vi.fn() -})) -vi.mock('./process-cwd', () => ({ resolveProcessCwd: cwd })) -vi.mock('./windows-pty-job-membership', () => ({ - readWindowsPtyJobProcessIds: membership, - isWindowsPtyJobReadable: () => true -})) - -const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')! -beforeEach(() => { - Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) - confirm.mockResolvedValue(true) - cwd.mockResolvedValue('C:\\work') -}) -afterEach(() => { - ptyProcesses.clear() - ptyShellPath.clear() - Object.defineProperty(process, 'platform', originalPlatform) - vi.restoreAllMocks() -}) - -describe('Bun in-process Windows shell identity', () => { - it('uses the child shell for cwd and foreground checks while signaling the owned job', async () => { - const proc = { - pid: 1200, - shellProcessId: 1201, - jobRootProcessIsWrapper: true as const, - processNameIsSpawnFile: true as const, - process: 'cmd.exe', - cols: 80, - rows: 24, - handleFlowControl: false, - onData: () => ({ dispose() {} }), - onExit: () => ({ dispose() {} }), - write() {}, - clear() {}, - pause() {}, - resume() {}, - resize() {}, - kill() {}, - signalProcess: vi.fn() - } - ptyProcesses.set('gated-shell', proc) - const shellPath = 'C:\\Windows\\System32\\cmd.exe' - ptyShellPath.set('gated-shell', shellPath) - const kill = vi.spyOn(process, 'kill').mockReturnValue(true) - expect(await getLocalPtyCwd('gated-shell')).toBe('C:\\work') - expect(cwd).toHaveBeenCalledWith(1201) - expect(await confirmLocalPtyShellForeground('gated-shell')).toBe(true) - expect(confirm).toHaveBeenCalledWith(1201, shellPath, expect.any(Object)) - await sendLocalPtySignal('gated-shell', 'SIGTERM') - expect(proc.signalProcess).toHaveBeenCalledWith('SIGTERM') - expect(kill).not.toHaveBeenCalled() - membership.mockReturnValue(new Set([1201])) - expect(await inspectLocalPtyChildProcesses('gated-shell')).toBe('no-children') - membership.mockReturnValue(new Set([1201, 1202])) - expect(await inspectLocalPtyChildProcesses('gated-shell')).toBe('children') - membership.mockReturnValue(null) - expect(await inspectLocalPtyChildProcesses('gated-shell')).toBe('unverifiable') - Reflect.deleteProperty(proc, 'shellProcessId') - cwd.mockClear() - expect(await getLocalPtyCwd('gated-shell')).toBe('') - expect(cwd).not.toHaveBeenCalled() - }) -}) diff --git a/src/main/providers/local-pty-child-process-verdict.test.ts b/src/main/providers/local-pty-child-process-verdict.test.ts deleted file mode 100644 index 857017b66710..000000000000 --- a/src/main/providers/local-pty-child-process-verdict.test.ts +++ /dev/null @@ -1,235 +0,0 @@ -import * as pty from 'node-pty' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' - -const { resolveForegroundMock } = vi.hoisted(() => ({ resolveForegroundMock: vi.fn() })) - -const { confirmShellForegroundMock } = vi.hoisted(() => ({ - confirmShellForegroundMock: vi.fn() -})) - -vi.mock('./agent-foreground-process', () => ({ - resolveAgentForegroundProcessWithAvailability: resolveForegroundMock, - confirmShellForegroundProcess: confirmShellForegroundMock -})) -import { isRetiredPtyMaster } from '../pty/node-pty-master-fd-retirement' -import { - confirmLocalPtyShellForeground, - hasLocalPtyChildProcesses, - inspectLocalPtyChildProcesses -} from './local-pty-foreground-inspection' -import { LocalPtyProvider } from './local-pty-provider' -import { ptyProcesses, ptyShellPath } from './local-pty-provider-state' -import { inspectPtyProviderProcess } from './pty-process-inspection' - -// Bare, so the retired pane's spawn file equals the recorded name (the path's basename). -const POSIX_SHELL = 'sh' - -function registerPane(id: string, foreground: string | (() => string), shell?: string): void { - const pane: pty.IPty = { - pid: 4242, - cols: 80, - rows: 24, - get process(): string { - return typeof foreground === 'function' ? foreground() : foreground - }, - handleFlowControl: false, - onData: () => ({ dispose() {} }), - onExit: () => ({ dispose() {} }), - resize() {}, - clear() {}, - write() {}, - kill() {}, - pause() {}, - resume() {} - } - ptyProcesses.set(id, pane) - if (shell) { - ptyShellPath.set(id, shell) - } -} - -/** - * A real node-pty whose master has been given up. The getter does not throw here -- it answers - * `POSIX_SHELL`, which is exactly the recorded shell name, so only the descriptor distinguishes - * this pane from an idle one. - */ -async function registerRetiredPane(id: string): Promise { - const term = pty.spawn(POSIX_SHELL, ['-c', 'exit 0'], { - name: 'xterm-256color', - cols: 80, - rows: 24, - cwd: process.cwd(), - env: { ...process.env } - }) - await new Promise((resolve) => { - term.onExit(() => resolve()) - }) - // `onExit` runs before node-pty's `_close()`, which is where the patch retires `_fd`. - await vi.waitFor(() => expect(isRetiredPtyMaster(term)).toBe(true), { - timeout: 10000, - interval: 10 - }) - ptyProcesses.set(id, term) - ptyShellPath.set(id, POSIX_SHELL) - return term -} - -beforeEach(() => { - resolveForegroundMock.mockReset() - resolveForegroundMock.mockResolvedValue({ available: true, processName: '/bin/zsh' }) -}) - -afterEach(() => { - ptyProcesses.clear() - ptyShellPath.clear() -}) - -describe('confirmLocalPtyShellForeground', () => { - it('proves against the spawned shell path, which tells the Git Bash launcher apart', async () => { - const launcher = 'C:\\Program Files\\Git\\bin\\bash.exe' - registerPane('pty-git-bash', 'bash.exe', launcher) - confirmShellForegroundMock.mockResolvedValueOnce(true) - - await expect(confirmLocalPtyShellForeground('pty-git-bash')).resolves.toBe(true) - expect(confirmShellForegroundMock).toHaveBeenCalledWith(4242, launcher, expect.any(Object)) - }) -}) - -// Windows has no master fd to retire, and `WindowsTerminal.process` answers from the spawn name. -const describeOnPosix = process.platform === 'win32' ? describe.skip : describe - -describe('inspectLocalPtyChildProcesses', () => { - it('reports unverifiable when the pty fd cannot be read', async () => { - registerPane( - 'pty-closed', - () => { - throw new Error('EBADF: bad file descriptor') - }, - '/bin/zsh' - ) - expect(await inspectLocalPtyChildProcesses('pty-closed')).toBe('unverifiable') - }) - - it('still answers no-children when the shell itself is in the foreground', async () => { - registerPane('pty-idle', 'zsh', '/bin/zsh') - expect(await inspectLocalPtyChildProcesses('pty-idle')).toBe('no-children') - }) - - it('answers children when something else is in the foreground', async () => { - registerPane('pty-busy', 'vim', '/bin/zsh') - expect(await inspectLocalPtyChildProcesses('pty-busy')).toBe('children') - }) - - it('treats a pane this provider does not hold as a real negative', async () => { - expect(await inspectLocalPtyChildProcesses('pty-absent')).toBe('no-children') - }) - - it('preserves uncertainty conservatively in the boolean adapter', async () => { - let reads = 0 - registerPane( - 'pty-closed', - () => { - reads += 1 - throw new Error('EBADF: bad file descriptor') - }, - '/bin/zsh' - ) - await expect(hasLocalPtyChildProcesses('pty-closed')).resolves.toBe(true) - // The result must come from the failed read, not from an earlier short-circuit. - expect(reads).toBe(1) - }) -}) - -describeOnPosix('inspectLocalPtyChildProcesses on a retired master', () => { - it('reports unverifiable rather than reading the spawn file as an idle shell', async () => { - const term = await registerRetiredPane('pty-retired') - - // The mechanism is silent: this is the same string an idle pane reports. - expect(term.process).toBe(POSIX_SHELL) - // Not `no-children`: the close guard reads that as "nothing is running here" and kills the pane. - expect(await inspectLocalPtyChildProcesses('pty-retired')).toBe('unverifiable') - }, 15000) - - it('preserves uncertainty conservatively in the boolean adapter', async () => { - await registerRetiredPane('pty-retired') - - // The adapter exists for `IPtyProvider.hasChildProcesses`, which has no third slot. - await expect(hasLocalPtyChildProcesses('pty-retired')).resolves.toBe(true) - }, 15000) -}) - -describe('inspectPtyProviderProcess child-process evidence', () => { - const provider = new LocalPtyProvider() - - it('carries unverifiable evidence when the child read fails after foreground inspection', async () => { - let reads = 0 - registerPane( - 'pty-closing', - () => { - reads += 1 - if (reads > 1) { - throw new Error('EBADF: bad file descriptor') - } - return '/bin/zsh' - }, - '/bin/zsh' - ) - await expect(inspectPtyProviderProcess(provider, 'pty-closing')).resolves.toEqual({ - foregroundProcess: '/bin/zsh', - hasChildProcesses: true, - childProcessEvidence: 'unverifiable' - }) - }) - - it('samples child evidence after foreground inspection', async () => { - let reads = 0 - registerPane('pty-became-busy', () => (reads++ === 0 ? '/bin/zsh' : 'vim'), '/bin/zsh') - - const inspection = await inspectPtyProviderProcess(provider, 'pty-became-busy') - expect(inspection.hasChildProcesses).toBe(true) - expect(inspection.childProcessEvidence).toBe('children') - }) - - it('carries no-children evidence from the local inspectProcess operation', async () => { - registerPane('pty-idle', 'zsh', '/bin/zsh') - - const inspection = await inspectPtyProviderProcess(provider, 'pty-idle') - expect(inspection.hasChildProcesses).toBe(false) - expect(inspection.childProcessEvidence).toBe('no-children') - }) - - it('carries children evidence from the local inspectProcess operation', async () => { - registerPane('pty-busy', 'vim', '/bin/zsh') - - const inspection = await inspectPtyProviderProcess(provider, 'pty-busy') - expect(inspection.hasChildProcesses).toBe(true) - expect(inspection.childProcessEvidence).toBe('children') - }) - - it('refuses to pair one panes foreground with its replacements children', async () => { - registerPane('pty-swapped', '/bin/zsh', '/bin/zsh') - resolveForegroundMock.mockImplementation(async () => { - // Cleanup plus reactivation lands a different IPty under the same id mid-read. - registerPane('pty-swapped', 'vim', '/bin/zsh') - return { available: true, processName: '/bin/zsh' } - }) - - await expect(inspectPtyProviderProcess(provider, 'pty-swapped')).resolves.toEqual({ - foregroundProcess: null, - hasChildProcesses: true, - childProcessEvidence: 'unverifiable' - }) - }) -}) - -describeOnPosix('inspectPtyProviderProcess on a retired master', () => { - const provider = new LocalPtyProvider() - - it('carries unverifiable child evidence beside the foreground it could still read', async () => { - await registerRetiredPane('pty-retired') - - const inspection = await inspectPtyProviderProcess(provider, 'pty-retired') - expect(inspection.hasChildProcesses).toBe(true) - expect(inspection.childProcessEvidence).toBe('unverifiable') - }, 15000) -}) diff --git a/src/main/providers/local-pty-default-shell.test.ts b/src/main/providers/local-pty-default-shell.test.ts deleted file mode 100644 index 8164749b0292..000000000000 --- a/src/main/providers/local-pty-default-shell.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { afterEach, describe, expect, it, vi } from 'vitest' -import { createLocalPtyLaunchPlan } from './local-pty-launch-plan' - -vi.mock('./local-pty-utils', () => ({ - ensureNodePtySpawnHelperExecutable: vi.fn(), - validateWorkingDirectory: vi.fn() -})) - -afterEach(() => vi.unstubAllEnvs()) - -describe.skipIf(process.platform === 'win32')('default terminal shell', () => { - it.each(['/bin/bash', '/bin/zsh', '/usr/bin/fish', '/usr/bin/nu'])( - 'uses the configured executable %s', - (shell) => { - const plan = createLocalPtyLaunchPlan({ cwd: '/tmp', cols: 80, rows: 24 }, () => ({ - getDefaultShell: () => shell - })) - expect(plan).toMatchObject({ shellPath: shell, shellArgs: ['-l'] }) - } - ) - - it('keeps an explicit per-terminal shell ahead of the default', () => { - const plan = createLocalPtyLaunchPlan( - { cwd: '/tmp', cols: 80, rows: 24, shellOverride: '/bin/bash' }, - () => ({ - getDefaultShell: () => '/usr/bin/fish' - }) - ) - expect(plan).toMatchObject({ shellPath: '/bin/bash' }) - }) - - it('uses the environment shell when no default is configured', () => { - vi.stubEnv('SHELL', '/bin/zsh') - const plan = createLocalPtyLaunchPlan({ cwd: '/tmp', cols: 80, rows: 24 }, () => ({ - getDefaultShell: () => '' - })) - expect(plan).toMatchObject({ shellPath: '/bin/zsh' }) - }) - - it('uses explicit interactive args for the configured default shell', () => { - const plan = createLocalPtyLaunchPlan( - { cwd: '/tmp', cols: 80, rows: 24, terminalShellArgs: ['--rcfile', '/tmp/orca rc'] }, - () => ({ getDefaultShell: () => '/bin/bash' }) - ) - expect(plan).toMatchObject({ shellPath: '/bin/bash', shellArgs: ['--rcfile', '/tmp/orca rc'] }) - }) - - it('allows an explicit empty argument list for wrapper shells', () => { - const plan = createLocalPtyLaunchPlan( - { cwd: '/tmp', cols: 80, rows: 24, terminalShellArgs: [] }, - () => ({ getDefaultShell: () => '/bin/zsh' }) - ) - expect(plan).toMatchObject({ shellArgs: [] }) - }) - - it('applies profile args when the shell path was resolved from the setting', () => { - // Why: the spawn path fills shellOverride from terminalDefaultShell, so a - // populated override is the normal profile launch, not a one-off pick. - const plan = createLocalPtyLaunchPlan( - { cwd: '/tmp', cols: 80, rows: 24, shellOverride: '/bin/bash', terminalShellArgs: [] }, - () => ({ getDefaultShell: () => '/bin/bash' }) - ) - expect(plan).toMatchObject({ shellPath: '/bin/bash', shellArgs: [] }) - }) - - it.each([{ command: 'codex' }, { launchAgent: 'codex' as const }])( - 'keeps controlled login args for non-profile launches (%o)', - (overrides) => { - const plan = createLocalPtyLaunchPlan( - { - cwd: '/tmp', - cols: 80, - rows: 24, - terminalShellArgs: ['--rcfile', '/tmp/orca'], - ...overrides - }, - () => ({ getDefaultShell: () => '/bin/zsh' }) - ) - expect('shellArgs' in plan && plan.shellArgs).toEqual(['-l']) - } - ) -}) diff --git a/src/main/providers/local-pty-finalize-environment.ts b/src/main/providers/local-pty-finalize-environment.ts deleted file mode 100644 index 1b385639e94c..000000000000 --- a/src/main/providers/local-pty-finalize-environment.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { win32 as pathWin32 } from 'node:path' -import { shouldUseShellReadyStartupDelivery } from '../../shared/codex-startup-delivery' -import { expandWindowsPathEnvironmentVariables } from '../../shared/windows-environment-expansion' -import { dropInheritedOrcaFishHistory } from '../fish-history-session' -import { dropIncoherentCondaActivationEnv } from '../pty/conda-activation-env' -import { stripLegacyTerminalShimEnv } from '../pty/legacy-terminal-shim-dir' -import { - POWERLEVEL10K_WIZARD_DISABLE_ENV, - seedPowerlevel10kWizardEnv -} from '../pty/powerlevel10k-wizard-env' -import { - POSIX_SHELL_STARTUP_COMMAND_ENV, - supportsPosixShellStartupCommand -} from '../pty/posix-shell-startup-command' -import { resolvePathEnvKey } from '../pty/windows-environment-path' -import { selectShellStartupFeatures } from '../shell-startup-features' -import { - injectHistoryEnv, - injectWslFishHistoryEnv, - logHistoryInjection, - type HistoryInjectionResult -} from '../terminal-history' -import { addWslEnvKeys } from '../wsl-env' -import { dropInheritedOrcaHistFile } from '../worktree-history-file-path' -import { promoteAgentTeamsShimPath } from './local-pty-launch-helpers' -import type { LocalPtyLaunchPlan } from './local-pty-launch-plan' -import type { LocalPtyProviderOptions } from './local-pty-provider-types' -import { getShellLaunchConfig } from './local-pty-shell-ready' -import { finalizeWindowsLocalPtySpawnEnvironment } from './local-pty-windows-spawn-environment' -import type { PtySpawnOptions } from './types' - -export function finalizeLocalPtySpawnEnvironment(args: { - spawn: PtySpawnOptions - getOptions: () => LocalPtyProviderOptions - plan: LocalPtyLaunchPlan - env: Record -}): HistoryInjectionResult | null { - const { spawn, getOptions, plan, env } = args - if (process.platform === 'win32') { - finalizeWindowsLocalPtySpawnEnvironment({ spawn, plan, env }) - } - seedPowerlevel10kWizardEnv(env, { envToDelete: spawn.envToDelete }) - if ( - env[POWERLEVEL10K_WIZARD_DISABLE_ENV] !== undefined && - process.platform === 'win32' && - pathWin32.basename(plan.shellPath).toLowerCase() === 'wsl.exe' - ) { - addWslEnvKeys(env, [POWERLEVEL10K_WIZARD_DISABLE_ENV]) - } - const requestedEnv = spawn.env - expandWindowsPathEnvironmentVariables(env) - promoteAgentTeamsShimPath( - env, - requestedEnv ? requestedEnv[resolvePathEnvKey(requestedEnv, process.platform)] : undefined - ) - // Why: raw requested PATH promotion runs after the host-env scrub. - stripLegacyTerminalShimEnv(env, process.platform) - // Why after every deletion pass: an envToDelete of CONDA_PREFIX must not leave the sentinel behind. - dropIncoherentCondaActivationEnv(env, process.platform) - - // Why: worktree-scoped HISTFILE — without it worktrees share one global history (terminal-history-scope-design §7–§10). - const worktreeId = spawn.worktreeId - const historyEnabled = worktreeId && (getOptions().isHistoryEnabled?.() ?? true) - // Effective shell for history injection: WSL's outer exe is wsl.exe but the inner login shell is bash. - const isWslTerminal = - Boolean(plan.wslInfo || plan.worktreeWslContext || plan.preferredWslContext) || - pathWin32.basename(plan.shellPath).toLowerCase() === 'wsl.exe' - const effectiveShellPath = isWslTerminal ? 'bash' : plan.shellPath - let historyResult: ReturnType | null = null - if (historyEnabled) { - historyResult = injectHistoryEnv(env, worktreeId, effectiveShellPath, plan.cwd, { - wslDistro: plan.launchWslDistro - }) - if (isWslTerminal && plan.launchWslDistro) { - injectWslFishHistoryEnv(env, worktreeId, plan.launchWslDistro) - addWslEnvKeys(env, ['HISTFILE', 'fish_history']) - } - logHistoryInjection(worktreeId, historyResult) - } else { - // Why: injectHistoryEnv is what normally clears it, so when history is off - // an inherited ORCA_HISTFILE would still reach the wrapper. Credit: #11146. - delete env.ORCA_HISTFILE - // Same for an exported `fish_history` from the fish pane that launched this - // Orca: history off means fish's own default, not another worktree's file. - dropInheritedOrcaFishHistory(env) - // And for an exported HISTFILE: history off means the shell's own default, - // not the history file of the worktree this Orca was launched from. - dropInheritedOrcaHistFile(env) - } - - if (!plan.wslInfo && process.platform !== 'win32') { - // Why after history injection: the wrapper is what repairs a worktree - // HISTFILE that the system zshrc clobbers, so the decision to wrap has to - // see whether this spawn actually injected one. - const isCodexStartupCommand = plan.startupAgentRecognition?.agent === 'codex' - const codexStartupCommand = isCodexStartupCommand ? spawn.command : undefined - const codexRequiresShellReady = - codexStartupCommand !== undefined && - shouldUseShellReadyStartupDelivery({ - command: codexStartupCommand, - startupCommandDelivery: spawn.startupCommandDelivery - }) - // Why delete: ORCA_SHELL_FEATURES is Orca-owned, and only the launch - // config below may name features for this shell. - delete env.ORCA_SHELL_FEATURES - delete env[POSIX_SHELL_STARTUP_COMMAND_ENV] - plan.getFallbackShellReadyConfig = (shell) => { - const wrapperStartupCommand = - codexStartupCommand !== undefined && supportsPosixShellStartupCommand(shell) - ? codexStartupCommand - : undefined - // Why no line-editor widening here (unlike the daemon and relay): a Codex - // startup command this provider wraps is run by the wrapper's own prompt - // hook, never written into the PTY, so there is no early write to double-echo. - const waitsForShellReady = - Boolean(spawn.command) && (!isCodexStartupCommand || codexRequiresShellReady) - return getShellLaunchConfig( - shell, - selectShellStartupFeatures({ - shellPath: shell, - env, - hasStartupCommand: Boolean(spawn.command), - waitsForShellReady, - // Why identical: the identity marker exists so the readiness - // handshake can bind output to the right shell PID. - emitsStartupIdentity: waitsForShellReady - }), - wrapperStartupCommand - ) - } - const shellLaunch = plan.getFallbackShellReadyConfig(plan.shellPath) - Object.assign(env, shellLaunch.env) - plan.shellArgs = shellLaunch.args ?? plan.shellArgs - plan.shellReadyLaunch = spawn.command ? shellLaunch : null - plan.primaryLaunchEnvKeys = Object.keys(shellLaunch.env) - } - return historyResult -} diff --git a/src/main/providers/local-pty-foreground-inspection-cheap-tier.test.ts b/src/main/providers/local-pty-foreground-inspection-cheap-tier.test.ts deleted file mode 100644 index 0d906dbd6610..000000000000 --- a/src/main/providers/local-pty-foreground-inspection-cheap-tier.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type * as ProcessTableSnapshotReader from '../../shared/process-table-snapshot-reader' - -const { cheapSnapshotMock, fullSnapshotMock, resolveMock } = vi.hoisted(() => ({ - cheapSnapshotMock: vi.fn(), - fullSnapshotMock: vi.fn(), - resolveMock: vi.fn() -})) - -vi.mock('../../shared/cheap-process-table-snapshot-reader', () => ({ - getCheapProcessTableSnapshot: cheapSnapshotMock -})) -vi.mock('../../shared/process-table-snapshot-reader', async (importOriginal) => ({ - ...(await importOriginal()), - getProcessTableSnapshot: fullSnapshotMock -})) -vi.mock('./agent-foreground-process', () => ({ - resolveAgentForegroundProcessWithAvailability: resolveMock, - confirmShellForegroundProcess: vi.fn() -})) - -import { getLocalPtyForegroundProcess } from './local-pty-foreground-inspection' -import { ptyLastRecognizedForeground, ptyProcesses, ptyShellPath } from './local-pty-provider-state' - -const SHELL_PID = 4242 -const AGENT_PID = 4300 -const ID = 'pty-1' - -type Table = 'agent' | 'shell-only' -let table: Table = 'agent' - -function rows(): Record[] { - const tpgid = table === 'agent' ? AGENT_PID : SHELL_PID - const out: Record[] = [ - { - pid: SHELL_PID, - ppid: 1, - pgid: SHELL_PID, - tpgid, - stat: table === 'agent' ? 'Ss' : 'Ss+', - tty: 'ttys004', - startTime: 'Thu Sep 3 16:02:01 2026', - command: '-zsh' - } - ] - if (table === 'agent') { - out.push({ - pid: AGENT_PID, - ppid: SHELL_PID, - pgid: AGENT_PID, - tpgid, - stat: 'S+', - tty: 'ttys004', - startTime: 'Thu Sep 3 16:02:05 2026', - command: 'node /usr/local/bin/claude' - }) - } - return out -} - -describe('local POSIX provider cheap-tier revalidation', () => { - let platform: PropertyDescriptor | undefined - const proc = { pid: SHELL_PID, process: 'node' } - - beforeEach(() => { - platform = Object.getOwnPropertyDescriptor(process, 'platform') - Object.defineProperty(process, 'platform', { configurable: true, value: 'darwin' }) - table = 'agent' - proc.process = 'node' - cheapSnapshotMock.mockReset() - cheapSnapshotMock.mockImplementation(async () => rows()) - fullSnapshotMock.mockReset() - fullSnapshotMock.mockImplementation(async () => rows()) - resolveMock.mockReset() - resolveMock.mockImplementation(async () => ({ - available: true, - processName: table === 'agent' ? 'claude' : 'zsh' - })) - ptyProcesses.set(ID, proc as never) - ptyShellPath.set(ID, '/bin/zsh') - ptyLastRecognizedForeground.delete(ID) - }) - - afterEach(() => { - ptyProcesses.delete(ID) - ptyShellPath.delete(ID) - ptyLastRecognizedForeground.delete(ID) - if (platform) { - Object.defineProperty(process, 'platform', platform) - } - }) - - it('a pane with NO recognized anchor never consults the cheap tier', async () => { - table = 'shell-only' - proc.process = 'zsh' - for (let i = 0; i < 3; i += 1) { - expect(await getLocalPtyForegroundProcess(ID)).toBe('zsh') - } - expect(cheapSnapshotMock).not.toHaveBeenCalled() - expect(resolveMock).toHaveBeenCalledTimes(3) - expect(ptyLastRecognizedForeground.get(ID)).toBeUndefined() - }) - - it('once recognized, an unchanged pane re-proves the agent from the cheap tier without a full scan', async () => { - expect(await getLocalPtyForegroundProcess(ID)).toBe('claude') - expect(resolveMock).toHaveBeenCalledTimes(1) - expect(ptyLastRecognizedForeground.get(ID)?.steady?.fingerprint).toEqual(expect.any(String)) - for (let i = 0; i < 3; i += 1) { - expect(await getLocalPtyForegroundProcess(ID)).toBe('claude') - } - expect(cheapSnapshotMock).toHaveBeenCalledTimes(3) - expect(resolveMock).toHaveBeenCalledTimes(1) - }) - - it('an agent exit changes the fingerprint, escalates to the full scan, and clears the anchor', async () => { - expect(await getLocalPtyForegroundProcess(ID)).toBe('claude') - table = 'shell-only' - expect(await getLocalPtyForegroundProcess(ID)).toBe('zsh') - expect(resolveMock).toHaveBeenCalledTimes(2) - expect(ptyLastRecognizedForeground.get(ID)).toBeUndefined() - }) - - it('a changed node-pty foreground name escalates without consulting the cheap tier', async () => { - expect(await getLocalPtyForegroundProcess(ID)).toBe('claude') - proc.process = 'zsh' - table = 'shell-only' - expect(await getLocalPtyForegroundProcess(ID)).toBe('zsh') - expect(cheapSnapshotMock).not.toHaveBeenCalled() - expect(resolveMock).toHaveBeenCalledTimes(2) - }) - - it('a cheap capture failure falls through to the full scan', async () => { - expect(await getLocalPtyForegroundProcess(ID)).toBe('claude') - cheapSnapshotMock.mockRejectedValueOnce(new Error('ps died')) - expect(await getLocalPtyForegroundProcess(ID)).toBe('claude') - expect(resolveMock).toHaveBeenCalledTimes(2) - }) -}) diff --git a/src/main/providers/local-pty-foreground-inspection.ts b/src/main/providers/local-pty-foreground-inspection.ts deleted file mode 100644 index 8c3b0a6dccef..000000000000 --- a/src/main/providers/local-pty-foreground-inspection.ts +++ /dev/null @@ -1,300 +0,0 @@ -import type { PtyChildProcessVerdict } from '../../shared/terminal-process-inspection' -import { recognizeAgentProcessFromCommandLine } from '../../shared/agent-process-recognition' -import { getCheapProcessTableSnapshot } from '../../shared/cheap-process-table-snapshot-reader' -import { - getProcessTableSnapshot, - getStrictProcessTableSnapshotWithAge -} from '../../shared/process-table-snapshot-reader' -import { confirmShellForegroundProcess } from './agent-foreground-process' -import { - createPtyForegroundResolver, - ptyProcessNameIsSpawnFile -} from '../daemon/pty-subprocess/spawn-file-foreground-process' -import { - inspectSpawnFileChildProcessesFromRows, - inspectSpawnFileWindowsChildProcesses -} from '../daemon/pty-subprocess/spawn-file-child-processes' -import { buildPaneProcessFingerprint } from './posix-pane-foreground-fingerprint' -import { isRetiredPtyMaster } from '../pty/node-pty-master-fd-retirement' -import { ptyShellProcessId } from '../windows/windows-pty-job' -import { resolveForegroundFallbackProcess } from './local-pty-launch-helpers' -import { - ptyAgentForegroundContextPaths, - ptyLastRecognizedForeground, - ptyProcesses, - getPtyShellName, - ptyShellPath -} from './local-pty-provider-state' -import { resolveStableForegroundProcess } from './stable-foreground-process' -import { - canRevalidateCachedAgentWithoutScan, - judgeCachedAgentJobEvidence -} from './windows-cached-agent-revalidation' -import { readWindowsConsoleAttachedProcessIds } from './windows-console-attached-processes' -import { isWindowsPtyJobReadable, readWindowsPtyJobProcessIds } from './windows-pty-job-membership' - -/** - * A retired master does not fail loudly: the `process` getter answers with the spawn file, which - * equals the recorded shell and would otherwise read as a real "nothing is running here". Ask the - * descriptor before the name, because an unreadable PTY is not evidence that its children exited. - */ -export async function inspectLocalPtyChildProcesses(id: string): Promise { - const proc = ptyProcesses.get(id) - if (!proc) { - return 'no-children' - } - if (isRetiredPtyMaster(proc)) { - return 'unverifiable' - } - try { - if (ptyProcessNameIsSpawnFile(proc)) { - if (process.platform === 'win32') { - return inspectSpawnFileWindowsChildProcesses(proc) - } - const snapshot = await getStrictProcessTableSnapshotWithAge() - return ptyProcesses.get(id) === proc - ? inspectSpawnFileChildProcessesFromRows( - snapshot.rows, - proc.pid, - getPtyShellName(id) ?? null - ) - : 'unverifiable' - } - const foreground = proc.process - const shell = getPtyShellName(id) - if (!shell) { - return 'children' - } - return foreground === shell ? 'no-children' : 'children' - } catch { - // An unreadable PTY is not evidence that its children exited. - return 'unverifiable' - } -} - -export async function hasLocalPtyChildProcesses(id: string): Promise { - return (await inspectLocalPtyChildProcesses(id)) !== 'no-children' -} - -/** - * POSIX twin of the Windows job-membership short-circuit below: a pane that already holds a - * recognized agent re-proves it from the cheap `ps` tier when the subtree fingerprint is - * unchanged. Panes with no anchor never get here, so start discovery is untouched. - */ -async function revalidateCachedPosixAgent( - proc: { pid: number }, - cachedEntry: { - name: string - steady?: { fingerprint: string; fallbackProcess: string | null } | null - }, - fallbackProcess: string | null -): Promise { - const steady = cachedEntry.steady - if (!steady || steady.fallbackProcess !== fallbackProcess) { - return false - } - try { - const observed = await buildPaneProcessFingerprint( - await getCheapProcessTableSnapshot(), - proc.pid - ) - return observed !== null && observed === steady.fingerprint - } catch { - return false - } -} - -export async function getLocalPtyForegroundProcess(id: string): Promise { - const proc = ptyProcesses.get(id) - if (!proc) { - ptyLastRecognizedForeground.delete(id) - return null - } - const fallbackProcess = resolveForegroundFallbackProcess( - ptyProcessNameIsSpawnFile(proc) ? (getPtyShellName(id) ?? null) : proc.process || null, - getPtyShellName(id) - ) - const cachedEntry = ptyLastRecognizedForeground.get(id) - const cachedAgent = cachedEntry?.name ?? null - let paneMembershipUnavailable = false - let cachedAgentAliveInJob = false - // Why: job membership preserves a live cached agent without the whole-table - // scan (incomplete under Windows load). Job, not console: this asks "is - // anything besides the shell alive?", which needs no console attachment and - // so needs no forked helper (#10857). - if ( - process.platform === 'win32' && - canRevalidateCachedAgentWithoutScan(cachedAgent, fallbackProcess) - ) { - try { - const paneProcessIds = readWindowsPtyJobProcessIds(proc) - if (ptyProcesses.get(id) !== proc) { - return null - } - const verdict = judgeCachedAgentJobEvidence({ - jobProcessIds: paneProcessIds, - jobSupported: isWindowsPtyJobReadable(), - shellPid: ptyShellProcessId(proc) ?? proc.pid, - anchorProcessId: cachedEntry?.pid ?? null, - identityAgeMs: Date.now() - (cachedEntry?.at ?? 0) - }) - if (verdict === 'confirmed' || verdict === 'unproven') { - return cachedAgent - } - if (verdict === 'exited') { - // The shell stands alone in a complete, inescapable job list: no - // successor is possible, so the identity retires before the scan. - ptyLastRecognizedForeground.delete(id) - } else if (verdict === 'anchor-exited' && cachedEntry) { - // The recognized process died but another member remains -- a - // leftover, or a restarted successor. Keep the name as unanchored, - // age-bounded evidence and let this cycle's scan decide: deleting - // here made a degraded scan read a mid-restart agent as an exit. - ptyLastRecognizedForeground.set(id, { ...cachedEntry, pid: null }) - } - cachedAgentAliveInJob = verdict === 'recheck' - paneMembershipUnavailable = verdict === 'unavailable' - } catch { - paneMembershipUnavailable = true - } - } - if ( - process.platform !== 'win32' && - cachedEntry && - cachedAgent !== null && - (await revalidateCachedPosixAgent(proc, cachedEntry, fallbackProcess)) - ) { - if (ptyProcesses.get(id) !== proc) { - return null - } - ptyLastRecognizedForeground.set(id, { ...cachedEntry, at: Date.now() }) - return cachedAgent - } - try { - const resolution = await createPtyForegroundResolver(proc)(proc.pid, fallbackProcess, { - contextPaths: ptyAgentForegroundContextPaths.get(id), - ...(cachedEntry?.pid != null - ? { anchorProcessId: cachedEntry.pid, anchorProcessName: cachedEntry.name } - : {}) - }) - // Why: the scan can outlive PTY teardown/id reuse; stale results must not resurrect cache for a foreign id. - if (ptyProcesses.get(id) !== proc) { - return null - } - // Why: a degraded scan reporting shell-as-foreground fires a false "agent done"; keep last recognized agent instead. - const lastRecognizedAgent = ptyLastRecognizedForeground.get(id)?.name ?? null - const resolvedAgent = resolution.processName - ? recognizeAgentProcessFromCommandLine(resolution.processName) - : null - // A recycled anchor pid keeps job membership truthful but the identity - // dead; the scan proving the pid now runs a non-agent settles it. - const anchorContradicted = resolution.anchorPidForeign === true - // Why: incomplete snapshot + unavailable job read isn't exit proof; and an - // anchor pid still alive in the job outranks a snapshot that lost its row. - const stableResolution = - (paneMembershipUnavailable || cachedAgentAliveInJob) && - !anchorContradicted && - resolvedAgent === null - ? { ...resolution, available: false } - : resolution - const stable = resolveStableForegroundProcess(stableResolution, lastRecognizedAgent) - if (stable.lastRecognizedAgent && stableResolution.available) { - const steady = await readPosixSteadyState(proc.pid, fallbackProcess) - if (ptyProcesses.get(id) !== proc) { - return null - } - // Only a positive recognition restarts the age bound. - ptyLastRecognizedForeground.set(id, { - name: stable.lastRecognizedAgent, - pid: - stable.lastRecognizedAgent === resolution.processName - ? (resolution.processId ?? null) - : null, - at: Date.now(), - steady - }) - } else if (stable.lastRecognizedAgent && cachedAgentAliveInJob && !anchorContradicted) { - // The anchor pid in the job is proof of life; restamp so the - // short-circuit resumes instead of scanning on every call. - const entry = ptyLastRecognizedForeground.get(id) - if (entry) { - ptyLastRecognizedForeground.set(id, { ...entry, at: Date.now() }) - } - } else if (!stable.lastRecognizedAgent) { - ptyLastRecognizedForeground.delete(id) - } - return stable.processName - } catch { - if (ptyProcesses.get(id) !== proc) { - return null - } - // Why: an inspection error is a degraded read; fall back to last recognized agent (null reads as an exit). - return ptyLastRecognizedForeground.get(id)?.name ?? null - } -} - -/** The fingerprint of the TTL-shared capture the recognition just read; null on Windows or - * when the pane is unfenced, which simply means the next read pays for the full scan. */ -async function readPosixSteadyState( - shellPid: number, - fallbackProcess: string | null -): Promise<{ fingerprint: string; fallbackProcess: string | null } | null> { - if (process.platform === 'win32') { - return null - } - try { - const fingerprint = await buildPaneProcessFingerprint(await getProcessTableSnapshot(), shellPid) - return fingerprint === null ? null : { fingerprint, fallbackProcess } - } catch { - return null - } -} - -export async function confirmLocalPtyForegroundProcess(id: string): Promise { - const proc = ptyProcesses.get(id) - if (!proc) { - return null - } - try { - const resolution = await createPtyForegroundResolver(proc)( - proc.pid, - resolveForegroundFallbackProcess( - ptyProcessNameIsSpawnFile(proc) ? (getPtyShellName(id) ?? null) : proc.process || null, - getPtyShellName(id) - ), - { - contextPaths: ptyAgentForegroundContextPaths.get(id), - fresh: true, - ...(process.platform === 'win32' - ? { - forceProcessScan: true, - readWindowsConsoleAttachedProcessIds: () => - readWindowsConsoleAttachedProcessIds(proc.pid) - } - : {}) - } - ) - // Why: a fresh scan can outlive this PTY id; never publish identity from an exited or same-id-reusing session. - if (ptyProcesses.get(id) !== proc) { - return null - } - return resolution.available ? resolution.processName : null - } catch { - return null - } -} - -export async function confirmLocalPtyShellForeground(id: string): Promise { - const proc = ptyProcesses.get(id) - if (!proc) { - return false - } - const confirmed = await confirmShellForegroundProcess( - ptyShellProcessId(proc), - ptyShellPath.get(id), - process.platform === 'win32' - ? { readWindowsPtyJobProcessIds: () => readWindowsPtyJobProcessIds(proc) } - : {} - ) - return ptyProcesses.get(id) === proc && confirmed -} diff --git a/src/main/providers/local-pty-launch-helpers.ts b/src/main/providers/local-pty-launch-helpers.ts deleted file mode 100644 index 220669d7cb7a..000000000000 --- a/src/main/providers/local-pty-launch-helpers.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { basename, delimiter, win32 as pathWin32 } from 'node:path' -import { splitWorktreeIdForFilesystem } from '../../shared/worktree/id' -import { parseWslPath } from '../wsl' -import { resolvePathEnvKey } from '../pty/windows-environment-path' -import { expandWindowsEnvironmentVariables } from '../../shared/windows-environment-expansion' -import { resolveSafePtyDefaultCwd } from './pty-default-cwd' - -const PANE_IDENTITY_ENV_KEYS = [ - 'ORCA_PANE_KEY', - 'ORCA_TAB_ID', - 'ORCA_WORKTREE_ID', - 'ORCA_AGENT_LAUNCH_TOKEN' -] as const - -export function getDefaultCwd(): string { - return resolveSafePtyDefaultCwd() -} - -export function removeUnspecifiedPaneIdentityEnv( - env: Record, - explicitEnv: Record | undefined -): void { - for (const key of PANE_IDENTITY_ENV_KEYS) { - if (!explicitEnv || !Object.hasOwn(explicitEnv, key)) { - delete env[key] - } - } -} - -export function promoteAgentTeamsShimPath( - env: Record, - requestedPath: string | undefined -): void { - if (!env.ORCA_AGENT_TEAMS_TEAM_ID || !requestedPath) { - return - } - const normalizedRequestedPath = - process.platform === 'win32' - ? expandWindowsEnvironmentVariables(requestedPath, env) - : requestedPath - const pathDelimiter = process.platform === 'win32' ? ';' : delimiter - const shimDir = normalizedRequestedPath.split(pathDelimiter)[0] - if (!shimDir) { - return - } - const pathKey = resolvePathEnvKey(env, process.platform) - const currentParts = env[pathKey]?.split(pathDelimiter).filter(Boolean) ?? [] - env[pathKey] = [shimDir, ...currentParts.filter((part) => part !== shimDir)].join(pathDelimiter) -} - -export function getWslContextFromWorktreeId( - worktreeId: string | undefined -): { distro: string; treatPosixCwdAsWsl: true } | undefined { - // Why: strip any synthetic `::workspace:` suffix so WSL detection parses the real path, not a nonexistent identifier. - const worktreePath = worktreeId - ? splitWorktreeIdForFilesystem(worktreeId)?.worktreePath - : undefined - const wslInfo = worktreePath ? parseWslPath(worktreePath) : null - return wslInfo ? { distro: wslInfo.distro, treatPosixCwdAsWsl: true } : undefined -} - -export function getWslContextFromPreferredDistro( - distro: string | null | undefined -): { distro: string; treatPosixCwdAsWsl: true } | undefined { - const trimmed = distro?.trim() - return trimmed ? { distro: trimmed, treatPosixCwdAsWsl: true } : undefined -} - -export function normalizeLocalCallerSessionId( - sessionId: string | undefined, - allowNumeric = false -): string | null { - const requested = sessionId?.trim() - return !requested || (!allowNumeric && /^\d+$/.test(requested)) ? null : requested -} - -export function normalizeForegroundProcessName( - processName: string | null | undefined -): string | null { - const trimmed = processName?.trim().replace(/^["']|["']$/g, '') ?? '' - return !trimmed || trimmed === 'xterm-256color' ? null : trimmed.split(/[\\/]/).pop() || null -} - -export function resolveForegroundFallbackProcess( - processName: string | null | undefined, - shellName: string | undefined -): string | null { - if (process.platform !== 'win32' || normalizeForegroundProcessName(processName)) { - return processName || null - } - return shellName ?? processName ?? null -} - -/** Basename of the spawned shell path, parsed for the *target* platform. - * Why: POSIX `basename` won't split a Windows `\` path (non-Windows host/CI), so it'd break the foreground comparison. */ -export function getSpawnedShellName(shellPath: string): string { - return process.platform === 'win32' ? pathWin32.basename(shellPath) : basename(shellPath) -} diff --git a/src/main/providers/local-pty-launch-plan.ts b/src/main/providers/local-pty-launch-plan.ts deleted file mode 100644 index 17351325300d..000000000000 --- a/src/main/providers/local-pty-launch-plan.ts +++ /dev/null @@ -1,260 +0,0 @@ -import { win32 as pathWin32 } from 'node:path' -import { canUseBunPty } from '../daemon/pty-subprocess/bun-pty-process-capabilities' -import { recognizeAgentProcessFromCommandLine } from '../../shared/agent-process-recognition' -import { WINDOWS_GIT_BASH_SHELL } from '../../shared/windows-terminal-shell' -import { resolveWindowsGitBashShellPath } from '../git-bash' -import { getDefaultWslDistro, parseWslPath } from '../wsl' -import { - getDefaultCwd, - getWslContextFromPreferredDistro, - getWslContextFromWorktreeId -} from './local-pty-launch-helpers' -import type { LocalPtyProviderOptions } from './local-pty-provider-types' -import type { getShellLaunchConfig } from './local-pty-shell-ready' -import { ensureNodePtySpawnHelperExecutable, validateWorkingDirectory } from './local-pty-utils' -import { - resolveEffectiveWindowsPowerShell, - shouldProbeWindowsPowerShellAvailability, - type WindowsPowerShellShellFamily -} from './windows-powershell' -import { buildWindowsPowerShellSpawnAttempts } from './windows-shell-fallback-chain' -import { resolveWindowsShellLaunchArgs } from './windows-shell-args' -import { assertSafeAgentStartupCwd } from './pty-default-cwd' -import type { PtySpawnOptions } from './types' - -type WslLaunchContext = { distro: string; treatPosixCwdAsWsl: true } -type LocalPtyLaunchSeed = { - args: PtySpawnOptions - startupAgentRecognition: ReturnType - defaultCwd: string - cwd: string - wslInfo: ReturnType - worktreeWslContext: WslLaunchContext | undefined - preferredWslContext: WslLaunchContext | undefined - launchWslContext: WslLaunchContext | undefined -} - -export type LocalPtyLaunchPlan = { - startupAgentRecognition: ReturnType - defaultCwd: string - cwd: string - wslInfo: ReturnType - worktreeWslContext: WslLaunchContext | undefined - preferredWslContext: WslLaunchContext | undefined - launchWslContext: WslLaunchContext | undefined - shellPath: string - shellArgs: string[] - effectiveCwd: string - validationCwd: string - startupCommandDeliveredInShellArgs: boolean - windowsFallbackAttempts: ReturnType - shellReadyLaunch: ReturnType | null - getFallbackShellReadyConfig: - | ((shell: string) => ReturnType) - | undefined - // Why hoisted: a fallback shell must drop the primary's launch env, and - // re-deriving the key names would re-run wrapper generation. - primaryLaunchEnvKeys: string[] - isWslShell: boolean - launchWslDistro: string | null -} - -export class DeferredLocalPtyLaunchPlan { - constructor( - readonly availability: boolean | Promise, - readonly finish: (available: boolean) => LocalPtyLaunchPlan - ) {} -} - -function finalizeLocalPtyLaunchPlan( - seed: LocalPtyLaunchSeed, - shell: { - shellPath: string - shellArgs: string[] - effectiveCwd: string - validationCwd: string - startupCommandDeliveredInShellArgs?: boolean - windowsFallbackAttempts?: ReturnType - } -): LocalPtyLaunchPlan { - if (!canUseBunPty()) { - ensureNodePtySpawnHelperExecutable() - } - if (seed.args.prevalidatedCwd !== shell.validationCwd) { - validateWorkingDirectory(shell.validationCwd) - } - const isWslShell = - Boolean(seed.wslInfo) || pathWin32.basename(shell.shellPath).toLowerCase() === 'wsl.exe' - return { - startupAgentRecognition: seed.startupAgentRecognition, - defaultCwd: seed.defaultCwd, - cwd: seed.cwd, - wslInfo: seed.wslInfo, - worktreeWslContext: seed.worktreeWslContext, - preferredWslContext: seed.preferredWslContext, - launchWslContext: seed.launchWslContext, - shellPath: shell.shellPath, - shellArgs: shell.shellArgs, - effectiveCwd: shell.effectiveCwd, - validationCwd: shell.validationCwd, - startupCommandDeliveredInShellArgs: shell.startupCommandDeliveredInShellArgs ?? false, - windowsFallbackAttempts: shell.windowsFallbackAttempts ?? [], - shellReadyLaunch: null, - getFallbackShellReadyConfig: undefined, - primaryLaunchEnvKeys: [], - isWslShell, - launchWslDistro: isWslShell ? (seed.launchWslContext?.distro ?? null) : null - } -} - -function createWindowsLocalPtyLaunchPlan( - seed: LocalPtyLaunchSeed, - getOptions: () => LocalPtyProviderOptions -): LocalPtyLaunchPlan | DeferredLocalPtyLaunchPlan { - const { args, cwd, defaultCwd, worktreeWslContext } = seed - // Why: shellOverride opens one tab in a non-default shell without changing the user's setting; it wins over the setting. - const requestedShellFamily = - args.shellOverride || - getOptions().getWindowsShell?.() || - process.env.COMSPEC || - 'powershell.exe' - const shellFamily = worktreeWslContext ? 'wsl.exe' : requestedShellFamily - if (!seed.launchWslContext && pathWin32.basename(shellFamily).toLowerCase() === 'wsl.exe') { - seed.launchWslContext = getWslContextFromPreferredDistro(getDefaultWslDistro()) - } - const normalizedShellFamily = pathWin32.basename(shellFamily).toLowerCase() - const resolvedGitBashPath = resolveWindowsGitBashShellPath(shellFamily) - // Why: normalize setting-value and path forms to the PowerShell family so the resolver can fall back to inbox powershell.exe. - const powerShellImplementation = getOptions().getWindowsPowerShellImplementation?.() - const resolvedShellFamily: WindowsPowerShellShellFamily = - normalizedShellFamily === 'powershell.exe' || normalizedShellFamily === 'pwsh.exe' - ? normalizedShellFamily - : normalizedShellFamily === 'cmd.exe' || normalizedShellFamily === 'wsl.exe' - ? normalizedShellFamily - : undefined - const shouldProbePwsh = shouldProbeWindowsPowerShellAvailability({ - shellFamily: resolvedShellFamily, - implementation: powerShellImplementation - }) - const shouldResolvePowerShellFamily = - powerShellImplementation !== undefined || pathWin32.basename(shellFamily) === shellFamily - const finish = (pwshAvailable: boolean): LocalPtyLaunchPlan => { - let shellPath: string - if (resolvedGitBashPath) { - shellPath = resolvedGitBashPath - } else if (shellFamily === WINDOWS_GIT_BASH_SHELL) { - shellPath = 'powershell.exe' - } else { - shellPath = shouldResolvePowerShellFamily - ? (resolveEffectiveWindowsPowerShell({ - shellFamily: resolvedShellFamily, - implementation: powerShellImplementation, - pwshAvailable - }) ?? shellFamily) - : shellFamily - } - // Why: bare `pwsh.exe` resolves to the Store App Execution Alias stub whose spawn fails (code 5); use an absolute exe + cmd.exe fallback. - const windowsFallbackAttempts = buildWindowsPowerShellSpawnAttempts({ - shellPath, - cwd, - defaultCwd, - wslContext: seed.launchWslContext, - startupCommand: args.command - }) - const primaryAttempt = windowsFallbackAttempts[0] - if (primaryAttempt) { - return finalizeLocalPtyLaunchPlan(seed, { - shellPath: primaryAttempt.shellPath, - shellArgs: primaryAttempt.shellArgs, - effectiveCwd: primaryAttempt.effectiveCwd, - validationCwd: primaryAttempt.validationCwd, - startupCommandDeliveredInShellArgs: primaryAttempt.startupCommandDeliveredInShellArgs, - windowsFallbackAttempts - }) - } - const resolved = resolveWindowsShellLaunchArgs( - shellPath, - cwd, - defaultCwd, - seed.launchWslContext, - args.command - ) - return finalizeLocalPtyLaunchPlan(seed, { - shellPath, - shellArgs: resolved.shellArgs, - effectiveCwd: resolved.effectiveCwd, - validationCwd: resolved.validationCwd, - startupCommandDeliveredInShellArgs: resolved.startupCommandDeliveredInShellArgs === true, - windowsFallbackAttempts - }) - } - return shouldProbePwsh - ? new DeferredLocalPtyLaunchPlan(getOptions().pwshAvailable?.() ?? false, finish) - : finish(false) -} - -export function createLocalPtyLaunchPlan( - args: PtySpawnOptions, - getOptions: () => LocalPtyProviderOptions -): LocalPtyLaunchPlan | DeferredLocalPtyLaunchPlan { - const startupAgentRecognition = args.command - ? recognizeAgentProcessFromCommandLine(args.command) - : null - - const defaultCwd = getDefaultCwd() - const cwd = args.cwd || defaultCwd - // Why: gate on the effective cwd, not raw args.cwd — an omitted cwd becomes a safe default and must not be rejected as root-like. - if (args.command && startupAgentRecognition) { - assertSafeAgentStartupCwd(cwd, args.command) - } - const wslInfo = process.platform === 'win32' ? parseWslPath(cwd) : null - const worktreeWslContext = - process.platform === 'win32' ? getWslContextFromWorktreeId(args.worktreeId) : undefined - const preferredWslContext = - process.platform === 'win32' - ? getWslContextFromPreferredDistro(args.terminalWindowsWslDistro) - : undefined - const seed: LocalPtyLaunchSeed = { - args, - startupAgentRecognition, - defaultCwd, - cwd, - wslInfo, - worktreeWslContext, - preferredWslContext, - launchWslContext: - wslInfo !== null - ? getWslContextFromPreferredDistro(wslInfo.distro) - : (worktreeWslContext ?? preferredWslContext) - } - if (wslInfo) { - const shellPath = 'wsl.exe' - const resolved = resolveWindowsShellLaunchArgs(shellPath, cwd, defaultCwd) - return finalizeLocalPtyLaunchPlan(seed, { - shellPath, - shellArgs: resolved.shellArgs, - effectiveCwd: resolved.effectiveCwd, - validationCwd: resolved.validationCwd - }) - } - if (process.platform === 'win32') { - return createWindowsLocalPtyLaunchPlan(seed, getOptions) - } - const shellPath = - args.shellOverride || - getOptions().getDefaultShell?.()?.trim() || - args.env?.SHELL || - process.env.SHELL || - '/bin/zsh' - return finalizeLocalPtyLaunchPlan(seed, { - shellPath, - // Why: shellOverride here is already resolved from the setting, so it cannot - // distinguish a one-off shell pick; the spawn path only sends args for the profile. - shellArgs: - !args.command && !args.launchAgent && args.terminalShellArgs !== undefined - ? args.terminalShellArgs - : ['-l'], - effectiveCwd: cwd, - validationCwd: cwd - }) -} diff --git a/src/main/providers/local-pty-pending-native-spawn.test.ts b/src/main/providers/local-pty-pending-native-spawn.test.ts deleted file mode 100644 index 7f292e41273a..000000000000 --- a/src/main/providers/local-pty-pending-native-spawn.test.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { spawnLocalPty } from './local-pty-spawn' -import { cancelPendingLocalPtySpawns } from './local-pty-spawn-state' -import { pendingLocalPtySpawns, ptyProcesses } from './local-pty-provider-state' - -const { spawn, activate, destroy } = vi.hoisted(() => ({ - spawn: vi.fn(), - activate: vi.fn(), - destroy: vi.fn() -})) -vi.mock('./local-pty-runtime-spawn', () => ({ loadLocalPtyRuntimeSpawn: async () => spawn })) -vi.mock('./macos-tcc-login-shell', () => ({ prepareMacosTccLoginShell: async () => {} })) -vi.mock('./local-pty-finalize-environment', () => ({ - finalizeLocalPtySpawnEnvironment: () => null -})) -vi.mock('./local-pty-spawn-environment', () => ({ - buildLocalPtySpawnEnvironment: () => ({}), - enforceLocalPtySpawnEnvironmentOverrides() {} -})) -vi.mock('./local-pty-launch-plan', () => ({ - DeferredLocalPtyLaunchPlan: class {}, - createLocalPtyLaunchPlan: () => ({ - shellPath: '/bin/sh', - shellArgs: [], - effectiveCwd: '/tmp', - cwd: '/tmp', - windowsFallbackAttempts: [] - }) -})) -vi.mock('./local-pty-session-activation', () => ({ activateLocalPtySession: activate })) -vi.mock('./local-pty-termination', () => ({ destroyPtyProcess: destroy })) - -function createProcess() { - return { - pid: 12345, - process: '/bin/sh', - cols: 80, - rows: 24, - handleFlowControl: false, - onData: () => ({ dispose() {} }), - onExit: () => ({ dispose() {} }), - write() {}, - clear() {}, - pause() {}, - resume() {}, - resize: vi.fn(), - kill: vi.fn() - } -} - -function start(id = 'pending-bun-shell') { - return spawnLocalPty({ sessionId: id, cols: 80, rows: 24 }, () => ({})) -} - -beforeEach(() => { - vi.clearAllMocks() - activate.mockImplementation(({ id, proc }) => { - ptyProcesses.set(id, proc) - return { id, pid: proc.pid } - }) -}) -afterEach(() => { - ptyProcesses.clear() - expect(pendingLocalPtySpawns.size).toBe(0) -}) - -describe('local PTY native spawn admission', () => { - it('aborts a pending receipt when the requesting client disconnects', async () => { - const controller = new AbortController() - spawn.mockImplementationOnce( - ({ signal }: { signal: AbortSignal }) => - new Promise((_resolve, reject) => { - signal.addEventListener('abort', () => reject(signal.reason), { once: true }) - }) - ) - const result = spawnLocalPty( - { sessionId: 'disconnected-shell', cols: 80, rows: 24, signal: controller.signal }, - () => ({}) - ) - const rejected = expect(result).rejects.toThrow('client disconnected') - await vi.waitFor(() => expect(spawn).toHaveBeenCalledOnce()) - controller.abort(new Error('client disconnected')) - await rejected - expect(activate).not.toHaveBeenCalled() - }) - - it('reserves the same session until a delayed shell receipt is activated', async () => { - const proc = createProcess() - let release!: () => void - spawn.mockImplementationOnce( - () => - new Promise((resolve) => { - release = () => resolve({ process: proc, shellPath: '/bin/sh' }) - }) - ) - const first = start() - await vi.waitFor(() => expect(spawn).toHaveBeenCalledOnce()) - const second = start() - await new Promise((resolve) => setImmediate(resolve)) - expect(spawn).toHaveBeenCalledOnce() - release() - expect(await first).toEqual({ id: 'pending-bun-shell', pid: proc.pid }) - expect(await second).toMatchObject({ id: 'pending-bun-shell', pid: proc.pid, isReattach: true }) - expect(activate).toHaveBeenCalledOnce() - expect(spawn).toHaveBeenCalledOnce() - }) - - it('aborts a pending receipt on shutdown and cancels queued same-session launches', async () => { - spawn.mockImplementationOnce( - ({ signal }: { signal: AbortSignal }) => - new Promise((_resolve, reject) => { - signal.addEventListener('abort', () => reject(signal.reason), { once: true }) - }) - ) - const first = start() - await vi.waitFor(() => expect(spawn).toHaveBeenCalledOnce()) - const second = start() - const results = Promise.allSettled([first, second]) - cancelPendingLocalPtySpawns('pending-bun-shell') - expect(await results).toEqual([ - { status: 'rejected', reason: new Error('PTY spawn canceled: pending-bun-shell') }, - { status: 'rejected', reason: new Error('PTY spawn canceled: pending-bun-shell') } - ]) - expect(spawn).toHaveBeenCalledOnce() - expect(activate).not.toHaveBeenCalled() - }) - - it('cleans a confirmed process when shutdown races the receipt continuation', async () => { - const proc = createProcess() - let release!: () => void - spawn.mockImplementationOnce( - () => - new Promise((resolve) => { - release = () => resolve({ process: proc, shellPath: '/bin/sh' }) - }) - ) - const first = start() - const rejected = expect(first).rejects.toThrow('PTY spawn canceled: pending-bun-shell') - await vi.waitFor(() => expect(spawn).toHaveBeenCalledOnce()) - release() - cancelPendingLocalPtySpawns('pending-bun-shell') - await rejected - expect(proc.kill).toHaveBeenCalledWith('SIGKILL') - expect(destroy).toHaveBeenCalledWith(proc) - expect(activate).not.toHaveBeenCalled() - }) -}) diff --git a/src/main/providers/local-pty-pi-owner-env.test.ts b/src/main/providers/local-pty-pi-owner-env.test.ts deleted file mode 100644 index 42e9773c993c..000000000000 --- a/src/main/providers/local-pty-pi-owner-env.test.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { afterEach, describe, expect, it, vi } from 'vitest' -import { - buildLocalPtySpawnEnvironment, - enforceLocalPtySpawnEnvironmentOverrides -} from './local-pty-spawn-environment' -import type { LocalPtyLaunchPlan } from './local-pty-launch-plan' - -const ownerEnv = { - ORCA_PI_STATUS_OWNED: '1234', - ORCA_PRIME_AGENT_STATUS_OWNED: '1234', - ORCA_PI_TITLE_MARKER_OWNED: '1234' -} -const terminalEnv = { - ORCA_PANE_KEY: 'new-tab:new-leaf', - ORCA_AGENT_LAUNCH_TOKEN: 'new-launch', - ORCA_AGENT_HOOK_TOKEN: 'receiver-token', - KEEP_ME: 'terminal-value' -} -const plan: LocalPtyLaunchPlan = { - startupAgentRecognition: null, - defaultCwd: '', - cwd: '', - wslInfo: null, - worktreeWslContext: undefined, - preferredWslContext: undefined, - launchWslContext: undefined, - shellPath: process.platform === 'win32' ? 'cmd.exe' : '/bin/sh', - shellArgs: [], - effectiveCwd: '', - validationCwd: '', - startupCommandDeliveredInShellArgs: false, - windowsFallbackAttempts: [], - shellReadyLaunch: null, - getFallbackShellReadyConfig: undefined, - primaryLaunchEnvKeys: [], - isWslShell: false, - launchWslDistro: null -} - -afterEach(() => vi.unstubAllEnvs()) - -describe('independent local terminal Pi ownership', () => { - it.each(['host', 'request'] as const)('does not inherit the %s process owner', async (source) => { - if (source === 'host') { - for (const [key, value] of Object.entries(ownerEnv)) { - vi.stubEnv(key, value) - } - } - const requestedEnv = { ...terminalEnv, ...(source === 'request' ? ownerEnv : {}) } - const env = await buildLocalPtySpawnEnvironment({ - id: 'new-terminal', - spawn: { cols: 80, rows: 24, env: requestedEnv }, - getOptions: () => ({}), - plan - }) - for (const key of Object.keys(ownerEnv)) { - expect(env[key]).toBeUndefined() - } - expect(env).toMatchObject(terminalEnv) - }) - - it('scrubs owners supplied by a late app-level environment builder', async () => { - const spawn = { cols: 80, rows: 24, env: terminalEnv } - const env = await buildLocalPtySpawnEnvironment({ - id: 'new-terminal', - spawn, - getOptions: () => ({ - buildSpawnEnv: (_id, baseEnv) => ({ ...baseEnv, ...ownerEnv, APP_ENV: 'kept' }) - }), - plan - }) - enforceLocalPtySpawnEnvironmentOverrides(spawn, env) - for (const key of Object.keys(ownerEnv)) { - expect(env[key]).toBeUndefined() - } - expect(env).toMatchObject({ ...terminalEnv, APP_ENV: 'kept' }) - }) -}) diff --git a/src/main/providers/local-pty-provider-foreground-process.test.ts b/src/main/providers/local-pty-provider-foreground-process.test.ts deleted file mode 100644 index b9e65af4e926..000000000000 --- a/src/main/providers/local-pty-provider-foreground-process.test.ts +++ /dev/null @@ -1,545 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import type * as MacosTccLoginShell from './macos-tcc-login-shell' - -const { - existsSyncMock, - statSyncMock, - accessSyncMock, - mkdirSyncMock, - writeFileSyncMock, - spawnMock, - prepareMacosTccLoginShellMock, - resolveAgentForegroundProcessMock, - readWindowsPtyJobProcessIdsMock, - killWithDescendantSweepMock, - isWslAvailableAsyncMock, - wslUncDirectoryExistsMock, - createShellPromptReadinessProbeMock -} = vi.hoisted(() => ({ - existsSyncMock: vi.fn(), - statSyncMock: vi.fn(), - accessSyncMock: vi.fn(), - mkdirSyncMock: vi.fn(), - writeFileSyncMock: vi.fn(), - spawnMock: vi.fn(), - prepareMacosTccLoginShellMock: vi.fn(), - resolveAgentForegroundProcessMock: vi.fn(), - readWindowsPtyJobProcessIdsMock: vi.fn(), - killWithDescendantSweepMock: vi.fn(), - isWslAvailableAsyncMock: vi.fn(), - wslUncDirectoryExistsMock: vi.fn(), - createShellPromptReadinessProbeMock: vi.fn() -})) - -vi.mock('fs', () => ({ - existsSync: existsSyncMock, - statSync: statSyncMock, - accessSync: accessSyncMock, - mkdirSync: mkdirSyncMock, - writeFileSync: writeFileSyncMock, - chmodSync: vi.fn(), - renameSync: vi.fn(), - rmSync: vi.fn(), - constants: { X_OK: 1 } -})) - -vi.mock('electron', () => ({ - app: { - getPath: vi.fn(() => '/tmp/orca-user-data') - } -})) - -vi.mock('node-pty', () => ({ - spawn: spawnMock -})) - -vi.mock('./macos-tcc-login-shell', async (importOriginal) => ({ - ...(await importOriginal()), - prepareMacosTccLoginShell: prepareMacosTccLoginShellMock -})) - -vi.mock('../pty-descendant-termination', () => ({ - killWithDescendantSweep: killWithDescendantSweepMock -})) - -// Resolve PowerShell family names to deterministic absolute paths (the fs mock -// above otherwise makes every probe miss). The real resolver — which skips the -// Store App Execution Alias stub — is covered in -// windows-powershell-executable.test.ts. -const WINDOWS_POWERSHELL_ABS = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' -const PWSH7_ABS = 'C:\\Program Files\\PowerShell\\7\\pwsh.exe' -const CMD_ABS = 'C:\\Windows\\System32\\cmd.exe' -vi.mock('./windows-powershell-executable', () => ({ - resolveWindowsPowerShellExecutablePath: (family: 'pwsh.exe' | 'powershell.exe') => - family === 'pwsh.exe' ? PWSH7_ABS : WINDOWS_POWERSHELL_ABS, - resolveWindowsPowerShellSpawnChain: (family: 'pwsh.exe' | 'powershell.exe') => - family === 'pwsh.exe' - ? [PWSH7_ABS, WINDOWS_POWERSHELL_ABS, CMD_ABS] - : [WINDOWS_POWERSHELL_ABS, CMD_ABS], - getWindowsCmdPath: () => CMD_ABS -})) - -vi.mock('./agent-foreground-process', () => ({ - resolveAgentForegroundProcessWithAvailability: (...args: unknown[]) => - resolveAgentForegroundProcessMock(...args) -})) - -vi.mock('./windows-pty-job-membership', () => ({ - readWindowsPtyJobProcessIds: (...args: unknown[]) => readWindowsPtyJobProcessIdsMock(...args), - isWindowsPtyJobReadable: () => true -})) - -vi.mock('../wsl', () => ({ - parseWslPath: (path: string) => { - const match = path.match(/^\\\\wsl\.localhost\\([^\\]+)(.*)$/) - if (!match) { - return null - } - return { - distro: match[1], - linuxPath: (match[2] || '').replace(/\\/g, '/') || '/' - } - }, - toLinuxPath: (path: string) => path.replace(/^C:\\/i, '/mnt/c/').replace(/\\/g, '/'), - toWindowsWslPath: (path: string, distro: string) => - `\\\\wsl.localhost\\${distro}${path.replace(/\//g, '\\')}`, - getDefaultWslDistro: () => 'Ubuntu', - isWslAvailableAsync: () => isWslAvailableAsyncMock(), - // Why: WSL worktree validation now asks the distro; these tests use WSL UNC - // cwds that are meant to exist, so report them present without spawning wsl.exe. - wslUncDirectoryExists: (...args: unknown[]) => wslUncDirectoryExistsMock(...args) -})) - -vi.mock('../shell-prompt-readiness-probe', () => ({ - createShellPromptReadinessProbe: createShellPromptReadinessProbeMock -})) - -import { LocalPtyProvider } from './local-pty-provider' -import { - applyLocalPtyProviderMockDefaults, - createLocalPtyMockProcess, - installLocalPtyProviderEnvSandbox, - type LocalPtyMockProcess -} from './local-pty-provider-test-harness' - -describe('LocalPtyProvider', () => { - let provider: LocalPtyProvider - let mockProc: LocalPtyMockProcess - let exitCb: ((info: { exitCode: number }) => void) | undefined - - installLocalPtyProviderEnvSandbox() - - beforeEach(() => { - applyLocalPtyProviderMockDefaults({ - existsSyncMock, - statSyncMock, - accessSyncMock, - mkdirSyncMock, - writeFileSyncMock, - prepareMacosTccLoginShellMock, - resolveAgentForegroundProcessMock, - readWindowsPtyJobProcessIdsMock, - killWithDescendantSweepMock, - isWslAvailableAsyncMock, - wslUncDirectoryExistsMock, - createShellPromptReadinessProbeMock - }) - - exitCb = undefined - mockProc = createLocalPtyMockProcess({ - get: () => exitCb, - set: (cb) => { - exitCb = cb - } - }) - spawnMock.mockReturnValue(mockProc) - - provider = new LocalPtyProvider() - }) - - describe('hasChildProcesses', () => { - it('returns false when foreground process matches shell', async () => { - const { id } = await provider.spawn({ cols: 80, rows: 24 }) - expect(await provider.hasChildProcesses(id)).toBe(false) - }) - - it('returns true when foreground process differs from shell', async () => { - mockProc.process = 'node' - const { id } = await provider.spawn({ cols: 80, rows: 24 }) - expect(await provider.hasChildProcesses(id)).toBe(true) - }) - - it('returns false for unknown PTY ids', async () => { - expect(await provider.hasChildProcesses('nonexistent')).toBe(false) - }) - }) - - describe('getForegroundProcess', () => { - it('returns the process name', async () => { - const { id } = await provider.spawn({ cols: 80, rows: 24 }) - expect(await provider.getForegroundProcess(id)).toBe('zsh') - }) - - it('uses the spawned Windows shell when node-pty reports only the terminal name', async () => { - Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) - mockProc.process = 'xterm-256color' - - const { id } = await provider.spawn({ - cols: 80, - rows: 24, - shellOverride: 'powershell.exe' - }) - - expect(await provider.getForegroundProcess(id)).toBe('powershell.exe') - expect(resolveAgentForegroundProcessMock).toHaveBeenCalledWith( - mockProc.pid, - 'powershell.exe', - expect.any(Object) - ) - }) - - it('returns null for unknown PTY ids', async () => { - expect(await provider.getForegroundProcess('nonexistent')).toBeNull() - }) - - it('keeps a recognized agent across an unavailable scan without adding probes', async () => { - resolveAgentForegroundProcessMock - .mockResolvedValueOnce({ available: true, processName: 'claude' }) - .mockResolvedValueOnce({ available: false, processName: 'powershell.exe' }) - const { id } = await provider.spawn({ cols: 80, rows: 24 }) - - await expect(provider.getForegroundProcess(id)).resolves.toBe('claude') - await expect(provider.getForegroundProcess(id)).resolves.toBe('claude') - expect(resolveAgentForegroundProcessMock).toHaveBeenCalledTimes(2) - }) - - it('drops a delayed scan result after the PTY exits', async () => { - let resolveScan!: (resolution: { available: boolean; processName: string }) => void - resolveAgentForegroundProcessMock.mockReturnValue( - new Promise((resolve) => { - resolveScan = resolve - }) - ) - const { id } = await provider.spawn({ cols: 80, rows: 24 }) - - const foreground = provider.getForegroundProcess(id) - exitCb?.({ exitCode: 0 }) - resolveScan({ available: true, processName: 'droid' }) - - await expect(foreground).resolves.toBeNull() - }) - - it('confirms a still-active agent from job membership without a whole-table scan', async () => { - Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) - mockProc.process = 'powershell.exe' - resolveAgentForegroundProcessMock.mockResolvedValue({ - available: true, - processName: 'claude' - }) - // A descendant beyond the shell is still in the pane's job. - readWindowsPtyJobProcessIdsMock.mockReturnValue(new Set([12345, 999])) - const { id } = await provider.spawn({ cols: 80, rows: 24 }) - - // First call establishes the agent identity via the scan. - await expect(provider.getForegroundProcess(id)).resolves.toBe('claude') - // node-pty still only names the shell, but console presence confirms the - // agent — no second whole-table scan. - await expect(provider.getForegroundProcess(id)).resolves.toBe('claude') - expect(resolveAgentForegroundProcessMock).toHaveBeenCalledTimes(1) - }) - - it('stops trusting job membership once the cached agent goes stale', async () => { - // The daemon path is not the only one that short-circuited on `size > 1`. - // Here the early return skips the scan that is the ONLY thing able to - // clear ptyLastRecognizedForeground, so on a WSL pane -- whose job always - // holds console-detached plumbing -- the identity was pinned for good. - Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) - mockProc.process = 'powershell.exe' - resolveAgentForegroundProcessMock.mockResolvedValue({ - available: true, - processName: 'claude' - }) - readWindowsPtyJobProcessIdsMock.mockReturnValue(new Set([12345, 999])) - vi.useFakeTimers({ toFake: ['Date'] }) - try { - vi.setSystemTime(1_000_000) - const { id } = await provider.spawn({ cols: 80, rows: 24 }) - - await expect(provider.getForegroundProcess(id)).resolves.toBe('claude') - await expect(provider.getForegroundProcess(id)).resolves.toBe('claude') - expect(resolveAgentForegroundProcessMock).toHaveBeenCalledTimes(1) - - // Past the bound, the superset answer stops standing in for a scan. - vi.setSystemTime(1_000_000 + 40_000) - resolveAgentForegroundProcessMock.mockResolvedValue({ - available: true, - processName: 'powershell.exe' - }) - await expect(provider.getForegroundProcess(id)).resolves.toBe('powershell.exe') - expect(resolveAgentForegroundProcessMock).toHaveBeenCalledTimes(2) - } finally { - vi.useRealTimers() - } - }) - - it('does not refresh the agent age after a degraded revalidation scan', async () => { - Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) - mockProc.process = 'powershell.exe' - resolveAgentForegroundProcessMock - .mockResolvedValueOnce({ available: true, processName: 'claude' }) - .mockResolvedValueOnce({ available: false, processName: null }) - .mockResolvedValue({ available: true, processName: 'powershell.exe' }) - readWindowsPtyJobProcessIdsMock.mockReturnValue(new Set([12345, 999])) - vi.useFakeTimers({ toFake: ['Date'] }) - try { - vi.setSystemTime(1_000_000) - const { id } = await provider.spawn({ cols: 80, rows: 24 }) - - await expect(provider.getForegroundProcess(id)).resolves.toBe('claude') - - vi.setSystemTime(1_000_000 + 40_000) - await expect(provider.getForegroundProcess(id)).resolves.toBe('claude') - - vi.setSystemTime(1_000_000 + 41_000) - await expect(provider.getForegroundProcess(id)).resolves.toBe('powershell.exe') - expect(resolveAgentForegroundProcessMock).toHaveBeenCalledTimes(3) - } finally { - vi.useRealTimers() - } - }) - - it('keeps short-circuiting a live agent instead of scanning on every call', async () => { - // The age bound must mean "time since we last confirmed the agent", not - // "how long the agent has run". Stamping only on a CHANGE of name makes a - // steadily-recognized agent age out permanently: the short-circuit dies - // after 30s, every call runs the whole-table scan, and one available-but- - // agentless snapshot then deletes a LIVE agent -- the false "agent done" - // #9258 exists to prevent. - Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) - mockProc.process = 'powershell.exe' - resolveAgentForegroundProcessMock.mockResolvedValue({ - available: true, - processName: 'claude' - }) - readWindowsPtyJobProcessIdsMock.mockReturnValue(new Set([12345, 999])) - vi.useFakeTimers({ toFake: ['Date'] }) - try { - vi.setSystemTime(1_000_000) - const { id } = await provider.spawn({ cols: 80, rows: 24 }) - - await expect(provider.getForegroundProcess(id)).resolves.toBe('claude') - expect(resolveAgentForegroundProcessMock).toHaveBeenCalledTimes(1) - - // Past the bound: one revalidating scan is expected, and it re-confirms. - vi.setSystemTime(1_000_000 + 40_000) - await expect(provider.getForegroundProcess(id)).resolves.toBe('claude') - expect(resolveAgentForegroundProcessMock).toHaveBeenCalledTimes(2) - - // That confirmation must restart the clock, so the short-circuit resumes. - vi.setSystemTime(1_000_000 + 45_000) - await expect(provider.getForegroundProcess(id)).resolves.toBe('claude') - await expect(provider.getForegroundProcess(id)).resolves.toBe('claude') - expect(resolveAgentForegroundProcessMock).toHaveBeenCalledTimes(2) - } finally { - vi.useRealTimers() - } - }) - - it('falls through to the scan when the job holds only the shell', async () => { - Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) - mockProc.process = 'powershell.exe' - resolveAgentForegroundProcessMock.mockResolvedValue({ - available: true, - processName: 'claude' - }) - readWindowsPtyJobProcessIdsMock.mockReturnValue(new Set([12345])) - const { id } = await provider.spawn({ cols: 80, rows: 24 }) - - await expect(provider.getForegroundProcess(id)).resolves.toBe('claude') - await expect(provider.getForegroundProcess(id)).resolves.toBe('claude') - expect(resolveAgentForegroundProcessMock).toHaveBeenCalledTimes(2) - }) - - it('keeps the cached agent when both the console probe and process snapshot are inconclusive', async () => { - Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) - mockProc.process = 'powershell.exe' - resolveAgentForegroundProcessMock - .mockResolvedValueOnce({ available: true, processName: 'claude' }) - .mockResolvedValue({ available: true, processName: null }) - readWindowsPtyJobProcessIdsMock.mockReturnValue(null) - const { id } = await provider.spawn({ cols: 80, rows: 24 }) - - await expect(provider.getForegroundProcess(id)).resolves.toBe('claude') - await expect(provider.getForegroundProcess(id)).resolves.toBe('claude') - expect(resolveAgentForegroundProcessMock).toHaveBeenCalledTimes(2) - }) - - it('retires an anchored agent immediately when its pid leaves the job, despite a leftover', async () => { - // Fix for stale-identity-behind-a-leftover: with an anchor pid, a detached - // descendant surviving in the job no longer stands in for the dead agent - // until the age bound -- the missing anchor is proof of exit right now. - Object.defineProperty(process, 'platform', { - configurable: true, - value: 'win32' - }) - mockProc.process = 'powershell.exe' - resolveAgentForegroundProcessMock - .mockResolvedValueOnce({ - available: true, - processName: 'claude', - processId: 999 - }) - .mockResolvedValue({ available: true, processName: null }) - readWindowsPtyJobProcessIdsMock.mockReturnValue(new Set([12345, 999])) - const { id } = await provider.spawn({ cols: 80, rows: 24 }) - - await expect(provider.getForegroundProcess(id)).resolves.toBe('claude') - // Agent 999 exits; a detached leftover 777 keeps the job larger than the shell. - readWindowsPtyJobProcessIdsMock.mockReturnValue(new Set([12345, 777])) - await expect(provider.getForegroundProcess(id)).resolves.toBeNull() - expect(resolveAgentForegroundProcessMock).toHaveBeenCalledTimes(2) - }) - - it('never expires an anchored agent the job still holds, even when scans miss it', async () => { - // Fix for false removal of a live agent: the anchor pid in the job is - // proof of life, so >30s of agentless-but-successful scans no longer - // retire a working agent -- and the confirmation restamps the clock. - Object.defineProperty(process, 'platform', { - configurable: true, - value: 'win32' - }) - mockProc.process = 'powershell.exe' - resolveAgentForegroundProcessMock - .mockResolvedValueOnce({ - available: true, - processName: 'claude', - processId: 999 - }) - .mockResolvedValue({ available: true, processName: null }) - readWindowsPtyJobProcessIdsMock.mockReturnValue(new Set([12345, 999])) - vi.useFakeTimers({ toFake: ['Date'] }) - try { - vi.setSystemTime(1_000_000) - const { id } = await provider.spawn({ cols: 80, rows: 24 }) - - await expect(provider.getForegroundProcess(id)).resolves.toBe('claude') - - // Past the bound: one drift-recheck scan runs, finds nothing, and the - // live anchor outranks the incomplete snapshot. - vi.setSystemTime(1_000_000 + 40_000) - await expect(provider.getForegroundProcess(id)).resolves.toBe('claude') - expect(resolveAgentForegroundProcessMock).toHaveBeenCalledTimes(2) - - // The proof of life restamped the clock, so the short-circuit resumes. - vi.setSystemTime(1_000_000 + 45_000) - await expect(provider.getForegroundProcess(id)).resolves.toBe('claude') - expect(resolveAgentForegroundProcessMock).toHaveBeenCalledTimes(2) - - // Much later again: recheck, still alive, still claude. - vi.setSystemTime(1_000_000 + 80_000) - await expect(provider.getForegroundProcess(id)).resolves.toBe('claude') - expect(resolveAgentForegroundProcessMock).toHaveBeenCalledTimes(3) - } finally { - vi.useRealTimers() - } - }) - - it('holds a restarting agent across a degraded scan instead of reporting an exit', async () => { - // The anchor pid died but another job member remains -- possibly the - // agent's restarted successor under a new pid. A degraded scan at that - // instant must not fire a false "agent done"; the next available scan - // re-recognizes and re-anchors. - Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) - mockProc.process = 'powershell.exe' - resolveAgentForegroundProcessMock - .mockResolvedValueOnce({ available: true, processName: 'claude', processId: 999 }) - .mockResolvedValueOnce({ available: false, processName: null }) - .mockResolvedValue({ available: true, processName: 'claude', processId: 1000 }) - readWindowsPtyJobProcessIdsMock.mockReturnValue(new Set([12345, 999])) - vi.useFakeTimers({ toFake: ['Date'] }) - try { - vi.setSystemTime(1_000_000) - const { id } = await provider.spawn({ cols: 80, rows: 24 }) - - await expect(provider.getForegroundProcess(id)).resolves.toBe('claude') - // Restart: 999 exits, successor 1000 joins the job; scan degrades. - readWindowsPtyJobProcessIdsMock.mockReturnValue(new Set([12345, 1000])) - await expect(provider.getForegroundProcess(id)).resolves.toBe('claude') - // Downgraded evidence ages out; the recheck scan re-anchors the successor. - vi.setSystemTime(1_000_000 + 40_000) - await expect(provider.getForegroundProcess(id)).resolves.toBe('claude') - expect(resolveAgentForegroundProcessMock).toHaveBeenCalledTimes(3) - // Re-anchored on pid 1000: the short-circuit resumes. - vi.setSystemTime(1_000_000 + 45_000) - await expect(provider.getForegroundProcess(id)).resolves.toBe('claude') - expect(resolveAgentForegroundProcessMock).toHaveBeenCalledTimes(3) - } finally { - vi.useRealTimers() - } - }) - - it('drops an anchored agent when the drift recheck proves the pid was recycled', async () => { - // The anchor pid stays in the job (a squatter reused it) but the scan - // proves the pid now runs a non-agent: proof of life must not apply. - Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) - mockProc.process = 'powershell.exe' - resolveAgentForegroundProcessMock - .mockResolvedValueOnce({ available: true, processName: 'claude', processId: 999 }) - .mockResolvedValue({ available: true, processName: null, anchorPidForeign: true }) - readWindowsPtyJobProcessIdsMock.mockReturnValue(new Set([12345, 999])) - vi.useFakeTimers({ toFake: ['Date'] }) - try { - vi.setSystemTime(1_000_000) - const { id } = await provider.spawn({ cols: 80, rows: 24 }) - - await expect(provider.getForegroundProcess(id)).resolves.toBe('claude') - - vi.setSystemTime(1_000_000 + 40_000) - await expect(provider.getForegroundProcess(id)).resolves.toBeNull() - // The recheck scan received the anchor to test against. - expect(resolveAgentForegroundProcessMock).toHaveBeenLastCalledWith( - mockProc.pid, - 'powershell.exe', - expect.objectContaining({ anchorProcessId: 999 }) - ) - } finally { - vi.useRealTimers() - } - }) - - it('retires the cached agent after verified shell-only membership and a no-agent scan', async () => { - Object.defineProperty(process, 'platform', { - configurable: true, - value: 'win32' - }) - mockProc.process = 'powershell.exe' - resolveAgentForegroundProcessMock - .mockResolvedValueOnce({ available: true, processName: 'claude' }) - .mockResolvedValue({ available: true, processName: null }) - readWindowsPtyJobProcessIdsMock.mockReturnValue(new Set([12345])) - const { id } = await provider.spawn({ cols: 80, rows: 24 }) - - await expect(provider.getForegroundProcess(id)).resolves.toBe('claude') - await expect(provider.getForegroundProcess(id)).resolves.toBeNull() - expect(resolveAgentForegroundProcessMock).toHaveBeenCalledTimes(2) - }) - }) - - describe('confirmForegroundProcess', () => { - it('drops a delayed result after the PTY exits', async () => { - let resolveScan!: (resolution: { available: boolean; processName: string }) => void - resolveAgentForegroundProcessMock.mockReturnValue( - new Promise((resolve) => { - resolveScan = resolve - }) - ) - const { id } = await provider.spawn({ cols: 80, rows: 24 }) - - const confirmation = provider.confirmForegroundProcess(id) - exitCb?.({ exitCode: 0 }) - resolveScan({ available: true, processName: 'droid' }) - - await expect(confirmation).resolves.toBeNull() - }) - }) -}) diff --git a/src/main/providers/local-pty-provider-io-events.test.ts b/src/main/providers/local-pty-provider-io-events.test.ts deleted file mode 100644 index 488e203e7e12..000000000000 --- a/src/main/providers/local-pty-provider-io-events.test.ts +++ /dev/null @@ -1,354 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import type * as MacosTccLoginShell from './macos-tcc-login-shell' - -const { - existsSyncMock, - statSyncMock, - accessSyncMock, - mkdirSyncMock, - writeFileSyncMock, - spawnMock, - prepareMacosTccLoginShellMock, - resolveAgentForegroundProcessMock, - readWindowsPtyJobProcessIdsMock, - killWithDescendantSweepMock, - isWslAvailableAsyncMock, - wslUncDirectoryExistsMock, - createShellPromptReadinessProbeMock -} = vi.hoisted(() => ({ - existsSyncMock: vi.fn(), - statSyncMock: vi.fn(), - accessSyncMock: vi.fn(), - mkdirSyncMock: vi.fn(), - writeFileSyncMock: vi.fn(), - spawnMock: vi.fn(), - prepareMacosTccLoginShellMock: vi.fn(), - resolveAgentForegroundProcessMock: vi.fn(), - readWindowsPtyJobProcessIdsMock: vi.fn(), - killWithDescendantSweepMock: vi.fn(), - isWslAvailableAsyncMock: vi.fn(), - wslUncDirectoryExistsMock: vi.fn(), - createShellPromptReadinessProbeMock: vi.fn() -})) - -vi.mock('fs', () => ({ - existsSync: existsSyncMock, - statSync: statSyncMock, - accessSync: accessSyncMock, - mkdirSync: mkdirSyncMock, - writeFileSync: writeFileSyncMock, - chmodSync: vi.fn(), - renameSync: vi.fn(), - rmSync: vi.fn(), - constants: { X_OK: 1 } -})) - -vi.mock('electron', () => ({ - app: { - getPath: vi.fn(() => '/tmp/orca-user-data') - } -})) - -vi.mock('node-pty', () => ({ - spawn: spawnMock -})) - -vi.mock('./macos-tcc-login-shell', async (importOriginal) => ({ - ...(await importOriginal()), - prepareMacosTccLoginShell: prepareMacosTccLoginShellMock -})) - -vi.mock('../pty-descendant-termination', () => ({ - killWithDescendantSweep: killWithDescendantSweepMock -})) - -// Resolve PowerShell family names to deterministic absolute paths (the fs mock -// above otherwise makes every probe miss). The real resolver — which skips the -// Store App Execution Alias stub — is covered in -// windows-powershell-executable.test.ts. -const WINDOWS_POWERSHELL_ABS = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' -const PWSH7_ABS = 'C:\\Program Files\\PowerShell\\7\\pwsh.exe' -const CMD_ABS = 'C:\\Windows\\System32\\cmd.exe' -vi.mock('./windows-powershell-executable', () => ({ - resolveWindowsPowerShellExecutablePath: (family: 'pwsh.exe' | 'powershell.exe') => - family === 'pwsh.exe' ? PWSH7_ABS : WINDOWS_POWERSHELL_ABS, - resolveWindowsPowerShellSpawnChain: (family: 'pwsh.exe' | 'powershell.exe') => - family === 'pwsh.exe' - ? [PWSH7_ABS, WINDOWS_POWERSHELL_ABS, CMD_ABS] - : [WINDOWS_POWERSHELL_ABS, CMD_ABS], - getWindowsCmdPath: () => CMD_ABS -})) - -vi.mock('./agent-foreground-process', () => ({ - resolveAgentForegroundProcessWithAvailability: (...args: unknown[]) => - resolveAgentForegroundProcessMock(...args) -})) - -vi.mock('./windows-pty-job-membership', () => ({ - readWindowsPtyJobProcessIds: (...args: unknown[]) => readWindowsPtyJobProcessIdsMock(...args), - isWindowsPtyJobReadable: () => true -})) - -vi.mock('../wsl', () => ({ - parseWslPath: (path: string) => { - const match = path.match(/^\\\\wsl\.localhost\\([^\\]+)(.*)$/) - if (!match) { - return null - } - return { - distro: match[1], - linuxPath: (match[2] || '').replace(/\\/g, '/') || '/' - } - }, - toLinuxPath: (path: string) => path.replace(/^C:\\/i, '/mnt/c/').replace(/\\/g, '/'), - toWindowsWslPath: (path: string, distro: string) => - `\\\\wsl.localhost\\${distro}${path.replace(/\//g, '\\')}`, - getDefaultWslDistro: () => 'Ubuntu', - isWslAvailableAsync: () => isWslAvailableAsyncMock(), - // Why: WSL worktree validation now asks the distro; these tests use WSL UNC - // cwds that are meant to exist, so report them present without spawning wsl.exe. - wslUncDirectoryExists: (...args: unknown[]) => wslUncDirectoryExistsMock(...args) -})) - -vi.mock('../shell-prompt-readiness-probe', () => ({ - createShellPromptReadinessProbe: createShellPromptReadinessProbeMock -})) - -import { LocalPtyProvider } from './local-pty-provider' -import { - applyLocalPtyProviderMockDefaults, - createLocalPtyMockProcess, - installLocalPtyProviderEnvSandbox, - type LocalPtyMockProcess -} from './local-pty-provider-test-harness' - -describe('LocalPtyProvider', () => { - let provider: LocalPtyProvider - let mockProc: LocalPtyMockProcess - let exitCb: ((info: { exitCode: number; signal?: number }) => void) | undefined - - installLocalPtyProviderEnvSandbox() - - beforeEach(() => { - applyLocalPtyProviderMockDefaults({ - existsSyncMock, - statSyncMock, - accessSyncMock, - mkdirSyncMock, - writeFileSyncMock, - prepareMacosTccLoginShellMock, - resolveAgentForegroundProcessMock, - readWindowsPtyJobProcessIdsMock, - killWithDescendantSweepMock, - isWslAvailableAsyncMock, - wslUncDirectoryExistsMock, - createShellPromptReadinessProbeMock - }) - - exitCb = undefined - mockProc = createLocalPtyMockProcess({ - get: () => exitCb, - set: (cb) => { - exitCb = cb - } - }) - spawnMock.mockReturnValue(mockProc) - - provider = new LocalPtyProvider() - }) - - describe('write', () => { - it('writes data to the PTY process', async () => { - const { id } = await provider.spawn({ cols: 80, rows: 24 }) - expect(provider.write(id, 'hello')).toBe(true) - expect(mockProc.write).toHaveBeenCalledWith('hello') - }) - - it('is a no-op for unknown PTY ids', () => { - expect(provider.write('nonexistent', 'hello')).toBe(false) - expect(mockProc.write).not.toHaveBeenCalled() - }) - }) - - describe('resize', () => { - it('resizes the PTY process', async () => { - const { id } = await provider.spawn({ cols: 80, rows: 24 }) - provider.resize(id, 120, 40) - expect(mockProc.resize).toHaveBeenCalledWith(120, 40) - }) - }) - - describe('producer flow control', () => { - it('pauses and resumes the node-pty process directly', async () => { - const { id } = await provider.spawn({ cols: 80, rows: 24 }) - provider.pauseProducer(id) - expect(mockProc.pause).toHaveBeenCalledTimes(1) - provider.resumeProducer(id) - expect(mockProc.resume).toHaveBeenCalledTimes(1) - }) - - it('is a no-op for unknown PTY ids', () => { - expect(() => { - provider.pauseProducer('nonexistent') - provider.resumeProducer('nonexistent') - }).not.toThrow() - expect(mockProc.pause).not.toHaveBeenCalled() - expect(mockProc.resume).not.toHaveBeenCalled() - }) - - it('swallows node-pty throws from a torn-down PTY', async () => { - const { id } = await provider.spawn({ cols: 80, rows: 24 }) - mockProc.pause.mockImplementation(() => { - throw new Error('read EIO') - }) - mockProc.resume.mockImplementation(() => { - throw new Error('read EIO') - }) - expect(() => { - provider.pauseProducer(id) - provider.resumeProducer(id) - }).not.toThrow() - }) - }) - - describe('event listeners', () => { - it('notifies data listeners when PTY produces output', async () => { - const dataHandler = vi.fn() - provider.onData(dataHandler) - const { id } = await provider.spawn({ cols: 80, rows: 24 }) - - // Simulate node-pty data event - const onDataCb = mockProc.onData.mock.calls[0][0] - onDataCb('hello world') - - expect(dataHandler).toHaveBeenCalledWith({ id, data: 'hello world' }) - }) - - it('classifies startup queries before runtime and public data listeners', async () => { - Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) - const runtimeData = vi.fn() - const dataHandler = vi.fn() - provider.configure({ onData: runtimeData }) - provider.onData(dataHandler) - const { id } = await provider.spawn({ - cols: 80, - rows: 24, - startupIngress: { - colors: { foreground: '#2e3434', background: '#ffffff' }, - deadlineMs: 5_000 - } - }) - const onDataCb = mockProc.onData.mock.calls[0][0] - const query = '\x1b]10;?\x07' - const echo = ']10;rgb:2e2e/3434/3434\\' - - onDataCb(query) - onDataCb(echo) - onDataCb('prompt') - - expect(mockProc.write).toHaveBeenCalledWith('\x1b]10;rgb:2e2e/3434/3434\x1b\\') - expect(runtimeData.mock.calls.map((call) => call.slice(1))).toEqual([ - ['', expect.any(Number), query.length, true], - ['', expect.any(Number), echo.length, true], - ['prompt', expect.any(Number)] - ]) - expect(dataHandler.mock.calls.map(([payload]) => payload)).toEqual([ - { id, data: '', sequenceChars: query.length, seq: query.length, transformed: true }, - { - id, - data: '', - sequenceChars: echo.length, - seq: query.length + echo.length, - transformed: true - }, - { id, data: 'prompt' } - ]) - }) - - it('consumes a native Windows OSC color query before renderer delivery', async () => { - Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) - const dataHandler = vi.fn() - provider.onData(dataHandler) - const { id } = await provider.spawn({ - cols: 80, - rows: 24, - shellOverride: 'powershell.exe' - }) - const onDataCb = mockProc.onData.mock.calls[0][0] - const query = '\x1b]10;?\x07' - - onDataCb(query) - - expect(dataHandler).toHaveBeenCalledWith({ - id, - data: '', - sequenceChars: query.length, - seq: query.length, - transformed: true - }) - expect(mockProc.write).not.toHaveBeenCalled() - }) - - it('keeps forwarded OSC color replies for a Windows-owned WSL PTY', async () => { - Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) - const { id } = await provider.spawn({ - cols: 80, - rows: 24, - shellOverride: 'wsl.exe', - terminalWindowsWslDistro: 'Ubuntu' - }) - const onDataCb = mockProc.onData.mock.calls[0][0] - const reply = '\x1b]11;rgb:ffff/ffff/ffff\x1b\\' - - onDataCb('\x1b]11;?\x07') - provider.write(id, reply) - - expect(mockProc.write).toHaveBeenCalledWith(reply) - }) - - it('notifies exit listeners when PTY exits', async () => { - const exitHandler = vi.fn() - provider.onExit(exitHandler) - const { id, incarnationId } = await provider.spawn({ cols: 80, rows: 24 }) - - // Simulate node-pty exit event - exitCb?.({ exitCode: 0 }) - - expect(exitHandler).toHaveBeenCalledWith({ - id, - code: 0, - incarnationId, - cause: { kind: 'exited', exitCode: 0 } - }) - }) - - it('reports a signalled death as a signal, not as the zero node-pty pairs with it', async () => { - const exitHandler = vi.fn() - provider.onExit(exitHandler) - const { id, incarnationId } = await provider.spawn({ cols: 80, rows: 24 }) - - // node-pty reports an OOM/SIGKILL as {exitCode: 0, signal: 9}; dropping - // the signal is what made a crash read as a clean finish (STA-4536). - exitCb?.({ exitCode: 0, signal: 9 }) - - expect(exitHandler).toHaveBeenCalledWith({ - id, - code: 0, - incarnationId, - cause: { kind: 'signaled', signal: 9 } - }) - }) - - it('allows unsubscribing from events', async () => { - const dataHandler = vi.fn() - const unsub = provider.onData(dataHandler) - const { id: _id } = await provider.spawn({ cols: 80, rows: 24 }) - - unsub() - const onDataCb = mockProc.onData.mock.calls[0][0] - onDataCb('hello') - - expect(dataHandler).not.toHaveBeenCalled() - }) - }) -}) diff --git a/src/main/providers/local-pty-provider-session-inventory.test.ts b/src/main/providers/local-pty-provider-session-inventory.test.ts deleted file mode 100644 index 802695581229..000000000000 --- a/src/main/providers/local-pty-provider-session-inventory.test.ts +++ /dev/null @@ -1,241 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import type * as MacosTccLoginShell from './macos-tcc-login-shell' - -const { - existsSyncMock, - statSyncMock, - accessSyncMock, - mkdirSyncMock, - writeFileSyncMock, - spawnMock, - prepareMacosTccLoginShellMock, - resolveAgentForegroundProcessMock, - readWindowsPtyJobProcessIdsMock, - killWithDescendantSweepMock, - isWslAvailableAsyncMock, - wslUncDirectoryExistsMock, - createShellPromptReadinessProbeMock -} = vi.hoisted(() => ({ - existsSyncMock: vi.fn(), - statSyncMock: vi.fn(), - accessSyncMock: vi.fn(), - mkdirSyncMock: vi.fn(), - writeFileSyncMock: vi.fn(), - spawnMock: vi.fn(), - prepareMacosTccLoginShellMock: vi.fn(), - resolveAgentForegroundProcessMock: vi.fn(), - readWindowsPtyJobProcessIdsMock: vi.fn(), - killWithDescendantSweepMock: vi.fn(), - isWslAvailableAsyncMock: vi.fn(), - wslUncDirectoryExistsMock: vi.fn(), - createShellPromptReadinessProbeMock: vi.fn() -})) - -vi.mock('fs', () => ({ - existsSync: existsSyncMock, - statSync: statSyncMock, - accessSync: accessSyncMock, - mkdirSync: mkdirSyncMock, - writeFileSync: writeFileSyncMock, - chmodSync: vi.fn(), - renameSync: vi.fn(), - rmSync: vi.fn(), - constants: { X_OK: 1 } -})) - -vi.mock('electron', () => ({ - app: { - getPath: vi.fn(() => '/tmp/orca-user-data') - } -})) - -vi.mock('node-pty', () => ({ - spawn: spawnMock -})) - -vi.mock('./macos-tcc-login-shell', async (importOriginal) => ({ - ...(await importOriginal()), - prepareMacosTccLoginShell: prepareMacosTccLoginShellMock -})) - -vi.mock('../pty-descendant-termination', () => ({ - killWithDescendantSweep: killWithDescendantSweepMock -})) - -// Resolve PowerShell family names to deterministic absolute paths (the fs mock -// above otherwise makes every probe miss). The real resolver — which skips the -// Store App Execution Alias stub — is covered in -// windows-powershell-executable.test.ts. -const WINDOWS_POWERSHELL_ABS = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' -const PWSH7_ABS = 'C:\\Program Files\\PowerShell\\7\\pwsh.exe' -const CMD_ABS = 'C:\\Windows\\System32\\cmd.exe' -vi.mock('./windows-powershell-executable', () => ({ - resolveWindowsPowerShellExecutablePath: (family: 'pwsh.exe' | 'powershell.exe') => - family === 'pwsh.exe' ? PWSH7_ABS : WINDOWS_POWERSHELL_ABS, - resolveWindowsPowerShellSpawnChain: (family: 'pwsh.exe' | 'powershell.exe') => - family === 'pwsh.exe' - ? [PWSH7_ABS, WINDOWS_POWERSHELL_ABS, CMD_ABS] - : [WINDOWS_POWERSHELL_ABS, CMD_ABS], - getWindowsCmdPath: () => CMD_ABS -})) - -vi.mock('./agent-foreground-process', () => ({ - resolveAgentForegroundProcessWithAvailability: (...args: unknown[]) => - resolveAgentForegroundProcessMock(...args) -})) - -vi.mock('./windows-pty-job-membership', () => ({ - readWindowsPtyJobProcessIds: (...args: unknown[]) => readWindowsPtyJobProcessIdsMock(...args), - isWindowsPtyJobReadable: () => true -})) - -vi.mock('../wsl', () => ({ - parseWslPath: (path: string) => { - const match = path.match(/^\\\\wsl\.localhost\\([^\\]+)(.*)$/) - if (!match) { - return null - } - return { - distro: match[1], - linuxPath: (match[2] || '').replace(/\\/g, '/') || '/' - } - }, - toLinuxPath: (path: string) => path.replace(/^C:\\/i, '/mnt/c/').replace(/\\/g, '/'), - toWindowsWslPath: (path: string, distro: string) => - `\\\\wsl.localhost\\${distro}${path.replace(/\//g, '\\')}`, - getDefaultWslDistro: () => 'Ubuntu', - isWslAvailableAsync: () => isWslAvailableAsyncMock(), - // Why: WSL worktree validation now asks the distro; these tests use WSL UNC - // cwds that are meant to exist, so report them present without spawning wsl.exe. - wslUncDirectoryExists: (...args: unknown[]) => wslUncDirectoryExistsMock(...args) -})) - -vi.mock('../shell-prompt-readiness-probe', () => ({ - createShellPromptReadinessProbe: createShellPromptReadinessProbeMock -})) - -import { LocalPtyProvider } from './local-pty-provider' -import { - applyLocalPtyProviderMockDefaults, - createLocalPtyMockProcess, - installLocalPtyProviderEnvSandbox, - type LocalPtyMockProcess -} from './local-pty-provider-test-harness' - -describe('LocalPtyProvider', () => { - let provider: LocalPtyProvider - let mockProc: LocalPtyMockProcess - let exitCb: ((info: { exitCode: number }) => void) | undefined - - installLocalPtyProviderEnvSandbox() - - beforeEach(() => { - applyLocalPtyProviderMockDefaults({ - existsSyncMock, - statSyncMock, - accessSyncMock, - mkdirSyncMock, - writeFileSyncMock, - prepareMacosTccLoginShellMock, - resolveAgentForegroundProcessMock, - readWindowsPtyJobProcessIdsMock, - killWithDescendantSweepMock, - isWslAvailableAsyncMock, - wslUncDirectoryExistsMock, - createShellPromptReadinessProbeMock - }) - - exitCb = undefined - mockProc = createLocalPtyMockProcess({ - get: () => exitCb, - set: (cb) => { - exitCb = cb - } - }) - spawnMock.mockReturnValue(mockProc) - - provider = new LocalPtyProvider() - }) - - describe('listProcesses', () => { - it('returns spawned PTYs', async () => { - const before = await provider.listProcesses() - await provider.spawn({ - cols: 80, - rows: 24, - cwd: '/tmp/owned-cwd', - worktreeId: 'repo::/tmp/owned-cwd' - }) - await provider.spawn({ cols: 80, rows: 24 }) - const after = await provider.listProcesses() - expect(after.length - before.length).toBe(2) - const newEntries = after.slice(before.length) - expect(newEntries[0]).toHaveProperty('id') - expect(newEntries[0]).toHaveProperty('title', 'zsh') - expect(newEntries[0]).toHaveProperty('cwd', '/tmp/owned-cwd') - expect(newEntries[0]).toHaveProperty('worktreeId', 'repo::/tmp/owned-cwd') - expect(newEntries[0]).not.toHaveProperty('wslDistro') - expect(newEntries[1]).not.toHaveProperty('wslDistro') - }) - - it('reports native and WSL ownership explicitly on Windows', async () => { - Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) - const native = await provider.spawn({ - cols: 80, - rows: 24, - cwd: 'C:\\repo', - shellOverride: 'powershell.exe' - }) - const wsl = await provider.spawn({ - cols: 80, - rows: 24, - cwd: '\\\\wsl.localhost\\Ubuntu\\home\\jin\\repo' - }) - - const processes = await provider.listProcesses() - - expect(processes.find((process) => process.id === native.id)?.wslDistro).toBeNull() - expect(processes.find((process) => process.id === wsl.id)?.wslDistro).toBe('Ubuntu') - }) - }) - - describe('getDefaultShell', () => { - it('returns SHELL env var on Unix', async () => { - const originalShell = process.env.SHELL - try { - process.env.SHELL = '/bin/bash' - expect(await provider.getDefaultShell()).toBe('/bin/bash') - } finally { - if (originalShell === undefined) { - delete process.env.SHELL - } else { - process.env.SHELL = originalShell - } - } - }) - }) - - describe('getProfiles', () => { - it('awaits asynchronous WSL availability on Windows', async () => { - Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) - let resolveAvailability!: (available: boolean) => void - isWslAvailableAsyncMock.mockReturnValue( - new Promise((resolve) => { - resolveAvailability = resolve - }) - ) - - const profiles = provider.getProfiles() - let settled = false - void profiles.then(() => { - settled = true - }) - await Promise.resolve() - expect(settled).toBe(false) - - resolveAvailability(true) - await expect(profiles).resolves.toContainEqual({ name: 'WSL', path: 'wsl.exe' }) - expect(isWslAvailableAsyncMock).toHaveBeenCalledOnce() - }) - }) -}) diff --git a/src/main/providers/local-pty-provider-shell-readiness.test.ts b/src/main/providers/local-pty-provider-shell-readiness.test.ts deleted file mode 100644 index 0457a6e170f0..000000000000 --- a/src/main/providers/local-pty-provider-shell-readiness.test.ts +++ /dev/null @@ -1,343 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import type * as MacosTccLoginShell from './macos-tcc-login-shell' - -const { - existsSyncMock, - statSyncMock, - accessSyncMock, - mkdirSyncMock, - writeFileSyncMock, - spawnMock, - prepareMacosTccLoginShellMock, - resolveAgentForegroundProcessMock, - readWindowsPtyJobProcessIdsMock, - killWithDescendantSweepMock, - isWslAvailableAsyncMock, - wslUncDirectoryExistsMock, - createShellPromptReadinessProbeMock -} = vi.hoisted(() => ({ - existsSyncMock: vi.fn(), - statSyncMock: vi.fn(), - accessSyncMock: vi.fn(), - mkdirSyncMock: vi.fn(), - writeFileSyncMock: vi.fn(), - spawnMock: vi.fn(), - prepareMacosTccLoginShellMock: vi.fn(), - resolveAgentForegroundProcessMock: vi.fn(), - readWindowsPtyJobProcessIdsMock: vi.fn(), - killWithDescendantSweepMock: vi.fn(), - isWslAvailableAsyncMock: vi.fn(), - wslUncDirectoryExistsMock: vi.fn(), - createShellPromptReadinessProbeMock: vi.fn() -})) - -vi.mock('fs', () => ({ - existsSync: existsSyncMock, - statSync: statSyncMock, - accessSync: accessSyncMock, - mkdirSync: mkdirSyncMock, - writeFileSync: writeFileSyncMock, - chmodSync: vi.fn(), - renameSync: vi.fn(), - rmSync: vi.fn(), - constants: { X_OK: 1 } -})) - -vi.mock('electron', () => ({ - app: { - getPath: vi.fn(() => '/tmp/orca-user-data') - } -})) - -vi.mock('node-pty', () => ({ - spawn: spawnMock -})) - -vi.mock('./macos-tcc-login-shell', async (importOriginal) => ({ - ...(await importOriginal()), - prepareMacosTccLoginShell: prepareMacosTccLoginShellMock -})) - -vi.mock('../pty-descendant-termination', () => ({ - killWithDescendantSweep: killWithDescendantSweepMock -})) - -// Resolve PowerShell family names to deterministic absolute paths (the fs mock -// above otherwise makes every probe miss). The real resolver — which skips the -// Store App Execution Alias stub — is covered in -// windows-powershell-executable.test.ts. -const WINDOWS_POWERSHELL_ABS = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' -const PWSH7_ABS = 'C:\\Program Files\\PowerShell\\7\\pwsh.exe' -const CMD_ABS = 'C:\\Windows\\System32\\cmd.exe' -vi.mock('./windows-powershell-executable', () => ({ - resolveWindowsPowerShellExecutablePath: (family: 'pwsh.exe' | 'powershell.exe') => - family === 'pwsh.exe' ? PWSH7_ABS : WINDOWS_POWERSHELL_ABS, - resolveWindowsPowerShellSpawnChain: (family: 'pwsh.exe' | 'powershell.exe') => - family === 'pwsh.exe' - ? [PWSH7_ABS, WINDOWS_POWERSHELL_ABS, CMD_ABS] - : [WINDOWS_POWERSHELL_ABS, CMD_ABS], - getWindowsCmdPath: () => CMD_ABS -})) - -vi.mock('./agent-foreground-process', () => ({ - resolveAgentForegroundProcessWithAvailability: (...args: unknown[]) => - resolveAgentForegroundProcessMock(...args) -})) - -vi.mock('./windows-pty-job-membership', () => ({ - readWindowsPtyJobProcessIds: (...args: unknown[]) => readWindowsPtyJobProcessIdsMock(...args), - isWindowsPtyJobReadable: () => true -})) - -vi.mock('../wsl', () => ({ - parseWslPath: (path: string) => { - const match = path.match(/^\\\\wsl\.localhost\\([^\\]+)(.*)$/) - if (!match) { - return null - } - return { - distro: match[1], - linuxPath: (match[2] || '').replace(/\\/g, '/') || '/' - } - }, - toLinuxPath: (path: string) => path.replace(/^C:\\/i, '/mnt/c/').replace(/\\/g, '/'), - toWindowsWslPath: (path: string, distro: string) => - `\\\\wsl.localhost\\${distro}${path.replace(/\//g, '\\')}`, - getDefaultWslDistro: () => 'Ubuntu', - isWslAvailableAsync: () => isWslAvailableAsyncMock(), - // Why: WSL worktree validation now asks the distro; these tests use WSL UNC - // cwds that are meant to exist, so report them present without spawning wsl.exe. - wslUncDirectoryExists: (...args: unknown[]) => wslUncDirectoryExistsMock(...args) -})) - -vi.mock('../shell-prompt-readiness-probe', () => ({ - createShellPromptReadinessProbe: createShellPromptReadinessProbeMock -})) - -import { LocalPtyProvider } from './local-pty-provider' -import { POSIX_SHELL_STARTUP_COMMAND_ENV } from '../pty/posix-shell-startup-command' -import { - applyLocalPtyProviderMockDefaults, - createLocalPtyMockProcess, - installLocalPtyProviderEnvSandbox, - type LocalPtyMockProcess -} from './local-pty-provider-test-harness' - -describe('LocalPtyProvider', () => { - let provider: LocalPtyProvider - let mockProc: LocalPtyMockProcess - let exitCb: ((info: { exitCode: number }) => void) | undefined - - installLocalPtyProviderEnvSandbox() - - beforeEach(() => { - applyLocalPtyProviderMockDefaults({ - existsSyncMock, - statSyncMock, - accessSyncMock, - mkdirSyncMock, - writeFileSyncMock, - prepareMacosTccLoginShellMock, - resolveAgentForegroundProcessMock, - readWindowsPtyJobProcessIdsMock, - killWithDescendantSweepMock, - isWslAvailableAsyncMock, - wslUncDirectoryExistsMock, - createShellPromptReadinessProbeMock - }) - - exitCb = undefined - mockProc = createLocalPtyMockProcess({ - get: () => exitCb, - set: (cb) => { - exitCb = cb - } - }) - spawnMock.mockReturnValue(mockProc) - - provider = new LocalPtyProvider() - }) - - describe('spawn', () => { - it('delegates markerless Codex startup to the shell wrapper without a PTY write', async () => { - process.env.SHELL = '/bin/zsh' - const command = "codex '--dangerously-bypass-approvals-and-sandbox'" - - await provider.spawn({ cols: 80, rows: 24, command }) - - const spawnEnv = spawnMock.mock.calls.at(-1)?.[2].env - expect(spawnEnv[POSIX_SHELL_STARTUP_COMMAND_ENV]).toBe(command) - expect(spawnEnv.ORCA_SHELL_FEATURES).toContain('startup') - expect(spawnEnv.ORCA_SHELL_FEATURES).not.toContain('ready') - expect(mockProc.write).not.toHaveBeenCalled() - }) - - it('keeps payload-bearing Codex startup in the wrapper readiness path', async () => { - process.env.SHELL = '/bin/zsh' - const command = "codex '--dangerously-bypass-approvals-and-sandbox' 'review this branch'" - - await provider.spawn({ - cols: 80, - rows: 24, - command, - startupCommandDelivery: 'shell-ready' - }) - - const spawnEnv = spawnMock.mock.calls.at(-1)?.[2].env - expect(spawnEnv[POSIX_SHELL_STARTUP_COMMAND_ENV]).toBe(command) - expect(spawnEnv.ORCA_SHELL_FEATURES).toContain('startup') - expect(spawnEnv.ORCA_SHELL_FEATURES).toContain('ready') - expect(mockProc.write).not.toHaveBeenCalled() - }) - - it('scrubs an inherited wrapper startup command from ordinary panes', async () => { - process.env.SHELL = '/bin/zsh' - - await provider.spawn({ - cols: 80, - rows: 24, - env: { [POSIX_SHELL_STARTUP_COMMAND_ENV]: 'poisoned command' } - }) - - const spawnEnv = spawnMock.mock.calls.at(-1)?.[2].env - expect(spawnEnv[POSIX_SHELL_STARTUP_COMMAND_ENV]).toBeUndefined() - expect(mockProc.write).not.toHaveBeenCalled() - }) - - it('retains PTY delivery for unsupported local shells without leaking wrapper state', async () => { - vi.useFakeTimers() - try { - process.env.SHELL = '/bin/sh' - const command = "codex '--dangerously-bypass-approvals-and-sandbox'" - - await provider.spawn({ cols: 80, rows: 24, command }) - - const spawnEnv = spawnMock.mock.calls.at(-1)?.[2].env - expect(spawnEnv[POSIX_SHELL_STARTUP_COMMAND_ENV]).toBeUndefined() - expect(mockProc.write).not.toHaveBeenCalled() - - await vi.advanceTimersByTimeAsync(200) - expect(mockProc.write).toHaveBeenCalledWith(`${command}\n`) - } finally { - vi.useRealTimers() - } - }) - - it('verifies shell identity against the exact spawn PATH', async () => { - provider.configure({ - buildSpawnEnv: (_id, env) => ({ ...env, PATH: '/post-hook/bin' }) - }) - - await provider.spawn({ - cols: 80, - rows: 24, - command: 'printf ready', - env: { PATH: '/pre-hook/bin' } - }) - - expect(spawnMock.mock.calls.at(-1)?.[2].env.PATH).toBe('/post-hook/bin') - expect(createShellPromptReadinessProbeMock).toHaveBeenCalledWith( - expect.objectContaining({ shellPathEnv: '/post-hook/bin' }) - ) - }) - - // Order matters: this case reads spawnMock.mock.calls[0], which the preceding - // default-shell spawn populates (spawnMock accumulates calls across tests). - it('uses fallback shell readiness when startup-command shell spawn falls back', async () => { - vi.useFakeTimers() - try { - process.env.SHELL = '/usr/bin/fish' - spawnMock.mockImplementationOnce(() => { - throw new Error('fish failed') - }) - spawnMock.mockReturnValue(mockProc) - - await provider.spawn({ cols: 80, rows: 24, command: "printf 'linked issue context'" }) - - expect(spawnMock.mock.calls[0]?.[0]).toBe('/bin/zsh') - await Promise.resolve() - vi.advanceTimersByTime(50) - await Promise.resolve() - expect(mockProc.write).not.toHaveBeenCalled() - - const dataCallback = mockProc.onData.mock.calls[0]?.[0] as (data: string) => void - dataCallback('\x1b]777;orca-shell-ready\x07user@host % ') - await Promise.resolve() - vi.advanceTimersByTime(29) - await Promise.resolve() - expect(mockProc.write).not.toHaveBeenCalled() - - vi.advanceTimersByTime(1) - await Promise.resolve() - expect(mockProc.write).toHaveBeenCalledWith("printf 'linked issue context'\n") - } finally { - vi.useRealTimers() - } - }) - - it.each([ - ['after the ready marker', ['\x1b]777;orca-shell-ready\x07', '\x1b[?2004hfish> ']], - ['after the ESC introducer', ['\x1b]777;orca-shell-ready\x07\x1b', '[?2004hfish> ']] - ])('preserves Fish bracketed-paste output split %s', async (_boundary, chunks) => { - process.env.SHELL = '/usr/bin/fish' - const received: string[] = [] - provider.configure({ onData: (_id, data) => received.push(data) }) - - await provider.spawn({ cols: 80, rows: 24, command: 'printf ready' }) - const dataCallback = mockProc.onData.mock.calls[0]?.[0] as (data: string) => void - for (const chunk of chunks) { - dataCallback(chunk) - } - - expect(received.join('')).toBe('\x1b[?2004hfish> ') - }) - - it('releases held marker-prefix bytes when local shell readiness times out', async () => { - vi.useFakeTimers() - const onData = vi.fn() - provider.configure({ onData }) - try { - await provider.spawn({ cols: 80, rows: 24, command: 'printf ready' }) - const dataCallback = mockProc.onData.mock.calls[0]?.[0] as (data: string) => void - - dataCallback('\x1b]777;orca-shell-ready') - expect(onData).not.toHaveBeenCalled() - - vi.advanceTimersByTime(1500) - await Promise.resolve() - - expect(onData).toHaveBeenCalledWith( - expect.any(String), - '\x1b]777;orca-shell-ready', - expect.any(Number) - ) - expect(mockProc.write).not.toHaveBeenCalled() - - vi.advanceTimersByTime(200) - await Promise.resolve() - expect(mockProc.write).toHaveBeenCalledWith('printf ready\n') - } finally { - vi.useRealTimers() - } - }) - - it('releases held marker-prefix bytes when local shell exits before readiness', async () => { - const onData = vi.fn() - provider.configure({ onData }) - - await provider.spawn({ cols: 80, rows: 24, command: 'printf ready' }) - const dataCallback = mockProc.onData.mock.calls[0]?.[0] as (data: string) => void - - dataCallback('\x1b]777;orca-shell-ready') - expect(onData).not.toHaveBeenCalled() - - exitCb?.({ exitCode: 0 }) - - expect(onData).toHaveBeenCalledWith( - expect.any(String), - '\x1b]777;orca-shell-ready', - expect.any(Number) - ) - }) - }) -}) diff --git a/src/main/providers/local-pty-provider-shutdown.test.ts b/src/main/providers/local-pty-provider-shutdown.test.ts deleted file mode 100644 index 950a7e9cbae7..000000000000 --- a/src/main/providers/local-pty-provider-shutdown.test.ts +++ /dev/null @@ -1,617 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import type * as MacosTccLoginShell from './macos-tcc-login-shell' - -const { - existsSyncMock, - statSyncMock, - accessSyncMock, - mkdirSyncMock, - writeFileSyncMock, - spawnMock, - prepareMacosTccLoginShellMock, - resolveAgentForegroundProcessMock, - readWindowsPtyJobProcessIdsMock, - killWithDescendantSweepMock, - isWslAvailableAsyncMock, - wslUncDirectoryExistsMock, - createShellPromptReadinessProbeMock -} = vi.hoisted(() => ({ - existsSyncMock: vi.fn(), - statSyncMock: vi.fn(), - accessSyncMock: vi.fn(), - mkdirSyncMock: vi.fn(), - writeFileSyncMock: vi.fn(), - spawnMock: vi.fn(), - prepareMacosTccLoginShellMock: vi.fn(), - resolveAgentForegroundProcessMock: vi.fn(), - readWindowsPtyJobProcessIdsMock: vi.fn(), - killWithDescendantSweepMock: vi.fn(), - isWslAvailableAsyncMock: vi.fn(), - wslUncDirectoryExistsMock: vi.fn(), - createShellPromptReadinessProbeMock: vi.fn() -})) - -vi.mock('fs', () => ({ - existsSync: existsSyncMock, - statSync: statSyncMock, - accessSync: accessSyncMock, - mkdirSync: mkdirSyncMock, - writeFileSync: writeFileSyncMock, - chmodSync: vi.fn(), - renameSync: vi.fn(), - rmSync: vi.fn(), - constants: { X_OK: 1 } -})) - -vi.mock('electron', () => ({ - app: { - getPath: vi.fn(() => '/tmp/orca-user-data') - } -})) - -vi.mock('node-pty', () => ({ - spawn: spawnMock -})) - -vi.mock('./macos-tcc-login-shell', async (importOriginal) => ({ - ...(await importOriginal()), - prepareMacosTccLoginShell: prepareMacosTccLoginShellMock -})) - -vi.mock('../pty-descendant-termination', () => ({ - killWithDescendantSweep: killWithDescendantSweepMock -})) - -// Resolve PowerShell family names to deterministic absolute paths (the fs mock -// above otherwise makes every probe miss). The real resolver — which skips the -// Store App Execution Alias stub — is covered in -// windows-powershell-executable.test.ts. -const WINDOWS_POWERSHELL_ABS = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' -const PWSH7_ABS = 'C:\\Program Files\\PowerShell\\7\\pwsh.exe' -const CMD_ABS = 'C:\\Windows\\System32\\cmd.exe' -vi.mock('./windows-powershell-executable', () => ({ - resolveWindowsPowerShellExecutablePath: (family: 'pwsh.exe' | 'powershell.exe') => - family === 'pwsh.exe' ? PWSH7_ABS : WINDOWS_POWERSHELL_ABS, - resolveWindowsPowerShellSpawnChain: (family: 'pwsh.exe' | 'powershell.exe') => - family === 'pwsh.exe' - ? [PWSH7_ABS, WINDOWS_POWERSHELL_ABS, CMD_ABS] - : [WINDOWS_POWERSHELL_ABS, CMD_ABS], - getWindowsCmdPath: () => CMD_ABS -})) - -vi.mock('./agent-foreground-process', () => ({ - resolveAgentForegroundProcessWithAvailability: (...args: unknown[]) => - resolveAgentForegroundProcessMock(...args) -})) - -vi.mock('./windows-pty-job-membership', () => ({ - readWindowsPtyJobProcessIds: (...args: unknown[]) => readWindowsPtyJobProcessIdsMock(...args), - isWindowsPtyJobReadable: () => true -})) - -vi.mock('../wsl', () => ({ - parseWslPath: (path: string) => { - const match = path.match(/^\\\\wsl\.localhost\\([^\\]+)(.*)$/) - if (!match) { - return null - } - return { - distro: match[1], - linuxPath: (match[2] || '').replace(/\\/g, '/') || '/' - } - }, - toLinuxPath: (path: string) => path.replace(/^C:\\/i, '/mnt/c/').replace(/\\/g, '/'), - toWindowsWslPath: (path: string, distro: string) => - `\\\\wsl.localhost\\${distro}${path.replace(/\//g, '\\')}`, - getDefaultWslDistro: () => 'Ubuntu', - isWslAvailableAsync: () => isWslAvailableAsyncMock(), - // Why: WSL worktree validation now asks the distro; these tests use WSL UNC - // cwds that are meant to exist, so report them present without spawning wsl.exe. - wslUncDirectoryExists: (...args: unknown[]) => wslUncDirectoryExistsMock(...args) -})) - -vi.mock('../shell-prompt-readiness-probe', () => ({ - createShellPromptReadinessProbe: createShellPromptReadinessProbeMock -})) - -import { - LOCAL_PTY_FORCE_KILL_RETRY_MS, - LOCAL_PTY_GRACEFUL_FORCE_TIMEOUT_MS, - LOCAL_PTY_PHYSICAL_EXIT_TIMEOUT_MS, - LocalPtyProvider -} from './local-pty-provider' -import { - applyLocalPtyProviderMockDefaults, - createLocalPtyMockProcess, - installLocalPtyProviderEnvSandbox, - type LocalPtyMockProcess -} from './local-pty-provider-test-harness' - -describe('LocalPtyProvider', () => { - let provider: LocalPtyProvider - let mockProc: LocalPtyMockProcess - let exitCb: ((info: { exitCode: number }) => void) | undefined - - installLocalPtyProviderEnvSandbox() - - beforeEach(() => { - applyLocalPtyProviderMockDefaults({ - existsSyncMock, - statSyncMock, - accessSyncMock, - mkdirSyncMock, - writeFileSyncMock, - prepareMacosTccLoginShellMock, - resolveAgentForegroundProcessMock, - readWindowsPtyJobProcessIdsMock, - killWithDescendantSweepMock, - isWslAvailableAsyncMock, - wslUncDirectoryExistsMock, - createShellPromptReadinessProbeMock - }) - - exitCb = undefined - mockProc = createLocalPtyMockProcess({ - get: () => exitCb, - set: (cb) => { - exitCb = cb - } - }) - spawnMock.mockReturnValue(mockProc) - - provider = new LocalPtyProvider() - }) - - describe('shutdown', () => { - it('kills the PTY process', async () => { - // Why: capture the spy reference before shutdown triggers onExit → - // POSIX kill neutralization. After neutralization, mockProc.kill is - // replaced with a non-spy no-op to close the UnixTerminal.destroy() → - // socket-close → SIGHUP-to-recycled-pid race (see docs/fix-pty-fd-leak.md). - const killSpy = mockProc.kill - const { id } = await provider.spawn({ cols: 80, rows: 24 }) - await provider.shutdown(id, { immediate: true }) - expect(killSpy).toHaveBeenCalled() - }) - - it('invokes onExit callback via the node-pty exit handler', async () => { - const onExit = vi.fn() - provider.configure({ onExit }) - const { id, incarnationId } = await provider.spawn({ cols: 80, rows: 24 }) - await provider.shutdown(id, { immediate: true }) - expect(onExit).toHaveBeenCalledWith(id, -1, incarnationId, { - kind: 'unknown', - reason: 'stop_unverified' - }) - }) - - it('does not destroy after an intentional Windows shutdown kill', async () => { - Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) - const killSpy = vi.fn() - const destroySpy = vi.fn(() => { - killSpy() - }) - spawnMock.mockReturnValue({ - ...mockProc, - kill: killSpy, - destroy: destroySpy - }) - - const { id } = await provider.spawn({ cols: 80, rows: 24 }) - const shutdown = provider.shutdown(id, { immediate: true }) - exitCb?.({ exitCode: -1 }) - await shutdown - - expect(killSpy).toHaveBeenCalledTimes(1) - expect(destroySpy).not.toHaveBeenCalled() - }) - - it('keeps shutdown and ownership pending until node-pty reports physical exit', async () => { - const killSpy = vi.fn() - mockProc.kill = killSpy - const { id } = await provider.spawn({ cols: 80, rows: 24 }) - - let settled = false - const shutdown = provider.shutdown(id, { immediate: true }).finally(() => { - settled = true - }) - await Promise.resolve() - - expect(killSpy).toHaveBeenCalledWith('SIGKILL') - expect(settled).toBe(false) - expect(provider.hasPty(id)).toBe(true) - exitCb?.({ exitCode: 137 }) - await shutdown - expect(provider.hasPty(id)).toBe(false) - }) - - it('keeps physical-exit tracking when orphan cleanup races immediate shutdown', async () => { - const killSpy = vi.fn(() => { - queueMicrotask(() => exitCb?.({ exitCode: 137 })) - }) - mockProc.kill = killSpy - const { id } = await provider.spawn({ cols: 80, rows: 24 }) - - const shutdown = provider.shutdown(id, { immediate: true }) - expect(provider.killOrphanedPtys(1)).toEqual([{ id }]) - expect(provider.hasPty(id)).toBe(true) - - await expect(shutdown).resolves.toBeUndefined() - expect(killSpy).toHaveBeenCalledTimes(1) - expect(provider.hasPty(id)).toBe(false) - }) - - it('escalates graceful shutdown before orphan cleanup disables the kill handle', async () => { - vi.useFakeTimers() - try { - const killSpy = vi.fn() - mockProc.kill = killSpy - const { id } = await provider.spawn({ cols: 80, rows: 24 }) - - const graceful = provider.shutdown(id, { immediate: false }) - expect(killSpy.mock.calls).toEqual([['SIGTERM']]) - - expect(provider.killOrphanedPtys(1)).toEqual([{ id }]) - expect(killSpy.mock.calls).toEqual([['SIGTERM'], ['SIGKILL']]) - - // The original graceful deadline was replaced by the immediate - // escalation, so it cannot call the now-neutralized proc.kill later. - await vi.advanceTimersByTimeAsync(LOCAL_PTY_GRACEFUL_FORCE_TIMEOUT_MS) - expect(killSpy).toHaveBeenCalledTimes(2) - exitCb?.({ exitCode: 137 }) - await graceful - expect(provider.hasPty(id)).toBe(false) - expect(vi.getTimerCount()).toBe(0) - } finally { - vi.useRealTimers() - } - }) - - it('escalates a graceful shutdown when destructive cleanup joins it', async () => { - const killSpy = vi.fn() - mockProc.kill = killSpy - const { id } = await provider.spawn({ cols: 80, rows: 24 }) - - const graceful = provider.shutdown(id, { immediate: false }) - const immediate = provider.shutdown(id, { immediate: true }) - expect(killSpy.mock.calls).toEqual([['SIGTERM'], ['SIGKILL']]) - - exitCb?.({ exitCode: 137 }) - await Promise.all([graceful, immediate]) - expect(provider.hasPty(id)).toBe(false) - }) - - it('force-kills a POSIX PTY that ignores graceful shutdown', async () => { - vi.useFakeTimers() - try { - const killSpy = vi.fn() - mockProc.kill = killSpy - const { id } = await provider.spawn({ cols: 80, rows: 24 }) - - const graceful = provider.shutdown(id, { immediate: false }) - expect(killSpy.mock.calls).toEqual([['SIGTERM']]) - - await vi.advanceTimersByTimeAsync(LOCAL_PTY_GRACEFUL_FORCE_TIMEOUT_MS) - expect(killSpy.mock.calls).toEqual([['SIGTERM'], ['SIGKILL']]) - exitCb?.({ exitCode: 137 }) - await graceful - expect(provider.hasPty(id)).toBe(false) - expect(vi.getTimerCount()).toBe(0) - } finally { - vi.useRealTimers() - } - }) - - it('retries a rejected graceful-deadline SIGKILL before the physical timeout', async () => { - vi.useFakeTimers() - try { - let forceAttempts = 0 - const killSpy = vi.fn((signal: string) => { - if (signal === 'SIGKILL' && forceAttempts++ === 0) { - throw new Error('transient force-kill failure') - } - }) - mockProc.kill = killSpy - const { id } = await provider.spawn({ cols: 80, rows: 24 }) - - const graceful = provider.shutdown(id, { immediate: false }) - await vi.advanceTimersByTimeAsync(LOCAL_PTY_GRACEFUL_FORCE_TIMEOUT_MS) - expect(killSpy.mock.calls).toEqual([['SIGTERM'], ['SIGKILL']]) - expect(provider.hasPty(id)).toBe(true) - - await vi.advanceTimersByTimeAsync(LOCAL_PTY_FORCE_KILL_RETRY_MS) - expect(killSpy.mock.calls).toEqual([['SIGTERM'], ['SIGKILL'], ['SIGKILL']]) - exitCb?.({ exitCode: 137 }) - await graceful - expect(provider.hasPty(id)).toBe(false) - expect(vi.getTimerCount()).toBe(0) - } finally { - vi.useRealTimers() - } - }) - - it('does not double-kill ConPTY when destructive cleanup joins shutdown', async () => { - Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) - const killSpy = vi.fn() - mockProc.kill = killSpy - const { id } = await provider.spawn({ cols: 80, rows: 24 }) - - const graceful = provider.shutdown(id, { immediate: false }) - const immediate = provider.shutdown(id, { immediate: true }) - expect(killSpy.mock.calls).toEqual([[]]) - - exitCb?.({ exitCode: 137 }) - await Promise.all([graceful, immediate]) - expect(killSpy).toHaveBeenCalledTimes(1) - expect(provider.hasPty(id)).toBe(false) - }) - - it('rejects a physical-exit timeout but retains the owner for a successful retry', async () => { - vi.useFakeTimers() - try { - const killSpy = vi.fn() - mockProc.kill = killSpy - const { id } = await provider.spawn({ cols: 80, rows: 24 }) - - const shutdown = provider.shutdown(id, { immediate: true }) - const rejected = expect(shutdown).rejects.toThrow('Timed out waiting for PTY process exit') - await vi.advanceTimersByTimeAsync(LOCAL_PTY_PHYSICAL_EXIT_TIMEOUT_MS) - await rejected - expect(provider.hasPty(id)).toBe(true) - - const retry = provider.shutdown(id, { immediate: true }) - expect(killSpy).toHaveBeenCalledTimes(1) - exitCb?.({ exitCode: 137 }) - await expect(retry).resolves.toBeUndefined() - expect(provider.hasPty(id)).toBe(false) - } finally { - vi.useRealTimers() - } - }) - - it('propagates kill failure without dropping the physical owner', async () => { - mockProc.kill = vi.fn(() => { - throw new Error('kill denied') - }) - const { id } = await provider.spawn({ cols: 80, rows: 24 }) - - await expect(provider.shutdown(id, { immediate: true })).rejects.toThrow('kill denied') - expect(provider.hasPty(id)).toBe(true) - - mockProc.kill = vi.fn(() => exitCb?.({ exitCode: 137 })) - await expect(provider.shutdown(id, { immediate: true })).resolves.toBeUndefined() - expect(provider.hasPty(id)).toBe(false) - }) - - it('cancels pending shell-ready startup delivery on forced shutdown', async () => { - vi.useFakeTimers() - try { - const { id } = await provider.spawn({ cols: 80, rows: 24, command: 'printf ready' }) - - await provider.shutdown(id, { immediate: true }) - vi.advanceTimersByTime(2000) - await Promise.resolve() - - expect(mockProc.write).not.toHaveBeenCalled() - } finally { - vi.useRealTimers() - } - }) - - it('is a no-op for unknown PTY ids', async () => { - await provider.shutdown('nonexistent', { immediate: true }) - expect(mockProc.kill).not.toHaveBeenCalled() - }) - - it('waits for an in-flight agent shutdown before reusing the same session id', async () => { - let releaseSweep!: () => void - killWithDescendantSweepMock.mockImplementation( - (_rootPid: number, killRoot: () => void) => - new Promise((resolve) => { - releaseSweep = () => { - killRoot() - resolve() - } - }) - ) - const spawnArgs = { - cols: 80, - rows: 24, - sessionId: 'stable-agent-session', - launchAgent: 'claude' as const - } - const spawnCallsBefore = spawnMock.mock.calls.length - const { id } = await provider.spawn(spawnArgs) - - const shutdown = provider.shutdown(id, { immediate: true }) - const respawn = provider.spawn(spawnArgs) - await Promise.resolve() - expect(spawnMock).toHaveBeenCalledTimes(spawnCallsBefore + 1) - - releaseSweep() - await shutdown - await respawn - expect(spawnMock).toHaveBeenCalledTimes(spawnCallsBefore + 2) - }) - - it('coalesces duplicate shutdown while descendant sweep is pending', async () => { - let releaseSweep!: () => void - killWithDescendantSweepMock.mockImplementation( - (_rootPid: number, killRoot: () => void) => - new Promise((resolve) => { - releaseSweep = () => { - killRoot() - resolve() - } - }) - ) - const { id } = await provider.spawn({ - cols: 80, - rows: 24, - launchAgent: 'claude' - }) - - const first = provider.shutdown(id, { immediate: true }) - const second = provider.shutdown(id, { immediate: true }) - expect(killWithDescendantSweepMock).toHaveBeenCalledOnce() - releaseSweep() - await Promise.all([first, second]) - expect(killWithDescendantSweepMock).toHaveBeenCalledOnce() - }) - - it('does not terminate descendants after the tracked root exits mid-sweep', async () => { - const terminateDescendants = vi.fn() - let releaseSweep!: () => void - killWithDescendantSweepMock.mockImplementation( - (_rootPid: number, killRoot: () => void, deps?: { ownsRoot?: () => boolean }) => - new Promise((resolve) => { - releaseSweep = () => { - // Production killWithDescendantSweep only signals descendants while ownsRoot. - if (deps?.ownsRoot?.() ?? true) { - terminateDescendants() - } - killRoot() - resolve() - } - }) - ) - const { id } = await provider.spawn({ - cols: 80, - rows: 24, - launchAgent: 'claude' - }) - - const shutdown = provider.shutdown(id, { immediate: true }) - exitCb?.({ exitCode: 0 }) - releaseSweep() - await shutdown - - expect(terminateDescendants).not.toHaveBeenCalled() - }) - - it('win32 immediate shutdown of a plain shell taskkills the descendant tree', async () => { - Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) - const { id } = await provider.spawn({ cols: 80, rows: 24 }) - - await provider.shutdown(id, { immediate: true }) - - // Why: an orphaned pnpm/node child otherwise keeps the ConPTY console alive and holds - // the worktree cwd; the sweep taskkill /T /F clears the tree so removal can proceed. - expect(killWithDescendantSweepMock).toHaveBeenCalledWith( - mockProc.pid, - expect.any(Function), - expect.objectContaining({ ownsRoot: expect.any(Function) }) - ) - }) - - it('win32 graceful shutdown of a plain shell does not taskkill the tree', async () => { - Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) - const { id } = await provider.spawn({ cols: 80, rows: 24 }) - - await provider.shutdown(id, { immediate: false }) - - expect(killWithDescendantSweepMock).not.toHaveBeenCalled() - }) - - it('POSIX immediate shutdown sweeps detached OMP tools without startup recognition', async () => { - const { id } = await provider.spawn({ cols: 80, rows: 24 }) - - await provider.shutdown(id, { immediate: true }) - - expect(killWithDescendantSweepMock).toHaveBeenCalledWith( - mockProc.pid, - expect.any(Function), - expect.objectContaining({ ownsRoot: expect.any(Function) }) - ) - }) - }) - - describe('killAll', () => { - it('kills all PTY processes', async () => { - // Why: each spawn needs its own proc so the onExit-triggered POSIX kill - // neutralization on one proc does not replace the kill function on the - // other (mockProc is shared by default in beforeEach). Each proc also - // needs its own exitCb holder — the default mockProc.onExit assigns to - // the shared `exitCb` variable, so the second spawn would overwrite the - // first's exit callback, and mock1Kill firing would trigger cleanup for - // id2 (removing it from the map before killAll iterates to it). - let exit1: ((e: { exitCode: number }) => void) | undefined - let exit2: ((e: { exitCode: number }) => void) | undefined - const mock1Kill = vi.fn(() => exit1?.({ exitCode: -1 })) - const mock2Kill = vi.fn(() => exit2?.({ exitCode: -1 })) - spawnMock - .mockReturnValueOnce({ - ...mockProc, - kill: mock1Kill, - onExit: vi.fn((cb) => { - exit1 = cb - }) - }) - .mockReturnValueOnce({ - ...mockProc, - kill: mock2Kill, - onExit: vi.fn((cb) => { - exit2 = cb - }) - }) - - await provider.spawn({ cols: 80, rows: 24 }) - await provider.spawn({ cols: 80, rows: 24 }) - - provider.killAll() - - expect(mock1Kill).toHaveBeenCalled() - expect(mock2Kill).toHaveBeenCalled() - const list = await provider.listProcesses() - expect(list).toHaveLength(0) - }) - - it('force-kills an OMP PTY root during app quit', async () => { - const killSpy = vi.fn() - spawnMock.mockReturnValue({ - ...mockProc, - kill: killSpy - }) - - await provider.spawn({ cols: 80, rows: 24, launchAgent: 'omp' }) - - provider.killAll() - - expect(killSpy).toHaveBeenCalledWith('SIGKILL') - }) - - it('does not destroy after intentional Windows orphan kills', async () => { - Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) - const destroySpy = vi.fn() - const killSpy = vi.fn() - spawnMock.mockReturnValue({ - ...mockProc, - kill: killSpy, - destroy: destroySpy - }) - - await provider.spawn({ cols: 80, rows: 24 }) - - provider.killAll() - - expect(killSpy).toHaveBeenCalledTimes(1) - expect(destroySpy).not.toHaveBeenCalled() - }) - - it('settles an overlapping shutdown when app quit takes final ownership', async () => { - mockProc.kill.mockImplementation(() => undefined) - const { id } = await provider.spawn({ cols: 80, rows: 24 }) - const shutdown = provider.shutdown(id, { immediate: true }) - let settled = false - void shutdown.then(() => { - settled = true - }) - await Promise.resolve() - expect(settled).toBe(false) - - provider.killAll() - - await expect(shutdown).resolves.toBeUndefined() - }) - }) -}) diff --git a/src/main/providers/local-pty-provider-spawn-cwd-safety.test.ts b/src/main/providers/local-pty-provider-spawn-cwd-safety.test.ts deleted file mode 100644 index de7934138077..000000000000 --- a/src/main/providers/local-pty-provider-spawn-cwd-safety.test.ts +++ /dev/null @@ -1,269 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import type * as MacosTccLoginShell from './macos-tcc-login-shell' - -const { - existsSyncMock, - statSyncMock, - accessSyncMock, - mkdirSyncMock, - writeFileSyncMock, - spawnMock, - prepareMacosTccLoginShellMock, - resolveAgentForegroundProcessMock, - readWindowsPtyJobProcessIdsMock, - killWithDescendantSweepMock, - isWslAvailableAsyncMock, - wslUncDirectoryExistsMock, - createShellPromptReadinessProbeMock -} = vi.hoisted(() => ({ - existsSyncMock: vi.fn(), - statSyncMock: vi.fn(), - accessSyncMock: vi.fn(), - mkdirSyncMock: vi.fn(), - writeFileSyncMock: vi.fn(), - spawnMock: vi.fn(), - prepareMacosTccLoginShellMock: vi.fn(), - resolveAgentForegroundProcessMock: vi.fn(), - readWindowsPtyJobProcessIdsMock: vi.fn(), - killWithDescendantSweepMock: vi.fn(), - isWslAvailableAsyncMock: vi.fn(), - wslUncDirectoryExistsMock: vi.fn(), - createShellPromptReadinessProbeMock: vi.fn() -})) - -vi.mock('fs', () => ({ - existsSync: existsSyncMock, - statSync: statSyncMock, - accessSync: accessSyncMock, - mkdirSync: mkdirSyncMock, - writeFileSync: writeFileSyncMock, - chmodSync: vi.fn(), - renameSync: vi.fn(), - rmSync: vi.fn(), - constants: { X_OK: 1 } -})) - -vi.mock('electron', () => ({ - app: { - getPath: vi.fn(() => '/tmp/orca-user-data') - } -})) - -vi.mock('node-pty', () => ({ - spawn: spawnMock -})) - -vi.mock('./macos-tcc-login-shell', async (importOriginal) => ({ - ...(await importOriginal()), - prepareMacosTccLoginShell: prepareMacosTccLoginShellMock -})) - -vi.mock('../pty-descendant-termination', () => ({ - killWithDescendantSweep: killWithDescendantSweepMock -})) - -// Resolve PowerShell family names to deterministic absolute paths (the fs mock -// above otherwise makes every probe miss). The real resolver — which skips the -// Store App Execution Alias stub — is covered in -// windows-powershell-executable.test.ts. -const WINDOWS_POWERSHELL_ABS = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' -const PWSH7_ABS = 'C:\\Program Files\\PowerShell\\7\\pwsh.exe' -const CMD_ABS = 'C:\\Windows\\System32\\cmd.exe' -vi.mock('./windows-powershell-executable', () => ({ - resolveWindowsPowerShellExecutablePath: (family: 'pwsh.exe' | 'powershell.exe') => - family === 'pwsh.exe' ? PWSH7_ABS : WINDOWS_POWERSHELL_ABS, - resolveWindowsPowerShellSpawnChain: (family: 'pwsh.exe' | 'powershell.exe') => - family === 'pwsh.exe' - ? [PWSH7_ABS, WINDOWS_POWERSHELL_ABS, CMD_ABS] - : [WINDOWS_POWERSHELL_ABS, CMD_ABS], - getWindowsCmdPath: () => CMD_ABS -})) - -vi.mock('./agent-foreground-process', () => ({ - resolveAgentForegroundProcessWithAvailability: (...args: unknown[]) => - resolveAgentForegroundProcessMock(...args) -})) - -vi.mock('./windows-pty-job-membership', () => ({ - readWindowsPtyJobProcessIds: (...args: unknown[]) => readWindowsPtyJobProcessIdsMock(...args), - isWindowsPtyJobReadable: () => true -})) - -vi.mock('../wsl', () => ({ - parseWslPath: (path: string) => { - const match = path.match(/^\\\\wsl\.localhost\\([^\\]+)(.*)$/) - if (!match) { - return null - } - return { - distro: match[1], - linuxPath: (match[2] || '').replace(/\\/g, '/') || '/' - } - }, - toLinuxPath: (path: string) => path.replace(/^C:\\/i, '/mnt/c/').replace(/\\/g, '/'), - toWindowsWslPath: (path: string, distro: string) => - `\\\\wsl.localhost\\${distro}${path.replace(/\//g, '\\')}`, - getDefaultWslDistro: () => 'Ubuntu', - isWslAvailableAsync: () => isWslAvailableAsyncMock(), - // Why: WSL worktree validation now asks the distro; these tests use WSL UNC - // cwds that are meant to exist, so report them present without spawning wsl.exe. - wslUncDirectoryExists: (...args: unknown[]) => wslUncDirectoryExistsMock(...args) -})) - -vi.mock('../shell-prompt-readiness-probe', () => ({ - createShellPromptReadinessProbe: createShellPromptReadinessProbeMock -})) - -import { LocalPtyProvider } from './local-pty-provider' -import { - applyLocalPtyProviderMockDefaults, - createLocalPtyMockProcess, - installLocalPtyProviderEnvSandbox, - type LocalPtyMockProcess -} from './local-pty-provider-test-harness' -import { isRootLikePath } from './pty-path-safety' - -describe('LocalPtyProvider', () => { - let provider: LocalPtyProvider - let mockProc: LocalPtyMockProcess - let exitCb: ((info: { exitCode: number }) => void) | undefined - - installLocalPtyProviderEnvSandbox() - - beforeEach(() => { - applyLocalPtyProviderMockDefaults({ - existsSyncMock, - statSyncMock, - accessSyncMock, - mkdirSyncMock, - writeFileSyncMock, - prepareMacosTccLoginShellMock, - resolveAgentForegroundProcessMock, - readWindowsPtyJobProcessIdsMock, - killWithDescendantSweepMock, - isWslAvailableAsyncMock, - wslUncDirectoryExistsMock, - createShellPromptReadinessProbeMock - }) - - exitCb = undefined - mockProc = createLocalPtyMockProcess({ - get: () => exitCb, - set: (cb) => { - exitCb = cb - } - }) - spawnMock.mockReturnValue(mockProc) - - provider = new LocalPtyProvider() - }) - - describe('spawn', () => { - it('calls node-pty spawn with correct args', async () => { - await provider.spawn({ cols: 120, rows: 40, cwd: '/tmp' }) - expect(spawnMock).toHaveBeenCalledWith( - expect.any(String), - expect.any(Array), - expect.objectContaining({ - cols: 120, - rows: 40, - cwd: '/tmp' - }) - ) - }) - - it('throws when cwd does not exist', async () => { - existsSyncMock.mockImplementation((p: string) => p !== '/nonexistent') - await expect(provider.spawn({ cols: 80, rows: 24, cwd: '/nonexistent' })).rejects.toThrow( - 'does not exist' - ) - }) - - it('allows an explicitly requested plain shell at POSIX root', async () => { - await provider.spawn({ cols: 80, rows: 24, cwd: '/' }) - - expect(spawnMock).toHaveBeenCalledWith( - expect.any(String), - expect.any(Array), - expect.objectContaining({ cwd: '/' }) - ) - }) - - it('falls back to the safe default cwd for automatic agent startup without an explicit cwd', async () => { - spawnMock.mockClear() - const origHome = process.env.HOME - // Pin HOME so we assert the exact resolved candidate, not just non-root-ness — - // catches regressions where resolveSafePtyDefaultCwd picks an unintended home. - process.env.HOME = '/home/testuser' - - try { - // Why: an omitted cwd resolves to a guaranteed-safe default home (the - // guard only rejects root-like paths), so the agent must still launch. - await expect( - provider.spawn({ cols: 80, rows: 24, command: 'codex' }) - ).resolves.toBeDefined() - - const spawnCall = spawnMock.mock.calls.at(-1)! - expect(spawnCall[2].cwd).toBe('/home/testuser') - expect(isRootLikePath(spawnCall[2].cwd)).toBe(false) - } finally { - if (origHome === undefined) { - delete process.env.HOME - } else { - process.env.HOME = origHome - } - } - }) - - it('rejects automatic agent startup at POSIX root', async () => { - spawnMock.mockClear() - - await expect( - provider.spawn({ cols: 80, rows: 24, cwd: '/', command: 'claude' }) - ).rejects.toThrow(/requires a non-root workspace/) - - expect(spawnMock).not.toHaveBeenCalled() - }) - - it('combines HOMEDRIVE and HOMEPATH for Windows default cwd', async () => { - const platform = Object.getOwnPropertyDescriptor(process, 'platform') - const originalUserProfile = process.env.USERPROFILE - const originalHomeDrive = process.env.HOMEDRIVE - const originalHomePath = process.env.HOMEPATH - - Object.defineProperty(process, 'platform', { value: 'win32' }) - delete process.env.USERPROFILE - process.env.HOMEDRIVE = 'D:' - process.env.HOMEPATH = '\\Users\\orca' - - try { - await provider.spawn({ cols: 80, rows: 24 }) - } finally { - if (platform) { - Object.defineProperty(process, 'platform', platform) - } - if (originalUserProfile === undefined) { - delete process.env.USERPROFILE - } else { - process.env.USERPROFILE = originalUserProfile - } - if (originalHomeDrive === undefined) { - delete process.env.HOMEDRIVE - } else { - process.env.HOMEDRIVE = originalHomeDrive - } - if (originalHomePath === undefined) { - delete process.env.HOMEPATH - } else { - process.env.HOMEPATH = originalHomePath - } - } - - expect(spawnMock).toHaveBeenCalledWith( - expect.any(String), - expect.any(Array), - expect.objectContaining({ cwd: 'D:\\Users\\orca' }) - ) - }) - }) -}) diff --git a/src/main/providers/local-pty-provider-spawn-env.test.ts b/src/main/providers/local-pty-provider-spawn-env.test.ts deleted file mode 100644 index daf6286b56df..000000000000 --- a/src/main/providers/local-pty-provider-spawn-env.test.ts +++ /dev/null @@ -1,638 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { delimiter } from 'node:path' -import type * as MacosTccLoginShell from './macos-tcc-login-shell' -import { stripLegacyTerminalShimEnv } from '../pty/legacy-terminal-shim-dir' - -const { - existsSyncMock, - statSyncMock, - accessSyncMock, - mkdirSyncMock, - writeFileSyncMock, - spawnMock, - prepareMacosTccLoginShellMock, - resolveAgentForegroundProcessMock, - readWindowsPtyJobProcessIdsMock, - killWithDescendantSweepMock, - isWslAvailableAsyncMock, - wslUncDirectoryExistsMock, - createShellPromptReadinessProbeMock -} = vi.hoisted(() => ({ - existsSyncMock: vi.fn(), - statSyncMock: vi.fn(), - accessSyncMock: vi.fn(), - mkdirSyncMock: vi.fn(), - writeFileSyncMock: vi.fn(), - spawnMock: vi.fn(), - prepareMacosTccLoginShellMock: vi.fn(), - resolveAgentForegroundProcessMock: vi.fn(), - readWindowsPtyJobProcessIdsMock: vi.fn(), - killWithDescendantSweepMock: vi.fn(), - isWslAvailableAsyncMock: vi.fn(), - wslUncDirectoryExistsMock: vi.fn(), - createShellPromptReadinessProbeMock: vi.fn() -})) - -vi.mock('fs', () => ({ - existsSync: existsSyncMock, - statSync: statSyncMock, - accessSync: accessSyncMock, - mkdirSync: mkdirSyncMock, - writeFileSync: writeFileSyncMock, - chmodSync: vi.fn(), - renameSync: vi.fn(), - rmSync: vi.fn(), - constants: { X_OK: 1 } -})) - -vi.mock('electron', () => ({ - app: { - getPath: vi.fn(() => '/tmp/orca-user-data') - } -})) - -vi.mock('node-pty', () => ({ - spawn: spawnMock -})) - -vi.mock('./macos-tcc-login-shell', async (importOriginal) => ({ - ...(await importOriginal()), - prepareMacosTccLoginShell: prepareMacosTccLoginShellMock -})) - -vi.mock('../pty-descendant-termination', () => ({ - killWithDescendantSweep: killWithDescendantSweepMock -})) - -// Resolve PowerShell family names to deterministic absolute paths (the fs mock -// above otherwise makes every probe miss). The real resolver — which skips the -// Store App Execution Alias stub — is covered in -// windows-powershell-executable.test.ts. -const WINDOWS_POWERSHELL_ABS = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' -const PWSH7_ABS = 'C:\\Program Files\\PowerShell\\7\\pwsh.exe' -const CMD_ABS = 'C:\\Windows\\System32\\cmd.exe' -vi.mock('./windows-powershell-executable', () => ({ - resolveWindowsPowerShellExecutablePath: (family: 'pwsh.exe' | 'powershell.exe') => - family === 'pwsh.exe' ? PWSH7_ABS : WINDOWS_POWERSHELL_ABS, - resolveWindowsPowerShellSpawnChain: (family: 'pwsh.exe' | 'powershell.exe') => - family === 'pwsh.exe' - ? [PWSH7_ABS, WINDOWS_POWERSHELL_ABS, CMD_ABS] - : [WINDOWS_POWERSHELL_ABS, CMD_ABS], - getWindowsCmdPath: () => CMD_ABS -})) - -vi.mock('./agent-foreground-process', () => ({ - resolveAgentForegroundProcessWithAvailability: (...args: unknown[]) => - resolveAgentForegroundProcessMock(...args) -})) - -vi.mock('./windows-pty-job-membership', () => ({ - readWindowsPtyJobProcessIds: (...args: unknown[]) => readWindowsPtyJobProcessIdsMock(...args), - isWindowsPtyJobReadable: () => true -})) - -vi.mock('../wsl', () => ({ - parseWslPath: (path: string) => { - const match = path.match(/^\\\\wsl\.localhost\\([^\\]+)(.*)$/) - if (!match) { - return null - } - return { - distro: match[1], - linuxPath: (match[2] || '').replace(/\\/g, '/') || '/' - } - }, - toLinuxPath: (path: string) => path.replace(/^C:\\/i, '/mnt/c/').replace(/\\/g, '/'), - toWindowsWslPath: (path: string, distro: string) => - `\\\\wsl.localhost\\${distro}${path.replace(/\//g, '\\')}`, - getDefaultWslDistro: () => 'Ubuntu', - isWslAvailableAsync: () => isWslAvailableAsyncMock(), - // Why: WSL worktree validation now asks the distro; these tests use WSL UNC - // cwds that are meant to exist, so report them present without spawning wsl.exe. - wslUncDirectoryExists: (...args: unknown[]) => wslUncDirectoryExistsMock(...args) -})) - -vi.mock('../shell-prompt-readiness-probe', () => ({ - createShellPromptReadinessProbe: createShellPromptReadinessProbeMock -})) - -import { LocalPtyProvider } from './local-pty-provider' -import { - applyLocalPtyProviderMockDefaults, - createLocalPtyMockProcess, - installLocalPtyProviderEnvSandbox, - type LocalPtyMockProcess -} from './local-pty-provider-test-harness' - -describe('LocalPtyProvider', () => { - let provider: LocalPtyProvider - let mockProc: LocalPtyMockProcess - let exitCb: ((info: { exitCode: number }) => void) | undefined - - installLocalPtyProviderEnvSandbox() - - beforeEach(() => { - applyLocalPtyProviderMockDefaults({ - existsSyncMock, - statSyncMock, - accessSyncMock, - mkdirSyncMock, - writeFileSyncMock, - prepareMacosTccLoginShellMock, - resolveAgentForegroundProcessMock, - readWindowsPtyJobProcessIdsMock, - killWithDescendantSweepMock, - isWslAvailableAsyncMock, - wslUncDirectoryExistsMock, - createShellPromptReadinessProbeMock - }) - - exitCb = undefined - mockProc = createLocalPtyMockProcess({ - get: () => exitCb, - set: (cb) => { - exitCb = cb - } - }) - spawnMock.mockReturnValue(mockProc) - - provider = new LocalPtyProvider() - }) - - describe('spawn', () => { - it('expands variables in PATH before spawning a Windows shell', async () => { - Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) - - await provider.spawn({ - cols: 80, - rows: 24, - cwd: 'C:\\repo', - env: { - ORCA_AGENT_TEAMS_TEAM_ID: 'team-test', - ORCA_PATH_ROOT: 'C:\\Users\\orca\\AppData\\Local', - PATH: '%orca_path_root%\\agy\\bin;C:\\Windows' - } - }) - - expect(spawnMock.mock.calls.at(-1)?.[2].env.PATH).toBe( - 'C:\\Users\\orca\\AppData\\Local\\agy\\bin;C:\\Windows' - ) - }) - - it('passes explicit pane environment separately from inherited process values', async () => { - const buildSpawnEnv = vi.fn((_id: string, env: Record) => env) - provider.configure({ buildSpawnEnv }) - const env = { XDG_DATA_HOME: '/pane/data' } - await provider.spawn({ cols: 80, rows: 24, env }) - expect(buildSpawnEnv).toHaveBeenCalledWith( - expect.any(String), - expect.any(Object), - expect.objectContaining({ explicitEnv: env }) - ) - }) - - it('invokes buildSpawnEnv callback to customize environment', async () => { - const buildSpawnEnv = vi.fn((_id: string, env: Record) => { - env.CUSTOM_VAR = 'custom-value' - return env - }) - provider.configure({ buildSpawnEnv }) - await provider.spawn({ cols: 80, rows: 24 }) - - const spawnCall = spawnMock.mock.calls.at(-1)! - expect(spawnCall[2].env.CUSTOM_VAR).toBe('custom-value') - }) - - it('re-reads buildSpawnEnv after a reentrant configuration change', async () => { - const initialBuildSpawnEnv = vi.fn((_id: string, env: Record) => env) - const configuredBuildSpawnEnv = vi.fn((_id: string, env: Record) => env) - provider.configure({ - get buildSpawnEnv() { - provider.configure({ buildSpawnEnv: configuredBuildSpawnEnv }) - return initialBuildSpawnEnv - } - }) - - await provider.spawn({ cols: 80, rows: 24 }) - - expect(initialBuildSpawnEnv).not.toHaveBeenCalled() - expect(configuredBuildSpawnEnv).toHaveBeenCalledOnce() - }) - - it.each([ - // fish EXPORTS fish_history, so an Orca launched from a fish pane hands every - // pane the LAUNCHING worktree's session — even with isolation off (STA-4682). - ['an inherited Orca session', 'orca_abc123', undefined], - ['a user value', 'mine', 'mine'] - ])('history isolation off: %s', async (_kind, inherited, expected) => { - const previous = process.env.fish_history - process.env.fish_history = inherited - try { - // No worktreeId: the history-disabled branch of spawn. - await provider.spawn({ cols: 80, rows: 24 }) - } finally { - if (previous === undefined) { - delete process.env.fish_history - } else { - process.env.fish_history = previous - } - } - - expect(spawnMock.mock.calls.at(-1)![2].env.fish_history).toBe(expected) - }) - - it.each([ - // HISTFILE is exported, so an Orca launched from a pane in another worktree - // hands every pane that worktree's history file — isolation off included. - [ - 'an inherited Orca path', - '/fake/userData/terminal-history/aabbccddeeff0011/zsh_history', - undefined - ], - ['a user value', '/home/me/.zsh_history', '/home/me/.zsh_history'] - ])('history isolation off: %s HISTFILE', async (_kind, inherited, expected) => { - const previous = process.env.HISTFILE - process.env.HISTFILE = inherited - try { - // No worktreeId: the history-disabled branch of spawn. - await provider.spawn({ cols: 80, rows: 24 }) - } finally { - if (previous === undefined) { - delete process.env.HISTFILE - } else { - process.env.HISTFILE = previous - } - } - - expect(spawnMock.mock.calls.at(-1)![2].env.HISTFILE).toBe(expected) - }) - - it('does not inherit NODE_ENV from the Orca process env', async () => { - // Why: NODE_ENV in Orca's process is Orca's build mode (electron-vite sets - // `development` in dev runs); leaking it breaks `next build` and Vitest. - const previous = process.env.NODE_ENV - process.env.NODE_ENV = 'development' - try { - await provider.spawn({ cols: 80, rows: 24 }) - } finally { - if (previous === undefined) { - delete process.env.NODE_ENV - } else { - process.env.NODE_ENV = previous - } - } - - const spawnCall = spawnMock.mock.calls.at(-1)! - expect(spawnCall[2].env.NODE_ENV).toBeUndefined() - const expectedEnv = { PATH: process.env.PATH ?? '' } - stripLegacyTerminalShimEnv(expectedEnv, process.platform) - expect(spawnCall[2].env.PATH).toBe(expectedEnv.PATH) - }) - - it('keeps an explicitly requested NODE_ENV for spawned terminals', async () => { - // Why: only the ambient value is stripped; a caller-supplied NODE_ENV still wins. - const previous = process.env.NODE_ENV - process.env.NODE_ENV = 'development' - try { - await provider.spawn({ cols: 80, rows: 24, env: { NODE_ENV: 'production' } }) - } finally { - if (previous === undefined) { - delete process.env.NODE_ENV - } else { - process.env.NODE_ENV = previous - } - } - - const spawnCall = spawnMock.mock.calls.at(-1)! - expect(spawnCall[2].env.NODE_ENV).toBe('production') - }) - - it('suppresses the first-run Powerlevel10k wizard for spawned terminals', async () => { - await provider.spawn({ cols: 80, rows: 24 }) - - const spawnCall = spawnMock.mock.calls.at(-1)! - expect(spawnCall[2].env.POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD).toBe('true') - }) - - it('preserves an explicit Powerlevel10k wizard env value', async () => { - await provider.spawn({ - cols: 80, - rows: 24, - env: { POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD: 'already-set' } - }) - - const spawnCall = spawnMock.mock.calls.at(-1)! - expect(spawnCall[2].env.POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD).toBe('already-set') - }) - - it('honors requests to delete the Powerlevel10k wizard env value', async () => { - await provider.spawn({ - cols: 80, - rows: 24, - envToDelete: ['POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD'] - }) - - const spawnCall = spawnMock.mock.calls.at(-1)! - expect(spawnCall[2].env.POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD).toBeUndefined() - }) - - it('honors explicit terminal env overrides after deleting requested defaults', async () => { - provider.configure({ - buildSpawnEnv: (_id, env) => { - env.TERM_PROGRAM = 'Orca' - env.ORCA_STALE_TEST_ENV = '/tmp/orca-stale' - env.PATH = `/tmp/orca-stale:${env.PATH ?? ''}` - return env - } - }) - - await provider.spawn({ - cols: 80, - rows: 24, - env: { - TERM: 'screen-256color', - PATH: '/tmp/orca-agent-teams-bin:/usr/bin', - ORCA_AGENT_TEAMS_TEAM_ID: 'team-test' - }, - envToDelete: ['TERM_PROGRAM', 'ORCA_STALE_TEST_ENV'] - }) - - const spawnCall = spawnMock.mock.calls.at(-1)! - expect(spawnCall[2].name).toBe('screen-256color') - expect(spawnCall[2].env.TERM).toBe('screen-256color') - expect(spawnCall[2].env.PATH.split(':')[0]).toBe('/tmp/orca-agent-teams-bin') - expect(spawnCall[2].env.TERM_PROGRAM).toBeUndefined() - expect(spawnCall[2].env.ORCA_STALE_TEST_ENV).toBeUndefined() - }) - - it('does not re-promote a legacy attribution path for Agent Teams', async () => { - await provider.spawn({ - cols: 80, - rows: 24, - env: { - PATH: '/tmp/orca-terminal-attribution/posix:/usr/bin', - ORCA_AGENT_TEAMS_TEAM_ID: 'team-test' - } - }) - - expect(spawnMock.mock.calls.at(-1)?.[2].env.PATH).toBe('/usr/bin') - }) - - it('drops stale inherited Git config indices behind a smaller explicit count', async () => { - const keys = [ - 'GIT_CONFIG_COUNT', - 'GIT_CONFIG_KEY_0', - 'GIT_CONFIG_VALUE_0', - 'GIT_CONFIG_KEY_1', - 'GIT_CONFIG_VALUE_1' - ] as const - const saved = Object.fromEntries(keys.map((key) => [key, process.env[key]])) - process.env.GIT_CONFIG_COUNT = '2' - process.env.GIT_CONFIG_KEY_0 = 'base.zero' - process.env.GIT_CONFIG_VALUE_0 = 'zero' - process.env.GIT_CONFIG_KEY_1 = 'base.one' - process.env.GIT_CONFIG_VALUE_1 = 'one' - - try { - await provider.spawn({ - cols: 80, - rows: 24, - env: { - GIT_CONFIG_COUNT: '1', - GIT_CONFIG_KEY_0: 'override.zero', - GIT_CONFIG_VALUE_0: 'override' - } - }) - - const spawnEnv = spawnMock.mock.calls.at(-1)?.[2]?.env as Record - expect(spawnEnv.GIT_CONFIG_COUNT).toBe('1') - expect(spawnEnv.GIT_CONFIG_KEY_0).toBe('override.zero') - expect(spawnEnv.GIT_CONFIG_KEY_1).toBeUndefined() - expect(spawnEnv.GIT_CONFIG_VALUE_1).toBeUndefined() - } finally { - for (const key of keys) { - if (saved[key] === undefined) { - delete process.env[key] - } else { - process.env[key] = saved[key] - } - } - } - }) - - it('does not inherit AppImage runtime env into Linux PTY shells', async () => { - const saved = { - APPIMAGE: process.env.APPIMAGE, - APPDIR: process.env.APPDIR, - ARGV0: process.env.ARGV0, - OWD: process.env.OWD, - APPIMAGE_LIBRARY_PATH: process.env.APPIMAGE_LIBRARY_PATH, - PATH: process.env.PATH, - LD_LIBRARY_PATH: process.env.LD_LIBRARY_PATH - } - process.env.APPIMAGE = '/data/apps/orca.appimage' - process.env.APPDIR = '/tmp/.mount_orca123' - process.env.ARGV0 = '/data/apps/orca.appimage' - process.env.OWD = '/home/user/project' - process.env.APPIMAGE_LIBRARY_PATH = '/tmp/.mount_orca123/usr/lib' - process.env.PATH = ['/tmp/.mount_orca123', '/tmp/.mount_orca123/usr/sbin', '/usr/bin'].join( - delimiter - ) - process.env.LD_LIBRARY_PATH = ['/tmp/.mount_orca123/usr/lib', '/opt/audio/lib'].join( - delimiter - ) - - try { - await provider.spawn({ cols: 80, rows: 24 }) - } finally { - for (const [key, value] of Object.entries(saved)) { - if (value === undefined) { - delete process.env[key] - } else { - process.env[key] = value - } - } - } - - const env = spawnMock.mock.calls.at(-1)?.[2].env - expect(env.APPIMAGE).toBeUndefined() - expect(env.APPDIR).toBeUndefined() - expect(env.ARGV0).toBeUndefined() - expect(env.OWD).toBeUndefined() - expect(env.APPIMAGE_LIBRARY_PATH).toBeUndefined() - expect(env.PATH).toBe('/usr/bin') - expect(env.LD_LIBRARY_PATH).toBe('/opt/audio/lib') - }) - - it('does not forward a half-activated conda env into the shell', async () => { - // Why: CONDA_SHLVL asserts CONDA_PREFIX exists; forwarding the sentinel - // alone makes the user's rc-file conda hook raise a TypeError (#14195). - const saved = { - CONDA_SHLVL: process.env.CONDA_SHLVL, - CONDA_PREFIX: process.env.CONDA_PREFIX, - CONDA_DEFAULT_ENV: process.env.CONDA_DEFAULT_ENV, - CONDA_PROMPT_MODIFIER: process.env.CONDA_PROMPT_MODIFIER, - CONDA_EXE: process.env.CONDA_EXE - } - delete process.env.CONDA_PREFIX - process.env.CONDA_SHLVL = '1' - process.env.CONDA_DEFAULT_ENV = 'base' - process.env.CONDA_PROMPT_MODIFIER = '(base) ' - process.env.CONDA_EXE = '/opt/miniconda3/bin/conda' - - try { - await provider.spawn({ cols: 80, rows: 24 }) - } finally { - for (const [key, value] of Object.entries(saved)) { - if (value === undefined) { - delete process.env[key] - } else { - process.env[key] = value - } - } - } - - const env = spawnMock.mock.calls.at(-1)?.[2].env - expect(env.CONDA_SHLVL).toBeUndefined() - expect(env.CONDA_DEFAULT_ENV).toBeUndefined() - expect(env.CONDA_PROMPT_MODIFIER).toBeUndefined() - expect(env.CONDA_EXE).toBe('/opt/miniconda3/bin/conda') - }) - - it('drops the conda sentinel when the client asks to delete CONDA_PREFIX', async () => { - // Why: envToDelete runs after the inherited-env scrub, so the coherence - // pass must run last or it re-creates the exact broken pair it prevents. - const saved = { - CONDA_SHLVL: process.env.CONDA_SHLVL, - CONDA_PREFIX: process.env.CONDA_PREFIX, - CONDA_DEFAULT_ENV: process.env.CONDA_DEFAULT_ENV - } - process.env.CONDA_SHLVL = '1' - process.env.CONDA_PREFIX = '/opt/miniconda3' - process.env.CONDA_DEFAULT_ENV = 'base' - - try { - await provider.spawn({ cols: 80, rows: 24, envToDelete: ['CONDA_PREFIX'] }) - } finally { - for (const [key, value] of Object.entries(saved)) { - if (value === undefined) { - delete process.env[key] - } else { - process.env[key] = value - } - } - } - - const env = spawnMock.mock.calls.at(-1)?.[2].env - expect(env.CONDA_PREFIX).toBeUndefined() - expect(env.CONDA_SHLVL).toBeUndefined() - expect(env.CONDA_DEFAULT_ENV).toBeUndefined() - }) - - it('uses shell wrapper when MiMo home must survive shell startup', async () => { - provider.configure({ - buildSpawnEnv: (_id, env) => { - env.MIMOCODE_HOME = '/tmp/orca-mimocode-overlay' - env.ORCA_MIMOCODE_HOME = '/tmp/orca-mimocode-overlay' - return env - } - }) - - await provider.spawn({ cols: 80, rows: 24 }) - - const spawnCall = spawnMock.mock.calls.at(-1)! - expect(spawnCall[1]).toEqual(['-l']) - expect(spawnCall[2].env.ZDOTDIR).toMatch(/shell-ready[\\/]zsh/) - expect(spawnCall[2].env.ORCA_SHELL_FEATURES).not.toContain('ready') - }) - - it('promotes the agent-teams shim onto the Windows `Path` spelling', async () => { - Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) - provider.configure({ - buildSpawnEnv: (_id, env) => { - // Why: host env collapses Windows PATH onto `Path` and prepends its own shim dir. - delete env.PATH - env.Path = `/tmp/orca-stale:${env.Path ?? ''}` - return env - } - }) - - await provider.spawn({ - cols: 80, - rows: 24, - env: { - Path: '/tmp/orca-agent-teams-bin:/usr/bin', - ORCA_AGENT_TEAMS_TEAM_ID: 'team-test' - } - }) - - const spawnEnv = spawnMock.mock.calls.at(-1)![2].env - expect(Object.keys(spawnEnv).filter((key) => /^path$/i.test(key))).toEqual(['Path']) - expect(spawnEnv.Path.split(':')[0]).toBe('/tmp/orca-agent-teams-bin') - }) - - it('does not inherit parent Orca pane identity when caller omits pane env', async () => { - const saved = { - ORCA_PANE_KEY: process.env.ORCA_PANE_KEY, - ORCA_TAB_ID: process.env.ORCA_TAB_ID, - ORCA_WORKTREE_ID: process.env.ORCA_WORKTREE_ID - } - process.env.ORCA_PANE_KEY = 'parent-tab:parent-leaf' - process.env.ORCA_TAB_ID = 'parent-tab' - process.env.ORCA_WORKTREE_ID = 'parent-worktree' - - try { - await provider.spawn({ cols: 80, rows: 24 }) - } finally { - for (const [key, value] of Object.entries(saved)) { - if (value === undefined) { - delete process.env[key] - } else { - process.env[key] = value - } - } - } - - const spawnCall = spawnMock.mock.calls.at(-1)! - expect(spawnCall[2].env.ORCA_PANE_KEY).toBeUndefined() - expect(spawnCall[2].env.ORCA_TAB_ID).toBeUndefined() - expect(spawnCall[2].env.ORCA_WORKTREE_ID).toBeUndefined() - }) - - it('preserves explicit child Orca pane identity over parent env', async () => { - const saved = { - ORCA_PANE_KEY: process.env.ORCA_PANE_KEY, - ORCA_TAB_ID: process.env.ORCA_TAB_ID, - ORCA_WORKTREE_ID: process.env.ORCA_WORKTREE_ID - } - process.env.ORCA_PANE_KEY = 'parent-tab:parent-leaf' - process.env.ORCA_TAB_ID = 'parent-tab' - process.env.ORCA_WORKTREE_ID = 'parent-worktree' - - try { - await provider.spawn({ - cols: 80, - rows: 24, - env: { - ORCA_PANE_KEY: 'child-tab:child-leaf', - ORCA_TAB_ID: 'child-tab', - ORCA_WORKTREE_ID: 'child-worktree' - } - }) - } finally { - for (const [key, value] of Object.entries(saved)) { - if (value === undefined) { - delete process.env[key] - } else { - process.env[key] = value - } - } - } - - const spawnCall = spawnMock.mock.calls.at(-1)! - expect(spawnCall[2].env.ORCA_PANE_KEY).toBe('child-tab:child-leaf') - expect(spawnCall[2].env.ORCA_TAB_ID).toBe('child-tab') - expect(spawnCall[2].env.ORCA_WORKTREE_ID).toBe('child-worktree') - }) - }) -}) diff --git a/src/main/providers/local-pty-provider-spawn-session.test.ts b/src/main/providers/local-pty-provider-spawn-session.test.ts deleted file mode 100644 index cf523a469fca..000000000000 --- a/src/main/providers/local-pty-provider-spawn-session.test.ts +++ /dev/null @@ -1,613 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import type * as MacosTccLoginShell from './macos-tcc-login-shell' - -const { - existsSyncMock, - statSyncMock, - accessSyncMock, - mkdirSyncMock, - writeFileSyncMock, - spawnMock, - prepareMacosTccLoginShellMock, - resolveAgentForegroundProcessMock, - readWindowsPtyJobProcessIdsMock, - killWithDescendantSweepMock, - isWslAvailableAsyncMock, - wslUncDirectoryExistsMock, - createShellPromptReadinessProbeMock -} = vi.hoisted(() => ({ - existsSyncMock: vi.fn(), - statSyncMock: vi.fn(), - accessSyncMock: vi.fn(), - mkdirSyncMock: vi.fn(), - writeFileSyncMock: vi.fn(), - spawnMock: vi.fn(), - prepareMacosTccLoginShellMock: vi.fn(), - resolveAgentForegroundProcessMock: vi.fn(), - readWindowsPtyJobProcessIdsMock: vi.fn(), - killWithDescendantSweepMock: vi.fn(), - isWslAvailableAsyncMock: vi.fn(), - wslUncDirectoryExistsMock: vi.fn(), - createShellPromptReadinessProbeMock: vi.fn() -})) - -vi.mock('fs', () => ({ - existsSync: existsSyncMock, - statSync: statSyncMock, - accessSync: accessSyncMock, - mkdirSync: mkdirSyncMock, - writeFileSync: writeFileSyncMock, - chmodSync: vi.fn(), - renameSync: vi.fn(), - rmSync: vi.fn(), - constants: { X_OK: 1 } -})) - -vi.mock('electron', () => ({ - app: { - getPath: vi.fn(() => '/tmp/orca-user-data') - } -})) - -vi.mock('node-pty', () => ({ - spawn: spawnMock -})) - -vi.mock('../daemon/pty-subprocess/bun-pty-process-capabilities', () => ({ - canUseBunPty: () => false -})) - -vi.mock('./macos-tcc-login-shell', async (importOriginal) => ({ - ...(await importOriginal()), - prepareMacosTccLoginShell: prepareMacosTccLoginShellMock -})) - -vi.mock('../pty-descendant-termination', () => ({ - killWithDescendantSweep: killWithDescendantSweepMock -})) - -// Resolve PowerShell family names to deterministic absolute paths (the fs mock -// above otherwise makes every probe miss). The real resolver — which skips the -// Store App Execution Alias stub — is covered in -// windows-powershell-executable.test.ts. -const WINDOWS_POWERSHELL_ABS = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' -const PWSH7_ABS = 'C:\\Program Files\\PowerShell\\7\\pwsh.exe' -const CMD_ABS = 'C:\\Windows\\System32\\cmd.exe' -vi.mock('./windows-powershell-executable', () => ({ - resolveWindowsPowerShellExecutablePath: (family: 'pwsh.exe' | 'powershell.exe') => - family === 'pwsh.exe' ? PWSH7_ABS : WINDOWS_POWERSHELL_ABS, - resolveWindowsPowerShellSpawnChain: (family: 'pwsh.exe' | 'powershell.exe') => - family === 'pwsh.exe' - ? [PWSH7_ABS, WINDOWS_POWERSHELL_ABS, CMD_ABS] - : [WINDOWS_POWERSHELL_ABS, CMD_ABS], - getWindowsCmdPath: () => CMD_ABS -})) - -vi.mock('./agent-foreground-process', () => ({ - resolveAgentForegroundProcessWithAvailability: (...args: unknown[]) => - resolveAgentForegroundProcessMock(...args) -})) - -vi.mock('./windows-pty-job-membership', () => ({ - readWindowsPtyJobProcessIds: (...args: unknown[]) => readWindowsPtyJobProcessIdsMock(...args), - isWindowsPtyJobReadable: () => true -})) - -vi.mock('../wsl', () => ({ - parseWslPath: (path: string) => { - const match = path.match(/^\\\\wsl\.localhost\\([^\\]+)(.*)$/) - if (!match) { - return null - } - return { - distro: match[1], - linuxPath: (match[2] || '').replace(/\\/g, '/') || '/' - } - }, - toLinuxPath: (path: string) => path.replace(/^C:\\/i, '/mnt/c/').replace(/\\/g, '/'), - toWindowsWslPath: (path: string, distro: string) => - `\\\\wsl.localhost\\${distro}${path.replace(/\//g, '\\')}`, - getDefaultWslDistro: () => 'Ubuntu', - isWslAvailableAsync: () => isWslAvailableAsyncMock(), - // Why: WSL worktree validation now asks the distro; these tests use WSL UNC - // cwds that are meant to exist, so report them present without spawning wsl.exe. - wslUncDirectoryExists: (...args: unknown[]) => wslUncDirectoryExistsMock(...args) -})) - -vi.mock('../shell-prompt-readiness-probe', () => ({ - createShellPromptReadinessProbe: createShellPromptReadinessProbeMock -})) - -import { LocalPtyProvider } from './local-pty-provider' -import { - pendingLocalPtySpawns, - ptyDisposables, - ptyExitDisposables, - ptyPhysicalExits, - startupIngressByPty -} from './local-pty-provider-state' -import { - applyLocalPtyProviderMockDefaults, - createLocalPtyMockProcess, - installLocalPtyProviderEnvSandbox, - type LocalPtyMockProcess -} from './local-pty-provider-test-harness' - -describe('LocalPtyProvider', () => { - let provider: LocalPtyProvider - let mockProc: LocalPtyMockProcess - let exitCb: ((info: { exitCode: number }) => void) | undefined - - installLocalPtyProviderEnvSandbox() - - beforeEach(() => { - applyLocalPtyProviderMockDefaults({ - existsSyncMock, - statSyncMock, - accessSyncMock, - mkdirSyncMock, - writeFileSyncMock, - prepareMacosTccLoginShellMock, - resolveAgentForegroundProcessMock, - readWindowsPtyJobProcessIdsMock, - killWithDescendantSweepMock, - isWslAvailableAsyncMock, - wslUncDirectoryExistsMock, - createShellPromptReadinessProbeMock - }) - - exitCb = undefined - mockProc = createLocalPtyMockProcess({ - get: () => exitCb, - set: (cb) => { - exitCb = cb - } - }) - spawnMock.mockReturnValue(mockProc) - - provider = new LocalPtyProvider() - }) - - describe('spawn', () => { - it('returns a unique PTY id', async () => { - const result = await provider.spawn({ cols: 80, rows: 24 }) - expect(result.id).toBeTruthy() - expect(typeof result.id).toBe('string') - }) - - it('retires buffered output and synchronous Bun exit before replying to spawn', async () => { - const disposeData = vi.fn() - const disposeExit = vi.fn() - const onExit = vi.fn() - provider.configure({ onExit }) - mockProc.onData.mockImplementation((listener: (data: string) => void) => { - listener('last output') - return { dispose: disposeData } - }) - mockProc.onExit.mockImplementation((listener: (event: { exitCode: number }) => void) => { - listener({ exitCode: 17 }) - return { dispose: disposeExit } - }) - const result = await provider.spawn({ - cols: 80, - rows: 24, - sessionId: 'already-exited-bun-shell', - command: 'must-not-run' - }) - expect(result.exitedBeforeSpawnReply).toBe(true) - expect(provider.getPtyProcess(result.id)).toBeUndefined() - for (const map of [ - ptyDisposables, - ptyExitDisposables, - ptyPhysicalExits, - startupIngressByPty - ]) { - expect(map.has(result.id)).toBe(false) - } - expect(disposeData).toHaveBeenCalledOnce() - expect(disposeExit).toHaveBeenCalledOnce() - expect(onExit).toHaveBeenCalledOnce() - await Promise.resolve() - expect(mockProc.write).not.toHaveBeenCalled() - }) - - it('reattaches to an existing caller-supplied session id without spawning', async () => { - const first = await provider.spawn({ cols: 80, rows: 24, sessionId: 'serve-session-1' }) - spawnMock.mockClear() - - const second = await provider.spawn({ cols: 120, rows: 40, sessionId: first.id }) - - expect(second).toEqual({ - id: 'serve-session-1', - incarnationId: first.incarnationId, - pid: 12345, - isReattach: true, - // Why published: this attach really moved the PTY, unlike daemon/relay attach, so main - // must record 120x40 rather than preserving the size it held for the session. - attachedGrid: { cols: 120, rows: 40 } - }) - expect(mockProc.resize).toHaveBeenCalledWith(120, 40) - expect(spawnMock).not.toHaveBeenCalled() - }) - - it('attaches only to an existing stable session', async () => { - await provider.spawn({ cols: 80, rows: 24, sessionId: 'stable-pane-session' }) - spawnMock.mockClear() - - const result = await provider.spawn({ - cols: 120, - rows: 40, - sessionId: 'stable-pane-session', - attachOnly: true - }) - - expect(result).toMatchObject({ id: 'stable-pane-session', isReattach: true }) - expect(spawnMock).not.toHaveBeenCalled() - }) - - it('does not create when an attach-only stable session is absent', async () => { - await expect( - provider.spawn({ - cols: 80, - rows: 24, - sessionId: 'missing-stable-pane-session', - attachOnly: true - }) - ).rejects.toThrow('Session not found: missing-stable-pane-session') - expect(spawnMock).not.toHaveBeenCalled() - }) - - it('attaches only to an existing numeric provider session', async () => { - const first = await provider.spawn({ cols: 80, rows: 24 }) - spawnMock.mockClear() - - const result = await provider.spawn({ - cols: 120, - rows: 40, - sessionId: first.id, - attachOnly: true - }) - - expect(result).toMatchObject({ - id: first.id, - incarnationId: first.incarnationId, - isReattach: true - }) - expect(spawnMock).not.toHaveBeenCalled() - }) - - it('does not create when a numeric attach-only provider session is absent', async () => { - await expect( - provider.spawn({ cols: 80, rows: 24, sessionId: '404', attachOnly: true }) - ).rejects.toThrow('Session not found: 404') - expect(spawnMock).not.toHaveBeenCalled() - }) - - it('keeps a native UNC session native on a conflicting WSL reattach', async () => { - Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) - const first = await provider.spawn({ - cols: 80, - rows: 24, - sessionId: 'native-session', - cwd: '\\\\server\\share\\repo', - shellOverride: 'powershell.exe' - }) - spawnMock.mockClear() - - const second = await provider.spawn({ - cols: 120, - rows: 40, - sessionId: first.id, - shellOverride: 'wsl.exe', - terminalWindowsWslDistro: 'Ubuntu' - }) - - expect(first.wslDistro).toBeNull() - expect(second.wslDistro).toBeNull() - expect(spawnMock).not.toHaveBeenCalled() - }) - - it('keeps the first WSL distro on a conflicting distro reattach', async () => { - Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) - const first = await provider.spawn({ - cols: 80, - rows: 24, - sessionId: 'wsl-session', - cwd: '\\\\wsl.localhost\\Ubuntu\\home\\jin\\repo' - }) - spawnMock.mockClear() - - const second = await provider.spawn({ - cols: 120, - rows: 40, - sessionId: first.id, - shellOverride: 'wsl.exe', - terminalWindowsWslDistro: 'Debian' - }) - - expect(first.wslDistro).toBe('Ubuntu') - expect(second.wslDistro).toBe('Ubuntu') - expect(spawnMock).not.toHaveBeenCalled() - }) - - it('does not reattach numeric caller session ids that can collide after restart', async () => { - const first = await provider.spawn({ cols: 80, rows: 24 }) - spawnMock.mockClear() - - const second = await provider.spawn({ cols: 120, rows: 40, sessionId: first.id }) - - expect(second.id).not.toBe(first.id) - expect(second.isReattach).toBeUndefined() - expect(spawnMock).toHaveBeenCalledOnce() - }) - - it('does not spawn after shutdown cancels a pending stable session id', async () => { - let finishPreparation!: () => void - spawnMock.mockClear() - prepareMacosTccLoginShellMock.mockImplementationOnce( - () => - new Promise((resolve) => { - finishPreparation = resolve - }) - ) - - const spawn = provider.spawn({ - cols: 80, - rows: 24, - sessionId: 'pending-local-session' - }) - const canceledSpawn = expect(spawn).rejects.toThrow( - 'PTY spawn canceled: pending-local-session' - ) - await vi.waitFor(() => expect(prepareMacosTccLoginShellMock).toHaveBeenCalledOnce()) - - await provider.shutdown('pending-local-session', { immediate: true }) - finishPreparation() - await canceledSpawn - expect(spawnMock).not.toHaveBeenCalled() - - await expect( - provider.spawn({ cols: 80, rows: 24, sessionId: 'pending-local-session' }) - ).resolves.toMatchObject({ id: 'pending-local-session' }) - expect(spawnMock).toHaveBeenCalledOnce() - }) - - it('cancels an immediate stable-session shutdown before any PTY is spawned', async () => { - spawnMock.mockClear() - - const spawn = provider.spawn({ - cols: 80, - rows: 24, - sessionId: 'immediate-shutdown-session' - }) - await provider.shutdown('immediate-shutdown-session', { immediate: true }) - - await expect(spawn).rejects.toThrow('PTY spawn canceled: immediate-shutdown-session') - expect(spawnMock).not.toHaveBeenCalled() - }) - - // Why (#16441): the Codex hook install and trust grant moved into - // buildSpawnEnv, so the env build is now the long await before node-pty - // exists — shutdown must be able to cancel the session id during it. - it('does not spawn after shutdown cancels a pending spawn during the env build', async () => { - spawnMock.mockClear() - let finishEnvBuild!: (env: Record) => void - const buildSpawnEnv = vi.fn( - (_id: string, baseEnv: Record) => - new Promise>((resolve) => { - finishEnvBuild = () => resolve(baseEnv) - }) - ) - const envProvider = new LocalPtyProvider({ buildSpawnEnv }) - - const spawn = envProvider.spawn({ cols: 80, rows: 24, sessionId: 'env-build-session' }) - const canceledSpawn = expect(spawn).rejects.toThrow('PTY spawn canceled: env-build-session') - await vi.waitFor(() => expect(buildSpawnEnv).toHaveBeenCalledOnce()) - - await envProvider.shutdown('env-build-session', { immediate: true }) - finishEnvBuild({}) - await canceledSpawn - expect(spawnMock).not.toHaveBeenCalled() - }) - - it.each([1, 2])( - 'keeps cancellation registered across %i environment-resume microtasks', - async (microtasks) => { - spawnMock.mockClear() - let finishEnvBuild!: () => void - const envProvider = new LocalPtyProvider({ - buildSpawnEnv: (_id, baseEnv) => - new Promise>((resolve) => { - finishEnvBuild = () => resolve(baseEnv) - }) - }) - const spawn = envProvider.spawn({ - cols: 80, - rows: 24, - sessionId: 'resolved-env-build-session' - }) - const canceledSpawn = expect(spawn).rejects.toThrow( - 'PTY spawn canceled: resolved-env-build-session' - ) - await vi.waitFor(() => expect(finishEnvBuild).toBeTypeOf('function')) - - finishEnvBuild() - const shutdown = new Promise((resolve, reject) => { - const shutdown = () => { - envProvider - .shutdown('resolved-env-build-session', { immediate: true }) - .then(resolve, reject) - } - queueMicrotask(() => (microtasks === 1 ? shutdown() : queueMicrotask(shutdown))) - }) - - await shutdown - await canceledSpawn - expect(spawnMock).not.toHaveBeenCalled() - } - ) - - it.each([1, 2])( - 'settles final-preflight shutdown without a late PTY (%i microtasks)', - async (microtasks) => { - spawnMock.mockClear() - let finishPreparation!: () => void - prepareMacosTccLoginShellMock.mockImplementationOnce( - () => - new Promise((resolve) => { - finishPreparation = resolve - }) - ) - const id = 'preflight-resume-session' - const kill = mockProc.kill - let committed = false - const outcome = provider - .spawn({ - cols: 80, - rows: 24, - sessionId: id, - onPtySpawnCommitted: () => { - committed = true - } - }) - .then( - (result) => ({ ok: true as const, result }), - (error: unknown) => ({ ok: false as const, error }) - ) - await import('node-pty') - await vi.waitFor(() => expect(finishPreparation).toBeTypeOf('function')) - finishPreparation() - let committedAtShutdown = false - await new Promise((resolve, reject) => { - const shutdown = () => { - committedAtShutdown = committed - provider.shutdown(id, { immediate: true }).then(resolve, reject) - } - queueMicrotask(() => (microtasks === 1 ? shutdown() : queueMicrotask(shutdown))) - }) - const settled = await outcome - if (committedAtShutdown) { - expect(settled.ok).toBe(true) - expect(kill).toHaveBeenCalled() - } else { - expect(settled).toEqual({ ok: false, error: new Error(`PTY spawn canceled: ${id}`) }) - expect(spawnMock.mock.calls.length).toBe(0) - } - expect(provider.getPtyProcess(id)).toBeUndefined() - expect(pendingLocalPtySpawns.has(id)).toBe(false) - } - ) - - it('cancels deferred shell availability before finalizing a launch plan', async () => { - spawnMock.mockClear() - Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) - let finishAvailability!: (available: boolean) => void - const buildSpawnEnv = vi.fn((_id: string, env: Record) => env) - provider.configure({ - getWindowsShell: () => 'powershell.exe', - getWindowsPowerShellImplementation: () => 'auto', - pwshAvailable: () => - new Promise((resolve) => { - finishAvailability = resolve - }), - buildSpawnEnv - }) - const id = 'deferred-availability-session' - const spawn = provider.spawn({ cols: 80, rows: 24, sessionId: id }) - const rejected = expect(spawn).rejects.toThrow(`PTY spawn canceled: ${id}`) - await provider.shutdown(id, { immediate: true }) - finishAvailability(true) - await rejected - expect(buildSpawnEnv).not.toHaveBeenCalled() - expect(spawnMock.mock.calls.length).toBe(0) - expect(pendingLocalPtySpawns.has(id)).toBe(false) - }) - - it.each(['synchronous environment', 'async environment', 'preflight'])( - 'releases cancellation state after failed %s and allows a fresh retry', - async (phase) => { - spawnMock.mockClear() - const id = 'failed-preparation-session' - let fail = true - provider.configure({ - buildSpawnEnv: (_id, env) => { - if (fail && phase === 'synchronous environment') { - throw new Error('preparation failed') - } - if (fail && phase === 'async environment') { - return Promise.reject(new Error('preparation failed')) - } - return env - } - }) - if (phase === 'preflight') { - prepareMacosTccLoginShellMock.mockRejectedValueOnce(new Error('preparation failed')) - } - await expect(provider.spawn({ cols: 80, rows: 24, sessionId: id })).rejects.toThrow( - 'preparation failed' - ) - expect(pendingLocalPtySpawns.has(id)).toBe(false) - expect(spawnMock.mock.calls.length).toBe(0) - await provider.shutdown(id, { immediate: true }) - fail = false - await expect(provider.spawn({ cols: 80, rows: 24, sessionId: id })).resolves.toMatchObject({ - id - }) - expect(spawnMock.mock.calls.length).toBe(1) - expect(pendingLocalPtySpawns.has(id)).toBe(false) - } - ) - - it('coalesces a concurrent same-session-id spawn before launching a redundant shell (F3)', async () => { - spawnMock.mockClear() - const procA = { ...mockProc, pid: 1001 } - spawnMock.mockReturnValueOnce(procA) - - // Hold both spawns past the existence check and inside the preflight so they - // race to register the same id; release only after both are parked. - let releasePreflight!: () => void - prepareMacosTccLoginShellMock.mockReturnValue( - new Promise((resolve) => { - releasePreflight = resolve - }) - ) - - const spawnA = provider.spawn({ cols: 80, rows: 24, sessionId: 'race-session' }) - const spawnB = provider.spawn({ cols: 132, rows: 44, sessionId: 'race-session' }) - await vi.waitFor(() => expect(prepareMacosTccLoginShellMock).toHaveBeenCalledTimes(2)) - releasePreflight() - - const [a, b] = await Promise.all([spawnA, spawnB]) - // The winner owns the tracked PTY; the loser attaches to it, not a second one. - expect(a.isReattach).toBeUndefined() - expect(b.isReattach).toBe(true) - expect(b.pid).toBe(procA.pid) - expect(provider.getPtyProcess('race-session')).toBe(procA) - expect(spawnMock).toHaveBeenCalledOnce() - expect(procA.resize).toHaveBeenCalledWith(132, 44) - }) - - it('invokes onSpawned callback', async () => { - const onSpawned = vi.fn() - provider.configure({ onSpawned }) - const { id, incarnationId } = await provider.spawn({ cols: 80, rows: 24 }) - expect(onSpawned).toHaveBeenCalledWith(id, incarnationId) - }) - - it('reports physical commit before post-spawn publication can fail', async () => { - spawnMock.mockClear() - const committed = vi.fn() - provider.configure({ - onSpawned: () => { - throw new Error('post-spawn publication failed') - } - }) - - await expect( - provider.spawn({ cols: 80, rows: 24, onPtySpawnCommitted: committed }) - ).rejects.toThrow('post-spawn publication failed') - expect(spawnMock).toHaveBeenCalledOnce() - expect(committed).toHaveBeenCalledOnce() - }) - }) -}) diff --git a/src/main/providers/local-pty-provider-state.ts b/src/main/providers/local-pty-provider-state.ts deleted file mode 100644 index 10cef331a620..000000000000 --- a/src/main/providers/local-pty-provider-state.ts +++ /dev/null @@ -1,170 +0,0 @@ -import type * as pty from 'node-pty' -import type { PhysicalExitTracker } from '../../shared/physical-exit-tracker' -import type { PtyStartupIngress } from '../../shared/pty-startup-ingress' -import type { TerminalExitCause } from '../../shared/terminal-exit-cause' -import { getSpawnedShellName, normalizeLocalCallerSessionId } from './local-pty-launch-helpers' - -export type PtyShutdownOperation = { - promise: Promise - immediate: boolean - rootSignalled: boolean - proc: pty.IPty -} - -export type PendingLocalPtySpawn = { - cancellation: AbortController -} - -export type DataCallback = (payload: { - id: string - data: string - sequenceChars?: number - transformed?: boolean - seq?: number -}) => void - -export type ExitCallback = (payload: { - id: string - code: number - incarnationId?: string - cause?: TerminalExitCause -}) => void - -let ptyCounter = 0 -export const ptyProcesses = new Map() -export const ptyIncarnations = new Map() -// Why: agent sessions always sweep descendant trees; plain terminals preserve nohup children except on immediate win32 shutdown. -export const ptyAgentSessionIds = new Set() -// Why: descendant capture is async, so reattach/duplicate shutdown must wait for the original owner, not return a dying PTY. -export const ptyShutdownOperations = new Map() -export const pendingLocalPtySpawns = new Map>() -// Why the full path: the shell proof must tell Git Bash's `bin\bash.exe` launcher from `usr\bin\bash.exe`. -export const ptyShellPath = new Map() -export const ptyAgentForegroundContextPaths = new Map() -// Why: remember the last recognized agent foreground so a degraded scan doesn't report the shell and look like an exit. -// `pid` anchors the identity to the row that proved it (null when ambiguous); -// `at` is the last confirmation, so unanchored job evidence -- only a superset -- cannot hold it forever. -// `steady` (POSIX) is the pane fingerprint the recognizing capture proved plus node-pty's name at -// that moment; a cheap capture matching it re-proves the identity without the full table. -export const ptyLastRecognizedForeground = new Map< - string, - { - name: string - pid: number | null - at: number - steady?: { fingerprint: string; fallbackProcess: string | null } | null - } ->() -export const ptyTerminalHandle = new Map() -export const ptyWorktreeId = new Map() -export const ptyInitialCwd = new Map() -// Why: reattach carries current settings, not the live process's launch context; keep the first creator's WSL/native identity. -export const ptyWslDistroById = new Map() -// Why: node-pty callbacks dispose before env teardown, but onExit separately owns physical-exit proof during termination. -export const ptyDisposables = new Map void }[]>() -export const ptyExitDisposables = new Map void }>() -export const ptyCleanupCallbacks = new Map void>() -export const ptyTerminationMode = new Map() -export const ptyPhysicalExits = new Map() -// Why: a wrapper spawn (macOS TCC login) reports its own status, never the -// shell's, so its exit numbers must not be read as the agent's (STA-4536). -export const ptyReportsChildExitStatus = new Map() -export const ptyForceKillTimers = new Map>() - -let loadGeneration = 0 -export const ptyLoadGeneration = new Map() - -export const dataListeners = new Set() -export const exitListeners = new Set() -export const startupIngressByPty = new Map() - -/** - * Disposes native node-pty listeners registered for a PTY id. - */ -export function disposePtyListeners(id: string): void { - const disposables = ptyDisposables.get(id) - if (disposables) { - for (const d of disposables) { - d.dispose() - } - ptyDisposables.delete(id) - } -} - -export function disposePtyExitListener(id: string): void { - ptyExitDisposables.get(id)?.dispose() - ptyExitDisposables.delete(id) -} - -export function clearLocalPtyForceKillTimer(id: string): void { - const timer = ptyForceKillTimers.get(id) - if (timer) { - clearTimeout(timer) - ptyForceKillTimers.delete(id) - } -} - -export function runPtyCleanup(id: string): void { - const cleanup = ptyCleanupCallbacks.get(id) - if (!cleanup) { - return - } - ptyCleanupCallbacks.delete(id) - cleanup() -} - -export function getPtyShellName(id: string): string | undefined { - const shellPath = ptyShellPath.get(id) - return shellPath === undefined ? undefined : getSpawnedShellName(shellPath) -} - -/** - * Removes all local tracking state for a PTY id after teardown. - */ -export function clearPtyState(id: string): void { - clearLocalPtyForceKillTimer(id) - runPtyCleanup(id) - disposePtyListeners(id) - disposePtyExitListener(id) - ptyProcesses.delete(id) - ptyIncarnations.delete(id) - ptyAgentSessionIds.delete(id) - ptyShellPath.delete(id) - ptyAgentForegroundContextPaths.delete(id) - ptyLastRecognizedForeground.delete(id) - ptyTerminalHandle.delete(id) - ptyWorktreeId.delete(id) - ptyInitialCwd.delete(id) - ptyWslDistroById.delete(id) - ptyLoadGeneration.delete(id) - ptyTerminationMode.delete(id) - ptyReportsChildExitStatus.delete(id) - ptyPhysicalExits.delete(id) -} - -/** - * Allocates either a stable caller-provided PTY id or a new numeric id. - */ -export function allocatePtyId(sessionId: string | undefined): string { - const requested = normalizeLocalCallerSessionId(sessionId) - if (requested) { - return requested - } - let id: string - do { - id = String(++ptyCounter) - } while (ptyProcesses.has(id)) - return id -} - -export function getLoadGeneration(): number { - return loadGeneration -} - -export function advanceLoadGeneration(): number { - return ++loadGeneration -} - -export function resetLoadGeneration(): void { - loadGeneration = 0 -} diff --git a/src/main/providers/local-pty-provider-test-harness.ts b/src/main/providers/local-pty-provider-test-harness.ts deleted file mode 100644 index cf95ae1d9627..000000000000 --- a/src/main/providers/local-pty-provider-test-harness.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { afterEach, beforeEach, vi, type Mock } from 'vitest' -import { _resetLocalPtyProviderStateForTest } from './local-pty-provider' - -export type LocalPtyExitCallback = (info: { exitCode: number }) => void - -export type LocalPtyMockProcess = { - onData: ReturnType - onExit: ReturnType - write: ReturnType - resize: ReturnType - pause: ReturnType - resume: ReturnType - kill: ReturnType - process: string - pid: number -} - -export type LocalPtyProviderMocks = { - existsSyncMock: Mock - statSyncMock: Mock - accessSyncMock: Mock - mkdirSyncMock: Mock - writeFileSyncMock: Mock - prepareMacosTccLoginShellMock: Mock - resolveAgentForegroundProcessMock: Mock - readWindowsPtyJobProcessIdsMock: Mock - killWithDescendantSweepMock: Mock - isWslAvailableAsyncMock: Mock - wslUncDirectoryExistsMock: Mock - createShellPromptReadinessProbeMock: Mock -} - -/** Pins platform/shell env for every test and restores it plus provider module state after. */ -export function installLocalPtyProviderEnvSandbox(): void { - let origShell: string | undefined - let origPowerlevelWizardDisable: string | undefined - let origHistFile: string | undefined - let origPlatform: PropertyDescriptor | undefined - - beforeEach(() => { - origPlatform = Object.getOwnPropertyDescriptor(process, 'platform') - Object.defineProperty(process, 'platform', { configurable: true, value: 'linux' }) - origShell = process.env.SHELL - origPowerlevelWizardDisable = process.env.POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD - origHistFile = process.env.HISTFILE - process.env.SHELL = '/bin/zsh' - delete process.env.POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD - // injectHistoryEnv preserves an inherited HISTFILE, so clear it for hermetic history assertions. - delete process.env.HISTFILE - }) - - afterEach(() => { - _resetLocalPtyProviderStateForTest() - if (origPlatform) { - Object.defineProperty(process, 'platform', origPlatform) - } - if (origShell === undefined) { - delete process.env.SHELL - } else { - process.env.SHELL = origShell - } - if (origPowerlevelWizardDisable === undefined) { - delete process.env.POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD - } else { - process.env.POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD = origPowerlevelWizardDisable - } - if (origHistFile === undefined) { - delete process.env.HISTFILE - } else { - process.env.HISTFILE = origHistFile - } - }) -} - -export function applyLocalPtyProviderMockDefaults(mocks: LocalPtyProviderMocks): void { - mocks.existsSyncMock.mockReturnValue(true) - // size: the wrapper writer verifies each generated file is non-empty. - mocks.statSyncMock.mockReturnValue({ isDirectory: () => true, mode: 0o755, size: 1 }) - mocks.accessSyncMock.mockReturnValue(undefined) - mocks.mkdirSyncMock.mockReset() - mocks.writeFileSyncMock.mockReset() - mocks.killWithDescendantSweepMock.mockReset() - // Default: no-op sweep that still runs killRoot (matches empty-snapshot degrade). - mocks.killWithDescendantSweepMock.mockImplementation( - async (_rootPid: number, killRoot: () => void, _deps?: { ownsRoot?: () => boolean }) => { - killRoot() - } - ) - mocks.prepareMacosTccLoginShellMock.mockReset() - mocks.prepareMacosTccLoginShellMock.mockResolvedValue(undefined) - mocks.resolveAgentForegroundProcessMock.mockReset() - mocks.resolveAgentForegroundProcessMock.mockImplementation( - async (_pid: number, fallbackProcess: string | null) => ({ - available: true, - processName: fallbackProcess - }) - ) - mocks.readWindowsPtyJobProcessIdsMock.mockReset() - mocks.readWindowsPtyJobProcessIdsMock.mockReturnValue(null) - mocks.isWslAvailableAsyncMock.mockReset() - mocks.isWslAvailableAsyncMock.mockResolvedValue(true) - mocks.wslUncDirectoryExistsMock.mockReset() - mocks.wslUncDirectoryExistsMock.mockReturnValue(true) - mocks.createShellPromptReadinessProbeMock.mockReset() -} - -/** node-pty stand-in; the exit callback lives in the test file so bodies can fire it directly. */ -export function createLocalPtyMockProcess(exitCallback: { - get: () => LocalPtyExitCallback | undefined - set: (cb: LocalPtyExitCallback | undefined) => void -}): LocalPtyMockProcess { - return { - onData: vi.fn(() => ({ dispose: vi.fn() })), - onExit: vi.fn((cb: LocalPtyExitCallback) => { - exitCallback.set(cb) - return { - dispose: () => { - if (exitCallback.get() === cb) { - exitCallback.set(undefined) - } - } - } - }), - write: vi.fn(), - resize: vi.fn(), - pause: vi.fn(), - resume: vi.fn(), - kill: vi.fn(() => { - exitCallback.get()?.({ exitCode: -1 }) - }), - process: 'zsh', - pid: 12345 - } -} diff --git a/src/main/providers/local-pty-provider-types.ts b/src/main/providers/local-pty-provider-types.ts deleted file mode 100644 index ebe90bbb049c..000000000000 --- a/src/main/providers/local-pty-provider-types.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { TerminalExitCause } from '../../shared/terminal-exit-cause' -import type { PtySpawnOptions } from './types' - -export type LocalPtyProviderOptions = { - /** Why: `ctx.command` (pi/omp/claude) must drive overlay source-dir selection — a disk-presence fallback shadows the other agent's extensions. */ - buildSpawnEnv?: ( - id: string, - baseEnv: Record, - ctx?: { - explicitEnv?: PtySpawnOptions['env'] - command?: string - launchAgent?: PtySpawnOptions['launchAgent'] - codexHomePathOverride?: PtySpawnOptions['codexHomePathOverride'] - cwd?: string - shellPath?: string - isWsl?: boolean - wslDistro?: string | null - } - // Why (#16441): Codex launch prep grants hook trust through a codex - // app-server session. `spawn` already awaits, so returning a promise keeps - // the Electron main thread responsive instead of blocking on spawnSync. - ) => Record | Promise> - /** Whether worktree-scoped shell history is enabled; when true (or absent) with a worktreeId, HISTFILE is scoped per-worktree. */ - isHistoryEnabled?: () => boolean - /** Why: COMSPEC is always cmd.exe, so this callback injects the user's persisted shell preference. Undefined when none set. */ - getWindowsShell?: () => string | undefined - getDefaultShell?: () => string | undefined - getWindowsPowerShellImplementation?: () => 'auto' | 'powershell.exe' | 'pwsh.exe' | undefined - pwshAvailable?: () => boolean | Promise - onSpawned?: (id: string, incarnationId: string) => void - onExit?: (id: string, code: number, incarnationId: string, cause?: TerminalExitCause) => void - onData?: ( - id: string, - data: string, - timestamp: number, - sequenceChars?: number, - transformed?: boolean - ) => void -} diff --git a/src/main/providers/local-pty-provider-windows-shell-launch.test.ts b/src/main/providers/local-pty-provider-windows-shell-launch.test.ts deleted file mode 100644 index bc96cdb1d6af..000000000000 --- a/src/main/providers/local-pty-provider-windows-shell-launch.test.ts +++ /dev/null @@ -1,668 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import type * as MacosTccLoginShell from './macos-tcc-login-shell' - -const { - existsSyncMock, - statSyncMock, - accessSyncMock, - mkdirSyncMock, - writeFileSyncMock, - spawnMock, - prepareMacosTccLoginShellMock, - resolveAgentForegroundProcessMock, - readWindowsPtyJobProcessIdsMock, - killWithDescendantSweepMock, - isWslAvailableAsyncMock, - wslUncDirectoryExistsMock, - createShellPromptReadinessProbeMock -} = vi.hoisted(() => ({ - existsSyncMock: vi.fn(), - statSyncMock: vi.fn(), - accessSyncMock: vi.fn(), - mkdirSyncMock: vi.fn(), - writeFileSyncMock: vi.fn(), - spawnMock: vi.fn(), - prepareMacosTccLoginShellMock: vi.fn(), - resolveAgentForegroundProcessMock: vi.fn(), - readWindowsPtyJobProcessIdsMock: vi.fn(), - killWithDescendantSweepMock: vi.fn(), - isWslAvailableAsyncMock: vi.fn(), - wslUncDirectoryExistsMock: vi.fn(), - createShellPromptReadinessProbeMock: vi.fn() -})) - -vi.mock('fs', () => ({ - existsSync: existsSyncMock, - statSync: statSyncMock, - accessSync: accessSyncMock, - mkdirSync: mkdirSyncMock, - writeFileSync: writeFileSyncMock, - chmodSync: vi.fn(), - renameSync: vi.fn(), - rmSync: vi.fn(), - constants: { X_OK: 1 } -})) - -vi.mock('electron', () => ({ - app: { - getPath: vi.fn(() => '/tmp/orca-user-data') - } -})) - -vi.mock('node-pty', () => ({ - spawn: spawnMock -})) - -vi.mock('./macos-tcc-login-shell', async (importOriginal) => ({ - ...(await importOriginal()), - prepareMacosTccLoginShell: prepareMacosTccLoginShellMock -})) - -vi.mock('../pty-descendant-termination', () => ({ - killWithDescendantSweep: killWithDescendantSweepMock -})) - -// Resolve PowerShell family names to deterministic absolute paths (the fs mock -// above otherwise makes every probe miss). The real resolver — which skips the -// Store App Execution Alias stub — is covered in -// windows-powershell-executable.test.ts. -const WINDOWS_POWERSHELL_ABS = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' -const PWSH7_ABS = 'C:\\Program Files\\PowerShell\\7\\pwsh.exe' -const CMD_ABS = 'C:\\Windows\\System32\\cmd.exe' -const CODEX_LAUNCH_PREFLIGHT = 'C:\\Program Files\\Orca\\orca.exe' -vi.mock('./windows-powershell-executable', () => ({ - resolveWindowsPowerShellExecutablePath: (family: 'pwsh.exe' | 'powershell.exe') => - family === 'pwsh.exe' ? PWSH7_ABS : WINDOWS_POWERSHELL_ABS, - resolveWindowsPowerShellSpawnChain: (family: 'pwsh.exe' | 'powershell.exe') => - family === 'pwsh.exe' - ? [PWSH7_ABS, WINDOWS_POWERSHELL_ABS, CMD_ABS] - : [WINDOWS_POWERSHELL_ABS, CMD_ABS], - getWindowsCmdPath: () => CMD_ABS -})) - -vi.mock('./agent-foreground-process', () => ({ - resolveAgentForegroundProcessWithAvailability: (...args: unknown[]) => - resolveAgentForegroundProcessMock(...args) -})) - -vi.mock('./windows-pty-job-membership', () => ({ - readWindowsPtyJobProcessIds: (...args: unknown[]) => readWindowsPtyJobProcessIdsMock(...args), - isWindowsPtyJobReadable: () => true -})) - -vi.mock('../wsl', () => ({ - parseWslPath: (path: string) => { - const match = path.match(/^\\\\wsl\.localhost\\([^\\]+)(.*)$/) - if (!match) { - return null - } - return { - distro: match[1], - linuxPath: (match[2] || '').replace(/\\/g, '/') || '/' - } - }, - toLinuxPath: (path: string) => path.replace(/^C:\\/i, '/mnt/c/').replace(/\\/g, '/'), - toWindowsWslPath: (path: string, distro: string) => - `\\\\wsl.localhost\\${distro}${path.replace(/\//g, '\\')}`, - getDefaultWslDistro: () => 'Ubuntu', - isWslAvailableAsync: () => isWslAvailableAsyncMock(), - // Why: WSL worktree validation now asks the distro; these tests use WSL UNC - // cwds that are meant to exist, so report them present without spawning wsl.exe. - wslUncDirectoryExists: (...args: unknown[]) => wslUncDirectoryExistsMock(...args) -})) - -vi.mock('../shell-prompt-readiness-probe', () => ({ - createShellPromptReadinessProbe: createShellPromptReadinessProbeMock -})) - -import { LocalPtyProvider } from './local-pty-provider' -import { - applyLocalPtyProviderMockDefaults, - createLocalPtyMockProcess, - installLocalPtyProviderEnvSandbox, - type LocalPtyMockProcess -} from './local-pty-provider-test-harness' -import { POWERLEVEL10K_WIZARD_DISABLE_ENV } from '../pty/powerlevel10k-wizard-env' - -describe('LocalPtyProvider', () => { - let provider: LocalPtyProvider - let mockProc: LocalPtyMockProcess - let exitCb: ((info: { exitCode: number }) => void) | undefined - - installLocalPtyProviderEnvSandbox() - - beforeEach(() => { - applyLocalPtyProviderMockDefaults({ - existsSyncMock, - statSyncMock, - accessSyncMock, - mkdirSyncMock, - writeFileSyncMock, - prepareMacosTccLoginShellMock, - resolveAgentForegroundProcessMock, - readWindowsPtyJobProcessIdsMock, - killWithDescendantSweepMock, - isWslAvailableAsyncMock, - wslUncDirectoryExistsMock, - createShellPromptReadinessProbeMock - }) - - exitCb = undefined - mockProc = createLocalPtyMockProcess({ - get: () => exitCb, - set: (cb) => { - exitCb = cb - } - }) - spawnMock.mockReturnValue(mockProc) - - provider = new LocalPtyProvider() - }) - - describe('spawn', () => { - it('does not pass a Windows Codex home into WSL terminals', async () => { - Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) - provider.configure({ - buildSpawnEnv: (_id, env) => { - env.CODEX_HOME = 'C:\\Users\\jin\\.codex' - env.ORCA_CODEX_HOME = 'C:\\Users\\jin\\.codex' - return env - } - }) - - await provider.spawn({ - cols: 80, - rows: 24, - cwd: '\\\\wsl.localhost\\Ubuntu\\home\\jin\\repo' - }) - - const spawnCall = spawnMock.mock.calls.at(-1)! - expect(spawnCall[0]).toBe('wsl.exe') - expect(spawnCall[2].env.CODEX_HOME).toBeUndefined() - expect(spawnCall[2].env.ORCA_CODEX_HOME).toBeUndefined() - }) - - it('does not pass a WSL managed Codex home into Windows terminals', async () => { - Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) - provider.configure({ - buildSpawnEnv: (_id, env) => { - env.CODEX_HOME = - '\\\\wsl.localhost\\Ubuntu\\home\\jin\\.local\\share\\orca\\codex-accounts\\a\\home' - env.ORCA_CODEX_HOME = - '\\\\wsl.localhost\\Ubuntu\\home\\jin\\.local\\share\\orca\\codex-accounts\\a\\home' - return env - } - }) - - await provider.spawn({ - cols: 80, - rows: 24, - cwd: 'C:\\Users\\jin\\repo' - }) - - const spawnCall = spawnMock.mock.calls.at(-1)! - expect(spawnCall[2].env.CODEX_HOME).toBeUndefined() - expect(spawnCall[2].env.ORCA_CODEX_HOME).toBeUndefined() - }) - - it('preserves an explicit Linux Codex home for WSL terminals', async () => { - Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) - provider.configure({ - buildSpawnEnv: (_id, env) => { - env.CODEX_HOME = '/home/jin/.codex-alt' - return env - } - }) - - await provider.spawn({ - cols: 80, - rows: 24, - cwd: '\\\\wsl.localhost\\Ubuntu\\home\\jin\\repo' - }) - - const spawnCall = spawnMock.mock.calls.at(-1)! - expect(spawnCall[0]).toBe('wsl.exe') - expect(spawnCall[2].env.CODEX_HOME).toBe('/home/jin/.codex-alt') - expect(spawnCall[2].env.WSLENV).toContain('CODEX_HOME') - }) - - it('translates a WSL managed Codex home before launching a WSL terminal', async () => { - Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) - provider.configure({ - buildSpawnEnv: (_id, env) => { - env.CODEX_HOME = - '\\\\wsl.localhost\\Ubuntu\\home\\jin\\.local\\share\\orca\\codex-accounts\\a\\home' - env.ORCA_CODEX_HOME = - '\\\\wsl.localhost\\Ubuntu\\home\\jin\\.local\\share\\orca\\codex-accounts\\a\\home' - return env - } - }) - - await provider.spawn({ - cols: 80, - rows: 24, - cwd: '\\\\wsl.localhost\\Ubuntu\\home\\jin\\repo' - }) - - const spawnCall = spawnMock.mock.calls.at(-1)! - expect(spawnCall[0]).toBe('wsl.exe') - expect(spawnCall[2].env.CODEX_HOME).toBe('/home/jin/.local/share/orca/codex-accounts/a/home') - expect(spawnCall[2].env.ORCA_CODEX_HOME).toBe( - '/home/jin/.local/share/orca/codex-accounts/a/home' - ) - expect(spawnCall[2].env.WSLENV).toContain('CODEX_HOME') - expect(spawnCall[2].env.WSLENV).toContain('ORCA_CODEX_HOME') - }) - - it('does not pass a WSL managed Codex home into a different WSL distro', async () => { - Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) - provider.configure({ - buildSpawnEnv: (_id, env) => { - env.CODEX_HOME = - '\\\\wsl.localhost\\Ubuntu\\home\\jin\\.local\\share\\orca\\codex-accounts\\a\\home' - env.ORCA_CODEX_HOME = - '\\\\wsl.localhost\\Ubuntu\\home\\jin\\.local\\share\\orca\\codex-accounts\\a\\home' - return env - } - }) - - await provider.spawn({ - cols: 80, - rows: 24, - cwd: '\\\\wsl.localhost\\Debian\\home\\jin\\repo' - }) - - const spawnCall = spawnMock.mock.calls.at(-1)! - expect(spawnCall[0]).toBe('wsl.exe') - expect(spawnCall[2].env.CODEX_HOME).toBeUndefined() - expect(spawnCall[2].env.ORCA_CODEX_HOME).toBeUndefined() - }) - - it('uses the preferred WSL distro for Windows cwd WSL terminals', async () => { - Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) - - await provider.spawn({ - cols: 80, - rows: 24, - worktreeId: 'repo-1::C:\\Users\\jin\\repo', - cwd: 'C:\\Users\\jin\\repo', - shellOverride: 'wsl.exe', - terminalWindowsWslDistro: 'Debian' - }) - - const spawnCall = spawnMock.mock.calls.at(-1)! - expect(spawnCall[0]).toBe('wsl.exe') - expect(spawnCall[1]).toEqual([ - '-d', - 'Debian', - '--exec', - 'sh', - '-c', - expect.stringContaining("cd '/mnt/c/Users/jin/repo'") - ]) - expect(spawnCall[1][5]).toContain('exec "$_orca_wsl_shell" -l') - expect(spawnCall[2].env.HISTFILE).toContain('terminal-history-wsl/Debian') - }) - - it.each(['/home/jin/repo', '/a', '/c'])( - 'preserves a POSIX cwd through the preferred WSL distro (%s)', - async (cwd) => { - Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) - - await provider.spawn({ - cols: 80, - rows: 24, - worktreeId: 'repo-1::C:\\Users\\jin\\repo', - cwd, - prevalidatedCwd: `\\\\wsl.localhost\\Debian${cwd.replaceAll('/', '\\')}`, - shellOverride: 'wsl.exe', - terminalWindowsWslDistro: 'Debian' - }) - - const spawnCall = spawnMock.mock.calls.at(-1)! - expect(spawnCall[0]).toBe('wsl.exe') - expect(spawnCall[1]).toEqual([ - '-d', - 'Debian', - '--exec', - 'sh', - '-c', - expect.stringContaining(`cd '${cwd}'`) - ]) - expect(spawnCall[2].cwd).not.toBe(cwd) - expect(wslUncDirectoryExistsMock).not.toHaveBeenCalled() - } - ) - - it('revalidates when cwd evidence does not exactly match the resolved WSL path', async () => { - Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) - - await provider.spawn({ - cols: 80, - rows: 24, - worktreeId: 'repo-1::C:\\Users\\jin\\repo', - cwd: '/a', - prevalidatedCwd: '\\\\wsl.localhost\\Debian\\c', - shellOverride: 'wsl.exe', - terminalWindowsWslDistro: 'Debian' - }) - - expect(wslUncDirectoryExistsMock).toHaveBeenCalledWith('\\\\wsl.localhost\\Debian\\a') - }) - - it('resolves and persists the default distro for Windows cwd WSL terminals', async () => { - Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) - const buildSpawnEnv = vi.fn( - ( - _id: string, - env: Record, - _ctx?: { isWsl?: boolean; wslDistro?: string | null } - ) => env - ) - provider.configure({ buildSpawnEnv }) - - const result = await provider.spawn({ - cols: 80, - rows: 24, - worktreeId: 'repo-1::C:\\Users\\jin\\repo', - cwd: 'C:\\Users\\jin\\repo', - shellOverride: 'wsl.exe', - terminalWindowsWslDistro: null - }) - - const spawnCall = spawnMock.mock.calls.at(-1)! - expect(spawnCall[1]).toEqual([ - '-d', - 'Ubuntu', - '--exec', - 'sh', - '-c', - expect.stringContaining("cd '/mnt/c/Users/jin/repo'") - ]) - expect(buildSpawnEnv.mock.calls[0]?.[2]).toMatchObject({ - isWsl: true, - wslDistro: 'Ubuntu' - }) - expect(result.wslDistro).toBe('Ubuntu') - expect( - (await provider.listProcesses()).find((entry) => entry.id === result.id)?.wslDistro - ).toBe('Ubuntu') - }) - - it('repro: keeps explicit PowerShell 7 selection when the pwsh probe is cold-false', async () => { - Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) - const pwshAvailable = vi.fn(() => false) - provider.configure({ - getWindowsShell: () => 'powershell.exe', - getWindowsPowerShellImplementation: () => 'pwsh.exe', - pwshAvailable - }) - - await provider.spawn({ - cols: 80, - rows: 24, - cwd: 'C:\\Users\\jin\\repo' - }) - - const spawnCall = spawnMock.mock.calls.at(-1)! - expect(spawnCall[0]).toBe(PWSH7_ABS) - expect(spawnCall[1]).toContain('-EncodedCommand') - expect(pwshAvailable).not.toHaveBeenCalled() - }) - - it('awaits PowerShell availability before resolving an automatic Windows shell', async () => { - Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) - let resolveAvailability!: (available: boolean) => void - const pwshAvailable = vi.fn( - () => - new Promise((resolve) => { - resolveAvailability = resolve - }) - ) - provider.configure({ - getWindowsShell: () => 'powershell.exe', - getWindowsPowerShellImplementation: () => 'auto', - pwshAvailable - }) - - const callsBeforeSpawn = spawnMock.mock.calls.length - const spawn = provider.spawn({ cols: 80, rows: 24, cwd: 'C:\\Users\\jin\\repo' }) - await Promise.resolve() - expect(spawnMock).toHaveBeenCalledTimes(callsBeforeSpawn) - - resolveAvailability(true) - await spawn - expect(spawnMock).toHaveBeenCalledTimes(callsBeforeSpawn + 1) - expect(spawnMock.mock.calls.at(-1)?.[0]).toBe(PWSH7_ABS) - }) - - it('re-reads Windows shell options after a reentrant configuration change', async () => { - Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) - const initialPowerShellImplementation = vi.fn(() => 'powershell.exe' as const) - const configuredPwshAvailable = vi.fn(() => true) - provider.configure({ - getWindowsShell: () => { - provider.configure({ - getWindowsPowerShellImplementation: () => 'auto', - pwshAvailable: configuredPwshAvailable - }) - return 'powershell.exe' - }, - getWindowsPowerShellImplementation: initialPowerShellImplementation - }) - - await provider.spawn({ cols: 80, rows: 24, cwd: 'C:\\Users\\jin\\repo' }) - - expect(initialPowerShellImplementation).not.toHaveBeenCalled() - expect(configuredPwshAvailable).toHaveBeenCalledOnce() - expect(spawnMock.mock.calls.at(-1)?.[0]).toBe(PWSH7_ABS) - }) - - it('registers a probe-complete spawn before a later microtask shutdown', async () => { - Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) - provider.configure({ - getWindowsShell: () => 'powershell.exe', - getWindowsPowerShellImplementation: () => 'auto', - pwshAvailable: () => false - }) - const callsBeforeSpawn = spawnMock.mock.calls.length - const spawn = provider.spawn({ - cols: 80, - rows: 24, - cwd: 'C:\\Users\\jin\\repo', - sessionId: 'probe-shutdown-session' - }) - const canceledSpawn = expect(spawn).rejects.toThrow( - 'PTY spawn canceled: probe-shutdown-session' - ) - const shutdown = new Promise((resolve, reject) => { - queueMicrotask(() => { - provider.shutdown('probe-shutdown-session', { immediate: true }).then(resolve, reject) - }) - }) - - await shutdown - await canceledSpawn - expect(spawnMock).toHaveBeenCalledTimes(callsBeforeSpawn) - }) - - it('marks Orca terminal handle for WSL import when buildSpawnEnv opts in', async () => { - Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) - const savedCodexHome = process.env.CODEX_HOME - const savedOrcaCodexHome = process.env.ORCA_CODEX_HOME - delete process.env.CODEX_HOME - delete process.env.ORCA_CODEX_HOME - provider.configure({ - buildSpawnEnv: (_id, env, ctx) => { - env.ORCA_TERMINAL_HANDLE = 'term_wsl' - if (ctx?.isWsl) { - env.WSLENV = 'ORCA_TERMINAL_HANDLE/u' - } - return env - } - }) - - try { - await provider.spawn({ - cols: 80, - rows: 24, - cwd: '\\\\wsl.localhost\\Ubuntu\\home\\jin\\repo', - env: { ORCA_HERMES_STARTUP_QUERY: 'line one\nline two' } - }) - } finally { - if (savedCodexHome === undefined) { - delete process.env.CODEX_HOME - } else { - process.env.CODEX_HOME = savedCodexHome - } - if (savedOrcaCodexHome === undefined) { - delete process.env.ORCA_CODEX_HOME - } else { - process.env.ORCA_CODEX_HOME = savedOrcaCodexHome - } - } - - const spawnCall = spawnMock.mock.calls.at(-1)! - expect(spawnCall[0]).toBe('wsl.exe') - expect(spawnCall[2].env.ORCA_TERMINAL_HANDLE).toBe('term_wsl') - expect(spawnCall[2].env.WSLENV?.split(':')).toEqual( - expect.arrayContaining([ - 'ORCA_TERMINAL_HANDLE/u', - 'ORCA_HERMES_STARTUP_QUERY', - POWERLEVEL10K_WIZARD_DISABLE_ENV - ]) - ) - }) - - it('does not mark deleted Powerlevel10k wizard env for WSL import', async () => { - Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) - - await provider.spawn({ - cols: 80, - rows: 24, - cwd: '\\\\wsl.localhost\\Ubuntu\\home\\jin\\repo', - envToDelete: [POWERLEVEL10K_WIZARD_DISABLE_ENV] - }) - - const spawnCall = spawnMock.mock.calls.at(-1)! - expect(spawnCall[0]).toBe('wsl.exe') - expect(spawnCall[2].env[POWERLEVEL10K_WIZARD_DISABLE_ENV]).toBeUndefined() - expect(spawnCall[2].env.WSLENV ?? '').not.toContain(POWERLEVEL10K_WIZARD_DISABLE_ENV) - }) - - it('launches POSIX cwd split panes through WSL when worktree context is WSL', async () => { - const platform = Object.getOwnPropertyDescriptor(process, 'platform') - Object.defineProperty(process, 'platform', { value: 'win32' }) - - try { - await provider.spawn({ - cols: 80, - rows: 24, - cwd: '/home/jin/repo/subdir', - shellOverride: 'powershell.exe', - worktreeId: 'repo::\\\\wsl.localhost\\Ubuntu\\home\\jin\\repo' - }) - } finally { - if (platform) { - Object.defineProperty(process, 'platform', platform) - } - } - - expect(spawnMock).toHaveBeenCalledWith( - 'wsl.exe', - [ - '-d', - 'Ubuntu', - '--exec', - 'sh', - '-c', - expect.stringContaining("cd '/home/jin/repo/subdir'") - ], - expect.objectContaining({ cwd: expect.any(String) }) - ) - }) - - it('resolves the Git Bash default shell and preserves the requested cwd', async () => { - const platform = Object.getOwnPropertyDescriptor(process, 'platform') - const originalProgramFiles = process.env.ProgramFiles - Object.defineProperty(process, 'platform', { value: 'win32' }) - process.env.ProgramFiles = 'C:\\Program Files' - provider.configure({ - getWindowsShell: () => 'git-bash', - buildSpawnEnv: (_id, env) => ({ - ...env, - ORCA_CODEX_LAUNCH_PREFLIGHT: CODEX_LAUNCH_PREFLIGHT - }) - }) - - try { - await provider.spawn({ - cols: 80, - rows: 24, - cwd: 'C:\\Users\\jin\\repo' - }) - } finally { - if (platform) { - Object.defineProperty(process, 'platform', platform) - } - if (originalProgramFiles === undefined) { - delete process.env.ProgramFiles - } else { - process.env.ProgramFiles = originalProgramFiles - } - } - - expect(spawnMock).toHaveBeenCalledWith( - 'C:\\Program Files\\Git\\bin\\bash.exe', - [ - '-c', - expect.stringMatching( - /^chcp\.com 65001 >\/dev\/null 2>&1; exec "\$BASH" --rcfile '.*shell-ready\/bash\/rcfile' -i$/ - ) - ], - expect.objectContaining({ - cwd: 'C:\\Users\\jin\\repo', - env: expect.objectContaining({ - CHERE_INVOKING: '1', - PYTHONUTF8: '1', - ORCA_CODEX_LAUNCH_PREFLIGHT: CODEX_LAUNCH_PREFLIGHT - }) - }) - ) - }) - - it('runs the Codex preflight once in the cmd.exe startup chain', async () => { - const platform = Object.getOwnPropertyDescriptor(process, 'platform') - Object.defineProperty(process, 'platform', { value: 'win32' }) - provider.configure({ - getWindowsShell: () => 'cmd.exe', - buildSpawnEnv: (_id, env) => ({ - ...env, - ORCA_CODEX_LAUNCH_PREFLIGHT: CODEX_LAUNCH_PREFLIGHT - }) - }) - - try { - await provider.spawn({ cols: 80, rows: 24, cwd: 'C:\\Users\\jin\\repo' }) - } finally { - if (platform) { - Object.defineProperty(process, 'platform', platform) - } - } - - expect(spawnMock).toHaveBeenCalledWith( - 'cmd.exe', - [ - '/K', - 'chcp 65001 > nul & if defined ORCA_CODEX_LAUNCH_PREFLIGHT call %ORCA_CODEX_LAUNCH_PREFLIGHT_CMD_QUOTE%%ORCA_CODEX_LAUNCH_PREFLIGHT%%ORCA_CODEX_LAUNCH_PREFLIGHT_CMD_QUOTE% agent hooks prepare-codex > nul 2>&1' - ], - expect.objectContaining({ - env: expect.objectContaining({ - ORCA_CODEX_LAUNCH_PREFLIGHT: CODEX_LAUNCH_PREFLIGHT, - ORCA_CODEX_LAUNCH_PREFLIGHT_CMD_QUOTE: '"' - }) - }) - ) - }) - }) -}) diff --git a/src/main/providers/local-pty-provider.ts b/src/main/providers/local-pty-provider.ts deleted file mode 100644 index a8e2f9ce6094..000000000000 --- a/src/main/providers/local-pty-provider.ts +++ /dev/null @@ -1,225 +0,0 @@ -import type * as pty from 'node-pty' -import type { IPtyProvider, PtyProcessInfo, PtySpawnOptions, PtySpawnResult } from './types' -import { - WRITE_ACCEPTED, - writeRefused, - type WriteSettlement -} from '../../shared/pty-write-settlement' -import { - confirmLocalPtyForegroundProcess, - confirmLocalPtyShellForeground, - getLocalPtyForegroundProcess, - hasLocalPtyChildProcesses, - inspectLocalPtyChildProcesses -} from './local-pty-foreground-inspection' -import type { LocalPtyProviderOptions } from './local-pty-provider-types' -import type { PtyProcessInspection } from './pty-process-inspection' -import { - advanceLoadGeneration, - clearPtyState, - pendingLocalPtySpawns, - ptyProcesses, - resetLoadGeneration, - type DataCallback, - type ExitCallback -} from './local-pty-provider-state' -import { - clearLocalPtyBuffer, - closeLocalPtyStartupQueryAuthority, - getDefaultLocalPtyShell, - getLocalPtyAppliedSize, - getLocalPtyCwd, - getLocalPtyProcess, - getLocalPtyProfiles, - listLocalPtyProcesses, - onLocalPtyData, - onLocalPtyExit, - pauseLocalPtyProducer, - resizeLocalPty, - resumeLocalPtyProducer, - sendLocalPtySignal, - writeLocalPty -} from './local-pty-session-operations' -import { spawnLocalPty } from './local-pty-spawn' -import { cancelAllPendingLocalPtySpawns } from './local-pty-spawn-state' -import { killAllLocalPtys, killOrphanedLocalPtys, shutdownLocalPty } from './local-pty-termination' - -export type { LocalPtyProviderOptions } from './local-pty-provider-types' -export { - LOCAL_PTY_FORCE_KILL_RETRY_MS, - LOCAL_PTY_GRACEFUL_FORCE_TIMEOUT_MS, - LOCAL_PTY_PHYSICAL_EXIT_TIMEOUT_MS -} from './local-pty-termination' - -export class LocalPtyProvider implements IPtyProvider { - private opts: LocalPtyProviderOptions - - constructor(opts: LocalPtyProviderOptions = {}) { - this.opts = opts - } - - /** Reconfigure the provider with new hooks (e.g. after window re-creation). */ - configure(opts: LocalPtyProviderOptions): void { - this.opts = opts - } - - /** - * Spawns or reattaches a local PTY session for the renderer process. - * - * Windows launches can pre-deliver startup commands in argv, so the stdin fallback only runs when needed. - */ - spawn(args: PtySpawnOptions): Promise { - return spawnLocalPty(args, () => this.opts) - } - - // Local PTYs are always attached -- no-op. Remote providers use this to resubscribe. - async attach(_id: string): Promise {} - hasPty(id: string): boolean { - return ptyProcesses.has(id) - } - write(id: string, data: string): boolean { - return writeLocalPty(id, data) - } - - // In-process node-pty is its own sole owner, so its synchronous answer is the settlement. - writeWithSettlement(id: string, data: string): WriteSettlement { - return writeLocalPty(id, data) ? WRITE_ACCEPTED : writeRefused('provider_refused_write') - } - resize(id: string, cols: number, rows: number): void { - resizeLocalPty(id, cols, rows) - } - - pauseProducer(id: string): void { - pauseLocalPtyProducer(id) - } - - resumeProducer(id: string): void { - resumeLocalPtyProducer(id) - } - - getAppliedSize(id: string): Promise<{ cols: number; rows: number } | null> { - return getLocalPtyAppliedSize(id) - } - - shutdown(id: string, opts: { immediate?: boolean; keepHistory?: boolean }): Promise { - return shutdownLocalPty(id, opts) - } - - sendSignal(id: string, signal: string): Promise { - return sendLocalPtySignal(id, signal) - } - - getCwd(id: string): Promise { - return getLocalPtyCwd(id) - } - async getInitialCwd(_id: string): Promise { - return '' - } - clearBuffer(id: string): Promise { - return clearLocalPtyBuffer(id) - } - closeStartupQueryAuthority(id: string): number { - return closeLocalPtyStartupQueryAuthority(id) - } - acknowledgeDataEvent(_id: string, _charCount: number): void { - /* no flow control for local */ - } - - hasChildProcesses(id: string): Promise { - return hasLocalPtyChildProcesses(id) - } - - async inspectProcess(id: string): Promise { - const proc = ptyProcesses.get(id) - const foregroundProcess = await getLocalPtyForegroundProcess(id) - const childProcessEvidence = await inspectLocalPtyChildProcesses(id) - // Neither asynchronous inspection may publish a replacement pane's identity. - if (ptyProcesses.get(id) !== proc) { - return { - foregroundProcess: null, - hasChildProcesses: true, - childProcessEvidence: 'unverifiable' - } - } - return { - foregroundProcess, - hasChildProcesses: childProcessEvidence !== 'no-children', - childProcessEvidence - } - } - - getForegroundProcess(id: string): Promise { - return getLocalPtyForegroundProcess(id) - } - - confirmForegroundProcess(id: string): Promise { - return confirmLocalPtyForegroundProcess(id) - } - - confirmShellForeground(id: string): Promise { - return confirmLocalPtyShellForeground(id) - } - - async serialize(_ids: string[]): Promise { - return '{}' - } - async revive(_state: string): Promise { - /* re-spawning handles local revival */ - } - - listProcesses(): Promise { - return listLocalPtyProcesses() - } - - getDefaultShell(): Promise { - return getDefaultLocalPtyShell(() => this.opts) - } - - getProfiles(): Promise<{ name: string; path: string }[]> { - return getLocalPtyProfiles() - } - - onData(callback: DataCallback): () => void { - return onLocalPtyData(callback) - } - - // Local PTYs don't replay -- this is for remote reconnection - onReplay(_callback: (payload: { id: string; data: string }) => void): () => void { - return () => {} - } - - onExit(callback: ExitCallback): () => void { - return onLocalPtyExit(callback) - } - - // ─── Local-only helpers (not part of IPtyProvider interface) ─────── - - /** Kill orphaned PTYs from previous page loads. */ - killOrphanedPtys(currentGeneration: number): { id: string }[] { - return killOrphanedLocalPtys(currentGeneration) - } - - /** Advance the load generation counter (called on renderer reload). */ - advanceGeneration(): number { - return advanceLoadGeneration() - } - - /** Get a writable reference to a PTY (for runtime controller). */ - getPtyProcess(id: string): pty.IPty | undefined { - return getLocalPtyProcess(id) - } - - /** Kill all in-process local PTYs. Call on app quit. */ - killAll(): void { - killAllLocalPtys() - } -} - -export function _resetLocalPtyProviderStateForTest(): void { - cancelAllPendingLocalPtySpawns() - pendingLocalPtySpawns.clear() - for (const id of ptyProcesses.keys()) { - clearPtyState(id) - } - resetLoadGeneration() -} diff --git a/src/main/providers/local-pty-runtime-spawn.ts b/src/main/providers/local-pty-runtime-spawn.ts deleted file mode 100644 index 7d3c26d90ade..000000000000 --- a/src/main/providers/local-pty-runtime-spawn.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { canUseBunPty, spawnBunPty } from '../daemon/pty-subprocess/bun-pty-process' -import { spawnNativeDaemonPty } from '../daemon/pty-subprocess/native-pty-spawn' -import { - spawnShellWithFallback, - type ShellSpawnParams, - type ShellSpawnResult -} from './local-pty-utils' - -type LocalPtySpawn = ( - params: Omit & { signal?: AbortSignal } -) => ShellSpawnResult | Promise - -/** Degraded daemon routing uses the same runtime as the packaged host. */ -export async function loadLocalPtyRuntimeSpawn(): Promise { - if (!canUseBunPty()) { - const pty = await import('node-pty') - return (params) => spawnShellWithFallback({ ...params, ptySpawn: pty.spawn }) - } - if (process.platform === 'win32') { - return (params) => - spawnNativeDaemonPty({ - ...params, - spawnCwd: params.cwd, - windowsFallbackAttempts: params.windowsFallbackAttempts ?? [] - }) - } - return (params) => - spawnShellWithFallback({ - ...params, - ptySpawn(file, args = [], options = {}) { - if (!Array.isArray(args)) { - throw new Error('POSIX PTY arguments must be an array') - } - return spawnBunPty({ - file, - args, - cwd: options.cwd ?? params.cwd, - env: params.env, - cols: options.cols ?? params.cols, - rows: options.rows ?? params.rows - }) - } - }) -} diff --git a/src/main/providers/local-pty-session-activation.ts b/src/main/providers/local-pty-session-activation.ts deleted file mode 100644 index e89f0c433162..000000000000 --- a/src/main/providers/local-pty-session-activation.ts +++ /dev/null @@ -1,204 +0,0 @@ -import type * as pty from 'node-pty' -import { isBracketedPasteSafeShell } from '../../shared/startup-command-submission' -import { PtyStartupIngress, type PtyIngressEmission } from '../../shared/pty-startup-ingress' -import { resolvePtyOwnerBackend } from '../../shared/pty-owner-backend' -import { resolveProcessExitCause } from '../../shared/terminal-exit-cause' -import { POSIX_SHELL_STARTUP_COMMAND_ENV } from '../pty/posix-shell-startup-command' -import { getAgentForegroundContextPaths } from './agent-foreground-context-paths' -import { getSpawnedShellName } from './local-pty-launch-helpers' -import type { LocalPtyLaunchPlan } from './local-pty-launch-plan' -import type { LocalPtyProviderOptions } from './local-pty-provider-types' -import { - clearPtyState, - dataListeners, - exitListeners, - getLoadGeneration, - ptyAgentForegroundContextPaths, - ptyAgentSessionIds, - ptyDisposables, - ptyExitDisposables, - ptyIncarnations, - ptyInitialCwd, - ptyLoadGeneration, - ptyPhysicalExits, - ptyProcesses, - ptyReportsChildExitStatus, - ptyShellPath, - ptyTerminalHandle, - ptyTerminationMode, - ptyWorktreeId, - ptyWslDistroById, - startupIngressByPty -} from './local-pty-provider-state' -import { createLocalPtyShellReadinessSession } from './local-pty-shell-readiness-session' -import { destroyPtyProcess, createPtyPhysicalExit } from './local-pty-termination' -import { writeStartupCommandWhenShellReady } from './local-pty-shell-ready-startup-command' -import type { PtySpawnOptions, PtySpawnResult } from './types' - -export function activateLocalPtySession(args: { - id: string - incarnationId: string - spawn: PtySpawnOptions - getOptions: () => LocalPtyProviderOptions - plan: LocalPtyLaunchPlan - env: Record - proc: pty.IPty - reportsChildExitStatus: boolean - spawnedWslDistro: string | null | undefined -}): PtySpawnResult { - const { id, incarnationId, spawn, getOptions, plan, env, proc, spawnedWslDistro } = args - createPtyPhysicalExit(id) - ptyReportsChildExitStatus.set(id, args.reportsChildExitStatus) - ptyProcesses.set(id, proc) - ptyInitialCwd.set(id, plan.cwd) - if (spawnedWslDistro !== undefined) { - ptyWslDistroById.set(id, spawnedWslDistro) - } - // Why both: launchAgent is explicit intent that survives command rewrites; recognition catches bare agent command lines. - if (spawn.launchAgent || plan.startupAgentRecognition) { - ptyAgentSessionIds.add(id) - } - ptyShellPath.set(id, plan.shellPath) - if (env.ORCA_TERMINAL_HANDLE) { - ptyTerminalHandle.set(id, env.ORCA_TERMINAL_HANDLE) - } - if (spawn.worktreeId) { - ptyWorktreeId.set(id, spawn.worktreeId) - } - ptyAgentForegroundContextPaths.set( - id, - getAgentForegroundContextPaths({ cwd: spawn.cwd, worktreeId: spawn.worktreeId }) - ) - ptyLoadGeneration.set(id, getLoadGeneration()) - ptyIncarnations.set(id, incarnationId) - getOptions().onSpawned?.(id, incarnationId) - - const emitIngressData = (emission: PtyIngressEmission): void => { - const sequenceChars = emission.rawEndSeq - emission.rawStartSeq - if (emission.transformed || sequenceChars !== emission.data.length) { - getOptions().onData?.(id, emission.data, Date.now(), sequenceChars, true) - } else { - getOptions().onData?.(id, emission.data, Date.now()) - } - for (const cb of dataListeners) { - cb( - emission.transformed || sequenceChars !== emission.data.length - ? { - id, - data: emission.data, - sequenceChars, - seq: emission.rawEndSeq, - transformed: true - } - : { id, data: emission.data } - ) - } - } - const startupIngress = new PtyStartupIngress({ - ...(spawn.startupIngress ? { intent: spawn.startupIngress } : {}), - ownerBackend: resolvePtyOwnerBackend({ - platform: process.platform, - shellPath: plan.shellPath, - wslDistro: spawnedWslDistro - }), - write: (data) => proc.write(data), - onEmission: emitIngressData - }) - startupIngressByPty.set(id, startupIngress) - - // Shell-ready startup command support - const readiness = createLocalPtyShellReadinessSession({ - id, - spawn, - plan, - env, - proc, - startupIngress - }) - const disposables: { dispose: () => void }[] = [] - const onDataDisposable = proc.onData((rawData) => { - readiness.acceptData(rawData) - }) - if (onDataDisposable) { - disposables.push(onDataDisposable) - } - ptyDisposables.set(id, disposables) - - let exitedBeforeSpawnReply = false - const onExitDisposable = proc.onExit(({ exitCode, signal }) => { - exitedBeforeSpawnReply = true - // Why: node-pty reports a signalled death as {exitCode: 0, signal: N}; the - // cause is built here, where the signal and the spawn's trustworthiness - // are both still in hand. - const cause = resolveProcessExitCause({ - exitCode, - signal, - hostReportsChildExitStatus: ptyReportsChildExitStatus.get(id) - }) - const wasTerminationRequested = ptyTerminationMode.has(id) - ptyPhysicalExits.get(id)?.markExited() - // Why: neutralize proc.kill before destroy — node-pty SIGHUPs on socket 'close', which can race here and signal a reaped/recycled pid. - if (process.platform !== 'win32') { - ;(proc as unknown as { kill: (sig?: string) => void }).kill = () => {} - } - readiness.prepareForExit() - clearPtyState(id) - startupIngress.drainAndClose() - startupIngressByPty.delete(id) - // Why: release the master ptmx fd on natural exit, else a clean exit leaks the fd until GC. See docs/fix-pty-fd-leak.md. - destroyPtyProcess(proc, { alreadyKilled: wasTerminationRequested }) - ptyReportsChildExitStatus.delete(id) - getOptions().onExit?.(id, exitCode, incarnationId, cause) - for (const cb of exitListeners) { - cb({ id, code: exitCode, incarnationId, cause }) - } - }) - if (onExitDisposable) { - if (exitedBeforeSpawnReply) { - onExitDisposable.dispose() - } else { - ptyExitDisposables.set(id, onExitDisposable) - } - } - - const startupCommandDeliveredByWrapper = - spawn.command !== undefined && - plan.shellReadyLaunch?.env[POSIX_SHELL_STARTUP_COMMAND_ENV] === spawn.command - if ( - !exitedBeforeSpawnReply && - spawn.command && - !plan.startupCommandDeliveredInShellArgs && - !startupCommandDeliveredByWrapper - ) { - // Why: shells with bracketed paste armed take a multiline startup prompt literally; others use raw submit. - const spawnedShellName = getSpawnedShellName(plan.shellPath).toLowerCase() - const bracketedPasteSafe = - process.platform !== 'win32' && - isBracketedPasteSafeShell({ - shellName: spawnedShellName, - waitsForShellReady: plan.shellReadyLaunch?.supportsReadyMarker === true - }) - writeStartupCommandWhenShellReady( - readiness.shellReadyPromise, - proc, - spawn.command, - (cleanup) => { - readiness.setStartupCommandCleanup(cleanup) - }, - { - bracketedPasteSafe - } - ) - } - - // Why: publish the OS pid for the memory collector; proc.pid can be briefly 0/undefined before node-pty sees the child. - const rawPid = proc.pid - const pid = typeof rawPid === 'number' && Number.isFinite(rawPid) && rawPid > 0 ? rawPid : null - return { - id, - incarnationId, - pid, - ...(exitedBeforeSpawnReply ? { exitedBeforeSpawnReply: true } : {}), - ...(spawnedWslDistro !== undefined ? { wslDistro: spawnedWslDistro } : {}) - } -} diff --git a/src/main/providers/local-pty-session-operations.ts b/src/main/providers/local-pty-session-operations.ts deleted file mode 100644 index 27a151580ce8..000000000000 --- a/src/main/providers/local-pty-session-operations.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { existsSync } from 'node:fs' -import { basename } from 'node:path' -import type * as pty from 'node-pty' -import { readPtsName } from '../pty/node-pty-pts-name' -import { signalPosixPtyForegroundGroup } from '../pty/posix-pty-foreground-group' -import { ptyShellProcessId } from '../windows/windows-pty-job' -import { isWslAvailableAsync } from '../wsl' -import { resolveGitBashPath } from '../git-bash' -import { resolveProcessCwd } from './process-cwd' -import { - dataListeners, - exitListeners, - ptyIncarnations, - ptyInitialCwd, - ptyProcesses, - getPtyShellName, - ptyTerminalHandle, - ptyWorktreeId, - ptyWslDistroById, - startupIngressByPty, - type DataCallback, - type ExitCallback -} from './local-pty-provider-state' -import type { LocalPtyProviderOptions } from './local-pty-provider-types' -import type { PtyProcessInfo } from './types' - -export function writeLocalPty(id: string, data: string): boolean { - // Cooked PTYs echo private DSR/OSC replies; CPR/DA stay immediate unless one of - // those is still held, which they must not overtake (#13137, #7329, #15559). - if (startupIngressByPty.get(id)?.answerLiveQueryReply(data)) { - return true - } - const proc = ptyProcesses.get(id) - if (!proc) { - return false - } - proc.write(data) - return true -} - -export function resizeLocalPty(id: string, cols: number, rows: number): void { - ptyProcesses.get(id)?.resize(cols, rows) -} - -// Why: node-pty pause() stops reading the master fd, so a flooding child blocks on write — true producer backpressure. -export function pauseLocalPtyProducer(id: string): void { - try { - ptyProcesses.get(id)?.pause() - } catch { - /* PTY already destroyed */ - } -} - -export function resumeLocalPtyProducer(id: string): void { - try { - ptyProcesses.get(id)?.resume() - } catch { - /* PTY already destroyed */ - } -} - -// Why: proc.cols/rows are node-pty's authoritative applied size (post-clamp/no-op), used by the renderer drift-check. -export async function getLocalPtyAppliedSize( - id: string -): Promise<{ cols: number; rows: number } | null> { - const proc = ptyProcesses.get(id) - if (!proc || proc.cols <= 0 || proc.rows <= 0) { - return null - } - return { cols: proc.cols, rows: proc.rows } -} - -export async function sendLocalPtySignal(id: string, signal: string): Promise { - const proc = ptyProcesses.get(id) - if (!proc) { - return - } - if ('signalProcess' in proc && typeof proc.signalProcess === 'function') { - proc.signalProcess(signal) - return - } - const signalRootPid = (): void => { - try { - process.kill(proc.pid, signal) - } catch { - /* Process may already be dead */ - } - } - // Why only SIGWINCH: see posix-pty-foreground-group — a real resize reaches the - // tty's foreground group, which proc.pid is never a member of. - if (signal === 'SIGWINCH') { - signalPosixPtyForegroundGroup(proc.pid, readPtsName(proc), signal, signalRootPid) - return - } - signalRootPid() -} - -export async function getLocalPtyCwd(id: string): Promise { - const proc = ptyProcesses.get(id) - // Why: '' not throw on unknown id — renderer reads empty as "try next fallback"; throwing is noisy for a normal case. - if (!proc) { - return '' - } - // Why: let resolveProcessCwd's '' surface for the renderer fallback chain; a fabricated cwd would short-circuit it. - const shellPid = ptyShellProcessId(proc) - return shellPid === undefined ? '' : resolveProcessCwd(shellPid) -} - -export async function clearLocalPtyBuffer(id: string): Promise { - // Why: ConPTY keeps its own screen buffer, so xterm clear() alone leaves a stale-cursor gap on the next prompt; POSIX no-op. - // No PSReadLine form-feed nudge here (unlike the daemon): safe only at an empty prompt, which this provider can't detect. - try { - startupIngressByPty.get(id)?.snapshotBarrier() - ptyProcesses.get(id)?.clear() - } catch { - /* PTY may have just exited */ - } -} - -export function closeLocalPtyStartupQueryAuthority(id: string): number { - return startupIngressByPty.get(id)?.closeQueryAuthority() ?? 0 -} - -export async function listLocalPtyProcesses(): Promise { - return Array.from(ptyProcesses.entries()).map(([id, proc]) => ({ - id, - ...(ptyIncarnations.get(id) ? { incarnationId: ptyIncarnations.get(id) } : {}), - cwd: ptyInitialCwd.get(id) ?? '', - title: proc.process || getPtyShellName(id) || 'shell', - ...(ptyWorktreeId.get(id) ? { worktreeId: ptyWorktreeId.get(id) } : {}), - ...(ptyTerminalHandle.get(id) ? { terminalHandle: ptyTerminalHandle.get(id) } : {}), - ...(ptyWslDistroById.has(id) ? { wslDistro: ptyWslDistroById.get(id) ?? null } : {}) - })) -} - -export async function getDefaultLocalPtyShell( - getOptions: () => LocalPtyProviderOptions -): Promise { - if (process.platform === 'win32') { - return getOptions().getWindowsShell?.() || process.env.COMSPEC || 'powershell.exe' - } - return process.env.SHELL?.trim() || '/bin/zsh' -} - -export async function getLocalPtyProfiles(): Promise<{ name: string; path: string }[]> { - if (process.platform === 'win32') { - const profiles: { name: string; path: string }[] = [ - { name: 'PowerShell', path: 'powershell.exe' }, - { name: 'Command Prompt', path: 'cmd.exe' } - ] - const gitBashPath = resolveGitBashPath() - if (gitBashPath) { - profiles.push({ name: 'Git Bash', path: gitBashPath }) - } - if (await isWslAvailableAsync()) { - profiles.push({ name: 'WSL', path: 'wsl.exe' }) - } - return profiles - } - const shells = ['/bin/zsh', '/bin/bash', '/bin/sh'] - return shells.filter((s) => existsSync(s)).map((s) => ({ name: basename(s), path: s })) -} - -export function onLocalPtyData(callback: DataCallback): () => void { - dataListeners.add(callback) - return () => dataListeners.delete(callback) -} - -export function onLocalPtyExit(callback: ExitCallback): () => void { - exitListeners.add(callback) - return () => exitListeners.delete(callback) -} - -export function getLocalPtyProcess(id: string): pty.IPty | undefined { - return ptyProcesses.get(id) -} diff --git a/src/main/providers/local-pty-shell-readiness-session.ts b/src/main/providers/local-pty-shell-readiness-session.ts deleted file mode 100644 index d005a4ae8639..000000000000 --- a/src/main/providers/local-pty-shell-readiness-session.ts +++ /dev/null @@ -1,149 +0,0 @@ -import type * as pty from 'node-pty' -import type { PtyStartupIngress } from '../../shared/pty-startup-ingress' -import { readPtySlavePath } from '../../shared/pty-slave-line-discipline-echo' -import { - createShellPromptReadinessProbe, - type ShellPromptReadinessProbe -} from '../shell-prompt-readiness-probe' -import { - createShellStartupOutputScanState, - drainShellStartupOutputScanState, - scanShellStartupOutput -} from '../shell-startup-output-scanner' -import type { LocalPtyLaunchPlan } from './local-pty-launch-plan' -import { ptyCleanupCallbacks } from './local-pty-provider-state' -import { - STARTUP_COMMAND_READY_MAX_WAIT_MS, - type ShellReadySignal -} from './local-pty-shell-ready-startup-command' -import type { PtySpawnOptions } from './types' - -export type LocalPtyShellReadinessSession = { - shellReadyPromise: Promise - acceptData(rawData: string): void - setStartupCommandCleanup(cleanup: () => void): void - prepareForExit(): void -} - -export function createLocalPtyShellReadinessSession(args: { - id: string - spawn: PtySpawnOptions - plan: LocalPtyLaunchPlan - env: Record - proc: pty.IPty - startupIngress: PtyStartupIngress -}): LocalPtyShellReadinessSession { - const { id, spawn, plan, env, proc, startupIngress } = args - let resolveShellReady: ((signal: ShellReadySignal) => void) | null = null - let shellReadyTimeout: ReturnType | null = null - let shellStartupPid: number | null = null - let shellPromptReadinessProbe: ShellPromptReadinessProbe | null = null - let shellStartupOutputScanState = plan.shellReadyLaunch?.supportsReadyMarker - ? createShellStartupOutputScanState() - : null - const shellReadyPromise = spawn.command - ? new Promise((resolve) => { - resolveShellReady = resolve - }) - : Promise.resolve({ postMarkerBytesObserved: false }) - const finishShellReady = (signal: ShellReadySignal): void => { - if (!resolveShellReady) { - return - } - if (shellReadyTimeout) { - clearTimeout(shellReadyTimeout) - shellReadyTimeout = null - } - shellPromptReadinessProbe?.dispose() - shellPromptReadinessProbe = null - const resolve = resolveShellReady - resolveShellReady = null - resolve(signal) - } - const releaseHeldShellReadyBytes = (): void => { - if (!shellStartupOutputScanState) { - return - } - const heldBytes = drainShellStartupOutputScanState(shellStartupOutputScanState) - shellStartupOutputScanState = null - if (heldBytes.length === 0) { - return - } - startupIngress.accept(heldBytes) - } - if (shellStartupOutputScanState) { - shellPromptReadinessProbe = createShellPromptReadinessProbe({ - ptyPid: proc.pid, - slavePath: readPtySlavePath(proc), - shellPath: plan.shellPath, - shellCwd: plan.effectiveCwd, - shellPathEnv: env.PATH, - getShellPid: () => shellStartupPid, - onPromptReady: () => { - console.warn( - `[pty] ${id}: shell-ready wrapper was replaced before its marker; releasing at the identified shell prompt. OSC 133 integration may be unavailable.` - ) - releaseHeldShellReadyBytes() - finishShellReady({ postMarkerBytesObserved: true }) - } - }) - } - if (spawn.command) { - if (plan.shellReadyLaunch?.supportsReadyMarker) { - shellReadyTimeout = setTimeout(() => { - releaseHeldShellReadyBytes() - finishShellReady({ postMarkerBytesObserved: false }) - }, STARTUP_COMMAND_READY_MAX_WAIT_MS) - } else { - finishShellReady({ postMarkerBytesObserved: false }) - } - } - let startupCommandCleanup: (() => void) | null = null - if (spawn.command) { - ptyCleanupCallbacks.set(id, () => { - if (shellReadyTimeout) { - clearTimeout(shellReadyTimeout) - shellReadyTimeout = null - } - releaseHeldShellReadyBytes() - startupCommandCleanup?.() - startupCommandCleanup = null - resolveShellReady = null - shellPromptReadinessProbe?.dispose() - shellPromptReadinessProbe = null - }) - } - - return { - shellReadyPromise, - acceptData: (rawData) => { - let data = rawData - if (shellStartupOutputScanState && resolveShellReady) { - const scanned = scanShellStartupOutput(shellStartupOutputScanState, data) - data = scanned.output - if (scanned.shellPid) { - shellStartupPid = scanned.shellPid - } - if (scanned.ready) { - finishShellReady({ postMarkerBytesObserved: scanned.postMarkerBytesObserved }) - } - } - startupIngress.accept(data) - if (resolveShellReady && data.length > 0) { - shellPromptReadinessProbe?.notifyOutput(data) - } - }, - setStartupCommandCleanup: (cleanup) => { - startupCommandCleanup = cleanup - }, - prepareForExit: () => { - if (shellReadyTimeout) { - clearTimeout(shellReadyTimeout) - shellReadyTimeout = null - } - startupCommandCleanup?.() - shellPromptReadinessProbe?.dispose() - shellPromptReadinessProbe = null - } - } -} diff --git a/src/main/providers/local-pty-shell-ready-startup-command.test.ts b/src/main/providers/local-pty-shell-ready-startup-command.test.ts deleted file mode 100644 index da843c6e42ad..000000000000 --- a/src/main/providers/local-pty-shell-ready-startup-command.test.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type * as pty from 'node-pty' -import { writeStartupCommandWhenShellReady } from './local-pty-shell-ready-startup-command' - -type DataCb = (data: string) => void -type ExitCb = (info: { exitCode: number }) => void - -function createMockProc(): pty.IPty & { - _emitData: (data: string) => void - _writes: string[] -} { - let onDataCbs: DataCb[] = [] - const writes: string[] = [] - const fake = { - pid: 1, - cols: 80, - rows: 24, - process: 'bash', - handleFlowControl: false, - write: (data: string) => { - writes.push(data) - }, - resize: () => {}, - clear: () => {}, - kill: () => {}, - pause: () => {}, - resume: () => {}, - onData: (cb: DataCb) => { - onDataCbs.push(cb) - return { - dispose: () => { - onDataCbs = onDataCbs.filter((c) => c !== cb) - } - } - }, - onExit: (_cb: ExitCb) => ({ dispose: () => {} }), - _emitData: (data: string) => { - for (const cb of onDataCbs.slice()) { - cb(data) - } - }, - _writes: writes - } as unknown as pty.IPty & { _emitData: (data: string) => void; _writes: string[] } - - return fake -} - -describe('writeStartupCommandWhenShellReady', () => { - let origPlatform: NodeJS.Platform - - beforeEach(() => { - vi.useFakeTimers() - origPlatform = process.platform - }) - - afterEach(() => { - vi.useRealTimers() - Object.defineProperty(process, 'platform', { value: origPlatform }) - }) - - it('appends LF on POSIX so bash/zsh submit the line', async () => { - Object.defineProperty(process, 'platform', { value: 'darwin' }) - const proc = createMockProc() - const ready = Promise.resolve() - writeStartupCommandWhenShellReady(ready, proc, 'claude', () => {}) - - await ready - proc._emitData('\r\nuser@host % ') - vi.advanceTimersByTime(30) - await Promise.resolve() - - expect(proc._writes).toEqual(['claude\n']) - }) - - it('appends CR on Windows so PowerShell/cmd.exe submit the line', async () => { - Object.defineProperty(process, 'platform', { value: 'win32' }) - const proc = createMockProc() - const ready = Promise.resolve() - writeStartupCommandWhenShellReady(ready, proc, 'claude', () => {}) - - await ready - proc._emitData('\r\nPS> ') - vi.advanceTimersByTime(30) - await Promise.resolve() - - expect(proc._writes).toEqual(['claude\r']) - }) - - it('does not re-append a submit byte if the command already ends in CR or LF', async () => { - Object.defineProperty(process, 'platform', { value: 'win32' }) - const proc = createMockProc() - const ready = Promise.resolve() - writeStartupCommandWhenShellReady(ready, proc, 'claude\n', () => {}) - - await ready - proc._emitData('\r\nPS> ') - vi.advanceTimersByTime(30) - await Promise.resolve() - - expect(proc._writes).toEqual(['claude\n']) - }) - - it('keeps the no-prompt fallback conservative to avoid duplicate shell echo', async () => { - Object.defineProperty(process, 'platform', { value: 'darwin' }) - const proc = createMockProc() - const ready = Promise.resolve() - writeStartupCommandWhenShellReady(ready, proc, 'codex', () => {}) - - await ready - vi.advanceTimersByTime(50) - await Promise.resolve() - - expect(proc._writes).toEqual([]) - - vi.advanceTimersByTime(150) - await Promise.resolve() - - expect(proc._writes).toEqual(['codex\n']) - }) - - it('uses the short settle delay when marker scan already observed post-marker bytes', async () => { - Object.defineProperty(process, 'platform', { value: 'darwin' }) - const proc = createMockProc() - const ready = Promise.resolve({ postMarkerBytesObserved: true }) - writeStartupCommandWhenShellReady(ready, proc, 'codex', () => {}) - - await ready - vi.advanceTimersByTime(29) - await Promise.resolve() - expect(proc._writes).toEqual([]) - - vi.advanceTimersByTime(1) - await Promise.resolve() - expect(proc._writes).toEqual(['codex\n']) - }) - - // Why: multiline startup commands must be bracketed-paste wrapped (ESC[200~ … ESC[201~) so shells insert them literally instead of treating each LF as Enter. - it('wraps a multiline startup command in bracketed paste when the shell supports it', async () => { - Object.defineProperty(process, 'platform', { value: 'darwin' }) - const proc = createMockProc() - const ready = Promise.resolve() - const command = "claude '--dangerously-skip-permissions' 'line one\nline two'" - writeStartupCommandWhenShellReady(ready, proc, command, () => {}, { - bracketedPasteSafe: true - }) - - await ready - proc._emitData('\r\nuser@host % ') - vi.advanceTimersByTime(30) - await Promise.resolve() - - expect(proc._writes).toEqual([`\x1b[200~${command}\x1b[201~\n`]) - }) - - it('leaves a single-line command on the raw submit path even when bracketed paste is safe', async () => { - Object.defineProperty(process, 'platform', { value: 'darwin' }) - const proc = createMockProc() - const ready = Promise.resolve() - writeStartupCommandWhenShellReady(ready, proc, 'claude', () => {}, { - bracketedPasteSafe: true - }) - - await ready - proc._emitData('\r\nuser@host % ') - vi.advanceTimersByTime(30) - await Promise.resolve() - - expect(proc._writes).toEqual(['claude\n']) - }) - - it('does not bracket-wrap a multiline command when the shell lacks bracketed paste', async () => { - Object.defineProperty(process, 'platform', { value: 'darwin' }) - const proc = createMockProc() - const ready = Promise.resolve() - const command = 'echo one\necho two' - // Why: bracketedPasteSafe defaults false, so keep the raw path to avoid echoing ESC[200~ on shells without bracketed paste. - writeStartupCommandWhenShellReady(ready, proc, command, () => {}) - - await ready - proc._emitData('\r\nuser@host % ') - vi.advanceTimersByTime(30) - await Promise.resolve() - - expect(proc._writes).toEqual([`${command}\n`]) - }) -}) diff --git a/src/main/providers/local-pty-shell-ready-startup-command.ts b/src/main/providers/local-pty-shell-ready-startup-command.ts deleted file mode 100644 index bf7f73c19d79..000000000000 --- a/src/main/providers/local-pty-shell-ready-startup-command.ts +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Writes a startup command into a local PTY once the shell reports readiness. - */ -import type * as pty from 'node-pty' -import { buildStartupCommandSubmission } from '../../shared/startup-command-submission' - -export const STARTUP_COMMAND_READY_MAX_WAIT_MS = 1500 -const POST_SHELL_READY_STARTUP_COMMAND_DELAY_MS = 30 -const POST_SHELL_READY_STARTUP_COMMAND_FALLBACK_MS = 200 - -export type ShellReadySignal = { - postMarkerBytesObserved: boolean -} - -export function writeStartupCommandWhenShellReady( - readyPromise: Promise, - proc: pty.IPty, - startupCommand: string, - onExit: (cleanup: () => void) => void, - // Why: only shells with bracketed-paste active (see isBracketedPasteSafeShell) accept the wrapper; others use the raw path so ESC[200~ isn't echoed. - options: { bracketedPasteSafe?: boolean } = {} -): void { - let sent = false - let postReadyTimer: ReturnType | null = null - let postReadyDataDisposable: { dispose: () => void } | null = null - - const cleanup = (): void => { - sent = true - if (postReadyTimer !== null) { - clearTimeout(postReadyTimer) - postReadyTimer = null - } - postReadyDataDisposable?.dispose() - postReadyDataDisposable = null - } - - const flush = (): void => { - if (sent) { - return - } - sent = true - postReadyDataDisposable?.dispose() - postReadyDataDisposable = null - if (postReadyTimer !== null) { - clearTimeout(postReadyTimer) - postReadyTimer = null - } - // Why: run in the same interactive shell (not `shell -c`) so the session survives after the agent exits. - // Why CR on Windows: PSReadLine/cmd.exe submit on `\r`, not LF; POSIX treats either as Enter under ICRNL. - const submit = process.platform === 'win32' ? '\r' : '\n' - // Why: single write after the ready barrier avoids incremental-paste char drops; multiline is bracketed-paste wrapped so newlines don't submit early. - proc.write( - buildStartupCommandSubmission(startupCommand, { - submit, - bracketedPasteSafe: options.bracketedPasteSafe === true - }) - ) - } - - const schedulePostReadyFlush = (): void => { - postReadyTimer = setTimeout(flush, POST_SHELL_READY_STARTUP_COMMAND_DELAY_MS) - } - - readyPromise.then((signal) => { - if (sent) { - return - } - // Why: marker fires from precmd before the line editor takes the PTY out of ECHO; writing now double-echoes the command, so settle first. - if (signal?.postMarkerBytesObserved === true) { - schedulePostReadyFlush() - return - } - postReadyDataDisposable = proc.onData(() => { - postReadyDataDisposable?.dispose() - postReadyDataDisposable = null - if (postReadyTimer !== null) { - clearTimeout(postReadyTimer) - } - schedulePostReadyFlush() - }) - postReadyTimer = setTimeout(() => { - postReadyDataDisposable?.dispose() - postReadyDataDisposable = null - postReadyTimer = null - flush() - }, POST_SHELL_READY_STARTUP_COMMAND_FALLBACK_MS) - }) - onExit(cleanup) -} diff --git a/src/main/providers/local-pty-shell-startup-command.node-pty.test.ts b/src/main/providers/local-pty-shell-startup-command.node-pty.test.ts deleted file mode 100644 index 936e10b2626c..000000000000 --- a/src/main/providers/local-pty-shell-startup-command.node-pty.test.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { spawnSync } from 'node:child_process' -import { mkdtempSync, rmSync } from 'node:fs' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import * as pty from 'node-pty' -import { afterEach, describe, expect, it } from 'vitest' -import { PtyStartupIngress } from '../../shared/pty-startup-ingress' -import type { TerminalViewRgb } from '../../shared/terminal-view-attributes' -import { HeadlessEmulator } from '../daemon/headless-emulator' -import { POSIX_SHELL_STARTUP_COMMAND_ENV } from '../pty/posix-shell-startup-command' -import { getShellLaunchConfig } from './local-pty-shell-ready' -import { setTestUserDataPath } from './local-pty-shell-ready-test-harness' - -const SHELLS = ['bash', 'zsh', 'fish'].filter( - (shell) => process.platform !== 'win32' && spawnSync(shell, ['--version']).status === 0 -) -const STARTUP_COMMAND = `bash -c 'read -r line; printf "__STARTUP_INPUT__:%s\\n" "$line"'` -const BLACK: TerminalViewRgb = [0, 0, 0] - -function afterCommand(shell: string): string { - if (shell === 'fish') { - return `if set -q ${POSIX_SHELL_STARTUP_COMMAND_ENV}; echo __AFTER_ENV__:present; else; echo __AFTER_ENV__:missing; end; exit\n` - } - return `printf '__AFTER_ENV__:%s\\n' "\${${POSIX_SHELL_STARTUP_COMMAND_ENV}-missing}"; exit\n` -} - -describe('local POSIX shell startup-command delivery', () => { - let testHome: string | undefined - - afterEach(() => { - if (testHome) { - rmSync(testHome, { recursive: true, force: true }) - testHome = undefined - } - }) - - it.each(SHELLS)( - '%s runs the command once, forwards stdin, and keeps the shell', - async (shell) => { - testHome = mkdtempSync(join(tmpdir(), `orca-${shell}-startup-command-`)) - setTestUserDataPath(testHome) - const launch = getShellLaunchConfig( - shell, - ['overlay', 'markers', 'ready', 'identity'], - STARTUP_COMMAND - ) - expect(launch.env[POSIX_SHELL_STARTUP_COMMAND_ENV]).toBe(STARTUP_COMMAND) - - const output = await new Promise((resolve, reject) => { - let proc!: pty.IPty - const emulator = new HeadlessEmulator({ - cols: 120, - rows: 30, - onQueryReply: (reply) => { - if (!ingress.answerLiveQueryReply(reply)) { - proc.write(reply) - } - } - }) - emulator.installViewAttributeResponder(() => ({ - foreground: [255, 255, 255], - background: BLACK, - cursor: [255, 255, 255], - ansi: Array.from({ length: 256 }, () => BLACK), - colorSchemeMode: 'dark', - cursorStyle: 'block', - cursorBlink: false - })) - proc = pty.spawn(shell, launch.args ?? [], { - cols: 120, - rows: 30, - cwd: testHome, - env: { - ...process.env, - ...launch.env, - HOME: testHome, - ORCA_ORIG_ZDOTDIR: testHome, - ORCA_ZSHENV_SOURCE_DIR: testHome, - TERM: 'xterm-256color' - } - }) - let transcript = '' - let sentInput = false - let sentAfter = false - const ingress = new PtyStartupIngress({ - ownerBackend: 'posix-pty', - write: (data) => proc.write(data), - onEmission: (emission) => { - transcript += emission.data - void emulator.write(emission.data, { forwardQueryReplies: true }) - if (!sentInput && transcript.includes(STARTUP_COMMAND)) { - sentInput = true - proc.write('hello\n') - } - if (!sentAfter && transcript.includes('__STARTUP_INPUT__:hello')) { - sentAfter = true - proc.write(afterCommand(shell)) - } - } - }) - const timeout = setTimeout(() => { - proc.kill() - emulator.dispose() - reject(new Error(`${shell} startup command timed out: ${JSON.stringify(transcript)}`)) - }, 5_000) - - proc.onData((data) => ingress.accept(data)) - proc.onExit(() => { - clearTimeout(timeout) - ingress.drainAndClose() - emulator.dispose() - resolve(transcript) - }) - }) - - expect(output.split(STARTUP_COMMAND)).toHaveLength(2) - expect(output).toContain('__STARTUP_INPUT__:hello') - expect(output).toContain('__AFTER_ENV__:missing') - } - ) -}) diff --git a/src/main/providers/local-pty-spawn-environment.ts b/src/main/providers/local-pty-spawn-environment.ts deleted file mode 100644 index 47aaac6cef0d..000000000000 --- a/src/main/providers/local-pty-spawn-environment.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { mergeGitConfigEnvProtocol } from '../../shared/git-credential-prompt-env' -import { - ORCA_IMAGE_PROTOCOL_ENV, - ORCA_IMAGE_PROTOCOL_VALUE -} from '../../shared/terminal-image-protocol' -import { removeAppImageRuntimeEnv } from '../pty/appimage-terminal-env' -import { stripInheritedBuildModeEnv } from '../pty/build-mode-env' -import { stripPiProcessOwnerEnv } from '../pty/pi-process-owner-env' -import { removeInheritedNoColor } from '../pty/terminal-color-env' -import { isWindowsGitBashShellPath } from '../git-bash' -import { removeUnspecifiedPaneIdentityEnv } from './local-pty-launch-helpers' -import type { LocalPtyLaunchPlan } from './local-pty-launch-plan' -import type { LocalPtyProviderOptions } from './local-pty-provider-types' -import type { PtySpawnOptions } from './types' - -/** Pane ownership must be fresh even when Orca itself was launched inside an agent. */ -export function buildLocalPtySpawnEnvironment(args: { - id: string - spawn: PtySpawnOptions - getOptions: () => LocalPtyProviderOptions - plan: LocalPtyLaunchPlan -}): Record | Promise> { - const { id, spawn, getOptions, plan } = args - const spawnEnv: Record = { - ...mergeGitConfigEnvProtocol(stripInheritedBuildModeEnv(process.env), spawn.env), - TERM: 'xterm-256color', - COLORTERM: 'truecolor', - TERM_PROGRAM: 'Orca', - // Why: TUIs feature-gate on TERM_PROGRAM_VERSION; the fallback keeps tests and non-Electron runs working. - TERM_PROGRAM_VERSION: process.env.ORCA_APP_VERSION ?? '0.0.0-dev', - // Why: supports-hyperlinks rejects TERM_PROGRAM=Orca, so tools drop OSC 8 links; force it since xterm.js parses them. - FORCE_HYPERLINK: '1', - [ORCA_IMAGE_PROTOCOL_ENV]: ORCA_IMAGE_PROTOCOL_VALUE - } satisfies Record - removeUnspecifiedPaneIdentityEnv(spawnEnv, spawn.env) - stripPiProcessOwnerEnv(spawnEnv) - removeAppImageRuntimeEnv(spawnEnv) - removeInheritedNoColor(spawnEnv) - for (const key of spawn.envToDelete ?? []) { - delete spawnEnv[key] - } - if (spawn.env?.TERM) { - spawnEnv.TERM = spawn.env.TERM - } - - spawnEnv.LANG ??= 'en_US.UTF-8' - - // Why: on Windows LANG doesn't set the console code page; PYTHONUTF8=1 forces Python UTF-8 stdio to avoid garbled CJK. - if (process.platform === 'win32') { - spawnEnv.PYTHONUTF8 ??= '1' - if (isWindowsGitBashShellPath(plan.shellPath)) { - // Why: Git for Windows login files otherwise cd to $HOME, ignoring node-pty's cwd for repo-scoped terminals. - spawnEnv.CHERE_INVOKING ??= '1' - } - } - - if (!getOptions().buildSpawnEnv) { - return spawnEnv - } - return getOptions().buildSpawnEnv!(id, spawnEnv, { - explicitEnv: spawn.env ?? {}, - command: spawn.command, - launchAgent: spawn.launchAgent, - codexHomePathOverride: spawn.codexHomePathOverride, - cwd: plan.cwd, - shellPath: plan.shellPath, - isWsl: plan.isWslShell, - wslDistro: plan.launchWslDistro - }) -} - -/** App-level env builders can reintroduce deleted keys; enforce isolation after they finish. */ -export function enforceLocalPtySpawnEnvironmentOverrides( - spawn: PtySpawnOptions, - finalEnv: Record -): void { - stripPiProcessOwnerEnv(finalEnv) - for (const key of spawn.envToDelete ?? []) { - delete finalEnv[key] - } - if (spawn.env?.TERM) { - finalEnv.TERM = spawn.env.TERM - } -} diff --git a/src/main/providers/local-pty-spawn-state.ts b/src/main/providers/local-pty-spawn-state.ts deleted file mode 100644 index d73d230f68b5..000000000000 --- a/src/main/providers/local-pty-spawn-state.ts +++ /dev/null @@ -1,83 +0,0 @@ -import type { PtySpawnResult } from './types' -import { - pendingLocalPtySpawns, - ptyIncarnations, - ptyProcesses, - ptyWslDistroById, - type PendingLocalPtySpawn -} from './local-pty-provider-state' - -const spawnReservations = new Map>() - -/** A Windows shell receipt can arrive after another request reaches the same native spawn. */ -export async function reserveLocalPtySpawn(id: string, operation: () => Promise): Promise { - const previous = spawnReservations.get(id) - const pending = previous ? previous.catch(() => {}).then(operation) : operation() - spawnReservations.set(id, pending) - try { - return await pending - } finally { - if (spawnReservations.get(id) === pending) { - spawnReservations.delete(id) - } - } -} - -/** Keep shutdown visible between awaits until the native process is registered. */ -export async function runCancelableLocalPtySpawn( - id: string, - operation: (throwIfCanceled: () => void, signal: AbortSignal) => Promise -): Promise { - const cancellation = new AbortController() - const pendingSpawn: PendingLocalPtySpawn = { cancellation } - const pending = pendingLocalPtySpawns.get(id) ?? new Set() - pending.add(pendingSpawn) - pendingLocalPtySpawns.set(id, pending) - try { - return await operation(() => cancellation.signal.throwIfAborted(), cancellation.signal) - } finally { - pending.delete(pendingSpawn) - if (pending.size === 0) { - pendingLocalPtySpawns.delete(id) - } - } -} - -export function cancelPendingLocalPtySpawns(id: string): void { - const pending = pendingLocalPtySpawns.get(id) - if (!pending) { - return - } - for (const pendingSpawn of pending) { - pendingSpawn.cancellation.abort(new Error(`PTY spawn canceled: ${id}`)) - } -} - -export function cancelAllPendingLocalPtySpawns(): void { - for (const id of pendingLocalPtySpawns.keys()) { - cancelPendingLocalPtySpawns(id) - } -} - -export function reattachLocalPty(id: string, cols: number, rows: number): PtySpawnResult | null { - const existing = ptyProcesses.get(id) - if (!existing) { - return null - } - let resized = false - try { - existing.resize(cols, rows) - resized = true - } catch { - /* Existing PTY may reject resize during teardown; still return the live handle. */ - } - return { - id, - ...(ptyIncarnations.has(id) ? { incarnationId: ptyIncarnations.get(id) } : {}), - pid: existing.pid, - ...(ptyWslDistroById.has(id) ? { wslDistro: ptyWslDistroById.get(id) ?? null } : {}), - isReattach: true, - // Why: unlike daemon/relay attach, this one really moved the live PTY to the caller's grid. - ...(resized ? { attachedGrid: { cols, rows } } : {}) - } -} diff --git a/src/main/providers/local-pty-spawn.ts b/src/main/providers/local-pty-spawn.ts deleted file mode 100644 index cc3c39979a87..000000000000 --- a/src/main/providers/local-pty-spawn.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { randomUUID } from 'node:crypto' -import { win32 as pathWin32 } from 'node:path' -import { SessionNotFoundError } from '../daemon/daemon-errors' -import { prepareMacosTccLoginShell } from './macos-tcc-login-shell' -import { finalizeLocalPtySpawnEnvironment } from './local-pty-finalize-environment' -import { normalizeLocalCallerSessionId } from './local-pty-launch-helpers' -import { createLocalPtyLaunchPlan, DeferredLocalPtyLaunchPlan } from './local-pty-launch-plan' -import type { LocalPtyProviderOptions } from './local-pty-provider-types' -import { allocatePtyId, ptyShutdownOperations } from './local-pty-provider-state' -import { activateLocalPtySession } from './local-pty-session-activation' -import { - buildLocalPtySpawnEnvironment, - enforceLocalPtySpawnEnvironmentOverrides -} from './local-pty-spawn-environment' -import { - runCancelableLocalPtySpawn, - reattachLocalPty, - reserveLocalPtySpawn -} from './local-pty-spawn-state' -import { loadLocalPtyRuntimeSpawn } from './local-pty-runtime-spawn' -import { destroyPtyProcess } from './local-pty-termination' -import { updateHistoryEnvForFallback } from '../terminal-history' -import type { PtySpawnOptions, PtySpawnResult } from './types' - -export async function spawnLocalPty( - args: PtySpawnOptions, - getOptions: () => LocalPtyProviderOptions -): Promise { - const reattachId = normalizeLocalCallerSessionId(args.sessionId, args.attachOnly === true) - if (reattachId) { - const pendingShutdown = ptyShutdownOperations.get(reattachId) - if (pendingShutdown) { - await pendingShutdown.promise - } - const existing = reattachLocalPty(reattachId, args.cols, args.rows) - if (existing) { - return existing - } - } - if (args.attachOnly) { - throw new SessionNotFoundError(args.sessionId ?? '') - } - const id = allocatePtyId(reattachId ?? undefined) - return runCancelableLocalPtySpawn(id, async (throwIfCanceled, cancellation) => { - const incarnationId = randomUUID() - let plan = createLocalPtyLaunchPlan(args, getOptions) - if (plan instanceof DeferredLocalPtyLaunchPlan) { - const available = await plan.availability - throwIfCanceled() - plan = plan.finish(available) - } - throwIfCanceled() - const envResult = buildLocalPtySpawnEnvironment({ - id, - spawn: args, - getOptions, - plan - }) - const finalEnv = envResult instanceof Promise ? await envResult : envResult - throwIfCanceled() - enforceLocalPtySpawnEnvironmentOverrides(args, finalEnv) - const historyResult = finalizeLocalPtySpawnEnvironment({ - spawn: args, - getOptions, - plan, - env: finalEnv - }) - - const fallbackHistory = historyResult?.historyDir ? historyResult : undefined - const [spawn] = await Promise.all([loadLocalPtyRuntimeSpawn(), prepareMacosTccLoginShell()]) - return reserveLocalPtySpawn(id, async () => { - const checkCanceled = (): void => { - throwIfCanceled() - if (args.signal?.aborted) { - throw new Error('client_disconnected') - } - } - checkCanceled() - // Why: another same-id request can win while this one awaits preflight; attach before launching a redundant shell. - const concurrentWinner = reattachId ? reattachLocalPty(id, args.cols, args.rows) : null - if (concurrentWinner) { - return concurrentWinner - } - const pendingSpawn = spawn({ - shellPath: plan.shellPath, - shellArgs: plan.shellArgs, - cols: args.cols, - rows: args.rows, - cwd: plan.effectiveCwd, - env: finalEnv, - termName: finalEnv.TERM, - signal: args.signal ? AbortSignal.any([args.signal, cancellation]) : cancellation, - getShellReadyConfig: plan.getFallbackShellReadyConfig, - launchEnvKeys: plan.primaryLaunchEnvKeys, - // Why: on zsh→bash fallback HISTFILE still points to zsh_history; update before spawn so the child inherits it (design doc §8). - onBeforeFallbackSpawn: fallbackHistory - ? (env, fallbackShell) => updateHistoryEnvForFallback(env, fallbackShell, fallbackHistory) - : undefined, - windowsFallbackAttempts: plan.windowsFallbackAttempts - }) - const spawnResult = pendingSpawn instanceof Promise ? await pendingSpawn : pendingSpawn - try { - checkCanceled() - } catch (error) { - try { - spawnResult.process.kill('SIGKILL') - } finally { - destroyPtyProcess(spawnResult.process) - } - throw error - } - args.onPtySpawnCommitted?.() - plan.shellPath = spawnResult.shellPath - // Why: a Windows fallback embeds its startup command in argv; honor the winning shell's delivery flag to avoid a double write. - if (spawnResult.startupCommandDeliveredInShellArgs !== undefined) { - plan.startupCommandDeliveredInShellArgs = spawnResult.startupCommandDeliveredInShellArgs - } - if (args.command && plan.getFallbackShellReadyConfig) { - plan.shellReadyLaunch = plan.getFallbackShellReadyConfig(plan.shellPath) - } - - if (process.platform !== 'win32') { - finalEnv.SHELL = plan.shellPath - } - - const proc = spawnResult.process - const spawnedShellIsWsl = - process.platform === 'win32' && - pathWin32.basename(plan.shellPath).toLowerCase() === 'wsl.exe' - const spawnedWslDistro = spawnedShellIsWsl - ? (plan.launchWslDistro ?? undefined) - : process.platform === 'win32' - ? null - : undefined - return activateLocalPtySession({ - id, - incarnationId, - spawn: args, - getOptions, - plan, - env: finalEnv, - proc, - reportsChildExitStatus: spawnResult.reportsChildExitStatus !== false, - spawnedWslDistro - }) - }) - }) -} diff --git a/src/main/providers/local-pty-termination.ts b/src/main/providers/local-pty-termination.ts deleted file mode 100644 index 426a7aad346e..000000000000 --- a/src/main/providers/local-pty-termination.ts +++ /dev/null @@ -1,263 +0,0 @@ -import type * as pty from 'node-pty' -import { PhysicalExitTracker } from '../../shared/physical-exit-tracker' -import { killWithDescendantSweep } from '../pty-descendant-termination' -import { forceKillPosixPtyProcessGroups } from '../pty/posix-pty-process-groups' -import { terminatePtyJob } from '../windows/windows-pty-job' -import { - clearLocalPtyForceKillTimer, - clearPtyState, - disposePtyExitListener, - disposePtyListeners, - ptyAgentSessionIds, - ptyForceKillTimers, - ptyPhysicalExits, - ptyProcesses, - ptyShutdownOperations, - ptyTerminationMode, - ptyLoadGeneration, - runPtyCleanup, - type PtyShutdownOperation -} from './local-pty-provider-state' -import { - cancelAllPendingLocalPtySpawns, - cancelPendingLocalPtySpawns -} from './local-pty-spawn-state' - -export const LOCAL_PTY_PHYSICAL_EXIT_TIMEOUT_MS = 8_000 -export const LOCAL_PTY_GRACEFUL_FORCE_TIMEOUT_MS = 5_000 -export const LOCAL_PTY_FORCE_KILL_RETRY_MS = 250 - -export function createPtyPhysicalExit(id: string): void { - ptyPhysicalExits.set(id, new PhysicalExitTracker()) -} - -function waitForPtyPhysicalExit(id: string, physicalExit?: PhysicalExitTracker): Promise { - if (!physicalExit) { - return Promise.reject(new Error(`PTY "${id}" exit tracking unavailable`)) - } - return physicalExit.waitForExit( - LOCAL_PTY_PHYSICAL_EXIT_TIMEOUT_MS, - () => new Error(`Timed out waiting for PTY process exit: ${id}`) - ) -} - -function killLocalPtyProcess(proc: pty.IPty, immediate: boolean): void { - if (process.platform === 'win32') { - proc.kill() - return - } - if (!immediate) { - proc.kill('SIGTERM') - return - } - forceKillPosixPtyProcessGroups(proc.pid, () => proc.kill('SIGKILL')) -} - -function armLocalPtyForceKill( - id: string, - proc: pty.IPty, - options: { delayMs?: number; attemptsRemaining?: number } = {} -): void { - if (ptyProcesses.get(id) !== proc || ptyTerminationMode.get(id) !== 'graceful') { - return - } - const attemptsRemaining = options.attemptsRemaining ?? 2 - const timer = setTimeout(() => { - ptyForceKillTimers.delete(id) - if (ptyProcesses.get(id) !== proc || ptyTerminationMode.get(id) !== 'graceful') { - return - } - ptyTerminationMode.set(id, 'force') - try { - killLocalPtyProcess(proc, true) - } catch (error) { - ptyTerminationMode.set(id, 'graceful') - console.error('[pty] failed to force-kill PTY after graceful deadline', { id, error }) - // Why: a transient native rejection must not consume the only SIGKILL owner while shutdown still awaits physical exit. - if (attemptsRemaining > 1) { - armLocalPtyForceKill(id, proc, { - delayMs: LOCAL_PTY_FORCE_KILL_RETRY_MS, - attemptsRemaining: attemptsRemaining - 1 - }) - } - } - }, options.delayMs ?? LOCAL_PTY_GRACEFUL_FORCE_TIMEOUT_MS) - timer.unref?.() - ptyForceKillTimers.set(id, timer) -} - -/** - * Disposes the native PTY handle while avoiding recycled-pid signals on POSIX. - */ -export function destroyPtyProcess(proc: pty.IPty, options: { alreadyKilled?: boolean } = {}): void { - // Why: neutralize proc.kill before destroy(), whose close-listener SIGHUPs a possibly-recycled POSIX pid; destroy() frees the ptmx fd (docs/fix-pty-fd-leak.md); on Windows destroy() is itself kill(). - if (process.platform === 'win32' && options.alreadyKilled) { - return - } - if (process.platform !== 'win32') { - ;(proc as unknown as { kill: (sig?: string) => void }).kill = () => {} - } - try { - ;(proc as unknown as { destroy?: () => void }).destroy?.() - } catch { - /* swallow — already torn down */ - } -} - -/** - * Requests local PTY termination while retaining physical-exit ownership. - */ -function requestPtyTermination(id: string, proc: pty.IPty): void { - runPtyCleanup(id) - disposePtyListeners(id) - const previousMode = ptyTerminationMode.get(id) - // Why: cleanup neutralizes proc.kill below, so escalate an outstanding graceful request before its deadline is disabled. - if (previousMode !== 'force') { - clearLocalPtyForceKillTimer(id) - ptyTerminationMode.set(id, 'force') - try { - killLocalPtyProcess(proc, true) - } catch { - if (previousMode === 'graceful') { - ptyTerminationMode.set(id, previousMode) - armLocalPtyForceKill(id, proc, { - delayMs: LOCAL_PTY_FORCE_KILL_RETRY_MS, - attemptsRemaining: 1 - }) - } else { - ptyTerminationMode.delete(id) - } - /* Process may already be dead. */ - return - } - } - // Why: shutdown and orphan cleanup can race; keep onExit + tracker installed until the OS proves the child was reaped. - destroyPtyProcess(proc, { alreadyKilled: true }) -} - -function requestTrackedPtyShutdown(id: string, proc: pty.IPty, immediate: boolean): void { - const previousMode = ptyTerminationMode.get(id) - // Why: ConPTY has no graceful signal — its first bare kill closes the pseudoconsole, so treat it as a final force request. - const requestedMode = immediate || process.platform === 'win32' ? 'force' : 'graceful' - if (!previousMode || (requestedMode === 'force' && previousMode !== 'force')) { - ptyTerminationMode.set(id, requestedMode) - try { - killLocalPtyProcess(proc, immediate) - if (requestedMode === 'graceful') { - armLocalPtyForceKill(id, proc) - } else { - clearLocalPtyForceKillTimer(id) - } - } catch (error) { - if (previousMode) { - ptyTerminationMode.set(id, previousMode) - } else { - ptyTerminationMode.delete(id) - } - throw error - } - } -} - -async function shutdownTrackedPty( - id: string, - proc: pty.IPty, - operation: PtyShutdownOperation -): Promise { - const physicalExit = ptyPhysicalExits.get(id) - const signalRoot = (): void => { - // Why: natural exit can race the sweep — never signal after this PTY loses ownership. - if (ptyProcesses.get(id) !== proc) { - return - } - // Cancel startup delivery now, but keep the exit listener and ownership maps until node-pty reports physical exit. - runPtyCleanup(id) - operation.rootSignalled = true - requestTrackedPtyShutdown(id, proc, operation.immediate) - } - if (ptyAgentSessionIds.has(id) || operation.immediate) { - // Typed agents also detach tool process groups; immediate close must snapshot before root exit. - await killWithDescendantSweep(proc.pid, signalRoot, { - ownsRoot: () => ptyProcesses.get(id) === proc, - terminateOwnedTree: () => terminatePtyJob(proc) - }) - } else { - signalRoot() - } - await waitForPtyPhysicalExit(id, physicalExit) -} - -export async function shutdownLocalPty( - id: string, - opts: { immediate?: boolean; keepHistory?: boolean } -): Promise { - cancelPendingLocalPtySpawns(id) - const pending = ptyShutdownOperations.get(id) - if (pending) { - if (opts.immediate === true) { - pending.immediate = true - if (pending.rootSignalled && ptyProcesses.get(id) === pending.proc) { - requestTrackedPtyShutdown(id, pending.proc, true) - } - } - await pending.promise - return - } - const proc = ptyProcesses.get(id) - if (!proc) { - return - } - const entry: PtyShutdownOperation = { - promise: Promise.resolve(), - immediate: opts.immediate === true, - rootSignalled: false, - proc - } - entry.promise = shutdownTrackedPty(id, proc, entry) - ptyShutdownOperations.set(id, entry) - try { - await entry.promise - } finally { - if (ptyShutdownOperations.get(id) === entry) { - ptyShutdownOperations.delete(id) - } - } -} - -export function killOrphanedLocalPtys(currentGeneration: number): { id: string }[] { - const killed: { id: string }[] = [] - for (const [id, proc] of ptyProcesses) { - if ((ptyLoadGeneration.get(id) ?? -1) < currentGeneration) { - requestPtyTermination(id, proc) - killed.push({ id }) - } - } - return killed -} - -export function killAllLocalPtys(): void { - cancelAllPendingLocalPtySpawns() - for (const [id, proc] of ptyProcesses) { - runPtyCleanup(id) - disposePtyListeners(id) - disposePtyExitListener(id) - if (!(process.platform === 'win32' && ptyTerminationMode.has(id))) { - try { - if (ptyAgentSessionIds.has(id) && process.platform !== 'win32') { - // App quit is synchronous, so sweep attached agent process groups before - // releasing node-pty; otherwise OMP workers can outlive the foreground PTY. - forceKillPosixPtyProcessGroups(proc.pid, () => proc.kill('SIGKILL')) - } else { - proc.kill() - } - } catch { - /* Process may already be dead. */ - } - } - // Why: app quit can't retain NAPI callbacks into FreeEnvironment; process exit is the final handle boundary here. - destroyPtyProcess(proc, { alreadyKilled: true }) - // Why: app quit replaces node-pty's onExit as final owner; overlapping shutdown waiters must join this boundary. - ptyPhysicalExits.get(id)?.markExited() - clearPtyState(id) - } -} diff --git a/src/main/providers/local-pty-utils-windows-fallback.test.ts b/src/main/providers/local-pty-utils-windows-fallback.test.ts deleted file mode 100644 index a71537ba8b29..000000000000 --- a/src/main/providers/local-pty-utils-windows-fallback.test.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { afterEach, describe, expect, it, vi } from 'vitest' -import type * as pty from 'node-pty' -import { spawnShellWithFallback, type WindowsShellSpawnAttempt } from './local-pty-utils' - -function setPlatform(platform: NodeJS.Platform): () => void { - const original = process.platform - Object.defineProperty(process, 'platform', { configurable: true, value: platform }) - return () => Object.defineProperty(process, 'platform', { configurable: true, value: original }) -} - -let restorePlatform: (() => void) | null = null -afterEach(() => { - restorePlatform?.() - restorePlatform = null - vi.restoreAllMocks() -}) - -const PWSH7 = 'C:\\Program Files\\PowerShell\\7\\pwsh.exe' -const WINDOWS_POWERSHELL = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' -const CMD = 'C:\\Windows\\System32\\cmd.exe' - -function makeFakePty(): pty.IPty { - return { pid: 1234 } as unknown as pty.IPty -} - -function makeAttempt( - shellPath: string, - overrides: Partial = {} -): WindowsShellSpawnAttempt { - return { - shellPath, - shellArgs: ['-NoLogo'], - effectiveCwd: 'C:\\repo', - validationCwd: 'C:\\repo', - startupCommandDeliveredInShellArgs: false, - ...overrides - } -} - -// error code 5 == ERROR_ACCESS_DENIED from CreateProcessW inside ConPTY when a -// bare/alias pwsh.exe is handed to node-pty. -const ACCESS_DENIED_5 = 'Cannot create process, error code: 5' - -describe('spawnShellWithFallback on Windows', () => { - it('repro: recovers when the primary PowerShell spawn fails with error code 5', () => { - restorePlatform = setPlatform('win32') - vi.spyOn(console, 'warn').mockImplementation(() => {}) - - const attempts: WindowsShellSpawnAttempt[] = [ - makeAttempt(PWSH7), - makeAttempt(WINDOWS_POWERSHELL), - makeAttempt(CMD, { shellArgs: ['/K', 'chcp 65001 > nul'] }) - ] - - const ptySpawn = vi.fn((shellPath: string) => { - if (shellPath === PWSH7) { - throw new Error(ACCESS_DENIED_5) - } - return makeFakePty() - }) as unknown as typeof pty.spawn - - const result = spawnShellWithFallback({ - shellPath: PWSH7, - shellArgs: attempts[0].shellArgs, - cols: 80, - rows: 24, - cwd: 'C:\\repo', - env: {}, - ptySpawn, - windowsFallbackAttempts: attempts - }) - - // Falls back to the next real absolute executable instead of throwing. - expect(result.shellPath).toBe(WINDOWS_POWERSHELL) - expect(ptySpawn).toHaveBeenNthCalledWith( - 1, - PWSH7, - attempts[0].shellArgs, - expect.objectContaining({ cwd: 'C:\\repo', useConptyDll: true }) - ) - expect(ptySpawn).toHaveBeenNthCalledWith( - 2, - WINDOWS_POWERSHELL, - attempts[1].shellArgs, - expect.objectContaining({ cwd: 'C:\\repo', useConptyDll: true }) - ) - }) - - it('falls all the way through to cmd.exe and surfaces its argv-delivery flag', () => { - restorePlatform = setPlatform('win32') - vi.spyOn(console, 'warn').mockImplementation(() => {}) - - const attempts: WindowsShellSpawnAttempt[] = [ - makeAttempt(PWSH7), - makeAttempt(WINDOWS_POWERSHELL), - makeAttempt(CMD, { - shellArgs: ['/K', 'chcp 65001 > nul & npm start'], - startupCommandDeliveredInShellArgs: true - }) - ] - - const ptySpawn = vi.fn((shellPath: string) => { - if (shellPath === CMD) { - return makeFakePty() - } - throw new Error(ACCESS_DENIED_5) - }) as unknown as typeof pty.spawn - - const result = spawnShellWithFallback({ - shellPath: PWSH7, - shellArgs: attempts[0].shellArgs, - cols: 80, - rows: 24, - cwd: 'C:\\repo', - env: {}, - ptySpawn, - windowsFallbackAttempts: attempts - }) - - expect(result.shellPath).toBe(CMD) - expect(result.startupCommandDeliveredInShellArgs).toBe(true) - }) - - it('throws a descriptive error when every Windows fallback fails', () => { - restorePlatform = setPlatform('win32') - vi.spyOn(console, 'warn').mockImplementation(() => {}) - - const attempts: WindowsShellSpawnAttempt[] = [ - makeAttempt(PWSH7), - makeAttempt(WINDOWS_POWERSHELL), - makeAttempt(CMD) - ] - const ptySpawn = vi.fn(() => { - throw new Error(ACCESS_DENIED_5) - }) as unknown as typeof pty.spawn - const previousVersion = process.env.ORCA_APP_VERSION - process.env.ORCA_APP_VERSION = '1.4.178-test' - - try { - expect(() => - spawnShellWithFallback({ - shellPath: PWSH7, - shellArgs: attempts[0].shellArgs, - cols: 80, - rows: 24, - cwd: 'C:\\repo', - env: {}, - ptySpawn, - windowsFallbackAttempts: attempts - }) - ).toThrow(/Failed to spawn shell.*orca: 1\.4\.178-test/) - } finally { - if (previousVersion === undefined) { - delete process.env.ORCA_APP_VERSION - } else { - process.env.ORCA_APP_VERSION = previousVersion - } - } - }) -}) diff --git a/src/main/providers/local-pty-utils.test.ts b/src/main/providers/local-pty-utils.test.ts deleted file mode 100644 index 032e85722633..000000000000 --- a/src/main/providers/local-pty-utils.test.ts +++ /dev/null @@ -1,312 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type * as fs from 'node:fs' -import type { Stats } from 'node:fs' - -const { existsSyncMock, statSyncMock, accessSyncMock, wslUncDirectoryExistsMock, wrapSpawnMock } = - vi.hoisted(() => ({ - existsSyncMock: vi.fn(), - statSyncMock: vi.fn(), - accessSyncMock: vi.fn(), - wslUncDirectoryExistsMock: vi.fn(), - wrapSpawnMock: vi.fn() - })) - -vi.mock('fs', async (importOriginal) => { - const actual = await importOriginal() - return { - ...actual, - existsSync: existsSyncMock, - statSync: statSyncMock, - accessSync: accessSyncMock - } -}) - -function dirStats(isDirectory: boolean): Stats { - return { isDirectory: () => isDirectory } as Stats -} - -vi.mock('../wsl', () => ({ - wslUncDirectoryExists: wslUncDirectoryExistsMock -})) - -vi.mock('./macos-tcc-login-shell', () => ({ - wrapShellSpawnForMacosTccAttribution: wrapSpawnMock, - hostReportsChildExitStatus: (file: string) => file !== '/usr/bin/login' -})) - -import { - resolveUnixShellPath, - spawnShellWithFallback, - validateWorkingDirectory -} from './local-pty-utils' - -const WSL_UNC_DIR = '\\\\wsl.localhost\\Ubuntu\\home\\jin\\repo' -const NATIVE_DIR = 'C:\\Users\\jin\\repo' - -describe('validateWorkingDirectory', () => { - beforeEach(() => { - existsSyncMock.mockReset() - statSyncMock.mockReset() - wslUncDirectoryExistsMock.mockReset() - }) - - afterEach(() => { - vi.restoreAllMocks() - }) - - it('accepts a WSL UNC worktree that exists inside the distro even when fs.statSync would fail', () => { - // Why: the Win32 9P stat is the exact path that falsely reported ENOENT and - // broke opening WSL worktrees. The distro answer must win. - wslUncDirectoryExistsMock.mockReturnValue(true) - existsSyncMock.mockReturnValue(false) - - expect(() => validateWorkingDirectory(WSL_UNC_DIR)).not.toThrow() - expect(wslUncDirectoryExistsMock).toHaveBeenCalledWith(WSL_UNC_DIR) - // The fs fallback must not run when the distro confirmed existence. - expect(existsSyncMock).not.toHaveBeenCalled() - }) - - it('rejects a WSL UNC worktree that does not exist inside the distro', () => { - wslUncDirectoryExistsMock.mockReturnValue(false) - - expect(() => validateWorkingDirectory(WSL_UNC_DIR)).toThrow(/does not exist/) - expect(existsSyncMock).not.toHaveBeenCalled() - }) - - it('falls back to the fs check when the distro answer is inconclusive', () => { - wslUncDirectoryExistsMock.mockReturnValue(null) - existsSyncMock.mockReturnValue(true) - statSyncMock.mockReturnValue(dirStats(true)) - - expect(() => validateWorkingDirectory(WSL_UNC_DIR)).not.toThrow() - expect(wslUncDirectoryExistsMock).toHaveBeenCalledWith(WSL_UNC_DIR) - expect(existsSyncMock).toHaveBeenCalledWith(WSL_UNC_DIR) - }) - - it('validates native Windows paths via fs without consulting the distro', () => { - existsSyncMock.mockReturnValue(true) - statSyncMock.mockReturnValue(dirStats(true)) - - expect(() => validateWorkingDirectory(NATIVE_DIR)).not.toThrow() - expect(wslUncDirectoryExistsMock).not.toHaveBeenCalled() - expect(existsSyncMock).toHaveBeenCalledWith(NATIVE_DIR) - }) - - it('rejects a missing native Windows path', () => { - existsSyncMock.mockReturnValue(false) - const previousVersion = process.env.ORCA_APP_VERSION - process.env.ORCA_APP_VERSION = '1.4.178-test' - - try { - expect(() => validateWorkingDirectory(NATIVE_DIR)).toThrow( - /does not exist.*orca: 1\.4\.178-test/ - ) - expect(wslUncDirectoryExistsMock).not.toHaveBeenCalled() - } finally { - if (previousVersion === undefined) { - delete process.env.ORCA_APP_VERSION - } else { - process.env.ORCA_APP_VERSION = previousVersion - } - } - }) - - it('rejects a native Windows path that exists but is not a directory', () => { - existsSyncMock.mockReturnValue(true) - statSyncMock.mockReturnValue(dirStats(false)) - - expect(() => validateWorkingDirectory(NATIVE_DIR)).toThrow(/is not a directory/) - }) -}) - -describe('resolveUnixShellPath', () => { - beforeEach(() => { - existsSyncMock.mockReset() - accessSyncMock.mockReset() - }) - - it('preserves non-absolute shells for execvp PATH and cwd resolution', () => { - expect(resolveUnixShellPath('bash')).toBe('bash') - expect(resolveUnixShellPath('./bin/custom-shell')).toBe('./bin/custom-shell') - expect(existsSyncMock).not.toHaveBeenCalled() - }) - - it('selects an executable fallback before node-pty can fork a missing shell', () => { - existsSyncMock.mockImplementation((path) => path === '/bin/sh') - accessSyncMock.mockReturnValue(undefined) - - expect(resolveUnixShellPath('/missing/zsh')).toBe('/bin/sh') - expect(existsSyncMock.mock.calls.map(([path]) => path)).toEqual([ - '/missing/zsh', - '/bin/zsh', - '/bin/bash', - '/bin/sh' - ]) - }) - - it('reports every attempted shell when no executable candidate exists', () => { - existsSyncMock.mockReturnValue(false) - - expect(() => resolveUnixShellPath('/missing/zsh')).toThrow( - 'No executable Unix shell found (tried: /missing/zsh, /bin/zsh, /bin/bash, /bin/sh)' - ) - }) - - it('dedupes a preferred shell that is already a fallback candidate', () => { - existsSyncMock.mockImplementation((path) => path === '/bin/sh') - accessSyncMock.mockReturnValue(undefined) - - expect(resolveUnixShellPath('/bin/sh')).toBe('/bin/sh') - expect(existsSyncMock.mock.calls.map(([path]) => path)).toEqual(['/bin/sh']) - }) -}) - -describe('spawnShellWithFallback macOS TCC login wrapping', () => { - let origPlatform: PropertyDescriptor | undefined - - beforeEach(() => { - origPlatform = Object.getOwnPropertyDescriptor(process, 'platform') - Object.defineProperty(process, 'platform', { configurable: true, value: 'darwin' }) - existsSyncMock.mockReturnValue(true) - statSyncMock.mockReturnValue(dirStats(true)) - accessSyncMock.mockReturnValue(undefined) - wrapSpawnMock.mockImplementation((file: string, args: string[]) => ({ - file: '/wrapped/login', - args: ['wrapped', file, ...args] - })) - }) - - afterEach(() => { - if (origPlatform) { - Object.defineProperty(process, 'platform', origPlatform) - } - vi.restoreAllMocks() - }) - - it('spawns the primary shell through the login wrapper', () => { - const ptySpawn = vi.fn().mockReturnValue({ pid: 1 }) - - const result = spawnShellWithFallback({ - shellPath: '/bin/zsh', - shellArgs: ['-l'], - cols: 80, - rows: 24, - cwd: '/work', - env: {}, - ptySpawn: ptySpawn as never - }) - - expect(wrapSpawnMock).toHaveBeenCalledWith('/bin/zsh', ['-l'], expect.any(Object)) - expect(ptySpawn).toHaveBeenCalledWith( - '/wrapped/login', - ['wrapped', '/bin/zsh', '-l'], - expect.objectContaining({ cwd: '/work', cols: 80, rows: 24 }) - ) - // The reported shellPath stays the real shell so identity/name logic is intact. - expect(result.shellPath).toBe('/bin/zsh') - }) - - it('wraps fallback shells too when the primary fails to spawn', () => { - const ptySpawn = vi - .fn() - .mockImplementationOnce(() => { - throw new Error('primary boom') - }) - .mockReturnValue({ pid: 2 }) - - const result = spawnShellWithFallback({ - shellPath: '/bin/zsh', - shellArgs: ['-l'], - cols: 80, - rows: 24, - cwd: '/work', - env: {}, - ptySpawn: ptySpawn as never - }) - - // First fallback candidate after /bin/zsh is /bin/bash, also login-wrapped. - // The wrapper must see the fallback-corrected env so SHELL survives login(1). - expect(wrapSpawnMock).toHaveBeenLastCalledWith( - '/bin/bash', - ['-l'], - expect.objectContaining({ SHELL: '/bin/bash' }) - ) - expect(ptySpawn).toHaveBeenLastCalledWith( - '/wrapped/login', - ['wrapped', '/bin/bash', '-l'], - expect.objectContaining({ cwd: '/work' }) - ) - expect(result.shellPath).toBe('/bin/bash') - }) - - it('drops the primary shell’s launch env when an unwrapped fallback takes over', () => { - // Why: nothing in an unwrapped bash pane consumes the feature channel, so a - // leftover ZDOTDIR would point a nested zsh at Orca's wrapper and turn on - // features Orca never selected for it. - const zshLaunchEnv = { - ZDOTDIR: '/userdata/shell-ready/zsh', - ORCA_ORIG_ZDOTDIR: '/home/jin', - ORCA_SHELL_FEATURES: 'history' - } - const env: Record = { HOME: '/home/jin', ...zshLaunchEnv } - const ptySpawn = vi - .fn() - .mockImplementationOnce(() => { - throw new Error('primary boom') - }) - .mockReturnValue({ pid: 3 }) - - spawnShellWithFallback({ - shellPath: '/bin/zsh', - shellArgs: ['-l'], - cols: 80, - rows: 24, - cwd: '/work', - env, - ptySpawn: ptySpawn as never, - launchEnvKeys: Object.keys(zshLaunchEnv), - getShellReadyConfig: (shell) => - shell === '/bin/zsh' ? { args: ['-l'], env: zshLaunchEnv } : { args: null, env: {} } - }) - - expect(env.ORCA_SHELL_FEATURES).toBeUndefined() - expect(env.ZDOTDIR).toBeUndefined() - expect(env.ORCA_ORIG_ZDOTDIR).toBeUndefined() - expect(env.HOME).toBe('/home/jin') - }) - - it('drops the first fallback’s launch env when a second fallback takes over', () => { - // Why: the keys to scrub are the ones the LAST attempt wrote. Computed once - // from the primary, a wrapped first fallback leaks its own ZDOTDIR and - // feature channel into the shell that finally starts. - const bashLaunchEnv = { ORCA_SHELL_FEATURES: 'markers', BASH_ENV: '/userdata/bash/rcfile' } - const env: Record = { HOME: '/home/jin', ZDOTDIR: '/userdata/shell-ready/zsh' } - const ptySpawn = vi - .fn() - .mockImplementationOnce(() => { - throw new Error('primary boom') - }) - .mockImplementationOnce(() => { - throw new Error('first fallback boom') - }) - .mockReturnValue({ pid: 4 }) - - spawnShellWithFallback({ - shellPath: '/bin/zsh', - shellArgs: ['-l'], - cols: 80, - rows: 24, - cwd: '/work', - env, - ptySpawn: ptySpawn as never, - launchEnvKeys: ['ZDOTDIR'], - getShellReadyConfig: (shell) => - shell === '/bin/bash' ? { args: ['--rcfile', '/rc'], env: bashLaunchEnv } : null - }) - - expect(env.ORCA_SHELL_FEATURES).toBeUndefined() - expect(env.BASH_ENV).toBeUndefined() - expect(env.ZDOTDIR).toBeUndefined() - expect(env.HOME).toBe('/home/jin') - }) -}) diff --git a/src/main/providers/local-pty-utils.ts b/src/main/providers/local-pty-utils.ts deleted file mode 100644 index 735393449f25..000000000000 --- a/src/main/providers/local-pty-utils.ts +++ /dev/null @@ -1,313 +0,0 @@ -import { basename, isAbsolute, join } from 'node:path' -import { existsSync, accessSync, statSync, chmodSync, constants as fsConstants } from 'node:fs' -import type * as pty from 'node-pty' -import { usesNodePtySpawnHelper } from '../../shared/node-pty-spawn-helper' -import { - hostReportsChildExitStatus, - wrapShellSpawnForMacosTccAttribution -} from './macos-tcc-login-shell' -import { formatLocalPtyEnvironmentDiag } from './working-directory-validation' - -export { - formatLocalPtyEnvironmentDiag, - validateWorkingDirectory, - validateWorkingDirectoryAsync, - WorkingDirectoryValidationAbortedError -} from './working-directory-validation' - -let didEnsureSpawnHelperExecutable = false - -const UNIX_SHELL_FALLBACKS = ['/bin/zsh', '/bin/bash', '/bin/sh'] as const - -function toUnpackedAsarPath(candidate: string): string { - return candidate - .replace(/app\.asar([/\\])/, 'app.asar.unpacked$1') - .replace(/node_modules\.asar([/\\])/, 'node_modules.asar.unpacked$1') -} - -export function getNodePtySpawnHelperCandidates(): string[] { - const unixTerminalPath = require.resolve('node-pty/lib/unixTerminal.js') - const packageRoot = - basename(unixTerminalPath) === 'unixTerminal.js' - ? unixTerminalPath.replace(/[/\\]lib[/\\]unixTerminal\.js$/, '') - : unixTerminalPath - - return [ - join(packageRoot, 'build', 'Release', 'spawn-helper'), - join(packageRoot, 'build', 'Debug', 'spawn-helper'), - join(packageRoot, 'prebuilds', `${process.platform}-${process.arch}`, 'spawn-helper') - ].map(toUnpackedAsarPath) -} - -/** - * Validate that a shell binary exists and is executable. - * Returns an error message string if invalid, null if valid. - */ -export function getShellValidationError(shellPath: string): string | null { - if (!existsSync(shellPath)) { - return ( - `Shell "${shellPath}" does not exist. ` + - `Set a valid SHELL environment variable or install zsh/bash.` - ) - } - try { - accessSync(shellPath, fsConstants.X_OK) - } catch { - return `Shell "${shellPath}" is not executable. Check file permissions.` - } - return null -} - -/** - * Resolves an absolute Unix shell before node-pty forks. Bare commands and - * relative paths stay untouched so execvp can resolve them against PATH or cwd. - */ -export function resolveUnixShellPath(shellPath: string): string { - if (!isAbsolute(shellPath)) { - return shellPath - } - const candidates = [ - shellPath, - ...UNIX_SHELL_FALLBACKS.filter((candidate) => candidate !== shellPath) - ] - const resolved = candidates.find((candidate) => getShellValidationError(candidate) === null) - if (resolved) { - return resolved - } - throw new Error(`No executable Unix shell found (tried: ${candidates.join(', ')})`) -} - -/** - * Ensure the node-pty spawn-helper binary has the executable bit set. - * - * Why: when Electron packages the app via asar, the native spawn-helper - * binary may lose its +x permission. This function detects and repairs - * that so pty.spawn() does not fail with EACCES on first launch. - * macOS only — no other platform builds or execs the helper. - */ -export function ensureNodePtySpawnHelperExecutable(): void { - if (didEnsureSpawnHelperExecutable || !usesNodePtySpawnHelper(process.platform)) { - return - } - didEnsureSpawnHelperExecutable = true - - try { - for (const candidate of getNodePtySpawnHelperCandidates()) { - if (!existsSync(candidate)) { - continue - } - const mode = statSync(candidate).mode - if ((mode & 0o111) !== 0) { - return - } - chmodSync(candidate, mode | 0o755) - return - } - } catch (error) { - console.warn( - `[pty] Failed to ensure node-pty spawn-helper is executable: ${error instanceof Error ? error.message : String(error)}` - ) - } -} - -/** A pre-resolved Windows shell attempt: an absolute executable plus the launch - * args + cwd computed for it. Used to walk the PowerShell -> Windows PowerShell - * -> cmd.exe fallback chain when ConPTY rejects the primary shell. */ -export type WindowsShellSpawnAttempt = { - shellPath: string - shellArgs: string[] - effectiveCwd: string - validationCwd: string - startupCommandDeliveredInShellArgs: boolean -} - -export type ShellSpawnParams = { - shellPath: string - shellArgs: string[] - termName?: string - cols: number - rows: number - cwd: string - env: Record - ptySpawn: typeof pty.spawn - getShellReadyConfig?: ( - shell: string - ) => { args: string[] | null; env: Record } | null - /** Env keys the primary shell's launch config wrote into `env`. Passed in - * rather than re-derived: asking for the config again re-runs wrapper - * generation just to read back its key names. */ - launchEnvKeys?: readonly string[] - /** Called before each fallback shell spawn so callers can update env vars - * (e.g. HISTFILE) that depend on which shell is about to run. */ - onBeforeFallbackSpawn?: (env: Record, fallbackShell: string) => void - /** Windows-only ordered fallback chain (PowerShell -> Windows PowerShell -> - * cmd.exe). When the first attempt (which must match shellPath/shellArgs) - * fails to spawn, the next real absolute executable is tried with its own - * recomputed args/cwd. */ - windowsFallbackAttempts?: WindowsShellSpawnAttempt[] -} - -export type ShellSpawnResult = { - process: pty.IPty - shellPath: string - /** False when a wrapper owns the reported status, so no exit code or signal - * from this process describes the shell (STA-4536). */ - reportsChildExitStatus?: boolean - /** True when the winning shell's startup command was already embedded in its - * argv, so callers must not re-deliver it through stdin. Only set when a - * Windows fallback attempt other than the primary was used. */ - startupCommandDeliveredInShellArgs?: boolean -} - -/** - * Walk the Windows PowerShell -> Windows PowerShell -> cmd.exe fallback chain. - * - * Why: ConPTY's CreateProcessW rejects a Store App Execution Alias stub with - * ERROR_ACCESS_DENIED (error code 5). The chain entries are real absolute - * executables with per-shell args, so when the primary fails we retry with the - * next safe shell instead of leaving the user with no terminal. - */ -// Why: match the daemon spawn path (pty-subprocess.ts) — the bundled ConPTY -// has the modern wrap-marker behavior xterm expects; legacy system ConPTY can -// corrupt full-width TUI rows in scrollback. Without this, degraded-mode and -// fresh-local spawns silently behave differently from daemon terminals. -function windowsConptyDllOptions(): { useConptyDll: true } | Record { - return process.platform === 'win32' ? { useConptyDll: true } : {} -} - -function spawnWindowsFallbackChain( - params: ShellSpawnParams, - primaryError: string -): ShellSpawnResult | null { - const { termName = 'xterm-256color', cols, rows, env, ptySpawn } = params - const attempts = params.windowsFallbackAttempts ?? [] - // Skip the first entry: it is the primary that already failed above. - for (const attempt of attempts.slice(1)) { - try { - const proc = ptySpawn(attempt.shellPath, attempt.shellArgs, { - name: termName, - cols, - rows, - cwd: attempt.effectiveCwd, - env, - ...windowsConptyDllOptions() - }) - console.warn( - `[pty] Primary shell "${params.shellPath}" failed (${primaryError}), fell back to "${attempt.shellPath}"` - ) - return { - process: proc, - shellPath: attempt.shellPath, - startupCommandDeliveredInShellArgs: attempt.startupCommandDeliveredInShellArgs - } - } catch { - // This fallback shell also failed -- try the next link in the chain. - } - } - return null -} - -/** - * Attempt to spawn a PTY shell. If the primary shell fails, try fallback shells - * (Unix: zsh/bash/sh; Windows: the PowerShell -> cmd.exe chain) before giving up. - */ -export function spawnShellWithFallback(params: ShellSpawnParams): ShellSpawnResult { - const { - shellPath, - shellArgs, - termName = 'xterm-256color', - cols, - rows, - cwd, - env, - ptySpawn, - getShellReadyConfig, - onBeforeFallbackSpawn - } = params - let primaryError: string | null = null - - if (process.platform !== 'win32') { - primaryError = getShellValidationError(shellPath) - } - - if (!primaryError) { - try { - const wrapped = wrapShellSpawnForMacosTccAttribution(shellPath, shellArgs, env) - return { - process: ptySpawn(wrapped.file, wrapped.args, { - name: termName, - cols, - rows, - cwd, - env, - ...windowsConptyDllOptions() - }), - shellPath, - reportsChildExitStatus: hostReportsChildExitStatus(wrapped.file) - } - } catch (err) { - primaryError = err instanceof Error ? err.message : String(err) - } - } - - if (process.platform === 'win32') { - const fallback = spawnWindowsFallbackChain(params, primaryError ?? 'unknown error') - if (fallback) { - return fallback - } - } - - // Try fallback shells on Unix - if (process.platform !== 'win32') { - const fallbackShells = UNIX_SHELL_FALLBACKS.filter((candidate) => candidate !== shellPath) - // Why: the previous shell's launch keys (its wrapper ZDOTDIR and the feature - // channel) mean nothing to a different shell. An unwrapped fallback writes - // none of them back, so they would stay exported to the pane and to every - // child — including a nested zsh that would then load Orca's wrapper. Tracked - // per attempt, not once: the second fallback must not inherit the first's. - let staleLaunchEnvKeys: readonly string[] = params.launchEnvKeys ?? [] - for (const fallback of fallbackShells) { - if (getShellValidationError(fallback)) { - continue - } - try { - const fallbackReady = getShellReadyConfig?.(fallback) - env.SHELL = fallback - onBeforeFallbackSpawn?.(env, fallback) - for (const key of staleLaunchEnvKeys) { - delete env[key] - } - Object.assign(env, fallbackReady?.env ?? {}) - staleLaunchEnvKeys = Object.keys(fallbackReady?.env ?? {}) - const wrapped = wrapShellSpawnForMacosTccAttribution( - fallback, - fallbackReady?.args ?? ['-l'], - env - ) - const proc = ptySpawn(wrapped.file, wrapped.args, { - name: termName, - cols, - rows, - cwd, - env - }) - console.warn( - `[pty] Primary shell "${shellPath}" failed (${primaryError ?? 'unknown error'}), fell back to "${fallback}"` - ) - return { - process: proc, - shellPath: fallback, - reportsChildExitStatus: hostReportsChildExitStatus(wrapped.file) - } - } catch { - // Fallback also failed -- try next. - } - } - } - - const diag = formatLocalPtyEnvironmentDiag({ shell: shellPath, cwd }) - throw new Error( - `Failed to spawn shell "${shellPath}": ${primaryError ?? 'unknown error'} (${diag}). ` + - `If this persists, please file an issue.` - ) -} diff --git a/src/main/providers/local-pty-windows-spawn-environment.ts b/src/main/providers/local-pty-windows-spawn-environment.ts deleted file mode 100644 index f7967d710f78..000000000000 --- a/src/main/providers/local-pty-windows-spawn-environment.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { win32 as pathWin32 } from 'node:path' -import { ORCA_HERMES_STARTUP_QUERY_ENV } from '../../shared/hermes-startup-query' -import { isHostCodexHomeForWsl, isWslCodexHomeForHost } from '../pty/codex-home-wsl-env' -import { addWslEnvKeys } from '../wsl-env' -import { parseWslPath } from '../wsl' -import { isWindowsGitBashShellPath } from '../git-bash' -import type { LocalPtyLaunchPlan } from './local-pty-launch-plan' -import { - ORCA_CODEX_LAUNCH_PREFLIGHT_CMD_QUOTE_ENV, - resolveWindowsShellLaunchArgs -} from './windows-shell-args' -import type { PtySpawnOptions } from './types' - -export function finalizeWindowsLocalPtySpawnEnvironment(args: { - spawn: PtySpawnOptions - plan: LocalPtyLaunchPlan - env: Record -}): void { - const { spawn, plan, env } = args - const codexHomeWslInfo = env.CODEX_HOME ? parseWslPath(env.CODEX_HOME) : null - if (pathWin32.basename(plan.shellPath).toLowerCase() === 'wsl.exe') { - if (codexHomeWslInfo) { - if (plan.launchWslDistro && plan.launchWslDistro !== codexHomeWslInfo.distro) { - delete env.CODEX_HOME - delete env.ORCA_CODEX_HOME - } else { - env.CODEX_HOME = codexHomeWslInfo.linuxPath - env.ORCA_CODEX_HOME = codexHomeWslInfo.linuxPath - // Why: wsl.exe only imports non-default env vars named in WSLENV. - addWslEnvKeys(env, ['CODEX_HOME', 'ORCA_CODEX_HOME']) - if (!plan.launchWslDistro) { - const resolved = resolveWindowsShellLaunchArgs( - plan.shellPath, - plan.cwd, - plan.defaultCwd, - { - distro: codexHomeWslInfo.distro - } - ) - plan.shellArgs = resolved.shellArgs - plan.effectiveCwd = resolved.effectiveCwd - plan.validationCwd = resolved.validationCwd - plan.startupCommandDeliveredInShellArgs = - resolved.startupCommandDeliveredInShellArgs === true - } - } - } else if (isHostCodexHomeForWsl(env.CODEX_HOME)) { - // Why: Orca's Codex home is host-local; WSL Codex must use its Linux-side ~/.codex, not a Windows path. - delete env.CODEX_HOME - delete env.ORCA_CODEX_HOME - } else if (env.CODEX_HOME) { - addWslEnvKeys(env, ['CODEX_HOME', 'ORCA_CODEX_HOME']) - } - if (env.CLAUDE_CONFIG_DIR) { - // Why: managed WSL Claude passes a Linux CLAUDE_CONFIG_DIR through wsl.exe; non-default vars need WSLENV import. - addWslEnvKeys(env, ['CLAUDE_CONFIG_DIR']) - } - if (env[ORCA_HERMES_STARTUP_QUERY_ENV] !== undefined) { - // Why: wsl.exe drops custom Windows env vars; the startup wrapper needs this imported inside WSL. - addWslEnvKeys(env, [ORCA_HERMES_STARTUP_QUERY_ENV]) - } - } else if (codexHomeWslInfo || isWslCodexHomeForHost(env.CODEX_HOME)) { - // Why: WSL Codex homes are Linux paths Windows can't use; also drop ORCA_CODEX_HOME (shell-ready restores CODEX_HOME from it). - delete env.CODEX_HOME - delete env.ORCA_CODEX_HOME - } - - const shellBasename = pathWin32.basename(plan.shellPath).toLowerCase() - const codexLaunchPreflightCommand = env.ORCA_CODEX_LAUNCH_PREFLIGHT - if ( - codexLaunchPreflightCommand && - (shellBasename === 'cmd.exe' || isWindowsGitBashShellPath(plan.shellPath)) - ) { - if (shellBasename === 'cmd.exe') { - // Why: node-pty backslash-escapes argv quotes; expand the quote inside cmd.exe instead. - env[ORCA_CODEX_LAUNCH_PREFLIGHT_CMD_QUOTE_ENV] = '"' - } - const resolved = resolveWindowsShellLaunchArgs( - plan.shellPath, - plan.cwd, - plan.defaultCwd, - plan.launchWslContext, - spawn.command, - codexLaunchPreflightCommand - ) - plan.shellArgs = resolved.shellArgs - plan.effectiveCwd = resolved.effectiveCwd - plan.validationCwd = resolved.validationCwd - plan.startupCommandDeliveredInShellArgs = resolved.startupCommandDeliveredInShellArgs === true - } -} diff --git a/src/main/providers/provider-dispatch.test.ts b/src/main/providers/provider-dispatch.test.ts index cc63cfb9d09d..963b144cda38 100644 --- a/src/main/providers/provider-dispatch.test.ts +++ b/src/main/providers/provider-dispatch.test.ts @@ -1,5 +1,5 @@ import { settledWriteStub } from './settled-pty-write-stub' -import { describe, expect, it, vi } from 'vitest' +import { afterEach, describe, expect, it, vi } from 'vitest' import { setPtyHostBindings } from '../ipc/pty-host-bindings' const { handleMock, onMock, removeHandlerMock, removeAllListenersMock } = vi.hoisted(() => ({ @@ -36,18 +36,6 @@ vi.mock('fs', () => ({ constants: { X_OK: 1 } })) -vi.mock('node-pty', () => ({ - spawn: vi.fn().mockReturnValue({ - onData: vi.fn(), - onExit: vi.fn(), - write: vi.fn(), - resize: vi.fn(), - kill: vi.fn(), - process: 'zsh', - pid: 12345 - }) -})) - vi.mock('../opencode/hook-service', () => ({ openCodeHookService: { buildPtyEnv: () => ({}), clearPty: vi.fn() } })) @@ -58,6 +46,8 @@ vi.mock('../pi/titlebar-extension-service', () => ({ import { deletePtyOwnership, + getLocalPtyProvider, + setLocalPtyProvider, registerPtyHandlers, registerSshPtyProvider, setPtyOwnership, @@ -74,7 +64,13 @@ describe('PTY provider dispatch', () => { } const mainWindowIpcEvent = { sender: mainWindow.webContents } + const originalLocalProvider = getLocalPtyProvider() + let local: IPtyProvider + afterEach(() => setLocalPtyProvider(originalLocalProvider)) + function setup(): void { + local = createMockProvider('local-test-pty') + setLocalPtyProvider(local) handlers.clear() handleMock.mockReset() onMock.mockReset() @@ -130,7 +126,10 @@ describe('PTY provider dispatch', () => { rows: 24, connectionId: null })) as { id: string } - expect(result.id).toBeTruthy() + expect(result.id).toBe('local-test-pty') + expect(local.spawn).toHaveBeenCalledExactlyOnceWith( + expect.objectContaining({ cols: 80, rows: 24 }) + ) }) it('routes to local provider when connectionId is undefined', async () => { @@ -139,7 +138,10 @@ describe('PTY provider dispatch', () => { cols: 80, rows: 24 })) as { id: string } - expect(result.id).toBeTruthy() + expect(result.id).toBe('local-test-pty') + expect(local.spawn).toHaveBeenCalledExactlyOnceWith( + expect.objectContaining({ cols: 80, rows: 24 }) + ) }) it('routes to SSH provider when connectionId is set', async () => { diff --git a/src/main/providers/pty-provider-contract.ts b/src/main/providers/pty-provider-contract.ts index 4b31fbbb2c8b..8feb62d448df 100644 --- a/src/main/providers/pty-provider-contract.ts +++ b/src/main/providers/pty-provider-contract.ts @@ -123,10 +123,6 @@ export type IPtyProvider = { params: unknown, options?: { signal?: AbortSignal; timeoutMs?: number } ) => Promise - /** Fresh local spawns currently route to an in-process, non-persistent fallback. */ - readonly routesFreshSpawnsToLocalProvider?: true - /** Re-probes a degraded durable host before main commits to fallback spawn semantics. */ - recoverFreshSpawnRouting?: () => Promise spawn(opts: PtySpawnOptions): Promise /** Process-owner cleanup for history stored outside the workspace tree. */ deleteWorktreeHistory?: (worktreeId: string) => Promise @@ -156,6 +152,7 @@ export type IPtyProvider = { * and callers must keep functioning without them (the pending-output cap * still bounds memory when pause is unavailable). */ + hasPtyDeliveryPauseAdapter?: () => boolean pauseProducer?: (id: string) => void resumeProducer?: (id: string) => void /** diff --git a/src/main/providers/pty-repaint-wide-char-buffer.node-pty.test.ts b/src/main/providers/pty-repaint-wide-char-buffer.bun.test.ts similarity index 62% rename from src/main/providers/pty-repaint-wide-char-buffer.node-pty.test.ts rename to src/main/providers/pty-repaint-wide-char-buffer.bun.test.ts index 525f337b82f5..d04cf4c02f29 100644 --- a/src/main/providers/pty-repaint-wide-char-buffer.node-pty.test.ts +++ b/src/main/providers/pty-repaint-wide-char-buffer.bun.test.ts @@ -1,36 +1,10 @@ -/** - * Real-ConPTY reproduction for #15192, asserted on BUFFER CONTENT rather than on - * the byte stream. - * - * Its sibling windows-conpty-wide-char-duplication.node-pty.test.ts asserts that - * no two identical wide characters arrive adjacent in the bytes node-pty emits. - * That passes on both backends, and it cannot fail for the fault the reporter - * actually described: in their decisive run the command echo was doubled while - * that same command's output was clean, so the shell never received doubled - * bytes. A repaint landing at the wrong cells is made entirely of legitimate - * bytes -- cursor positioning plus text -- and only the cells it lands on are - * wrong. The doubled text also pasted doubled into Notepad, so the corruption - * was in the buffer. - * - * So the ConPTY output is replayed into Orca's own main-side emulator, resizing - * it before the pty exactly as Session.resize does (session.ts), and the - * resulting lines are compared to the fixtures. - * - * Everything that does not need ConPTY runs on every platform: on macOS and - * Linux node-pty spawns a real pty, so the same assertions guard those and prove - * the replay harness is not silently vacuous on the Windows lane. Only the - * backend A/B (bundled OpenConsole vs the system ConPTY) is win32-only. - * - * The width sweep behind these fixtures lives in - * src/main/daemon/headless-emulator-wide-char-repaint.test.ts. - */ +/** Replays real Bun terminal output into Orca's emulator to check wide-cell fidelity. */ +import { recordBunPtyTranscript, type PtyTranscriptEvent } from './bun-pty-transcript-fixture' import { describe, expect, it } from 'vitest' import type { Terminal } from '@xterm/headless' import { HeadlessEmulator } from '../daemon/headless-emulator' import { isWideGlyph, readWrappedLineGlyphs } from '../daemon/__fixtures__/terminal-wide-cell-grid' -const itOnWindows = process.platform === 'win32' ? it : it.skip - const KOREAN_LINE = '안녕하세요 오르카 테스트입니다. 결론부터 말씀드리면 시각적 피로도' const LATIN_LINE = 'roadmap/complete-overhaul-backlog-history.md (1.75) R-08)' const LINE_REPEATS = 8 @@ -41,10 +15,9 @@ const glyphsOf = (line: string): string => line.replace(/\s+/g, '') const KOREAN_GLYPHS = glyphsOf(KOREAN_LINE) const LATIN_GLYPHS = glyphsOf(LATIN_LINE) -type Event = { data: string } | { resizeTo: number } +type Event = PtyTranscriptEvent type RunOptions = { - useConptyDll: boolean /** Milliseconds after spawn at which to resize, paired with the target width. */ resizes?: { atMs: number; cols: number }[] /** How long the child stays alive after its last write, so a resize can trigger a repaint. */ @@ -63,40 +36,12 @@ function childScript(holdMs: number): string { } async function recordConpty(options: RunOptions): Promise { - const nodePty = await import('node-pty') - const proc = nodePty.spawn(process.execPath, ['-e', childScript(options.holdMs)], { - name: 'xterm-256color', + return recordBunPtyTranscript({ + script: childScript(options.holdMs), cols: COLS, rows: ROWS, - cwd: process.cwd(), - env: process.env as Record, - ...(options.useConptyDll ? { useConptyDll: true } : {}) - }) - - // One ordered log: a resize recorded here replays at the same point in the - // stream it happened at, which is the only way the replay can be faithful. - const events: Event[] = [] - proc.onData((chunk) => { - events.push({ data: chunk }) + resizes: options.resizes }) - const timers = (options.resizes ?? []).map((resize) => - setTimeout(() => { - events.push({ resizeTo: resize.cols }) - try { - proc.resize(resize.cols, ROWS) - } catch { - /* the child may already have exited */ - } - }, resize.atMs) - ) - - await new Promise((resolve) => { - proc.onExit(() => resolve()) - }) - for (const timer of timers) { - clearTimeout(timer) - } - return events } function replayIntoEmulator(events: Event[]): string[] { @@ -147,26 +92,13 @@ describe('doubled-wide-glyph detector', () => { describe('pty repaint fidelity in the terminal buffer (#15192)', () => { it('leaves the buffer holding exactly the lines that were printed', async () => { - expectFixtureLinesOnly( - replayIntoEmulator(await recordConpty({ useConptyDll: true, holdMs: 50 })) - ) + expectFixtureLinesOnly(replayIntoEmulator(await recordConpty({ holdMs: 50 }))) }, 60_000) - itOnWindows( - 'holds the same lines on the system ConPTY (A/B for the bundled build)', - async () => { - expectFixtureLinesOnly( - replayIntoEmulator(await recordConpty({ useConptyDll: false, holdMs: 50 })) - ) - }, - 60_000 - ) - it('survives the resize the reporter used as a workaround, after output settles', async () => { // Resizing once the child is quiet isolates the repaint: nothing is being // written, so any line that changes was moved by ConPTY's redraw alone. const events = await recordConpty({ - useConptyDll: true, holdMs: 900, resizes: [ { atMs: 450, cols: 31 }, @@ -182,7 +114,6 @@ describe('pty repaint fidelity in the terminal buffer (#15192)', () => { // which would fail an exact comparison without proving duplication. What // must never happen either way is a glyph appearing twice. const events = await recordConpty({ - useConptyDll: true, holdMs: 200, resizes: [ { atMs: 60, cols: 33 }, diff --git a/src/main/providers/pty-spawn-validation.test.ts b/src/main/providers/pty-spawn-validation.test.ts new file mode 100644 index 000000000000..2baac47072ca --- /dev/null +++ b/src/main/providers/pty-spawn-validation.test.ts @@ -0,0 +1,153 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type * as fs from 'node:fs' +import type { Stats } from 'node:fs' + +const { existsSyncMock, statSyncMock, accessSyncMock, wslUncDirectoryExistsMock } = vi.hoisted( + () => ({ + existsSyncMock: vi.fn(), + statSyncMock: vi.fn(), + accessSyncMock: vi.fn(), + wslUncDirectoryExistsMock: vi.fn() + }) +) + +vi.mock('fs', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + existsSync: existsSyncMock, + statSync: statSyncMock, + accessSync: accessSyncMock + } +}) + +function dirStats(isDirectory: boolean): Stats { + return { isDirectory: () => isDirectory } as Stats +} + +vi.mock('../wsl', () => ({ + wslUncDirectoryExists: wslUncDirectoryExistsMock +})) + +import { resolveUnixShellPath, validateWorkingDirectory } from './pty-spawn-validation' + +const WSL_UNC_DIR = '\\\\wsl.localhost\\Ubuntu\\home\\jin\\repo' +const NATIVE_DIR = 'C:\\Users\\jin\\repo' + +describe('validateWorkingDirectory', () => { + beforeEach(() => { + existsSyncMock.mockReset() + statSyncMock.mockReset() + wslUncDirectoryExistsMock.mockReset() + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + it('accepts a WSL UNC worktree that exists inside the distro even when fs.statSync would fail', () => { + // Why: the Win32 9P stat is the exact path that falsely reported ENOENT and + // broke opening WSL worktrees. The distro answer must win. + wslUncDirectoryExistsMock.mockReturnValue(true) + existsSyncMock.mockReturnValue(false) + + expect(() => validateWorkingDirectory(WSL_UNC_DIR)).not.toThrow() + expect(wslUncDirectoryExistsMock).toHaveBeenCalledWith(WSL_UNC_DIR) + // The fs fallback must not run when the distro confirmed existence. + expect(existsSyncMock).not.toHaveBeenCalled() + }) + + it('rejects a WSL UNC worktree that does not exist inside the distro', () => { + wslUncDirectoryExistsMock.mockReturnValue(false) + + expect(() => validateWorkingDirectory(WSL_UNC_DIR)).toThrow(/does not exist/) + expect(existsSyncMock).not.toHaveBeenCalled() + }) + + it('falls back to the fs check when the distro answer is inconclusive', () => { + wslUncDirectoryExistsMock.mockReturnValue(null) + existsSyncMock.mockReturnValue(true) + statSyncMock.mockReturnValue(dirStats(true)) + + expect(() => validateWorkingDirectory(WSL_UNC_DIR)).not.toThrow() + expect(wslUncDirectoryExistsMock).toHaveBeenCalledWith(WSL_UNC_DIR) + expect(existsSyncMock).toHaveBeenCalledWith(WSL_UNC_DIR) + }) + + it('validates native Windows paths via fs without consulting the distro', () => { + existsSyncMock.mockReturnValue(true) + statSyncMock.mockReturnValue(dirStats(true)) + + expect(() => validateWorkingDirectory(NATIVE_DIR)).not.toThrow() + expect(wslUncDirectoryExistsMock).not.toHaveBeenCalled() + expect(existsSyncMock).toHaveBeenCalledWith(NATIVE_DIR) + }) + + it('rejects a missing native Windows path', () => { + existsSyncMock.mockReturnValue(false) + const previousVersion = process.env.ORCA_APP_VERSION + process.env.ORCA_APP_VERSION = '1.4.178-test' + + try { + expect(() => validateWorkingDirectory(NATIVE_DIR)).toThrow( + /does not exist.*orca: 1\.4\.178-test/ + ) + expect(wslUncDirectoryExistsMock).not.toHaveBeenCalled() + } finally { + if (previousVersion === undefined) { + delete process.env.ORCA_APP_VERSION + } else { + process.env.ORCA_APP_VERSION = previousVersion + } + } + }) + + it('rejects a native Windows path that exists but is not a directory', () => { + existsSyncMock.mockReturnValue(true) + statSyncMock.mockReturnValue(dirStats(false)) + + expect(() => validateWorkingDirectory(NATIVE_DIR)).toThrow(/is not a directory/) + }) +}) + +describe('resolveUnixShellPath', () => { + beforeEach(() => { + existsSyncMock.mockReset() + accessSyncMock.mockReset() + }) + + it('preserves non-absolute shells for execvp PATH and cwd resolution', () => { + expect(resolveUnixShellPath('bash')).toBe('bash') + expect(resolveUnixShellPath('./bin/custom-shell')).toBe('./bin/custom-shell') + expect(existsSyncMock).not.toHaveBeenCalled() + }) + + it('selects an executable fallback before node-pty can fork a missing shell', () => { + existsSyncMock.mockImplementation((path) => path === '/bin/sh') + accessSyncMock.mockReturnValue(undefined) + + expect(resolveUnixShellPath('/missing/zsh')).toBe('/bin/sh') + expect(existsSyncMock.mock.calls.map(([path]) => path)).toEqual([ + '/missing/zsh', + '/bin/zsh', + '/bin/bash', + '/bin/sh' + ]) + }) + + it('reports every attempted shell when no executable candidate exists', () => { + existsSyncMock.mockReturnValue(false) + + expect(() => resolveUnixShellPath('/missing/zsh')).toThrow( + 'No executable Unix shell found (tried: /missing/zsh, /bin/zsh, /bin/bash, /bin/sh)' + ) + }) + + it('dedupes a preferred shell that is already a fallback candidate', () => { + existsSyncMock.mockImplementation((path) => path === '/bin/sh') + accessSyncMock.mockReturnValue(undefined) + + expect(resolveUnixShellPath('/bin/sh')).toBe('/bin/sh') + expect(existsSyncMock.mock.calls.map(([path]) => path)).toEqual(['/bin/sh']) + }) +}) diff --git a/src/main/providers/pty-spawn-validation.ts b/src/main/providers/pty-spawn-validation.ts new file mode 100644 index 000000000000..25349780cb90 --- /dev/null +++ b/src/main/providers/pty-spawn-validation.ts @@ -0,0 +1,45 @@ +import { isAbsolute } from 'node:path' +import { existsSync, accessSync, constants as fsConstants } from 'node:fs' + +export { + formatLocalPtyEnvironmentDiag, + validateWorkingDirectory, + validateWorkingDirectoryAsync, + WorkingDirectoryValidationAbortedError +} from './working-directory-validation' + +const UNIX_SHELL_FALLBACKS = ['/bin/zsh', '/bin/bash', '/bin/sh'] as const + +export function getShellValidationError(shellPath: string): string | null { + if (!existsSync(shellPath)) { + return ( + `Shell "${shellPath}" does not exist. ` + + `Set a valid SHELL environment variable or install zsh/bash.` + ) + } + try { + accessSync(shellPath, fsConstants.X_OK) + } catch { + return `Shell "${shellPath}" is not executable. Check file permissions.` + } + return null +} + +/** + * Resolves an absolute Unix shell before the native terminal forks. Bare commands and + * relative paths stay untouched so execvp can resolve them against PATH or cwd. + */ +export function resolveUnixShellPath(shellPath: string): string { + if (!isAbsolute(shellPath)) { + return shellPath + } + const candidates = [ + shellPath, + ...UNIX_SHELL_FALLBACKS.filter((candidate) => candidate !== shellPath) + ] + const resolved = candidates.find((candidate) => getShellValidationError(candidate) === null) + if (resolved) { + return resolved + } + throw new Error(`No executable Unix shell found (tried: ${candidates.join(', ')})`) +} diff --git a/src/main/providers/relay-pty-generation-registry.ts b/src/main/providers/relay-pty-generation-registry.ts new file mode 100644 index 000000000000..e9e6aca9d17a --- /dev/null +++ b/src/main/providers/relay-pty-generation-registry.ts @@ -0,0 +1,20 @@ +import type { IPtyProvider } from './types' + +/** Source-credit generations identify transports, independently of SSH or WSL owner names. */ +export const relayProvidersByGeneration = new Map() + +export function registerRelayPtyGeneration(generation: number, provider: IPtyProvider): () => void { + if (!Number.isSafeInteger(generation) || generation <= 0) { + throw new Error('Invalid relay provider generation') + } + const existing = relayProvidersByGeneration.get(generation) + if (existing && existing !== provider) { + throw new Error('Relay provider generation already registered') + } + relayProvidersByGeneration.set(generation, provider) + return () => { + if (relayProvidersByGeneration.get(generation) === provider) { + relayProvidersByGeneration.delete(generation) + } + } +} diff --git a/src/main/providers/relay-pty-id-mapping.ts b/src/main/providers/relay-pty-id-mapping.ts new file mode 100644 index 000000000000..71e2344bcec0 --- /dev/null +++ b/src/main/providers/relay-pty-id-mapping.ts @@ -0,0 +1,13 @@ +import { toAppSshPtyId, toRelaySshPtyId } from './ssh-pty-id' + +export type RelayPtyIdMapping = { + toAppPtyId: (relayPtyId: string) => string + toRelayPtyId: (appPtyId: string) => string +} + +export function sshRelayPtyIdMapping(connectionId: string): RelayPtyIdMapping { + return { + toAppPtyId: (id) => toAppSshPtyId(connectionId, id), + toRelayPtyId: (id) => toRelaySshPtyId(connectionId, id) + } +} diff --git a/src/main/providers/relay-pty-provider.ts b/src/main/providers/relay-pty-provider.ts new file mode 100644 index 000000000000..3c18a324e925 --- /dev/null +++ b/src/main/providers/relay-pty-provider.ts @@ -0,0 +1,317 @@ +import type { SshChannelMultiplexer } from '../ssh/ssh-channel-multiplexer' +import type { IPtyProvider, PtyProcessInfo, PtySpawnOptions, PtySpawnResult } from './types' +import type { WriteSettlement } from '../../shared/pty-write-settlement' +import type { RelayPtyIdMapping } from './relay-pty-id-mapping' +import { createSshPtyAppliedSizeReader } from './ssh-pty-applied-size' +import type { + RemoteCliBridgeEnv, + SshPtyDataCallback, + SshPtyDeliveryPauseAdapter, + SshPtyExitCallback, + SshPtyReplayCallback +} from './ssh-pty-provider-contract' +import { SshPtyProviderOutputState } from './ssh-pty-provider-output-state' +import { spawnFreshSshPty } from './ssh-agent-session-create-operation' +import { mapSshPtyProcessList } from './ssh-agent-session-process-list' +import { + requestSshPtyAttach, + reattachSshPtySessionForSpawn, + type PtySourceRecoveryRequest, + type SshPtyAttachResult +} from './ssh-pty-session-reattach' +import { buildSshPtySpawnRequest } from './ssh-pty-spawn-request' +import { SshPtySpawnExitRaceTracker } from './ssh-pty-spawn-exit-race' +import { SshAgentSessionCapabilities } from './ssh-agent-session-capabilities' +import type { PtyProcessInspection } from './pty-process-inspection' +import { spawnWithTerminalRuntimeRepair, type TerminalRepairHook } from './ssh-pty-spawn-repair' +import { createSshPtyProviderRpcOperations } from './ssh-pty-provider-rpc-operations' + +// Why: sequential relay teardown calls share one absolute budget; convert to the mux-relative timeout only at dispatch. +function relayTimeoutOptions(deadlineMs: number | undefined): { timeoutMs: number } | undefined { + return deadlineMs === undefined ? undefined : { timeoutMs: Math.max(1, deadlineMs - Date.now()) } +} + +/** Remote PTY provider that proxies IPtyProvider operations through the relay. */ +export class RelayPtyProvider implements IPtyProvider { + private mux: SshChannelMultiplexer + private readonly ids: RelayPtyIdMapping + private livePtyIds = new Set() + readonly getAppliedSize: NonNullable + private readonly agentSessionCapabilities: SshAgentSessionCapabilities + private spawnExitRaces = new SshPtySpawnExitRaceTracker() + private readonly outputState: SshPtyProviderOutputState + private recoverFromTerminalUnavailable: TerminalRepairHook | null = null + private readonly rpcOperations: ReturnType + + deleteWorktreeHistory = (worktreeId: string): Promise => + this.rpcOperations.deleteWorktreeHistory(worktreeId) + write = (id: string, data: string): boolean => this.rpcOperations.write(id, data) + writeWithSettlement = (id: string, data: string): Promise => + this.rpcOperations.writeWithSettlement(id, data) + resize = (id: string, cols: number, rows: number): void => + this.rpcOperations.resize(id, cols, rows) + sendSignal = (id: string, signal: string): Promise => + this.rpcOperations.sendSignal(id, signal) + getCwd = (id: string): Promise => this.rpcOperations.getCwd(id) + getInitialCwd = (id: string): Promise => this.rpcOperations.getInitialCwd(id) + clearBuffer = (id: string): Promise => this.rpcOperations.clearBuffer(id) + closeStartupQueryAuthority = (id: string): Promise => + this.rpcOperations.closeStartupQueryAuthority(id) + acknowledgeDataEvent = (id: string, charCount: number): void => + this.rpcOperations.acknowledgeDataEvent(id, charCount) + hasChildProcesses = (id: string): Promise => this.rpcOperations.hasChildProcesses(id) + getForegroundProcess = (id: string): Promise => + this.rpcOperations.getForegroundProcess(id) + inspectProcess = ( + id: string, + options?: { expectedIncarnationId?: string; scanChildProcesses?: boolean } + ): Promise => this.rpcOperations.inspectProcess(id, options) + serialize = (ids: string[]): Promise => this.rpcOperations.serialize(ids) + revive = (state: string): Promise => this.rpcOperations.revive(state) + getDefaultShell = (): Promise => this.rpcOperations.getDefaultShell() + getProfiles = (): Promise<{ name: string; path: string }[]> => this.rpcOperations.getProfiles() + + requestHostRpc: NonNullable = (method, params, options) => + this.mux.request(method, params as Record, options) + + constructor( + ids: RelayPtyIdMapping, + mux: SshChannelMultiplexer, + private readonly remoteCliBridgeEnv?: RemoteCliBridgeEnv, + readonly providerGeneration = 1 + ) { + this.ids = ids + this.mux = mux + this.rpcOperations = createSshPtyProviderRpcOperations({ + mux, + toRelayPtyId: (id) => this.toRelayPtyId(id) + }) + this.agentSessionCapabilities = new SshAgentSessionCapabilities(mux) + this.getAppliedSize = createSshPtyAppliedSizeReader(mux, ids.toRelayPtyId) + + this.outputState = new SshPtyProviderOutputState(providerGeneration, { + mux, + toAppPtyId: (id) => this.toAppPtyId(id), + livePtyIds: this.livePtyIds, + recordExit: (relayPtyId, incarnationId) => { + this.spawnExitRaces.recordExit(relayPtyId, incarnationId) + } + }) + } + + dispose(): void { + this.outputState.dispose() + this.livePtyIds.clear() + } + + canProvideAuthoritativeBufferSnapshot = (_id: string): boolean => false + + private toRelayPtyId = (id: string): string => this.ids.toRelayPtyId(id) + + private toAppPtyId = (id: string): string => this.ids.toAppPtyId(id) + + /** Installed by SshRelaySession, which owns the connection, the repair lock and the reconnect. */ + setTerminalUnavailableRecovery(recover: TerminalRepairHook): void { + this.recoverFromTerminalUnavailable = recover + } + + hasLivePtys(): boolean { + return this.livePtyIds.size > 0 + } + + async spawn(opts: PtySpawnOptions): Promise { + return await spawnWithTerminalRuntimeRepair({ + attempt: () => this.spawnWithoutTerminalRuntimeRepair(opts), + recover: this.recoverFromTerminalUnavailable, + retry: (provider) => provider.spawnWithoutTerminalRuntimeRepair(opts) + }) + } + + private async spawnWithoutTerminalRuntimeRepair(opts: PtySpawnOptions): Promise { + if (opts.agentSessionEnsure && opts.sessionId) { + throw new Error('agent_session_claim_unavailable') + } + if (opts.agentSessionEnsure) { + const supportsClaims = await this.supportsAgentSessionClaims({ signal: opts.signal }) + if (opts.signal?.aborted) { + throw new Error('client_disconnected') + } + if (!supportsClaims) { + throw new Error('agent_session_claim_unavailable') + } + } + if (opts.sessionId) { + return await reattachSshPtySessionForSpawn({ + mux: this.mux, + idMapping: this.ids, + sessionId: opts.sessionId, + options: opts, + exitRaceTracker: this.spawnExitRaces, + installSourceActivation: (relayPtyId, activation) => + this.outputState.installReceivingActivation(relayPtyId, activation), + rememberPtyIncarnation: (relayPtyId, incarnationId) => + this.outputState.rememberPtyIncarnation(relayPtyId, incarnationId), + acceptLivePty: (relayPtyId) => this.livePtyIds.add(relayPtyId) + }) + } + + const supportsCreateOperation = opts.agentSessionCreateOperationId + ? await this.supportsAgentSessionCreateOperations({ signal: opts.signal }) + : false + if (opts.signal?.aborted) { + throw new Error('client_disconnected') + } + if (opts.agentSessionCreateOperationId && !supportsCreateOperation) { + // Why: host routing owns legacy selection; a changed relay must not downgrade after dispatch. + throw new Error('execution_owner_unavailable') + } + return await spawnFreshSshPty({ + mux: this.mux, + options: opts, + params: buildSshPtySpawnRequest({ + options: opts, + remoteCliBridgeEnv: this.remoteCliBridgeEnv, + supportsCreateOperation + }), + exitRaceTracker: this.spawnExitRaces, + installSourceActivation: (id, activation) => + this.outputState.installReceivingActivation(id, activation), + rememberPtyIncarnation: (id, incarnation) => + this.outputState.rememberPtyIncarnation(id, incarnation), + acceptLivePty: (id) => this.livePtyIds.add(id), + toAppPtyId: this.toAppPtyId + }) + } + + async supportsAgentSessionClaims(options: { signal?: AbortSignal } = {}): Promise { + return await this.agentSessionCapabilities.supportsClaims(options) + } + + providesAgentSessionOwnerListings(_ptyId: string): boolean { + return this.agentSessionCapabilities.providesOwnerListings() + } + + async supportsAgentSessionCreateOperations( + options: { signal?: AbortSignal } = {} + ): Promise { + return await this.agentSessionCapabilities.supportsCreateOperations(options) + } + + async supportsForegroundProcessEvidence( + options: { signal?: AbortSignal } = {} + ): Promise { + return await this.agentSessionCapabilities.supportsForegroundProcessEvidence(options) + } + + async attach(id: string): Promise { + const relayPtyId = this.toRelayPtyId(id) + await requestSshPtyAttach({ + mux: this.mux, + relayPtyId, + params: { id: relayPtyId }, + commitSourceActivation: true, + installSourceActivation: (ptyId, activation) => + this.outputState.installReceivingActivation(ptyId, activation), + rememberPtyIncarnation: (ptyId, incarnationId) => + this.outputState.rememberPtyIncarnation(ptyId, incarnationId) + }) + } + + async attachForReconnect( + id: string, + expected?: { paneKey?: string; tabId?: string }, + sourceRecovery?: PtySourceRecoveryRequest, + requireReplay = false + ): Promise { + // Why: reconnect owns replay delivery so stale/duplicate attach results can + // be filtered before they reach the renderer. The expected identity lets the + // relay reject a cross-generation id collision instead of reattaching this + // lease to a different pane's freshly spawned PTY. + const params = { + id: this.toRelayPtyId(id), + suppressReplayNotification: true, + ...(requireReplay ? { requireReplay: true } : {}), + ...(sourceRecovery ? { sourceRecovery } : {}), + ...(expected?.paneKey ? { expectedPaneKey: expected.paneKey } : {}), + ...(expected?.tabId ? { expectedTabId: expected.tabId } : {}) + } + const relayPtyId = this.toRelayPtyId(id) + return await requestSshPtyAttach({ + mux: this.mux, + relayPtyId, + params, + timeoutMs: 10_000, + installSourceActivation: (ptyId, activation) => + this.outputState.installReceivingActivation(ptyId, activation), + rememberPtyIncarnation: (ptyId, incarnationId) => + this.outputState.rememberPtyIncarnation(ptyId, incarnationId) + }) + } + + async shutdown(id: string, opts: Parameters[1]): Promise { + // Both fences are omitted rather than sent undefined: a host that predates either must see no + // key at all, and the owner fence in particular must never reach it as a falsy claim. + const { expectedIncarnationId, expectedOwnerClientInstanceId } = opts + await this.mux.request( + 'pty.shutdown', + { + id: this.toRelayPtyId(id), + immediate: opts.immediate ?? false, + keepHistory: opts.keepHistory ?? false, + ...(expectedIncarnationId === undefined ? {} : { expectedIncarnationId }), + ...(expectedOwnerClientInstanceId === undefined ? {} : { expectedOwnerClientInstanceId }) + }, + relayTimeoutOptions(opts.deadlineMs) + ) + this.livePtyIds.delete(id) + } + + async listProcesses(opts?: { + deadlineMs?: number + includeForegroundProcessEvidence?: boolean + }): Promise { + const result = await this.mux.request( + 'pty.listProcesses', + opts?.includeForegroundProcessEvidence === undefined + ? undefined + : { includeForegroundProcessEvidence: opts.includeForegroundProcessEvidence }, + relayTimeoutOptions(opts?.deadlineMs) + ) + const processes = mapSshPtyProcessList(result as PtyProcessInfo[], (id) => this.toAppPtyId(id)) + for (const process of processes) { + this.livePtyIds.add(process.id) + const relayPtyId = this.toRelayPtyId(process.id) + this.outputState.rememberPtyIncarnation(relayPtyId, process.incarnationId) + } + return processes + } + + hasPty = (id: string): boolean => this.livePtyIds.has(id) + + onData = (callback: SshPtyDataCallback): (() => void) => this.outputState.onData(callback) + onRejectedData = (callback: SshPtyDataCallback): (() => void) => + this.outputState.onRejectedData(callback) + onReplay = (callback: SshPtyReplayCallback): (() => void) => this.outputState.onReplay(callback) + onExit = (callback: SshPtyExitCallback): (() => void) => this.outputState.onExit(callback) + + setPtyDeliveryPauseAdapter(adapter: SshPtyDeliveryPauseAdapter | null): void { + this.outputState.setDeliveryPauseAdapter(adapter) + } + + hasPtyDeliveryPauseAdapter(): boolean { + return this.outputState.hasDeliveryPauseAdapter() + } + + pauseProducer(id: string): void { + this.outputState.pause(this.toRelayPtyId(id)) + } + + resumeProducer(id: string): void { + this.outputState.resume(this.toRelayPtyId(id)) + } + + closeOutputIntake(reason: string): void { + this.mux.dispose('connection_lost') + console.error('[ssh-pty-provider] closed after bounded output intake failure', { reason }) + } +} diff --git a/src/main/providers/relay-pty-source-credit-registration.test.ts b/src/main/providers/relay-pty-source-credit-registration.test.ts new file mode 100644 index 000000000000..f47e78bfab49 --- /dev/null +++ b/src/main/providers/relay-pty-source-credit-registration.test.ts @@ -0,0 +1,72 @@ +import { beforeEach, expect, it, vi } from 'vitest' +import { registerRelayPtySourceCredit } from './relay-pty-source-credit-registration' +import { SshChannelMultiplexer } from '../ssh/ssh-channel-multiplexer' +import { + installSshPtySourceAckPublisher, + installSshPtySourceCancellationPublisher +} from '../ipc/ssh-pty-output-intake-registry' +vi.mock('../ipc/ssh-pty-output-intake-registry', () => ({ + installSshPtySourceAckPublisher: vi.fn(), + installSshPtySourceCancellationPublisher: vi.fn() +})) +const releaseAck = vi.fn() +const releaseCancellation = vi.fn() +beforeEach(() => { + vi.clearAllMocks() + vi.mocked(installSshPtySourceAckPublisher).mockReturnValue(releaseAck) + vi.mocked(installSshPtySourceCancellationPublisher).mockReturnValue(releaseCancellation) +}) +function fixture() { + const mux = new SshChannelMultiplexer({ + write: vi.fn(), + onData: vi.fn(), + onClose: vi.fn(), + close: vi.fn() + }) + const request = vi + .spyOn(mux, 'request') + .mockResolvedValue({ canceled: true, sentEndSu: 10, creditedEndSu: 5 }) + const registration = registerRelayPtySourceCredit(mux, 9, (id) => id.slice('wsl:'.length)) + const publish = vi.mocked(installSshPtySourceCancellationPublisher).mock.calls[0][1] + return { mux, request, registration, publish } +} +it('maps owner-specific cancellation IDs without an SSH identity', async () => { + const { mux, request, registration, publish } = fixture() + expect( + await publish({ + id: 'wsl:pty2:owner:1', + clientGeneration: 2, + ownerGeneration: 3, + deliveryToken: 'token' + }) + ).toEqual({ sentEndSu: 10, creditedEndSu: 5 }) + expect(request).toHaveBeenCalledWith( + 'pty.cancelDelivery', + expect.objectContaining({ id: 'pty2:owner:1' }) + ) + registration.releaseAck() + registration.releaseCancellation() + expect(releaseAck).toHaveBeenCalledOnce() + expect(releaseCancellation).toHaveBeenCalledOnce() + mux.dispose() +}) +it.each([ + null, + { canceled: false }, + { canceled: true, sentEndSu: 4, creditedEndSu: 5 }, + { canceled: true, sentEndSu: -1, creditedEndSu: 0 } +])('rejects invalid cancellation proof %j', async (result) => { + const { mux, request, publish } = fixture() + request.mockResolvedValue(result) + await expect( + publish({ id: 'wsl:pty', clientGeneration: 2, ownerGeneration: 3, deliveryToken: 'token' }) + ).rejects.toThrow('proof_invalid') + mux.dispose() +}) +it('releases the first registration if the second fails', () => { + vi.mocked(installSshPtySourceCancellationPublisher).mockImplementation(() => { + throw new Error('duplicate') + }) + expect(fixture).toThrow('duplicate') + expect(releaseAck).toHaveBeenCalledOnce() +}) diff --git a/src/main/providers/relay-pty-source-credit-registration.ts b/src/main/providers/relay-pty-source-credit-registration.ts new file mode 100644 index 000000000000..a66dada12620 --- /dev/null +++ b/src/main/providers/relay-pty-source-credit-registration.ts @@ -0,0 +1,53 @@ +import { + installSshPtySourceAckPublisher, + installSshPtySourceCancellationPublisher +} from '../ipc/ssh-pty-output-intake-registry' +import type { SshChannelMultiplexer } from '../ssh/ssh-channel-multiplexer' + +/** SSH and guest relays share one credit authority; only their terminal ID mapping differs. */ +export function registerRelayPtySourceCredit( + mux: SshChannelMultiplexer, + generation: number, + toRelayPtyId: (id: string) => string +): { releaseAck: () => void; releaseCancellation: () => void } { + const releaseAck = installSshPtySourceAckPublisher(generation, (batch, onSettled) => + mux.notifyWithSettlement('pty.ackData', { ...batch }, (settlement) => + onSettled( + settlement.outcome === 'accepted' ? { ok: true } : { ok: false, error: settlement.error } + ) + ) + ) + try { + const releaseCancellation = installSshPtySourceCancellationPublisher( + generation, + async (request) => { + const result: unknown = await mux.request('pty.cancelDelivery', { + ...request, + id: toRelayPtyId(request.id) + }) + if ( + !result || + typeof result !== 'object' || + !('canceled' in result) || + result.canceled !== true || + !('sentEndSu' in result) || + typeof result.sentEndSu !== 'number' || + !Number.isSafeInteger(result.sentEndSu) || + result.sentEndSu < 0 || + !('creditedEndSu' in result) || + typeof result.creditedEndSu !== 'number' || + !Number.isSafeInteger(result.creditedEndSu) || + result.creditedEndSu < 0 || + result.creditedEndSu > result.sentEndSu + ) { + throw new Error('ssh_source_cancellation_proof_invalid') + } + return { sentEndSu: result.sentEndSu, creditedEndSu: result.creditedEndSu } + } + ) + return { releaseAck, releaseCancellation } + } catch (error) { + releaseAck() + throw error + } +} diff --git a/src/main/providers/relay-pty-source-recovery.ts b/src/main/providers/relay-pty-source-recovery.ts new file mode 100644 index 000000000000..73bef2a92566 --- /dev/null +++ b/src/main/providers/relay-pty-source-recovery.ts @@ -0,0 +1,97 @@ +import { + beginSshPtyOutputGenerationMigration, + closeSshPtyOutputGeneration, + getSshPtyAcceptedSourceCheckpoints +} from '../ipc/ssh-pty-output-intake-registry' +import type { SshPtyAcceptedSourceCheckpoint } from '../ipc/ssh-pty-output-source-obligations' +import type { SshPtyOutputMigrationResult } from '../ipc/ssh-pty-output-model-migration' +import type { PtySourceRecoveryRequest } from './ssh-pty-session-reattach' + +export type RelayPtySourceRecovery = { + checkpointsByAppPtyId: Map + modelMigrationsByAppPtyId: Map> +} + +export async function relayPtySourceRecoveryRequest( + recovery: RelayPtySourceRecovery | undefined, + appPtyId: string, + relayPtyId: string +): Promise { + const migration = recovery?.modelMigrationsByAppPtyId.get(appPtyId) + if (migration) { + const outcome = await migration + if (recovery?.modelMigrationsByAppPtyId.get(appPtyId) === migration) { + recovery.modelMigrationsByAppPtyId.delete(appPtyId) + } + if (outcome.status !== 'settled') { + return Object.freeze({ status: 'checkpointUnavailable' }) + } + } + const checkpoints = recovery?.checkpointsByAppPtyId + const checkpoint = checkpoints?.get(appPtyId) ?? checkpoints?.get(relayPtyId) + if (!checkpoint) { + return Object.freeze({ status: 'checkpointUnavailable' }) + } + return Object.freeze({ + status: 'checkpoint', + clientGeneration: checkpoint.clientGeneration, + ownerGeneration: checkpoint.ownerGeneration, + ptyIncarnation: checkpoint.ptyIncarnation, + deliveryToken: checkpoint.deliveryToken, + acceptedSourceEndSu: checkpoint.acceptedSourceEndSu + }) +} + +/** Move accepted model obligations before retiring a replaceable relay transport. */ +export function migrateRelayPtySourceGeneration(args: { + generation: number + closeReason: string + getRecovery: () => RelayPtySourceRecovery | undefined + toRelayPtyId: (id: string) => string +}): void { + const { generation, closeReason, getRecovery, toRelayPtyId } = args + const recovery = getRecovery() + if (!recovery) { + closeSshPtyOutputGeneration(generation, closeReason) + return + } + for (const checkpoint of getSshPtyAcceptedSourceCheckpoints(generation)) { + recovery.checkpointsByAppPtyId.set(checkpoint.id, checkpoint) + } + const migration = beginSshPtyOutputGenerationMigration(generation) + for (const [ptyId, result] of migration.byPty) { + const previous = recovery.modelMigrationsByAppPtyId.get(ptyId) + const fence = previous ? previous.then(() => result) : result + recovery.modelMigrationsByAppPtyId.set(ptyId, fence) + void fence.then((outcome) => { + const current = getRecovery() + if (current?.modelMigrationsByAppPtyId.get(ptyId) !== fence) { + return + } + if (outcome.status === 'settled') { + current.checkpointsByAppPtyId.set(ptyId, outcome.checkpoint) + } else { + current.checkpointsByAppPtyId.delete(ptyId) + current.checkpointsByAppPtyId.delete(toRelayPtyId(ptyId)) + } + current.modelMigrationsByAppPtyId.delete(ptyId) + }) + } + void migration.completion.then(() => closeSshPtyOutputGeneration(generation, closeReason)) +} + +/** A rejected lease cannot authorize checkpoints captured under its previous generation. */ +export function invalidateRelayPtySourceRecovery(recovery: RelayPtySourceRecovery): void { + recovery.checkpointsByAppPtyId.clear() + for (const [ptyId, migration] of recovery.modelMigrationsByAppPtyId) { + recovery.modelMigrationsByAppPtyId.set( + ptyId, + migration.then(() => + Object.freeze({ + status: 'checkpoint-unavailable' as const, + reason: 'completion-failed' as const + }) + ) + ) + } +} diff --git a/src/main/providers/settled-pty-writer-census.test.ts b/src/main/providers/settled-pty-writer-census.test.ts index 08dd99894007..1890ae62213b 100644 --- a/src/main/providers/settled-pty-writer-census.test.ts +++ b/src/main/providers/settled-pty-writer-census.test.ts @@ -1,12 +1,11 @@ -import { readFileSync } from 'node:fs' -import { join } from 'node:path' -import { execFileSync } from 'node:child_process' +import { readdirSync, readFileSync } from 'node:fs' +import { join, relative } from 'node:path' import { describe, expect, it, vi } from 'vitest' -import { LocalPtyProvider } from './local-pty-provider' import { SshPtyProvider } from './ssh-pty-provider' import { createMockMux } from './ssh-pty-provider-mock-multiplexer' import { DaemonPtyRouter } from '../daemon/daemon-pty-router' -import { DegradedDaemonPtyProvider } from '../daemon/degraded-daemon-pty-provider' +import { WslDaemonPtyProvider } from '../wsl/wsl-daemon-pty-provider' +import { createUnavailablePtyProvider } from './unavailable-pty-provider' import { DaemonPtyAdapter } from '../daemon/daemon-pty-adapter' vi.mock('electron', () => ({ @@ -25,33 +24,35 @@ const REPO_ROOT = join(__dirname, '..', '..', '..') * fabricated handoff fails here rather than silently clearing a mailbox reservation. */ const SETTLED_PTY_WRITER_FILES = [ - 'src/main/providers/local-pty-provider.ts', - 'src/main/providers/ssh-pty-provider.ts', + 'src/main/providers/relay-pty-provider.ts', 'src/main/daemon/daemon-pty-router.ts', - 'src/main/daemon/degraded-daemon-pty-provider.ts', - 'src/main/daemon/daemon-pty-adapter.ts' + 'src/main/daemon/daemon-pty-adapter.ts', + 'src/main/wsl/wsl-daemon-pty-provider.ts' ] /** Where the provider-side settlement is actually decided; the adapter inherits its own. */ const SETTLED_WRITER_DECLARATIONS = [ - 'src/main/providers/local-pty-provider.ts', - 'src/main/providers/ssh-pty-provider.ts', + 'src/main/providers/relay-pty-provider.ts', 'src/main/providers/ssh-pty-provider-rpc-operations.ts', 'src/main/daemon/daemon-pty-router.ts', - 'src/main/daemon/degraded-daemon-pty-provider.ts', - 'src/main/daemon/daemon-pty-session-input.ts' + 'src/main/daemon/daemon-pty-session-input.ts', + 'src/main/wsl/wsl-daemon-pty-provider.ts', + 'src/main/providers/unavailable-pty-provider.ts' ] -function declaredProviderFiles(): string[] { - const output = execFileSync('git', ['grep', '-l', '--', 'implements IPtyProvider', 'src/main'], { - cwd: REPO_ROOT, - encoding: 'utf8' - }) - // Tests may name the clause while pinning it; only production declarations count. - return output - .split('\n') - .filter((file) => file && !file.endsWith('.test.ts')) - .sort() +function declaredProviderFiles(directory = join(REPO_ROOT, 'src/main')): string[] { + const files: string[] = [] + for (const entry of readdirSync(directory, { withFileTypes: true })) { + const path = join(directory, entry.name) + if (entry.isDirectory()) { + files.push(...declaredProviderFiles(path)) + } else if (entry.name.endsWith('.ts') && !entry.name.endsWith('.test.ts')) { + if (/\bclass\s+\w+[^{}]*\bimplements\s+IPtyProvider\b/.test(readFileSync(path, 'utf8'))) { + files.push(relative(REPO_ROOT, path).split('\\').join('/')) + } + } + } + return files.sort() } function settledWriterBody(file: string): string { @@ -68,18 +69,16 @@ describe('settled PTY writer census', () => { }) it('exposes a settled writer on every production provider instance', () => { - const daemonClient = { isConnected: () => false, onEvent: vi.fn(() => vi.fn()) } - const adapter = new DaemonPtyAdapter(daemonClient as never) + const adapter = new DaemonPtyAdapter({ + socketPath: join(__dirname, 'census.sock'), + tokenPath: join(__dirname, 'census.token') + }) const instances = [ - new LocalPtyProvider({} as never), new SshPtyProvider('conn-census', createMockMux() as never), new DaemonPtyRouter({ current: adapter, legacy: [] }), - new DegradedDaemonPtyProvider({ - current: adapter, - legacy: [], - fallback: new LocalPtyProvider({} as never) - }), - adapter + adapter, + new WslDaemonPtyProvider({ distro: 'Ubuntu', relayBuildId: 'census-build' }, adapter), + createUnavailablePtyProvider() ] for (const provider of instances) { expect(typeof provider.writeWithSettlement, provider.constructor.name).toBe('function') diff --git a/src/main/providers/shell-startup-command-bun-fixture.ts b/src/main/providers/shell-startup-command-bun-fixture.ts new file mode 100644 index 000000000000..cf021160b429 --- /dev/null +++ b/src/main/providers/shell-startup-command-bun-fixture.ts @@ -0,0 +1,116 @@ +import { spawnBunPty } from '../daemon/pty-subprocess/bun-pty-process' +import { PtyStartupIngress } from '../../shared/pty-startup-ingress' +import type { TerminalViewRgb } from '../../shared/terminal-view-attributes' +import { HeadlessEmulator } from '../daemon/headless-emulator' +import { POSIX_SHELL_STARTUP_COMMAND_ENV } from '../pty/posix-shell-startup-command' +import { setAppEnvironment } from '../../shared/app-environment' +import { getShellLaunchConfig } from './local-pty-shell-ready' + +const STARTUP_COMMAND = `bash -c 'read -r line; printf "__STARTUP_INPUT__:%s\\n" "$line"'` +const BLACK: TerminalViewRgb = [0, 0, 0] + +function afterCommand(shell: string): string { + if (shell === 'fish') { + return `if set -q ${POSIX_SHELL_STARTUP_COMMAND_ENV}; echo __AFTER_ENV__:present; else; echo __AFTER_ENV__:missing; end; exit\n` + } + return `printf '__AFTER_ENV__:%s\\n' "\${${POSIX_SHELL_STARTUP_COMMAND_ENV}-missing}"; exit\n` +} + +export async function runShellStartupCommandFixture({ + shell, + home +}: { + shell: string + home: string +}): Promise { + setAppEnvironment({ + getPath: () => home, + getAppPath: () => home, + getVersion: () => 'test', + isPackaged: () => false, + onWillQuit() {}, + exit: (code) => process.exit(code), + getAppMetrics: () => [] + }) + const launch = getShellLaunchConfig( + shell, + ['overlay', 'markers', 'ready', 'identity'], + STARTUP_COMMAND + ) + if (launch.env[POSIX_SHELL_STARTUP_COMMAND_ENV] !== STARTUP_COMMAND) { + throw new Error('Startup command was not embedded') + } + return await new Promise((resolve, reject) => { + let proc!: ReturnType + const emulator = new HeadlessEmulator({ + cols: 120, + rows: 30, + onQueryReply: (reply) => { + if (!ingress.answerLiveQueryReply(reply)) { + proc.write(reply) + } + } + }) + emulator.installViewAttributeResponder(() => ({ + foreground: [255, 255, 255], + background: BLACK, + cursor: [255, 255, 255], + ansi: Array.from({ length: 256 }, () => BLACK), + colorSchemeMode: 'dark', + cursorStyle: 'block', + cursorBlink: false + })) + proc = spawnBunPty({ + file: shell, + args: launch.args ?? [], + cols: 120, + rows: 30, + cwd: home, + env: { + ...Object.fromEntries( + Object.entries(process.env).filter( + (entry): entry is [string, string] => entry[1] !== undefined + ) + ), + ...launch.env, + HOME: home, + ORCA_ORIG_ZDOTDIR: home, + ORCA_ZSHENV_SOURCE_DIR: home, + TERM: 'xterm-256color' + } + }) + let transcript = '' + let sentInput = false + let sentAfter = false + const ingress = new PtyStartupIngress({ + ownerBackend: 'posix-pty', + write: (data) => proc.write(data), + onEmission: (emission) => { + transcript += emission.data + void emulator.write(emission.data, { forwardQueryReplies: true }) + if (!sentInput && transcript.includes(STARTUP_COMMAND)) { + sentInput = true + proc.write('hello\n') + } + if (!sentAfter && transcript.includes('__STARTUP_INPUT__:hello')) { + sentAfter = true + proc.write(afterCommand(shell)) + } + } + }) + const timeout = setTimeout(() => { + proc.kill() + emulator.dispose() + reject(new Error(`${shell} startup command timed out: ${JSON.stringify(transcript)}`)) + }, 5_000) + + proc.onData((data) => ingress.accept(data)) + proc.onExit(() => { + clearTimeout(timeout) + ingress.drainAndClose() + emulator.dispose() + proc.destroy() + resolve(transcript) + }) + }) +} diff --git a/src/main/providers/shell-startup-command-bun.integration.test.ts b/src/main/providers/shell-startup-command-bun.integration.test.ts new file mode 100644 index 000000000000..a61ae8fe54b8 --- /dev/null +++ b/src/main/providers/shell-startup-command-bun.integration.test.ts @@ -0,0 +1,45 @@ +import { mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { describe, expect, it } from 'vitest' +import { runProcessSync } from '../../shared/child-process/run-process' +import { runBundledBunFixture } from '../bundled-bun-test-execution' + +const SHELLS = ['bash', 'zsh', 'fish'].filter((shell) => { + if (process.platform === 'win32') { + return false + } + try { + return runProcessSync({ program: shell, args: ['--version'] }).code === 0 + } catch { + return false + } +}) +const STARTUP_COMMAND = `bash -c 'read -r line; printf "__STARTUP_INPUT__:%s\\n" "$line"'` + +describe('bundled Bun POSIX shell startup-command delivery', () => { + it.each(SHELLS)( + '%s runs the command once, forwards stdin, and keeps the shell', + async (shell) => { + const home = mkdtempSync(join(tmpdir(), `orca-${shell}-startup-command-`)) + try { + const output = await runBundledBunFixture( + join(__dirname, 'shell-startup-command-bun-fixture.ts'), + 'runShellStartupCommandFixture', + { shell, home }, + 10_000 + ) + expect(typeof output).toBe('string') + if (typeof output !== 'string') { + throw new Error('Expected terminal transcript') + } + expect(output.split(STARTUP_COMMAND)).toHaveLength(2) + expect(output).toContain('__STARTUP_INPUT__:hello') + expect(output).toContain('__AFTER_ENV__:missing') + } finally { + rmSync(home, { recursive: true, force: true }) + } + }, + 15_000 + ) +}) diff --git a/src/main/providers/ssh-pty-applied-size.ts b/src/main/providers/ssh-pty-applied-size.ts index f725f7c2a091..b92517103162 100644 --- a/src/main/providers/ssh-pty-applied-size.ts +++ b/src/main/providers/ssh-pty-applied-size.ts @@ -8,7 +8,7 @@ const SSH_PTY_APPLIED_SIZE_TIMEOUT_MS = 1_000 export function createSshPtyAppliedSizeReader( mux: SshChannelMultiplexer, - connectionId: string + owner: string | ((id: string) => string) ): (id: string) => Promise { let supported: boolean | null = null return async (id) => { @@ -16,19 +16,19 @@ export function createSshPtyAppliedSizeReader( return null } try { - const result = (await mux.request( + const result = await mux.request( 'pty.getSize', { - id: toRelaySshPtyId(connectionId, id) + id: typeof owner === 'string' ? toRelaySshPtyId(owner, id) : owner(id) }, { timeoutMs: SSH_PTY_APPLIED_SIZE_TIMEOUT_MS } - )) as { - cols?: unknown - rows?: unknown - } | null + ) supported = true if ( !result || + typeof result !== 'object' || + !('cols' in result) || + !('rows' in result) || !Number.isInteger(result.cols) || !Number.isInteger(result.rows) || Number(result.cols) <= 0 || diff --git a/src/main/providers/ssh-pty-provider.ts b/src/main/providers/ssh-pty-provider.ts index 76b5d9f85e48..810d4697e4f0 100644 --- a/src/main/providers/ssh-pty-provider.ts +++ b/src/main/providers/ssh-pty-provider.ts @@ -1,317 +1,18 @@ import type { SshChannelMultiplexer } from '../ssh/ssh-channel-multiplexer' -import type { IPtyProvider, PtyProcessInfo, PtySpawnOptions, PtySpawnResult } from './types' -import type { WriteSettlement } from '../../shared/pty-write-settlement' -import { toAppSshPtyId, toRelaySshPtyId } from './ssh-pty-id' -import { createSshPtyAppliedSizeReader } from './ssh-pty-applied-size' -import type { - RemoteCliBridgeEnv, - SshPtyDataCallback, - SshPtyDeliveryPauseAdapter, - SshPtyExitCallback, - SshPtyReplayCallback -} from './ssh-pty-provider-contract' -import { SshPtyProviderOutputState } from './ssh-pty-provider-output-state' -import { spawnFreshSshPty } from './ssh-agent-session-create-operation' -import { mapSshPtyProcessList } from './ssh-agent-session-process-list' -import { - requestSshPtyAttach, - reattachSshPtySessionForSpawn, - type PtySourceRecoveryRequest, - type SshPtyAttachResult -} from './ssh-pty-session-reattach' -import { buildSshPtySpawnRequest } from './ssh-pty-spawn-request' -import { SshPtySpawnExitRaceTracker } from './ssh-pty-spawn-exit-race' -import { SshAgentSessionCapabilities } from './ssh-agent-session-capabilities' -import type { PtyProcessInspection } from './pty-process-inspection' -import { spawnWithTerminalRuntimeRepair, type TerminalRepairHook } from './ssh-pty-spawn-repair' -import { createSshPtyProviderRpcOperations } from './ssh-pty-provider-rpc-operations' - -// Why: sequential relay teardown calls share one absolute budget; convert to the mux-relative timeout only at dispatch. -function relayTimeoutOptions(deadlineMs: number | undefined): { timeoutMs: number } | undefined { - return deadlineMs === undefined ? undefined : { timeoutMs: Math.max(1, deadlineMs - Date.now()) } -} - -/** Remote PTY provider that proxies IPtyProvider operations through the relay. */ -export class SshPtyProvider implements IPtyProvider { - private mux: SshChannelMultiplexer - private connectionId: string - private livePtyIds = new Set() - readonly getAppliedSize: NonNullable - private readonly agentSessionCapabilities: SshAgentSessionCapabilities - private spawnExitRaces = new SshPtySpawnExitRaceTracker() - private readonly outputState: SshPtyProviderOutputState - private recoverFromTerminalUnavailable: TerminalRepairHook | null = null - private readonly rpcOperations: ReturnType - - deleteWorktreeHistory = (worktreeId: string): Promise => - this.rpcOperations.deleteWorktreeHistory(worktreeId) - write = (id: string, data: string): boolean => this.rpcOperations.write(id, data) - writeWithSettlement = (id: string, data: string): Promise => - this.rpcOperations.writeWithSettlement(id, data) - resize = (id: string, cols: number, rows: number): void => - this.rpcOperations.resize(id, cols, rows) - sendSignal = (id: string, signal: string): Promise => - this.rpcOperations.sendSignal(id, signal) - getCwd = (id: string): Promise => this.rpcOperations.getCwd(id) - getInitialCwd = (id: string): Promise => this.rpcOperations.getInitialCwd(id) - clearBuffer = (id: string): Promise => this.rpcOperations.clearBuffer(id) - closeStartupQueryAuthority = (id: string): Promise => - this.rpcOperations.closeStartupQueryAuthority(id) - acknowledgeDataEvent = (id: string, charCount: number): void => - this.rpcOperations.acknowledgeDataEvent(id, charCount) - hasChildProcesses = (id: string): Promise => this.rpcOperations.hasChildProcesses(id) - getForegroundProcess = (id: string): Promise => - this.rpcOperations.getForegroundProcess(id) - inspectProcess = ( - id: string, - options?: { expectedIncarnationId?: string; scanChildProcesses?: boolean } - ): Promise => this.rpcOperations.inspectProcess(id, options) - serialize = (ids: string[]): Promise => this.rpcOperations.serialize(ids) - revive = (state: string): Promise => this.rpcOperations.revive(state) - getDefaultShell = (): Promise => this.rpcOperations.getDefaultShell() - getProfiles = (): Promise<{ name: string; path: string }[]> => this.rpcOperations.getProfiles() - - requestHostRpc: NonNullable = (method, params, options) => - this.mux.request(method, params as Record, options) +import type { RemoteCliBridgeEnv } from './ssh-pty-provider-contract' +import { RelayPtyProvider } from './relay-pty-provider' +import { sshRelayPtyIdMapping } from './relay-pty-id-mapping' +/** SSH owns connection identity; the relay provider owns transport and terminal semantics. */ +export class SshPtyProvider extends RelayPtyProvider { constructor( - connectionId: string, + private readonly connectionId: string, mux: SshChannelMultiplexer, - private readonly remoteCliBridgeEnv?: RemoteCliBridgeEnv, - readonly providerGeneration = 1 + remoteCliBridgeEnv?: RemoteCliBridgeEnv, + providerGeneration = 1 ) { - this.connectionId = connectionId - this.mux = mux - this.rpcOperations = createSshPtyProviderRpcOperations({ - mux, - toRelayPtyId: (id) => this.toRelayPtyId(id) - }) - this.agentSessionCapabilities = new SshAgentSessionCapabilities(mux) - this.getAppliedSize = createSshPtyAppliedSizeReader(mux, connectionId) - - this.outputState = new SshPtyProviderOutputState(providerGeneration, { - mux, - toAppPtyId: (id) => this.toAppPtyId(id), - livePtyIds: this.livePtyIds, - recordExit: (relayPtyId, incarnationId) => { - this.spawnExitRaces.recordExit(relayPtyId, incarnationId) - } - }) - } - - dispose(): void { - this.outputState.dispose() - this.livePtyIds.clear() + super(sshRelayPtyIdMapping(connectionId), mux, remoteCliBridgeEnv, providerGeneration) } getConnectionId = (): string => this.connectionId - - canProvideAuthoritativeBufferSnapshot = (_id: string): boolean => false - - private toRelayPtyId = (id: string): string => toRelaySshPtyId(this.connectionId, id) - - private toAppPtyId = (id: string): string => toAppSshPtyId(this.connectionId, id) - - /** Installed by SshRelaySession, which owns the connection, the repair lock and the reconnect. */ - setTerminalUnavailableRecovery(recover: TerminalRepairHook): void { - this.recoverFromTerminalUnavailable = recover - } - - hasLivePtys(): boolean { - return this.livePtyIds.size > 0 - } - - async spawn(opts: PtySpawnOptions): Promise { - return await spawnWithTerminalRuntimeRepair({ - attempt: () => this.spawnWithoutTerminalRuntimeRepair(opts), - recover: this.recoverFromTerminalUnavailable, - retry: (provider) => provider.spawnWithoutTerminalRuntimeRepair(opts) - }) - } - - private async spawnWithoutTerminalRuntimeRepair(opts: PtySpawnOptions): Promise { - if (opts.agentSessionEnsure && opts.sessionId) { - throw new Error('agent_session_claim_unavailable') - } - if (opts.agentSessionEnsure) { - const supportsClaims = await this.supportsAgentSessionClaims({ signal: opts.signal }) - if (opts.signal?.aborted) { - throw new Error('client_disconnected') - } - if (!supportsClaims) { - throw new Error('agent_session_claim_unavailable') - } - } - if (opts.sessionId) { - return await reattachSshPtySessionForSpawn({ - mux: this.mux, - connectionId: this.connectionId, - sessionId: opts.sessionId, - options: opts, - exitRaceTracker: this.spawnExitRaces, - installSourceActivation: (relayPtyId, activation) => - this.outputState.installReceivingActivation(relayPtyId, activation), - rememberPtyIncarnation: (relayPtyId, incarnationId) => - this.outputState.rememberPtyIncarnation(relayPtyId, incarnationId), - acceptLivePty: (relayPtyId) => this.livePtyIds.add(relayPtyId) - }) - } - - const supportsCreateOperation = opts.agentSessionCreateOperationId - ? await this.supportsAgentSessionCreateOperations({ signal: opts.signal }) - : false - if (opts.signal?.aborted) { - throw new Error('client_disconnected') - } - if (opts.agentSessionCreateOperationId && !supportsCreateOperation) { - // Why: host routing owns legacy selection; a changed relay must not downgrade after dispatch. - throw new Error('execution_owner_unavailable') - } - return await spawnFreshSshPty({ - mux: this.mux, - options: opts, - params: buildSshPtySpawnRequest({ - options: opts, - remoteCliBridgeEnv: this.remoteCliBridgeEnv, - supportsCreateOperation - }), - exitRaceTracker: this.spawnExitRaces, - installSourceActivation: (id, activation) => - this.outputState.installReceivingActivation(id, activation), - rememberPtyIncarnation: (id, incarnation) => - this.outputState.rememberPtyIncarnation(id, incarnation), - acceptLivePty: (id) => this.livePtyIds.add(id), - toAppPtyId: this.toAppPtyId - }) - } - - async supportsAgentSessionClaims(options: { signal?: AbortSignal } = {}): Promise { - return await this.agentSessionCapabilities.supportsClaims(options) - } - - providesAgentSessionOwnerListings(_ptyId: string): boolean { - return this.agentSessionCapabilities.providesOwnerListings() - } - - async supportsAgentSessionCreateOperations( - options: { signal?: AbortSignal } = {} - ): Promise { - return await this.agentSessionCapabilities.supportsCreateOperations(options) - } - - async supportsForegroundProcessEvidence( - options: { signal?: AbortSignal } = {} - ): Promise { - return await this.agentSessionCapabilities.supportsForegroundProcessEvidence(options) - } - - async attach(id: string): Promise { - const relayPtyId = this.toRelayPtyId(id) - await requestSshPtyAttach({ - mux: this.mux, - relayPtyId, - params: { id: relayPtyId }, - commitSourceActivation: true, - installSourceActivation: (ptyId, activation) => - this.outputState.installReceivingActivation(ptyId, activation), - rememberPtyIncarnation: (ptyId, incarnationId) => - this.outputState.rememberPtyIncarnation(ptyId, incarnationId) - }) - } - - async attachForReconnect( - id: string, - expected?: { paneKey?: string; tabId?: string }, - sourceRecovery?: PtySourceRecoveryRequest - ): Promise { - // Why: reconnect owns replay delivery so stale/duplicate attach results can - // be filtered before they reach the renderer. The expected identity lets the - // relay reject a cross-generation id collision instead of reattaching this - // lease to a different pane's freshly spawned PTY. - const params = { - id: this.toRelayPtyId(id), - suppressReplayNotification: true, - ...(sourceRecovery ? { sourceRecovery } : {}), - ...(expected?.paneKey ? { expectedPaneKey: expected.paneKey } : {}), - ...(expected?.tabId ? { expectedTabId: expected.tabId } : {}) - } - const relayPtyId = this.toRelayPtyId(id) - return await requestSshPtyAttach({ - mux: this.mux, - relayPtyId, - params, - timeoutMs: 10_000, - installSourceActivation: (ptyId, activation) => - this.outputState.installReceivingActivation(ptyId, activation), - rememberPtyIncarnation: (ptyId, incarnationId) => - this.outputState.rememberPtyIncarnation(ptyId, incarnationId) - }) - } - - async shutdown(id: string, opts: Parameters[1]): Promise { - // Both fences are omitted rather than sent undefined: a host that predates either must see no - // key at all, and the owner fence in particular must never reach it as a falsy claim. - const { expectedIncarnationId, expectedOwnerClientInstanceId } = opts - await this.mux.request( - 'pty.shutdown', - { - id: this.toRelayPtyId(id), - immediate: opts.immediate ?? false, - keepHistory: opts.keepHistory ?? false, - ...(expectedIncarnationId === undefined ? {} : { expectedIncarnationId }), - ...(expectedOwnerClientInstanceId === undefined ? {} : { expectedOwnerClientInstanceId }) - }, - relayTimeoutOptions(opts.deadlineMs) - ) - this.livePtyIds.delete(id) - } - - async listProcesses(opts?: { - deadlineMs?: number - includeForegroundProcessEvidence?: boolean - }): Promise { - const result = await this.mux.request( - 'pty.listProcesses', - opts?.includeForegroundProcessEvidence === undefined - ? undefined - : { includeForegroundProcessEvidence: opts.includeForegroundProcessEvidence }, - relayTimeoutOptions(opts?.deadlineMs) - ) - const processes = mapSshPtyProcessList(result as PtyProcessInfo[], (id) => this.toAppPtyId(id)) - for (const process of processes) { - this.livePtyIds.add(process.id) - const relayPtyId = this.toRelayPtyId(process.id) - this.outputState.rememberPtyIncarnation(relayPtyId, process.incarnationId) - } - return processes - } - - hasPty = (id: string): boolean => this.livePtyIds.has(id) - - onData = (callback: SshPtyDataCallback): (() => void) => this.outputState.onData(callback) - onRejectedData = (callback: SshPtyDataCallback): (() => void) => - this.outputState.onRejectedData(callback) - onReplay = (callback: SshPtyReplayCallback): (() => void) => this.outputState.onReplay(callback) - onExit = (callback: SshPtyExitCallback): (() => void) => this.outputState.onExit(callback) - - setPtyDeliveryPauseAdapter(adapter: SshPtyDeliveryPauseAdapter | null): void { - this.outputState.setDeliveryPauseAdapter(adapter) - } - - hasPtyDeliveryPauseAdapter(): boolean { - return this.outputState.hasDeliveryPauseAdapter() - } - - pauseProducer(id: string): void { - this.outputState.pause(this.toRelayPtyId(id)) - } - - resumeProducer(id: string): void { - this.outputState.resume(this.toRelayPtyId(id)) - } - - closeOutputIntake(reason: string): void { - this.mux.dispose('connection_lost') - console.error('[ssh-pty-provider] closed after bounded output intake failure', { reason }) - } } diff --git a/src/main/providers/ssh-pty-session-reattach.ts b/src/main/providers/ssh-pty-session-reattach.ts index f05373a03dc3..3367e7bc2e5e 100644 --- a/src/main/providers/ssh-pty-session-reattach.ts +++ b/src/main/providers/ssh-pty-session-reattach.ts @@ -10,7 +10,7 @@ import { isSshPtyNotFoundError } from './ssh-pty-errors' import { isProvenExitedPtyAttachRefusal } from '../../shared/pty-attach-absence-evidence' -import { toAppSshPtyId, toRelaySshPtyId } from './ssh-pty-id' +import { sshRelayPtyIdMapping, type RelayPtyIdMapping } from './relay-pty-id-mapping' import type { PtySpawnOptions, PtySpawnResult } from './types' import type { SshPtySpawnExitRaceTracker } from './ssh-pty-spawn-exit-race' import type { @@ -177,18 +177,27 @@ export type { PtySourceRecoveryRequest } const RESTORE_REQUIRED_ATTACH_ATTEMPTS = 2 -export async function reattachSshPtySession(args: { - mux: SshChannelMultiplexer - connectionId: string - sessionId: string - options: PtySpawnOptions - rememberPtyIncarnation?: (relayPtyId: string, incarnationId: unknown) => void - installSourceActivation?: ( - relayPtyId: string, - activation: PtySourceReceivingActivation - ) => SshPtyReceivingActivationLease -}): Promise { - const relaySessionId = toRelaySshPtyId(args.connectionId, args.sessionId) +type RelayPtyReattachIdentity = + | { connectionId: string; idMapping?: never } + | { connectionId?: never; idMapping: RelayPtyIdMapping } + +function reattachIds(identity: RelayPtyReattachIdentity): RelayPtyIdMapping { + return identity.idMapping ?? sshRelayPtyIdMapping(identity.connectionId) +} + +export async function reattachSshPtySession( + args: RelayPtyReattachIdentity & { + mux: SshChannelMultiplexer + sessionId: string + options: PtySpawnOptions + rememberPtyIncarnation?: (relayPtyId: string, incarnationId: unknown) => void + installSourceActivation?: ( + relayPtyId: string, + activation: PtySourceReceivingActivation + ) => SshPtyReceivingActivationLease + } +): Promise { + const relaySessionId = reattachIds(args).toRelayPtyId(args.sessionId) console.warn(`[ssh-pty] spawn() called with sessionId=${args.sessionId}, attempting pty.attach`) try { // Why: expected pane identity prevents a reused relay id from attaching the wrong shell. @@ -217,7 +226,7 @@ export async function reattachSshPtySession(args: { `[ssh-pty] pty.attach succeeded for ${args.sessionId}, replay=${!!attachResult.replay}` ) return { - id: toAppSshPtyId(args.connectionId, relaySessionId), + id: reattachIds(args).toAppPtyId(relaySessionId), isReattach: true, ...(attachResult.replay ? { replay: attachResult.replay } : {}), ...(attachResult.incarnationId ? { incarnationId: attachResult.incarnationId } : {}), @@ -262,7 +271,7 @@ export async function reattachSshPtySessionWithExitFence( let result: SshPtyReattachResult | undefined try { result = await reattachSshPtySession(args) - const relayPtyId = toRelaySshPtyId(args.connectionId, result.id) + const relayPtyId = reattachIds(args).toRelayPtyId(result.id) if ( args.exitRaceTracker.didMatchingExitArrive(operation, { id: relayPtyId, @@ -329,9 +338,6 @@ export async function reattachSshPtySessionForSpawn( // and positive evidence the PTY is live. Claiming expiry here made the caller retire the pane // binding and cold-restore the agent into a second copy of a running session. throw new Error( - `${SSH_PTY_SOURCE_RESTORE_REQUIRED_ERROR}: ${toRelaySshPtyId( - args.connectionId, - args.sessionId - )} ${restoreRequiredReason}` + `${SSH_PTY_SOURCE_RESTORE_REQUIRED_ERROR}: ${reattachIds(args).toRelayPtyId(args.sessionId)} ${restoreRequiredReason}` ) } diff --git a/src/main/providers/unavailable-pty-provider.test.ts b/src/main/providers/unavailable-pty-provider.test.ts new file mode 100644 index 000000000000..f5680d9c0117 --- /dev/null +++ b/src/main/providers/unavailable-pty-provider.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from 'vitest' +import { createUnavailablePtyProvider } from './unavailable-pty-provider' + +describe('unavailable terminal service', () => { + it('refuses new work without claiming existing processes exited', async () => { + const provider = createUnavailablePtyProvider() + await expect(provider.spawn({ cols: 80, rows: 24 })).rejects.toThrow( + 'Terminal service unavailable' + ) + await expect(provider.listProcesses()).rejects.toThrow('Terminal service unavailable') + await expect(provider.shutdown('existing', {})).rejects.toThrow('Terminal service unavailable') + await expect(provider.probePtyLiveness?.('existing')).resolves.toBeNull() + expect(provider.writeWithSettlement('existing', 'command')).toEqual({ + outcome: 'refused', + reason: 'provider_unavailable' + }) + }) +}) diff --git a/src/main/providers/unavailable-pty-provider.ts b/src/main/providers/unavailable-pty-provider.ts new file mode 100644 index 000000000000..6f567ae6993b --- /dev/null +++ b/src/main/providers/unavailable-pty-provider.ts @@ -0,0 +1,41 @@ +import { writeRefused } from '../../shared/pty-write-settlement' +import type { IPtyProvider } from './types' + +export function terminalServiceUnavailable(): Error { + return new Error( + 'Terminal service unavailable. Retry starting the terminal service; existing terminals have not been stopped.' + ) +} + +const unavailable = async (): Promise => { + throw terminalServiceUnavailable() +} +const unsubscribe = (): (() => void) => () => {} + +/** Startup must never silently move terminal ownership into Electron. */ +export function createUnavailablePtyProvider(): IPtyProvider { + return { + spawn: unavailable, + attach: unavailable, + probePtyLiveness: async () => null, + write: () => false, + writeWithSettlement: () => writeRefused('provider_unavailable'), + resize: () => {}, + shutdown: unavailable, + sendSignal: unavailable, + getCwd: unavailable, + getInitialCwd: unavailable, + clearBuffer: unavailable, + acknowledgeDataEvent: () => {}, + hasChildProcesses: async () => true, + getForegroundProcess: async () => null, + serialize: unavailable, + revive: unavailable, + listProcesses: unavailable, + getDefaultShell: unavailable, + getProfiles: unavailable, + onData: unsubscribe, + onReplay: unsubscribe, + onExit: unsubscribe + } +} diff --git a/src/main/providers/windows-conpty-wide-char-duplication.node-pty.test.ts b/src/main/providers/windows-conpty-wide-char-duplication.bun.test.ts similarity index 50% rename from src/main/providers/windows-conpty-wide-char-duplication.node-pty.test.ts rename to src/main/providers/windows-conpty-wide-char-duplication.bun.test.ts index 5f449bbfe498..f3eddde9a7f1 100644 --- a/src/main/providers/windows-conpty-wide-char-duplication.node-pty.test.ts +++ b/src/main/providers/windows-conpty-wide-char-duplication.bun.test.ts @@ -1,23 +1,5 @@ -/** - * Real-ConPTY reproduction for #15192: wide characters arrive doubled in the - * terminal buffer on Windows (`안녕하세요` -> `안안녕녕하하세세요요`), while - * Latin is never affected. - * - * Why at this layer and not in the renderer: every renderer-side duplication - * path (delivery replay, snapshot restore, output-queue chunking) is - * script-blind and would double ASCII too. The one component in the stack that - * represents a character as TWO cells is the Windows console text buffer, where - * a DBCS glyph occupies a leading and a trailing cell that both carry the same - * wchar. Emitting the trailing cell as text produces exactly the reported - * signature. So the assertion is on the bytes node-pty hands us, BEFORE xterm. - * - * Orca pins the ConPTY implementation with `useConptyDll: true` - * (local-pty-utils.ts, pty-subprocess.ts, windows-conpty-warmup.ts), so the - * bundled OpenConsole build is a variable we control and the system one is the - * A/B. Both are exercised here. - * - * Runs only on win32; skipped elsewhere (macOS/Linux have no ConPTY). - */ +/** Real Windows terminal-byte fidelity for Korean wide characters and resize repaints. */ +import { recordBunPtyTranscript } from './bun-pty-transcript-fixture' import { describe, expect, it } from 'vitest' import { stripAnsiEscapeSequences } from '../../shared/ansi-escape-sequences' import { isWideGlyph } from '../daemon/__fixtures__/terminal-wide-cell-grid' @@ -40,8 +22,7 @@ function doubledWideRuns(text: string): string[] { return hits } -async function runThroughConpty(opts: { useConptyDll: boolean }): Promise { - const nodePty = await import('node-pty') +async function runThroughConpty(): Promise { // Why node and not `echo`: cmd.exe's output codepage depends on the machine's // ANSI codepage, which would make the fixture bytes untrustworthy. Node always // writes UTF-8 here. @@ -53,54 +34,13 @@ async function runThroughConpty(opts: { useConptyDll: boolean }): Promise=${LINE_REPEATS}){clearInterval(t);process.exit(0);}},20);` ].join('') - const proc = nodePty.spawn(process.execPath, ['-e', script], { - name: 'xterm-256color', - // Why narrow: the reporter's doubling shows on wrapped rows, and a wide glyph - // straddling the wrap boundary is where the leading/trailing cell pair matters. + const events = await recordBunPtyTranscript({ + script, cols: 40, rows: 10, - cwd: process.cwd(), - env: process.env as Record, - ...(opts.useConptyDll ? { useConptyDll: true } : {}) + resizes: [38, 44, 36, 46].map((cols, index) => ({ cols, atMs: 60 + index * 50 })) }) - - let out = '' - proc.onData((chunk) => { - out += chunk - }) - // Why a resize mid-stream: ConPTY repaints its viewport on a size change, and - // the report is intermittent in a way that tracks repaints (the reporter's own - // workaround is resizing the window). - const resizes = [38, 44, 36, 46] - const timers = resizes.map((cols, index) => - setTimeout( - () => { - try { - proc.resize(cols, 10) - } catch { - /* the child may already have exited */ - } - }, - 60 + index * 50 - ) - ) - - try { - await new Promise((resolve) => { - proc.onExit(() => resolve()) - }) - } finally { - // Why: a child that never exits would otherwise outlive the run and leak into the rest of the job. - for (const timer of timers) { - clearTimeout(timer) - } - try { - proc.kill() - } catch { - /* already gone */ - } - } - return out + return events.map((event) => ('data' in event ? event.data : '')).join('') } // Why a self-test: the ConPTY cases only run on Windows, so without this the @@ -124,9 +64,9 @@ describe('doubled-wide-character detector', () => { describe('Windows ConPTY wide-character fidelity (#15192)', () => { itOnWindows( - 'does not double wide characters with the bundled ConPTY (useConptyDll)', + 'does not double wide characters through the Bun ConPTY backend', async () => { - const text = stripAnsiEscapeSequences(await runThroughConpty({ useConptyDll: true })) + const text = stripAnsiEscapeSequences(await runThroughConpty()) // Guard against a vacuous pass: the fixture must actually have reached us. expect(text).toContain(KOREAN_LINE.slice(0, 5)) expect(doubledWideRuns(text)).toEqual([]) @@ -135,13 +75,4 @@ describe('Windows ConPTY wide-character fidelity (#15192)', () => { }, 60_000 ) - - itOnWindows( - 'does not double wide characters with the system ConPTY (A/B for the bundled build)', - async () => { - const text = stripAnsiEscapeSequences(await runThroughConpty({ useConptyDll: false })) - expect(doubledWideRuns(text)).toEqual([]) - }, - 60_000 - ) }) diff --git a/src/main/providers/windows-console-attached-processes.test.ts b/src/main/providers/windows-console-attached-processes.test.ts index 5f96604ac162..73dd93657613 100644 --- a/src/main/providers/windows-console-attached-processes.test.ts +++ b/src/main/providers/windows-console-attached-processes.test.ts @@ -8,7 +8,26 @@ * it used to live in now answers a different question. */ import { EventEmitter } from 'node:events' -import { describe, expect, it, vi } from 'vitest' +import { beforeEach, describe, expect, it, vi } from 'vitest' +const { runtimeMock, spawnMock, versionMock } = vi.hoisted(() => ({ + runtimeMock: vi.fn(), + spawnMock: vi.fn(), + versionMock: vi.fn(() => 'version-one') +})) +vi.mock('../../shared/app-environment', () => ({ + getAppEnvironment: () => ({ + getAppPath: () => '/app', + getVersion: versionMock, + onWillQuit: vi.fn() + }) +})) +vi.mock('../daemon/daemon-bun-runtime', () => ({ resolveDesktopDaemonBunRuntime: runtimeMock })) +vi.mock('../../shared/child-process/run-process', () => ({ spawnProcess: spawnMock })) +beforeEach(() => { + runtimeMock.mockReset() + spawnMock.mockReset() + versionMock.mockReturnValue('version-one') +}) import { readWindowsConsoleAttachedProcessIds } from './windows-console-attached-processes' function forkWith(event: 'message' | 'error' | 'none', value?: unknown, pid?: number) { @@ -50,16 +69,17 @@ describe('readWindowsConsoleAttachedProcessIds', () => { } }) - it('returns exact console membership from the fixed node-pty helper', async () => { + it('returns exact console membership from the fixed Bun helper', async () => { const { forkProcess } = forkWith('message', [999, 101, 202, 303], 999) await expect( readWindowsConsoleAttachedProcessIds(101, { forkProcess, - resolveAgentPath: () => '/fixed/node-pty/conpty_console_list_agent.js' + resolveAgentPath: () => '/fixed/windows-bun-pty-gate-entry.js' }) ).resolves.toEqual(new Set([101, 202, 303])) - expect(forkProcess).toHaveBeenCalledWith('/fixed/node-pty/conpty_console_list_agent.js', [ + expect(forkProcess).toHaveBeenCalledWith('/fixed/windows-bun-pty-gate-entry.js', [ + '--console-process-list', '101' ]) }) @@ -107,6 +127,23 @@ describe('readWindowsConsoleAttachedProcessIds', () => { } }) + it('returns unknown when killing a timed-out helper throws', async () => { + vi.useFakeTimers() + try { + const { child, forkProcess } = forkWith('none') + child.kill.mockImplementation(() => { + throw new Error('access denied') + }) + const result = readWindowsConsoleAttachedProcessIds(101, { forkProcess, timeoutMs: 10 }) + await vi.advanceTimersByTimeAsync(10) + await expect(result).resolves.toBeNull() + child.emit('exit', 1) + expect(child.listenerCount('error')).toBe(0) + } finally { + vi.useRealTimers() + } + }) + it('absorbs an asynchronous kill error after timeout settlement', async () => { vi.useFakeTimers() try { @@ -123,3 +160,88 @@ describe('readWindowsConsoleAttachedProcessIds', () => { } }) }) + +it('uses bundled Bun rather than the Electron executable for console attachment', async () => { + const { child, forkProcess } = forkWith('message', [101, 999], 999) + runtimeMock.mockResolvedValue({ + execPath: '/durable/bun-runtime.exe', + entryPath: '/durable/daemon-entry.js' + }) + spawnMock.mockImplementation(forkProcess) + await expect(readWindowsConsoleAttachedProcessIds(101)).resolves.toEqual(new Set([101])) + expect(spawnMock).toHaveBeenCalledWith( + expect.objectContaining({ + program: '/durable/bun-runtime.exe', + args: [ + '--no-env-file', + '--config=NUL', + '--no-install', + '/durable/windows-bun-pty-gate-entry.js', + '--console-process-list', + '101' + ] + }) + ) + expect(child.kill).not.toHaveBeenCalled() + await expect(readWindowsConsoleAttachedProcessIds(101)).resolves.toEqual(new Set([101])) + expect(runtimeMock).toHaveBeenCalledTimes(1) +}) + +it('does not start a console helper after runtime resolution exceeds its budget', async () => { + vi.useFakeTimers() + try { + let resolve!: (value: { execPath: string; entryPath: string }) => void + runtimeMock.mockReturnValue( + new Promise((done) => { + resolve = done + }) + ) + vi.resetModules() + const { readWindowsConsoleAttachedProcessIds: readFresh } = + await import('./windows-console-attached-processes') + const pending = readFresh(101, { timeoutMs: 10 }) + await vi.advanceTimersByTimeAsync(10) + await expect(pending).resolves.toBeNull() + resolve({ execPath: '/late/bun', entryPath: '/late/daemon-entry.js' }) + await vi.advanceTimersByTimeAsync(1) + expect(spawnMock).not.toHaveBeenCalled() + } finally { + vi.useRealTimers() + } +}) + +it('coalesces concurrent verification and refreshes when the installed version changes', async () => { + vi.resetModules() + const { readWindowsConsoleAttachedProcessIds: readFresh } = + await import('./windows-console-attached-processes') + vi.stubGlobal('process', { ...process, versions: { ...process.versions, electron: '43.7.0' } }) + try { + let finish!: (runtime: { execPath: string; entryPath: string }) => void + runtimeMock.mockReturnValue( + new Promise((resolve) => { + finish = resolve + }) + ) + spawnMock.mockImplementation(() => { + const { child } = forkWith('none', undefined, 999) + queueMicrotask(() => child.emit('message', { consoleProcessList: [101, 999] })) + return child + }) + const first = readFresh(101) + const second = readFresh(101) + expect(runtimeMock).toHaveBeenCalledTimes(1) + finish({ execPath: '/v1/bun-runtime.exe', entryPath: '/v1/daemon-entry.js' }) + await expect(first).resolves.toEqual(new Set([101])) + await expect(second).resolves.toEqual(new Set([101])) + versionMock.mockReturnValue('version-two') + runtimeMock.mockResolvedValue({ + execPath: '/v2/bun-runtime.exe', + entryPath: '/v2/daemon-entry.js' + }) + await expect(readFresh(101)).resolves.toEqual(new Set([101])) + expect(runtimeMock).toHaveBeenCalledTimes(2) + expect(spawnMock.mock.lastCall?.[0].program).toBe('/v2/bun-runtime.exe') + } finally { + vi.unstubAllGlobals() + } +}) diff --git a/src/main/providers/windows-console-attached-processes.ts b/src/main/providers/windows-console-attached-processes.ts index 5c288ac8f8e9..933a46f25ebb 100644 --- a/src/main/providers/windows-console-attached-processes.ts +++ b/src/main/providers/windows-console-attached-processes.ts @@ -1,8 +1,11 @@ +import { bunOwnedRuntimeArgs } from '../../shared/bun-owned-runtime-args' import type { ChildProcess } from 'node:child_process' -import { forkProcess } from '../../shared/child-process/fork-process' +import { spawnProcess } from '../../shared/child-process/run-process' import { resolveWindowsBunPtyGateEntry } from '../daemon/pty-subprocess/windows-bun-pty-launch' import { WINDOWS_BUN_CONSOLE_LIST_ARGUMENT } from './windows-bun-console-process-list' +import { acquireWindowsConsoleRuntime } from './windows-console-runtime' + const CONPTY_PROCESS_LIST_TIMEOUT_MS = 3_000 type ProcessListMessage = { consoleProcessList?: unknown } @@ -13,14 +16,18 @@ type WindowsConsoleAttachedProcessDeps = { timeoutMs?: number } -function resolveConsoleListAgent(): string { - return process.versions.bun - ? resolveWindowsBunPtyGateEntry() - : require.resolve('node-pty/lib/conpty_console_list_agent.js') -} - -function spawnConsoleListAgent(modulePath: string, args: string[]): ChildProcess { - return forkProcess({ modulePath, args, stdio: ['ignore', 'ignore', 'ignore', 'ipc'] }) +function spawnConsoleListAgent(execPath: string, modulePath: string, args: string[]): ChildProcess { + const env = { ...process.env } + delete env.ELECTRON_RUN_AS_NODE + delete env.NODE_OPTIONS + delete env.NODE_PATH + delete env.BUN_OPTIONS + return spawnProcess({ + program: execPath, + args: [...bunOwnedRuntimeArgs('win32'), modulePath, ...args], + env, + stdio: ['ignore', 'ignore', 'ignore', 'ipc'] + }) } /** @@ -28,8 +35,8 @@ function spawnConsoleListAgent(modulePath: string, args: string[]): ChildProcess * * Distinct from job membership on purpose. `GetConsoleProcessList` must be * called from a process attached to that console, and a process can hold only - * one console at a time -- which is why node-pty answers it from a separate - * process, and why this still forks. + * one console at a time -- so the Bun gate answers it in a separate + * process without changing the caller's console. * * Only the candidate FILTER may use this. That filter exists to drop a * descendant which detached from the console (`Start-Process`, a GUI child), and @@ -40,23 +47,33 @@ function spawnConsoleListAgent(modulePath: string, args: string[]): ChildProcess * candidate already exists, not on every foreground poll. Bounding it to one * pooled, supervised helper is the remaining half of that fix. */ -export function readWindowsConsoleAttachedProcessIds( +export async function readWindowsConsoleAttachedProcessIds( rootPid: number, deps: WindowsConsoleAttachedProcessDeps = {} ): Promise | null> { if (!Number.isSafeInteger(rootPid) || rootPid <= 0 || rootPid >= 0xffff_ffff) { - return Promise.resolve(null) + return null } + const deadline = Date.now() + (deps.timeoutMs ?? CONPTY_PROCESS_LIST_TIMEOUT_MS) let child: ChildProcess + let runtime: Awaited> = null try { - child = (deps.forkProcess ?? spawnConsoleListAgent)( - (deps.resolveAgentPath ?? resolveConsoleListAgent)(), - process.versions.bun - ? [WINDOWS_BUN_CONSOLE_LIST_ARGUMENT, String(rootPid)] - : [String(rootPid)] - ) + const args = [WINDOWS_BUN_CONSOLE_LIST_ARGUMENT, String(rootPid)] + if (deps.forkProcess) { + child = deps.forkProcess((deps.resolveAgentPath ?? resolveWindowsBunPtyGateEntry)(), args) + } else { + runtime = await acquireWindowsConsoleRuntime(deadline - Date.now()) + if (!runtime || Date.now() >= deadline) { + return null + } + child = spawnConsoleListAgent(runtime.execPath, runtime.entryPath, args) + } } catch { - return Promise.resolve(null) + runtime?.invalidate() + return null + } finally { + // A spawned helper is now protected by the process-table retention check. + runtime?.release() } return new Promise((resolve) => { @@ -66,6 +83,9 @@ export function readWindowsConsoleAttachedProcessIds( return } settled = true + if (value === null) { + runtime?.invalidate() + } clearTimeout(timeout) child.removeListener('message', onMessage) // Why: kill failures can emit asynchronously after timeout settlement; @@ -96,10 +116,17 @@ export function readWindowsConsoleAttachedProcessIds( consoleProcessIds.delete(helperPid) finish(consoleProcessIds) } - const timeout = setTimeout(() => { - child.kill() - finish(null) - }, deps.timeoutMs ?? CONPTY_PROCESS_LIST_TIMEOUT_MS) + const timeout = setTimeout( + () => { + try { + child.kill() + } catch { + // A failed kill leaves console ownership unknown. + } + finish(null) + }, + Math.max(1, deadline - Date.now()) + ) child.once('message', onMessage) child.once('error', onFailure) child.once('exit', onExit) diff --git a/src/main/providers/windows-console-runtime.test.ts b/src/main/providers/windows-console-runtime.test.ts new file mode 100644 index 000000000000..ccc93611d32a --- /dev/null +++ b/src/main/providers/windows-console-runtime.test.ts @@ -0,0 +1,89 @@ +import { afterEach, beforeEach, expect, it, vi } from 'vitest' +const mocks = vi.hoisted(() => ({ resolve: vi.fn(), version: vi.fn(), shutdown: vi.fn() })) +vi.mock('../daemon/daemon-bun-runtime', () => ({ resolveDesktopDaemonBunRuntime: mocks.resolve })) +vi.mock('../../shared/app-environment', () => ({ + getAppEnvironment: () => ({ + getAppPath: () => '/app', + getVersion: mocks.version, + onWillQuit: mocks.shutdown + }) +})) +import { + acquireWindowsConsoleRuntime, + disposeWindowsConsoleRuntime +} from './windows-console-runtime' +beforeEach(() => { + vi.stubGlobal('process', { ...process, versions: { ...process.versions, electron: '43.7.0' } }) + mocks.resolve.mockReset() + mocks.version.mockReturnValue('first') +}) +afterEach(() => { + disposeWindowsConsoleRuntime() + vi.unstubAllGlobals() + vi.useRealTimers() +}) +it('holds invalidated pins until concurrent pending launches release them', async () => { + const releaseLaunchPin = vi.fn() + mocks.resolve.mockResolvedValue({ + execPath: '/bun', + entryPath: '/daemon-entry.js', + releaseLaunchPin + }) + const first = await acquireWindowsConsoleRuntime(1000) + const second = await acquireWindowsConsoleRuntime(1000) + expect(mocks.resolve).toHaveBeenCalledTimes(1) + first?.invalidate() + first?.release() + await Promise.resolve() + expect(releaseLaunchPin).not.toHaveBeenCalled() + second?.release() + await Promise.resolve() + expect(releaseLaunchPin).toHaveBeenCalledTimes(1) + second?.release() + expect(releaseLaunchPin).toHaveBeenCalledTimes(1) +}) +it('releases a late resolver after every waiter has timed out', async () => { + vi.useFakeTimers() + const releaseLaunchPin = vi.fn() + let finish: + | ((runtime: { execPath: string; entryPath: string; releaseLaunchPin: () => void }) => void) + | undefined + mocks.resolve.mockReturnValue( + new Promise((resolve) => { + finish = resolve + }) + ) + const pending = acquireWindowsConsoleRuntime(10) + const failure = expect(pending).rejects.toThrow() + await vi.advanceTimersByTimeAsync(10) + await failure + finish?.({ execPath: '/bun', entryPath: '/daemon-entry.js', releaseLaunchPin }) + await vi.advanceTimersByTimeAsync(1) + expect(releaseLaunchPin).toHaveBeenCalledTimes(1) +}) +it('releases replaced and disposed caches without invalidating successors', async () => { + const oldRelease = vi.fn(), + newRelease = vi.fn() + mocks.resolve + .mockResolvedValueOnce({ + execPath: '/old', + entryPath: '/old/daemon-entry.js', + releaseLaunchPin: oldRelease + }) + .mockResolvedValueOnce({ + execPath: '/new', + entryPath: '/new/daemon-entry.js', + releaseLaunchPin: newRelease + }) + const old = await acquireWindowsConsoleRuntime(1000) + old?.release() + mocks.version.mockReturnValue('second') + const current = await acquireWindowsConsoleRuntime(1000) + current?.release() + expect(oldRelease).toHaveBeenCalledTimes(1) + old?.invalidate() + expect(newRelease).not.toHaveBeenCalled() + disposeWindowsConsoleRuntime() + await Promise.resolve() + expect(newRelease).toHaveBeenCalledTimes(1) +}) diff --git a/src/main/providers/windows-console-runtime.ts b/src/main/providers/windows-console-runtime.ts new file mode 100644 index 000000000000..4a52029acbad --- /dev/null +++ b/src/main/providers/windows-console-runtime.ts @@ -0,0 +1,98 @@ +import { dirname, join } from 'node:path' +import { getAppEnvironment } from '../../shared/app-environment' +import { waitForPromiseWithSignal } from '../../shared/abort-signal-reason' +import { resolveDesktopDaemonBunRuntime, type DaemonBunRuntime } from '../daemon/daemon-bun-runtime' +import { resolveWindowsBunPtyGateEntry } from '../daemon/pty-subprocess/windows-bun-pty-launch' + +type RuntimeCache = { + key: string + promise: Promise + users: number + invalidated: boolean + released: boolean +} +let cached: RuntimeCache | null = null +let watchingShutdown = false + +function releaseUnused(entry: RuntimeCache): void { + if (!entry.invalidated || entry.users !== 0 || entry.released) { + return + } + entry.released = true + void entry.promise.then((runtime) => runtime?.releaseLaunchPin?.()).catch(() => {}) +} +function invalidate(entry: RuntimeCache): void { + entry.invalidated = true + if (cached === entry) { + cached = null + } + releaseUnused(entry) +} +export function disposeWindowsConsoleRuntime(): void { + if (cached) { + invalidate(cached) + } +} + +/** A cached path owns its pin; pending spawns keep invalidated entries alive until used. */ +export async function acquireWindowsConsoleRuntime(timeoutMs: number) { + if (process.versions.bun) { + return { + execPath: process.execPath, + entryPath: resolveWindowsBunPtyGateEntry(), + release() {}, + invalidate() {} + } + } + const environment = process.versions.electron ? getAppEnvironment() : null + if (environment && !watchingShutdown) { + environment.onWillQuit(disposeWindowsConsoleRuntime) + watchingShutdown = true + } + const key = environment + ? `${environment.getAppPath()}\0${environment.getVersion()}` + : process.execPath + if (cached?.key !== key) { + disposeWindowsConsoleRuntime() + cached = { + key, + promise: resolveDesktopDaemonBunRuntime(), + users: 0, + invalidated: false, + released: false + } + } + const entry = cached + entry.users++ + let released = false + const release = (): void => { + if (released) { + return + } + released = true + entry.users-- + releaseUnused(entry) + } + const controller = new AbortController() + const timer = setTimeout(() => controller.abort(), timeoutMs) + try { + const runtime = await waitForPromiseWithSignal(entry.promise, controller.signal) + if (!runtime) { + invalidate(entry) + release() + return null + } + return { + execPath: runtime.execPath, + entryPath: join(dirname(runtime.entryPath), 'windows-bun-pty-gate-entry.js'), + release, + invalidate: () => invalidate(entry) + } + } catch (error) { + invalidate(entry) + release() + throw error + } finally { + clearTimeout(timer) + } +} diff --git a/src/main/providers/windows-pty-job-membership.test.ts b/src/main/providers/windows-pty-job-membership.test.ts index 8c8ab42122e9..46b34704415e 100644 --- a/src/main/providers/windows-pty-job-membership.test.ts +++ b/src/main/providers/windows-pty-job-membership.test.ts @@ -1,6 +1,6 @@ import type * as ChildProcess from 'node:child_process' import { describe, expect, it, vi } from 'vitest' -import type { IPty } from 'node-pty' +import type { TerminalProcess } from '../../shared/terminal-process' // Module-level, so it intercepts the module's own import binding. A spyOn of a // require()'d child_process does not: the first version of this test passed @@ -13,7 +13,20 @@ vi.mock('node:child_process', async (importOriginal) => ({ import { readWindowsPtyJobProcessIds } from './windows-pty-job-membership' -const pty = (pid = 100): IPty => ({ pid }) as unknown as IPty +const pty = (pid = 100): TerminalProcess => ({ + pid, + cols: 80, + rows: 24, + process: 'cmd.exe', + onData: () => ({ dispose() {} }), + onExit: () => ({ dispose() {} }), + write() {}, + resize() {}, + clear() {}, + kill() {}, + pause() {}, + resume() {} +}) describe('readWindowsPtyJobProcessIds', () => { it('never spawns a child process to answer', () => { diff --git a/src/main/providers/windows-pty-job-membership.ts b/src/main/providers/windows-pty-job-membership.ts index 7bb37323b39d..f42e64e74c83 100644 --- a/src/main/providers/windows-pty-job-membership.ts +++ b/src/main/providers/windows-pty-job-membership.ts @@ -1,4 +1,4 @@ -import type { IPty } from 'node-pty' +import type { TerminalProcess } from '../../shared/terminal-process' import { isPtyJobOwnershipAvailable, listPtyJobProcessIds, @@ -24,8 +24,8 @@ import { * that processes died. */ export function readWindowsPtyJobProcessIds( - proc: IPty, - listJobProcessIds: (proc: IPty) => readonly number[] | null = listPtyJobProcessIds + proc: TerminalProcess, + listJobProcessIds: (proc: TerminalProcess) => readonly number[] | null = listPtyJobProcessIds ): ReadonlySet | null { const pids = listJobProcessIds(proc) if (!pids) { diff --git a/src/main/providers/windows-shell-preflight-runtime.windows.test.ts b/src/main/providers/windows-shell-preflight-runtime.windows.test.ts index 91f228b6e856..b648345427b7 100644 --- a/src/main/providers/windows-shell-preflight-runtime.windows.test.ts +++ b/src/main/providers/windows-shell-preflight-runtime.windows.test.ts @@ -10,7 +10,8 @@ import { } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' -import * as pty from 'node-pty' +import { z } from 'zod' +import { runBundledBunFixture } from '../bundled-bun-test-execution' import { afterEach, describe, expect, it } from 'vitest' import { resolveGitBashPath } from '../git-bash' import { @@ -69,51 +70,18 @@ async function runPty(options: { input?: string timeoutMs?: number }): Promise { - const proc = pty.spawn(options.shellPath, options.shellArgs, { - name: 'xterm-256color', - cols: 100, - rows: 30, - cwd: options.cwd, - env: options.env - }) - let output = '' - proc.onData((data) => { - output += data - }) - let exited = false - const exitPromise = new Promise((resolve) => { - proc.onExit(({ exitCode }) => { - exited = true - resolve(exitCode) - }) - }) - let timeout: ReturnType | undefined - const timeoutPromise = new Promise((_resolve, reject) => { - timeout = setTimeout( - () => reject(new Error(`timed out waiting for Windows shell PTY:\n${output}`)), - options.timeoutMs ?? 10_000 + const result = z + .object({ output: z.string(), exitCode: z.number() }) + .parse( + await runBundledBunFixture( + join(__dirname, '../daemon/windows-shell-preflight-bun-fixture.ts'), + 'runWindowsShellPreflight', + options, + (options.timeoutMs ?? 10_000) + 5_000 + ) ) - }) - - try { - if (options.input) { - proc.write(options.input.replaceAll('\n', '\r')) - } - const exitCode = await Promise.race([exitPromise, timeoutPromise]) - expect(exitCode, output).toBe(0) - return output - } finally { - if (timeout) { - clearTimeout(timeout) - } - if (!exited) { - try { - proc.kill() - } catch { - // The PTY may have exited while cleanup was starting. - } - } - } + expect(result.exitCode, result.output).toBe(0) + return result.output } afterEach(() => { diff --git a/src/main/providers/working-directory-validation.test.ts b/src/main/providers/working-directory-validation.test.ts index 8c7d3867b11c..d23a430b794b 100644 --- a/src/main/providers/working-directory-validation.test.ts +++ b/src/main/providers/working-directory-validation.test.ts @@ -10,7 +10,7 @@ vi.mock('../wsl', async (importOriginal) => { return { ...actual, wslUncDirectoryExistsAsync: wslUncDirectoryExistsAsyncMock } }) -import { validateWorkingDirectoryAsync } from './local-pty-utils' +import { validateWorkingDirectoryAsync } from './pty-spawn-validation' import { _resetWorkingDirectoryValidationStateForTest } from './working-directory-validation' let tempDir: string diff --git a/src/main/providers/working-directory-validation.ts b/src/main/providers/working-directory-validation.ts index cf098cad2c33..e45d4b53cc77 100644 --- a/src/main/providers/working-directory-validation.ts +++ b/src/main/providers/working-directory-validation.ts @@ -1,5 +1,5 @@ // Validates a PTY working directory before spawn, so node-pty cannot fail with -// an opaque ENOENT. Split from local-pty-utils to keep that file under its line +// an opaque ENOENT. Split from pty-spawn-validation to keep that file under its line // cap; the async path carries the cancellation contract described below. import { existsSync, statSync } from 'node:fs' diff --git a/src/main/pty-descendant-termination-job-coverage.test.ts b/src/main/pty-descendant-termination-job-coverage.test.ts index 738d16ab16fb..b8040e203999 100644 --- a/src/main/pty-descendant-termination-job-coverage.test.ts +++ b/src/main/pty-descendant-termination-job-coverage.test.ts @@ -16,8 +16,8 @@ import { describe, expect, it } from 'vitest' */ const SRC_DIR = join(__dirname, '..') const CALL = 'killWithDescendantSweep(' -// Local immediate and recognized-agent shutdown share one guarded call site. -const EXPECTED_MINIMUM_SITES = 4 +// TerminalHost teardown and Session termination each own guarded sweep sites. +const EXPECTED_MINIMUM_SITES = 3 function collectTypeScriptFiles(dir: string): string[] { const found: string[] = [] diff --git a/src/main/pty/node-pty-master-fd-retirement.test.ts b/src/main/pty/node-pty-master-fd-retirement.test.ts deleted file mode 100644 index a0b537de1ca7..000000000000 --- a/src/main/pty/node-pty-master-fd-retirement.test.ts +++ /dev/null @@ -1,194 +0,0 @@ -import * as pty from 'node-pty' -import { describe, expect, it } from 'vitest' - -/** - * node-pty hands the master fd to libuv, which closes it on EIO/EOF, but upstream - * never invalidated `_fd`, and none of the three fd-addressed surfaces consulted - * anything: `resize()`, the `process` getter, and `CustomWriteStream`, which holds - * its own plain-number copy of the fd taken at spawn. Orca's patch retires all - * three in the same block that gives up the handle - * (config/patches/node-pty@1.1.0.patch). - * - * Scope: this narrows the window, it does not close it. libuv closes the fd - * synchronously inside `uv_close`, before the JS `'close'` that runs `_close()`, - * so callers still need their own liveness verdict for that tick — and a relay - * host installs node-pty from npm, where this patch is not applied at all. - */ - -const POSIX_SHELL = '/bin/sh' - -function spawnPty(command: string, cols = 80, rows = 24): pty.IPty { - return pty.spawn(POSIX_SHELL, ['-c', command], { - name: 'xterm-256color', - cols, - rows, - cwd: process.cwd(), - env: { ...process.env } - }) -} - -function masterFd(term: pty.IPty): number { - return (term as unknown as { fd: number }).fd -} - -type CustomWriteStream = { _fd: number; _writeQueue: unknown[]; write(data: string): void } - -/** - * `Terminal._close()` already shadows `terminal.write` with a no-op, so the stream - * itself is the surface that still reached the fd: a residual `_writeQueue` and an - * in-flight `fs.write` both re-enter it after the close. - */ -function writeStream(term: pty.IPty): CustomWriteStream { - return (term as unknown as { _writeStream: CustomWriteStream })._writeStream -} - -/** - * Run `command` to completion and let node-pty finish giving up the master. - * - * `destroy: false` exercises only the EIO/EOF read-error path, which reaches - * `_close()` without ever calling `destroy()` — the path the exit of a shell - * actually takes, and the one the write stream was previously never told about. - */ -async function retiredPty( - command = 'exit 0', - { destroy = true }: { destroy?: boolean } = {} -): Promise<{ term: pty.IPty; spawnFd: number }> { - const term = spawnPty(command) - const spawnFd = masterFd(term) - await new Promise((resolve) => { - term.onExit(() => resolve()) - }) - if (destroy) { - ;(term as unknown as { destroy?: () => void }).destroy?.() - } - await new Promise((resolve) => setTimeout(resolve, 400)) - return { term, spawnFd } -} - -// Windows never reaches this code: WindowsTerminal.resize goes through the conpty -// agent and reads no fd, so the sentinel is written and never consulted there. -const describeOnPosix = process.platform === 'win32' ? describe.skip : describe - -describeOnPosix('node-pty master fd retirement', () => { - it('invalidates the descriptor once it gives up the handle', async () => { - const { term, spawnFd } = await retiredPty() - - expect(spawnFd).toBeGreaterThanOrEqual(0) - expect(masterFd(term)).toBe(-1) - }, 15000) - - it('answers a resize past retirement without issuing the ioctl', async () => { - const { term } = await retiredPty() - - // Pre-patch this threw `ioctl(2) failed, EBADF` out of whatever called it. - expect(() => term.resize(200, 50)).not.toThrow() - // Geometry stays at the last size actually applied rather than claiming one - // that no descriptor ever received. - expect([term.cols, term.rows]).toEqual([80, 24]) - }, 15000) - - it('retires the write stream fd on _close(), not only on destroy()', async () => { - const { term } = await retiredPty('exit 0', { destroy: false }) - - // The stream copied the fd number at spawn, so `Terminal._fd = -1` alone - // leaves it addressing a descriptor the kernel may already have reissued. - expect(writeStream(term)._fd).toBe(-1) - - writeStream(term).write('x') - expect(writeStream(term)._writeQueue).toHaveLength(0) - }, 15000) - - it('names the spawn file rather than tcgetpgrp on a retired descriptor', async () => { - const { term } = await retiredPty() - - expect(term.process).toBe(POSIX_SHELL) - }, 15000) -}) - -// Linux frees the master synchronously enough that the very next pty is handed the -// same descriptor number every time, which makes the reuse hazard directly -// observable rather than a race to reproduce. -// -// Which is also the constraint on writing a case here: the kernel hands out the -// lowest free number, so a case that returns while its live pty is still open -// leaks that descriptor into the next case's premise as an off-by-one. Await the -// exit, never a fixed sleep. -const describeOnLinux = process.platform === 'linux' ? describe : describe.skip - -describeOnLinux('node-pty master fd reuse', () => { - it('cannot resize a live pty handed the retired descriptor number', async () => { - const { term: retired, spawnFd } = await retiredPty() - - const live = spawnPty('sleep 1; stty size') - let output = '' - live.onData((data) => { - output += data - }) - try { - // The premise of this test: the kernel really did reissue the number. If it - // stops holding, the assertion below would pass for the wrong reason. - expect(masterFd(live)).toBe(spawnFd) - - // Pre-patch this reached TIOCSWINSZ on `live`'s master and silently resized - // a terminal it has no relationship to — no error, nothing for a liveness - // probe of the retired pid to observe. - retired.resize(200, 50) - - await new Promise((resolve) => { - live.onExit(() => resolve()) - }) - expect(output.trim()).toBe('24 80') - } finally { - live.kill() - } - }, 15000) - - it('cannot write into a live pty handed the retired descriptor number', async () => { - const { term: retired, spawnFd } = await retiredPty('exit 0', { destroy: false }) - - const live = spawnPty('sleep 1') - let output = '' - live.onData((data) => { - output += data - }) - try { - expect(masterFd(live)).toBe(spawnFd) - - // Pre-patch this fs.write reached `live`'s master, and the line discipline - // echoed it straight back: a retired pane's bytes landing in an unrelated - // terminal. Nothing has to read them for the leak to be observable. - writeStream(retired).write('leak\r') - - // Await the exit rather than sleeping: a fixed wait leaves this descriptor - // open into the next case, whose `expect(masterFd(live)).toBe(spawnFd)` - // premise then sees the kernel hand out the lower number this pty was still - // holding. That is what broke `node 24 1/8` on Linux, where alone among the - // platforms these cases actually run. - await new Promise((resolve) => { - live.onExit(() => resolve()) - }) - expect(output).not.toContain('leak') - } finally { - live.kill() - } - }, 15000) - - it('does not name a live pty foreground process off the retired descriptor', async () => { - const { term: retired, spawnFd } = await retiredPty() - - // `exec` replaces the shell, so the foreground pgrp's cmdline is distinct - // from the file this pty was spawned with. - const live = spawnPty('exec sleep 5') - try { - expect(masterFd(live)).toBe(spawnFd) - // Let the shell finish exec'ing, or its own cmdline is still the fallback. - await new Promise((resolve) => setTimeout(resolve, 300)) - - // Pre-patch this read tcgetpgrp off `live`'s master and reported `sleep`, - // attributing an unrelated pane's process to a pty that had already exited. - expect(retired.process).toBe(POSIX_SHELL) - } finally { - live.kill() - } - }, 15000) -}) diff --git a/src/main/pty/node-pty-master-fd-retirement.ts b/src/main/pty/node-pty-master-fd-retirement.ts deleted file mode 100644 index 292578dc9072..000000000000 --- a/src/main/pty/node-pty-master-fd-retirement.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * node-pty hands the master fd to libuv, and Orca's patch sets it to -1 in the same block that - * gives up the handle (config/patches/node-pty@1.1.0.patch). Past that point every fd-addressed - * answer is a stand-in rather than an error: the `process` getter names the spawn file instead of - * whatever `tcgetpgrp` would have reported, so callers that need a real observation have to ask - * about the descriptor first. Windows exposes no master fd, so it never reads as retired; an - * unpatched (relay-installed) node-pty never retires the number at all. - */ -export function isRetiredPtyMaster(proc: unknown): boolean { - if (typeof proc !== 'object' || proc === null || !('fd' in proc)) { - return false - } - const fd: unknown = proc.fd - return typeof fd === 'number' && fd < 0 -} diff --git a/src/main/pty/node-pty-pts-name.ts b/src/main/pty/node-pty-pts-name.ts deleted file mode 100644 index c3c4d441cebd..000000000000 --- a/src/main/pty/node-pty-pts-name.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * node-pty's unix terminal exposes the slave device path (`/dev/ttys003`, - * `/dev/pts/3`) but does not declare it on IPty, and the Windows terminal has no - * equivalent. Read it defensively so callers get `undefined` rather than a throw - * on a platform or version that lacks it. - */ -export function readPtsName(proc: unknown): string | undefined { - const value = (proc as { ptsName?: unknown } | null | undefined)?.ptsName - return typeof value === 'string' && value.length > 0 ? value : undefined -} diff --git a/src/main/pty/node-pty-self-exit-pseudoconsole-close.test.ts b/src/main/pty/node-pty-self-exit-pseudoconsole-close.test.ts deleted file mode 100644 index 6d8b6411ba56..000000000000 --- a/src/main/pty/node-pty-self-exit-pseudoconsole-close.test.ts +++ /dev/null @@ -1,180 +0,0 @@ -import { readFileSync } from 'node:fs' -import { join } from 'node:path' -import { describe, expect, it } from 'vitest' - -/** - * A shell that exits by itself must still close its pseudoconsole. - * - * `ClosePseudoConsole` is the only thing that reaps a ConPTY's console host — - * Orca's own job-ownership patch says so, because `CreatePseudoConsole` spawns - * that host before the per-pty job exists and it is therefore not a job member. - * Upstream node-pty calls it from exactly one place, `PtyKill`, which begins by - * looking the baton up by id — and the exit watcher in `SetupExitCallback` - * erased the baton the moment the shell died. So on the self-exit path (typing - * `exit`, which is how panes usually close) that lookup missed, `PtyKill` did - * nothing at all, and the pseudoconsole was never closed. - * - * There is a SECOND, independent defect on the same path: the `useConptyDll` - * branch of `WindowsPtyAgent.kill()` disposed the conout worker only from an - * `_outSocket.on('data')` handler, and no more data arrives once the shell has - * gone — so that worker leaked too. The non-DLL branch beside it already - * disposed unconditionally. The desktop always sets `useConptyDll`, so it hit - * both; the relay sets neither and hit only the first. - * - * Measured on Windows 11 / awin, 20 cycles, handles bucketed by NT object type, - * totals before -> after: - * - * self-exit, relay spawn 225 -> 285 becomes 219 -> 219 FLAT - * self-exit, desktop spawn 239 -> 439 becomes 222 -> 222 FLAT - * explicit kill, relay spawn 225 -> 285 becomes 219 -> 219 FLAT - * explicit kill, desktop spawn 235 -> 395 becomes 219 -> 219 FLAT - * - * Neither fix alone is enough on the desktop: the pseudoconsole close is worth - * +1 Process +1 File per terminal, the dispose +2 Thread +4 File. - * - * WHY THIS IS A PATCH-CONTENT PIN AND NOT A BEHAVIOURAL TEST: the defect is - * only observable as a per-NT-type handle count, which needs - * `NtQuerySystemInformation(SystemExtendedHandleInformation)`. Nothing in the - * repo can read that, and the cheaper Windows-observable proxies do not - * discriminate — the console host process is reaped either way (the leak is a - * handle to an already-exited object, not an orphaned process), and the - * `\\.\pipe\conpty-*` entries disappear either way. Both were measured and - * rejected as assertions rather than assumed. So this pins the mechanism - * instead, which is the real risk: a future resync of the vendored patch - * silently dropping the hunk. - */ - -const PATCH = readFileSync(join(__dirname, '../../../config/patches/node-pty@1.1.0.patch'), 'utf8') - -/** - * Just the `PtyKill` hunk. Several markers below also occur in the `PtyConnect` - * hunk above it, and a bare `indexOf` on the whole patch silently matched the - * wrong one — an assertion that then held regardless of what `PtyKill` did. - */ -const ptyKillHunk = (() => { - // Anchored on the hunk header's function context rather than its line - // numbers, which shift whenever anything above it in the patch changes. - const header = /^@@ .* @@ static Napi::Value PtyKill\(.*$/m.exec(PATCH) - if (!header) { - throw new Error('no PtyKill hunk in config/patches/node-pty@1.1.0.patch') - } - const from = header.index - const next = PATCH.indexOf('\n@@ ', from + 1) - return PATCH.slice(from, next === -1 ? undefined : next) -})() - -/** - * `indexOf` that throws instead of returning -1. A missing marker must fail the - * assertion that depends on it, not quietly make a slice or comparison vacuous. - */ -function indexIn(haystack: string, marker: string): number { - const at = haystack.indexOf(marker) - if (at === -1) { - throw new Error(`marker not found in the PtyKill hunk: ${marker}`) - } - return at -} - -describe('node-pty patch: pseudoconsole close on the self-exit path', () => { - it('does not let the exit watcher free the baton while the close is still owed', () => { - // Pinned as one block: the erase must stay INSIDE the consoleClosed guard. - // Upstream ran it unconditionally, which is the line that caused the leak, - // and a resync that re-flattens this is the failure mode worth catching. - expect(PATCH).toContain( - [ - '+ baton->shellExited = true;', - '+ if (baton->consoleClosed) {', - '+ const bool removed = remove_pty_baton_locked(baton->id);', - '+ assert(removed);', - '+ (void)removed;', - '+ }' - ].join('\n') - ) - }) - - it('closes the pseudoconsole from PtyKill even after the shell has exited', () => { - // hpc is copied out under the lock, so the close survives the baton's removal. - expect(PATCH).toContain('+ hpc = handle->hpc;') - expect(PATCH).toContain('+ pfnClosePseudoConsole(hpc);') - }) - - it('resolves the ConPTY DLL before it claims the close', () => { - // LoadConptyDll throws when conpty.dll is missing. Throwing after - // consoleClosed was set would strand the pseudoconsole for good: the retry - // finds the work claimed and does nothing. - // - // Anchored inside PtyKill, not by a bare indexOf: the identical line also - // appears in the PtyConnect hunk, earlier in the file, and matching that one - // made this assertion pass no matter where PtyKill resolved the DLL. - const dllResolve = indexIn( - ptyKillHunk, - '+ HANDLE hLibrary = LoadConptyDll(info, useConptyDll);' - ) - const claim = indexIn(ptyKillHunk, '+ handle->consoleClosed = true;') - expect(dllResolve).toBeLessThan(claim) - }) - - it('reaches hShell only under the null check the watcher can trip', () => { - // Pinned as one block. The watcher nulls hShell on exit, and upstream - // dereferenced it unconditionally; every remaining use — the duplication and - // the failure fallback below it — must stay inside this guard. - const start = indexIn(ptyKillHunk, '+ if (useConptyDll && handle->hShell != nullptr) {') - const end = indexIn(ptyKillHunk, '+ if (handle->shellExited) {') - const guarded = ptyKillHunk.slice(start, end) - expect(guarded).toContain('DuplicateHandle(GetCurrentProcess(), handle->hShell') - expect(guarded).toContain('TerminateProcess(handle->hShell, 1);') - // No ADDED line outside that guard may terminate through hShell. Removed - // (`-`) lines still carry upstream's unguarded call, which is the point. - const strayAdds = PATCH.replace(guarded, '') - .split('\n') - .filter((line) => line.startsWith('+') && line.includes('TerminateProcess(handle->hShell')) - expect(strayAdds).toEqual([]) - }) - - it('frees the baton from PtyKill when the shell has already exited', () => { - // The other half of the two-sided handshake. Without it a self-exit followed - // by kill() — the ordinary pane close — leaks one baton and one entry in the - // vector get_pty_baton scans linearly, forever. - expect(ptyKillHunk).toContain( - [ - '+ if (handle->shellExited) {', - '+ const bool removed = remove_pty_baton_locked(id);', - '+ assert(removed);', - '+ (void)removed;' - ].join('\n') - ) - }) - - it('still kills the shell when DuplicateHandle fails', () => { - // A null hShellDup is indistinguishable from the self-exit case, so a - // swallowed failure would leave the shell running after its pane closed — - // a worse outcome than the leak this patch exists to fix. - expect(PATCH).toContain( - [ - '+ hShellDup = nullptr;', - '+ TerminateProcess(handle->hShell, 1);', - '+ }' - ].join('\n') - ) - }) - - it('keeps the close idempotent so a second kill cannot double-close', () => { - expect(PATCH).toContain('+ if (handle != nullptr && !handle->consoleClosed) {') - expect(PATCH).toContain('+ handle->consoleClosed = true;') - }) -}) - -describe('node-pty patch: conout worker disposal on the self-exit path', () => { - // The desktop's larger half: 8 of its 10 leaked handles per terminal. - it('disposes the conout worker unconditionally in the useConptyDll branch', () => { - expect(PATCH).toContain('+ this._conoutSocketWorker.dispose();') - // The data handler is what never fired once the shell had gone. - expect(PATCH).toContain("- this._outSocket.on('data', function () {") - expect(PATCH).toContain('- _this._conoutSocketWorker.dispose();') - }) - - it('applies the same change to the TypeScript source the patch also carries', () => { - expect(PATCH).toContain('+ this._conoutSocketWorker.dispose();') - expect(PATCH).toContain("- this._outSocket.on('data', () => {") - }) -}) diff --git a/src/main/pty/omp-shell-wrapper-bun-fixture.ts b/src/main/pty/omp-shell-wrapper-bun-fixture.ts new file mode 100644 index 000000000000..90b069d7c497 --- /dev/null +++ b/src/main/pty/omp-shell-wrapper-bun-fixture.ts @@ -0,0 +1,63 @@ +import { writeFileSync } from 'node:fs' +import { join } from 'node:path' +import { spawnBunPty } from '../daemon/pty-subprocess/bun-pty-process' + +type PosixShell = 'bash' | 'zsh' + +export async function runInteractivePosixPty(args: { + rcfileContent: string + env: Record + input: string + cwd: string + shell?: PosixShell +}): Promise { + const rcfile = join(args.cwd, 'rcfile') + writeFileSync(rcfile, args.rcfileContent) + const shell = args.shell ?? 'bash' + const shellArgs = shell === 'bash' ? ['--noprofile', '--rcfile', rcfile, '-i'] : ['-f', '-i'] + + const proc = spawnBunPty({ + file: shell, + args: shellArgs, + cols: 100, + rows: 30, + cwd: args.cwd, + env: { ...args.env, ORCA_TEST_RCFILE: rcfile } + }) + + let output = '' + proc.onData((data) => { + output += data + }) + + const exitPromise = new Promise<{ exitCode: number }>((resolve) => { + proc.onExit(({ exitCode }) => resolve({ exitCode })) + }) + + let timeout: ReturnType | null = null + const timeoutPromise = new Promise((_resolve, reject) => { + timeout = setTimeout( + () => reject(new Error(`timed out waiting for ${shell} PTY output:\n${output}`)), + 5000 + ) + }) + + try { + const input = shell === 'zsh' ? `source "$ORCA_TEST_RCFILE"\n${args.input}` : args.input + proc.write(input.replace(/\n/g, '\r')) + const { exitCode } = await Promise.race([exitPromise, timeoutPromise]) + if (exitCode !== 0) { + throw new Error(`Shell exited ${exitCode}: ${output}`) + } + return output + } finally { + if (timeout !== null) { + clearTimeout(timeout) + } + try { + proc.kill() + } catch { + // The process may already have exited normally before cleanup runs. + } + } +} diff --git a/src/main/pty/omp-shell-wrapper.node-pty.test.ts b/src/main/pty/omp-shell-wrapper.bun.test.ts similarity index 90% rename from src/main/pty/omp-shell-wrapper.node-pty.test.ts rename to src/main/pty/omp-shell-wrapper.bun.test.ts index 15bc98bdb3be..9163576ebc5e 100644 --- a/src/main/pty/omp-shell-wrapper.node-pty.test.ts +++ b/src/main/pty/omp-shell-wrapper.bun.test.ts @@ -12,7 +12,7 @@ import { } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' -import * as pty from 'node-pty' +import { runBundledBunFixture } from '../bundled-bun-test-execution' import { afterEach, describe, expect, it } from 'vitest' import { getPosixOmpShellWrapper } from './omp-shell-wrapper' @@ -27,7 +27,7 @@ type PosixShell = 'bash' | 'zsh' const tempDirs: string[] = [] function makeTempDir(): string { - const dir = mkdtempSync(join(tmpdir(), 'orca-omp-node-pty-')) + const dir = mkdtempSync(join(tmpdir(), 'orca-omp-bun-')) tempDirs.push(dir) return dir } @@ -66,55 +66,19 @@ async function runInteractivePosixPty(args: { cwd: string shell?: PosixShell }): Promise { - const rcfile = join(args.cwd, 'rcfile') - writeFileSync(rcfile, args.rcfileContent) - const shell = args.shell ?? 'bash' - const shellArgs = shell === 'bash' ? ['--noprofile', '--rcfile', rcfile, '-i'] : ['-f', '-i'] - - const proc = pty.spawn(shell, shellArgs, { - name: 'xterm-256color', - cols: 100, - rows: 30, - cwd: args.cwd, - env: { ...args.env, ORCA_TEST_RCFILE: rcfile } - }) - - let output = '' - proc.onData((data) => { - output += data - }) - - const exitPromise = new Promise<{ exitCode: number }>((resolve) => { - proc.onExit(({ exitCode }) => resolve({ exitCode })) - }) - - let timeout: ReturnType | null = null - const timeoutPromise = new Promise((_resolve, reject) => { - timeout = setTimeout( - () => reject(new Error(`timed out waiting for ${shell} PTY output:\n${output}`)), - 5000 - ) - }) - - try { - const input = shell === 'zsh' ? `source "$ORCA_TEST_RCFILE"\n${args.input}` : args.input - proc.write(input.replace(/\n/g, '\r')) - const { exitCode } = await Promise.race([exitPromise, timeoutPromise]) - expect(exitCode).toBe(0) - return output - } finally { - if (timeout !== null) { - clearTimeout(timeout) - } - try { - proc.kill() - } catch { - // The process may already have exited normally before cleanup runs. - } + const output = await runBundledBunFixture( + join(import.meta.dirname, 'omp-shell-wrapper-bun-fixture.ts'), + 'runInteractivePosixPty', + args, + 10_000 + ) + if (typeof output !== 'string') { + throw new Error('Invalid shell fixture output') } + return output } -describePosix('OMP shell wrapper node-pty reproduction', () => { +describePosix('OMP shell wrapper bundled Bun reproduction', () => { afterEach(() => { for (const dir of tempDirs.splice(0)) { rmSync(dir, { recursive: true, force: true }) diff --git a/src/main/pty/posix-pty-process-groups-fixture.ts b/src/main/pty/posix-pty-process-groups-fixture.ts new file mode 100644 index 000000000000..271d599b1b51 --- /dev/null +++ b/src/main/pty/posix-pty-process-groups-fixture.ts @@ -0,0 +1,117 @@ +import { runProcessSync } from '../../shared/child-process/run-process' +import { spawnBunPty } from '../daemon/pty-subprocess/bun-pty-process' +import { forceKillPosixPtyProcessGroups } from './posix-pty-process-groups' + +type TaggedProcess = { pid: number; pgid: number } + +function readPs(args: string[]): string { + const result = runProcessSync({ program: 'ps', args, timeoutMs: 2_000 }) + if (result.code !== 0 || result.timedOut || result.outputTruncated) { + throw new Error( + `PTY fixture process table is unavailable (exit=${result.code}, timeout=${result.timedOut}, truncated=${result.outputTruncated}): ${result.stderr}` + ) + } + return result.stdout +} + +function findTaggedProcesses(token: string): TaggedProcess[] { + // BusyBox uses nonempty headers as exact widths; retain the default command width. + const output = readPs(['-e', '-o', 'pid=PROCESS_ID,pgid=PROCESS_GID,stat=PROCESS_STATE,args=']) + const matches: TaggedProcess[] = [] + for (const line of output.split(/\r?\n/)) { + if (!line.includes(token)) { + continue + } + const match = /^\s*(\d+)\s+(\d+)\s+(\S+)\s+/.exec(line) + if (match && !match[3].includes('Z')) { + matches.push({ pid: Number(match[1]), pgid: Number(match[2]) }) + } + } + return matches +} + +async function waitFor( + predicate: () => boolean, + timeoutMs = 5_000, + intervalMs = 25 +): Promise { + const deadline = Date.now() + timeoutMs + while (!predicate()) { + if (Date.now() >= deadline) { + throw new Error('Timed out waiting for PTY process state') + } + await new Promise((resolve) => setTimeout(resolve, intervalMs)) + } +} + +function cleanupTaggedProcesses(token: string): void { + for (const pgid of new Set(findTaggedProcesses(token).map((process) => process.pgid))) { + try { + process.kill(-pgid, 'SIGKILL') + } catch { + // The tagged test group may already be gone. + } + } +} + +function readProcessGroup(pid: number): number { + const rows = readPs(['-e', '-o', 'pid=PROCESS_ID,pgid=PROCESS_GID']) + for (const row of rows.split(/\r?\n/)) { + const match = /^\s*(\d+)\s+(\d+)\s*$/.exec(row) + if (match && Number(match[1]) === pid) { + return Number(match[2]) + } + } + throw new Error('PTY fixture root process group is unavailable') +} + +export async function reapPosixForegroundJob(): Promise<{ + separateForegroundGroup: boolean + leaderExited: boolean + remainingTaggedProcesses: number +}> { + const token = `ORCA_PTY_GROUP_TEST_${process.pid}_${Date.now()}` + const proc = spawnBunPty({ + file: '/bin/sh', + args: [], + cols: 80, + rows: 24, + cwd: process.cwd(), + env: Object.fromEntries( + Object.entries(process.env).filter( + (entry): entry is [string, string] => entry[1] !== undefined + ) + ) + }) + let leaderExited = false + const exitSubscription = proc.onExit(() => { + leaderExited = true + }) + const dataSubscription = proc.onData(() => {}) + try { + await proc.waitForSpawn?.() + proc.write(`/bin/sh -lc 'trap "" HUP TERM; while :; do sleep 60; done' ${token}\n`) + await waitFor(() => findTaggedProcesses(token).length > 0) + const rootProcessGroup = readProcessGroup(proc.pid) + const separateForegroundGroup = findTaggedProcesses(token).some( + (process) => process.pgid !== rootProcessGroup + ) + forceKillPosixPtyProcessGroups(proc.pid, () => proc.kill('SIGKILL')) + await waitFor(() => leaderExited) + await waitFor(() => findTaggedProcesses(token).length === 0) + return { + separateForegroundGroup, + leaderExited, + remainingTaggedProcesses: findTaggedProcesses(token).length + } + } finally { + exitSubscription.dispose() + dataSubscription.dispose() + cleanupTaggedProcesses(token) + try { + proc.kill('SIGKILL') + } catch { + // The PTY leader may already have exited. + } + } +} diff --git a/src/main/pty/posix-pty-process-groups.integration.test.ts b/src/main/pty/posix-pty-process-groups.integration.test.ts index f8b191f98566..4bf72b6322aa 100644 --- a/src/main/pty/posix-pty-process-groups.integration.test.ts +++ b/src/main/pty/posix-pty-process-groups.integration.test.ts @@ -1,94 +1,23 @@ -import { execFileSync } from 'node:child_process' -import * as pty from 'node-pty' +import { join } from 'node:path' import { expect, it } from 'vitest' -import { forceKillPosixPtyProcessGroups } from './posix-pty-process-groups' - -type TaggedProcess = { pid: number; pgid: number } +import { runBundledBunFixture } from '../bundled-bun-test-execution' const itOnPosix = process.platform === 'win32' ? it.skip : it -function findTaggedProcesses(token: string): TaggedProcess[] { - const output = execFileSync('ps', ['-axo', 'pid=,pgid=,state=,command='], { - encoding: 'utf8' - }) - const matches: TaggedProcess[] = [] - for (const line of output.split(/\r?\n/)) { - if (!line.includes(token)) { - continue - } - const match = /^\s*(\d+)\s+(\d+)\s+(\S+)\s+/.exec(line) - if (match && !match[3].includes('Z')) { - matches.push({ pid: Number(match[1]), pgid: Number(match[2]) }) - } - } - return matches -} - -async function waitFor( - predicate: () => boolean, - timeoutMs = 5_000, - intervalMs = 25 -): Promise { - const deadline = Date.now() + timeoutMs - while (!predicate()) { - if (Date.now() >= deadline) { - throw new Error('Timed out waiting for PTY process state') - } - await new Promise((resolve) => setTimeout(resolve, intervalMs)) - } -} - -function cleanupTaggedProcesses(token: string): void { - for (const pgid of new Set(findTaggedProcesses(token).map((process) => process.pgid))) { - try { - process.kill(-pgid, 'SIGKILL') - } catch { - // The tagged test group may already be gone. - } - } -} - -function readProcessGroup(pid: number): number { - return Number(execFileSync('ps', ['-p', String(pid), '-o', 'pgid='], { encoding: 'utf8' }).trim()) -} - itOnPosix( 'reaps a foreground job that ignores terminal shutdown signals', async () => { - const token = `ORCA_PTY_GROUP_TEST_${process.pid}_${Date.now()}` - const proc = pty.spawn('/bin/sh', [], { - name: 'xterm-256color', - cols: 80, - rows: 24, - cwd: process.cwd(), - env: process.env as Record + const result = await runBundledBunFixture( + join(__dirname, 'posix-pty-process-groups-fixture.ts'), + 'reapPosixForegroundJob', + null, + 15_000 + ) + expect(result).toEqual({ + separateForegroundGroup: true, + leaderExited: true, + remainingTaggedProcesses: 0 }) - const exited = new Promise((resolve) => proc.onExit(() => resolve())) - - try { - proc.write(`/bin/sh -lc 'trap "" HUP TERM; while :; do sleep 60; done' ${token}\n`) - await waitFor(() => findTaggedProcesses(token).length > 0) - - const tagged = findTaggedProcesses(token) - const rootProcessGroup = readProcessGroup(proc.pid) - expect(tagged.some((process) => process.pgid !== rootProcessGroup)).toBe(true) - - forceKillPosixPtyProcessGroups(proc.pid, () => proc.kill('SIGKILL')) - await Promise.race([ - exited, - new Promise((_, reject) => - setTimeout(() => reject(new Error('PTY leader did not exit')), 5_000) - ) - ]) - await waitFor(() => findTaggedProcesses(token).length === 0) - } finally { - cleanupTaggedProcesses(token) - try { - proc.kill('SIGKILL') - } catch { - // node-pty may already have reaped and disposed the leader. - } - } }, - 10_000 + 20_000 ) diff --git a/src/main/pty/terminal-slave-device.ts b/src/main/pty/terminal-slave-device.ts new file mode 100644 index 000000000000..339dbf69e8eb --- /dev/null +++ b/src/main/pty/terminal-slave-device.ts @@ -0,0 +1,7 @@ +/** Backend-specific slave-device paths are optional on POSIX and absent on Windows. */ +export function readPtsName(proc: unknown): string | undefined { + if (typeof proc !== 'object' || proc === null || !('ptsName' in proc)) { + return undefined + } + return typeof proc.ptsName === 'string' && proc.ptsName.length > 0 ? proc.ptsName : undefined +} diff --git a/src/main/rate-limits/claude-pty.test.ts b/src/main/rate-limits/claude-pty.test.ts index f2fd976821b6..d5ace8b54ef1 100644 --- a/src/main/rate-limits/claude-pty.test.ts +++ b/src/main/rate-limits/claude-pty.test.ts @@ -9,8 +9,8 @@ vi.mock('../codex-cli/command', () => ({ resolveClaudeCommand: resolveClaudeCommandMock })) -vi.mock('node-pty', () => ({ - spawn: spawnMock +vi.mock('./hidden-daemon-pty', () => ({ + spawnHiddenDaemonPty: spawnMock })) import { fetchViaPty } from './claude-pty' @@ -22,6 +22,7 @@ function makeDisposable() { type MockTerm = { onData: ReturnType + onError: ReturnType onExit: ReturnType write: ReturnType kill: ReturnType @@ -38,6 +39,7 @@ function makeMockTerm(): MockTerm & { dataHandler = handler return makeDisposable() }), + onError: vi.fn(() => ({ dispose: vi.fn() })), onExit: vi.fn((handler: () => void) => { exitHandler = handler return makeDisposable() @@ -56,6 +58,24 @@ describe('fetchViaPty', () => { resolveClaudeCommandMock.mockReturnValue('claude') }) + it('settles a transport error immediately and releases the hidden probe', async () => { + const term = makeMockTerm() + spawnMock.mockReturnValue(term) + const result = fetchViaPty() + await vi.advanceTimersByTimeAsync(0) + const callback = term.onError.mock.calls[0][0] + callback(new Error('Terminal service connection lost during usage probe')) + await expect(result).resolves.toMatchObject({ + status: 'error', + session: null, + weekly: null, + error: 'Terminal service connection lost during usage probe' + }) + expect(term.kill).toHaveBeenCalledOnce() + expect(getActiveHiddenRateLimitPtyCount()).toBe(0) + expect(vi.getTimerCount()).toBe(0) + }) + it('disposes node-pty listeners before killing the hidden PTY on timeout', async () => { const onDataDisposable = makeDisposable() const onExitDisposable = makeDisposable() @@ -63,6 +83,7 @@ describe('fetchViaPty', () => { spawnMock.mockReturnValue({ onData: vi.fn(() => onDataDisposable), + onError: vi.fn(() => ({ dispose: vi.fn() })), onExit: vi.fn(() => onExitDisposable), write: vi.fn(), kill: killMock diff --git a/src/main/rate-limits/claude-pty.ts b/src/main/rate-limits/claude-pty.ts index cbaedbd17f9d..7cda9ddab2e8 100644 --- a/src/main/rate-limits/claude-pty.ts +++ b/src/main/rate-limits/claude-pty.ts @@ -1,3 +1,4 @@ +import { spawnHiddenDaemonPty } from './hidden-daemon-pty' import type { ProviderRateLimits } from '../../shared/rate-limit-types' import { buildConfiguredProxyEnv, type NetworkProxySettings } from '../../shared/network-proxy' import { resolveClaudeCommand } from '../codex-cli/command' @@ -48,9 +49,77 @@ export async function fetchViaPty(options?: { if (options?.signal?.aborted) { return abortedClaudeUsageResult() } - const pty = await import('node-pty') - if (options?.signal?.aborted) { - return abortedClaudeUsageResult() + + const claudeCommand = resolveClaudeCommand() + + // Windows CLI shims require their existing cmd.exe wrapper. + const spawnEnv = applyClaudeEnvPatch( + { ...process.env, TERM: 'xterm-256color' } as Record, + options?.authPreparation?.envPatch ?? {}, + { stripAuthEnv: options?.authPreparation?.stripAuthEnv ?? false } + ) + // Why: this hidden usage PTY spawns `claude` directly, not the user's shell + // wrapper, so without the configured proxy it would reach api.anthropic.com + // from the app's own IP — bypassing the proxy the user set for Claude and + // risking rate-limit/geo signals on the account. Falls back to {} when unset. + const proxyEnv = buildConfiguredProxyEnv(options?.networkProxySettings) + Object.assign(spawnEnv, proxyEnv) + const authPreparation = options?.authPreparation + const wslConfig = + authPreparation?.runtime === 'wsl' && + authPreparation.wslDistro && + authPreparation.wslLinuxConfigDir + ? { + distro: authPreparation.wslDistro, + linuxConfigDir: authPreparation.wslLinuxConfigDir + } + : null + const spawnFile = wslConfig ? 'wsl.exe' : process.platform === 'win32' ? 'cmd.exe' : claudeCommand + const spawnArgs = wslConfig + ? [ + '-d', + wslConfig.distro, + '--exec', + 'bash', + '-lc', + // Why: Windows-side env does not cross into the distro without WSLENV, + // so export the configured proxy inside the command for the inner claude. + [ + // Why: hidden usage probes must not inherit a root-like WSL cwd; + // keep Claude discovery bounded to a tiny temp directory. + ...getHiddenRateLimitWslCwdSetupCommands(), + `export CLAUDE_CONFIG_DIR=${quoteHiddenRateLimitShellValue(wslConfig.linuxConfigDir)}`, + ...Object.entries(proxyEnv).map( + ([key, value]) => `export ${key}=${quoteHiddenRateLimitShellValue(value)}` + ), + 'exec claude' + ].join(' && ') + ] + : process.platform === 'win32' + ? ['/c', `"${claudeCommand}"`] + : [] + + let term: Awaited> + try { + term = await spawnHiddenDaemonPty( + spawnFile, + spawnArgs, + { + name: 'xterm-256color', + cols: 120, + rows: 40, + // Why: hidden usage PTYs must not inherit the process cwd (e.g. / or a + // drive root), which can trigger unbounded file discovery. + cwd: resolveHiddenRateLimitPtyCwd(), + env: withCliRuntimeOnPath(claudeCommand, spawnEnv) + }, + options?.signal + ) + } catch (error) { + if (options?.signal?.aborted) { + return abortedClaudeUsageResult() + } + throw error } return new Promise((resolve) => { @@ -63,67 +132,6 @@ export async function fetchViaPty(options?: { let stopSettleTimer: ReturnType | null = null let claude21UsageSettleTimer: ReturnType | null = null - const claudeCommand = resolveClaudeCommand() - - // Why: node-pty cannot spawn .cmd/.bat batch scripts directly on Windows — - // those need cmd.exe as an interpreter. Always route through cmd.exe on win32 - // and ensure the command path is properly quoted if it contains spaces. - const isWin32 = process.platform === 'win32' - const spawnEnv = applyClaudeEnvPatch( - { ...process.env, TERM: 'xterm-256color' } as Record, - options?.authPreparation?.envPatch ?? {}, - { stripAuthEnv: options?.authPreparation?.stripAuthEnv ?? false } - ) - // Why: this hidden usage PTY spawns `claude` directly, not the user's shell - // wrapper, so without the configured proxy it would reach api.anthropic.com - // from the app's own IP — bypassing the proxy the user set for Claude and - // risking rate-limit/geo signals on the account. Falls back to {} when unset. - const proxyEnv = buildConfiguredProxyEnv(options?.networkProxySettings) - Object.assign(spawnEnv, proxyEnv) - const authPreparation = options?.authPreparation - const wslConfig = - authPreparation?.runtime === 'wsl' && - authPreparation.wslDistro && - authPreparation.wslLinuxConfigDir - ? { - distro: authPreparation.wslDistro, - linuxConfigDir: authPreparation.wslLinuxConfigDir - } - : null - const spawnFile = wslConfig ? 'wsl.exe' : isWin32 ? 'cmd.exe' : claudeCommand - const spawnArgs = wslConfig - ? [ - '-d', - wslConfig.distro, - '--exec', - 'bash', - '-lc', - // Why: Windows-side env does not cross into the distro without WSLENV, - // so export the configured proxy inside the command for the inner claude. - [ - // Why: hidden usage probes must not inherit a root-like WSL cwd; - // keep Claude discovery bounded to a tiny temp directory. - ...getHiddenRateLimitWslCwdSetupCommands(), - `export CLAUDE_CONFIG_DIR=${quoteHiddenRateLimitShellValue(wslConfig.linuxConfigDir)}`, - ...Object.entries(proxyEnv).map( - ([key, value]) => `export ${key}=${quoteHiddenRateLimitShellValue(value)}` - ), - 'exec claude' - ].join(' && ') - ] - : isWin32 - ? ['/c', `"${claudeCommand}"`] - : [] - - const term = pty.spawn(spawnFile, spawnArgs, { - name: 'xterm-256color', - cols: 120, - rows: 40, - // Why: hidden usage PTYs must not inherit the process cwd (e.g. / or a - // drive root), which can trigger unbounded file discovery. - cwd: resolveHiddenRateLimitPtyCwd(), - env: withCliRuntimeOnPath(claudeCommand, spawnEnv) - }) const termDisposables: { dispose: () => void }[] = [registerHiddenRateLimitPty(term)] let enterInterval: ReturnType | null = null let timeout: ReturnType | null = null @@ -318,12 +326,10 @@ export async function fetchViaPty(options?: { } } }) - if (onDataDisposable) { - termDisposables.push(onDataDisposable) - } + termDisposables.push(onDataDisposable) - const onExitDisposable = term.onExit(() => { - cleanupHiddenRateLimitPty(term, termDisposables, { kill: false }) + const finish = (failure?: Error): void => { + cleanupHiddenRateLimitPty(term, termDisposables, { kill: Boolean(failure) }) clearFollowupTimers() if (!resolved) { resolved = true @@ -332,7 +338,9 @@ export async function fetchViaPty(options?: { timeout = null } const clean = stripTerminalControlSequences(output) - const { session, weekly, fableWeekly } = parseClaudePtyUsage(clean) + const { session, weekly, fableWeekly } = failure + ? { session: null, weekly: null, fableWeekly: null } + : parseClaudePtyUsage(clean) resolve({ provider: 'claude', session, @@ -340,15 +348,15 @@ export async function fetchViaPty(options?: { fableWeekly, updatedAt: Date.now(), error: - session || weekly || fableWeekly + failure?.message ?? + (session || weekly || fableWeekly ? null - : withMacTailscaleDnsHint('CLI exited before /usage rendered', clean), + : withMacTailscaleDnsHint('CLI exited before /usage rendered', clean)), status: session || weekly || fableWeekly ? 'ok' : 'error' }) } - }) - if (onExitDisposable) { - termDisposables.push(onExitDisposable) } + termDisposables.push(term.onError(finish)) + termDisposables.push(term.onExit(() => finish())) }) } diff --git a/src/main/rate-limits/codex-fetcher-auth-errors.test.ts b/src/main/rate-limits/codex-fetcher-auth-errors.test.ts index f482d2594b3c..5674cbc0116d 100644 --- a/src/main/rate-limits/codex-fetcher-auth-errors.test.ts +++ b/src/main/rate-limits/codex-fetcher-auth-errors.test.ts @@ -15,8 +15,8 @@ vi.mock('../codex-cli/command', () => ({ resolveCodexCommand: resolveCodexCommandMock })) -vi.mock('node-pty', () => ({ - spawn: ptySpawnMock +vi.mock('./hidden-daemon-pty', () => ({ + spawnHiddenDaemonPty: ptySpawnMock })) // Auth gate is covered separately; these tests assume a signed-in Codex. @@ -166,6 +166,7 @@ describe('fetchCodexRateLimits auth errors', () => { ptyHandlers.onData = callback return makeDisposable() }), + onError: vi.fn(() => ({ dispose: vi.fn() })), onExit: vi.fn((callback) => { ptyHandlers.onExit = callback return makeDisposable() @@ -202,6 +203,7 @@ describe('fetchCodexRateLimits auth errors', () => { ptyHandlers.onData = callback return makeDisposable() }), + onError: vi.fn(() => ({ dispose: vi.fn() })), onExit: vi.fn(() => makeDisposable()), write: ptyWrite, kill: ptyKill diff --git a/src/main/rate-limits/codex-fetcher-backend.test.ts b/src/main/rate-limits/codex-fetcher-backend.test.ts index 3fefe5e3a2fd..3faaf88b4c9e 100644 --- a/src/main/rate-limits/codex-fetcher-backend.test.ts +++ b/src/main/rate-limits/codex-fetcher-backend.test.ts @@ -10,7 +10,7 @@ const { childSpawnMock, readFileMock, ptySpawnMock } = vi.hoisted(() => ({ vi.mock('node:child_process', () => ({ spawn: childSpawnMock })) vi.mock('node:fs/promises', () => ({ readFile: readFileMock })) -vi.mock('node-pty', () => ({ spawn: ptySpawnMock })) +vi.mock('./hidden-daemon-pty', () => ({ spawnHiddenDaemonPty: ptySpawnMock })) vi.mock('./codex-auth-presence', () => ({ probeCodexAuthPresence: vi.fn(async () => 'present') })) diff --git a/src/main/rate-limits/codex-fetcher-probe-shutdown.test.ts b/src/main/rate-limits/codex-fetcher-probe-shutdown.test.ts index db0c4a7f5ed2..ef3d0a2f9953 100644 --- a/src/main/rate-limits/codex-fetcher-probe-shutdown.test.ts +++ b/src/main/rate-limits/codex-fetcher-probe-shutdown.test.ts @@ -20,8 +20,8 @@ vi.mock('../codex-cli/command', () => ({ resolveCodexCommand: resolveCodexCommandMock })) -vi.mock('node-pty', () => ({ - spawn: ptySpawnMock +vi.mock('./hidden-daemon-pty', () => ({ + spawnHiddenDaemonPty: ptySpawnMock })) // Auth gate is covered by codex-auth-presence.test.ts; these tests assume a signed-in Codex. diff --git a/src/main/rate-limits/codex-fetcher-process-contract.test.ts b/src/main/rate-limits/codex-fetcher-process-contract.test.ts index 8d067a686ad3..24e5687c0194 100644 --- a/src/main/rate-limits/codex-fetcher-process-contract.test.ts +++ b/src/main/rate-limits/codex-fetcher-process-contract.test.ts @@ -19,8 +19,8 @@ vi.mock('../codex-cli/command', () => ({ resolveCodexCommand: resolveCodexCommandMock })) -vi.mock('node-pty', () => ({ - spawn: ptySpawnMock +vi.mock('./hidden-daemon-pty', () => ({ + spawnHiddenDaemonPty: ptySpawnMock })) vi.mock('./codex-auth-presence', () => ({ diff --git a/src/main/rate-limits/codex-fetcher-pty-settle.test.ts b/src/main/rate-limits/codex-fetcher-pty-settle.test.ts index 15c338aad89b..90c1c4c1ae34 100644 --- a/src/main/rate-limits/codex-fetcher-pty-settle.test.ts +++ b/src/main/rate-limits/codex-fetcher-pty-settle.test.ts @@ -14,8 +14,8 @@ vi.mock('../codex-cli/command', () => ({ resolveCodexCommand: resolveCodexCommandMock })) -vi.mock('node-pty', () => ({ - spawn: ptySpawnMock +vi.mock('./hidden-daemon-pty', () => ({ + spawnHiddenDaemonPty: ptySpawnMock })) // Auth gate is covered separately; these tests assume a signed-in Codex. @@ -47,6 +47,7 @@ describe('fetchCodexRateLimits PTY settle timers', () => { ptyHandlers.onData = callback return makeDisposable() }), + onError: vi.fn(() => ({ dispose: vi.fn() })), onExit: vi.fn(() => makeDisposable()), write: vi.fn(), kill: vi.fn() @@ -90,6 +91,7 @@ describe('fetchCodexRateLimits PTY settle timers', () => { ptyHandlers.onData = callback return makeDisposable() }), + onError: vi.fn(() => ({ dispose: vi.fn() })), onExit: vi.fn(() => makeDisposable()), write: vi.fn(), kill: vi.fn() @@ -131,6 +133,7 @@ describe('fetchCodexRateLimits PTY settle timers', () => { ptyHandlers.onData = callback return makeDisposable() }), + onError: vi.fn(() => ({ dispose: vi.fn() })), onExit: vi.fn(() => makeDisposable()), write: vi.fn(), kill: vi.fn() @@ -176,6 +179,7 @@ describe('fetchCodexRateLimits PTY settle timers', () => { ptyHandlers.onData = callback return makeDisposable() }), + onError: vi.fn(() => ({ dispose: vi.fn() })), onExit: vi.fn(() => makeDisposable()), write, kill: vi.fn() @@ -229,6 +233,7 @@ describe('fetchCodexRateLimits PTY settle timers', () => { ptyHandlers.onData = callback return makeDisposable() }), + onError: vi.fn(() => ({ dispose: vi.fn() })), onExit: vi.fn(() => makeDisposable()), write: vi.fn(), kill: vi.fn() @@ -268,6 +273,7 @@ describe('fetchCodexRateLimits PTY settle timers', () => { ptyHandlers.onData = callback return makeDisposable() }), + onError: vi.fn(() => ({ dispose: vi.fn() })), onExit: vi.fn(() => makeDisposable()), write, kill: vi.fn() diff --git a/src/main/rate-limits/codex-fetcher-rpc-exit-diagnostics.test.ts b/src/main/rate-limits/codex-fetcher-rpc-exit-diagnostics.test.ts index ee874ef59429..5f7516edaca6 100644 --- a/src/main/rate-limits/codex-fetcher-rpc-exit-diagnostics.test.ts +++ b/src/main/rate-limits/codex-fetcher-rpc-exit-diagnostics.test.ts @@ -20,8 +20,8 @@ vi.mock('../codex-cli/command', () => ({ resolveCodexCommand: resolveCodexCommandMock })) -vi.mock('node-pty', () => ({ - spawn: ptySpawnMock +vi.mock('./hidden-daemon-pty', () => ({ + spawnHiddenDaemonPty: ptySpawnMock })) vi.mock('./codex-auth-presence', () => ({ diff --git a/src/main/rate-limits/codex-fetcher-runtime-pairing.test.ts b/src/main/rate-limits/codex-fetcher-runtime-pairing.test.ts index 0353bd4c31da..094cbbaf7134 100644 --- a/src/main/rate-limits/codex-fetcher-runtime-pairing.test.ts +++ b/src/main/rate-limits/codex-fetcher-runtime-pairing.test.ts @@ -14,7 +14,7 @@ const { childSpawnMock, readFileMock, resolveCodexCommandMock, ptySpawnMock } = vi.mock('node:child_process', () => ({ spawn: childSpawnMock })) vi.mock('node:fs/promises', () => ({ readFile: readFileMock })) vi.mock('../codex-cli/command', () => ({ resolveCodexCommand: resolveCodexCommandMock })) -vi.mock('node-pty', () => ({ spawn: ptySpawnMock })) +vi.mock('./hidden-daemon-pty', () => ({ spawnHiddenDaemonPty: ptySpawnMock })) vi.mock('../codex/codex-state-db', () => ({ isCodexStateDbBackfillPending: vi.fn(() => false) })) vi.mock('../codex/codex-state-db-backfill-recovery', () => ({ startCodexStateDbBackfillRecoveryInBackground: vi.fn(() => Promise.resolve(null)) diff --git a/src/main/rate-limits/codex-fetcher-session-supplement.test.ts b/src/main/rate-limits/codex-fetcher-session-supplement.test.ts index 232bfc9a83dc..22efa3375302 100644 --- a/src/main/rate-limits/codex-fetcher-session-supplement.test.ts +++ b/src/main/rate-limits/codex-fetcher-session-supplement.test.ts @@ -9,7 +9,7 @@ const { childSpawnMock, readFileMock } = vi.hoisted(() => ({ vi.mock('node:child_process', () => ({ spawn: childSpawnMock })) vi.mock('node:fs/promises', () => ({ readFile: readFileMock })) vi.mock('../codex-cli/command', () => ({ resolveCodexCommand: () => 'codex' })) -vi.mock('node-pty', () => ({ spawn: vi.fn() })) +vi.mock('./hidden-daemon-pty', () => ({ spawnHiddenDaemonPty: vi.fn() })) vi.mock('./codex-auth-presence', () => ({ probeCodexAuthPresence: vi.fn(async () => 'present') })) diff --git a/src/main/rate-limits/codex-fetcher.test.ts b/src/main/rate-limits/codex-fetcher.test.ts index be6edd3b3fb6..bd50a14d3804 100644 --- a/src/main/rate-limits/codex-fetcher.test.ts +++ b/src/main/rate-limits/codex-fetcher.test.ts @@ -1,3 +1,4 @@ +import { makeDisposable, makePtyTerm } from './codex-rate-limit-test-pty' import { EventEmitter } from 'node:events' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' @@ -30,8 +31,8 @@ vi.mock('../codex-cli/command', () => ({ resolveCodexCommand: resolveCodexCommandMock })) -vi.mock('node-pty', () => ({ - spawn: ptySpawnMock +vi.mock('./hidden-daemon-pty', () => ({ + spawnHiddenDaemonPty: ptySpawnMock })) vi.mock('../codex/codex-state-db', () => ({ @@ -54,10 +55,6 @@ import { getActiveHiddenRateLimitPtyCount } from './hidden-pty-cleanup' import { getCmdExePath } from '../win32-utils' import { CODEX_SHORT_LIVED_PROBE_APP_SERVER_ARGS } from '../codex-cli/codex-read-only-app-server-args' -function makeDisposable() { - return { dispose: vi.fn() } -} - function makeRpcChild() { const child = new EventEmitter() as EventEmitter & { stdout: EventEmitter @@ -109,25 +106,6 @@ function respondToRpcRateLimitRead( }) } -function makePtyTerm() { - let dataHandler: ((data: string) => void) | null = null - let exitHandler: (() => void) | null = null - return { - onData: vi.fn((callback: (data: string) => void) => { - dataHandler = callback - return makeDisposable() - }), - onExit: vi.fn((callback: () => void) => { - exitHandler = callback - return makeDisposable() - }), - write: vi.fn(), - kill: vi.fn(), - emitData: (data: string) => dataHandler?.(data), - emitExit: () => exitHandler?.() - } -} - describe('fetchCodexRateLimits', () => { beforeEach(() => { vi.useFakeTimers() @@ -222,6 +200,7 @@ describe('fetchCodexRateLimits', () => { }) ptySpawnMock.mockReturnValue({ onData: vi.fn(() => onDataDisposable), + onError: vi.fn(() => ({ dispose: vi.fn() })), onExit: vi.fn(() => onExitDisposable), write: vi.fn(), kill: killMock @@ -328,6 +307,7 @@ describe('fetchCodexRateLimits', () => { ptyHandlers.onData = callback return makeDisposable() }), + onError: vi.fn(() => ({ dispose: vi.fn() })), onExit: vi.fn(() => makeDisposable()), write: vi.fn(), kill: vi.fn() @@ -824,6 +804,7 @@ describe('fetchCodexRateLimits', () => { ptyHandlers.onData = callback return makeDisposable() }), + onError: vi.fn(() => ({ dispose: vi.fn() })), onExit: vi.fn(() => makeDisposable()), write: vi.fn(), kill: vi.fn() diff --git a/src/main/rate-limits/codex-pty-rate-limit-probe.test.ts b/src/main/rate-limits/codex-pty-rate-limit-probe.test.ts index de3c6631dcc2..01178c10969d 100644 --- a/src/main/rate-limits/codex-pty-rate-limit-probe.test.ts +++ b/src/main/rate-limits/codex-pty-rate-limit-probe.test.ts @@ -1,6 +1,11 @@ -import { describe, expect, it, vi } from 'vitest' +import { afterEach, describe, expect, it, vi } from 'vitest' +const { spawnMock } = vi.hoisted(() => ({ spawnMock: vi.fn() })) +vi.mock('./hidden-daemon-pty', () => ({ spawnHiddenDaemonPty: spawnMock })) +import { getActiveHiddenRateLimitPtyCount } from './hidden-pty-cleanup' import { fetchCodexRateLimitsViaPty } from './codex-pty-rate-limit-probe' +afterEach(() => vi.useRealTimers()) + describe('Codex PTY rate-limit probe cancellation', () => { it('does not resolve the process command after cancellation', async () => { const controller = new AbortController() @@ -21,4 +26,56 @@ describe('Codex PTY rate-limit probe cancellation', () => { }) expect(resolveCommand).not.toHaveBeenCalled() }) + + it('settles a transport failure once and cancels pending status input', async () => { + vi.useFakeTimers() + const data = new Set<(value: string) => void>() + const errors = new Set<(error: Error) => void>() + const exits = new Set<() => void>() + const term = { + pid: 123, + write: vi.fn(), + kill: vi.fn(), + onData: (listener: (value: string) => void) => { + data.add(listener) + return { dispose: () => data.delete(listener) } + }, + onError: (listener: (error: Error) => void) => { + errors.add(listener) + return { dispose: () => errors.delete(listener) } + }, + onExit: (listener: () => void) => { + exits.add(listener) + return { dispose: () => exits.delete(listener) } + } + } + spawnMock.mockResolvedValue(term) + const result = fetchCodexRateLimitsViaPty(() => ({ + command: 'codex', + args: [], + cwd: '.', + env: {} + })) + await Promise.resolve() + for (const listener of data) { + listener('> ') + } + expect(term.write).toHaveBeenCalledExactlyOnceWith('/status') + for (const listener of errors) { + listener(new Error('transport unavailable')) + } + await expect(result).resolves.toMatchObject({ + provider: 'codex', + status: 'error', + session: null, + weekly: null, + error: 'transport unavailable' + }) + expect(term.kill).toHaveBeenCalledOnce() + expect(getActiveHiddenRateLimitPtyCount()).toBe(0) + expect([data.size, errors.size, exits.size]).toEqual([0, 0, 0]) + expect(vi.getTimerCount()).toBe(0) + await vi.advanceTimersByTimeAsync(20_000) + expect(term.write).toHaveBeenCalledOnce() + }) }) diff --git a/src/main/rate-limits/codex-pty-rate-limit-probe.ts b/src/main/rate-limits/codex-pty-rate-limit-probe.ts index 194e7d19fd65..f085cbb543b7 100644 --- a/src/main/rate-limits/codex-pty-rate-limit-probe.ts +++ b/src/main/rate-limits/codex-pty-rate-limit-probe.ts @@ -1,3 +1,4 @@ +import { spawnHiddenDaemonPty } from './hidden-daemon-pty' import type { ProviderRateLimits } from '../../shared/rate-limit-types' import { extractCodexAuthError } from '../../shared/codex-auth-errors' import { withMacTailscaleDnsHint } from '../network/macos-tailscale-dns-diagnostic' @@ -27,15 +28,32 @@ export async function fetchCodexRateLimitsViaPty( resolveCommand: () => CodexPtyRateLimitCommand, options?: CodexRateLimitFetchOptions ): Promise { - if (options?.signal?.aborted) { - return abortedCodexRateLimitResult() - } - const pty = await import('node-pty') if (options?.signal?.aborted) { return abortedCodexRateLimitResult() } const command = resolveCommand() + let term: Awaited> + try { + term = await spawnHiddenDaemonPty( + command.command, + command.args, + { + name: 'xterm-256color', + cols: 120, + rows: 40, + cwd: command.cwd, + env: command.env + }, + options?.signal + ) + } catch (error) { + if (options?.signal?.aborted) { + return abortedCodexRateLimitResult() + } + throw error + } + return new Promise((resolve) => { let output = '' let resolved = false @@ -43,13 +61,6 @@ export async function fetchCodexRateLimitsViaPty( let settleTimer: ReturnType | null = null let timeout: ReturnType | null = null - const term = pty.spawn(command.command, command.args, { - name: 'xterm-256color', - cols: 120, - rows: 40, - cwd: command.cwd, - env: command.env - }) const termDisposables: { dispose: () => void }[] = [registerHiddenRateLimitPty(term)] let statusEnter: ReturnType | null = null @@ -202,6 +213,25 @@ export async function fetchCodexRateLimitsViaPty( termDisposables.push(onDataDisposable) } + termDisposables.push( + term.onError((error) => { + if (resolved) { + return + } + resolved = true + clearSettleTimers() + cleanupHiddenRateLimitPty(term, termDisposables, { kill: true }) + resolve({ + provider: 'codex', + session: null, + weekly: null, + updatedAt: Date.now(), + error: error.message, + status: 'error' + }) + }) + ) + const onExitDisposable = term.onExit(() => { cleanupHiddenRateLimitPty(term, termDisposables, { kill: false }) if (settleTimer) { diff --git a/src/main/rate-limits/codex-rate-limit-test-pty.ts b/src/main/rate-limits/codex-rate-limit-test-pty.ts new file mode 100644 index 000000000000..299cebdf5198 --- /dev/null +++ b/src/main/rate-limits/codex-rate-limit-test-pty.ts @@ -0,0 +1,26 @@ +import { vi } from 'vitest' + +export function makeDisposable() { + return { dispose: vi.fn<() => void>() } +} + +export function makePtyTerm() { + let dataHandler: ((data: string) => void) | null = null + let exitHandler: (() => void) | null = null + return { + onData: vi.fn((callback: (data: string) => void) => { + dataHandler = callback + return makeDisposable() + }), + onError: vi.fn(() => ({ dispose: vi.fn<() => void>() })), + onExit: vi.fn((callback: () => void) => { + exitHandler = callback + return makeDisposable() + }), + write: vi.fn<(data: string) => void>(), + kill: vi.fn<() => void>(), + emitData: (data: string) => dataHandler?.(data), + emitExit: () => exitHandler?.() + } +} + diff --git a/src/main/rate-limits/hidden-daemon-pty.test.ts b/src/main/rate-limits/hidden-daemon-pty.test.ts new file mode 100644 index 000000000000..76bd6d0c3c3a --- /dev/null +++ b/src/main/rate-limits/hidden-daemon-pty.test.ts @@ -0,0 +1,200 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const state = vi.hoisted(() => ({ + event: (_value: unknown) => {}, + disconnected: () => {}, + connect: vi.fn(), + request: vi.fn(), + disconnect: vi.fn(), + notify: vi.fn(), + removeEvent: vi.fn(), + removeDisconnect: vi.fn() +})) +vi.mock('../daemon/daemon-provider-state', () => ({ + getDaemonEndpointFacts: () => ({ socketPath: '/fixture/socket', tokenPath: '/fixture/token' }) +})) +vi.mock('../daemon/client', () => ({ + DaemonClient: class { + ensureConnected = state.connect + request = state.request + disconnect = state.disconnect + notify = state.notify + onEvent(callback: typeof state.event) { + state.event = callback + return state.removeEvent + } + onDisconnected(callback: () => void) { + state.disconnected = callback + return state.removeDisconnect + } + } +})) +import { spawnHiddenDaemonPty } from './hidden-daemon-pty' +const options = { + cwd: '/fixture', + cols: 120, + rows: 40, + env: { TOKEN: 'fixture', REMOVED: undefined } +} + +beforeEach(() => { + vi.clearAllMocks() + state.notify.mockReturnValue(true) + state.connect.mockResolvedValue(undefined) + state.request.mockImplementation(async (type) => + type === 'ping' ? { pong: true, capabilities: { transientPty: 1 } } : { pid: 123 } + ) +}) + +describe('hidden daemon PTY client', () => { + it('refuses an old daemon without spawning or replacing it', async () => { + state.request.mockResolvedValue({ pong: true }) + await expect(spawnHiddenDaemonPty('/tool', [], options)).rejects.toThrow('does not support') + expect(state.request).toHaveBeenCalledTimes(1) + expect(state.disconnect).toHaveBeenCalledOnce() + }) + + it('preserves exact arguments and strips absent environment values', async () => { + const term = await spawnHiddenDaemonPty('/tool', ['a b', '$literal'], options) + expect(state.request.mock.calls[1][1]).toMatchObject({ + file: '/tool', + args: ['a b', '$literal'], + env: { TOKEN: 'fixture' }, + cwd: '/fixture' + }) + expect(state.request.mock.calls[1][1].env).not.toHaveProperty('REMOVED') + term.kill() + expect(state.removeEvent).toHaveBeenCalledOnce() + expect(state.removeDisconnect).toHaveBeenCalledOnce() + }) + + it('buffers output and exit arriving before the create reply', async () => { + state.request.mockImplementation(async (type, payload) => { + if (type === 'ping') { + return { capabilities: { transientPty: 1 } } + } + state.event({ + type: 'event', + event: 'data', + sessionId: payload.id, + payload: { data: 'first' } + }) + state.event({ type: 'event', event: 'exit', sessionId: payload.id, payload: { code: 0 } }) + return { pid: 123 } + }) + const term = await spawnHiddenDaemonPty('/tool', [], options) + const events: unknown[] = [] + term.onData((data) => events.push(data)) + term.onExit((event) => events.push(event)) + await Promise.resolve() + expect(events).toEqual(['first', { exitCode: 0 }]) + term.destroy() + }) + + it('closes a connection that completes after abort', async () => { + let connected: () => void = () => {} + state.connect.mockImplementation( + () => + new Promise((resolve) => { + connected = resolve + }) + ) + const controller = new AbortController() + const creation = spawnHiddenDaemonPty('/tool', [], options, controller.signal) + controller.abort() + connected() + await expect(creation).rejects.toThrow() + expect(state.request).not.toHaveBeenCalled() + expect(state.disconnect).toHaveBeenCalledTimes(2) + }) + it.each(['disconnect', 'terminalError', 'overflow'])( + 'reports %s as failure, never process exit', + async (failure) => { + const term = await spawnHiddenDaemonPty('/tool', [], options) + const onError = vi.fn() + const onExit = vi.fn() + term.onError(onError) + term.onExit(onExit) + const id = state.request.mock.calls[1][1].id + if (failure === 'disconnect') { + state.disconnected() + } else { + state.event({ + type: 'event', + event: failure === 'overflow' ? 'data' : 'terminalError', + sessionId: id, + payload: failure === 'overflow' ? { data: 'x'.repeat(128 * 1024 + 1) } : {} + }) + } + await Promise.resolve() + expect(onError).toHaveBeenCalledOnce() + expect(onError.mock.calls[0][0]).toBeInstanceOf(Error) + expect(onExit).not.toHaveBeenCalled() + expect(state.removeEvent).toHaveBeenCalledOnce() + expect(state.removeDisconnect).toHaveBeenCalledOnce() + } + ) + + it('keeps a confirmed exit when transport closes and delivers it only once', async () => { + const term = await spawnHiddenDaemonPty('/tool', [], options) + const onExit = vi.fn() + const onError = vi.fn() + term.onExit(onExit) + term.onError(onError) + const id = state.request.mock.calls[1][1].id + state.event({ type: 'event', event: 'exit', sessionId: id, payload: { code: 7 } }) + state.disconnected() + await Promise.resolve() + term.onData(vi.fn()) + await Promise.resolve() + expect(onExit).toHaveBeenCalledExactlyOnceWith({ exitCode: 7 }) + expect(onError).not.toHaveBeenCalled() + }) + + it('reports a failed input delivery once without claiming process exit', async () => { + const term = await spawnHiddenDaemonPty('/tool', [], options) + const onError = vi.fn() + const onExit = vi.fn() + term.onError(onError) + term.onExit(onExit) + state.notify.mockReturnValue(false) + term.write('/status') + term.write('\r') + state.disconnected() + await Promise.resolve() + expect(state.notify).toHaveBeenCalledOnce() + expect(onError).toHaveBeenCalledExactlyOnceWith( + new Error('Terminal service could not receive usage probe input') + ) + expect(onExit).not.toHaveBeenCalled() + }) + + it('rejects a create receipt arriving after transport failure and removes listeners', async () => { + state.request.mockImplementation(async (type) => { + if (type === 'ping') { + return { capabilities: { transientPty: 1 } } + } + state.disconnected() + return { pid: 123 } + }) + await expect(spawnHiddenDaemonPty('/tool', [], options)).rejects.toThrow('connection lost') + expect(state.removeEvent).toHaveBeenCalledOnce() + expect(state.removeDisconnect).toHaveBeenCalledOnce() + }) + + it('retains a failure until listeners attach and ignores late exit evidence', async () => { + const term = await spawnHiddenDaemonPty('/tool', [], options) + state.disconnected() + await Promise.resolve() + const onError = vi.fn() + const onExit = vi.fn() + term.onError(onError) + term.onExit(onExit) + const id = state.request.mock.calls[1][1].id + state.event({ type: 'event', event: 'exit', sessionId: id, payload: { code: 0 } }) + await Promise.resolve() + expect(onError).toHaveBeenCalledOnce() + expect(onExit).not.toHaveBeenCalled() + expect(state.removeEvent).toHaveBeenCalledOnce() + }) +}) diff --git a/src/main/rate-limits/hidden-daemon-pty.ts b/src/main/rate-limits/hidden-daemon-pty.ts new file mode 100644 index 000000000000..3aaf3fb90f8f --- /dev/null +++ b/src/main/rate-limits/hidden-daemon-pty.ts @@ -0,0 +1,198 @@ +import { randomUUID } from 'node:crypto' +import { z } from 'zod' +import { DaemonClient } from '../daemon/client' +import { getDaemonEndpointFacts } from '../daemon/daemon-provider-state' + +const probeEvent = z.object({ + type: z.literal('event'), + event: z.enum(['data', 'exit', 'terminalError']), + sessionId: z.string(), + payload: z.object({ data: z.string().optional(), code: z.number().optional() }) +}) +const capability = z.object({ capabilities: z.object({ transientPty: z.literal(1) }) }) + +export async function spawnHiddenDaemonPty( + file: string, + args: string[], + options: { cwd: string; env: NodeJS.ProcessEnv; cols: number; rows: number; name?: string }, + signal?: AbortSignal +) { + signal?.throwIfAborted() + const endpoint = getDaemonEndpointFacts() + if (!endpoint) { + throw new Error('Terminal service unavailable for usage probe') + } + const client = new DaemonClient(endpoint) + const id = randomUUID() + const dataListeners = new Set<(data: string) => void>() + const exitListeners = new Set<(event: { exitCode: number }) => void>() + let pending = '' + const errorListeners = new Set<(error: Error) => void>() + let outcome: { exitCode: number } | Error | null = null + const getFailure = (): Error | null => (outcome instanceof Error ? outcome : null) + let delivered = false + let closed = false + let activationQueued = false + const close = (): void => { + if (closed) { + client.disconnect() + return + } + closed = true + signal?.removeEventListener('abort', close) + removeEvents() + removeDisconnect() + dataListeners.clear() + exitListeners.clear() + errorListeners.clear() + pending = '' + client.disconnect() + } + const drain = (): void => { + if (activationQueued) { + return + } + activationQueued = true + queueMicrotask(() => { + activationQueued = false + if (closed) { + return + } + if (pending && dataListeners.size) { + const output = pending + pending = '' + for (const listener of dataListeners) { + listener(output) + } + } + if (closed || delivered || outcome === null) { + return + } + if (outcome instanceof Error ? errorListeners.size : exitListeners.size) { + delivered = true + try { + if (outcome instanceof Error) { + for (const listener of errorListeners) { + listener(outcome) + } + } else { + for (const listener of exitListeners) { + listener(outcome) + } + } + } finally { + close() + } + } + }) + } + const fail = (message: string): void => { + if (closed || outcome !== null) { + return + } + outcome = new Error(message) + pending = '' + client.disconnect() + drain() + } + const removeEvents = client.onEvent((input) => { + const parsed = probeEvent.safeParse(input) + if (!parsed.success || parsed.data.sessionId !== id || closed || outcome !== null) { + return + } + const event = parsed.data + if (event.event === 'data' && event.payload.data !== undefined) { + pending += event.payload.data + if (pending.length > 128 * 1024) { + fail('Usage probe output exceeded its buffer limit') + return + } + } else if (event.event === 'exit' && event.payload.code !== undefined) { + outcome = { exitCode: event.payload.code } + } else if (event.event !== 'data') { + fail('Terminal service could not complete the usage probe') + return + } + drain() + }) + const removeDisconnect = client.onDisconnected(() => { + fail('Terminal service connection lost during usage probe') + }) + signal?.addEventListener('abort', close, { once: true }) + try { + await client.ensureConnected() + signal?.throwIfAborted() + if (!capability.safeParse(await client.request('ping', {}, 5_000, signal)).success) { + throw new Error( + 'Running terminal service does not support hidden usage probes; retry after its terminals close' + ) + } + const env = Object.fromEntries( + Object.entries(options.env).filter( + (entry): entry is [string, string] => typeof entry[1] === 'string' + ) + ) + const result = await client.request<{ pid: number }>( + 'createTransientPty', + { + id, + file, + args, + cwd: options.cwd, + env, + cols: options.cols, + rows: options.rows + }, + 10_000, + signal + ) + signal?.throwIfAborted() + if (closed) { + throw new Error('Usage probe canceled during startup') + } + const failure = getFailure() + if (failure) { + throw failure + } + return { + pid: result.pid, + write(data: string) { + if (!closed && outcome === null && !client.notify('writeTransientPty', { id, data })) { + fail('Terminal service could not receive usage probe input') + } + }, + kill: close, + destroy: close, + onData(listener: (data: string) => void) { + dataListeners.add(listener) + drain() + return { + dispose: () => { + dataListeners.delete(listener) + } + } + }, + onError(listener: (error: Error) => void) { + errorListeners.add(listener) + drain() + return { + dispose: () => { + errorListeners.delete(listener) + } + } + }, + onExit(listener: (event: { exitCode: number }) => void) { + exitListeners.add(listener) + drain() + return { + dispose: () => { + exitListeners.delete(listener) + } + } + } + } + } catch (error) { + close() + throw error + } +} diff --git a/src/main/rate-limits/hidden-pty-cleanup.test.ts b/src/main/rate-limits/hidden-pty-cleanup.test.ts index 43e8453fa0fd..0d2054a5ddb0 100644 --- a/src/main/rate-limits/hidden-pty-cleanup.test.ts +++ b/src/main/rate-limits/hidden-pty-cleanup.test.ts @@ -20,7 +20,7 @@ describe('cleanupHiddenRateLimitPty', () => { vi.clearAllMocks() }) - it('disposes listeners, kills the child, then destroys the PTY fd on POSIX', () => { + it('disposes listeners before closing the hidden probe lease', () => { setPlatform('darwin') const dataDisposable = { dispose: vi.fn() } const exitDisposable = { dispose: vi.fn() } @@ -38,9 +38,7 @@ describe('cleanupHiddenRateLimitPty', () => { expect(exitDisposable.dispose.mock.invocationCallOrder[0]).toBeLessThan( killMock.mock.invocationCallOrder[0] ) - expect(killMock.mock.invocationCallOrder[0]).toBeLessThan( - term.destroy.mock.invocationCallOrder[0] - ) + expect(term.destroy).not.toHaveBeenCalled() }) it('releases the PTY fd without killing again after natural exit', () => { @@ -57,52 +55,6 @@ describe('cleanupHiddenRateLimitPty', () => { expect(term.destroy).toHaveBeenCalledTimes(1) }) - it('neutralizes POSIX destroy-time SIGHUP after the intentional kill', () => { - setPlatform('linux') - const killMock = vi.fn() - const term = { - kill: killMock, - destroy: vi.fn(() => { - term.kill('SIGHUP') - }) - } - - cleanupHiddenRateLimitPty(term, [], { kill: true }) - - expect(killMock).toHaveBeenCalledTimes(1) - expect(killMock).toHaveBeenCalledWith() - }) - - it('does not destroy after an intentional Windows kill because destroy kills again', () => { - setPlatform('win32') - const term = { - kill: vi.fn(), - destroy: vi.fn(() => { - term.kill() - }) - } - - cleanupHiddenRateLimitPty(term, [], { kill: true }) - - expect(term.kill).toHaveBeenCalledTimes(1) - expect(term.destroy).not.toHaveBeenCalled() - }) - - it('destroys a Windows PTY after natural exit so ConPTY cleanup still runs', () => { - setPlatform('win32') - const term = { - kill: vi.fn(), - destroy: vi.fn(() => { - term.kill() - }) - } - - cleanupHiddenRateLimitPty(term, [], { kill: false }) - - expect(term.kill).toHaveBeenCalledTimes(1) - expect(term.destroy).toHaveBeenCalledTimes(1) - }) - it('removes registered hidden PTYs when cleanup kills them', () => { setPlatform('darwin') const term = { diff --git a/src/main/rate-limits/hidden-pty-cleanup.ts b/src/main/rate-limits/hidden-pty-cleanup.ts index 068b937ee844..4c868e61f754 100644 --- a/src/main/rate-limits/hidden-pty-cleanup.ts +++ b/src/main/rate-limits/hidden-pty-cleanup.ts @@ -31,27 +31,12 @@ export function cleanupHiddenRateLimitPty( disposable.dispose() } - if (options.kill) { - try { + try { + if (options.kill) { term.kill() - } catch { - /* already exited */ - } - - // Why: node-pty WindowsTerminal.destroy() calls kill() again, which can - // close the same ConPTY handle twice after an intentional termination. - if (process.platform === 'win32') { - return + } else { + term.destroy?.() } - } - - // Why: node-pty destroy releases the master PTY fd; on POSIX, neutralize - // the post-close SIGHUP hook after exit/kill to avoid pid reuse. - if (process.platform !== 'win32') { - term.kill = () => {} - } - try { - term.destroy?.() } catch { /* already torn down */ } diff --git a/src/main/runtime/orca-runtime-serialize-main-terminal-buffer.ts b/src/main/runtime/orca-runtime-serialize-main-terminal-buffer.ts index 7994f7d8857d..1110caa42f1f 100644 --- a/src/main/runtime/orca-runtime-serialize-main-terminal-buffer.ts +++ b/src/main/runtime/orca-runtime-serialize-main-terminal-buffer.ts @@ -115,7 +115,7 @@ export class OrcaRuntimeWithSerializeMainTerminalBuffer extends OrcaRuntimeWithA size?: { cols: number; rows: number }, metadata: HeadlessSeedMetadata = {} ): void { - if (!data) { + if (!data && !metadata.allowEmpty) { return } const existing = this.headlessTerminals.get(ptyId) diff --git a/src/main/runtime/orca-runtime-wait-for-leaf-pty-id.ts b/src/main/runtime/orca-runtime-wait-for-leaf-pty-id.ts index 37ac8c5a8394..fbe75df04703 100644 --- a/src/main/runtime/orca-runtime-wait-for-leaf-pty-id.ts +++ b/src/main/runtime/orca-runtime-wait-for-leaf-pty-id.ts @@ -110,9 +110,6 @@ export class OrcaRuntimeWithWaitForLeafPtyId extends OrcaRuntimeWithRestoreLiveP }, trailingOutput: { data: string; seq: number }[] = [] ): void { - if (!snapshot.data) { - return - } // Why: a redraw byte can create a suffix-only model before the renderer settles; replace it with the exact snapshot already sent mobile. this.providerSnapshotPreferredPtys.add(ptyId) this.disposeHeadlessTerminal(ptyId) @@ -120,7 +117,7 @@ export class OrcaRuntimeWithWaitForLeafPtyId extends OrcaRuntimeWithRestoreLiveP ptyId, snapshot.data, { cols: snapshot.cols, rows: snapshot.rows }, - { cwd: snapshot.cwd, oscLinks: snapshot.oscLinks } + { cwd: snapshot.cwd, oscLinks: snapshot.oscLinks, allowEmpty: true } ) for (const chunk of trailingOutput) { this.trackHeadlessTerminalData(ptyId, chunk.data, chunk.seq) diff --git a/src/main/runtime/orchestration-codex-real-pty.integration.test.ts b/src/main/runtime/orchestration-codex-real-pty.integration.test.ts index 28be6b513a75..cb0843e44cea 100644 --- a/src/main/runtime/orchestration-codex-real-pty.integration.test.ts +++ b/src/main/runtime/orchestration-codex-real-pty.integration.test.ts @@ -2,7 +2,8 @@ import { createServer } from 'node:http' import { mkdtempSync, mkdirSync, realpathSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' -import * as pty from 'node-pty' +import { startBundledBunPty } from '../daemon/pty-subprocess/bun-tests/bundled-bun-pty-fixture' +import { getForegroundProcessName } from '../../relay/pty-shell-utils' import { expect, it, vi } from 'vitest' import { AgentHookServer } from '../agent-hooks/server' import { getManagedScript } from '../codex/codex-hook-script' @@ -144,7 +145,8 @@ it.skipIf(!binary || process.platform === 'win32').each(trials)( writeFileSync( join(directory, 'config.toml'), [ - 'model="gpt-5.6-terra"', + // A synthetic model avoids first-run migration prompts for retired public models. + 'model="orca-fixture-model"', 'model_provider="fixture"', 'check_for_update_on_startup=false', '[model_providers.fixture]', @@ -164,27 +166,27 @@ it.skipIf(!binary || process.platform === 'win32').each(trials)( value !== undefined && !key.startsWith('ORCA_') && !key.startsWith('CODEX_') ) ) as Record - const terminal = pty.spawn( - binary!, - ['--no-alt-screen', '--dangerously-bypass-hook-trust', 'Reply OK only'], - { - name: 'xterm-256color', - cols: 120, - rows: 40, - cwd: workspace, - env: { - ...env, - ...hooks.buildPtyEnv(), - CODEX_HOME: directory, - TERM: 'xterm-256color', - ORCA_BACKGROUND_LAUNCH: '1', - ORCA_PANE_KEY: PANE_KEY, - ORCA_TAB_ID: TAB_ID, - ORCA_WORKTREE_ID: WORKTREE_ID, - ORCA_AGENT_LAUNCH_TOKEN: LAUNCH_TOKEN - } + if (!binary) { + throw new Error('Missing real Codex binary') + } + const terminal = await startBundledBunPty({ + file: binary, + args: ['--no-alt-screen', '--dangerously-bypass-hook-trust', 'Reply OK only'], + cols: 120, + rows: 40, + cwd: workspace, + env: { + ...env, + ...hooks.buildPtyEnv(), + CODEX_HOME: directory, + TERM: 'xterm-256color', + ORCA_BACKGROUND_LAUNCH: '1', + ORCA_PANE_KEY: PANE_KEY, + ORCA_TAB_ID: TAB_ID, + ORCA_WORKTREE_ID: WORKTREE_ID, + ORCA_AGENT_LAUNCH_TOKEN: LAUNCH_TOKEN } - ) + }) let exited = false const exit = new Promise((resolve) => terminal.onExit(() => { @@ -207,7 +209,7 @@ it.skipIf(!binary || process.platform === 'win32').each(trials)( return true }, getForegroundProcess: async () => { - const name = terminal.process + const name = await getForegroundProcessName(terminal.pid, binary) record('foreground', name) return name } @@ -252,9 +254,10 @@ it.skipIf(!binary || process.platform === 'win32').each(trials)( expect(submittedMail).toBe(true) } finally { if (!exited) { - terminal.kill('SIGKILL') + terminal.kill() } await Promise.race([exit, delay(2000)]) + await terminal.dispose() hooks.stop() model.closeAllConnections() await new Promise((resolve) => model.close(() => resolve())) diff --git a/src/main/runtime/pty-exit-agent-status-reconciliation.test.ts b/src/main/runtime/pty-exit-agent-status-reconciliation.test.ts index f516e4f5b6d0..b92d13d2f644 100644 --- a/src/main/runtime/pty-exit-agent-status-reconciliation.test.ts +++ b/src/main/runtime/pty-exit-agent-status-reconciliation.test.ts @@ -47,8 +47,7 @@ describe('onPtyExit agent-status reconciliation', () => { }) it('reconciles a physical -1 exit reported by the provider callback', () => { - // node-pty forwards real exits as -1 (local-pty-provider-shutdown.test.ts pins this), so the - // numeric code alone cannot separate a dead process from a failed stop. + // A negative code alone cannot separate an observed process exit from a failed stop. const reconcile = vi.fn() runtimeWithBoundPane(reconcile).onPtyExit(PTY, -1, undefined, { providerExitObserved: true }) diff --git a/src/main/runtime/runtime-terminal-state-records.ts b/src/main/runtime/runtime-terminal-state-records.ts index 82e5124bf402..341af339445d 100644 --- a/src/main/runtime/runtime-terminal-state-records.ts +++ b/src/main/runtime/runtime-terminal-state-records.ts @@ -146,6 +146,7 @@ export type RuntimeTerminalBufferSnapshot = { } export type HeadlessSeedMetadata = { + allowEmpty?: boolean cwd?: string | null oscLinks?: TerminalOscLinkRange[] preferProviderIfExisting?: boolean diff --git a/src/main/runtime/tui-idle-name-only-real-pty.integration.test.ts b/src/main/runtime/tui-idle-name-only-real-pty.integration.test.ts index 562ab23ee3ec..b270ea3800c8 100644 --- a/src/main/runtime/tui-idle-name-only-real-pty.integration.test.ts +++ b/src/main/runtime/tui-idle-name-only-real-pty.integration.test.ts @@ -1,136 +1,43 @@ -import { fileURLToPath } from 'node:url' -import * as pty from 'node-pty' -import { afterEach, describe, expect, it } from 'vitest' -import type { OrcaRuntimeService } from './orca-runtime' -import { makeTuiIdleRuntime } from './tui-idle-wait-test-harness' -import type { RuntimeSyncWindowGraph } from '../../shared/runtime-types' -import { TERMINAL_LIFECYCLE_METHODS } from './rpc/methods/terminal/terminal-lifecycle-methods' -import { getForegroundProcessName } from '../../relay/pty-shell-utils' - -// #6011 end-to-end: a REAL pty running a REAL process that emits a REAL name-only -// OSC title while streaming must not satisfy `orca terminal wait --for tui-idle`. -// Everything below is live — real bytes, real `ps` foreground reads, real timers — -// because the bug was a wait that returned satisfied in ~0s, so timing IS the proof. - -const FIXTURE = fileURLToPath(new URL('./tui-idle-agent-fixture.mjs', import.meta.url)) -const WORKTREE_ID = 'repo-1::/tmp/tui-idle-real-pty' -const TAB_ID = '55555555-5555-4555-8555-555555555555' -const LEAF_ID = '66666666-6666-4666-8666-666666666666' -const PTY_ID = 'pty-tui-idle-real' - -const waitMethod = TERMINAL_LIFECYCLE_METHODS.find((method) => method.name === 'terminal.wait')! - -const running: pty.IPty[] = [] - -afterEach(() => { - while (running.length > 0) { - try { - running.pop()?.kill() - } catch { - // The fixture may already be gone. - } - } -}) - -async function startRealAgentPane(mode: 'explicit-idle' | 'quiet', workMs: number) { - const child = pty.spawn(process.execPath, [FIXTURE, mode, String(workMs)], { - name: 'xterm-256color', - cols: 120, - rows: 30, - cwd: '/tmp' - }) - running.push(child) - - // Real foreground read against the real pty: the same helper the relay serves - // `pty.getForegroundProcess` with, so corroboration is host-produced here too. - const runtime = makeTuiIdleRuntime({ - repoPath: '/tmp/tui-idle-real-pty', - getForegroundProcess: () => getForegroundProcessName(child.pid, child.process || null) - }) - runtime.attachWindow(1) - const graph: RuntimeSyncWindowGraph = { - tabs: [ - { - tabId: TAB_ID, - worktreeId: WORKTREE_ID, - title: 'Agent', - activeLeafId: LEAF_ID, - layout: null - } - ], - leaves: [ - { - tabId: TAB_ID, - worktreeId: WORKTREE_ID, - leafId: LEAF_ID, - paneRuntimeId: 1, - ptyId: PTY_ID, - paneTitle: null, - title: '' - } - ] - } - runtime.syncWindowGraph(1, graph) - - const transcript: string[] = [] - child.onData((data) => { - transcript.push(data) - runtime.onPtyData(PTY_ID, data, Date.now()) - }) - - const { terminals } = await runtime.listTerminals(`id:${WORKTREE_ID}`) - return { runtime, transcript, handle: terminals[0].handle } -} - -/** Exactly what `orca terminal wait --terminal --for tui-idle` reaches over RPC. */ -async function terminalWait( - runtime: OrcaRuntimeService, - terminal: string, - timeoutMs: number -): Promise<{ satisfied: boolean; elapsedMs: number }> { - const startedAt = Date.now() - try { - const result = await waitMethod.handler( - { terminal, for: 'tui-idle', timeoutMs }, - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: terminal.wait reads only `runtime` off its context; the rest is request plumbing this fixture has no use for. - { runtime } as Parameters[1] +import { join } from 'node:path' +import { z } from 'zod' +import { describe, expect, it } from 'vitest' +import { runBundledBunFixture } from '../bundled-bun-test-execution' + +// Real PTY bytes, foreground reads and elapsed quiescence guard the name-only title regression. +async function runAgentWait( + mode: 'explicit-idle' | 'quiet', + workMs: number, + timeoutMs: number, + waitForTitle: boolean +) { + return z + .object({ satisfied: z.boolean(), elapsedMs: z.number(), transcript: z.string() }) + .parse( + await runBundledBunFixture( + join(__dirname, '../daemon/tui-idle-agent-bun-fixture.ts'), + 'runTuiIdleAgentWait', + { mode, workMs, timeoutMs, waitForTitle }, + timeoutMs + 7_000 + ) ) - return { satisfied: result.wait.satisfied === true, elapsedMs: Date.now() - startedAt } - } catch (error) { - // Why only `timeout`: an unsatisfied wait is the outcome under test, but any other - // failure means the harness broke and must not read as a passing refusal. - if ((error instanceof Error ? error.message : String(error)) !== 'timeout') { - throw error - } - return { satisfied: false, elapsedMs: Date.now() - startedAt } - } } describe.skipIf(process.platform === 'win32')('tui-idle against a real agent pty', () => { it('does not satisfy while the real process streams under a name-only title', async () => { - const { runtime, transcript, handle } = await startRealAgentPane('quiet', 60_000) - // Wait for real control bytes before measuring idle behavior. - await expect.poll(() => transcript.join(''), { timeout: 5_000 }).toContain(']0;Codex') - - const outcome = await terminalWait(runtime, handle, 8_000) + const outcome = await runAgentWait('quiet', 60_000, 8_000, true) + expect(outcome.transcript).toContain('\x1b]0;Codex\x07') expect(outcome.satisfied).toBe(false) expect(outcome.elapsedMs).toBeGreaterThanOrEqual(7_500) }, 25_000) it('satisfies once the real process emits an explicit idle title', async () => { - const { runtime, handle } = await startRealAgentPane('explicit-idle', 3_000) - await new Promise((resolve) => setTimeout(resolve, 500)) - - const outcome = await terminalWait(runtime, handle, 20_000) + const outcome = await runAgentWait('explicit-idle', 3_000, 20_000, false) expect(outcome.satisfied).toBe(true) expect(outcome.elapsedMs).toBeGreaterThanOrEqual(1_500) }, 28_000) it('satisfies once the real process goes quiet with the agent still in foreground', async () => { - const { runtime, handle } = await startRealAgentPane('quiet', 3_000) - await new Promise((resolve) => setTimeout(resolve, 500)) - - const outcome = await terminalWait(runtime, handle, 20_000) + const outcome = await runAgentWait('quiet', 3_000, 20_000, false) expect(outcome.satisfied).toBe(true) // Corroboration is never instant: quiescence must elapse after the last byte. expect(outcome.elapsedMs).toBeGreaterThanOrEqual(3_000) diff --git a/src/main/ssh-expired-lease-pane-readoption.test.ts b/src/main/ssh-expired-lease-pane-readoption.test.ts index 11bf5fe2083d..eaecc4133a2f 100644 --- a/src/main/ssh-expired-lease-pane-readoption.test.ts +++ b/src/main/ssh-expired-lease-pane-readoption.test.ts @@ -15,7 +15,6 @@ vi.mock('electron', () => ({ app: { getPath: () => testState.dir }, safeStorage: { isEncryptionAvailable: () => false } })) -vi.mock('node-pty', () => ({ spawn: vi.fn(), default: { spawn: vi.fn() } })) const TARGET = 'ssh-1' const HOST_ID = 'ssh:ssh-1' as const diff --git a/src/main/ssh/orcad-activation-gate.ts b/src/main/ssh/orcad-activation-gate.ts index c95819191179..2081822a0242 100644 --- a/src/main/ssh/orcad-activation-gate.ts +++ b/src/main/ssh/orcad-activation-gate.ts @@ -39,7 +39,7 @@ export type OrcadActivationVerdict = | { decision: 'reject'; code: OrcadActivationRejectCode; reason: string } export type OrcadActivationExpectation = { - /** sha256(orcad.js).slice(0,16) computed from the bytes this client just uploaded. */ + /** Hash of the launcher and application bytes this client just uploaded. */ buildHash: string /** The full content-hashed version this deploy installed. */ fullVersion: string diff --git a/src/main/ssh/orcad-artifact-materializer.test.ts b/src/main/ssh/orcad-artifact-materializer.test.ts index beff61176eea..b5dc84e546c8 100644 --- a/src/main/ssh/orcad-artifact-materializer.test.ts +++ b/src/main/ssh/orcad-artifact-materializer.test.ts @@ -19,6 +19,7 @@ import { ORCAD_TEMPLATE_MANIFEST_FILENAME, ORCAD_TEMPLATE_TARGETS_DIR, ORCAD_VERSION_FILENAME, + ORCAD_WINDOWS_CONPTY_ARTIFACTS, orcadArtifactFilenames, orcadTemplateCommonFilenames } from '../../shared/orcad-artifacts' @@ -90,6 +91,9 @@ function createTemplate(target: OrcadBunTarget = TARGET): { write(runtimePath, 'bun-executable') if (target.startsWith('win32-')) { write(join(templateDir, 'windows-process-tree.node'), 'process-table') + for (const filename of ORCAD_WINDOWS_CONPTY_ARTIFACTS) { + write(join(templateDir, filename), filename) + } } write( join(templateDir, ORCAD_TEMPLATE_MANIFEST_FILENAME), diff --git a/src/main/ssh/sftp-upload.test.ts b/src/main/ssh/sftp-upload.test.ts index d5cf25abe70b..0548fff460cc 100644 --- a/src/main/ssh/sftp-upload.test.ts +++ b/src/main/ssh/sftp-upload.test.ts @@ -66,6 +66,26 @@ describe('sftp-upload', () => { expect(writeStream.listenerCount('error')).toBe(1) }) + it('accepts an aliased upload root while checking canonical containment', async () => { + const localDir = await mkdtemp(join(tmpdir(), 'orca-sftp-upload-root-')) + const aliasedDir = `${localDir}-alias` + await writeFile(join(localDir, 'asset.txt'), 'asset') + await symlink(localDir, aliasedDir, process.platform === 'win32' ? 'junction' : 'dir') + const sftp = createSftpMock() + + try { + await uploadDirectory(sftp, aliasedDir, '/remote/assets', aliasedDir, { + exclusive: true + }) + expect(sftp.createWriteStream).toHaveBeenCalledWith('/remote/assets/asset.txt', { + flags: 'wx' + }) + } finally { + await rm(localDir, { recursive: true, force: true }) + await rm(aliasedDir, { recursive: true, force: true }) + } + }) + it('uploads files from valid dot-dot-prefixed local directories', async () => { const localDir = await mkdtemp(join(tmpdir(), 'orca-sftp-upload-')) await mkdir(join(localDir, '..fixtures')) diff --git a/src/main/ssh/sftp-upload.ts b/src/main/ssh/sftp-upload.ts index 514df81ed1d9..4af80a8c7899 100644 --- a/src/main/ssh/sftp-upload.ts +++ b/src/main/ssh/sftp-upload.ts @@ -222,13 +222,25 @@ export async function uploadDirectory( options?: { exclusive?: boolean; signal?: AbortSignal } ): Promise { options?.signal?.throwIfAborted() - await assertLocalUploadPathInsideRoot(rootRealPath, localDir) + const canonicalRootRealPath = await realpath(rootRealPath) + await uploadDirectoryWithinRoot(sftp, localDir, remoteDir, canonicalRootRealPath, options) +} + +async function uploadDirectoryWithinRoot( + sftp: SFTPWrapper, + localDir: string, + remoteDir: string, + canonicalRootRealPath: string, + options?: { exclusive?: boolean; signal?: AbortSignal } +): Promise { + options?.signal?.throwIfAborted() + await assertLocalUploadPathInsideRoot(canonicalRootRealPath, localDir) const entries = await readdir(localDir, { withFileTypes: true }) for (const entry of entries) { options?.signal?.throwIfAborted() const localPath = pathJoin(localDir, entry.name) const remotePath = `${remoteDir}/${entry.name}` - await assertLocalUploadPathInsideRoot(rootRealPath, localPath) + await assertLocalUploadPathInsideRoot(canonicalRootRealPath, localPath) const statResult = await lstat(localPath) // Why: skip symlinks and special files (sockets, FIFOs, devices) to @@ -241,7 +253,7 @@ export async function uploadDirectory( if (statResult.isDirectory()) { await mkdirSftp(sftp, remotePath, { allowExisting: !options?.exclusive }) - await uploadDirectory(sftp, localPath, remotePath, rootRealPath, options) + await uploadDirectoryWithinRoot(sftp, localPath, remotePath, canonicalRootRealPath, options) } else { await uploadFile(sftp, localPath, remotePath, options) } diff --git a/src/main/ssh/ssh-relay-deploy-helpers.ts b/src/main/ssh/ssh-relay-deploy-helpers.ts index dc8098041279..c9038fe37cf4 100644 --- a/src/main/ssh/ssh-relay-deploy-helpers.ts +++ b/src/main/ssh/ssh-relay-deploy-helpers.ts @@ -1,4 +1,4 @@ -import type { ClientChannel } from 'ssh2' +import type { Writable } from 'node:stream' import { createSshOperationAbortError } from './ssh-connection-utils' import { RELAY_SENTINEL, RELAY_SENTINEL_TIMEOUT_MS } from './relay-protocol' import type { MultiplexerTransport } from './ssh-channel-multiplexer' @@ -12,8 +12,24 @@ export { execCommand, isUnconfirmedSshCommandTermination } from './ssh-relay-exe const MAX_RELAY_STARTUP_BUFFER_BYTES = 64 * 1024 const RELAY_SENTINEL_BUFFER = Buffer.from(RELAY_SENTINEL, 'utf-8') +/** Shared startup boundary for SSH channels and guest-process pipes. */ +export type RelayStartupChannel = { + stdin: Pick + stderr: { + on(event: 'data', listener: (data: Buffer) => void): unknown + on(event: 'error', listener: (error: Error) => void): unknown + } + on(event: 'data', listener: (data: Buffer) => void): unknown + on(event: 'exit', listener: (code: number | null) => void): unknown + on(event: 'error', listener: (error: Error) => void): unknown + on(event: 'close', listener: () => void): unknown + pause(): unknown + resume(): unknown + close(): void +} + export function waitForSentinel( - channel: ClientChannel, + channel: RelayStartupChannel, signal?: AbortSignal ): Promise { return new Promise((resolve, reject) => { diff --git a/src/main/ssh/ssh-relay-native-deps-cache-commands.ts b/src/main/ssh/ssh-relay-native-deps-cache-commands.ts index f2ce32a2222a..78feb621d993 100644 --- a/src/main/ssh/ssh-relay-native-deps-cache-commands.ts +++ b/src/main/ssh/ssh-relay-native-deps-cache-commands.ts @@ -1,160 +1,19 @@ -/** - * The remote shell for the shared native-deps cache (`ssh-relay-native-deps-cache.ts`). - * - * Every script here is POSIX `sh` and answers with one token, because the only alternative to a - * token is inferring success from an exit status the transport can also produce. A command that - * cannot answer is a cache miss, never a licence to delete: `MISS` and `NOT_PROMOTED` both leave - * the relay directory owning its own `node_modules`, which is exactly today's behaviour. - */ +/** Historical cache scans require complete reference evidence before removal. */ import { shellEscape } from './ssh-connection-utils' import { RELAY_NATIVE_DEPS_CACHE_COMPLETE_NAME, RELAY_NATIVE_DEPS_CACHE_TOMBSTONE_PREFIX, LEGACY_RELAY_NATIVE_DEPS_CACHE_TOMBSTONE_PREFIX, relayNativeDepsCacheBaseDir, - relayNativeDepsCacheEntryDir, - relayNativeDepsCacheNodeModulesPath, remoteInstallRootDir } from './ssh-relay-native-deps-cache' -import { joinRemotePath, type RemoteHostPlatform } from './ssh-remote-platform' +import type { RemoteHostPlatform } from './ssh-remote-platform' -export const RELAY_NATIVE_CACHE_LINKED = '__ORCA_NATIVE_CACHE__LINKED' -export const RELAY_NATIVE_CACHE_SEEDED = '__ORCA_NATIVE_CACHE__SEEDED' -export const RELAY_NATIVE_CACHE_MISS = '__ORCA_NATIVE_CACHE__MISS' -export const RELAY_NATIVE_CACHE_PROMOTED = '__ORCA_NATIVE_CACHE__PROMOTED' -export const RELAY_NATIVE_CACHE_NOT_PROMOTED = '__ORCA_NATIVE_CACHE__NOT_PROMOTED' export const RELAY_NATIVE_CACHE_LIST_OK = '__ORCA_NATIVE_CACHE__LIST_OK' export const RELAY_NATIVE_CACHE_REFS_OK = '__ORCA_NATIVE_CACHE__REFS_OK' export const RELAY_NATIVE_CACHE_REFS_ERR = '__ORCA_NATIVE_CACHE__REFS_ERR' - -/** - * How old an entry without `.deps-complete` must be before another deploy may reclaim it. Well - * past the 15-minute deploy ceiling, so a live installer is never mistaken for a crashed one. - * Reclaiming is safe at any age in principle — nothing links an entry until it is complete — but - * the margin is what keeps that argument from resting on a single `[ -f ]`. - */ -const CACHE_TAKEOVER_MINUTES = 120 - -/** Bounds every listing, matching `MAX_RELAY_GC_LISTING_ENTRIES`' role for version dirs. */ export const MAX_RELAY_NATIVE_CACHE_LISTING_ENTRIES = 64 -export type RelayNativeDepsCachePaths = { - host: RemoteHostPlatform - remoteHome: string - relayDir: string - key: string -} - -function cachePaths(paths: RelayNativeDepsCachePaths): { - base: string - entry: string - target: string - nodeModules: string - root: string -} { - const { host, remoteHome, relayDir, key } = paths - return { - base: relayNativeDepsCacheBaseDir(host, remoteHome), - entry: relayNativeDepsCacheEntryDir(host, remoteHome, key), - target: relayNativeDepsCacheNodeModulesPath(host, remoteHome, key), - nodeModules: joinRemotePath(host, relayDir, 'node_modules'), - root: remoteInstallRootDir(host, remoteHome) - } -} - -/** - * Link a complete entry into the relay directory, or seed a private tree from a sibling relay - * directory that already has a matching one. - * - * The seed exists so the first deploy after this ships does not recompile once more on a host - * that already paid for the compile. It is not trusted: the copy is a plain private install until - * the normal probe loads both addons, and only then is it promoted. - */ -export function ensureRelayNativeDepsCacheCommand( - paths: RelayNativeDepsCachePaths, - deps: Readonly> -): string { - const { entry, target, nodeModules, root } = cachePaths(paths) - // Why grep the sibling's manifest: an older Orca pinned different versions, and a - // toolchain-skip host wrote one with node-pty removed. Both must fail to qualify. - const depGuards = Object.entries(deps).map( - ([name, version]) => `grep -F -q ${shellEscape(`"${name}":"${version}"`)} "$pj" || continue` - ) - return [ - `cache=${shellEscape(entry)}`, - `target=${shellEscape(target)}`, - `nm=${shellEscape(nodeModules)}`, - `root=${shellEscape(root)}`, - `if [ -f "$cache/${RELAY_NATIVE_DEPS_CACHE_COMPLETE_NAME}" ] && [ -d "$target" ]; then`, - ' if [ -L "$nm" ]; then', - ` if [ "$(readlink "$nm" 2>/dev/null)" = "$target" ]; then printf '%s\\n' ${RELAY_NATIVE_CACHE_LINKED}; exit 0; fi`, - ' rm -f "$nm" 2>/dev/null || true', - ' fi', - ` if [ ! -e "$nm" ] && ln -s "$target" "$nm" 2>/dev/null; then printf '%s\\n' ${RELAY_NATIVE_CACHE_LINKED}; exit 0; fi`, - ` printf '%s\\n' ${RELAY_NATIVE_CACHE_MISS}; exit 0`, - 'fi', - 'if [ ! -e "$nm" ] && [ ! -L "$nm" ]; then', - ' for cand in "$root"/relay-*/node_modules; do', - ' [ -d "$cand" ] || continue', - ' [ -L "$cand" ] && continue', - ' [ -d "$cand/node-pty" ] || continue', - ' [ -d "$cand/@parcel/watcher" ] || continue', - ' pj="${cand%/node_modules}/package.json"', - ' [ -f "$pj" ] || continue', - ...depGuards.map((guard) => ` ${guard}`), - ' seed="$nm.seed.$$"', - ' rm -rf "$seed" 2>/dev/null || true', - ' if cp -Rp "$cand" "$seed" 2>/dev/null && mv "$seed" "$nm" 2>/dev/null; then', - ` printf '%s\\n' ${RELAY_NATIVE_CACHE_SEEDED}; exit 0`, - ' fi', - ' rm -rf "$seed" 2>/dev/null || true', - ' break', - ' done', - 'fi', - `printf '%s\\n' ${RELAY_NATIVE_CACHE_MISS}` - ].join('\n') -} - -/** - * Publish a probe-verified private tree as the shared entry, then link the relay directory to it. - * - * `mkdir "$cache"` is the election: exactly one deploy creates the directory, and a loser keeps - * its own tree rather than writing into someone else's. `.deps-complete` is written last, after - * the symlink exists, so an entry is never linkable before it is referenced. - */ -export function promoteRelayNativeDepsCacheCommand(paths: RelayNativeDepsCachePaths): string { - const { base, entry, target, nodeModules } = cachePaths(paths) - const notPromoted = `printf '%s\\n' ${RELAY_NATIVE_CACHE_NOT_PROMOTED}` - return [ - `base=${shellEscape(base)}`, - `cache=${shellEscape(entry)}`, - `target=${shellEscape(target)}`, - `nm=${shellEscape(nodeModules)}`, - `[ -d "$nm" ] || { ${notPromoted}; exit 0; }`, - `if [ -L "$nm" ]; then ${notPromoted}; exit 0; fi`, - `mkdir -p "$base" 2>/dev/null || { ${notPromoted}; exit 0; }`, - `if [ -d "$cache" ] && [ ! -f "$cache/${RELAY_NATIVE_DEPS_CACHE_COMPLETE_NAME}" ]; then`, - ` if [ -n "$(find "$cache" -maxdepth 0 -mmin +${CACHE_TAKEOVER_MINUTES} 2>/dev/null)" ]; then`, - ' rm -rf "$cache" 2>/dev/null || true', - ' fi', - 'fi', - `mkdir "$cache" 2>/dev/null || { ${notPromoted}; exit 0; }`, - 'if mv "$nm" "$target" 2>/dev/null; then', - ' if ln -s "$target" "$nm" 2>/dev/null; then', - ` : > "$cache/${RELAY_NATIVE_DEPS_CACHE_COMPLETE_NAME}" 2>/dev/null || true`, - ` if [ -f "$cache/${RELAY_NATIVE_DEPS_CACHE_COMPLETE_NAME}" ]; then printf '%s\\n' ${RELAY_NATIVE_CACHE_PROMOTED}; exit 0; fi`, - ' fi', - ' rm -f "$nm" 2>/dev/null || true', - // Why the rm is conditional on the move back: a failed restore leaves the only copy of the - // tree inside an incomplete entry. Deleting it there would cost the relay its native deps. - ' if mv "$target" "$nm" 2>/dev/null; then rm -rf "$cache" 2>/dev/null || true; fi', - ` ${notPromoted}; exit 0`, - 'fi', - 'rm -rf "$cache" 2>/dev/null || true', - notPromoted - ].join('\n') -} - /** Complete entries and tombstones; deletion requires a separate reference scan. */ export function listRelayNativeDepsCacheEntriesCommand( host: RemoteHostPlatform, diff --git a/src/main/ssh/ssh-relay-native-deps-cache-install.ts b/src/main/ssh/ssh-relay-native-deps-cache-install.ts deleted file mode 100644 index adc1f01d54f3..000000000000 --- a/src/main/ssh/ssh-relay-native-deps-cache-install.ts +++ /dev/null @@ -1,209 +0,0 @@ -/** - * The deploy-side half of the shared native-deps cache: resolve this build's key, try to link an - * existing entry, and publish a probe-verified tree afterwards. - * - * Both entry points answer with a value, never an exception. The cache is an optimization on a - * path that must still connect a host with a read-only home, no `ln`, or an SSH server that drops - * the channel — every one of those is a plain per-directory install, which is what the relay did - * before this existed. - */ -import { existsSync, readFileSync } from 'node:fs' -import { join } from 'node:path' -import type { SshConnection } from './ssh-connection' -import { RELAY_ARTIFACTS } from '../../shared/relay-artifacts' -import { execCommand } from './ssh-relay-deploy-helpers' -import { NATIVE_DEPS_COMMAND_TIMEOUT_MS } from './ssh-relay-deploy-timing' -import { - computeRelayNativeDepsCacheKey, - supportsRelayNativeDepsCache, - RELAY_NATIVE_DEPS_PATCH_ARTIFACT_PATTERN, - type RelayNativeDepsCachePatchSource -} from './ssh-relay-native-deps-cache' -import { - ensureRelayNativeDepsCacheCommand, - promoteRelayNativeDepsCacheCommand, - RELAY_NATIVE_CACHE_LINKED, - RELAY_NATIVE_CACHE_PROMOTED, - RELAY_NATIVE_CACHE_SEEDED, - type RelayNativeDepsCachePaths -} from './ssh-relay-native-deps-cache-commands' -import type { RemoteHostPlatform } from './ssh-remote-platform' - -/** - * `linked` — the relay directory now points at a complete shared entry and needs no install. - * `private` — it owns (or is about to own) its own tree, which promotion may later publish. - */ -export type RelayNativeDepsCacheAttachment = { - mode: 'linked' | 'private' - key: string -} - -export type RelayNativeDepsCacheContext = { - hostPlatform: RemoteHostPlatform - remoteHome: string - relayDir: string - platform: string - localRelayDir: string - deps: Readonly> - signal?: AbortSignal -} - -function execHostCommand( - conn: SshConnection, - host: RemoteHostPlatform, - command: string, - signal?: AbortSignal -): Promise { - return execCommand(conn, command, { - wrapCommand: host.commandDialect !== 'powershell', - // Why the native-deps budget and not the default 30s: a seeding copy moves a whole - // node_modules on the host's own disk, which is fast but not instant on a cold cache. - timeoutMs: NATIVE_DEPS_COMMAND_TIMEOUT_MS, - signal - }) -} - -/** - * Every shipped artifact that patches the installed native tree, read for hashing. - * - * Reading is best-effort by design: a patch this client cannot read must not silently drop out of - * the key, so an unreadable one disables the cache rather than producing a key that claims the - * patch was applied. - */ -export function readRelayNativeDepsPatchSources( - localRelayDir: string -): RelayNativeDepsCachePatchSource[] | null { - const sources: RelayNativeDepsCachePatchSource[] = [] - for (const artifact of RELAY_ARTIFACTS) { - if (!RELAY_NATIVE_DEPS_PATCH_ARTIFACT_PATTERN.test(artifact.filename)) { - continue - } - const path = join(localRelayDir, artifact.filename) - try { - if (!existsSync(path)) { - continue - } - sources.push({ filename: artifact.filename, contents: readFileSync(path, 'utf-8') }) - } catch { - return null - } - } - return sources -} - -/** This build's cache key, or null when it cannot be computed and the cache must stay off. */ -export function resolveRelayNativeDepsCacheKey(context: { - platform: string - localRelayDir: string - deps: Readonly> -}): string | null { - const patchSources = readRelayNativeDepsPatchSources(context.localRelayDir) - if (!patchSources) { - return null - } - try { - return computeRelayNativeDepsCacheKey({ - platform: context.platform, - deps: context.deps, - patchSources - }) - } catch (err) { - console.warn( - `[ssh-relay] Native-deps cache key unavailable for ${context.platform}: ${ - err instanceof Error ? err.message : String(err) - }` - ) - return null - } -} - -/** - * Link a complete entry, or leave the relay directory to install privately. - * - * Returns null when the cache is off for this host, which keeps the caller on the exact command - * sequence it ran before the cache existed. - */ -export async function attachRelayNativeDepsCache( - conn: SshConnection, - context: RelayNativeDepsCacheContext -): Promise { - if (!supportsRelayNativeDepsCache(context.hostPlatform)) { - return null - } - const key = resolveRelayNativeDepsCacheKey(context) - if (!key) { - return null - } - const paths = cachePathsFor(context, key) - try { - const output = await execHostCommand( - conn, - context.hostPlatform, - ensureRelayNativeDepsCacheCommand(paths, context.deps), - context.signal - ) - if (output.includes(RELAY_NATIVE_CACHE_LINKED)) { - console.log(`[ssh-relay] Native deps linked from shared cache entry ${key}`) - return { mode: 'linked', key } - } - if (output.includes(RELAY_NATIVE_CACHE_SEEDED)) { - console.log(`[ssh-relay] Seeded native deps for ${key} from an existing install on this host`) - } - return { mode: 'private', key } - } catch (err) { - context.signal?.throwIfAborted() - // Why still 'private' and not null: the relay directory owns nothing yet either way, and the - // install that follows is identical. Promotion afterwards is separately best-effort. - console.warn( - `[ssh-relay] Native-deps cache probe for ${key} failed; installing per-directory: ${ - err instanceof Error ? err.message : String(err) - }` - ) - return { mode: 'private', key } - } -} - -/** - * Publish a private tree the probe just loaded, and link the relay directory to it. - * - * Called only after `probeInstalledNativeDeps` reported both addons loadable on this host, so an - * entry is never published on the strength of a successful `npm install` alone. - */ -export async function promoteRelayNativeDepsCache( - conn: SshConnection, - context: RelayNativeDepsCacheContext, - key: string -): Promise { - try { - const output = await execHostCommand( - conn, - context.hostPlatform, - promoteRelayNativeDepsCacheCommand(cachePathsFor(context, key)), - context.signal - ) - console.log( - output.includes(RELAY_NATIVE_CACHE_PROMOTED) - ? `[ssh-relay] Published native deps as shared cache entry ${key}` - : `[ssh-relay] Native deps stay per-directory; shared cache entry ${key} was not published` - ) - } catch (err) { - context.signal?.throwIfAborted() - console.warn( - `[ssh-relay] Could not publish native-deps cache entry ${key}: ${ - err instanceof Error ? err.message : String(err) - }` - ) - } -} - -function cachePathsFor( - context: RelayNativeDepsCacheContext, - key: string -): RelayNativeDepsCachePaths { - return { - host: context.hostPlatform, - remoteHome: context.remoteHome, - relayDir: context.relayDir, - key - } -} diff --git a/src/main/ssh/ssh-relay-native-deps-cache-shell.test.ts b/src/main/ssh/ssh-relay-native-deps-cache-shell.test.ts index 530eed7238ea..fff61a030f2a 100644 --- a/src/main/ssh/ssh-relay-native-deps-cache-shell.test.ts +++ b/src/main/ssh/ssh-relay-native-deps-cache-shell.test.ts @@ -1,90 +1,42 @@ -// The cache's safety argument is made of `sh`, not TypeScript: `mkdir` elects the publisher, -// `.deps-complete` gates linking, and a failed publish must put the tree back. Asserting on the -// command strings cannot show any of that, so these run the real scripts against a real tree. - import { execFileSync } from 'node:child_process' -import { - mkdirSync, - mkdtempSync, - readlinkSync, - rmSync, - statSync, - symlinkSync, - writeFileSync, - existsSync, - lstatSync -} from 'node:fs' +import { existsSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it } from 'vitest' - import { getRemoteHostPlatform } from './ssh-remote-platform' import { - computeRelayNativeDepsCacheKey, relayNativeDepsCacheEntryDir, relayNativeDepsCacheNodeModulesPath } from './ssh-relay-native-deps-cache' import { - ensureRelayNativeDepsCacheCommand, listRelayNativeDepsCacheEntriesCommand, listRelayNativeDepsCacheReferencesCommand, - promoteRelayNativeDepsCacheCommand, - RELAY_NATIVE_CACHE_LINKED, RELAY_NATIVE_CACHE_LIST_OK, - RELAY_NATIVE_CACHE_MISS, - RELAY_NATIVE_CACHE_NOT_PROMOTED, - RELAY_NATIVE_CACHE_PROMOTED, - RELAY_NATIVE_CACHE_REFS_OK, - RELAY_NATIVE_CACHE_SEEDED + RELAY_NATIVE_CACHE_REFS_OK } from './ssh-relay-native-deps-cache-commands' const HOST = getRemoteHostPlatform('linux-x64') -const DEPS = { 'node-pty': '1.1.0', '@parcel/watcher': '2.5.6' } as const -const KEY = computeRelayNativeDepsCacheKey({ platform: 'linux-x64', deps: DEPS }) +const KEY = 'linux-x64-99355f00e9e557a3' -// Debian and Ubuntu point /bin/sh at dash, which is stricter than the bash-in-sh-mode that macOS -// ships; run against both when both exist so a bashism cannot pass here and fail on a host. const SHELLS = ['/bin/sh', '/bin/dash'].filter((shell) => existsSync(shell)) describe.runIf(process.platform !== 'win32').each(SHELLS)( - 'relay native-deps cache shell scripts (%s)', + 'historical relay native cache scans (%s)', (shell) => { let home: string - const relayDir = (version: string): string => join(home, '.orca-remote', `relay-${version}`) function sh(command: string): string { return execFileSync(shell, ['-c', command], { encoding: 'utf-8' }) } - /** A relay directory holding its own installed tree, exactly as `npm install` leaves it. */ - function makePrivateInstall(version: string, deps: Record = DEPS): string { - const dir = relayDir(version) - mkdirSync(join(dir, 'node_modules', 'node-pty', 'build'), { recursive: true }) - mkdirSync(join(dir, 'node_modules', '@parcel', 'watcher'), { recursive: true }) - writeFileSync(join(dir, 'node_modules', 'node-pty', 'build', 'pty.node'), 'binary') - writeFileSync(join(dir, 'package.json'), JSON.stringify({ dependencies: deps })) - return dir - } - - function ensure(version: string): string { - return sh( - ensureRelayNativeDepsCacheCommand( - { host: HOST, remoteHome: home, relayDir: relayDir(version), key: KEY }, - DEPS - ) - ).trim() - } - - function promote(version: string): string { - return sh( - promoteRelayNativeDepsCacheCommand({ - host: HOST, - remoteHome: home, - relayDir: relayDir(version), - key: KEY - }) - ).trim() + function makeLegacyCache(version: string): void { + const entry = relayNativeDepsCacheEntryDir(HOST, home, KEY) + const target = relayNativeDepsCacheNodeModulesPath(HOST, home, KEY) + mkdirSync(target, { recursive: true }) + writeFileSync(join(entry, '.deps-complete'), '') + mkdirSync(relayDir(version), { recursive: true }) + symlinkSync(target, join(relayDir(version), 'node_modules')) } beforeEach(() => { @@ -96,119 +48,8 @@ describe.runIf(process.platform !== 'win32').each(SHELLS)( rmSync(home, { recursive: true, force: true }) }) - it('publishes a probe-verified tree and links the relay directory to it', () => { - makePrivateInstall('0.1.0+aaa') - - expect(promote('0.1.0+aaa')).toBe(RELAY_NATIVE_CACHE_PROMOTED) - - const target = relayNativeDepsCacheNodeModulesPath(HOST, home, KEY) - expect(readlinkSync(join(relayDir('0.1.0+aaa'), 'node_modules'))).toBe(target) - expect(statSync(join(target, 'node-pty', 'build', 'pty.node')).isFile()).toBe(true) - expect( - existsSync(join(relayNativeDepsCacheEntryDir(HOST, home, KEY), '.deps-complete')) - ).toBe(true) - }) - - it('links a second bundle to the published tree with no install of its own', () => { - makePrivateInstall('0.1.0+aaa') - promote('0.1.0+aaa') - - // A different bundle: fresh directory, no node_modules, nothing installed. - mkdirSync(relayDir('0.1.0+bbb'), { recursive: true }) - expect(ensure('0.1.0+bbb')).toBe(RELAY_NATIVE_CACHE_LINKED) - - const linked = join(relayDir('0.1.0+bbb'), 'node_modules') - expect(readlinkSync(linked)).toBe(relayNativeDepsCacheNodeModulesPath(HOST, home, KEY)) - expect(statSync(join(linked, 'node-pty', 'build', 'pty.node')).isFile()).toBe(true) - }) - - it('will not link an entry whose completion sentinel is absent', () => { - makePrivateInstall('0.1.0+aaa') - promote('0.1.0+aaa') - rmSync(join(relayNativeDepsCacheEntryDir(HOST, home, KEY), '.deps-complete')) - - mkdirSync(relayDir('0.1.0+bbb'), { recursive: true }) - expect(ensure('0.1.0+bbb')).toBe(RELAY_NATIVE_CACHE_MISS) - expect(existsSync(join(relayDir('0.1.0+bbb'), 'node_modules'))).toBe(false) - }) - - it('seeds from a sibling install rather than recompiling once more', () => { - makePrivateInstall('0.1.0+aaa') - mkdirSync(relayDir('0.1.0+bbb'), { recursive: true }) - - expect(ensure('0.1.0+bbb')).toBe(RELAY_NATIVE_CACHE_SEEDED) - - const seeded = join(relayDir('0.1.0+bbb'), 'node_modules') - expect(lstatSync(seeded).isSymbolicLink()).toBe(false) - expect(statSync(join(seeded, 'node-pty', 'build', 'pty.node')).isFile()).toBe(true) - // The source is untouched, so a relay running out of it is unaffected. - expect(existsSync(join(relayDir('0.1.0+aaa'), 'node_modules', 'node-pty'))).toBe(true) - }) - - it('refuses to seed from a sibling pinned to different versions', () => { - makePrivateInstall('0.1.0+aaa', { 'node-pty': '1.0.0', '@parcel/watcher': '2.5.6' }) - mkdirSync(relayDir('0.1.0+bbb'), { recursive: true }) - - expect(ensure('0.1.0+bbb')).toBe(RELAY_NATIVE_CACHE_MISS) - expect(existsSync(join(relayDir('0.1.0+bbb'), 'node_modules'))).toBe(false) - }) - - it('refuses to seed from a sibling that had node-pty skipped', () => { - makePrivateInstall('0.1.0+aaa') - rmSync(join(relayDir('0.1.0+aaa'), 'node_modules', 'node-pty'), { recursive: true }) - mkdirSync(relayDir('0.1.0+bbb'), { recursive: true }) - - expect(ensure('0.1.0+bbb')).toBe(RELAY_NATIVE_CACHE_MISS) - }) - - it('leaves a directory that installed for itself alone', () => { - makePrivateInstall('0.1.0+aaa') - promote('0.1.0+aaa') - const own = makePrivateInstall('0.1.0+bbb') - - expect(ensure('0.1.0+bbb')).toBe(RELAY_NATIVE_CACHE_MISS) - expect(lstatSync(join(own, 'node_modules')).isSymbolicLink()).toBe(false) - }) - - it('elects exactly one publisher and leaves the loser its own tree', () => { - makePrivateInstall('0.1.0+aaa') - makePrivateInstall('0.1.0+bbb') - - expect(promote('0.1.0+aaa')).toBe(RELAY_NATIVE_CACHE_PROMOTED) - expect(promote('0.1.0+bbb')).toBe(RELAY_NATIVE_CACHE_NOT_PROMOTED) - - // The loser must not have handed its tree to an entry it lost the race for. - const loser = join(relayDir('0.1.0+bbb'), 'node_modules') - expect(lstatSync(loser).isSymbolicLink()).toBe(false) - expect(statSync(join(loser, 'node-pty', 'build', 'pty.node')).isFile()).toBe(true) - }) - - it('never republishes through a symlink it already holds', () => { - makePrivateInstall('0.1.0+aaa') - promote('0.1.0+aaa') - - expect(promote('0.1.0+aaa')).toBe(RELAY_NATIVE_CACHE_NOT_PROMOTED) - expect(statSync(join(relayDir('0.1.0+aaa'), 'node_modules', 'node-pty')).isDirectory()).toBe( - true - ) - }) - - it('survives removing a linked relay directory without touching the shared tree', () => { - makePrivateInstall('0.1.0+aaa') - promote('0.1.0+aaa') - mkdirSync(relayDir('0.1.0+bbb'), { recursive: true }) - ensure('0.1.0+bbb') - - // Exactly what version GC does to an idle directory. - sh(`rm -rf ${JSON.stringify(relayDir('0.1.0+bbb'))}`) - - const target = relayNativeDepsCacheNodeModulesPath(HOST, home, KEY) - expect(statSync(join(target, 'node-pty', 'build', 'pty.node')).isFile()).toBe(true) - }) - it('reports the published entry and every symlink that references it', () => { - makePrivateInstall('0.1.0+aaa') - promote('0.1.0+aaa') + makeLegacyCache('0.1.0+aaa') const entries = sh(listRelayNativeDepsCacheEntriesCommand(HOST, home)).trim().split('\n') expect(entries).toEqual([`ENTRY ${KEY}`, RELAY_NATIVE_CACHE_LIST_OK]) @@ -221,8 +62,7 @@ describe.runIf(process.platform !== 'win32').each(SHELLS)( }) it('reports a symlink no Orca version wrote, so GC can refuse the pass', () => { - makePrivateInstall('0.1.0+aaa') - promote('0.1.0+aaa') + makeLegacyCache('0.1.0+aaa') mkdirSync(relayDir('0.1.0+bbb'), { recursive: true }) symlinkSync('../relay-0.1.0+aaa/node_modules', join(relayDir('0.1.0+bbb'), 'node_modules')) diff --git a/src/main/ssh/ssh-relay-native-deps-cache.test.ts b/src/main/ssh/ssh-relay-native-deps-cache.test.ts index 7fee587b3ec6..5ab4b7acf16e 100644 --- a/src/main/ssh/ssh-relay-native-deps-cache.test.ts +++ b/src/main/ssh/ssh-relay-native-deps-cache.test.ts @@ -1,7 +1,3 @@ -// The cache is shared across every relay directory on a host, so these cover the two things that -// make sharing safe: the key changes when the tree's inputs change, and GC refuses to delete on -// anything short of a complete, attributable reference listing. - import { beforeEach, describe, expect, it, vi } from 'vitest' vi.mock('./ssh-relay-deploy-helpers', () => ({ @@ -12,15 +8,12 @@ import { execCommand } from './ssh-relay-deploy-helpers' import type { SshConnection } from './ssh-connection' import { getRemoteHostPlatform } from './ssh-remote-platform' import { - computeRelayNativeDepsCacheKey, isRelayNativeDepsCacheEntryName, relayNativeDepsCacheEntryDir, relayNativeDepsCacheNodeModulesPath, supportsRelayNativeDepsCache } from './ssh-relay-native-deps-cache' import { - ensureRelayNativeDepsCacheCommand, - promoteRelayNativeDepsCacheCommand, RELAY_NATIVE_CACHE_LIST_OK, RELAY_NATIVE_CACHE_REFS_ERR, RELAY_NATIVE_CACHE_REFS_OK @@ -30,9 +23,7 @@ import { gcRelayNativeDepsCache } from './ssh-relay-native-deps-cache-gc' const POSIX = getRemoteHostPlatform('linux-x64') const WINDOWS = getRemoteHostPlatform('win32-x64') const HOME = '/home/u' -const DEPS = { 'node-pty': '1.1.0', '@parcel/watcher': '2.5.6' } as const -const KEY = computeRelayNativeDepsCacheKey({ platform: 'linux-x64', deps: DEPS }) -const RELAY_DIR = `${HOME}/.orca-remote/relay-0.1.0+aaa` +const KEY = 'linux-x64-99355f00e9e557a3' const conn = {} as SshConnection const mockExec = vi.mocked(execCommand) @@ -45,112 +36,21 @@ function listing(...keys: string[]): string { return [...keys.map((k) => `ENTRY ${k}`), RELAY_NATIVE_CACHE_LIST_OK].join('\n') } -describe('computeRelayNativeDepsCacheKey', () => { - it('keys on the dependency set, not on the relay bundle', () => { - expect(computeRelayNativeDepsCacheKey({ platform: 'linux-x64', deps: DEPS })).toBe(KEY) - expect( - computeRelayNativeDepsCacheKey({ - platform: 'linux-x64', - deps: { '@parcel/watcher': '2.5.6', 'node-pty': '1.1.0' } - }) - ).toBe(KEY) - }) - - it('mints a new entry when a dependency version moves', () => { - expect( - computeRelayNativeDepsCacheKey({ - platform: 'linux-x64', - deps: { ...DEPS, 'node-pty': '1.2.0' } - }) - ).not.toBe(KEY) - }) - - it('mints a new entry when a patch applied to the tree changes', () => { - const withPatch = computeRelayNativeDepsCacheKey({ - platform: 'linux-x64', - deps: DEPS, - patchSources: [{ filename: 'node-pty-1.1.0-patch.cjs', contents: 'a' }] - }) - const withChangedPatch = computeRelayNativeDepsCacheKey({ - platform: 'linux-x64', - deps: DEPS, - patchSources: [{ filename: 'node-pty-1.1.0-patch.cjs', contents: 'b' }] - }) - expect(withPatch).not.toBe(KEY) - expect(withChangedPatch).not.toBe(withPatch) - }) - - it('separates platforms so one host never links another architecture', () => { - expect(computeRelayNativeDepsCacheKey({ platform: 'linux-arm64', deps: DEPS })).not.toBe(KEY) - expect(KEY.startsWith('linux-x64-')).toBe(true) - }) - - it('refuses a platform it cannot recognise rather than building a path from it', () => { - expect(() => computeRelayNativeDepsCacheKey({ platform: '../../etc', deps: DEPS })).toThrow( +describe('historical native cache identities', () => { + it('recognizes old keys and rejects unsafe directory names', () => { + expect(isRelayNativeDepsCacheEntryName(KEY)).toBe(true) + expect(isRelayNativeDepsCacheEntryName('../../etc')).toBe(false) + expect(() => relayNativeDepsCacheEntryDir(POSIX, HOME, '../../etc')).toThrow( /Unsafe relay native-deps cache key/ ) - expect(isRelayNativeDepsCacheEntryName('../../etc')).toBe(false) - expect(isRelayNativeDepsCacheEntryName(KEY)).toBe(true) }) - it('leaves Windows on the per-directory install', () => { + it('preserves the POSIX-only cache boundary', () => { expect(supportsRelayNativeDepsCache(POSIX)).toBe(true) expect(supportsRelayNativeDepsCache(WINDOWS)).toBe(false) }) }) -describe('ensureRelayNativeDepsCacheCommand', () => { - const command = ensureRelayNativeDepsCacheCommand( - { host: POSIX, remoteHome: HOME, relayDir: RELAY_DIR, key: KEY }, - DEPS - ) - - it('links only an entry that carries the completion sentinel', () => { - expect(command).toContain(`[ -f "$cache/.deps-complete" ]`) - expect(command).toContain('ln -s "$target" "$nm"') - expect(command).toContain(relayNativeDepsCacheNodeModulesPath(POSIX, HOME, KEY)) - }) - - it('never overwrites a directory the relay installed for itself', () => { - // The link is only created on a path that does not exist; a real node_modules reads as a miss. - expect(command).toContain('if [ ! -e "$nm" ] && ln -s "$target" "$nm"') - }) - - it('seeds only from a sibling whose manifest pins the same versions', () => { - for (const [name, version] of Object.entries(DEPS)) { - expect(command).toContain(`grep -F -q '"${name}":"${version}"' "$pj"`) - } - expect(command).toContain('[ -d "$cand/node-pty" ] || continue') - }) -}) - -describe('promoteRelayNativeDepsCacheCommand', () => { - const command = promoteRelayNativeDepsCacheCommand({ - host: POSIX, - remoteHome: HOME, - relayDir: RELAY_DIR, - key: KEY - }) - - it('elects one publisher with mkdir rather than a lock', () => { - expect(command).toContain('mkdir "$cache" 2>/dev/null') - }) - - it('writes the completion sentinel after the symlink exists', () => { - expect(command.indexOf('ln -s "$target" "$nm"')).toBeLessThan( - command.indexOf(': > "$cache/.deps-complete"') - ) - }) - - it('never deletes the entry unless the tree made it back to the relay directory', () => { - expect(command).toContain('if mv "$target" "$nm" 2>/dev/null; then rm -rf "$cache"') - }) - - it('refuses to publish a directory that is already a shared symlink', () => { - expect(command).toContain('if [ -L "$nm" ]; then') - }) -}) - describe('gcRelayNativeDepsCache', () => { beforeEach(() => { mockExec.mockReset().mockResolvedValue('') diff --git a/src/main/ssh/ssh-relay-native-deps-cache.ts b/src/main/ssh/ssh-relay-native-deps-cache.ts index 79f04221c010..069ed225a23b 100644 --- a/src/main/ssh/ssh-relay-native-deps-cache.ts +++ b/src/main/ssh/ssh-relay-native-deps-cache.ts @@ -1,36 +1,4 @@ -/** - * Where the relay's compiled native dependencies live, and what their identity is keyed on. - * - * A relay install directory is keyed on the JS bundle hash, which moves on every commit to - * `src/relay/` or the `src/shared/` it pulls in. `node_modules` used to live inside it, so a - * dependency set that is a pinned constant (`RELAY_NATIVE_DEPS`) was reinstalled — and on Linux, - * where node-pty ships no prebuild, recompiled from source — on every new bundle (#18009). The - * directory key was coupled to the wrong quantity. - * - * The tree now lives at `~/.orca-remote/native/-/node_modules` and each - * relay directory holds a symlink to it. Three rules make one tree safe to share: - * - * 1. **A published entry is immutable.** `.deps-complete` is written last, only after a probe on - * this host loaded both addons. Nothing installs, rebuilds or resets into a published entry: a - * repair detaches the symlink and installs privately, so a host with a broken toolchain can - * never `rm -rf node_modules/node-pty` out from under a live relay that shares the tree. - * 2. **Publication elects one winner with `mkdir`.** The entry either does not exist (this deploy - * builds it privately and promotes it) or is already complete (this deploy links it). There is - * no window in which two deploys write one tree, so no client-side lock is needed. - * 3. **Every failure degrades to today's per-directory install.** A host that cannot symlink, - * cannot create the directory, or answers nothing still deploys, one bundle at a time. - * - * Windows is deliberately excluded. node-pty's npm tarball ships win32 prebuilts, so there is no - * compile to avoid there, and `node-pty-1.1.0-console-list-agent-patch.cjs` mutates the installed - * tree in place — which rule 1 forbids for a shared one. - * - * Linux's `node-pty-1.1.0-master-cloexec-patch.cjs` also mutates in place, but it stays inside rule - * 1: the deploy path runs it before promotion, and returns early on a linked entry, so it only ever - * touches a private tree. Its bytes are in the key, so a patched build never links a pre-patch - * entry -- and a tree whose patch was refused or rolled back is not promoted at all, because under - * that same key it would publish the leak to every later host on the machine. - */ -import { createHash } from 'node:crypto' +/** Historical cache identities remain recognizable while older relay owners may use them. */ import { RELAY_REMOTE_DIR } from './relay-protocol' import { RELAY_BUILD_PLATFORMS } from '../../shared/relay-artifacts' import { isWindowsRemoteHost, joinRemotePath, type RemoteHostPlatform } from './ssh-remote-platform' @@ -45,61 +13,12 @@ export const RELAY_NATIVE_DEPS_CACHE_COMPLETE_NAME = '.deps-complete' export const RELAY_NATIVE_DEPS_CACHE_TOMBSTONE_PREFIX = '.native-gc-' export const LEGACY_RELAY_NATIVE_DEPS_CACHE_TOMBSTONE_PREFIX = '.gc-tombstone.' -/** - * Bump when the remote install starts mutating the installed tree in a way the hashed inputs - * below cannot see — a new `npm rebuild` flag, a new post-install step, a patch applied by - * something other than a shipped `node-pty-*` artifact. A published entry is never repaired in - * place; only a new key retires it. - */ -export const RELAY_NATIVE_DEPS_CACHE_EPOCH = 1 - -/** - * Shipped relay artifacts that patch the installed native tree. Their bytes go into the key, so - * changing a patch mints a new entry instead of leaving hosts on a tree built from the old one. - */ -export const RELAY_NATIVE_DEPS_PATCH_ARTIFACT_PATTERN = /^node-pty-.*\.(cjs|js|patch)$/ - const CACHE_KEY_HASH_LENGTH = 16 const CACHE_ENTRY_NAME_REGEX = new RegExp( `^(${RELAY_BUILD_PLATFORMS.join('|')})-[0-9a-f]{${CACHE_KEY_HASH_LENGTH}}$` ) -export type RelayNativeDepsCachePatchSource = { - filename: string - contents: string -} - -/** - * `-` over the dependency set, the epoch, and every patch the - * remote install applies. Platform and arch stay in the name rather than the hash so an operator - * reading `~/.orca-remote/native/` can tell what an entry is for. - */ -export function computeRelayNativeDepsCacheKey(input: { - platform: string - deps: Readonly> - patchSources?: readonly RelayNativeDepsCachePatchSource[] -}): string { - const hash = createHash('sha256') - hash.update(`epoch ${RELAY_NATIVE_DEPS_CACHE_EPOCH}\n`) - for (const [name, version] of Object.entries(input.deps).sort(([a], [b]) => (a < b ? -1 : 1))) { - hash.update(`dep ${name} ${version}\n`) - } - const patches = [...(input.patchSources ?? [])].sort((a, b) => (a.filename < b.filename ? -1 : 1)) - for (const patch of patches) { - hash.update( - `patch ${patch.filename} ${createHash('sha256').update(patch.contents).digest('hex')}\n` - ) - } - const key = `${input.platform}-${hash.digest('hex').slice(0, CACHE_KEY_HASH_LENGTH)}` - if (!isRelayNativeDepsCacheEntryName(key)) { - // Why: the key reaches the host inside `mv` and `rm -rf`; an unrecognized platform must - // disable the cache rather than arrive as a path fragment nobody validated. - throw new Error(`Unsafe relay native-deps cache key: ${JSON.stringify(key)}`) - } - return key -} - /** * Whether a name the host listed is one this client may move or delete. Every GC candidate goes * through here before it reaches a shell. @@ -143,10 +62,7 @@ export function relayNativeDepsCacheNodeModulesPath( return joinRemotePath(host, relayNativeDepsCacheEntryDir(host, remoteHome, key), 'node_modules') } -/** - * Windows hosts install node-pty from an npm prebuilt and then patch the tree in place, so they - * keep the per-directory install. Nothing else about their deploy changes. - */ +/** Older Windows relays never used the shared cache. */ export function supportsRelayNativeDepsCache(host: RemoteHostPlatform): boolean { return !isWindowsRemoteHost(host) } diff --git a/src/main/ssh/ssh-relay-offline-node-headers.docker.test.ts b/src/main/ssh/ssh-relay-offline-node-headers.docker.test.ts deleted file mode 100644 index c5700c991c19..000000000000 --- a/src/main/ssh/ssh-relay-offline-node-headers.docker.test.ts +++ /dev/null @@ -1,204 +0,0 @@ -// Why this exists (STA-6674): a Linux host whose only unreachable endpoint is nodejs.org could -// not run a relay. node-pty ships no Linux prebuild, so npm hands it to node-gyp, and node-gyp -// downloads `node-v-headers.tar.gz` unless told the host already has the headers -- which -// every official Node install does, at `/include/node`. This drives the real deploy at a -// Docker sshd whose nodejs.org resolves to 127.0.0.1 (ECONNREFUSED, exactly what the user saw). -// -// Run: ORCA_REVIEW_SSH_OFFLINE_HEADERS=1 pnpm test src/main/ssh/ssh-relay-offline-node-headers.docker.test.ts -// Needs Docker and `pnpm build:relay`. ORCA_REVIEW_SSH_NODE_IMAGE picks the Node image -// (default node:24.12.0-bookworm, the user's version); ORCA_REVIEW_SSH_TARGET_HOST overrides -// the address the app connects to (default 127.0.0.1). -import { execFileSync, spawnSync } from 'node:child_process' -import { randomUUID } from 'node:crypto' -import { mkdtempSync, readFileSync, rmSync } from 'node:fs' -import { connect } from 'node:net' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' - -vi.mock('electron', () => ({ app: { getAppPath: () => process.cwd() } })) - -import { SshConnection } from './ssh-connection' -import { deployAndLaunchRelay } from './ssh-relay-deploy' -import type { SshTarget } from '../../shared/ssh-types' - -const RUN_REVIEW_ORACLE = process.env.ORCA_REVIEW_SSH_OFFLINE_HEADERS === '1' -const NODE_IMAGE = process.env.ORCA_REVIEW_SSH_NODE_IMAGE ?? 'node:24.12.0-bookworm' -const TARGET_HOST = process.env.ORCA_REVIEW_SSH_TARGET_HOST ?? '127.0.0.1' - -type TargetFixture = { - containerName: string - identityFile: string - port: number - tempDir: string -} - -function run(command: string, args: string[], timeout = 30_000, input?: string): string { - return execFileSync(command, args, { - encoding: 'utf8', - stdio: [input === undefined ? 'ignore' : 'pipe', 'pipe', 'pipe'], - timeout, - input - }).trim() -} - -function dockerExec(fixture: TargetFixture, command: string): string { - return run('docker', ['exec', fixture.containerName, 'bash', '-lc', command], 60_000) -} - -async function startTarget(): Promise { - const image = `orca-review-offline-headers:${NODE_IMAGE.replace(/[^A-Za-z0-9_.-]/g, '-')}` - run( - 'docker', - ['build', '-q', '-t', image, '-'], - 600_000, - [ - `FROM ${NODE_IMAGE}`, - 'RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends openssh-server git && rm -rf /var/lib/apt/lists/* && mkdir -p /run/sshd /root/.ssh && chmod 700 /root/.ssh', - '' - ].join('\n') - ) - const tempDir = mkdtempSync(join(tmpdir(), 'orca-offline-headers-ssh-')) - const identityFile = join(tempDir, 'id_ed25519') - run('ssh-keygen', ['-t', 'ed25519', '-N', '', '-f', identityFile, '-q']) - const publicKey = readFileSync(`${identityFile}.pub`, 'utf8').trim() - const containerName = `orca-offline-headers-${randomUUID().slice(0, 12)}` - // Why a refused connection and not a dropped one: a timeout takes node-gyp's retry path and - // burns the deploy budget; the user's host refused, and that is the path under test. - run( - 'docker', - [ - 'run', - '-d', - '--name', - containerName, - '--add-host', - 'nodejs.org:127.0.0.1', - '-p', - '0.0.0.0::22', - '-e', - `AUTHORIZED_KEY=${publicKey}`, - image, - 'bash', - '-lc', - 'printf "%s\\n" "$AUTHORIZED_KEY" > /root/.ssh/authorized_keys && chmod 600 /root/.ssh/authorized_keys && exec /usr/sbin/sshd -D -e' - ], - 120_000 - ) - const port = Number(run('docker', ['port', containerName, '22/tcp']).split(':').at(-1)) - // `docker run -d` returns before sshd binds; connect() against a closed port is a flake. - await waitForSshBanner(port) - return { containerName, identityFile, port, tempDir } -} - -/** Resolves once sshd answers with its banner on the mapped port, or throws after the deadline. */ -async function waitForSshBanner(port: number, deadlineMs = 60_000): Promise { - const deadline = Date.now() + deadlineMs - for (;;) { - const gotBanner = await new Promise((resolve) => { - const socket = connect({ host: TARGET_HOST, port }) - const done = (value: boolean): void => { - socket.destroy() - resolve(value) - } - socket.setTimeout(2_000, () => done(false)) - socket.once('data', (chunk) => done(chunk.toString('utf8').startsWith('SSH-'))) - socket.once('error', () => done(false)) - }) - if (gotBanner) { - return - } - if (Date.now() > deadline) { - throw new Error(`sshd on port ${port} did not answer within ${deadlineMs / 1000}s`) - } - await new Promise((resolve) => setTimeout(resolve, 500)) - } -} - -function stopTarget(fixture: TargetFixture | null): void { - if (!fixture) { - return - } - spawnSync('docker', ['rm', '-f', fixture.containerName], { stdio: 'ignore', timeout: 30_000 }) - rmSync(fixture.tempDir, { recursive: true, force: true }) -} - -function createConnection(fixture: TargetFixture): SshConnection { - const target: SshTarget = { - id: `offline-headers-${randomUUID()}`, - label: 'Offline node headers Docker SSH target', - source: 'manual', - host: TARGET_HOST, - port: fixture.port, - username: 'root', - identityFile: fixture.identityFile, - identitiesOnly: true - } - return new SshConnection(target, { onStateChange: vi.fn() }) -} - -describe.skipIf(!RUN_REVIEW_ORACLE)( - 'SSH relay deploy on a host that cannot reach nodejs.org', - () => { - let fixture: TargetFixture | null = null - - beforeAll(async () => { - fixture = await startTarget() - }, 900_000) - - afterAll(() => { - stopTarget(fixture) - }) - - it('compiles node-pty from the host Node install headers instead of downloading them', async () => { - const activeFixture = fixture as TargetFixture - expect(dockerExec(activeFixture, 'getent hosts nodejs.org')).toContain('127.0.0.1') - const connection = createConnection(activeFixture) - await connection.connect() - try { - const result = await deployAndLaunchRelay(connection, undefined, 60) - expect(result.remoteRelayDir).toBeTruthy() - - const evidence = dockerExec( - activeFixture, - [ - `cd '${result.remoteRelayDir}'`, - 'test -f node_modules/node-pty/build/Release/pty.node && echo PTY_NODE=built', - 'test -d /root/.cache/node-gyp && echo HEADERS=downloaded || echo HEADERS=local', - `node -e "require('node-pty'); require('@parcel/watcher'); console.log('NATIVE=loadable')"` - ].join('; ') - ) - console.log(`[offline-node-headers] ${NODE_IMAGE}: ${evidence.replace(/\n/g, ' ')}`) - expect(evidence).toContain('PTY_NODE=built') - expect(evidence).toContain('HEADERS=local') - expect(evidence).toContain('NATIVE=loadable') - } finally { - await connection.disconnect() - } - }, 600_000) - - it('names the missing-local-headers cause, not an Orca defect, when the host ships no headers', async () => { - // Same offline host, headers removed and the relay uninstalled so the deploy compiles again. - // This is the shape a review found misreported: the exec-failure message quotes the whole - // command (marker echo included) ahead of the output, and the parser must not read that copy. - const activeFixture = fixture as TargetFixture - dockerExec( - activeFixture, - 'rm -rf /usr/local/include/node /root/.orca-remote /root/.cache/node-gyp' - ) - const connection = createConnection(activeFixture) - await connection.connect() - try { - const error = await deployAndLaunchRelay(connection, undefined, 60).catch((e: Error) => e) - expect(error).toBeInstanceOf(Error) - const message = (error as Error).message - console.log(`[offline-node-headers] ${NODE_IMAGE} no-headers: ${message.split('\n')[0]}`) - expect(message).toContain('no local headers matching its own version') - expect(message).not.toContain('Orca defect') - expect(message).toContain('ECONNREFUSED') - } finally { - await connection.disconnect() - } - }, 600_000) - } -) diff --git a/src/main/ssh/ssh-relay-session.ts b/src/main/ssh/ssh-relay-session.ts index b739fbd3d8ce..edeaaf02f30c 100644 --- a/src/main/ssh/ssh-relay-session.ts +++ b/src/main/ssh/ssh-relay-session.ts @@ -1,6 +1,12 @@ /* oxlint-disable max-lines */ // Why: single authority for all relay lifecycle state per SSH target (previously scattered across module Maps/Sets with duplicated paths). +import { + invalidateRelayPtySourceRecovery, + migrateRelayPtySourceGeneration, + relayPtySourceRecoveryRequest +} from '../providers/relay-pty-source-recovery' +import { registerRelayPtySourceCredit } from '../providers/relay-pty-source-credit-registration' import { randomUUID } from 'node:crypto' import type { BrowserWindow } from 'electron' import { deployAndLaunchRelay } from './ssh-relay-deploy' @@ -56,11 +62,7 @@ import { allocateSshPtyProviderGeneration, applySshPtySourceCancellationProof, applySshPtySourceRecoveryCancellationProof, - beginSshPtyOutputGenerationMigration, - closeSshPtyOutputGeneration, - getSshPtyAcceptedSourceCheckpoints, - installSshPtySourceAckPublisher, - installSshPtySourceCancellationPublisher + closeSshPtyOutputGeneration } from '../ipc/ssh-pty-output-intake-registry' import { registerSshFilesystemProvider, @@ -1132,42 +1134,11 @@ export class SshRelaySession { this.sourceCancellationPublisherCleanup?.() this.sourceCancellationPublisherCleanup = null if (consumerOwnerState?.outputFlowControl) { - this.sourceAckPublisherCleanup = installSshPtySourceAckPublisher( - providerGeneration, - // ACK delivery is idempotent and re-derived from credit state, so it consumes - // the two-valued projection of the write settlement rather than the three arms. - (batch, onSettled) => - mux.notifyWithSettlement( - 'pty.ackData', - batch as unknown as Record, - (settlement) => - onSettled( - settlement.outcome === 'accepted' - ? { ok: true } - : { ok: false, error: settlement.error } - ) - ) - ) - this.sourceCancellationPublisherCleanup = installSshPtySourceCancellationPublisher( - providerGeneration, - async (request) => { - const result = (await mux.request('pty.cancelDelivery', { - ...request, - id: toRelaySshPtyId(this.targetId, request.id) - })) as Record - if ( - result.canceled !== true || - !Number.isSafeInteger(result.sentEndSu) || - !Number.isSafeInteger(result.creditedEndSu) - ) { - throw new Error('ssh_source_cancellation_proof_invalid') - } - return { - sentEndSu: result.sentEndSu as number, - creditedEndSu: result.creditedEndSu as number - } - } + const registration = registerRelayPtySourceCredit(mux, providerGeneration, (id) => + toRelaySshPtyId(this.targetId, id) ) + this.sourceAckPublisherCleanup = registration.releaseAck + this.sourceCancellationPublisherCleanup = registration.releaseCancellation } this.activePtyProviderGeneration = providerGeneration registerSshPtyProvider(this.targetId, ptyProvider) @@ -1326,18 +1297,7 @@ export class SshRelaySession { return } delete recovery.owner - recovery.checkpointsByAppPtyId.clear() - for (const [ptyId, migration] of recovery.modelMigrationsByAppPtyId) { - recovery.modelMigrationsByAppPtyId.set( - ptyId, - migration.then(() => - Object.freeze({ - status: 'checkpoint-unavailable' as const, - reason: 'completion-failed' as const - }) - ) - ) - } + invalidateRelayPtySourceRecovery(recovery) } private async rememberPtyConsumerRecovery(serverBuildId: string | undefined): Promise { @@ -2958,65 +2918,19 @@ export class SshRelaySession { if (!this.activePtyConsumerOwner()?.outputFlowControl) { return undefined } - const recovery = getSshPtyConsumerRecovery(this.targetId) - const migration = recovery?.modelMigrationsByAppPtyId.get(appPtyId) - if (migration) { - const outcome = await migration - if (recovery?.modelMigrationsByAppPtyId.get(appPtyId) === migration) { - recovery.modelMigrationsByAppPtyId.delete(appPtyId) - } - if (outcome.status !== 'settled') { - return Object.freeze({ status: 'checkpointUnavailable' }) - } - } - const checkpoints = recovery?.checkpointsByAppPtyId - const relayPtyId = toRelaySshPtyId(this.targetId, appPtyId) - // Why: every checkpoint writer records app-id keys now, so the relay-id - // lookup (and its paired delete below) is a legacy guard only. - const checkpoint = checkpoints?.get(appPtyId) ?? checkpoints?.get(relayPtyId) - if (!checkpoint) { - return Object.freeze({ status: 'checkpointUnavailable' }) - } - return Object.freeze({ - status: 'checkpoint', - clientGeneration: checkpoint.clientGeneration, - ownerGeneration: checkpoint.ownerGeneration, - ptyIncarnation: checkpoint.ptyIncarnation, - deliveryToken: checkpoint.deliveryToken, - acceptedSourceEndSu: checkpoint.acceptedSourceEndSu - }) + return relayPtySourceRecoveryRequest( + getSshPtyConsumerRecovery(this.targetId), + appPtyId, + toRelaySshPtyId(this.targetId, appPtyId) + ) } private beginPtyModelMigration(providerGeneration: number, closeReason: string): void { - const recovery = getSshPtyConsumerRecovery(this.targetId) - if (!recovery) { - closeSshPtyOutputGeneration(providerGeneration, closeReason) - return - } - for (const checkpoint of getSshPtyAcceptedSourceCheckpoints(providerGeneration)) { - recovery.checkpointsByAppPtyId.set(checkpoint.id, checkpoint) - } - const migration = beginSshPtyOutputGenerationMigration(providerGeneration) - for (const [ptyId, result] of migration.byPty) { - const previous = recovery.modelMigrationsByAppPtyId.get(ptyId) - const fence = previous ? previous.then(() => result) : result - recovery.modelMigrationsByAppPtyId.set(ptyId, fence) - void fence.then((outcome) => { - const current = getSshPtyConsumerRecovery(this.targetId) - if (current?.modelMigrationsByAppPtyId.get(ptyId) !== fence) { - return - } - if (outcome.status === 'settled') { - current.checkpointsByAppPtyId.set(ptyId, outcome.checkpoint) - } else { - current.checkpointsByAppPtyId.delete(ptyId) - current.checkpointsByAppPtyId.delete(toRelaySshPtyId(this.targetId, ptyId)) - } - current.modelMigrationsByAppPtyId.delete(ptyId) - }) - } - void migration.completion.then(() => { - closeSshPtyOutputGeneration(providerGeneration, closeReason) + migrateRelayPtySourceGeneration({ + generation: providerGeneration, + closeReason, + getRecovery: () => getSshPtyConsumerRecovery(this.targetId), + toRelayPtyId: (id) => toRelaySshPtyId(this.targetId, id) }) } diff --git a/src/main/startup/codex-launch-preparation.test.ts b/src/main/startup/codex-launch-preparation.test.ts new file mode 100644 index 000000000000..35ae9a8ffd90 --- /dev/null +++ b/src/main/startup/codex-launch-preparation.test.ts @@ -0,0 +1,121 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const mocks = vi.hoisted(() => ({ + prepare: vi.fn(), + realHomeSelected: vi.fn(), + hooks: vi.fn(), + realHooks: vi.fn(), + settings: { agentStatusHooksEnabled: true }, + trust: vi.fn() +})) +vi.mock('electron', () => ({ app: { getPath: () => '/profile' } })) +vi.mock('./main-process-state', () => ({ + mainProcessState: { + codexRuntimeHome: { + prepareForCodexLaunchAsync: mocks.prepare, + isHostSystemDefaultRealHomeSelected: mocks.realHomeSelected + }, + store: { getSettings: () => mocks.settings } + } +})) +vi.mock('../agent-trust-presets', () => ({ markCodexProjectTrusted: mocks.trust })) +vi.mock('../codex/hook-service', () => ({ + codexHookService: { prepareRuntimeHomeForLaunch: mocks.hooks } +})) +vi.mock('../codex/codex-real-home-hook-install', () => ({ + ensureRealHomeCodexHookState: mocks.realHooks +})) +vi.mock('../wsl', () => ({ getDefaultWslDistro: () => 'wrong-default' })) +vi.mock('../agent-hooks/managed-agent-hook-controls', () => ({ + isAgentStatusHooksEnabled: (settings: { agentStatusHooksEnabled: boolean }) => + settings.agentStatusHooksEnabled +})) + +import { prepareCodexRuntimeHomeForLaunch } from './codex-launch-preparation' + +const owner = { distro: 'Ubuntu', userName: 'alice', userId: '1000', home: '/home/alice' } +beforeEach(() => { + vi.clearAllMocks() + mocks.prepare.mockResolvedValue('/home/alice/.codex') + mocks.realHomeSelected.mockReturnValue(false) + mocks.hooks.mockResolvedValue({ state: 'installed' }) + mocks.settings.agentStatusHooksEnabled = true +}) + +describe('Codex startup launch wrapper', () => { + it.each([true, false])( + 'leaves captured guest hooks to guest preparation, hooks enabled=%s', + async (enabled) => { + mocks.settings.agentStatusHooksEnabled = enabled + const execution = { ...owner } + mocks.prepare.mockImplementation(async () => { + execution.userName = 'bob' + execution.home = '/home/bob' + return '/home/alice/.codex' + }) + expect( + await prepareCodexRuntimeHomeForLaunch( + { runtime: 'wsl', wslDistro: 'Ubuntu' }, + {}, + { wslExecution: execution } + ) + ).toBe('/home/alice/.codex') + expect(mocks.prepare).toHaveBeenCalledWith( + { runtime: 'wsl', wslDistro: 'Ubuntu' }, + {}, + { unavailableManagedHomePath: undefined, wslExecution: owner } + ) + expect(Object.isFrozen(mocks.prepare.mock.calls[0]?.[2].wslExecution)).toBe(true) + expect(mocks.hooks).not.toHaveBeenCalled() + expect(mocks.realHooks).not.toHaveBeenCalled() + expect(mocks.trust).not.toHaveBeenCalled() + } + ) + + it('refuses a captured owner paired with a host or different distro before preparation', async () => { + await expect( + prepareCodexRuntimeHomeForLaunch({ runtime: 'host' }, {}, { wslExecution: owner }) + ).rejects.toThrow('captured execution owner') + await expect( + prepareCodexRuntimeHomeForLaunch( + { runtime: 'wsl', wslDistro: 'Debian' }, + {}, + { wslExecution: owner } + ) + ).rejects.toThrow('captured execution owner') + expect(mocks.prepare).not.toHaveBeenCalled() + expect(mocks.hooks).not.toHaveBeenCalled() + }) + + it.each([true, false])( + 'keeps legacy noncaptured hook preparation, hooks enabled=%s', + async (enabled) => { + mocks.settings.agentStatusHooksEnabled = enabled + await prepareCodexRuntimeHomeForLaunch({ runtime: 'wsl', wslDistro: 'Ubuntu' }) + expect(mocks.hooks).toHaveBeenCalledWith( + '/home/alice/.codex', + { runtime: 'wsl', wslDistro: 'Ubuntu' }, + enabled + ) + } + ) + + it('preserves host managed-home fallback and second preparation', async () => { + mocks.prepare.mockResolvedValueOnce(null).mockResolvedValueOnce('/managed') + mocks.realHomeSelected.mockReturnValueOnce(false).mockReturnValueOnce(true) + expect( + await prepareCodexRuntimeHomeForLaunch( + { runtime: 'host' }, + {}, + { unavailableManagedHomePath: '/old' } + ) + ).toBe('/managed') + expect(mocks.prepare).toHaveBeenCalledTimes(2) + expect(mocks.prepare).toHaveBeenLastCalledWith( + { runtime: 'host' }, + {}, + { unavailableManagedHomePath: '/old' } + ) + expect(mocks.realHooks).toHaveBeenCalledTimes(1) + }) +}) diff --git a/src/main/startup/codex-launch-preparation.ts b/src/main/startup/codex-launch-preparation.ts index 3b94c00b11d7..5be035de8334 100644 --- a/src/main/startup/codex-launch-preparation.ts +++ b/src/main/startup/codex-launch-preparation.ts @@ -1,3 +1,4 @@ +import { assertWslAccountExecutionTarget } from '../wsl/wsl-account-execution-context' import { app } from 'electron' import type { CodexHomeLaunchContext } from '../ipc/pty' import type { CodexAccountSelectionTarget } from '../codex-accounts/runtime-selection' @@ -13,6 +14,12 @@ export async function prepareCodexRuntimeHomeForLaunch( launchEnv?: NodeJS.ProcessEnv, launchContext?: CodexHomeLaunchContext ): Promise { + const wslExecution = launchContext?.wslExecution + ? Object.freeze({ ...launchContext.wslExecution }) + : undefined + if (wslExecution) { + assertWslAccountExecutionTarget(wslExecution, target) + } const runtimeHome = state.codexRuntimeHome if (!runtimeHome) { throw new Error('Codex runtime home service is not initialized') @@ -48,7 +55,8 @@ export async function prepareCodexRuntimeHomeForLaunch( // the fallbacks below all key off `null`, which means "system default", so // swallowing the refusal would launch the wrong account (#STA-4422). let runtimeHomePath = await runtimeHome.prepareForCodexLaunchAsync(target, launchEnv, { - unavailableManagedHomePath: launchContext?.unavailableManagedHomePath + unavailableManagedHomePath: launchContext?.unavailableManagedHomePath, + ...(wslExecution ? { wslExecution } : {}) }) if (runtimeHomePath === null && !realHomeHooksPrepared) { // Why: launch prep can reject an untrusted managed home and clear its @@ -57,7 +65,8 @@ export async function prepareCodexRuntimeHomeForLaunch( realHomeHooksPrepared = await ensureRealHomeHooksIfSelected() if (realHomeHooksPrepared) { runtimeHomePath = await runtimeHome.prepareForCodexLaunchAsync(target, launchEnv, { - unavailableManagedHomePath: launchContext?.unavailableManagedHomePath + unavailableManagedHomePath: launchContext?.unavailableManagedHomePath, + ...(wslExecution ? { wslExecution } : {}) }) } } @@ -66,6 +75,10 @@ export async function prepareCodexRuntimeHomeForLaunch( // install below would target a home Codex never reads on this lane. return null } + // Guest preparation installs hooks through the captured user’s relay and honors hooks-off. + if (wslExecution) { + return runtimeHomePath + } const hookTarget = target?.runtime === 'wsl' ? { runtime: 'wsl' as const, wslDistro: target.wslDistro?.trim() || getDefaultWslDistro() } diff --git a/src/main/startup/codex-session-resume-launch.test.ts b/src/main/startup/codex-session-resume-launch.test.ts new file mode 100644 index 000000000000..544f35567df4 --- /dev/null +++ b/src/main/startup/codex-session-resume-launch.test.ts @@ -0,0 +1,73 @@ +import { beforeEach, expect, it, vi } from 'vitest' +import { prepareCodexSessionResumeForLaunch } from './codex-session-resume-launch' +const mocks = vi.hoisted(() => ({ + prepare: vi.fn(), + settings: { + codexManagedAccounts: [], + activeCodexManagedAccountIdsByRuntime: { host: null, wsl: { Ubuntu: 'selected' } } + } +})) +vi.mock('electron', () => ({ app: { getPath: () => '/data' } })) +vi.mock('../codex/codex-wsl-session-resume', () => ({ + prepareCapturedWslCodexSessionResume: mocks.prepare +})) +vi.mock('./main-process-state', () => ({ + mainProcessState: { codexRuntimeHome: null, store: { getSettings: () => mocks.settings } } +})) +vi.mock('../codex/hook-service', () => ({ codexHookService: {} })) +vi.mock('../codex/codex-real-home-hook-install', () => ({ ensureRealHomeCodexHookState: vi.fn() })) +vi.mock('../codex/codex-legacy-session-resume', () => ({ + prepareLegacySharedCodexSessionResume: vi.fn() +})) +vi.mock('../agent-hooks/managed-agent-hook-controls', () => ({ + isAgentStatusHooksEnabled: vi.fn() +})) +vi.mock('../agent-trust-presets', () => ({ markCodexProjectTrusted: vi.fn() })) +vi.mock('../codex/codex-home-paths', () => ({ + getSystemCodexHomePath: vi.fn(), + getOrcaManagedCodexHomePath: vi.fn() +})) +const execution = { distro: 'Ubuntu', userName: 'alice', userId: '1000', home: '/home/alice' } +const providerSession = { key: 'session_id', id: '11111111-2222-3333-4444-555555555555' } as const +beforeEach(() => { + mocks.prepare.mockReset() +}) +it('preserves the existing WSL route when no captured execution identity is supplied', async () => { + expect( + await prepareCodexSessionResumeForLaunch({ + target: { runtime: 'wsl', wslDistro: 'Ubuntu' }, + providerSession + }) + ).toBeNull() + expect(mocks.prepare).not.toHaveBeenCalled() +}) +it('routes captured WSL resume through guest verification even without a host runtime home', async () => { + mocks.prepare.mockResolvedValue({ + outcome: 'resume', + codexHomePath: '/home/alice/account-origin' + }) + expect( + await prepareCodexSessionResumeForLaunch({ + wslExecution: execution, + target: { runtime: 'wsl', wslDistro: 'Ubuntu' }, + providerSession + }) + ).toEqual({ outcome: 'resume', codexHomePath: '/home/alice/account-origin' }) + expect(mocks.prepare).toHaveBeenCalledWith({ + execution, + target: { runtime: 'wsl', wslDistro: 'Ubuntu' }, + providerSession, + accounts: [], + selectedAccountId: 'selected' + }) +}) +it('propagates unverifiable guest ownership instead of silently using the selected account', async () => { + mocks.prepare.mockRejectedValue(new Error('owner changed')) + await expect( + prepareCodexSessionResumeForLaunch({ + wslExecution: execution, + target: { runtime: 'wsl', wslDistro: 'Ubuntu' }, + providerSession + }) + ).rejects.toThrow('owner changed') +}) diff --git a/src/main/startup/codex-session-resume-launch.ts b/src/main/startup/codex-session-resume-launch.ts index c537c4f75ef9..4c661a57ca49 100644 --- a/src/main/startup/codex-session-resume-launch.ts +++ b/src/main/startup/codex-session-resume-launch.ts @@ -1,3 +1,6 @@ +import { prepareCapturedWslCodexSessionResume } from '../codex/codex-wsl-session-resume' +import { getSelectedCodexAccountIdForTarget } from '../codex-accounts/runtime-selection' +import type { WslAccountExecutionContext } from '../wsl/wsl-account-execution-context' import { app } from 'electron' import type { AgentProviderSessionMetadata } from '../../shared/agent-session-resume' import type { CodexAccountSelectionTarget } from '../codex-accounts/runtime-selection' @@ -14,6 +17,7 @@ import { normalizeRuntimePathForComparison } from '../../shared/cross-platform-p import { mainProcessState as state } from './main-process-state' export async function prepareCodexSessionResumeForLaunch(args: { + wslExecution?: WslAccountExecutionContext providerSession: AgentProviderSessionMetadata target: CodexAccountSelectionTarget launchEnv?: NodeJS.ProcessEnv @@ -21,6 +25,19 @@ export async function prepareCodexSessionResumeForLaunch(args: { }): Promise { const runtimeHome = state.codexRuntimeHome const store = state.store + if (args.target.runtime === 'wsl' && args.wslExecution) { + if (!store) { + throw new Error('Codex session resume requires profile account state') + } + const settings = store.getSettings() + return prepareCapturedWslCodexSessionResume({ + execution: args.wslExecution, + target: args.target, + providerSession: args.providerSession, + accounts: settings.codexManagedAccounts, + selectedAccountId: getSelectedCodexAccountIdForTarget(settings, args.target) + }) + } if (args.target.runtime === 'wsl' || !runtimeHome || !store) { return null } diff --git a/src/main/startup/first-window-startup-services.test.ts b/src/main/startup/first-window-startup-services.test.ts index 728fd5c41b93..987998f4c0ba 100644 --- a/src/main/startup/first-window-startup-services.test.ts +++ b/src/main/startup/first-window-startup-services.test.ts @@ -143,8 +143,7 @@ describe('startFirstWindowStartupServices', () => { await vi.advanceTimersByTimeAsync(FIRST_WINDOW_STARTUP_SERVICE_TIMEOUT_MS) await expect(started.firstWindowReady).resolves.toBeUndefined() - // Why: opening the PTY gate before the daemon attempt finishes would - // spawn non-restorable LocalPtyProvider fallback terminals (#5232). + // Restored terminals wait for daemon authority while startup is still within its budget. expect(ptyGateOpened).toBe(false) expect(daemonSignal?.aborted).toBe(false) expect(onDaemonError).not.toHaveBeenCalled() @@ -159,17 +158,25 @@ describe('startFirstWindowStartupServices', () => { } }) - it('fails open the local PTY gate at the hard cap while aborting hung services', async () => { + it('opens the hard-cap gate while allowing a late daemon to become ready', async () => { vi.useFakeTimers() const onDaemonError = vi.fn() const onAgentHookServerError = vi.fn() let daemonSignal: AbortSignal | undefined + let finishDaemon!: () => void + const installed = vi.fn() try { const started = startFirstWindowStartupServices({ startDaemonPtyProvider: (signal) => { daemonSignal = signal - return new Promise(() => {}) + return new Promise((resolve) => { + finishDaemon = resolve + }).then(() => { + if (!signal.aborted) { + installed() + } + }) }, startAgentHookServer: () => Promise.resolve(), onDaemonError, @@ -184,7 +191,10 @@ describe('startFirstWindowStartupServices', () => { expect(onDaemonError).toHaveBeenCalledWith(expect.any(Error)) expect(onAgentHookServerError).not.toHaveBeenCalled() - expect(daemonSignal?.aborted).toBe(true) + expect(daemonSignal?.aborted).toBe(false) + finishDaemon() + await vi.advanceTimersByTimeAsync(0) + expect(installed).toHaveBeenCalledOnce() } finally { vi.useRealTimers() } diff --git a/src/main/startup/first-window-startup-services.ts b/src/main/startup/first-window-startup-services.ts index 407ba23f5555..9f464021c439 100644 --- a/src/main/startup/first-window-startup-services.ts +++ b/src/main/startup/first-window-startup-services.ts @@ -17,17 +17,14 @@ type FirstWindowStartupServicesResult = { } export const FIRST_WINDOW_STARTUP_SERVICE_TIMEOUT_MS = 12_000 -// Why: a slow (but succeeding) daemon start must not flip terminals to the -// LocalPtyProvider fallback — local PTYs are killed on quit, so panes bound to -// them lose their daemon sessions permanently (#5232). The PTY gate therefore -// waits for the daemon attempt itself and only fail-opens at a hard cap that -// exists solely as a deadlock backstop. +// Bound the startup gate without discarding a daemon that becomes ready later. export const LOCAL_PTY_STARTUP_FAIL_OPEN_TIMEOUT_MS = 60_000 function startService( label: string, start: (signal: AbortSignal) => Promise, - onError: (error: unknown) => void + onError: (error: unknown) => void, + abortOnTimeout = true ): StartupService { const abortController = new AbortController() let settled = false @@ -50,7 +47,9 @@ function startService( return } reportedTimeout = true - abortController.abort() + if (abortOnTimeout) { + abortController.abort() + } onError(new Error(`${label} startup timed out`)) } } @@ -65,14 +64,8 @@ export function startFirstWindowStartupServices({ onDaemonError, onAgentHookServerError }: FirstWindowStartupServices): FirstWindowStartupServicesResult { - // Why: daemon startup and hook-server binding are independent, but both gate - // restored terminals; run them together so cold-start latency is max(), not sum(). - // The first window fails open quickly so the user sees the app; the local PTY - // gate waits for the services themselves (a slow daemon must not flip spawns - // to the non-restorable LocalPtyProvider fallback) and only fails open at the - // hard cap, which also aborts the services so a late daemon swap cannot - // strand any fallback PTYs that spawn after the gate opens. - const daemon = startService('daemon PTY provider', startDaemonPtyProvider, onDaemonError) + // Independent services start together; a timeout opens the UI but leaves daemon recovery running. + const daemon = startService('daemon PTY provider', startDaemonPtyProvider, onDaemonError, false) const hooks = startService('agent hook server', startAgentHookServer, onAgentHookServerError) const allServicesReady = Promise.all([daemon.ready, hooks.ready]).then(() => undefined) let windowTimeout: ReturnType | null = null diff --git a/src/main/startup/legacy-worker-renderer-recovery.test.ts b/src/main/startup/legacy-worker-renderer-recovery.test.ts index d20ac3b8675d..838037033213 100644 --- a/src/main/startup/legacy-worker-renderer-recovery.test.ts +++ b/src/main/startup/legacy-worker-renderer-recovery.test.ts @@ -126,7 +126,7 @@ describe('legacy worker renderer recovery', () => { ) await startup expect(reconcile).toHaveBeenCalledOnce() - expect(daemonSignal?.aborted).toBe(true) + expect(daemonSignal?.aborted).toBe(false) } finally { vi.useRealTimers() } diff --git a/src/main/startup/main-process-pty-startup.ts b/src/main/startup/main-process-pty-startup.ts index 162faf66b2f4..622f326f7a57 100644 --- a/src/main/startup/main-process-pty-startup.ts +++ b/src/main/startup/main-process-pty-startup.ts @@ -2,6 +2,7 @@ import { app } from 'electron' import { classifyError } from '../telemetry/classify-error' import { track } from '../telemetry/client' import { getPtyIdForPaneKey } from '../ipc/pty' +import { tryGetProviderForPty } from '../ipc/pty/provider/registry' import { getDaemonProvider, initDaemonPtyProvider, @@ -99,6 +100,8 @@ export async function reapRestoredSubagentsWithoutLiveAgent(): Promise { ) await sweepRestoredSubagentsWithoutLiveAgent({ probeLiveLocalPty: (ptyId) => provider.probePtyLiveness(ptyId), + probeLiveWslPty: async (ptyId) => + (await tryGetProviderForPty(ptyId)?.probePtyLiveness?.(ptyId)) ?? null, isLocalExecutionHost: (worktreeId) => isLocalExecutionHost( resolveAgentWorkspaceExecutionHostId(worktreeId, { @@ -182,10 +185,10 @@ export function startTerminalRuntimeStartupServices(): WindowsDesktopStartupServ logStartupMilestone('startup-service-done', { service: 'agent-hook-server' }) }, onDaemonError: (error) => { - // Why: daemon failure silently falls back to non-persistent local PTYs; log + telemetry so a fleet-wide outage is observable (was invisible in v1.4.129-rc.1). + // Report service failure without implying terminal ownership moved into Electron. const reason = error instanceof Error ? error.message : String(error) console.error( - `[daemon] STARTUP FAILED — falling back to local PTYs; terminals will not persist across quit. Reason: ${reason}` + `[daemon] STARTUP FAILED — new terminals are unavailable until the terminal service recovers. Reason: ${reason}` ) track('daemon_start_failed', classifyError(error)) }, diff --git a/src/main/startup/main-process-quit.ts b/src/main/startup/main-process-quit.ts index ad4548db7bd4..108a8f55caa8 100644 --- a/src/main/startup/main-process-quit.ts +++ b/src/main/startup/main-process-quit.ts @@ -177,6 +177,9 @@ function installWillQuitHandler(): void { }) : Promise.resolve() // Why: cancels relay restart/reinstall timers and kills wsl.exe children deterministically, not via stdio-pipe teardown. + // Fence delayed guest owner publication before the final profile write. + const wslDaemonShutdown = state.wslDaemonSessions?.dispose() ?? Promise.resolve() + void wslDaemonShutdown.catch(() => {}) wslHookRelayManager.disposeAll() const statsFlush = state.stats?.flushAsync() ?? Promise.resolve() // Why: agent-browser daemon processes would otherwise linger after quit, holding ports and stale session state on disk. @@ -250,6 +253,7 @@ function installWillQuitHandler(): void { // temp+rename swap means a write cut short by the deadline leaves the old file intact. settleTeardownWithinDeadline([ { name: 'daemon', promise: daemonTeardown }, + { name: 'wsl-daemons', promise: wslDaemonShutdown }, { name: 'browser', promise: browserShutdown }, { name: 'runtime-rpc', promise: rpcStopAndClear }, { name: 'watchers', promise: watcherShutdown }, diff --git a/src/main/startup/main-process-runtime-launch-activation.test.ts b/src/main/startup/main-process-runtime-launch-activation.test.ts index 42813af450e7..7de354e94aaf 100644 --- a/src/main/startup/main-process-runtime-launch-activation.test.ts +++ b/src/main/startup/main-process-runtime-launch-activation.test.ts @@ -1,5 +1,6 @@ import { readFileSync } from 'node:fs' import { join } from 'node:path' +import type { UpdaterSetupOptions } from '../updater' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' const electronApp = vi.hoisted(() => ({ @@ -15,6 +16,16 @@ const launchHooks = vi.hoisted(() => ({ failBeforeWindow: false })) +const updater = vi.hoisted(() => ({ + setupAutoUpdater: vi.fn<(window: unknown, options?: UpdaterSetupOptions) => void>(), + resolveUpdateInstallMode: vi.fn(() => 'supervised-headless-serve'), + preserveAuth: vi.fn(async () => {}) +})) +vi.mock('../updater', () => updater) +vi.mock('../agent-auth-restart-preservation', () => ({ + preserveAgentAuthBeforeRestart: updater.preserveAuth +})) + vi.mock('electron', () => ({ app: electronApp, powerMonitor: { on: vi.fn() } })) vi.mock('@electron-toolkit/utils', () => ({ is: { dev: false } })) vi.mock('../orca-profiles/profile-cloud-auth-config', () => ({ @@ -36,7 +47,6 @@ vi.mock('../ipc/pty', () => ({ getLocalPtyProvider: vi.fn(), registerHeadlessPtyRuntime: vi.fn() })) -vi.mock('../providers/local-pty-provider', () => ({ LocalPtyProvider: class {} })) vi.mock('../browser/offscreen-browser-backend', () => ({ OffscreenBrowserBackend: class {} })) vi.mock('../browser/browser-manager', () => ({ browserManager: {} })) vi.mock('./main-process-relay-status', () => ({ @@ -101,6 +111,7 @@ vi.mock('./main-process-i18n-menu', () => ({ initializeMainProcessI18nAndMenu: vi.fn(async () => {}) })) +const { getServeOptions, printServeReady } = await import('./main-process-serve') const { initializeMainProcessReady } = await import('./main-process-ready') const { mainProcessState: state } = await import('./main-process-state') const { createServeDesktopActivationGate } = await import('./serve-desktop-activation') @@ -145,6 +156,8 @@ describe('desktop startup activation', () => { } beforeEach(() => { + vi.clearAllMocks() + vi.mocked(getServeOptions).mockReset() windows = [] showWindowWithoutStealingFocus.mockClear() ipcHandles = new Set() @@ -219,6 +232,69 @@ describe('desktop startup activation', () => { expect(state.desktopActivationGate).toBeNull() }) + it('configures the windowless serve updater after proxy setup and before readiness', async () => { + state.isServeMode = true + vi.mocked(getServeOptions).mockReturnValue({ + json: true, + noPairing: true, + mobilePairing: false, + recipeJson: false, + pairingAddress: null, + projectRoot: null + }) + const flushPendingAsync = vi.fn(async () => {}) + const exportCompatibility = vi.fn(async () => {}) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: serve startup and updater callbacks use only these store methods. + state.store = { + getUI: () => ({}), + updateUI: vi.fn(), + flushPendingAsync, + writeLatestProfileStateJsonCompatibilityExportAsync: exportCompatibility + } as unknown as NonNullable + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: auth preservation is mocked and only the presence of these services is checked. + state.codexRuntimeHome = {} as NonNullable + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: auth preservation and headless PTY registration are mocked. + state.claudeRuntimeAuth = {} as NonNullable + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: these are the runtime methods called by the actual serve launch branch. + state.runtime = { + refreshRestoredOrchestrationAuthority: vi.fn(), + reconcileLegacyWorkerTerminals: vi.fn(), + syncWindowGraph: vi.fn() + } as unknown as NonNullable + let releaseProxy = (): void => {} + state.initialProxyApplicationReady = new Promise((resolve) => { + releaseProxy = resolve + }) + const openMainWindow = vi.fn(() => { + throw new Error('serve must not construct a window') + }) + const startup = initializeMainProcessReady({ openMainWindow, handleMacAppActivation: vi.fn() }) + await new Promise((resolve) => setImmediate(resolve)) + expect(updater.setupAutoUpdater).not.toHaveBeenCalled() + releaseProxy() + await startup + expect(openMainWindow).not.toHaveBeenCalled() + expect(updater.resolveUpdateInstallMode).toHaveBeenCalledWith(true) + expect(updater.setupAutoUpdater).toHaveBeenCalledWith( + null, + expect.objectContaining({ + installMode: 'supervised-headless-serve', + onBeforeQuitFailure: 'abort' + }) + ) + expect(updater.setupAutoUpdater.mock.invocationCallOrder[0]).toBeLessThan( + vi.mocked(printServeReady).mock.invocationCallOrder[0] + ) + const options = updater.setupAutoUpdater.mock.calls[0][1]! + await options.onBeforeQuit!() + expect(updater.preserveAuth).toHaveBeenCalledOnce() + expect(exportCompatibility).toHaveBeenCalledOnce() + expect(flushPendingAsync).toHaveBeenCalledOnce() + exportCompatibility.mockRejectedValueOnce(new Error('export failed')) + await expect(options.onBeforeQuit!()).rejects.toThrow('export failed') + expect(flushPendingAsync).toHaveBeenCalledTimes(2) + }) + it('holds every launch mode behind the gate until startup settles it', () => { const preflightSource = readFileSync( join(process.cwd(), 'src/main/startup/main-process-preflight.ts'), diff --git a/src/main/startup/main-process-runtime-launch.ts b/src/main/startup/main-process-runtime-launch.ts index 858c6b757867..e62f05653f0c 100644 --- a/src/main/startup/main-process-runtime-launch.ts +++ b/src/main/startup/main-process-runtime-launch.ts @@ -8,8 +8,8 @@ import { } from '../persistence' import { OrcaRuntimeRpcServer } from '../runtime/runtime-rpc' import { registerMobileHandlers } from '../ipc/mobile' -import { getLocalPtyProvider, registerHeadlessPtyRuntime } from '../ipc/pty' -import { LocalPtyProvider } from '../providers/local-pty-provider' +import { registerHeadlessPtyRuntime } from '../ipc/pty' +import { daemonOwnsFreshPersistentPtys } from '../daemon/daemon-provider-state' import { HEADLESS_RUNTIME_WINDOW_ID } from '../../shared/runtime-types' import { OffscreenBrowserBackend } from '../browser/offscreen-browser-backend' import { browserManager } from '../browser/browser-manager' @@ -40,6 +40,7 @@ import { startDesktopPushService } from './main-process-push-startup' import { mainProcessState as state } from './main-process-state' import { logStartupMilestone } from './startup-diagnostics' import { emitServeBrowserIdentityActionLine } from '../server/serve-stdout-boundary' +import { configureServeAutoUpdater } from './main-process-updater' import { getBrowserIdentityModeStatus } from '../browser/browser-identity-mode-store' type RuntimeService = NonNullable @@ -51,12 +52,11 @@ export type MainProcessRuntimeLaunchOptions = { function settleDesktopActivation(): void { const gate = state.desktopActivationGate - if (!gate) { - return + if (gate) { + settleServeDesktopActivation(gate, { + hasPersistentPtyProvider: daemonOwnsFreshPersistentPtys() + }) } - settleServeDesktopActivation(gate, { - hasPersistentPtyProvider: !(getLocalPtyProvider() instanceof LocalPtyProvider) - }) } function installRuntimeRpc( @@ -125,9 +125,9 @@ async function launchServeMode( runtimeRpc: OrcaRuntimeRpcServer, serveOptions: NonNullable> ): Promise { - // Why here: headless serve has no window to unblock, so keep the persisted proxy strictly - // ahead of every fetcher this phase can reach (relay, CLI install, RPC clients). + // Apply the persisted proxy before starting fetchers, including the updater. await state.initialProxyApplicationReady + configureServeAutoUpdater() // Why: give managed WSL launchers a brief chance to migrate before headless PTYs go live, without slow repairs withholding all RPC readiness. logStartupMilestone('wsl-cli-barrier-start') await state.managedWslCliStartupBarrierReady @@ -141,10 +141,14 @@ async function launchServeMode( runtime, prepareCodexRuntimeHomeForLaunch, () => state.store!.getSettings(), - (target) => state.claudeRuntimeAuth!.prepareForClaudeLaunch(target), + (target, execution) => state.claudeRuntimeAuth!.prepareForClaudeLaunch(target, execution), state.store!, prepareCodexSessionResumeForLaunch, - { onCodexHomePtySpawned: handleCodexHomePtySpawned, onPtyExit: handlePtyExit } + { + onCodexHomePtySpawned: handleCodexHomePtySpawned, + onPtyExit: handlePtyExit, + wslDaemonSessions: state.wslDaemonSessions + } ) await runtime.refreshRestoredOrchestrationAuthority() await runtime.reconcileLegacyWorkerTerminals() diff --git a/src/main/startup/main-process-runtime-service.ts b/src/main/startup/main-process-runtime-service.ts index 20ea8262c9a2..2aaa702fe387 100644 --- a/src/main/startup/main-process-runtime-service.ts +++ b/src/main/startup/main-process-runtime-service.ts @@ -1,3 +1,6 @@ +import { join } from 'node:path' +import { WslDaemonSessions } from '../wsl/wsl-daemon-sessions' +import { getDaemonHistoryDir } from '../daemon/daemon-launch-paths' import { applySessionSearchSettingsChange, installChildSessionSearchService @@ -48,6 +51,13 @@ export function initializeMainProcessRuntime(): OrcaRuntimeService { if (!store || !stats) { throw new Error('Store and stats must be initialized before runtime') } + if (process.platform === 'win32' && !state.wslDaemonSessions) { + state.wslDaemonSessions = new WslDaemonSessions({ + store, + profileScope: getProfileUserDataPath(), + historyRoot: join(getDaemonHistoryDir(), 'wsl') + }) + } const orchestrationEnvironmentTransport: OrchestrationEnvironmentTransport = { resolve: (selector) => { const environment = resolveEnvironment(app.getPath('userData'), selector) @@ -74,7 +84,8 @@ export function initializeMainProcessRuntime(): OrcaRuntimeService { // `orca serve`, which never opens one, and the fleet path runs there too. const observedPaneIdentities = new AgentStatusObservedPaneIdentities() const runtime = new OrcaRuntimeService(store, stats, { - prepareClaudeAuth: (target) => state.claudeRuntimeAuth!.prepareForClaudeLaunch(target), + prepareClaudeAuth: (target, execution) => + state.claudeRuntimeAuth!.prepareForClaudeLaunch(target, execution), agentSessionClaimSigner: loadAgentSessionClaimSigner( getProfileUserDataPath(), getProfileUserDataPath() diff --git a/src/main/startup/main-process-state.ts b/src/main/startup/main-process-state.ts index f6e86423029e..7ebc3a8ec4d9 100644 --- a/src/main/startup/main-process-state.ts +++ b/src/main/startup/main-process-state.ts @@ -1,3 +1,4 @@ +import type { WslDaemonSessions } from '../wsl/wsl-daemon-sessions' import type { BrowserWindow, Tray } from 'electron' import { app } from 'electron' import type { Store } from '../persistence' @@ -63,6 +64,10 @@ function createInitialProfileStateAdmission(): ProfileStateRuntimeAdmission | un return undefined } +function createInitialWslDaemonSessions(): WslDaemonSessions | undefined { + return undefined +} + /** Mutable composition-root state shared by startup, window, serve, and quit phases. */ export const mainProcessState = { mainWindow: null as BrowserWindow | null, @@ -70,6 +75,7 @@ export const mainProcessState = { isQuitting: false, store: null as Store | null, profileStateStartup: createInitialProfileStateStartup(), + wslDaemonSessions: createInitialWslDaemonSessions(), profileStateAdmission: createInitialProfileStateAdmission(), stats: null as StatsCollector | null, claudeUsage: null as ClaudeUsageStore | null, diff --git a/src/main/startup/main-process-updater.ts b/src/main/startup/main-process-updater.ts new file mode 100644 index 000000000000..cf7bac9e79c6 --- /dev/null +++ b/src/main/startup/main-process-updater.ts @@ -0,0 +1,30 @@ +import { preserveAgentAuthBeforeRestart } from '../agent-auth-restart-preservation' +import { resolveUpdateInstallMode } from '../updater' +import { + configureProfileAutoUpdater, + type ProfileAutoUpdaterOptions +} from '../updater/profile-auto-updater' +import { mainProcessState as state } from './main-process-state' + +export function getMainProcessUpdaterOptions(): ProfileAutoUpdaterOptions { + const { store, codexRuntimeHome, claudeRuntimeAuth } = state + if (!store || !codexRuntimeHome || !claudeRuntimeAuth) { + throw new Error('Profile and auth services must be initialized before the updater') + } + return { + onBeforeUpdateQuit: async () => { + await preserveAgentAuthBeforeRestart({ codexRuntimeHome, claudeRuntimeAuth, store }) + await store.writeLatestProfileStateJsonCompatibilityExportAsync() + }, + onBeforeUpdateQuitFailure: 'abort', + updateInstallMode: resolveUpdateInstallMode(state.isServeMode) + } +} + +export function configureServeAutoUpdater(): void { + const options = getMainProcessUpdaterOptions() + if (!state.store) { + throw new Error('Profile store must be initialized before the updater') + } + configureProfileAutoUpdater(null, state.store, options) +} diff --git a/src/main/startup/main-window-core-services.test.ts b/src/main/startup/main-window-core-services.test.ts index 50d0bd49db53..f6f1c04bb779 100644 --- a/src/main/startup/main-window-core-services.test.ts +++ b/src/main/startup/main-window-core-services.test.ts @@ -40,6 +40,7 @@ const { pluginMarketplaceInstaller: null, desktopRelayService: null, isServeMode: false, + wslDaemonSessions: { prepareFresh: vi.fn(), reconnect: vi.fn(), dispose: vi.fn() }, localPtyStartupReady: Promise.resolve(), localPtyProviderStartupReady: Promise.resolve() }, @@ -81,6 +82,26 @@ describe('main window profile-state update preparation', () => { vi.clearAllMocks() }) + it('forwards captured guest account identity through the terminal auth callback', async () => { + const window = { webContents: { id: 17 } } + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: this mocked composition root only reads webContents. + attachMainWindowCoreServices(window as never, { + markExpectedRendererReload: vi.fn(), + recordRendererReload: vi.fn() + }) + expect(attachMainWindowServicesMock.mock.calls[0]?.[5]).toMatchObject({ + wslDaemonSessions: state.wslDaemonSessions + }) + const prepare = attachMainWindowServicesMock.mock.calls[0]?.[4] + if (typeof prepare !== 'function') { + throw new Error('Expected terminal auth preparation callback') + } + const target = { runtime: 'wsl', wslDistro: 'Ubuntu' } + const execution = { distro: 'Ubuntu', userName: 'alice', userId: '1000', home: '/home/alice' } + await prepare(target, execution) + expect(state.claudeRuntimeAuth.prepareForClaudeLaunch).toHaveBeenCalledWith(target, execution) + }) + it('publishes both recovery forms with one profile checkpoint before an update quit', async () => { const window = { webContents: { id: 17 } } diff --git a/src/main/startup/main-window-core-services.ts b/src/main/startup/main-window-core-services.ts index c292a4455c7c..e41a00d3a00e 100644 --- a/src/main/startup/main-window-core-services.ts +++ b/src/main/startup/main-window-core-services.ts @@ -2,7 +2,7 @@ import type { BrowserWindow } from 'electron' import { registerCoreHandlers } from '../ipc/register-core-handlers/register-core-handlers' import { attachMainWindowServices } from '../window/attach-main-window-services' import { initTccPromptNotice } from '../macos-tcc-prompt-notice' -import { resolveUpdateInstallMode } from '../updater' +import { getMainProcessUpdaterOptions } from './main-process-updater' import { mainProcessState as state } from './main-process-state' import { prepareCodexAiVaultSessionResume } from '../codex/codex-ai-vault-session-resume' import { resolveHostCodexSessionSourceHome } from '../codex/codex-session-source-home' @@ -111,9 +111,10 @@ export function attachMainWindowCoreServices( store, runtime, prepareCodexRuntimeHomeForLaunch, - (target) => claudeRuntimeAuth.prepareForClaudeLaunch(target), + (target, execution) => claudeRuntimeAuth.prepareForClaudeLaunch(target, execution), { prepareCodexSessionResume: prepareCodexSessionResumeForLaunch, + wslDaemonSessions: state.wslDaemonSessions, awaitLocalPtyStartup: () => state.localPtyStartupReady, awaitLocalPtyProviderStartup: () => state.localPtyProviderStartupReady, onBeforeRendererReload: ({ ignoreCache, webContentsId }) => { @@ -126,12 +127,7 @@ export function attachMainWindowCoreServices( isRecoveryReloadInFlight, onCodexHomePtySpawned: handleCodexHomePtySpawned, onPtyExit: handlePtyExit, - onBeforeUpdateQuit: async () => { - await preserveAgentAuthBeforeRestart({ codexRuntimeHome, claudeRuntimeAuth, store }) - await store.writeLatestProfileStateJsonCompatibilityExportAsync() - }, - onBeforeUpdateQuitFailure: 'abort', - updateInstallMode: resolveUpdateInstallMode(state.isServeMode), + ...getMainProcessUpdaterOptions(), onWorktreeLifecycle: emitPluginWorktreeLifecycle } ) diff --git a/src/main/startup/serve-desktop-activation-wiring.test.ts b/src/main/startup/serve-desktop-activation-wiring.test.ts index 64d628c51ff7..072afa5f9919 100644 --- a/src/main/startup/serve-desktop-activation-wiring.test.ts +++ b/src/main/startup/serve-desktop-activation-wiring.test.ts @@ -79,7 +79,12 @@ describe('serve desktop activation wiring', () => { }) it('keeps the headless install policy after desktop promotion', () => { - expect(windowCoreSource).toContain( + expect(windowCoreSource).toContain('...getMainProcessUpdaterOptions()') + const updaterSource = readFileSync( + join(process.cwd(), 'src/main/startup/main-process-updater.ts'), + 'utf8' + ) + expect(updaterSource).toContain( 'updateInstallMode: resolveUpdateInstallMode(state.isServeMode)' ) }) diff --git a/src/main/terminal-history-fish-session-bun-fixture.ts b/src/main/terminal-history-fish-session-bun-fixture.ts new file mode 100644 index 000000000000..0bf01293f05a --- /dev/null +++ b/src/main/terminal-history-fish-session-bun-fixture.ts @@ -0,0 +1,88 @@ +import assert from 'node:assert/strict' +import { spawnBunPty } from './daemon/pty-subprocess/bun-pty-process' +const sleep = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)) + +async function waitUntil(predicate: () => boolean, timeoutMs: number): Promise { + const deadline = Date.now() + timeoutMs + while (Date.now() < deadline) { + if (predicate()) { + return true + } + await sleep(20) + } + return false +} + +export async function runFishHistoryFixture({ + binary, + home, + dataHome, + session, + promptMark, + marker +}: { + binary: string + home: string + dataHome: string + session: string + promptMark: string + marker: string +}): Promise { + const term = spawnBunPty({ + file: binary, + args: ['-l', '-i'], + cols: 120, + rows: 30, + cwd: home, + // Fully pinned: no ambient HOME/XDG_* reaches fish, so this cannot pass + // only on a machine whose real fish config happens to cooperate. + env: { + PATH: process.env.PATH ?? '/usr/bin:/bin', + HOME: home, + TERM: 'xterm-256color', + // LC_ALL wins over any LANG/LC_* a host might contribute, pinning fish's locale. + LANG: 'en_US.UTF-8', + LC_ALL: 'en_US.UTF-8', + XDG_CONFIG_HOME: home, + XDG_DATA_HOME: dataHome, + // The production injection under test (terminal-history.ts). + fish_history: session + } + }) + + let rendered = '' + term.onData((chunk) => { + rendered += chunk + if (chunk.includes('\x1b[0c') || chunk.includes('\x1b[c')) { + term.write('\x1b[?62;4;6;22c') + } + if (chunk.includes('\x1b[6n')) { + term.write('\x1b[1;1R') + } + if (chunk.includes('\x1b]10;?') || chunk.includes('\x1b]11;?')) { + term.write('\x1b]11;rgb:1e1e/1e1e/1e1e\x1b\\') + } + }) + let exited = false + term.onExit(() => { + exited = true + }) + + try { + assert.equal(await waitUntil(() => rendered.includes(promptMark), 15_000), true) + term.write(`${marker}\r`) + assert.equal( + await waitUntil(() => rendered.includes('orca-worktree-scoped-history'), 5_000), + true + ) + // fish flushes history on exit, so the read must wait for the process to go. + term.write('exit\r') + assert.equal(await waitUntil(() => exited, 10_000), true) + } finally { + if (!exited) { + term.kill() + } + assert.equal(await waitUntil(() => exited, 3000), true) + term.destroy() + } +} diff --git a/src/main/terminal-history-fish-session.node-pty.test.ts b/src/main/terminal-history-fish-session.bun.test.ts similarity index 58% rename from src/main/terminal-history-fish-session.node-pty.test.ts rename to src/main/terminal-history-fish-session.bun.test.ts index f4d7b3cf404f..04c65a59a90e 100644 --- a/src/main/terminal-history-fish-session.node-pty.test.ts +++ b/src/main/terminal-history-fish-session.bun.test.ts @@ -18,6 +18,7 @@ import { tmpdir } from 'node:os' import path from 'node:path' import { afterEach, describe, expect, it } from 'vitest' import { fishRequirementViolation, resolveFishBinary } from '../shared/fish-binary-requirement' +import { runBundledBunFixture } from './bundled-bun-test-execution' import { fishHistorySessionName, resolveFishHistoryDir } from './fish-history-session' const FISH = resolveFishBinary(4) @@ -27,19 +28,6 @@ const PROMPT_MARK = 'ORCAHIST> ' const WORKTREE_HASH = 'deadbeefdeadbeef' const MARKER = 'echo orca-worktree-scoped-history' -const sleep = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)) - -async function waitUntil(predicate: () => boolean, timeoutMs: number): Promise { - const deadline = Date.now() + timeoutMs - while (Date.now() < deadline) { - if (predicate()) { - return true - } - await sleep(20) - } - return false -} - describe('fish keeps per-worktree history under the session Orca names', () => { let home: string | null = null @@ -58,8 +46,6 @@ describe('fish keeps per-worktree history under the session Orca names', () => { itWithFish( 'writes an interactive command to $XDG_DATA_HOME/fish/_history, not the shared file', async () => { - const nodePty = await import('node-pty') - home = mkdtempSync(path.join(tmpdir(), 'orca-fish-history-')) const dataHome = path.join(home, 'data') mkdirSync(path.join(home, 'fish'), { recursive: true }) @@ -74,65 +60,22 @@ describe('fish keeps per-worktree history under the session Orca names', () => { ) const session = fishHistorySessionName(WORKTREE_HASH) - const term = nodePty.spawn(FISH.path as string, ['-l', '-i'], { - name: 'xterm-256color', - cols: 120, - rows: 30, - cwd: home, - // Fully pinned: no ambient HOME/XDG_* reaches fish, so this cannot pass - // only on a machine whose real fish config happens to cooperate. - env: { - PATH: process.env.PATH ?? '/usr/bin:/bin', - HOME: home, - TERM: 'xterm-256color', - // LC_ALL wins over any LANG/LC_* a host might contribute, pinning fish's locale. - LANG: 'en_US.UTF-8', - LC_ALL: 'en_US.UTF-8', - XDG_CONFIG_HOME: home, - XDG_DATA_HOME: dataHome, - // The production injection under test (terminal-history.ts). - fish_history: session - } - }) - - let rendered = '' - term.onData((chunk) => { - rendered += chunk - if (chunk.includes('\x1b[0c') || chunk.includes('\x1b[c')) { - term.write('\x1b[?62;4;6;22c') - } - if (chunk.includes('\x1b[6n')) { - term.write('\x1b[1;1R') - } - if (chunk.includes('\x1b]10;?') || chunk.includes('\x1b]11;?')) { - term.write('\x1b]11;rgb:1e1e/1e1e/1e1e\x1b\\') - } - }) - let exited = false - term.onExit(() => { - exited = true - }) - - expect(await waitUntil(() => rendered.includes(PROMPT_MARK), 15_000)).toBe(true) - term.write(`${MARKER}\r`) - expect(await waitUntil(() => rendered.includes('orca-worktree-scoped-history'), 5_000)).toBe( - true - ) - // fish flushes history on exit, so the read must wait for the process to go. - term.write('exit\r') - expect(await waitUntil(() => exited, 10_000)).toBe(true) - try { - term.kill() - } catch { - // already gone + if (!FISH.path) { + throw new Error('Fish binary missing') } + await runBundledBunFixture( + path.join(__dirname, 'terminal-history-fish-session-bun-fixture.ts'), + 'runFishHistoryFixture', + { binary: FISH.path, home, dataHome, session, promptMark: PROMPT_MARK, marker: MARKER }, + 35_000 + ) const scopedPath = path.join( resolveFishHistoryDir({ XDG_DATA_HOME: dataHome }), `${session}_history` ) expect(scopedPath).toBe(path.join(dataHome, 'fish', `${session}_history`)) - const scoped = readFileSync(scopedPath as string, 'utf8') + const scoped = readFileSync(scopedPath, 'utf8') // YAML-ish records, not one line per command — any reader must handle this shape. expect(scoped).toContain(`- cmd: ${MARKER}`) expect(scoped).toMatch(/^ {2}when: \d+$/m) diff --git a/src/main/updater-events.ts b/src/main/updater-events.ts index d2b7f2a1e83b..76d7c714b189 100644 --- a/src/main/updater-events.ts +++ b/src/main/updater-events.ts @@ -21,6 +21,7 @@ const AUTO_UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000 const AUTO_UPDATE_RETRY_INTERVAL_MS = 60 * 60 * 1000 type UpdaterHandlerContext = { + onNativeInstallEvent?: (event: 'ready' | 'error', error?: unknown) => void autoUpdater: ElectronAutoUpdater clearBackgroundCheckLaunchPending: () => void clearAvailableUpdateContext: () => void @@ -64,6 +65,7 @@ type UpdaterHandlerContext = { } export function registerAutoUpdaterHandlers({ + onNativeInstallEvent, autoUpdater, clearBackgroundCheckLaunchPending, clearAvailableUpdateContext, @@ -101,6 +103,7 @@ export function registerAutoUpdaterHandlers({ setUserInitiatedCheck }: UpdaterHandlerContext): void { registerMacUpdaterEvents({ + onNativeInstallEvent, getCurrentStatus, hasInstallableDownloadedVersion, getPendingInstallVersion, @@ -111,7 +114,7 @@ export function registerAutoUpdaterHandlers({ }) autoUpdater.on('checking-for-update', () => { - if (!markUpdateCheckEventAttempt()) { + if (isQuitAndInstallHandoffActive() || !markUpdateCheckEventAttempt()) { return } clearBackgroundCheckLaunchPending() @@ -125,7 +128,7 @@ export function registerAutoUpdaterHandlers({ autoUpdater.on('update-available', (info) => { const attemptId = getActiveUpdateCheckEventAttemptId() - if (attemptId === null) { + if (isQuitAndInstallHandoffActive() || attemptId === null) { return } clearBackgroundCheckLaunchPending() @@ -174,7 +177,7 @@ export function registerAutoUpdaterHandlers({ : await fetchChangelog(info.version, app.getVersion()).catch(() => null) // Why: async fetch may take seconds; bail if a newer event superseded this attempt to avoid a stale 'available' broadcast. - if (!isActiveUpdateCheckAttempt(attemptId)) { + if (isQuitAndInstallHandoffActive() || !isActiveUpdateCheckAttempt(attemptId)) { return } if (getCurrentStatus().state !== 'checking' && getCurrentStatus().state !== 'idle') { @@ -215,7 +218,7 @@ export function registerAutoUpdaterHandlers({ }) autoUpdater.on('update-not-available', () => { - if (getActiveUpdateCheckEventAttemptId() === null) { + if (isQuitAndInstallHandoffActive() || getActiveUpdateCheckEventAttemptId() === null) { return } clearBackgroundCheckLaunchPending() @@ -292,8 +295,8 @@ export function registerAutoUpdaterHandlers({ sendStatus(linuxPackageStatus) return } - // On macOS, defer 'downloaded' until Squirrel.Mac finishes processing; other platforms are ready immediately. - if (process.platform === 'darwin' && !macInstallerReady) { + // Explicit-only Mac installs stage only after the install request and durable handoff. + if (process.platform === 'darwin' && autoUpdater.autoInstallOnAppQuit && !macInstallerReady) { // Keep the UI at 100% downloaded while Squirrel processes, to avoid a premature "ready to install". recordUpdaterLifecycle('macos_waiting_for_squirrel', { version: info.version }) sendStatus({ state: 'downloading', percent: 100, version: info.version }) diff --git a/src/main/updater-mac-install.ts b/src/main/updater-mac-install.ts index 234cdc0b2c45..785ab1601eda 100644 --- a/src/main/updater-mac-install.ts +++ b/src/main/updater-mac-install.ts @@ -11,8 +11,10 @@ export function registerMacUpdaterEvents({ getKnownReleaseUrl, performQuitAndInstall, shouldDeferMacQuitForInstall, - sendStatus + sendStatus, + onNativeInstallEvent }: { + onNativeInstallEvent?: (event: 'ready' | 'error', error?: unknown) => void getCurrentStatus: () => UpdateStatus hasInstallableDownloadedVersion: () => boolean getPendingInstallVersion: () => string @@ -22,6 +24,10 @@ export function registerMacUpdaterEvents({ sendStatus: (status: UpdateStatus) => void }): void { if (process.platform === 'darwin') { + nativeUpdater.on('error', (error) => onNativeInstallEvent?.('error', error)) + nativeUpdater.on('update-not-available', () => { + onNativeInstallEvent?.('error', new Error('The native updater found no update to install.')) + }) nativeUpdater.on('update-downloaded', () => { const hasInstallableVersion = hasInstallableDownloadedVersion() handleMacInstallerReady(hasInstallableVersion, performQuitAndInstall, () => { @@ -31,6 +37,7 @@ export function registerMacUpdaterEvents({ releaseUrl: getKnownReleaseUrl() }) }) + onNativeInstallEvent?.('ready') }) } diff --git a/src/main/updater-mac-staging-contract.test.ts b/src/main/updater-mac-staging-contract.test.ts new file mode 100644 index 000000000000..d1dc31075e2d --- /dev/null +++ b/src/main/updater-mac-staging-contract.test.ts @@ -0,0 +1,138 @@ +import { readFileSync } from 'node:fs' +import { runInNewContext } from 'node:vm' +import { createRequire } from 'node:module' +import { EventEmitter } from 'node:events' +import type * as MacUpdaterModule from 'electron-updater/out/MacUpdater' +import type { Server } from 'node:http' +import { describe, expect, it, vi } from 'vitest' + +const require = createRequire(__filename) +// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: this is the installed dependency's declared CommonJS export, bypassing updater mocks. +const { MacUpdater } = require('electron-updater/out/MacUpdater') as typeof MacUpdaterModule + +const pinnedPrototype: unknown = MacUpdater.prototype +if ( + !pinnedPrototype || + typeof pinnedPrototype !== 'object' || + !('updateDownloaded' in pinnedPrototype) || + typeof pinnedPrototype.updateDownloaded !== 'function' || + !('handleUpdateDownloaded' in pinnedPrototype) || + typeof pinnedPrototype.handleUpdateDownloaded !== 'function' || + !('quitAndInstall' in pinnedPrototype) || + typeof pinnedPrototype.quitAndInstall !== 'function' +) { + throw new Error('Pinned MacUpdater staging contract changed') +} +const pinnedMethods = { + updateDownloaded: pinnedPrototype.updateDownloaded, + handleUpdateDownloaded: pinnedPrototype.handleUpdateDownloaded, + quitAndInstall: pinnedPrototype.quitAndInstall +} + +describe('pinned MacUpdater native staging contract', () => { + it('downloads the payload without native staging until explicit installation', async () => { + const lifecycle: string[] = [] + const native = Object.assign(new EventEmitter(), { + setFeedURL: vi.fn(), + checkForUpdates: vi.fn(() => lifecycle.push('native-staging-started')), + quitAndInstall: vi.fn() + }) + const serverSlot: { server?: Server } = {} + const updater = { + ...serverSlot, + autoInstallOnAppQuit: false, + autoRunAppAfterInstall: false, + squirrelDownloadedUpdate: false, + nativeUpdater: native, + _logger: { info: vi.fn(), warn: vi.fn() }, + debug: vi.fn(), + dispatchUpdateDownloaded: vi.fn(() => lifecycle.push('payload-downloaded')), + app: { quit: vi.fn(() => lifecycle.push('app-quit')) }, + closeServerIfExists: (): void => { + updater.server?.close() + }, + handleUpdateDownloaded: (): void => { + pinnedMethods.handleUpdateDownloaded.call(updater) + } + } + try { + // Known size avoids any payload read; the real implementation still opens its staging proxy. + await pinnedMethods.updateDownloaded.call( + updater, + { url: new URL('https://example.invalid/update.zip'), info: { size: 1 } }, + { version: '1.0.61', downloadedFile: '/unused-contract-payload.zip' } + ) + expect(lifecycle).toEqual(['payload-downloaded']) + expect(native.setFeedURL).toHaveBeenCalledOnce() + expect(native.checkForUpdates).not.toHaveBeenCalled() + expect(native.listenerCount('update-downloaded')).toBe(0) + expect(updater.app.quit).not.toHaveBeenCalled() + + pinnedMethods.quitAndInstall.call(updater) + expect(lifecycle).toEqual(['payload-downloaded', 'native-staging-started']) + expect(updater.app.quit).not.toHaveBeenCalled() + native.emit('update-downloaded') + expect(lifecycle).toEqual(['payload-downloaded', 'native-staging-started', 'app-quit']) + expect(native.quitAndInstall).not.toHaveBeenCalled() + } finally { + updater.server?.close() + } + }) + it('observes native readiness before Orca quits, without retaining a future quit listener', () => { + const native = Object.assign(new EventEmitter(), { + checkForUpdates: vi.fn(), + quitAndInstall: vi.fn() + }) + const exports: Record = {} + const source = readFileSync(require.resolve('electron-updater/out/MacUpdater'), 'utf8') + const dependencyRequire = createRequire(require.resolve('electron-updater/out/MacUpdater')) + runInNewContext(source, { + exports, + Buffer, + process, + require: (name: string) => + name === 'electron' ? { autoUpdater: native } : dependencyRequire(name) + }) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: executing the pinned CommonJS module above produces its declared exports with only Electron replaced. + const Constructor = (exports as unknown as typeof MacUpdaterModule).MacUpdater + const quit = vi.fn() + const updater = new Constructor(undefined, { + version: '1.0.51', + name: 'Orca', + isPackaged: true, + appUpdateConfigPath: '', + userDataPath: '', + baseCachePath: '', + whenReady: async () => {}, + relaunch: vi.fn(), + quit, + onQuit: vi.fn() + }) + updater.autoInstallOnAppQuit = false + updater.autoRunAppAfterInstall = false + updater.logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn() } + const errorOrder: string[] = [] + updater.on('error', () => errorOrder.push('forwarded-js-error')) + native.on('error', () => errorOrder.push('authoritative-native-error')) + native.emit('error', new Error('native staging rejected')) + expect(errorOrder).toEqual(['forwarded-js-error', 'authoritative-native-error']) + expect(quit).not.toHaveBeenCalled() + let installOwned = true + native.on('update-downloaded', () => { + if (!installOwned) { + return + } + installOwned = false + updater.quitAndInstall() + }) + const listeners = native.listenerCount('update-downloaded') + native.checkForUpdates() + expect(quit).not.toHaveBeenCalled() + native.emit('update-downloaded') + expect(quit).toHaveBeenCalledOnce() + expect(native.listenerCount('update-downloaded')).toBe(listeners) + native.emit('update-downloaded') + expect(quit).toHaveBeenCalledOnce() + expect(native.checkForUpdates).toHaveBeenCalledOnce() + }) +}) diff --git a/src/main/updater.headless-serve-install.test.ts b/src/main/updater.headless-serve-install.test.ts index bae6474cc667..aa11e94bb8fa 100644 --- a/src/main/updater.headless-serve-install.test.ts +++ b/src/main/updater.headless-serve-install.test.ts @@ -9,9 +9,11 @@ const { recordUpdaterLifecycleMock, requestServeUpdateHandoffMock, failServeUpdateHandoffMock, + armWatchdog, resetHandlers } = vi.hoisted(() => { const appHandlers = new Map void)[]>() + const nativeHandlers = new Map void)[]>() const updaterHandlers = new Map void)[]>() const emit = ( @@ -54,12 +56,20 @@ const { return { appMock, autoUpdaterMock, - nativeUpdaterMock: { on: vi.fn() }, + nativeUpdaterMock: { + on: vi.fn((event: string, handler: (...args: unknown[]) => void) => { + nativeHandlers.set(event, [...(nativeHandlers.get(event) ?? []), handler]) + }), + checkForUpdates: vi.fn(), + emit: (event: string, ...args: unknown[]) => emit(nativeHandlers, event, ...args) + }, + armWatchdog: vi.fn(), killAllPtyMock: vi.fn(), recordUpdaterLifecycleMock: vi.fn(), requestServeUpdateHandoffMock: vi.fn(() => true), failServeUpdateHandoffMock: vi.fn(), resetHandlers: () => { + nativeHandlers.clear() appHandlers.clear() updaterHandlers.clear() } @@ -97,7 +107,7 @@ vi.mock('./updater-prerelease-feed', () => ({ getReleaseDownloadUrl: vi.fn() })) vi.mock('./update-install-exit-watchdog', () => ({ - armUpdateInstallExitWatchdog: vi.fn(), + armUpdateInstallExitWatchdog: armWatchdog, disarmUpdateInstallExitWatchdog: vi.fn() })) vi.mock('./updater-lifecycle-diagnostics', () => ({ @@ -123,7 +133,9 @@ describe('headless serve update install handoff', () => { autoUpdaterMock.on.mockClear() autoUpdaterMock.autoInstallOnAppQuit = false autoUpdaterMock.autoRunAppAfterInstall = true - nativeUpdaterMock.on.mockReset() + nativeUpdaterMock.on.mockClear() + nativeUpdaterMock.checkForUpdates.mockReset() + armWatchdog.mockReset() appMock.on.mockClear() appMock.quit.mockReset() killAllPtyMock.mockReset() @@ -270,65 +282,224 @@ describe('headless serve update install handoff', () => { ).toHaveLength(1) }) - it('hands a supervised install to the serve parent before native quit and cleanup', async () => { - const lifecycle: string[] = [] - const daemonSession = { alive: true } - const send = vi.fn() - const disconnectPairedClients = vi.fn(() => lifecycle.push('paired-clients-disconnected')) - appMock.on('will-quit', disconnectPairedClients) - requestServeUpdateHandoffMock.mockImplementation(() => { - lifecycle.push('handoff-persisted') - return true - }) - autoUpdaterMock.checkForUpdates.mockImplementation(() => { - autoUpdaterMock.emit('checking-for-update') - queueMicrotask(() => autoUpdaterMock.emit('update-available', { version: '1.0.61' })) - return Promise.resolve(null) - }) - autoUpdaterMock.quitAndInstall.mockImplementation(() => { - lifecycle.push('native-quit-and-install') - appMock.emit('will-quit', { preventDefault: vi.fn() }) - }) - killAllPtyMock.mockImplementation(() => lifecycle.push('in-process-pty-cleanup')) + it.each([ + { cleanupFails: false, synchronousReady: false }, + { cleanupFails: false, synchronousReady: true }, + { cleanupFails: true, synchronousReady: false } + ])( + 'handles windowless supervised installation: $cleanupFails / $synchronousReady', + async ({ cleanupFails, synchronousReady }) => { + const lifecycle: string[] = [] + nativeUpdaterMock.checkForUpdates.mockImplementation(() => { + lifecycle.push('native-staging-started') + if (synchronousReady) { + nativeUpdaterMock.emit('update-downloaded') + } + }) + const daemonSession = { alive: true } + const disconnectPairedClients = vi.fn(() => lifecycle.push('paired-clients-disconnected')) + appMock.on('will-quit', disconnectPairedClients) + requestServeUpdateHandoffMock.mockImplementation(() => { + lifecycle.push('handoff-persisted') + return true + }) + autoUpdaterMock.checkForUpdates.mockImplementation(() => { + autoUpdaterMock.emit('checking-for-update') + queueMicrotask(() => autoUpdaterMock.emit('update-available', { version: '1.0.61' })) + return Promise.resolve(null) + }) + autoUpdaterMock.quitAndInstall.mockImplementation(() => { + lifecycle.push('native-quit-and-install') + appMock.emit('will-quit', { preventDefault: vi.fn() }) + }) + killAllPtyMock.mockImplementation(() => lifecycle.push('in-process-pty-cleanup')) + + const { + checkForUpdatesFromMenu, + downloadUpdate, + quitAndInstall, + setupAutoUpdater, + getUpdateStatus + } = await loadUpdaterModule() + setupAutoUpdater(null, { + getLastUpdateCheckAt: () => Date.now(), + installMode: 'supervised-headless-serve', + onBeforeQuitFailure: 'abort', + onBeforeQuit: () => { + lifecycle.push('pre-quit-checkpoint') + if (cleanupFails) { + throw new Error('profile export failed') + } + } + }) + checkForUpdatesFromMenu() + await vi.advanceTimersByTimeAsync(0) + downloadUpdate() + autoUpdaterMock.emit('update-downloaded', { version: '1.0.61' }) + expect(getUpdateStatus()).toMatchObject({ state: 'downloaded', version: '1.0.61' }) + const preventDefault = vi.fn() + appMock.emit('before-quit', { preventDefault }) + expect(preventDefault).not.toHaveBeenCalled() + expect(autoUpdaterMock.quitAndInstall).not.toHaveBeenCalled() + expect(requestServeUpdateHandoffMock).not.toHaveBeenCalled() - const { checkForUpdatesFromMenu, downloadUpdate, quitAndInstall, setupAutoUpdater } = - await loadUpdaterModule() - setupAutoUpdater({ webContents: { send } } as never, { - getLastUpdateCheckAt: () => Date.now(), - installMode: 'supervised-headless-serve', - onBeforeQuit: () => { - lifecycle.push('pre-quit-checkpoint') + quitAndInstall() + quitAndInstall() + await vi.advanceTimersByTimeAsync(100) + quitAndInstall() + + if (cleanupFails) { + expect(nativeUpdaterMock.checkForUpdates).not.toHaveBeenCalled() + expect(requestServeUpdateHandoffMock).not.toHaveBeenCalled() + expect(autoUpdaterMock.quitAndInstall).not.toHaveBeenCalled() + expect(killAllPtyMock).not.toHaveBeenCalled() + expect(disconnectPairedClients).not.toHaveBeenCalled() + return } - }) - checkForUpdatesFromMenu() - await vi.advanceTimersByTimeAsync(0) - downloadUpdate() - autoUpdaterMock.emit('update-downloaded', { version: '1.0.61' }) - const nativeReadyHandler = nativeUpdaterMock.on.mock.calls.find( - ([event]) => event === 'update-downloaded' - )?.[1] as (() => void) | undefined - nativeReadyHandler?.() + if (process.platform === 'darwin') { + if (!synchronousReady) { + expect(autoUpdaterMock.quitAndInstall).not.toHaveBeenCalled() + } + expect(nativeUpdaterMock.checkForUpdates).toHaveBeenCalledOnce() + nativeUpdaterMock.emit('update-downloaded') + } else { + expect(nativeUpdaterMock.checkForUpdates).not.toHaveBeenCalled() + } + expect(armWatchdog).toHaveBeenCalledOnce() + expect(requestServeUpdateHandoffMock).toHaveBeenCalledWith('1.0.61') + expect(autoUpdaterMock.autoInstallOnAppQuit).toBe(false) + expect(autoUpdaterMock.autoRunAppAfterInstall).toBe(false) + expect(autoUpdaterMock.downloadUpdate).toHaveBeenCalledOnce() + expect(autoUpdaterMock.quitAndInstall).toHaveBeenCalledWith(true, false) + expect(autoUpdaterMock.quitAndInstall).toHaveBeenCalledOnce() + expect(daemonSession).toEqual({ alive: true }) + expect(lifecycle).toEqual([ + 'pre-quit-checkpoint', + 'handoff-persisted', + ...(process.platform === 'darwin' ? ['native-staging-started'] : []), + 'native-quit-and-install', + 'paired-clients-disconnected', + ...(process.platform !== 'darwin' ? ['in-process-pty-cleanup'] : []) + ]) + } + ) - quitAndInstall() - quitAndInstall() - await vi.advanceTimersByTimeAsync(100) - quitAndInstall() + it.runIf(process.platform === 'darwin').each(['error', 'update-not-available'])( + 'recovers a supervised install after native %s', + async (nativeEvent) => { + autoUpdaterMock.checkForUpdates.mockImplementation(() => { + autoUpdaterMock.emit('checking-for-update') + queueMicrotask(() => autoUpdaterMock.emit('update-available', { version: '1.0.61' })) + return Promise.resolve(null) + }) + const updater = await loadUpdaterModule() + updater.setupAutoUpdater(null, { + getLastUpdateCheckAt: () => Date.now(), + installMode: 'supervised-headless-serve' + }) + nativeUpdaterMock.emit(nativeEvent) + expect(failServeUpdateHandoffMock).not.toHaveBeenCalled() + updater.checkForUpdatesFromMenu() + await vi.advanceTimersByTimeAsync(0) + updater.downloadUpdate() + autoUpdaterMock.emit('update-downloaded', { version: '1.0.61' }) + expect(updater.installRemoteServerUpdate('runtime').accepted).toBe(true) + await vi.advanceTimersByTimeAsync(100) + expect(requestServeUpdateHandoffMock).toHaveBeenCalledOnce() + expect(nativeUpdaterMock.checkForUpdates).toHaveBeenCalledOnce() + expect(autoUpdaterMock.quitAndInstall).not.toHaveBeenCalled() + expect(updater.isQuittingForUpdate()).toBe(true) + autoUpdaterMock.emit('error', new Error('JS check failed')) + expect(updater.isQuittingForUpdate()).toBe(true) + expect(failServeUpdateHandoffMock).not.toHaveBeenCalled() + nativeUpdaterMock.emit(nativeEvent, new Error('Native staging failed')) + expect(updater.isQuittingForUpdate()).toBe(false) + expect(failServeUpdateHandoffMock).toHaveBeenCalledOnce() + expect(updater.getUpdateStatus()).toMatchObject({ state: 'error' }) + const terminalFailure = updater.getUpdateStatus() + await vi.advanceTimersByTimeAsync(120_001) + expect(updater.getUpdateStatus()).toEqual(terminalFailure) + expect(appMock.quit).not.toHaveBeenCalled() + nativeUpdaterMock.emit('update-downloaded') + expect(autoUpdaterMock.quitAndInstall).not.toHaveBeenCalled() + updater.downloadUpdate() + autoUpdaterMock.emit('update-downloaded', { version: '1.0.61' }) + updater.installRemoteServerUpdate('runtime') + await vi.advanceTimersByTimeAsync(100) + expect(requestServeUpdateHandoffMock).toHaveBeenCalledTimes(2) + nativeUpdaterMock.emit('update-downloaded') + expect(autoUpdaterMock.quitAndInstall).toHaveBeenCalledOnce() + expect(armWatchdog).toHaveBeenCalledOnce() + nativeUpdaterMock.emit(nativeEvent) + expect(failServeUpdateHandoffMock).toHaveBeenCalledOnce() + } + ) - expect(requestServeUpdateHandoffMock).toHaveBeenCalledWith('1.0.61') - expect(autoUpdaterMock.autoInstallOnAppQuit).toBe(false) - expect(autoUpdaterMock.autoRunAppAfterInstall).toBe(false) - expect(autoUpdaterMock.downloadUpdate).toHaveBeenCalledOnce() - expect(autoUpdaterMock.quitAndInstall).toHaveBeenCalledWith(true, false) - expect(autoUpdaterMock.quitAndInstall).toHaveBeenCalledOnce() - expect(daemonSession).toEqual({ alive: true }) - expect(lifecycle).toEqual([ - 'pre-quit-checkpoint', - 'handoff-persisted', - 'native-quit-and-install', - 'paired-clients-disconnected', - 'in-process-pty-cleanup' - ]) - }) + it.runIf(process.platform === 'darwin').each([false, true])( + 'retains handoff through staging timeout and committed quit failure=%s', + async (quitThrows) => { + autoUpdaterMock.checkForUpdates.mockImplementation(() => { + autoUpdaterMock.emit('checking-for-update') + queueMicrotask(() => autoUpdaterMock.emit('update-available', { version: '1.0.61' })) + return Promise.resolve(null) + }) + const updater = await loadUpdaterModule() + updater.setupAutoUpdater(null, { + getLastUpdateCheckAt: () => Date.now(), + installMode: 'supervised-headless-serve' + }) + updater.checkForUpdatesFromMenu() + await vi.advanceTimersByTimeAsync(0) + updater.downloadUpdate() + autoUpdaterMock.emit('update-downloaded', { version: '1.0.61' }) + const accepted = updater.installRemoteServerUpdate('runtime') + const checks = autoUpdaterMock.checkForUpdates.mock.calls.length + updater.checkForRemoteServerUpdate('runtime') + updater.checkForUpdatesFromMenu({ localBuild: true }) + updater.checkForUpdatesFromMenu({ channel: 'stable', targetTag: 'v1.0.99' }) + updater.dismissAvailableUpdate() + autoUpdaterMock.emit('checking-for-update') + autoUpdaterMock.emit('update-available', { version: '1.0.99' }) + autoUpdaterMock.emit('update-not-available') + await vi.advanceTimersByTimeAsync(99) + expect(nativeUpdaterMock.checkForUpdates).not.toHaveBeenCalled() + expect(updater.getUpdateStatus()).toMatchObject({ + state: 'downloaded', + version: accepted.targetVersion + }) + expect(autoUpdaterMock.checkForUpdates).toHaveBeenCalledTimes(checks) + await vi.advanceTimersByTimeAsync(1) + expect(requestServeUpdateHandoffMock).toHaveBeenCalledWith(accepted.targetVersion) + updater.checkForRemoteServerUpdate('runtime') + updater.checkForUpdatesFromMenu({ localBuild: true }) + updater.checkForUpdatesFromMenu({ channel: 'stable', targetTag: 'v1.0.99' }) + updater.dismissAvailableUpdate() + await vi.advanceTimersByTimeAsync(120_000) + expect(updater.getUpdateStatus()).toMatchObject({ + state: 'error', + retryable: false, + version: '1.0.61' + }) + expect(updater.isQuittingForUpdate()).toBe(true) + expect(failServeUpdateHandoffMock).not.toHaveBeenCalled() + expect(autoUpdaterMock.checkForUpdates).toHaveBeenCalledTimes(checks) + expect(armWatchdog).not.toHaveBeenCalled() + updater.downloadUpdate() + expect(autoUpdaterMock.downloadUpdate).toHaveBeenCalledOnce() + if (quitThrows) { + autoUpdaterMock.quitAndInstall.mockImplementationOnce(() => { + throw new Error('quit hook failed after staging') + }) + } + expect(() => nativeUpdaterMock.emit('update-downloaded')).not.toThrow() + nativeUpdaterMock.emit('update-downloaded') + autoUpdaterMock.emit('error', new Error('late unrelated error')) + expect(autoUpdaterMock.quitAndInstall).toHaveBeenCalledOnce() + expect(armWatchdog).toHaveBeenCalledOnce() + expect(failServeUpdateHandoffMock).not.toHaveBeenCalled() + expect(updater.isQuittingForUpdate()).toBe(true) + } + ) it('keeps the serving owner intact when the supervisor handoff cannot be persisted', async () => { const send = vi.fn() @@ -347,10 +518,6 @@ describe('headless serve update install handoff', () => { checkForUpdatesFromMenu() await vi.advanceTimersByTimeAsync(0) autoUpdaterMock.emit('update-downloaded', { version: '1.0.61' }) - const nativeReadyHandler = nativeUpdaterMock.on.mock.calls.find( - ([event]) => event === 'update-downloaded' - )?.[1] as (() => void) | undefined - nativeReadyHandler?.() quitAndInstall() await vi.advanceTimersByTimeAsync(100) diff --git a/src/main/updater.mac-install.test.ts b/src/main/updater.mac-install.test.ts index 563b8562fd6a..7e1f494ed98a 100644 --- a/src/main/updater.mac-install.test.ts +++ b/src/main/updater.mac-install.test.ts @@ -158,7 +158,12 @@ describe('updater mac install handoff', () => { // Flush microtasks so setAvailableVersion runs before update-downloaded fires. await new Promise((r) => setTimeout(r, 0)) downloadUpdate() + expect(autoUpdaterMock.autoInstallOnAppQuit).toBe(true) autoUpdaterMock.emit('update-downloaded', { version: '1.0.61' }) + expect(sendMock).not.toHaveBeenCalledWith( + 'updater:status', + expect.objectContaining({ state: 'downloaded' }) + ) const preventDefault = vi.fn() appMock.emit('before-quit', { preventDefault }) diff --git a/src/main/updater.ts b/src/main/updater.ts index 95c457b82c3b..945ee7d94cf0 100644 --- a/src/main/updater.ts +++ b/src/main/updater.ts @@ -93,6 +93,9 @@ export function dismissAvailableUpdate(): void { updater.dismissAvailableUpdate() } -export function setupAutoUpdater(mainWindow: BrowserWindow, opts?: UpdaterSetupOptions): void { +export function setupAutoUpdater( + mainWindow: BrowserWindow | null, + opts?: UpdaterSetupOptions +): void { updater.setupAutoUpdater(mainWindow, opts) } diff --git a/src/main/updater/profile-auto-updater.ts b/src/main/updater/profile-auto-updater.ts new file mode 100644 index 000000000000..f28c59659348 --- /dev/null +++ b/src/main/updater/profile-auto-updater.ts @@ -0,0 +1,51 @@ +import type { BrowserWindow } from 'electron' +import type { Store } from '../persistence' +import { logStartupMilestone } from '../startup/startup-diagnostics' +import { + setupAutoUpdater, + type PreQuitCleanupFailureMode, + type UpdateInstallMode +} from '../updater' + +export type ProfileAutoUpdaterOptions = { + onBeforeUpdateQuit?: () => void | Promise + onBeforeUpdateQuitFailure?: PreQuitCleanupFailureMode + updateInstallMode?: UpdateInstallMode +} + +export function configureProfileAutoUpdater( + mainWindow: BrowserWindow | null, + store: Store, + options?: ProfileAutoUpdaterOptions +): void { + setupAutoUpdater(mainWindow, { + getLastUpdateCheckAt: () => store.getUI().lastUpdateCheckAt, + onBeforeQuit: async () => { + try { + await options?.onBeforeUpdateQuit?.() + } finally { + await store.flushPendingAsync() + } + }, + setLastUpdateCheckAt: (timestamp) => { + store.updateUI({ lastUpdateCheckAt: timestamp }) + }, + getPendingUpdateNudgeId: () => store.getUI().pendingUpdateNudgeId ?? null, + getDismissedUpdateNudgeId: () => store.getUI().dismissedUpdateNudgeId ?? null, + setPendingUpdateNudgeId: (id) => { + // Why: only the apply branch also nulls dismissedUpdateVersion so relaunch can't resurrect the old hidden card; clearing must not, or it un-dismisses. + if (id) { + store.updateUI({ pendingUpdateNudgeId: id, dismissedUpdateVersion: null }) + } else { + store.updateUI({ pendingUpdateNudgeId: null }) + } + }, + setDismissedUpdateNudgeId: (id) => { + store.updateUI({ dismissedUpdateNudgeId: id }) + }, + getReleaseChannelOverride: () => store.getUI().releaseChannelOverride ?? null, + onBeforeQuitFailure: options?.onBeforeUpdateQuitFailure, + installMode: options?.updateInstallMode + }) + logStartupMilestone('updater-setup-done') +} diff --git a/src/main/updater/updater-download-install.ts b/src/main/updater/updater-download-install.ts index 455e56e6efac..ed4193de72d5 100644 --- a/src/main/updater/updater-download-install.ts +++ b/src/main/updater/updater-download-install.ts @@ -39,6 +39,7 @@ export abstract class UpdaterDownloadInstall extends UpdaterRemoteStatus { protected downloadUpdate(): void { if ( + this.isQuitAndInstallHandoffActive() || this.localBuildSelectionInProgress || this.pinnedBuildSelectionInProgress || this.downloadInFlight diff --git a/src/main/updater/updater-install-execution.ts b/src/main/updater/updater-install-execution.ts index 257f8e4fa934..d10fe7625c88 100644 --- a/src/main/updater/updater-install-execution.ts +++ b/src/main/updater/updater-install-execution.ts @@ -1,4 +1,4 @@ -import { BrowserWindow } from 'electron' +import { BrowserWindow, autoUpdater as nativeUpdater } from 'electron' import { killAllPty } from '../ipc/pty' import { withUpdaterSpan } from '../observability/instrumentation' import { runWithLaunchPath } from '../startup/hydrate-shell-path' @@ -105,6 +105,27 @@ export abstract class UpdaterInstallExecution extends UpdaterPackageRecovery { } // Why: mark before the call so a sync 'error' during quitAndInstall can recover; pre-native errors must not look like install failure. this.quitAndInstallNativeInvoked = true + if ( + process.platform === 'darwin' && + this.updateInstallMode === 'supervised-headless-serve' && + !isMacInstallerReady() + ) { + this.macNativeStaging = true + this.macNativeStagingTimer = setTimeout(() => { + this.macNativeStagingTimer = null + // Native staging cannot be cancelled; its durable handoff must survive late readiness. + this.sendInstallFailureStatus({ + state: 'error', + version: pendingVersion, + retryable: false, + message: + 'The server update is still being prepared. Orca remains running and will restart when preparation finishes. Do not start another update.' + }) + }, 120_000) + this.macNativeStagingTimer.unref?.() + nativeUpdater.checkForUpdates() + return + } // Why: invoke before killAllPty/removing close listeners so a sync 'error' can recover while windows and PTYs are intact. const supervisorOwnsRelaunch = this.updateInstallMode === 'supervised-headless-serve' runWithLaunchPath(() => @@ -172,13 +193,52 @@ export abstract class UpdaterInstallExecution extends UpdaterPackageRecovery { // A synchronous throw carries the same installer text the 'error' event would have. message: quitAndInstallNativeInvokedBeforeReset ? this.withInstallFailureCause(this.getPreCommitInstallFailureMessage(), error) - : 'Could not restart to install the update. Quit and reopen Orca, then try again.' + : this.updateInstallMode === 'supervised-headless-serve' + ? this.getPreCommitInstallFailureMessage() + : 'Could not restart to install the update. Quit and reopen Orca, then try again.' + }) + } + } + + protected handleMacNativeInstallReady(): void { + if (!this.macNativeStaging || !this.quitAndInstallInProgress) { + return + } + this.macNativeStaging = false + if (this.macNativeStagingTimer) { + clearTimeout(this.macNativeStagingTimer) + } + this.macNativeStagingTimer = null + this.updateInstallCommitted = true + armUpdateInstallExitWatchdog() + // A serve process can acquire desktop windows after startup. + for (const window of BrowserWindow.getAllWindows()) { + window.removeAllListeners('close') + } + // MacUpdater observes readiness before our listener, so this cannot add a pending quit callback. + try { + runWithLaunchPath(() => this.getAutoUpdater().quitAndInstall(true, false)) + } catch (error) { + recordUpdaterLifecycle('post_commit_cleanup_failed', { + errorType: error instanceof Error ? error.name : typeof error }) } } + protected handleMacNativeInstallError(error: unknown): void { + if (!this.macNativeStaging) { + return + } + this.macNativeStaging = false + this.handleQuitAndInstallFailure(error) + } + // Why: quitAndInstall failures arrive via 'error'; recover only after native invoke and before commit, else clearing quittingForUpdate lets dock activate reopen the old process mid-installer. protected handleQuitAndInstallFailure(error?: unknown): boolean { + // JS check errors cannot cancel an independently running native staging operation. + if (this.macNativeStaging) { + return true + } if ( !this.quitAndInstallInProgress || !this.quitAndInstallNativeInvoked || diff --git a/src/main/updater/updater-install-support.ts b/src/main/updater/updater-install-support.ts index 2da273e152e6..3903ea14c1f6 100644 --- a/src/main/updater/updater-install-support.ts +++ b/src/main/updater/updater-install-support.ts @@ -72,6 +72,11 @@ export abstract class UpdaterInstallSupport extends UpdaterCheckState { } protected resetQuitForUpdateState(): void { + this.macNativeStaging = false + if (this.macNativeStagingTimer) { + clearTimeout(this.macNativeStagingTimer) + } + this.macNativeStagingTimer = null this.quitAndInstallInProgress = false this.quittingForUpdate = false this.updateInstallCommitted = false @@ -85,6 +90,9 @@ export abstract class UpdaterInstallSupport extends UpdaterCheckState { * it — so keep that advice there. Everywhere else a restart is not known to help. */ protected getPreCommitInstallFailureMessage(): string { + if (this.updateInstallMode === 'supervised-headless-serve') { + return 'Could not install the server update. Orca remains running; download the update again to retry.' + } return process.platform === 'darwin' ? 'Could not restart to install the update. Quit and reopen Orca, then try again.' : 'Could not start the update installer. Orca remains open.' @@ -129,7 +137,7 @@ export abstract class UpdaterInstallSupport extends UpdaterCheckState { * recover it. Must run before `resetQuitForUpdateState()` clears the attempt diagnostic. */ protected isQuitAndInstallHandoffActive(): boolean { - return this.quitAndInstallInProgress + return this.pendingQuitAndInstallTimer !== null || this.quitAndInstallInProgress } protected async runBeforeUpdateQuitCleanup(): Promise { diff --git a/src/main/updater/updater-menu-checks.ts b/src/main/updater/updater-menu-checks.ts index 5e5294f29dc7..14629e06f802 100644 --- a/src/main/updater/updater-menu-checks.ts +++ b/src/main/updater/updater-menu-checks.ts @@ -7,6 +7,9 @@ import { UpdaterScheduling } from './updater-scheduling' /** Handles checks initiated from the desktop menu and modifier-key variants. */ export abstract class UpdaterMenuChecks extends UpdaterScheduling { protected checkForUpdatesFromMenu(options?: UpdateCheckOptions): void { + if (this.isQuitAndInstallHandoffActive()) { + return + } if (!app.isPackaged || is.dev) { this.sendStatus({ state: 'not-available', userInitiated: true }) return @@ -60,7 +63,7 @@ export abstract class UpdaterMenuChecks extends UpdaterScheduling { const attemptId = this.beginUpdateCheckAttempt() const autoUpdater = this.getAutoUpdater() const launch = (): Promise | undefined => { - if (!this.isActiveUpdateCheckAttempt(attemptId)) { + if (this.isQuitAndInstallHandoffActive() || !this.isActiveUpdateCheckAttempt(attemptId)) { return undefined } this.markUpdateCheckLaunched(attemptId) @@ -68,7 +71,7 @@ export abstract class UpdaterMenuChecks extends UpdaterScheduling { } const run = this.pinDefaultReleaseFeed(checkVariant).then((preflightResult) => { if (preflightResult === 'not-available') { - if (!this.isActiveUpdateCheckAttempt(attemptId)) { + if (this.isQuitAndInstallHandoffActive() || !this.isActiveUpdateCheckAttempt(attemptId)) { return false } this.userInitiatedCheck = false @@ -81,13 +84,13 @@ export abstract class UpdaterMenuChecks extends UpdaterScheduling { }) void Promise.resolve(run) .then((launchResult) => { - if (launchResult === false) { + if (this.isQuitAndInstallHandoffActive() || launchResult === false) { return } this.handleSettledUpdateCheckPromise(attemptId) }) .catch((err) => { - if (!this.isActiveUpdateCheckAttempt(attemptId)) { + if (this.isQuitAndInstallHandoffActive() || !this.isActiveUpdateCheckAttempt(attemptId)) { return } this.userInitiatedCheck = false diff --git a/src/main/updater/updater-nudge.ts b/src/main/updater/updater-nudge.ts index 765af67cd53e..614f31636ee3 100644 --- a/src/main/updater/updater-nudge.ts +++ b/src/main/updater/updater-nudge.ts @@ -7,6 +7,9 @@ import { UpdaterBuildSelection } from './updater-build-selection' /** Polls update campaigns and exposes their dismissal actions. */ export abstract class UpdaterNudge extends UpdaterBuildSelection { protected async checkForUpdateNudge(): Promise { + if (this.isQuitAndInstallHandoffActive()) { + return + } if (!app.isPackaged || is.dev) { return } @@ -21,7 +24,7 @@ export abstract class UpdaterNudge extends UpdaterBuildSelection { this.nudgeCheckInFlight = true try { const nudge = await fetchNudge() - if (!nudge) { + if (!nudge || this.isQuitAndInstallHandoffActive()) { return } if (this.currentStatus.state === 'checking' || this.currentStatus.state === 'downloading') { @@ -68,6 +71,9 @@ export abstract class UpdaterNudge extends UpdaterBuildSelection { /** Abandons an un-acted local or pinned update and restores the release feed. */ protected dismissAvailableUpdate(): void { + if (this.isQuitAndInstallHandoffActive()) { + return + } if (this.activeUpdateSource === 'release' && !this.isPinnedBuildActive) { return } diff --git a/src/main/updater/updater-scheduling.ts b/src/main/updater/updater-scheduling.ts index 954431385d08..2d06c81f7d9c 100644 --- a/src/main/updater/updater-scheduling.ts +++ b/src/main/updater/updater-scheduling.ts @@ -47,6 +47,7 @@ export abstract class UpdaterScheduling extends UpdaterCheckFailure { ): boolean { // Why: a pinned dev jump owns the feed until it settles; a background check would repoint it mid-flight and download the wrong build. if ( + this.isQuitAndInstallHandoffActive() || this.activeUpdateSource !== 'release' || this.isPinnedBuildActive || this.localBuildSelectionInProgress || @@ -69,7 +70,7 @@ export abstract class UpdaterScheduling extends UpdaterCheckFailure { const attemptId = this.beginUpdateCheckAttempt() const autoUpdater = this.getAutoUpdater() const launch = (): Promise | undefined => { - if (!this.isActiveUpdateCheckAttempt(attemptId)) { + if (this.isQuitAndInstallHandoffActive() || !this.isActiveUpdateCheckAttempt(attemptId)) { return undefined } this.markUpdateCheckLaunched(attemptId) @@ -77,9 +78,13 @@ export abstract class UpdaterScheduling extends UpdaterCheckFailure { } const run = this.pinDefaultReleaseFeed().then(launch) void Promise.resolve(run) - .then(() => this.handleSettledUpdateCheckPromise(attemptId)) + .then(() => { + if (!this.isQuitAndInstallHandoffActive()) { + this.handleSettledUpdateCheckPromise(attemptId) + } + }) .catch((err) => { - if (!this.isActiveUpdateCheckAttempt(attemptId)) { + if (this.isQuitAndInstallHandoffActive() || !this.isActiveUpdateCheckAttempt(attemptId)) { return } const wasUserInitiated = this.getSettledCheckUserInitiated() diff --git a/src/main/updater/updater-setup.ts b/src/main/updater/updater-setup.ts index 2b43964d5c0f..5b7629bfb88b 100644 --- a/src/main/updater/updater-setup.ts +++ b/src/main/updater/updater-setup.ts @@ -111,7 +111,7 @@ export class UpdaterSetup extends UpdaterDownloadInstall { super.dismissAvailableUpdate() } - setupAutoUpdater(mainWindow: BrowserWindow, opts?: UpdaterSetupOptions): void { + setupAutoUpdater(mainWindow: BrowserWindow | null, opts?: UpdaterSetupOptions): void { this.mainWindowRef = mainWindow this.onBeforeQuitCleanup = opts?.onBeforeQuit ?? null this.onBeforeQuitFailure = opts?.onBeforeQuitFailure ?? 'continue' @@ -170,6 +170,13 @@ export class UpdaterSetup extends UpdaterDownloadInstall { this.autoUpdaterInitialized = true registerAutoUpdaterHandlers({ + onNativeInstallEvent: (event, error) => { + if (event === 'ready') { + this.handleMacNativeInstallReady() + } else { + this.handleMacNativeInstallError(error) + } + }, autoUpdater, clearBackgroundCheckLaunchPending: () => this.clearBackgroundCheckLaunchPending(), clearAvailableUpdateContext: () => this.clearAvailableUpdateContext(), diff --git a/src/main/updater/updater-state.ts b/src/main/updater/updater-state.ts index 32177e3183c1..9348b39bd28e 100644 --- a/src/main/updater/updater-state.ts +++ b/src/main/updater/updater-state.ts @@ -65,6 +65,8 @@ export abstract class UpdaterState { // Why: once install has committed, late 'error' events must not clear quittingForUpdate — that would re-enable dock activate mid-installer. protected updateInstallCommitted = false // Why: recovery must only run after the native quitAndInstall call; pre-native errors must not clear quittingForUpdate or look like install recovery. + protected macNativeStaging = false + protected macNativeStagingTimer: ReturnType | null = null protected quitAndInstallNativeInvoked = false protected persistLastUpdateCheckAt: ((timestamp: number) => void) | null = null protected _getLastUpdateCheckAt: (() => number | null) | null = null diff --git a/src/main/window/attach-main-window-services.ts b/src/main/window/attach-main-window-services.ts index 5a7aab6b145e..8e97f754c28a 100644 --- a/src/main/window/attach-main-window-services.ts +++ b/src/main/window/attach-main-window-services.ts @@ -1,3 +1,4 @@ +import type { WslDaemonSessions } from '../wsl/wsl-daemon-sessions' import { ipcMain } from 'electron' import type { BrowserWindow, IpcMainInvokeEvent } from 'electron' import type { Store } from '../persistence' @@ -27,10 +28,9 @@ import type { OrcaRuntimeService, RuntimeWorktreeLifecycleEvent } from '../runti import type { PreQuitCleanupFailureMode, UpdateInstallMode } from '../updater' import { scheduleHistoryGc } from '../terminal-history-gc' import { hydrateLocalPtyRegistryAtBoot } from '../memory/hydrate-local-pty-registry' -import type { ClaudeRuntimeAuthPreparation } from '../claude-accounts/runtime-auth-service' +import type { PrepareClaudeAuth } from '../ipc/pty/host-env/types' import { getKnownWorktreeIdsForHistoryGc } from './history-gc-worktree-ids' import { isNativeFileDropPayload, type NativeFileDropPayload } from '../../shared/native-file-drop' -import type { ClaudeAccountSelectionTarget } from '../claude-accounts/runtime-selection' import { scheduleWorktreeBaseDirectoryWatcherSync, setWorktreeBaseDirectoryWatcherSyncContext @@ -51,10 +51,9 @@ export function attachMainWindowServices( store: Store, runtime: OrcaRuntimeService, getSelectedCodexHomePath?: GetSelectedCodexHomePath, - prepareClaudeAuth?: ( - target?: ClaudeAccountSelectionTarget - ) => Promise, + prepareClaudeAuth?: PrepareClaudeAuth, options?: { + wslDaemonSessions?: WslDaemonSessions prepareCodexSessionResume?: PrepareCodexSessionResume awaitLocalPtyStartup?: () => Promise awaitLocalPtyProviderStartup?: () => Promise @@ -93,6 +92,7 @@ export function attachMainWindowServices( store, { prepareCodexSessionResume: options?.prepareCodexSessionResume, + wslDaemonSessions: options?.wslDaemonSessions, awaitLocalPtyStartup: options?.awaitLocalPtyStartup, awaitLocalPtyProviderStartup: options?.awaitLocalPtyProviderStartup, isRecoveryReloadInFlight: options?.isRecoveryReloadInFlight, diff --git a/src/main/window/main-window-updater.ts b/src/main/window/main-window-updater.ts index 7cdcdd8cd732..141ec45f1059 100644 --- a/src/main/window/main-window-updater.ts +++ b/src/main/window/main-window-updater.ts @@ -4,7 +4,10 @@ import type { ReleaseBuildListResult, UpdateCheckOptions } from '../../shared/up import { RELEASE_CHANNELS, type ReleaseChannel } from '../../shared/release-channel' import { isTrustedUIRenderer } from '../ipc/ui' import type { Store } from '../persistence' -import { logStartupMilestone } from '../startup/startup-diagnostics' +import { + configureProfileAutoUpdater, + type ProfileAutoUpdaterOptions +} from '../updater/profile-auto-updater' import { checkForUpdatesFromMenu, dismissAvailableUpdate, @@ -14,10 +17,7 @@ import { getUpdateStatus, listAvailableReleaseBuilds, quitAndInstall, - setupAutoUpdater, - showLinuxPackage, - type PreQuitCleanupFailureMode, - type UpdateInstallMode + showLinuxPackage } from '../updater' const UPDATER_SETUP_FALLBACK_MS = 15_000 @@ -32,11 +32,7 @@ export function ensureAutoUpdaterConfigured(): void { export function scheduleMainWindowAutoUpdaterSetup( mainWindow: BrowserWindow, store: Store, - options?: { - onBeforeUpdateQuit?: () => void | Promise - onBeforeUpdateQuitFailure?: PreQuitCleanupFailureMode - updateInstallMode?: UpdateInstallMode - } + options?: ProfileAutoUpdaterOptions ): void { // Why: setupAutoUpdater sync-require()s electron-updater (slow on cold Windows w/ Defender, #7225), so defer past first paint; timer fallback covers crash-looping renderers. let updaterSetupDone = false @@ -45,36 +41,7 @@ export function scheduleMainWindowAutoUpdaterSetup( return } updaterSetupDone = true - setupAutoUpdater(mainWindow, { - getLastUpdateCheckAt: () => store.getUI().lastUpdateCheckAt, - onBeforeQuit: async () => { - try { - await options?.onBeforeUpdateQuit?.() - } finally { - await store.flushPendingAsync() - } - }, - setLastUpdateCheckAt: (timestamp) => { - store.updateUI({ lastUpdateCheckAt: timestamp }) - }, - getPendingUpdateNudgeId: () => store.getUI().pendingUpdateNudgeId ?? null, - getDismissedUpdateNudgeId: () => store.getUI().dismissedUpdateNudgeId ?? null, - setPendingUpdateNudgeId: (id) => { - // Why: only the apply branch also nulls dismissedUpdateVersion so relaunch can't resurrect the old hidden card; clearing must not, or it un-dismisses. - if (id) { - store.updateUI({ pendingUpdateNudgeId: id, dismissedUpdateVersion: null }) - } else { - store.updateUI({ pendingUpdateNudgeId: null }) - } - }, - setDismissedUpdateNudgeId: (id) => { - store.updateUI({ dismissedUpdateNudgeId: id }) - }, - getReleaseChannelOverride: () => store.getUI().releaseChannelOverride ?? null, - onBeforeQuitFailure: options?.onBeforeUpdateQuitFailure, - installMode: options?.updateInstallMode - }) - logStartupMilestone('updater-setup-done') + configureProfileAutoUpdater(mainWindow, store, options) } pendingAutoUpdaterSetup = setupAutoUpdaterDeferred mainWindow.once('ready-to-show', () => setImmediate(setupAutoUpdaterDeferred)) diff --git a/src/main/windows/windows-conpty-provider.test.ts b/src/main/windows/windows-conpty-provider.test.ts new file mode 100644 index 000000000000..856f3318799d --- /dev/null +++ b/src/main/windows/windows-conpty-provider.test.ts @@ -0,0 +1,51 @@ +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { resolveWindowsConptyProvider } from './windows-conpty-provider' + +vi.mock('../../shared/windows-conpty-release', async () => { + const { createHash } = await import('node:crypto') + return { + WINDOWS_CONPTY_FILES: Object.fromEntries( + ['x64', 'arm64'].map((arch) => [ + arch, + Object.fromEntries( + ['conpty.dll', 'OpenConsole.exe'].map((file) => [ + file, + createHash('sha256').update(`${arch}/${file}`).digest('hex') + ]) + ) + ]) + ) + } +}) +const roots: string[] = [] +afterEach(() => roots.splice(0).forEach((root) => rmSync(root, { recursive: true, force: true }))) +function fixture(arch: string): string { + const root = mkdtempSync(join(tmpdir(), 'orca-conpty-provider-')) + roots.push(root) + for (const file of ['conpty.dll', 'OpenConsole.exe']) { + writeFileSync(join(root, file), `${arch}/${file}`) + } + return root +} +describe('trusted Windows ConPTY provider', () => { + it.each(['x64', 'arm64'])('selects only the pinned %s pair', (arch) => { + const root = fixture(arch) + expect(resolveWindowsConptyProvider(root, arch)).toBe(join(root, 'conpty.dll')) + expect(() => resolveWindowsConptyProvider(root, arch === 'x64' ? 'arm64' : 'x64')).toThrow( + 'identity mismatch' + ) + }) + it.each(['conpty.dll', 'OpenConsole.exe'])('rejects a missing or corrupt %s', (file) => { + const root = fixture('x64') + rmSync(join(root, file)) + expect(() => resolveWindowsConptyProvider(root, 'x64')).toThrow() + writeFileSync(join(root, file), 'foreign bytes') + expect(() => resolveWindowsConptyProvider(root, 'x64')).toThrow('identity mismatch') + }) + it('rejects unknown architectures before opening files', () => { + expect(() => resolveWindowsConptyProvider('/missing', 'ia32')).toThrow('Unsupported') + }) +}) diff --git a/src/main/windows/windows-conpty-provider.ts b/src/main/windows/windows-conpty-provider.ts new file mode 100644 index 000000000000..183f3a38d6e3 --- /dev/null +++ b/src/main/windows/windows-conpty-provider.ts @@ -0,0 +1,21 @@ +import { createHash } from 'node:crypto' +import { readFileSync } from 'node:fs' +import { join, resolve } from 'node:path' +import { WINDOWS_CONPTY_FILES } from '../../shared/windows-conpty-release' + +/** Validate both halves before Bun can load the provider or start its console host. */ +export function resolveWindowsConptyProvider(directory: string, arch: string): string { + if (arch !== 'x64' && arch !== 'arm64') { + throw new Error(`Unsupported ConPTY architecture: ${arch}`) + } + const root = resolve(directory) + for (const [filename, expected] of Object.entries(WINDOWS_CONPTY_FILES[arch])) { + const actual = createHash('sha256') + .update(readFileSync(join(root, filename))) + .digest('hex') + if (actual !== expected) { + throw new Error(`Windows ConPTY identity mismatch: ${arch}/${filename}`) + } + } + return join(root, 'conpty.dll') +} diff --git a/src/main/windows/windows-host-job.win32.test.ts b/src/main/windows/windows-host-job.win32.test.ts index 941d81fd4d31..6b4046a3b9bd 100644 --- a/src/main/windows/windows-host-job.win32.test.ts +++ b/src/main/windows/windows-host-job.win32.test.ts @@ -1,7 +1,8 @@ -import { mkdtempSync, writeFileSync } from 'node:fs' +import { existsSync, mkdtempSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' -import { spawn } from 'node:child_process' +import { spawnProcess } from '../../shared/child-process/run-process' +import { ORCAD_BUN_VERSION } from '../../shared/orcad-bun-runtime' import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { removeTreeSync } from '../../shared/windows-transient-lock-removal' @@ -55,18 +56,27 @@ describeOnWindows('host job reaps the tree when the host dies', () => { }) it('kills a pty and its detached grandchild when the host is force-killed', async () => { - const nodePtyDir = join(process.cwd(), 'node_modules', 'node-pty') + const runtime = [ + process.env.BUN_EXECUTABLE, + join(process.cwd(), 'out/orcad/bun-runtime.exe'), + join(process.cwd(), 'out/cli-runtime', `win32-${process.arch}`, 'bun-runtime.exe') + ].find((entry): entry is string => Boolean(entry && existsSync(entry))) + if (!runtime) { + throw new Error('Pinned bundled Bun runtime is required') + } + const source = join(process.cwd(), 'src/main/daemon/pty-subprocess') const hostScript = join(dir, 'host.js') // The host assigns itself, then opens a pty whose shell spawns a DETACHED // grandchild — the process a parent-pid walk cannot see. writeFileSync( hostScript, [ - `const pty = require(${JSON.stringify(nodePtyDir)});`, - `const { loadNativeModule } = require(${JSON.stringify(`${nodePtyDir}/lib/utils`)});`, - `const native = loadNativeModule('conpty').module;`, - `console.log('ASSIGNED=' + native.assignCurrentProcessToJob());`, - `const term = pty.spawn('cmd.exe', [], { name: 'xterm', cols: 80, rows: 30, cwd: ${JSON.stringify(dir)}, useConptyDll: true });`, + `if (process.versions.bun !== ${JSON.stringify(ORCAD_BUN_VERSION)}) throw new Error('Unexpected Bun runtime');`, + `const { spawnBunPty } = require(${JSON.stringify(join(source, 'bun-pty-process.ts'))});`, + `const { createWindowsBunPtyLaunch } = require(${JSON.stringify(join(source, 'windows-bun-pty-launch.ts'))});`, + `const { assignCurrentProcessToBunPtyHostJob } = require(${JSON.stringify(join(source, 'windows-bun-pty-job.ts'))});`, + `console.log('ASSIGNED=' + assignCurrentProcessToBunPtyHostJob());`, + `const term = spawnBunPty({file: process.env.ComSpec || 'cmd.exe', args: [], env: process.env, cols: 80, rows: 30, cwd: ${JSON.stringify(dir)}}, {createWindowsLaunch: args => createWindowsBunPtyLaunch(args, {workerPath: ${JSON.stringify(join(source, 'windows-bun-pty-gate-entry.ts'))}})});`, `term.onData((d) => { const m = /GC=(\\d+)/.exec(d); if (m) console.log('GRANDCHILD=' + m[1]); });`, `term.write('node -e "const{spawn}=require(\\'child_process\\');const c=spawn(process.execPath,[\\'-e\\',\\'setInterval(()=>{},1000)\\'],{detached:true,windowsHide:true,stdio:\\'ignore\\'});c.unref();console.log(\\'GC=\\'+c.pid);"\\r');`, `setTimeout(() => console.log('SHELL=' + term.pid), 4000);`, @@ -74,7 +84,11 @@ describeOnWindows('host job reaps the tree when the host dies', () => { ].join('\n') ) - const host = spawn(process.execPath, [hostScript], { stdio: ['ignore', 'pipe', 'pipe'] }) + const host = spawnProcess({ + program: runtime, + args: ['--no-env-file', hostScript], + stdio: ['ignore', 'pipe', 'pipe'] + }) let output = '' host.stdout.on('data', (chunk) => { output += String(chunk) @@ -82,21 +96,23 @@ describeOnWindows('host job reaps the tree when the host dies', () => { host.stdout.on('error', () => {}) host.stderr?.on('error', () => {}) - for (let attempt = 0; attempt < 60 && !/SHELL=/.test(output); attempt += 1) { - await sleep(250) - } + let shellPid = 0 + let grandchildPid = 0 + try { + for (let attempt = 0; attempt < 60 && !/SHELL=/.test(output); attempt += 1) { + await sleep(250) + } - expect(output).toContain('ASSIGNED=true') - const shellPid = Number(/SHELL=(\d+)/.exec(output)?.[1]) - const grandchildPid = Number(/GRANDCHILD=(\d+)/.exec(output)?.[1]) - expect(Number.isInteger(shellPid)).toBe(true) - expect(Number.isInteger(grandchildPid)).toBe(true) - expect(isAlive(grandchildPid)).toBe(true) + expect(output).toContain('ASSIGNED=true') + shellPid = Number(/SHELL=(\d+)/.exec(output)?.[1]) + grandchildPid = Number(/GRANDCHILD=(\d+)/.exec(output)?.[1]) + expect(Number.isInteger(shellPid)).toBe(true) + expect(Number.isInteger(grandchildPid)).toBe(true) + expect(isAlive(grandchildPid)).toBe(true) - // Force-kill only the host: no tree kill, nothing given a chance to unwind. - // This is the daemon-crash shape. - process.kill(host.pid!, 'SIGKILL') - try { + // Force-kill only the host: no tree kill, nothing given a chance to unwind. + // This is the daemon-crash shape. + process.kill(host.pid!, 'SIGKILL') const [shellExited, grandchildExited] = await Promise.all([ waitForProcessExit(shellPid, 15_000), waitForProcessExit(grandchildPid, 15_000) @@ -104,7 +120,10 @@ describeOnWindows('host job reaps the tree when the host dies', () => { expect(shellExited).toBe(true) expect(grandchildExited).toBe(true) } finally { - for (const pid of [shellPid, grandchildPid]) { + for (const pid of [host.pid, shellPid, grandchildPid]) { + if (!pid || pid <= 0) { + continue + } try { process.kill(pid) } catch { diff --git a/src/main/windows/windows-msys-job.win32.test.ts b/src/main/windows/windows-msys-job.win32.test.ts index 9bf5485102e9..4267091c3c07 100644 --- a/src/main/windows/windows-msys-job.win32.test.ts +++ b/src/main/windows/windows-msys-job.win32.test.ts @@ -1,3 +1,5 @@ +import { createWindowsBunPtyLaunch } from '../daemon/pty-subprocess/windows-bun-pty-launch' +import { canUseBunPty, spawnBunPty } from '../daemon/pty-subprocess/bun-pty-process' import { mkdtempSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' @@ -7,7 +9,15 @@ import { resolveGitBashPath } from '../git-bash' import { quotePosixShell } from '../../shared/wsl-login-shell-command' import { listPtyJobProcessIds, terminatePtyJob } from './windows-pty-job' -const describeOnWindows = process.platform === 'win32' ? describe : describe.skip +const spawnTestPty: typeof spawnBunPty = (args) => + spawnBunPty(args, { + createWindowsLaunch: (launch) => + createWindowsBunPtyLaunch(launch, { + workerPath: join(__dirname, '../daemon/pty-subprocess/windows-bun-pty-gate-entry.ts') + }) + }) + +const describeOnWindows = process.platform === 'win32' && canUseBunPty() ? describe : describe.skip function isAlive(pid: number): boolean { try { @@ -28,12 +38,17 @@ describeOnWindows('MSYS terminal job ownership', () => { script, "console.log('MSYS_OWNED_CHILD=' + process.pid); setInterval(() => {}, 1000)\n" ) - const pty = await import('node-pty') - const proc = pty.spawn(shell!, ['-c', 'exec "$BASH" --noprofile --norc -i'], { + const proc = spawnTestPty({ + file: shell!, + args: ['-c', 'exec "$BASH" --noprofile --norc -i'], + env: Object.fromEntries( + Object.entries(process.env).filter( + (entry): entry is [string, string] => entry[1] !== undefined + ) + ), cwd: tmpdir(), cols: 120, - rows: 30, - useConptyDll: true + rows: 30 }) let output = '' let childPid: number | undefined @@ -49,6 +64,7 @@ describeOnWindows('MSYS terminal job ownership', () => { } }) try { + await proc.waitForSpawn?.() proc.write( `${quotePosixShell(process.execPath.replace(/\\/g, '/'))} ${quotePosixShell(script.replace(/\\/g, '/'))}\r` ) diff --git a/src/main/windows/windows-process-tree-runtime-manifest.ts b/src/main/windows/windows-process-tree-runtime-manifest.ts new file mode 100644 index 000000000000..bae0668f15d7 --- /dev/null +++ b/src/main/windows/windows-process-tree-runtime-manifest.ts @@ -0,0 +1,15 @@ +export const WINDOWS_PROCESS_TREE_REQUIRED = [ + 'package.json', + 'lib/index.js', + 'build/Release/windows_process_tree.node' +] as const + +export function isRuntimeProcessTreePath(packageRel: string): boolean { + return ( + packageRel === '' || + packageRel.startsWith('lib/') || + WINDOWS_PROCESS_TREE_REQUIRED.some( + (required) => required === packageRel || required.startsWith(`${packageRel}/`) + ) + ) +} diff --git a/src/main/windows/windows-pty-job.test.ts b/src/main/windows/windows-pty-job.test.ts index 6210ac9d0469..3bb0cd6de2f2 100644 --- a/src/main/windows/windows-pty-job.test.ts +++ b/src/main/windows/windows-pty-job.test.ts @@ -1,206 +1,84 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { IPty } from 'node-pty' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { mockPtyProcess } from '../daemon/pty-subprocess-test-harness' +import { listPtyJobProcessIds, ptyShellProcessId, terminatePtyJob } from './windows-pty-job' -const { recordSelfInitiatedTreeKillMock } = vi.hoisted(() => ({ - recordSelfInitiatedTreeKillMock: vi.fn() -})) +const { recordKill } = vi.hoisted(() => ({ recordKill: vi.fn() })) vi.mock('../crash-reporting/self-initiated-tree-kill-log', () => ({ - recordSelfInitiatedTreeKill: recordSelfInitiatedTreeKillMock + recordSelfInitiatedTreeKill: recordKill })) - -import { - __setConptyJobNativeForTests, - assignHostProcessToKillOnCloseJob, - isPtyJobOwnershipAvailable, - listPtyJobProcessIds, - terminatePtyJob -} from './windows-pty-job' - -const ptyWithHandle = (id: unknown, pid = 4242): IPty => ({ _pty: id, pid }) as unknown as IPty - beforeEach(() => { - recordSelfInitiatedTreeKillMock.mockReset() -}) - -afterEach(() => { - __setConptyJobNativeForTests() + recordKill.mockReset() }) -describe('terminatePtyJob', () => { - it('terminates the job for a pty that has one', () => { - const terminateJob = vi.fn().mockReturnValue(true) - __setConptyJobNativeForTests(() => ({ - terminateJob, - listJobProcessIds: vi.fn(), - assignCurrentProcessToJob: vi.fn().mockReturnValue(true) - })) - - expect(terminatePtyJob(ptyWithHandle(7))).toBe('terminated') - expect(terminateJob).toHaveBeenCalledWith(7, 4242) - }) - - it.each([ - ['no job assigned', false], - ['native refusal', false] - ])('reports unavailable rather than success on %s', (_case, nativeResult) => { - // Why this matters: "we could not tell" is precisely the state the old - // parent-pid probe returned, and treating it as success is how a live - // agent tree was declared dead and left holding the worktree open. - __setConptyJobNativeForTests(() => ({ - terminateJob: vi.fn().mockReturnValue(nativeResult), - listJobProcessIds: vi.fn(), - assignCurrentProcessToJob: vi.fn().mockReturnValue(true) - })) - expect(terminatePtyJob(ptyWithHandle(7))).toBe('unavailable') - }) - - it.each([ - ['a POSIX terminal with no handle id', undefined], - ['a non-integer handle id', 'nope'] - ])('reports unavailable for %s', (_case, id) => { - __setConptyJobNativeForTests(() => ({ - terminateJob: vi.fn().mockReturnValue(true), - listJobProcessIds: vi.fn(), - assignCurrentProcessToJob: vi.fn().mockReturnValue(true) - })) - expect(terminatePtyJob(ptyWithHandle(id))).toBe('unavailable') - }) - - it('passes the shell pid so an id from another backend cannot match', () => { - // winpty mints `pty` ids from its own counter and the JS layer stores both - // in the same field, so an id alone can name a live ConPTY pane. The native - // side refuses on a pid mismatch; this pins that we always send the pid. - const terminateJob = vi.fn().mockReturnValue(true) - __setConptyJobNativeForTests(() => ({ - terminateJob, - listJobProcessIds: vi.fn(), - assignCurrentProcessToJob: vi.fn().mockReturnValue(true) - })) - - terminatePtyJob(ptyWithHandle(3, 9001)) - expect(terminateJob).toHaveBeenCalledWith(3, 9001) - }) - - it('reports unavailable when the shell pid is unusable', () => { - __setConptyJobNativeForTests(() => ({ - terminateJob: vi.fn().mockReturnValue(true), - listJobProcessIds: vi.fn(), - assignCurrentProcessToJob: vi.fn().mockReturnValue(true) - })) - expect(terminatePtyJob(ptyWithHandle(3, 0))).toBe('unavailable') - }) - - it('reports unavailable when this build has no job support', () => { - // A node-pty rebuilt from unpatched sources exports neither symbol. - __setConptyJobNativeForTests(() => null) - expect(terminatePtyJob(ptyWithHandle(7))).toBe('unavailable') - expect(isPtyJobOwnershipAvailable()).toBe(false) +describe('owned Windows PTY jobs', () => { + it('does not infer job ownership from a PID or an obsolete native handle', () => { + const proc = { ...mockPtyProcess(4242), _pty: 7 } + expect(terminatePtyJob(proc)).toBe('unavailable') + expect(listPtyJobProcessIds(proc)).toBeNull() + expect(recordKill).not.toHaveBeenCalled() }) - it('reports unavailable when the native call throws', () => { - __setConptyJobNativeForTests(() => ({ - terminateJob: vi.fn().mockImplementation(() => { - throw new Error('handle closed') - }), - listJobProcessIds: vi.fn(), - assignCurrentProcessToJob: vi.fn().mockReturnValue(true) - })) - expect(terminatePtyJob(ptyWithHandle(7))).toBe('unavailable') - }) -}) - -describe('listPtyJobProcessIds', () => { - it('returns the live pids, including a detached grandchild', () => { - // Measured on Windows 11: a grandchild spawned detached leaves the console - // and reparents, so neither GetConsoleProcessList nor a parent-pid walk - // sees it. Job membership does. - __setConptyJobNativeForTests(() => ({ - terminateJob: vi.fn(), - listJobProcessIds: vi.fn().mockReturnValue([107184, 91480]), - assignCurrentProcessToJob: vi.fn().mockReturnValue(true) - })) - expect(listPtyJobProcessIds(ptyWithHandle(1))).toEqual([107184, 91480]) - }) - - it('reports null when there is no answer to give', () => { - // Null is 'unverifiable', never 'they died'. A dead tree also reads null, - // because node-pty drops its handle record on exit -- so a caller that - // treats null as proof of death would be right by accident here and wrong - // on a host that refused the job assignment. - __setConptyJobNativeForTests(() => null) - expect(listPtyJobProcessIds(ptyWithHandle(1))).toBeNull() - - __setConptyJobNativeForTests(() => ({ - terminateJob: vi.fn(), - listJobProcessIds: vi.fn().mockReturnValue(null), - assignCurrentProcessToJob: vi.fn().mockReturnValue(true) - })) - expect(listPtyJobProcessIds(ptyWithHandle(1))).toBeNull() - }) -}) - -describe('assignHostProcessToKillOnCloseJob', () => { - it('reports whether the host process is now kill-on-close', () => { - const assignCurrentProcessToJob = vi.fn().mockReturnValue(true) - __setConptyJobNativeForTests(() => ({ - terminateJob: vi.fn(), - listJobProcessIds: vi.fn(), - assignCurrentProcessToJob - })) - expect(assignHostProcessToKillOnCloseJob()).toBe(true) - }) - - it('degrades to false when the OS refuses the assignment', () => { - // An outer job that forbids nesting; orphan behaviour is then simply what - // it was before, rather than a crash. - __setConptyJobNativeForTests(() => ({ - terminateJob: vi.fn(), - listJobProcessIds: vi.fn(), - assignCurrentProcessToJob: vi.fn().mockReturnValue(false) - })) - expect(assignHostProcessToKillOnCloseJob()).toBe(false) - - __setConptyJobNativeForTests(() => null) - expect(assignHostProcessToKillOnCloseJob()).toBe(false) - }) -}) - -describe('PTY Job Object teardown breadcrumbs', () => { - function withNative(terminateJob: () => boolean): void { - __setConptyJobNativeForTests(() => ({ - terminateJob, - listJobProcessIds: vi.fn(), - assignCurrentProcessToJob: vi.fn().mockReturnValue(true) - })) - } - - it('records the shell pid whose job it terminated', () => { - withNative(() => true) - - expect(terminatePtyJob(ptyWithHandle(7, 4242))).toBe('terminated') - expect(recordSelfInitiatedTreeKillMock).toHaveBeenCalledWith({ + it('uses retained ownership and records confirmed termination', () => { + const terminateOwnedTree = vi.fn(() => 'terminated' as const) + const proc = { ...mockPtyProcess(4242), terminateOwnedTree } + expect(terminatePtyJob(proc)).toBe('terminated') + expect(terminateOwnedTree).toHaveBeenCalledOnce() + expect(recordKill).toHaveBeenCalledWith({ pid: 4242, site: 'windows-pty-job-teardown', scope: 'win-pty-job' }) }) - it('records nothing when the native side refused', () => { - withNative(() => false) - - expect(terminatePtyJob(ptyWithHandle(7))).toBe('unavailable') - expect(recordSelfInitiatedTreeKillMock).not.toHaveBeenCalled() + it.each(['refused', 'throws'])('keeps %s termination unverifiable', (mode) => { + const proc = { + ...mockPtyProcess(4242), + terminateOwnedTree: () => { + if (mode === 'throws') { + throw new Error('job unavailable') + } + return 'unavailable' as const + } + } + expect(terminatePtyJob(proc)).toBe('unavailable') + expect(recordKill).not.toHaveBeenCalled() }) - it('never downgrades a real termination to unavailable because diagnostics failed', () => { - // `unavailable` escalates callers to the pid-addressed taskkill this - // instrumentation exists to constrain, so the breadcrumb must sit outside - // the native call's catch. - withNative(() => true) - recordSelfInitiatedTreeKillMock.mockImplementation(() => { - throw new Error('breadcrumb store exploded') + it('does not downgrade confirmed termination if diagnostics throw', () => { + recordKill.mockImplementation(() => { + throw new Error('diagnostic failure') }) + const proc = { ...mockPtyProcess(4242), terminateOwnedTree: () => 'terminated' as const } + expect(() => terminatePtyJob(proc)).toThrow('diagnostic failure') + }) + + it('preserves live, empty and unverifiable job results', () => { + for (const result of [[4242, 4243], [], null]) { + expect( + listPtyJobProcessIds({ ...mockPtyProcess(4242), listOwnedProcessIds: () => result }) + ).toEqual(result) + } + expect( + listPtyJobProcessIds({ + ...mockPtyProcess(4242), + listOwnedProcessIds: () => { + throw new Error('unreadable') + } + }) + ).toBeNull() + }) - expect(() => terminatePtyJob(ptyWithHandle(7))).toThrow('breadcrumb store exploded') + it('never substitutes a Windows gate PID for an unknown shell', () => { + expect( + ptyShellProcessId({ ...mockPtyProcess(4242), jobRootProcessIsWrapper: true }) + ).toBeUndefined() + expect( + ptyShellProcessId({ + ...mockPtyProcess(4242), + jobRootProcessIsWrapper: true, + shellProcessId: 4243 + }) + ).toBe(4243) + expect(ptyShellProcessId(mockPtyProcess(4242))).toBe(4242) }) }) diff --git a/src/main/windows/windows-pty-job.ts b/src/main/windows/windows-pty-job.ts index 814f2dbaa8ab..17908e66431b 100644 --- a/src/main/windows/windows-pty-job.ts +++ b/src/main/windows/windows-pty-job.ts @@ -1,41 +1,11 @@ -import type { IPty } from 'node-pty' +import type { TerminalProcess } from '../../shared/terminal-process' import { canUseBunPty } from '../daemon/pty-subprocess/bun-pty-process-capabilities' import { loadWindowsBunPtyJobNative } from '../daemon/pty-subprocess/windows-bun-pty-native' -import { createRequire } from 'node:module' import { recordSelfInitiatedTreeKill } from '../crash-reporting/self-initiated-tree-kill-log' -/** - * Job-object ownership for a ConPTY's process tree. - * - * Before this, Orca answered "is this tree mine, and how do I kill it?" by - * scraping the process table, walking parent pids back to itself, and then - * running `taskkill /T /F` only if the walk said yes. Every step of that is a - * guess: - * - * - a pid walk cannot survive pid reuse, so teardown had to refuse whenever it - * was unsure, and a refused kill is an orphaned agent tree holding the - * worktree directory open (#9045, #10475, #10087); - * - a descendant that reparented is invisible to the walk entirely; - * - the scrape itself could be blocked by policy, which read as "no evidence". - * - * A job object replaces the inference with a handle. node-pty puts each shell - * into its own job at creation, before it can spawn anything, so membership is - * the kernel's answer rather than ours. - * - * The job is not kill-on-close: it makes an explicit teardown exact, and - * deliberately does not change what a clean shell exit means for anything the - * user backgrounded. - */ - -const requireFromMain = createRequire(__filename) - -type ConptyNative = { - terminateJob: (id: number, shellPid: number) => boolean - listJobProcessIds: (id: number, shellPid: number) => number[] | null - assignCurrentProcessToJob: () => boolean -} +export type JobTerminationOutcome = 'terminated' | 'unavailable' -type SelfOwnedPty = IPty & { +type SelfOwnedPty = Pick & { jobRootProcessIsWrapper?: true shellProcessId?: number terminateOwnedTree?: () => JobTerminationOutcome @@ -43,202 +13,43 @@ type SelfOwnedPty = IPty & { } /** The gate's pid never proves that its user shell remains alive. */ -export function ptyShellProcessId(proc: IPty): number | undefined { +export function ptyShellProcessId(proc: SelfOwnedPty): number | undefined { const owned: SelfOwnedPty = proc return owned.jobRootProcessIsWrapper ? owned.shellProcessId : proc.pid } -let cachedNative: ConptyNative | null | undefined -let nativeLoader: () => ConptyNative | null = loadConptyNative - -function loadConptyNative(): ConptyNative | null { - if (cachedNative !== undefined) { - return cachedNative - } - if (process.platform !== 'win32') { - cachedNative = null - return cachedNative - } - try { - const { loadNativeModule } = requireFromMain('node-pty/lib/utils') as { - loadNativeModule: (name: string) => { module: unknown } - } - const native = loadNativeModule('conpty').module as Partial - // Why feature-detect: a node-pty rebuilt from unpatched sources exports - // neither symbol, and calling through would throw on every teardown. - cachedNative = - typeof native?.terminateJob === 'function' && typeof native?.listJobProcessIds === 'function' - ? (native as ConptyNative) - : null - } catch { - cachedNative = null - } - return cachedNative -} - -/** - * node-pty's per-terminal handle id, paired with the shell pid that proves it. - * - * Neither is on the public `IPty` surface, so read defensively. The pid is not - * belt-and-braces: the winpty backend mints `pty` ids from its own counter and - * the JS layer stores both in the same field, so an id alone can name a - * different, live ConPTY pane. The native side refuses on a pid mismatch. - */ -function ptyJobTarget(proc: IPty): { id: number; shellPid: number } | null { - const id = (proc as unknown as { _pty?: unknown })._pty - const shellPid = proc.pid - if (!Number.isInteger(id) || !Number.isInteger(shellPid) || (shellPid as number) <= 0) { - return null - } - return { id: id as number, shellPid: shellPid as number } -} - -export type JobTerminationOutcome = 'terminated' | 'unavailable' - -/** - * Kill a PTY's entire process tree. - * - * Returns `unavailable` — never a false `terminated` — when the tree has no - * job. Callers must degrade to the older best-effort path rather than treat - * that as success, because "we could not tell" is exactly the state that used - * to be misread as "nothing to kill". - */ -export function terminatePtyJob(proc: IPty): JobTerminationOutcome { +/** Only the terminal's retained job handle can authorize whole-tree termination. */ +export function terminatePtyJob(proc: SelfOwnedPty): JobTerminationOutcome { const owned: SelfOwnedPty = proc - if (typeof owned.terminateOwnedTree === 'function') { - let outcome: JobTerminationOutcome - try { - outcome = owned.terminateOwnedTree() - } catch { - return 'unavailable' - } - if (outcome === 'terminated') { - recordSelfInitiatedTreeKill({ - pid: proc.pid, - site: 'windows-pty-job-teardown', - scope: 'win-pty-job' - }) - } - return outcome - } - const target = ptyJobTarget(proc) - const native = nativeLoader() - if (!target || !native) { + if (!owned.terminateOwnedTree) { return 'unavailable' } - let terminated: boolean + let outcome: JobTerminationOutcome try { - terminated = native.terminateJob(target.id, target.shellPid) + outcome = owned.terminateOwnedTree() } catch { return 'unavailable' } - if (!terminated) { - return 'unavailable' + if (outcome === 'terminated') { + recordSelfInitiatedTreeKill({ + pid: proc.pid, + site: 'windows-pty-job-teardown', + scope: 'win-pty-job' + }) } - // Outside the try: that catch is the native-refusal contract, and a throw from - // the breadcrumb path would downgrade a real termination to `unavailable`, - // escalating callers to the pid-addressed taskkill this instrumentation exists - // to constrain. - recordSelfInitiatedTreeKill({ - pid: target.shellPid, - site: 'windows-pty-job-teardown', - scope: 'win-pty-job' - }) - return 'terminated' + return outcome } -/** - * Pids still alive in a PTY's tree, or null when there is no answer. - * - * Measured on Windows 11: once the shell exits, node-pty closes the job, so a - * terminated tree reports **null**, not `[]`. (Its handle record now outlives - * the shell until `kill()` runs — see config/patches/node-pty@1.1.0.patch — but - * the nulled job handle is what makes the answer null either way.) - * Null therefore means "unverifiable" in the sense of - * docs/reference/ssh-execution-boundary.md — this build has no job support, - * the terminal is not a ConPTY, or it is no longer tracked. It is never - * evidence that processes died. - * - * The value this does add is descendant liveness for a tree that IS still - * tracked: a pane whose shell is alive can be asked what is running under it, - * including children that detached from the console. - */ -export function listPtyJobProcessIds(proc: IPty): readonly number[] | null { +/** A missing or unreadable job is unverifiable, never evidence that its processes exited. */ +export function listPtyJobProcessIds(proc: SelfOwnedPty): readonly number[] | null { const owned: SelfOwnedPty = proc - if (typeof owned.listOwnedProcessIds === 'function') { - try { - return owned.listOwnedProcessIds() - } catch { - return null - } - } - const target = ptyJobTarget(proc) - const native = nativeLoader() - if (!target || !native) { - return null - } try { - return native.listJobProcessIds(target.id, target.shellPid) + return owned.listOwnedProcessIds?.() ?? null } catch { return null } } -/** - * Put this process in a kill-on-close job so its descendants die with it. - * - * Why this is separate from the per-PTY jobs: those cannot carry - * KILL_ON_JOB_CLOSE, because their handle is released when the shell exits and - * that would reap whatever the user had backgrounded. This one is released only - * when the process itself dies, so it reaps a crashed host without changing - * what a clean exit means. Children inherit membership, so the per-PTY jobs - * nest inside it and every pty is covered. - * - * Call it from the pty spawn path, not from startup: resolving the native - * module loads the ConPTY addon, and paying that before the daemon publishes - * its endpoint delays readiness — the boot smoke test caught exactly that. - * - * Call it BEFORE the first pty. `AssignProcessToJobObject` adds only the named - * process; children inherit membership, but a pty that already exists does not - * join retroactively and would not be reaped. - * - * Call it from the terminal daemon, not from the app: an app-main crash must - * still leave sessions alive, which is what win-crash-survival-e2e asserts. A - * daemon death reaping its shells is the intended change (#9195, #10415). - * - * Returns false when the OS refuses -- an outer job that forbids nesting -- in - * which case orphan behaviour is simply what it was before. - */ -let hostJobAssigned: boolean | null = null - -export function assignHostProcessToKillOnCloseJob(): boolean { - if (hostJobAssigned !== null) { - return hostJobAssigned - } - hostJobAssigned = assignHostProcessOnce() - return hostJobAssigned -} - -function assignHostProcessOnce(): boolean { - const native = nativeLoader() - if (!native) { - return false - } - try { - return native.assignCurrentProcessToJob() - } catch { - return false - } -} - -/** Whether this build can own PTY trees with job objects at all. */ export function isPtyJobOwnershipAvailable(): boolean { - return canUseBunPty() ? loadWindowsBunPtyJobNative() !== null : nativeLoader() !== null -} - -/** Test-only: substitute the native module (it is resolved via createRequire). */ -export function __setConptyJobNativeForTests(loader?: () => ConptyNative | null): void { - nativeLoader = loader ?? loadConptyNative - cachedNative = undefined - hostJobAssigned = null + return canUseBunPty() && loadWindowsBunPtyJobNative() !== null } diff --git a/src/main/windows/windows-pty-job.win32.test.ts b/src/main/windows/windows-pty-job.win32.test.ts index cb15463bff85..c59d919a8212 100644 --- a/src/main/windows/windows-pty-job.win32.test.ts +++ b/src/main/windows/windows-pty-job.win32.test.ts @@ -1,8 +1,10 @@ +import { createWindowsBunPtyLaunch } from '../daemon/pty-subprocess/windows-bun-pty-launch' import { existsSync, mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it, vi } from 'vitest' -import type { IPty } from 'node-pty' +import { canUseBunPty, spawnBunPty } from '../daemon/pty-subprocess/bun-pty-process' +import type { BunPtyProcess } from '../daemon/pty-subprocess/bun-pty-process-contract' import { runProcess } from '../../shared/child-process/run-process' import { isPtyJobOwnershipAvailable, @@ -17,11 +19,19 @@ import { * neither `GetConsoleProcessList` nor a parent-pid walk can see it. That is the * process that outlived its pane and held the worktree directory open * (#9045, #10475, #10897). Job membership is the only mechanism that finds it, - * so the assertion below is the whole justification for the node-pty patch. + * so the assertion below is the reason this runtime must retain exact job ownership. * * Runs only on win32; skipped elsewhere. */ -const describeOnWindows = process.platform === 'win32' ? describe : describe.skip +const spawnTestPty: typeof spawnBunPty = (args) => + spawnBunPty(args, { + createWindowsLaunch: (launch) => + createWindowsBunPtyLaunch(launch, { + workerPath: join(__dirname, '../daemon/pty-subprocess/windows-bun-pty-gate-entry.ts') + }) + }) + +const describeOnWindows = process.platform === 'win32' && canUseBunPty() ? describe : describe.skip const sleep = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)) @@ -44,7 +54,7 @@ async function waitUntilDead(pid: number, timeoutMs = 30_000): Promise { } describeOnWindows('ConPTY job ownership', () => { - const spawned: IPty[] = [] + const spawned: BunPtyProcess[] = [] afterEach(() => { for (const proc of spawned.splice(0)) { @@ -57,18 +67,23 @@ describeOnWindows('ConPTY job ownership', () => { }) async function spawnShellWithDetachedGrandchild(): Promise<{ - proc: IPty + proc: BunPtyProcess grandchildPid: number }> { - const nodePty = await import('node-pty') - const proc = nodePty.spawn('cmd.exe', [], { - name: 'xterm-256color', + const proc = spawnTestPty({ + file: process.env.ComSpec || 'cmd.exe', + args: [], + env: Object.fromEntries( + Object.entries(process.env).filter( + (entry): entry is [string, string] => entry[1] !== undefined + ) + ), cols: 80, rows: 30, - cwd: process.cwd(), - useConptyDll: true + cwd: process.cwd() }) spawned.push(proc) + await proc.waitForSpawn?.() let grandchildPid: number | null = null proc.onData((chunk) => { @@ -84,7 +99,7 @@ describeOnWindows('ConPTY job ownership', () => { "{detached:true,windowsHide:true,stdio:'ignore'});", "c.unref();console.log('ORCA_GC='+c.pid);" ].join('') - proc.write(`node -e "${script}"\r`) + proc.write(`"${process.execPath}" -e "${script}"\r`) for (let attempt = 0; attempt < 80 && grandchildPid === null; attempt += 1) { await sleep(250) @@ -98,15 +113,13 @@ describeOnWindows('ConPTY job ownership', () => { } it('reports this build as able to own pty trees', () => { - // A node-pty rebuilt from unpatched sources would silently fall back to the - // old probe, so every assertion below would pass vacuously. expect(isPtyJobOwnershipAvailable()).toBe(true) }) it('keeps the native table intact while shell cleanup overlaps new terminals', async () => { const result = await runProcess({ program: process.execPath, - args: [join(process.cwd(), 'config', 'scripts', 'windows-pty-table-stress.cjs')], + args: [join(process.cwd(), 'config', 'scripts', 'windows-pty-table-stress.mjs')], env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1' }, timeoutMs: 90_000 }) @@ -123,7 +136,7 @@ describeOnWindows('ConPTY job ownership', () => { const pids = listPtyJobProcessIds(proc) expect(pids).not.toBeNull() - expect(pids).toContain(proc.pid) + expect(pids).toContain(proc.shellProcessId) expect(pids).toContain(grandchildPid) }, 60_000) @@ -160,23 +173,22 @@ describeOnWindows('ConPTY job ownership', () => { }, 60_000) it('still starts a backgrounded child from inside the job', async () => { - // Scope note: `start /b` uses CREATE_NEW_CONSOLE, not - // CREATE_BREAKAWAY_FROM_JOB, so this does NOT prove the BREAKAWAY_OK flag - // is doing its job -- it proves job membership does not block ordinary - // backgrounding. The flag itself rests on the Win32 contract (a job lacking - // JOB_OBJECT_LIMIT_BREAKAWAY_OK denies breakaway with ERROR_ACCESS_DENIED - // regardless of its other limits) and is not covered by a test here. - // Covering it needs a helper that passes the flag to CreateProcess. - const nodePty = await import('node-pty') + // Denying explicit breakaway must still permit ordinary background commands. const marker = join(mkdtempSync(join(tmpdir(), 'orca-breakaway-')), 'marker.txt') - const proc = nodePty.spawn('cmd.exe', [], { - name: 'xterm-256color', + const proc = spawnTestPty({ + file: process.env.ComSpec || 'cmd.exe', + args: [], + env: Object.fromEntries( + Object.entries(process.env).filter( + (entry): entry is [string, string] => entry[1] !== undefined + ) + ), cols: 100, rows: 30, - cwd: tmpdir(), - useConptyDll: true + cwd: tmpdir() }) spawned.push(proc) + await proc.waitForSpawn?.() let output = '' proc.onData((chunk) => { @@ -190,7 +202,7 @@ describeOnWindows('ConPTY job ownership', () => { }, 60_000) it('stops answering once the tree is gone, rather than claiming it is empty', async () => { - // Measured, not assumed: node-pty drops its handle record and closes the + // The adapter closes its handle record and the // job when the shell exits, so a dead tree is unverifiable here rather than // observably empty. Callers must not read null as proof of death -- the // verdict vocabulary in docs/reference/ssh-execution-boundary.md applies. diff --git a/src/main/wsl/__fixtures__/wsl-invocation-allowlist.txt b/src/main/wsl/__fixtures__/wsl-invocation-allowlist.txt index 45f99ffa2467..31eac7ed773e 100644 --- a/src/main/wsl/__fixtures__/wsl-invocation-allowlist.txt +++ b/src/main/wsl/__fixtures__/wsl-invocation-allowlist.txt @@ -25,7 +25,6 @@ main/git/command-runner/wsl-command-resolution.ts main/git/wsl-git-read-environment.ts main/ipc/filesystem-watcher-wsl.ts main/local-worktree-filesystem.ts -main/providers/local-pty-spawn.ts main/rate-limits/claude-pty.ts main/rate-limits/codex-fetcher.ts main/wsl-availability.ts diff --git a/src/main/wsl/__fixtures__/wsl-probe-failure-swallow-allowlist.txt b/src/main/wsl/__fixtures__/wsl-probe-failure-swallow-allowlist.txt index f0a4c4f10826..494befc7bc28 100644 --- a/src/main/wsl/__fixtures__/wsl-probe-failure-swallow-allowlist.txt +++ b/src/main/wsl/__fixtures__/wsl-probe-failure-swallow-allowlist.txt @@ -29,3 +29,7 @@ main/wsl.ts # point of the module (#16463). A failed stat drops to the next candidate for # that one call and is re-asked on the next, so there is no value to pin. main/wsl-interop-spawn-directory.ts + +# Artifact verification failures refuse installation or replacement; no negative +# discovery result is cached. +main/wsl/wsl-guest-artifact-install-script.ts diff --git a/src/main/wsl/wsl-account-execution-context.ts b/src/main/wsl/wsl-account-execution-context.ts new file mode 100644 index 000000000000..71ceb875edf2 --- /dev/null +++ b/src/main/wsl/wsl-account-execution-context.ts @@ -0,0 +1,23 @@ +/** Captured operation identity; account selection remains keyed by distro. */ +export type WslAccountExecutionContext = Readonly<{ + distro: string + userName: string + userId: string + home: string +}> + +export function assertWslAccountExecutionTarget( + execution: WslAccountExecutionContext, + target?: { runtime?: 'host' | 'wsl'; wslDistro?: string | null } +): void { + if ( + target?.runtime !== 'wsl' || + target.wslDistro?.toLowerCase() !== execution.distro.toLowerCase() || + !execution.userName || + /[\0\r\n]/.test(execution.userName) || + !/^\d+$/.test(execution.userId) || + !execution.home.startsWith('/') + ) { + throw new Error('WSL account preparation does not match the captured execution owner') + } +} diff --git a/src/main/wsl/wsl-bun-runtime.ts b/src/main/wsl/wsl-bun-runtime.ts index be3215493ea3..f163cf58c332 100644 --- a/src/main/wsl/wsl-bun-runtime.ts +++ b/src/main/wsl/wsl-bun-runtime.ts @@ -39,7 +39,8 @@ export async function assertWslRuntimeDistroRunning( /** Refuse guest commands without a confirmed running verdict; WSL has no atomic check-and-exec. */ export function createRunningWslRuntimeRunner( distro: string, - callerSignal?: AbortSignal + callerSignal?: AbortSignal, + user?: string ): { run: (spec: WslSpec) => Promise signal: AbortSignal @@ -52,6 +53,7 @@ export function createRunningWslRuntimeRunner( const result = await runWslProcess({ ...spec, distro, + ...(user ? { user } : {}), timeoutMs: Math.max(1, Math.min(15_000, deadline - Date.now())), maxOutputBytes: 16 * 1024 }) diff --git a/src/main/wsl/wsl-bun-runtime.win32.integration.test.ts b/src/main/wsl/wsl-bun-runtime.win32.integration.test.ts index 3ebff9dbecc2..c9a6335833fb 100644 --- a/src/main/wsl/wsl-bun-runtime.win32.integration.test.ts +++ b/src/main/wsl/wsl-bun-runtime.win32.integration.test.ts @@ -1,4 +1,3 @@ -import { bunOwnedRuntimeArgs } from '../../shared/bun-owned-runtime-args' import { mkdtemp, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' @@ -8,7 +7,7 @@ import { hasAppEnvironment, setAppEnvironment } from '../../shared/app-environment' -import { ORCAD_BUN_VERSION } from '../../shared/orcad-bun-runtime' +import { ORCAD_BUN_VERSION, orcadBunVersionProbeArgs } from '../../shared/orcad-bun-runtime' import { getMainHttpClient, setMainHttpClient } from '../network/http-client' import { shellEscape } from '../ssh/ssh-connection-utils' import { createRunningWslRuntimeRunner, ensureWslBunRuntime } from './wsl-bun-runtime' @@ -101,7 +100,7 @@ it.skipIf(process.platform !== 'win32' || !distro)( expect( await run({ program: executable, - args: [...bunOwnedRuntimeArgs('linux'), '--print', 'process.versions.bun'], + args: orcadBunVersionProbeArgs('linux'), loginPath: 'none' }) ).toBe(ORCAD_BUN_VERSION) @@ -118,7 +117,7 @@ it.skipIf(process.platform !== 'win32' || !distro)( expect( await run({ program: repaired, - args: [...bunOwnedRuntimeArgs('linux'), '--print', 'process.versions.bun'], + args: orcadBunVersionProbeArgs('linux'), loginPath: 'none' }) ).toBe(ORCAD_BUN_VERSION) diff --git a/src/main/wsl/wsl-daemon-connector-protocol.test.ts b/src/main/wsl/wsl-daemon-connector-protocol.test.ts new file mode 100644 index 000000000000..34876270122c --- /dev/null +++ b/src/main/wsl/wsl-daemon-connector-protocol.test.ts @@ -0,0 +1,75 @@ +import { expect, it } from 'vitest' +import { mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { DaemonClient } from '../daemon/client' +import { DaemonServer } from '../daemon/daemon-server' +import { runProcess } from '../../shared/child-process/run-process' +import { openWslDaemonConnectorStream } from './wsl-daemon-connector-stream' +import { + WSL_DAEMON_CONNECTOR_SCRIPT, + WSL_DAEMON_READ_TOKEN_SCRIPT +} from './wsl-daemon-connector-script' + +it.skipIf(process.platform === 'win32')( + 'authenticates the actual daemon over two child pipes and reconnects to the same owner', + async () => { + const directory = mkdtempSync(join(tmpdir(), 'orca-guest-protocol-')) + const plan = { + socket: join(directory, 'd.sock'), + tokenPath: join(directory, 'token'), + userId: String(process.getuid?.()), + home: process.env.HOME + } + const server = new DaemonServer({ + socketPath: plan.socket, + tokenPath: plan.tokenPath, + spawnSubprocess: () => { + throw new Error('No terminal spawn expected') + } + }) + const observer = new DaemonClient({ socketPath: plan.socket, tokenPath: plan.tokenPath }) + const client = new DaemonClient({ + transport: { + async readToken({ signal, timeoutMs }) { + const result = await runProcess({ + program: process.execPath, + args: ['-e', WSL_DAEMON_READ_TOKEN_SCRIPT, JSON.stringify(plan)], + signal, + timeoutMs + }) + if (result.code !== 0) { + throw new Error('Token read failed') + } + return result.stdout + }, + connect: (_role, { signal }) => + openWslDaemonConnectorStream( + { + program: process.execPath, + args: ['-e', WSL_DAEMON_CONNECTOR_SCRIPT, JSON.stringify(plan)] + }, + signal + ) + } + }) + try { + await server.start() + await observer.ensureConnected() + await client.ensureConnected() + const identity = client.getDaemonIdentity() + expect(identity?.pid).toBe(process.pid) + expect(await client.request('ping', undefined)).toEqual({ pong: true }) + expect(await client.request('listSessions', undefined)).toEqual({ sessions: [] }) + client.disconnect() + await client.ensureConnected() + expect(client.getDaemonIdentity()).toEqual(identity) + expect(await client.request('ping', undefined)).toEqual({ pong: true }) + } finally { + client.disconnect() + observer.disconnect() + await server.shutdown() + rmSync(directory, { recursive: true, force: true }) + } + } +) diff --git a/src/main/wsl/wsl-daemon-connector-script.ts b/src/main/wsl/wsl-daemon-connector-script.ts new file mode 100644 index 000000000000..f7c530bd4c3b --- /dev/null +++ b/src/main/wsl/wsl-daemon-connector-script.ts @@ -0,0 +1,36 @@ +export const WSL_DAEMON_CONNECTOR_READY = 'ORCA_WSL_DAEMON_CONNECTED\n' + +/** No shell or stdout preamble: both daemon channels retain their existing byte protocol. */ +export const WSL_DAEMON_CONNECTOR_SCRIPT = String.raw` +const net = require('node:net'); +const plan = JSON.parse(process.argv[1]); +if (String(process.getuid()) !== plan.userId || process.env.HOME !== plan.home) { + throw new Error('WSL daemon execution owner changed'); +} +const socket = net.createConnection(plan.socket); +socket.once('connect', () => { + process.stderr.write('ORCA_WSL_DAEMON_CONNECTED\n'); + process.stdin.pipe(socket); + socket.pipe(process.stdout); +}); +socket.once('error', () => { process.exitCode = 1; }); +socket.once('close', () => { process.stdin.destroy(); }); +process.stdin.once('error', () => socket.destroy()); +process.stdout.once('error', () => socket.destroy()); +` + +export const WSL_DAEMON_READ_TOKEN_SCRIPT = String.raw` +const fs = require('node:fs'); +const plan = JSON.parse(process.argv[1]); +if (String(process.getuid()) !== plan.userId || process.env.HOME !== plan.home) { + throw new Error('WSL daemon execution owner changed'); +} +const fd = fs.openSync(plan.tokenPath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW); +try { + const stat = fs.fstatSync(fd); + if (!stat.isFile() || stat.uid !== process.getuid() || (stat.mode & 0o077) || stat.size > 4096) { + throw new Error('WSL daemon token ownership is unverifiable'); + } + process.stdout.write(fs.readFileSync(fd, 'utf8').trim()); +} finally { fs.closeSync(fd); } +` diff --git a/src/main/wsl/wsl-daemon-connector-stream.test.ts b/src/main/wsl/wsl-daemon-connector-stream.test.ts new file mode 100644 index 000000000000..2ceae531fb7e --- /dev/null +++ b/src/main/wsl/wsl-daemon-connector-stream.test.ts @@ -0,0 +1,112 @@ +import { DaemonConnectionLostError } from '../daemon/daemon-errors' +import { afterEach, expect, it } from 'vitest' +import { createServer } from 'node:net' +import { mkdtempSync, rmSync, writeFileSync, symlinkSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { once } from 'node:events' +import { runProcess } from '../../shared/child-process/run-process' +import { openWslDaemonConnectorStream } from './wsl-daemon-connector-stream' +import { + WSL_DAEMON_CONNECTOR_SCRIPT, + WSL_DAEMON_READ_TOKEN_SCRIPT +} from './wsl-daemon-connector-script' + +const cleanups: (() => void)[] = [] +afterEach(() => + cleanups + .splice(0) + .toReversed() + .forEach((cleanup) => cleanup()) +) +function fixture() { + const directory = mkdtempSync(join(tmpdir(), 'orca-guest-channel-')) + cleanups.push(() => rmSync(directory, { recursive: true, force: true })) + return { + directory, + socket: join(directory, 'd.sock'), + home: process.env.HOME, + userId: String(process.getuid?.()) + } +} +const unix = it.skipIf(process.platform === 'win32') + +unix( + 'preserves raw bytes and the last reply through connector EOF without stopping the server', + async () => { + const plan = fixture() + const reply = Buffer.alloc(256 * 1024) + for (let index = 0; index < reply.length; index++) { + reply[index] = index % 256 + } + const server = createServer((socket) => { + socket.once('data', () => socket.end(reply)) + }) + cleanups.push(() => server.close()) + server.listen(plan.socket) + await once(server, 'listening') + const stream = await openWslDaemonConnectorStream( + { + program: process.execPath, + args: ['-e', WSL_DAEMON_CONNECTOR_SCRIPT, JSON.stringify(plan)] + }, + AbortSignal.timeout(10_000) + ) + expect(stream.readableObjectMode).toBe(false) + expect(stream.writableObjectMode).toBe(false) + const chunks: Buffer[] = [] + stream.on('data', (chunk: Buffer) => chunks.push(chunk)) + const ended = once(stream, 'end') + stream.write('request') + await ended + expect(Buffer.concat(chunks).equals(reply)).toBe(true) + expect(server.listening).toBe(true) + stream.destroy() + } +) + +unix('rejects changed guest identity before connecting', async () => { + const plan = { ...fixture(), userId: '-1' } + await expect( + openWslDaemonConnectorStream( + { + program: process.execPath, + args: ['-e', WSL_DAEMON_CONNECTOR_SCRIPT, JSON.stringify(plan)] + }, + AbortSignal.timeout(10_000) + ) + ).rejects.toBeInstanceOf(DaemonConnectionLostError) +}) + +it('cancels a connector that never reaches readiness', async () => { + const abort = new AbortController() + const pending = openWslDaemonConnectorStream( + { + program: process.execPath, + args: ['-e', 'setInterval(()=>{},1000)'] + }, + abort.signal + ) + abort.abort() + await expect(pending).rejects.toThrow('canceled') +}) + +unix('only reads an owner-private regular token, never a symlink', async () => { + const plan = fixture() + const tokenPath = join(plan.directory, 'token') + writeFileSync(tokenPath, 'test-private-token', { mode: 0o600 }) + const read = (file: string) => + runProcess({ + program: process.execPath, + args: ['-e', WSL_DAEMON_READ_TOKEN_SCRIPT, JSON.stringify({ ...plan, tokenPath: file })], + timeoutMs: 10_000 + }) + const valid = await read(tokenPath) + expect(valid.code).toBe(0) + expect(valid.stdout).toBe('test-private-token') + const alias = join(plan.directory, 'alias') + symlinkSync(tokenPath, alias) + const refused = await read(alias) + expect(refused.code).not.toBe(0) + expect(refused.stdout).toBe('') +}) diff --git a/src/main/wsl/wsl-daemon-connector-stream.ts b/src/main/wsl/wsl-daemon-connector-stream.ts new file mode 100644 index 000000000000..d8ac01873eaa --- /dev/null +++ b/src/main/wsl/wsl-daemon-connector-stream.ts @@ -0,0 +1,61 @@ +import { DaemonConnectionLostError } from '../daemon/daemon-errors' +import { Duplex } from 'node:stream' +import { spawnProcess, type ProcessSpec } from '../../shared/child-process/run-process' +import { WSL_DAEMON_CONNECTOR_READY } from './wsl-daemon-connector-script' + +/** Closing this stream only retires its disposable connector, never the guest daemon. */ +export function openWslDaemonConnectorStream( + spec: ProcessSpec, + signal: AbortSignal +): Promise { + signal.throwIfAborted() + const child = spawnProcess(spec) + const pair = { readable: child.stdout, writable: child.stdin } + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Node supports Node-stream pairs; current typings omit them. Byte/EOF tests cover this path. + const stream = Duplex.from(pair as unknown as Parameters[0]) + // A child can fail before the awaiting protocol consumer has installed its listener. + stream.on('error', () => {}) + stream.once('close', () => child.kill()) + child.once('error', (error) => stream.destroy(error)) + child.once('close', () => stream.end()) + return new Promise((resolve, reject) => { + let preamble = '' + let settled = false + const cleanup = () => { + signal.removeEventListener('abort', abort) + child.stderr.off('data', onStderr) + child.off('error', fail) + child.off('close', closed) + child.stderr.resume() + } + const fail = (error: Error) => { + if (settled) { + return + } + settled = true + cleanup() + stream.destroy() + reject(error) + } + const closed = () => fail(new DaemonConnectionLostError('Connection lost')) + const abort = () => + fail(new Error('WSL daemon connector connection canceled', { cause: signal.reason })) + const onStderr = (bytes: Buffer) => { + preamble += bytes.toString('utf8') + if (preamble.length > 4096) { + fail(new Error('WSL daemon connector readiness output exceeded its limit')) + } else if (preamble === WSL_DAEMON_CONNECTOR_READY) { + settled = true + cleanup() + resolve(stream) + } + } + child.stderr.on('data', onStderr) + child.once('error', fail) + child.once('close', closed) + signal.addEventListener('abort', abort, { once: true }) + if (signal.aborted) { + abort() + } + }) +} diff --git a/src/main/wsl/wsl-daemon-endpoint.test.ts b/src/main/wsl/wsl-daemon-endpoint.test.ts new file mode 100644 index 000000000000..0ccbdefc862f --- /dev/null +++ b/src/main/wsl/wsl-daemon-endpoint.test.ts @@ -0,0 +1,149 @@ +import { PROTOCOL_VERSION } from '../daemon/daemon-protocol-version' +import { proveWslDaemonIncarnationExited } from './wsl-daemon-incarnation' +import { beforeEach, expect, it, vi } from 'vitest' +import { readFile } from 'node:fs/promises' +import { prepareWslGuestOwner } from './wsl-guest-owner-preparation' +import { + prepareWslDaemonEndpoint, + startPreparedWslDaemonOwner, + startRetainedWslDaemonOwner +} from './wsl-daemon-endpoint' +import { createRunningWslRuntimeRunner } from './wsl-bun-runtime' +import { readWslDistributionIdentity } from './wsl-distribution-identity' + +vi.mock('./wsl-daemon-incarnation', () => ({ proveWslDaemonIncarnationExited: vi.fn() })) +vi.mock('node:fs/promises', () => ({ readFile: vi.fn() })) +vi.mock('../daemon/daemon-bun-runtime', () => ({ + desktopDaemonBundleDir: () => '/app/terminal-daemon' +})) +vi.mock('./wsl-guest-owner-preparation', () => ({ prepareWslGuestOwner: vi.fn() })) +vi.mock('./wsl-bun-runtime', () => ({ createRunningWslRuntimeRunner: vi.fn() })) +vi.mock('./wsl-distribution-identity', () => ({ readWslDistributionIdentity: vi.fn() })) +const run = vi.fn() +beforeEach(() => { + vi.clearAllMocks() + run.mockImplementation(async (spec) => + spec.program === 'wslpath' ? '/mnt/app/terminal-daemon' : 'ready' + ) + vi.mocked(readFile).mockResolvedValue(Buffer.from('daemon-artifact')) + vi.mocked(readWslDistributionIdentity).mockResolvedValue('registered-distro') + const execution = { run, signal: new AbortController().signal } + vi.mocked(createRunningWslRuntimeRunner).mockReturnValue(execution) + vi.mocked(prepareWslGuestOwner).mockResolvedValue({ + distributionId: 'registered-distro', + userName: 'captured', + userId: '1000', + platform: 'linux-x64', + libc: 'glibc', + runtime: '/private/bun', + execution, + environment: { home: '/home/u', path: '/usr/bin', envBinary: '/usr/bin/env' } + }) +}) + +it('installs only the daemon bundle with captured-owner guards and a private token path', async () => { + const prepared = await prepareWslDaemonEndpoint('Ubuntu', 'profile') + const command = run.mock.calls.find(([spec]) => spec.program === '/usr/bin/env')?.[0] + const plan = JSON.parse(command.args.at(-1)) + expect(plan.files).toEqual([ + { name: 'daemon-entry.js', sha256: expect.stringMatching(/^[a-f0-9]{64}$/) } + ]) + expect(plan).toMatchObject({ + userId: '1000', + home: '/home/u', + source: '/mnt/app/terminal-daemon' + }) + expect(command.args).toEqual( + expect.arrayContaining(['--no-env-file', '--config=/dev/null', '--no-install']) + ) + expect(prepared.endpoint).toMatchObject({ + userName: 'captured', + distributionId: 'registered-distro', + protocolVersion: PROTOCOL_VERSION + }) + expect(prepared.endpoint.tokenPath).toBe(`${plan.ownerDirectory}/token`) + expect(Object.isFrozen(prepared.endpoint)).toBe(true) +}) + +it('keeps profiles and changed artifact contents on independent endpoints', async () => { + const first = await prepareWslDaemonEndpoint('Ubuntu', 'first') + const otherProfile = await prepareWslDaemonEndpoint('Ubuntu', 'second') + expect(otherProfile.entry).toBe(first.entry) + expect(otherProfile.endpoint.socket).not.toBe(first.endpoint.socket) + vi.mocked(readFile).mockResolvedValue(Buffer.from('new-daemon')) + const changed = await prepareWslDaemonEndpoint('Ubuntu', 'first') + expect(changed.entry).not.toBe(first.entry) + expect(changed.endpoint.socket).not.toBe(first.endpoint.socket) +}) + +it('never launches after the registered distro is replaced', async () => { + const prepared = await prepareWslDaemonEndpoint('Ubuntu', 'profile') + run.mockClear() + vi.mocked(readWslDistributionIdentity).mockResolvedValue('different-distro') + await expect(startPreparedWslDaemonOwner(prepared)).rejects.toThrow('replaced') + expect(run).not.toHaveBeenCalled() +}) + +it('launches through the captured user and accepts only confirmed readiness', async () => { + const prepared = await prepareWslDaemonEndpoint('Ubuntu', 'profile') + run.mockResolvedValue('started') + await startPreparedWslDaemonOwner(prepared) + expect(run.mock.lastCall?.[0].args).toEqual( + expect.arrayContaining(['--no-env-file', '--config=/dev/null', '--no-install']) + ) + expect(createRunningWslRuntimeRunner).toHaveBeenCalledWith('ubuntu', undefined, 'captured') + run.mockResolvedValue('incomplete') + await expect(startPreparedWslDaemonOwner(prepared)).rejects.toThrow('could not be started') +}) + +it('uses independent fresh endpoints for renamed users or changed environment executables', async () => { + const first = await prepareWslDaemonEndpoint('Ubuntu', 'profile') + const initial = await prepareWslGuestOwner('Ubuntu') + vi.mocked(prepareWslGuestOwner).mockResolvedValue({ ...initial, userName: 'renamed' }) + const renamed = await prepareWslDaemonEndpoint('Ubuntu', 'profile') + expect(renamed.owner.relayBuildId).not.toBe(first.owner.relayBuildId) + expect(renamed.endpoint.socket).not.toBe(first.endpoint.socket) + vi.mocked(prepareWslGuestOwner).mockResolvedValue({ + ...initial, + environment: { ...initial.environment, envBinary: '/opt/homebrew/bin/env' } + }) + const changed = await prepareWslDaemonEndpoint('Ubuntu', 'profile') + expect(changed.owner.relayBuildId).not.toBe(first.owner.relayBuildId) + expect(changed.endpoint.socket).not.toBe(first.endpoint.socket) + expect(first.endpoint.userName).toBe('captured') + expect(first.endpoint.envBinary).toBe('/usr/bin/env') +}) + +it('canonicalizes fresh distro aliases without modifying persisted reconnect identities', async () => { + const first = await prepareWslDaemonEndpoint('Ubuntu', 'profile') + const alias = await prepareWslDaemonEndpoint('ubuntu', 'profile') + expect(alias).toEqual(first) + expect(alias.owner.distro).toBe('ubuntu') +}) + +it('checks old process death before starting the retained artifact after bundle replacement', async () => { + const old = await prepareWslDaemonEndpoint('Ubuntu', 'profile') + vi.mocked(readFile).mockResolvedValue(Buffer.from('new-daemon')) + const current = await prepareWslDaemonEndpoint('Ubuntu', 'profile') + expect(current.endpoint.entry).not.toBe(old.endpoint.entry) + const incarnation = { + pid: 42, + startedAtMs: 100, + launchNonce: 'old', + linuxStartTicks: '123', + bootId: 'boot' + } + run.mockClear().mockResolvedValue('started') + vi.mocked(proveWslDaemonIncarnationExited).mockRejectedValueOnce(new Error('still live')) + await expect(startRetainedWslDaemonOwner(old.endpoint, incarnation)).rejects.toThrow('still live') + expect(run).not.toHaveBeenCalled() + vi.mocked(proveWslDaemonIncarnationExited).mockResolvedValue(undefined) + await startRetainedWslDaemonOwner(old.endpoint, incarnation) + expect(proveWslDaemonIncarnationExited).toHaveBeenLastCalledWith( + old.endpoint, + incarnation, + undefined + ) + expect(JSON.parse(run.mock.lastCall?.[0].args.at(-1))).toEqual(old.endpoint) + expect(createRunningWslRuntimeRunner).toHaveBeenLastCalledWith('ubuntu', undefined, 'captured') +}) diff --git a/src/main/wsl/wsl-daemon-endpoint.ts b/src/main/wsl/wsl-daemon-endpoint.ts new file mode 100644 index 000000000000..d18b82d395f0 --- /dev/null +++ b/src/main/wsl/wsl-daemon-endpoint.ts @@ -0,0 +1,172 @@ +import { PROTOCOL_VERSION } from '../daemon/daemon-protocol-version' +import { proveWslDaemonIncarnationExited } from './wsl-daemon-incarnation' +import { bunOwnedRuntimeArgs } from '../../shared/bun-owned-runtime-args' +import { createHash } from 'node:crypto' +import { readFile } from 'node:fs/promises' +import { join, posix } from 'node:path' +import { desktopDaemonBundleDir } from '../daemon/daemon-bun-runtime' +import { ORCAD_BUN_RELEASE_ASSETS } from '../../shared/orcad-bun-runtime' +import { prepareWslGuestOwner } from './wsl-guest-owner-preparation' +import { WSL_GUEST_ARTIFACT_INSTALL_SCRIPT } from './wsl-guest-artifact-install-script' +import { WSL_DAEMON_START_SCRIPT } from './wsl-daemon-start-script' +import { createRunningWslRuntimeRunner } from './wsl-bun-runtime' +import { readWslDistributionIdentity } from './wsl-distribution-identity' +import type { + PersistedWslDaemonEndpoint, + WslDaemonIncarnation +} from '../../shared/wsl-daemon-recovery' +import type { WslPtyOwner } from '../../shared/wsl-pty-id' + +export type PreparedWslDaemonEndpoint = Readonly<{ + endpoint: Readonly + owner: Readonly + entry: string + path: string + artifactId: string +}> + +const hash = (bytes: string | Buffer) => createHash('sha256').update(bytes).digest('hex') +const runtimeEnvArgs = [ + 'NODE_OPTIONS', + 'NODE_PATH', + 'BUN_OPTIONS', + 'BUN_INSPECT', + 'ELECTRON_RUN_AS_NODE' +].flatMap((key) => ['-u', key]) + +/** The platform-neutral daemon bundle runs with the verified guest-native Bun runtime. */ +export async function prepareWslDaemonEndpoint( + distro: string, + profileScope: string, + signal?: AbortSignal +): Promise { + if (!profileScope) { + throw new Error('WSL daemon requires a profile scope') + } + distro = distro.trim().toLowerCase() + const { distributionId, userName, userId, platform, libc, runtime, environment, execution } = + await prepareWslGuestOwner(distro, signal) + const directory = desktopDaemonBundleDir() + const entryName = 'daemon-entry.js' + const files = [{ name: entryName, sha256: hash(await readFile(join(directory, entryName))) }] + const target = `${platform}-${libc}` as const + const artifactId = hash( + JSON.stringify({ runtime: ORCAD_BUN_RELEASE_ASSETS[target].executableSha256, files }) + ) + const home = environment.home + const ownerId = hash( + JSON.stringify({ + distributionId, + userId, + userName, + home, + profileScope, + runtime, + envBinary: environment.envBinary + }) + ) + const artifactRoot = posix.join(home, '.cache/orca/terminal-daemons') + const guestDirectory = posix.join(artifactRoot, artifactId) + const ownerDirectory = posix.join( + home, + '.orca-wsl', + ownerId.slice(0, 20), + artifactId.slice(0, 20) + ) + const socket = posix.join(ownerDirectory, 'd.sock') + if (Buffer.byteLength(posix.join(ownerDirectory, '.p0000000000')) > 100) { + throw new Error('WSL home path is too long for a private terminal socket') + } + const source = await execution.run({ + program: 'wslpath', + args: ['-a', '-u', directory], + loginPath: 'none' + }) + const installed = await execution.run({ + program: environment.envBinary, + args: [ + ...runtimeEnvArgs, + runtime, + ...bunOwnedRuntimeArgs('linux'), + '-e', + WSL_GUEST_ARTIFACT_INSTALL_SCRIPT, + JSON.stringify({ + source, + artifactRoot, + directory: guestDirectory, + ownerDirectory, + files, + userId, + home + }) + ], + loginPath: 'none' + }) + if (installed !== 'ready') { + throw new Error('WSL daemon artifact preparation did not finish') + } + return Object.freeze({ + endpoint: Object.freeze({ + distro, + entry: posix.join(guestDirectory, entryName), + serverBuildId: artifactId, + protocolVersion: PROTOCOL_VERSION, + distributionId, + userName, + userId, + home, + envBinary: environment.envBinary, + runtime, + socket, + tokenPath: posix.join(ownerDirectory, 'token') + }), + owner: Object.freeze({ distro, relayBuildId: `daemon+${artifactId}+${ownerId}` }), + entry: posix.join(guestDirectory, entryName), + path: environment.path, + artifactId + }) +} + +/** The daemon itself arbitrates concurrent launches; contact failure never authorizes replacement. */ +export async function startPreparedWslDaemonOwner( + prepared: PreparedWslDaemonEndpoint, + signal?: AbortSignal +): Promise { + await startWslDaemonOwner(prepared.endpoint, prepared.path, signal) +} + +/** A retained endpoint can restart only after its admitted process incarnation exited. */ +export async function startRetainedWslDaemonOwner( + endpoint: PersistedWslDaemonEndpoint, + incarnation: WslDaemonIncarnation | undefined, + signal?: AbortSignal +): Promise { + await proveWslDaemonIncarnationExited(endpoint, incarnation, signal) + await startWslDaemonOwner(endpoint, undefined, signal) +} + +async function startWslDaemonOwner( + endpoint: PersistedWslDaemonEndpoint, + path: string | undefined, + signal?: AbortSignal +): Promise { + if ((await readWslDistributionIdentity(endpoint.distro)) !== endpoint.distributionId) { + throw new Error('WSL distribution was replaced; refusing to start a different terminal owner') + } + const execution = createRunningWslRuntimeRunner(endpoint.distro, signal, endpoint.userName) + const result = await execution.run({ + program: endpoint.envBinary, + args: [ + ...runtimeEnvArgs, + endpoint.runtime, + ...bunOwnedRuntimeArgs('linux'), + '-e', + WSL_DAEMON_START_SCRIPT, + JSON.stringify({ ...endpoint, path }) + ], + loginPath: path === undefined ? 'preferred' : 'none' + }) + if (result !== 'started' && result !== 'existing') { + throw new Error('WSL terminal daemon could not be started') + } +} diff --git a/src/main/wsl/wsl-daemon-incarnation.test.ts b/src/main/wsl/wsl-daemon-incarnation.test.ts new file mode 100644 index 000000000000..1aae69a808de --- /dev/null +++ b/src/main/wsl/wsl-daemon-incarnation.test.ts @@ -0,0 +1,64 @@ +import { runInNewContext } from 'node:vm' +import { expect, it, vi } from 'vitest' +import { WSL_DAEMON_INCARNATION_SCRIPT } from './wsl-daemon-incarnation' + +it.each([ + 'same', + 'new-boot', + 'missing-pid', + 'hidden-pid', + 'hidden-EPERM', + 'reused-pid', + 'EACCES', + 'malformed', + 'no-proof' +])('classifies retained guest incarnation: %s', (scenario) => { + const log = vi.fn() + const stat = `42 (shell ) with spaces) ${Array.from({ length: 20 }, (_, i) => (i === 19 ? (scenario === 'reused-pid' ? '999' : '123') : '0')).join(' ')}` + const check = () => + runInNewContext(WSL_DAEMON_INCARNATION_SCRIPT, { + require: () => ({ + readFileSync: (path: string) => { + if (path.endsWith('boot_id')) { + return scenario === 'new-boot' ? 'new-boot' : 'original-boot' + } + if (['missing-pid', 'hidden-pid', 'hidden-EPERM', 'EACCES'].includes(scenario)) { + throw Object.assign(new Error('unreadable'), { + code: scenario === 'EACCES' ? 'EACCES' : 'ENOENT' + }) + } + return scenario === 'malformed' ? 'bad stat' : stat + } + }), + process: { + kill: () => { + if (scenario !== 'hidden-pid') { + throw Object.assign(new Error('probe'), { + code: scenario === 'hidden-EPERM' ? 'EPERM' : 'ESRCH' + }) + } + }, + getuid: () => 1000, + env: { HOME: '/home/u' }, + argv: [ + 'bun', + JSON.stringify({ + userId: '1000', + home: '/home/u', + incarnation: + scenario === 'no-proof' + ? undefined + : { pid: 42, linuxStartTicks: '123', bootId: 'original-boot' } + }) + ] + }, + console: { log } + }) + if (['new-boot', 'missing-pid', 'reused-pid'].includes(scenario)) { + check() + expect(log).toHaveBeenCalledWith('exited') + } else { + expect(check).toThrow() + expect(log).not.toHaveBeenCalled() + } +}) diff --git a/src/main/wsl/wsl-daemon-incarnation.ts b/src/main/wsl/wsl-daemon-incarnation.ts new file mode 100644 index 000000000000..ed679af6b094 --- /dev/null +++ b/src/main/wsl/wsl-daemon-incarnation.ts @@ -0,0 +1,72 @@ +import { bunOwnedRuntimeArgs } from '../../shared/bun-owned-runtime-args' +import type { + PersistedWslDaemonEndpoint, + WslDaemonIncarnation +} from '../../shared/wsl-daemon-recovery' +import { createRunningWslRuntimeRunner } from './wsl-bun-runtime' +import { readWslDistributionIdentity } from './wsl-distribution-identity' + +/** A missing socket never proves that the daemon's surviving terminals exited. */ +export const WSL_DAEMON_INCARNATION_SCRIPT = String.raw` +const fs=require('node:fs'); +const plan=JSON.parse(process.argv[1]); +if(String(process.getuid())!==plan.userId || process.env.HOME!==plan.home) throw Error('WSL daemon execution owner changed'); +const prior=plan.incarnation; +if(!prior) throw Error('Retained guest owner has no process identity; recovery is unverifiable'); +const boot=fs.readFileSync('/proc/sys/kernel/random/boot_id','utf8').trim(); +if(!boot) throw Error('Guest boot identity is unverifiable'); +let exited=boot!==prior.bootId; +if(!exited) { + let stat; + try {stat=fs.readFileSync('/proc/'+prior.pid+'/stat','utf8');} + catch(error) { + if(error.code!=='ENOENT') throw error; + try {process.kill(prior.pid,0);} + catch(probe) {if(probe.code==='ESRCH') exited=true; else throw probe;} + if(!exited) throw Error('Guest process identity is unverifiable'); + } + if(stat!==undefined) { + const close=stat.lastIndexOf(')'); + const ticks=close<0 ? undefined : stat.slice(close+2).split(/\s+/)[19]; + if(!ticks || !/^\d+$/.test(ticks)) throw Error('Guest process identity is unverifiable'); + exited=ticks!==prior.linuxStartTicks; + } +} +if(!exited) throw Error('Retained guest daemon is still live; refusing replacement'); +console.log('exited'); +` + +export async function proveWslDaemonIncarnationExited( + endpoint: PersistedWslDaemonEndpoint, + incarnation: WslDaemonIncarnation | undefined, + signal?: AbortSignal +): Promise { + if (!incarnation) { + throw new Error('Retained guest owner has no process identity; recovery is unverifiable') + } + if ((await readWslDistributionIdentity(endpoint.distro)) !== endpoint.distributionId) { + throw new Error('WSL distribution was replaced; terminal owner is unverifiable') + } + const runner = createRunningWslRuntimeRunner(endpoint.distro, signal, endpoint.userName) + const result = await runner.run({ + program: endpoint.envBinary, + args: [ + ...[ + 'NODE_OPTIONS', + 'NODE_PATH', + 'BUN_OPTIONS', + 'BUN_INSPECT', + 'ELECTRON_RUN_AS_NODE' + ].flatMap((key) => ['-u', key]), + endpoint.runtime, + ...bunOwnedRuntimeArgs('linux'), + '-e', + WSL_DAEMON_INCARNATION_SCRIPT, + JSON.stringify({ userId: endpoint.userId, home: endpoint.home, incarnation }) + ], + loginPath: 'none' + }) + if (result !== 'exited') { + throw new Error('Retained guest daemon death is unverifiable') + } +} diff --git a/src/main/wsl/wsl-daemon-owner-admission.test.ts b/src/main/wsl/wsl-daemon-owner-admission.test.ts new file mode 100644 index 000000000000..f03d8f0e7082 --- /dev/null +++ b/src/main/wsl/wsl-daemon-owner-admission.test.ts @@ -0,0 +1,94 @@ +import { beforeEach, expect, it, vi } from 'vitest' +import { WslDaemonOwnerAdmission } from './wsl-daemon-owner-admission' +import { proveWslDaemonIncarnationExited } from './wsl-daemon-incarnation' +import { startRetainedWslDaemonOwner } from './wsl-daemon-endpoint' +import type { WslDaemonRecovery } from '../../shared/wsl-daemon-recovery' +vi.mock('./wsl-daemon-incarnation', () => ({ proveWslDaemonIncarnationExited: vi.fn() })) +vi.mock('./wsl-daemon-endpoint', () => ({ + startRetainedWslDaemonOwner: vi.fn(), + startPreparedWslDaemonOwner: vi.fn() +})) +const owner = { distro: 'Ubuntu', relayBuildId: 'old-build' } +const endpoint = { + distro: 'Ubuntu', + distributionId: 'distro', + userName: 'alice', + userId: '1000', + home: '/home/alice', + runtime: '/old/bun', + entry: '/old/daemon-entry.js', + envBinary: '/usr/bin/env', + socket: '/old/socket', + tokenPath: '/old/token', + serverBuildId: 'old-build' +} +const prior = { + pid: 42, + startedAtMs: 100, + launchNonce: 'old', + linuxStartTicks: '123', + bootId: 'boot' +} +const next = { ...prior, pid: 43, launchNonce: 'new', linuxStartTicks: '456' } +function setup(record?: WslDaemonRecovery) { + const store = { + getWslDaemonRecovery: vi.fn(() => record ?? null), + upsertWslDaemonRecovery: vi.fn(async (value: WslDaemonRecovery) => { + record = value + }) + } + const admission = new WslDaemonOwnerAdmission( + store, + owner, + endpoint, + new AbortController().signal + ) + return { admission, store } +} +beforeEach(() => { + vi.clearAllMocks() + vi.mocked(proveWslDaemonIncarnationExited).mockReset().mockResolvedValue(undefined) +}) +it('gains proof by attaching to an identity-less record and admits before returning', async () => { + const { admission, store } = setup({ kind: 'daemon', ...owner, endpoint }) + await admission.admitIdentity(prior) + expect(store.upsertWslDaemonRecovery).toHaveBeenCalledWith( + { kind: 'daemon', ...owner, endpoint, incarnation: prior }, + null + ) + expect(proveWslDaemonIncarnationExited).not.toHaveBeenCalled() +}) +it('refuses displaced but live prior owner even when the new hello authenticated', async () => { + const { admission, store } = setup({ kind: 'daemon', ...owner, endpoint, incarnation: prior }) + vi.mocked(proveWslDaemonIncarnationExited).mockRejectedValueOnce(new Error('still live')) + await expect(admission.admitIdentity(next)).rejects.toThrow('still live') + expect(store.upsertWslDaemonRecovery).not.toHaveBeenCalled() +}) +it('proves old incarnation exited before CAS admission of the new one', async () => { + const { admission, store } = setup({ kind: 'daemon', ...owner, endpoint, incarnation: prior }) + await admission.admitIdentity(next) + expect(proveWslDaemonIncarnationExited).toHaveBeenCalledWith( + endpoint, + prior, + expect.any(AbortSignal) + ) + expect(store.upsertWslDaemonRecovery).toHaveBeenCalledWith( + { kind: 'daemon', ...owner, endpoint, incarnation: next }, + prior + ) +}) +it('never starts another daemon after a durable admission failure', async () => { + const { admission, store } = setup() + store.upsertWslDaemonRecovery.mockRejectedValueOnce(new Error('disk full')) + await expect(admission.establishLease(() => admission.admitIdentity(prior))).rejects.toThrow( + 'disk full' + ) + expect(startRetainedWslDaemonOwner).not.toHaveBeenCalled() +}) +it('passes the retained incarnation to the checked starter on cached-adapter recovery', async () => { + const { admission } = setup({ kind: 'daemon', ...owner, endpoint, incarnation: prior }) + await admission.recover() + expect(startRetainedWslDaemonOwner).toHaveBeenCalledWith(endpoint, prior, expect.any(AbortSignal)) + vi.mocked(startRetainedWslDaemonOwner).mockRejectedValueOnce(new Error('unverifiable')) + await expect(admission.recover()).rejects.toThrow('unverifiable') +}) diff --git a/src/main/wsl/wsl-daemon-owner-admission.ts b/src/main/wsl/wsl-daemon-owner-admission.ts new file mode 100644 index 000000000000..94d0135ac008 --- /dev/null +++ b/src/main/wsl/wsl-daemon-owner-admission.ts @@ -0,0 +1,100 @@ +import { + wslDaemonIncarnationSchema, + type PersistedWslDaemonEndpoint +} from '../../shared/wsl-daemon-recovery' +import type { WslPtyOwner } from '../../shared/wsl-pty-id' +import type { DaemonEndpointIdentity } from '../daemon/daemon-hello-protocol' +import type { Store } from '../persistence' +import { proveWslDaemonIncarnationExited } from './wsl-daemon-incarnation' +import { + startPreparedWslDaemonOwner, + startRetainedWslDaemonOwner, + type PreparedWslDaemonEndpoint +} from './wsl-daemon-endpoint' + +export class WslDaemonAdmissionError extends Error {} + +/** Durable incarnation admission fences cold restore from displaced but still-live owners. */ +export class WslDaemonOwnerAdmission { + private pending = Promise.resolve() + + constructor( + private readonly store: Pick, + private readonly owner: WslPtyOwner, + private readonly endpoint: PersistedWslDaemonEndpoint, + private readonly signal: AbortSignal, + private readonly prepared?: PreparedWslDaemonEndpoint + ) {} + + admitIdentity = (identity: DaemonEndpointIdentity | null): Promise => { + const pending = this.pending + .then(() => this.admit(identity)) + .catch((cause) => { + throw new WslDaemonAdmissionError( + `WSL daemon identity could not be durably admitted: ${cause instanceof Error ? cause.message : String(cause)}`, + { cause } + ) + }) + this.pending = pending.catch(() => {}) + return pending + } + + private async admit(identity: DaemonEndpointIdentity | null): Promise { + this.signal.throwIfAborted() + const parsed = wslDaemonIncarnationSchema.safeParse( + identity && { + pid: identity.pid, + startedAtMs: identity.startedAtMs, + launchNonce: identity.launchNonce, + linuxStartTicks: identity.linuxStartTicks, + bootId: identity.bootId + } + ) + const previous = this.store.getWslDaemonRecovery(this.owner) + const incarnation = parsed.success ? parsed.data : undefined + if ( + previous?.incarnation && + JSON.stringify(previous.incarnation) !== JSON.stringify(incarnation) + ) { + if (!incarnation) { + throw new Error('Authenticated guest owner has no incarnation evidence') + } + await proveWslDaemonIncarnationExited(this.endpoint, previous.incarnation, this.signal) + } + await this.store.upsertWslDaemonRecovery( + { + kind: 'daemon', + ...this.owner, + endpoint: this.endpoint, + ...(incarnation ? { incarnation } : {}) + }, + previous?.incarnation ?? null + ) + this.signal.throwIfAborted() + } + + recover = async (): Promise => { + this.signal.throwIfAborted() + const previous = this.store.getWslDaemonRecovery(this.owner) + if (previous) { + await startRetainedWslDaemonOwner(this.endpoint, previous.incarnation, this.signal) + } else if (this.prepared) { + await startPreparedWslDaemonOwner(this.prepared, this.signal) + } else { + throw new Error('WSL daemon owner endpoint is unverifiable') + } + this.signal.throwIfAborted() + } + + async establishLease(establish: () => Promise): Promise { + try { + await establish() + } catch (error) { + if (error instanceof WslDaemonAdmissionError) { + throw error + } + await this.recover() + await establish() + } + } +} diff --git a/src/main/wsl/wsl-daemon-protocol-retention.test.ts b/src/main/wsl/wsl-daemon-protocol-retention.test.ts new file mode 100644 index 000000000000..2973a6a5a3c3 --- /dev/null +++ b/src/main/wsl/wsl-daemon-protocol-retention.test.ts @@ -0,0 +1,164 @@ +import '../daemon/mock-descendant-sweep' +import { afterEach, expect, it, vi } from 'vitest' +import { mkdtempSync, readFileSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { getDaemonSocketPath } from '../daemon/daemon-spawner' +import { DaemonServer } from '../daemon/daemon-server' +import { DaemonPtyAdapter } from '../daemon/daemon-pty-adapter' +import { connectDaemonSocket } from '../daemon/daemon-client-socket-connect' +import { createMockSubprocess } from '../daemon/daemon-pty-adapter-test-harness' +import type * as DaemonProtocol from '../daemon/daemon-protocol-version' +import { WslDaemonSessions } from './wsl-daemon-sessions' +import { toAppWslPtyId } from '../../shared/wsl-pty-id' +import type { WslDaemonRecovery } from '../../shared/wsl-daemon-recovery' +import { createWslDaemonTransport } from './wsl-daemon-transport' +import { prepareWslDaemonEndpoint, startRetainedWslDaemonOwner } from './wsl-daemon-endpoint' + +// A v37 desktop reconnects to the original v36 guest, including records without metadata. +vi.mock('../daemon/daemon-protocol-version', async (original) => { + const actual = await original() + return { ...actual, PROTOCOL_VERSION: 37, PREVIOUS_DAEMON_PROTOCOL_VERSIONS: [35, 36] } +}) +vi.mock('./wsl-daemon-transport', () => ({ createWslDaemonTransport: vi.fn() })) +vi.mock('./wsl-daemon-endpoint', () => ({ + prepareWslDaemonEndpoint: vi.fn(), + startPreparedWslDaemonOwner: vi.fn(), + startRetainedWslDaemonOwner: vi.fn() +})) +vi.mock('./wsl-bun-runtime', () => ({ + createRunningWslRuntimeRunner: () => ({ run: async () => '/bin/bash' }) +})) +vi.mock('./wsl-distribution-identity', () => ({ + readWslDistributionIdentity: async () => 'registration' +})) +vi.mock('../ipc/pty/provider/registry', () => ({ + registerWslPtyProvider: vi.fn(() => () => {}) +})) + +const cleanup: (() => Promise)[] = [] +afterEach(async () => { + for (const dispose of cleanup.splice(0).toReversed()) { + await dispose() + } + vi.clearAllMocks() +}) + +async function fixture(protocolVersion?: number) { + const dir = mkdtempSync(join(tmpdir(), 'wsl-protocol-')) + const socketPath = getDaemonSocketPath(dir, 36) + const tokenPath = join(dir, 'token') + const subprocess = createMockSubprocess() + const spawn = vi.fn(() => subprocess) + const server = new DaemonServer({ + socketPath, + tokenPath, + protocolVersion: 36, + spawnSubprocess: spawn, + log: { log() {}, close() {} } + }) + await server.start() + cleanup.push(async () => { + await server.shutdown() + rmSync(dir, { recursive: true, force: true }) + }) + const original = new DaemonPtyAdapter({ socketPath, tokenPath, protocolVersion: 36 }) + cleanup.push(async () => original.disconnectOnly()) + const terminal = await original.spawn({ cols: 80, rows: 24, isNewSession: true }) + await original.disconnectOnly() + vi.mocked(createWslDaemonTransport).mockReturnValue({ + readToken: () => readFileSync(tokenPath, 'utf8').trim(), + connect: (_role, operation) => connectDaemonSocket(socketPath, operation.timeoutMs) + }) + const owner = { distro: 'Ubuntu', relayBuildId: 'daemon+original+profile' } + let record: WslDaemonRecovery = { + kind: 'daemon', + ...owner, + endpoint: { + distro: owner.distro, + distributionId: 'registration', + userName: 'alice', + userId: '1000', + home: '/home/alice', + runtime: '/bin/bun', + entry: '/daemon.js', + envBinary: '/usr/bin/env', + socket: '/private/socket', + tokenPath: '/private/token', + serverBuildId: 'original', + ...(protocolVersion === undefined ? {} : { protocolVersion }) + } + } + const store = { + getWslDaemonRecovery: () => record, + upsertWslDaemonRecovery: vi.fn(async (value: WslDaemonRecovery) => { + record = value + }) + } + const sessions = new WslDaemonSessions({ store, profileScope: '/profile', historyRoot: dir }) + cleanup.push(() => sessions.dispose()) + return { sessions, owner, terminal, subprocess, spawn, store, endpoint: record.endpoint } +} + +it.each([36, undefined])( + 'retains attach and control after upgrade with owner protocol %s', + async (version) => { + const { sessions, owner, terminal, subprocess, spawn } = await fixture(version) + const { provider } = await sessions.reconnect(owner) + const id = toAppWslPtyId(owner, terminal.id) + await provider.attach(id) + await provider.writeWithSettlement(id, 'retained input\r') + await vi.waitFor(() => expect(subprocess.write).toHaveBeenCalledWith('retained input\r')) + expect(spawn).toHaveBeenCalledOnce() + expect(startRetainedWslDaemonOwner).not.toHaveBeenCalled() + expect(subprocess.kill).not.toHaveBeenCalled() + expect(await provider.probePtyLiveness(id)).toBe(true) + } +) + +it.each([35, 999])( + 'refuses unsupported protocol %s without rewriting, recovering or killing the live owner', + async (version) => { + const { sessions, owner, subprocess, store } = await fixture(version) + await expect(sessions.reconnect(owner)).rejects.toThrow(`protocol ${version} is unsupported`) + expect(createWslDaemonTransport).not.toHaveBeenCalled() + expect(startRetainedWslDaemonOwner).not.toHaveBeenCalled() + expect(store.upsertWslDaemonRecovery).not.toHaveBeenCalled() + expect(subprocess.kill).not.toHaveBeenCalled() + } +) + +it('reuses a missing-version owner for fresh preparation of the same v36 artifact', async () => { + const { sessions, owner, endpoint, spawn, store } = await fixture() + const retained = await sessions.reconnect(owner) + vi.mocked(prepareWslDaemonEndpoint).mockResolvedValue({ + owner, + endpoint: { ...endpoint, protocolVersion: 36 }, + entry: endpoint.entry, + path: '/bin', + artifactId: endpoint.serverBuildId + }) + const fresh = await sessions.prepareFresh(owner.distro) + expect(fresh.connection).toBe(retained) + await fresh.connection.provider.spawn({ cols: 80, rows: 24, isNewSession: true }) + expect(spawn).toHaveBeenCalledTimes(2) + expect(store.getWslDaemonRecovery().endpoint).toEqual(endpoint) + expect(startRetainedWslDaemonOwner).not.toHaveBeenCalled() +}) + +it.each([{ protocolVersion: 37 }, { userName: 'bob' }])( + 'refuses a changed owner endpoint after missing-version reconnect: %s', + async (change) => { + const { sessions, owner, endpoint } = await fixture() + await sessions.reconnect(owner) + vi.mocked(prepareWslDaemonEndpoint).mockResolvedValue({ + owner, + endpoint: { ...endpoint, protocolVersion: 36, ...change }, + entry: endpoint.entry, + path: '/bin', + artifactId: endpoint.serverBuildId + }) + await expect(sessions.prepareFresh(owner.distro)).rejects.toThrow('cannot change') + expect(startRetainedWslDaemonOwner).not.toHaveBeenCalled() + } +) diff --git a/src/main/wsl/wsl-daemon-protocol.ts b/src/main/wsl/wsl-daemon-protocol.ts new file mode 100644 index 000000000000..f1f380063c55 --- /dev/null +++ b/src/main/wsl/wsl-daemon-protocol.ts @@ -0,0 +1,20 @@ +import { + INITIAL_WSL_DAEMON_PROTOCOL_VERSION, + type PersistedWslDaemonEndpoint +} from '../../shared/wsl-daemon-recovery' +import { + PREVIOUS_DAEMON_PROTOCOL_VERSIONS, + PROTOCOL_VERSION +} from '../daemon/daemon-protocol-version' + +export function retainedWslDaemonProtocolVersion(endpoint: PersistedWslDaemonEndpoint): number { + const version = endpoint.protocolVersion ?? INITIAL_WSL_DAEMON_PROTOCOL_VERSION + if ( + version < INITIAL_WSL_DAEMON_PROTOCOL_VERSION || + (version !== PROTOCOL_VERSION && + !PREVIOUS_DAEMON_PROTOCOL_VERSIONS.some((supported) => supported === version)) + ) { + throw new Error(`WSL daemon protocol ${version} is unsupported; its terminals are unverifiable`) + } + return version +} diff --git a/src/main/wsl/wsl-daemon-pty-provider.ts b/src/main/wsl/wsl-daemon-pty-provider.ts new file mode 100644 index 000000000000..0e1bc1d00807 --- /dev/null +++ b/src/main/wsl/wsl-daemon-pty-provider.ts @@ -0,0 +1,138 @@ +import { + parseAppWslPtyId, + toAppWslPtyId, + toRelayWslPtyId, + type WslPtyOwner +} from '../../shared/wsl-pty-id' +import type { DaemonPtyAdapter } from '../daemon/daemon-pty-adapter' +import type { IPtyProvider, PtySpawnOptions, PtySpawnResult } from '../providers/types' + +/** Only translates identities; the shared daemon adapter owns sessions, snapshots and recovery. */ +export class WslDaemonPtyProvider implements IPtyProvider { + readonly owner: Readonly + + constructor( + owner: WslPtyOwner, + private readonly adapter: DaemonPtyAdapter + ) { + this.owner = Object.freeze({ ...owner }) + } + + private guestId = (id: string): string => toRelayWslPtyId(this.owner, id) + private appId = (id: string): string => toAppWslPtyId(this.owner, id) + + async spawn(options: PtySpawnOptions): Promise { + const encoded = options.sessionId && parseAppWslPtyId(options.sessionId) + if (options.sessionId && !encoded && (options.attachOnly || !options.isNewSession)) { + throw new Error('Guest daemon attach requires an encoded WSL terminal identity') + } + const result = await this.adapter.spawn({ + ...options, + sessionId: encoded ? this.guestId(options.sessionId!) : options.sessionId, + terminalWindowsWslDistro: undefined, + terminalWindowsPowerShellImplementation: undefined + }) + return { + ...result, + id: this.appId(result.id), + wslDistro: this.owner.distro, + ...(result.agentSessionEnsure + ? { + agentSessionEnsure: { + ...result.agentSessionEnsure, + owner: { + ...result.agentSessionEnsure.owner, + ptyId: this.appId(result.agentSessionEnsure.owner.ptyId) + } + } + } + : {}) + } + } + + attach = (id: string) => this.adapter.attach(this.guestId(id)) + hasPty = (id: string) => this.adapter.hasPty(this.guestId(id)) + probePtyLiveness = (id: string) => this.adapter.probePtyLiveness(this.guestId(id)) + write = (id: string, data: string) => this.adapter.write(this.guestId(id), data) + writeWithSettlement = (id: string, data: string) => + this.adapter.writeWithSettlement(this.guestId(id), data) + resize = (id: string, cols: number, rows: number) => + this.adapter.resize(this.guestId(id), cols, rows) + pauseProducer = (id: string) => this.adapter.pauseProducer(this.guestId(id)) + resumeProducer = (id: string) => this.adapter.resumeProducer(this.guestId(id)) + setPtyBackgrounded = (id: string, background: boolean) => + this.adapter.setPtyBackgrounded(this.guestId(id), background) + shutdown: IPtyProvider['shutdown'] = (id, options) => + this.adapter.shutdown(this.guestId(id), options) + sendSignal = (id: string, signal: string) => this.adapter.sendSignal(this.guestId(id), signal) + getCwd = (id: string) => this.adapter.getCwd(this.guestId(id)) + getInitialCwd = (id: string) => this.adapter.getInitialCwd(this.guestId(id)) + clearBuffer = (id: string) => this.adapter.clearBuffer(this.guestId(id)) + closeStartupQueryAuthority = (id: string) => + this.adapter.closeStartupQueryAuthority(this.guestId(id)) + acknowledgeDataEvent = (id: string, count: number) => + this.adapter.acknowledgeDataEvent(this.guestId(id), count) + async hasChildProcesses(id: string): Promise { + const guestId = this.guestId(id) + try { + const inspection = await this.adapter.inspectProcess(guestId) + return inspection.childProcessEvidence !== 'no-children' + } catch { + return true + } + } + getForegroundProcess = (id: string) => this.adapter.getForegroundProcess(this.guestId(id)) + confirmForegroundProcess = (id: string) => this.adapter.confirmForegroundProcess(this.guestId(id)) + confirmShellForeground = (id: string) => this.adapter.confirmShellForeground(this.guestId(id)) + inspectProcess = (id: string, options?: Parameters[1]) => + this.adapter.inspectProcess(this.guestId(id), options) + getBufferSnapshot = ( + id: string, + options?: Parameters[1] + ) => this.adapter.getBufferSnapshot(this.guestId(id), options) + canProvideAuthoritativeBufferSnapshot = (id: string) => + this.adapter.canProvideAuthoritativeBufferSnapshot(this.guestId(id)) + getAppliedSize = (id: string) => this.adapter.getAppliedSize(this.guestId(id)) + supportsGitCredentialGuardHost = () => this.adapter.supportsGitCredentialGuardHost() + supportsAgentSessionClaims = () => this.adapter.supportsAgentSessionClaims() + supportsAgentSessionCreateOperations = () => this.adapter.supportsAgentSessionCreateOperations() + providesAgentSessionOwnerListings = (id: string) => + this.adapter.providesAgentSessionOwnerListings(this.guestId(id)) + getDefaultShell = () => this.adapter.getDefaultShell() + getProfiles = () => this.adapter.getProfiles() + serialize = (ids: string[]) => this.adapter.serialize(ids.map(this.guestId)) + revive = (state: string) => this.adapter.revive(state) + ackColdRestore = (id: string) => this.adapter.ackColdRestore(this.guestId(id)) + + async listProcesses(options?: Parameters[0]) { + const processes = await this.adapter.listProcesses(options) + return processes.map((entry) => ({ + ...entry, + id: this.appId(entry.id), + wslDistro: this.owner.distro, + ...(entry.agentSessionOwners + ? { + agentSessionOwners: entry.agentSessionOwners.map((owner) => ({ + ...owner, + ptyId: this.appId(owner.ptyId) + })) + } + : {}) + })) + } + + onData: IPtyProvider['onData'] = (listener) => + this.adapter.onData((event) => listener({ ...event, id: this.appId(event.id) })) + onReplay: IPtyProvider['onReplay'] = (listener) => + this.adapter.onReplay((event) => listener({ ...event, id: this.appId(event.id) })) + onExit: IPtyProvider['onExit'] = (listener) => + this.adapter.onExit((event) => listener({ ...event, id: this.appId(event.id) })) + onBackgroundStreamEvent: NonNullable = (listener) => + this.adapter.onBackgroundStreamEvent((event) => + listener({ ...event, id: this.appId(event.id) }) + ) + onWriteUnavailable: NonNullable = (listener) => + this.adapter.onWriteUnavailable((event) => listener({ ...event, id: this.appId(event.id) })) + establishLifecycleLease = () => this.adapter.establishLifecycleLease() + disconnectOnly = () => this.adapter.disconnectOnly() +} diff --git a/src/main/wsl/wsl-daemon-reattach-hooks.test.ts b/src/main/wsl/wsl-daemon-reattach-hooks.test.ts new file mode 100644 index 000000000000..583b930d1513 --- /dev/null +++ b/src/main/wsl/wsl-daemon-reattach-hooks.test.ts @@ -0,0 +1,52 @@ +import { beforeEach, expect, it, vi } from 'vitest' +import { prepareWslDaemonReattachHooks } from './wsl-daemon-reattach-hooks' +import { wslHookRelayManager } from '../agent-hooks/wsl-hook-relay-manager' +import { toAppWslPtyId } from '../../shared/wsl-pty-id' +import type { WslDaemonConnection } from './wsl-daemon-sessions' + +vi.mock('../agent-hooks/wsl-hook-relay-manager', () => ({ + wslHookRelayManager: { ensureForDistro: vi.fn(async () => {}) } +})) +beforeEach(() => vi.clearAllMocks()) + +it('restarts hooks as the persisted user rather than the current default', async () => { + const owner = { distro: 'Ubuntu', relayBuildId: 'retained-owner' } + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Hook preparation reads only endpoint.distro and endpoint.userName from this connection. + const connection = { endpoint: { distro: 'Ubuntu', userName: 'root' } } as WslDaemonConnection + const reconnect = vi.fn(async () => connection) + const result = { id: toAppWslPtyId(owner, 'retained'), isReattach: true } + await expect( + prepareWslDaemonReattachHooks({ result, sessions: { reconnect }, hooksEnabled: true }) + ).resolves.toBe(true) + expect(reconnect).toHaveBeenCalledWith({ ...owner, relayPtyId: 'retained' }) + expect(wslHookRelayManager.ensureForDistro).toHaveBeenCalledWith( + 'Ubuntu', + undefined, + undefined, + 'root' + ) +}) + +it('does not prepare hooks for disabled hooks or a fresh guest spawn', async () => { + const id = toAppWslPtyId({ distro: 'Ubuntu', relayBuildId: 'owner' }, 'terminal') + await expect( + prepareWslDaemonReattachHooks({ result: { id, isReattach: true }, hooksEnabled: false }) + ).resolves.toBe(true) + await expect(prepareWslDaemonReattachHooks({ result: { id }, hooksEnabled: true })).resolves.toBe( + true + ) + expect(wslHookRelayManager.ensureForDistro).not.toHaveBeenCalled() +}) + +it('does not delay an already-live attach on unavailable hook preparation', async () => { + const reconnect = vi.fn(() => new Promise(() => {})) + const id = toAppWslPtyId({ distro: 'Ubuntu', relayBuildId: 'owner' }, 'terminal') + await expect( + prepareWslDaemonReattachHooks({ + result: { id, isReattach: true }, + sessions: { reconnect }, + hooksEnabled: true + }) + ).resolves.toBe(true) + expect(wslHookRelayManager.ensureForDistro).not.toHaveBeenCalled() +}) diff --git a/src/main/wsl/wsl-daemon-reattach-hooks.ts b/src/main/wsl/wsl-daemon-reattach-hooks.ts new file mode 100644 index 000000000000..0ba46596f812 --- /dev/null +++ b/src/main/wsl/wsl-daemon-reattach-hooks.ts @@ -0,0 +1,38 @@ +import { waitForPromiseWithSignal } from '../../shared/abort-signal-reason' +import { parseAppWslPtyId } from '../../shared/wsl-pty-id' +import type { WslDaemonSessions } from './wsl-daemon-sessions' +import { wslHookRelayManager } from '../agent-hooks/wsl-hook-relay-manager' +import type { PtySpawnResult } from '../providers/types' + +/** Return true for guest owners so callers never fall back to default-user hook preparation. */ +export async function prepareWslDaemonReattachHooks(args: { + result: Pick + sessions?: Pick + hooksEnabled: boolean + selectedCodexHomePath?: string | null +}): Promise { + const owner = parseAppWslPtyId(args.result.id) + if (!owner) { + return false + } + if (!args.result.isReattach || !args.hooksEnabled) { + return true + } + const sessions = args.sessions + if (!sessions) { + console.warn('[wsl] Terminal hooks unavailable: captured owner is not connected') + return true + } + void waitForPromiseWithSignal( + sessions.reconnect(owner).then(async ({ endpoint }) => { + await wslHookRelayManager.ensureForDistro( + endpoint.distro, + args.selectedCodexHomePath, + undefined, + endpoint.userName + ) + }), + AbortSignal.timeout(15_000) + ).catch((error) => console.warn('[wsl] Terminal reattach hooks unavailable', error)) + return true +} diff --git a/src/main/wsl/wsl-daemon-sessions.test.ts b/src/main/wsl/wsl-daemon-sessions.test.ts new file mode 100644 index 000000000000..ca13a5aa1acf --- /dev/null +++ b/src/main/wsl/wsl-daemon-sessions.test.ts @@ -0,0 +1,329 @@ +import { beforeEach, expect, it, vi } from 'vitest' +import { parseAppWslPtyId, toAppWslPtyId } from '../../shared/wsl-pty-id' +import { WslDaemonSessions } from './wsl-daemon-sessions' +import type { WslDaemonRecovery } from '../../shared/wsl-daemon-recovery' +import { DaemonPtyAdapter } from '../daemon/daemon-pty-adapter' +import { prepareWslDaemonEndpoint, startPreparedWslDaemonOwner } from './wsl-daemon-endpoint' +import { createWslDaemonTransport } from './wsl-daemon-transport' +import { createRunningWslRuntimeRunner } from './wsl-bun-runtime' +import { registerWslPtyProvider } from '../ipc/pty/provider/registry' +import { readWslDistributionIdentity } from './wsl-distribution-identity' + +const { lease, detach, unregister, run } = vi.hoisted(() => ({ + lease: vi.fn(), + detach: vi.fn(), + unregister: vi.fn(), + run: vi.fn() +})) +vi.mock('../daemon/daemon-pty-adapter', () => ({ + DaemonPtyAdapter: vi.fn( + class { + constructor(private readonly options: ConstructorParameters[0]) {} + establishLifecycleLease = async () => { + await lease() + await this.options.guest?.admitIdentity?.(null) + } + disconnectOnly = detach + } + ) +})) +vi.mock('../ipc/pty/provider/registry', () => ({ registerWslPtyProvider: vi.fn(() => unregister) })) +vi.mock('./wsl-daemon-endpoint', () => ({ + prepareWslDaemonEndpoint: vi.fn(), + startPreparedWslDaemonOwner: vi.fn(), + startRetainedWslDaemonOwner: vi.fn(async () => { + throw new Error('owner unverifiable') + }) +})) +vi.mock('./wsl-daemon-transport', () => ({ createWslDaemonTransport: vi.fn(() => ({})) })) +vi.mock('./wsl-bun-runtime', () => ({ createRunningWslRuntimeRunner: vi.fn(() => ({ run })) })) +vi.mock('./wsl-distribution-identity', () => ({ readWslDistributionIdentity: vi.fn() })) +const owner = { distro: 'Ubuntu', relayBuildId: 'daemon+artifact+profile-user' } +const endpoint = { + distro: 'Ubuntu', + distributionId: 'registration', + userName: 'alice', + userId: '1000', + home: '/home/alice', + envBinary: '/usr/bin/env', + runtime: '/bin/bun', + entry: '/daemon.js', + socket: '/private/socket', + tokenPath: '/private/token', + serverBuildId: 'artifact' +} +const prepared = { owner, endpoint, entry: '/daemon.js', path: '/bin', artifactId: 'artifact' } +function deferred() { + let resolve!: (value: T) => void + const promise = new Promise((done) => { + resolve = done + }) + return { promise, resolve } +} +function setup(record: WslDaemonRecovery | null = null) { + const store = { + getWslDaemonRecovery: vi.fn(() => record), + upsertWslDaemonRecovery: vi.fn(async (value: WslDaemonRecovery) => { + record = value + }) + } + return { + store, + sessions: new WslDaemonSessions({ store, profileScope: '/profile', historyRoot: '/history' }) + } +} +beforeEach(() => { + vi.clearAllMocks() + lease.mockReset().mockResolvedValue(undefined) + detach.mockReset().mockResolvedValue(undefined) + run.mockReset().mockResolvedValue('/bin/bash') + vi.mocked(prepareWslDaemonEndpoint).mockReset().mockResolvedValue(prepared) + vi.mocked(startPreparedWslDaemonOwner).mockReset().mockResolvedValue(undefined) + vi.mocked(readWslDistributionIdentity).mockResolvedValue('registration') +}) + +it('coalesces preparation and publishes only after durable owner admission', async () => { + const { store, sessions } = setup() + const persisted = deferred() + store.upsertWslDaemonRecovery.mockReturnValue(persisted.promise) + const first = sessions.prepareFresh('Ubuntu') + const second = sessions.prepareFresh('ubuntu') + await vi.waitFor(() => expect(store.upsertWslDaemonRecovery).toHaveBeenCalledOnce()) + expect(registerWslPtyProvider).not.toHaveBeenCalled() + persisted.resolve() + const [a, b] = await Promise.all([first, second]) + expect(a).toBe(b) + expect(Object.isFrozen(a.execution)).toBe(true) + expect(a.execution).toEqual({ + distro: 'Ubuntu', + userName: 'alice', + userId: '1000', + home: '/home/alice' + }) + expect(prepareWslDaemonEndpoint).toHaveBeenCalledOnce() + expect(registerWslPtyProvider).toHaveBeenCalledOnce() + expect(startPreparedWslDaemonOwner).not.toHaveBeenCalled() + expect(DaemonPtyAdapter).toHaveBeenCalledWith( + expect.objectContaining({ + historyPath: expect.stringMatching(/history[/\\][a-f0-9]{64}$/), + guest: expect.objectContaining({ defaultShell: '/bin/bash', defaultCwd: '/home/alice' }) + }) + ) + await sessions.dispose() +}) + +it('reconnects only the persisted explicit owner without preparing or starting', async () => { + const { sessions } = setup({ kind: 'daemon', ...owner, endpoint }) + const [a, b] = await Promise.all([sessions.reconnect(owner), sessions.reconnect(owner)]) + expect(a).toBe(b) + expect(createWslDaemonTransport).toHaveBeenCalledWith(endpoint) + expect(run.mock.lastCall?.[0].args).toEqual( + expect.arrayContaining(['--no-env-file', '--config=/dev/null', '--no-install']) + ) + expect(createRunningWslRuntimeRunner).toHaveBeenCalledWith( + 'Ubuntu', + expect.any(AbortSignal), + 'alice' + ) + expect(prepareWslDaemonEndpoint).not.toHaveBeenCalled() + expect(startPreparedWslDaemonOwner).not.toHaveBeenCalled() + await sessions.dispose() +}) + +it('delegates failed fresh contact to the safe starter, but never starts on durable-write failure', async () => { + lease.mockRejectedValueOnce(new Error('missing endpoint')) + const { store, sessions } = setup() + store.upsertWslDaemonRecovery.mockRejectedValueOnce(new Error('disk full')) + await expect(sessions.prepareFresh('Ubuntu')).rejects.toThrow('disk full') + expect(startPreparedWslDaemonOwner).toHaveBeenCalledOnce() + expect(lease).toHaveBeenCalledTimes(2) + expect(detach).toHaveBeenCalledOnce() + expect(registerWslPtyProvider).not.toHaveBeenCalled() + await sessions.dispose() +}) + +it('does not retry or publish when the fresh owner cannot be verified by the starter', async () => { + lease.mockRejectedValueOnce(new Error('missing endpoint')) + vi.mocked(startPreparedWslDaemonOwner).mockRejectedValueOnce(new Error('owner unverifiable')) + const { store, sessions } = setup() + await expect(sessions.prepareFresh('Ubuntu')).rejects.toThrow('owner unverifiable') + expect(startPreparedWslDaemonOwner).toHaveBeenCalledWith(prepared, expect.any(AbortSignal)) + expect(lease).toHaveBeenCalledOnce() + expect(store.upsertWslDaemonRecovery).not.toHaveBeenCalled() + expect(registerWslPtyProvider).not.toHaveBeenCalled() + expect(detach).toHaveBeenCalledOnce() + await sessions.dispose() +}) + +it('isolates caller cancellation from another waiter and the durable connection', async () => { + const { store, sessions } = setup() + const persisted = deferred() + store.upsertWslDaemonRecovery.mockReturnValue(persisted.promise) + const abort = new AbortController() + const canceled = sessions.prepareFresh('Ubuntu', abort.signal) + const survivor = sessions.prepareFresh('Ubuntu') + abort.abort(new Error('cancel only this spawn')) + await expect(canceled).rejects.toThrow('cancel only this spawn') + persisted.resolve() + await survivor + expect(detach).not.toHaveBeenCalled() + await sessions.dispose() +}) + +it('fences publication after profile disposal during a durable write', async () => { + const { store, sessions } = setup() + const persisted = deferred() + store.upsertWslDaemonRecovery.mockReturnValue(persisted.promise) + const opening = sessions.prepareFresh('Ubuntu') + const rejected = expect(opening).rejects.toThrow('disposed') + await vi.waitFor(() => expect(store.upsertWslDaemonRecovery).toHaveBeenCalledOnce()) + const closing = sessions.dispose() + persisted.resolve() + await Promise.all([rejected, closing]) + expect(registerWslPtyProvider).not.toHaveBeenCalled() + expect(detach).toHaveBeenCalledOnce() +}) + +it('unregisters immediately and awaits detach checkpoint completion', async () => { + const { sessions } = setup() + await sessions.prepareFresh('Ubuntu') + const checkpoint = deferred() + detach.mockReturnValue(checkpoint.promise) + let completed = false + const closing = sessions.dispose().then(() => { + completed = true + }) + expect(unregister).toHaveBeenCalledOnce() + await Promise.resolve() + expect(completed).toBe(false) + checkpoint.resolve() + await closing + expect(completed).toBe(true) +}) + +it('does not execute guest probes after a distro replacement', async () => { + vi.mocked(readWslDistributionIdentity).mockResolvedValue('replacement') + const { sessions } = setup({ kind: 'daemon', ...owner, endpoint }) + await expect(sessions.reconnect(owner)).rejects.toThrow('unverifiable') + expect(run).not.toHaveBeenCalled() + expect(registerWslPtyProvider).not.toHaveBeenCalled() + await sessions.dispose() +}) + +it('refuses unknown persisted owners instead of capturing the current default user', async () => { + const { sessions } = setup() + await expect(sessions.reconnect(owner)).rejects.toThrow('unverifiable') + expect(prepareWslDaemonEndpoint).not.toHaveBeenCalled() + expect(run).not.toHaveBeenCalled() + await sessions.dispose() +}) + +it('does not start or republish an unreachable persisted daemon', async () => { + const { sessions, store } = setup({ kind: 'daemon', ...owner, endpoint }) + lease.mockRejectedValueOnce(new Error('owner contact unverifiable')) + await expect(sessions.reconnect(owner)).rejects.toThrow('unverifiable') + expect(startPreparedWslDaemonOwner).not.toHaveBeenCalled() + expect(store.upsertWslDaemonRecovery).not.toHaveBeenCalled() + expect(registerWslPtyProvider).not.toHaveBeenCalled() + expect(detach).toHaveBeenCalledOnce() + await sessions.dispose() +}) + +it('does not publish delayed preparation after the profile has closed', async () => { + const preparation = deferred() + vi.mocked(prepareWslDaemonEndpoint).mockReturnValue(preparation.promise) + const { sessions } = setup() + const opening = sessions.prepareFresh('Ubuntu') + const rejected = expect(opening).rejects.toThrow('disposed') + const closing = sessions.dispose() + preparation.resolve(prepared) + await Promise.all([rejected, closing]) + expect(DaemonPtyAdapter).not.toHaveBeenCalled() + expect(registerWslPtyProvider).not.toHaveBeenCalled() +}) + +it('refuses a changed endpoint under an already-published owner identity', async () => { + const { sessions } = setup() + await sessions.prepareFresh('Ubuntu') + vi.mocked(prepareWslDaemonEndpoint).mockResolvedValue({ + ...prepared, + endpoint: { ...endpoint, userName: 'bob' } + }) + await expect(sessions.prepareFresh('Ubuntu')).rejects.toThrow('cannot change') + expect(DaemonPtyAdapter).toHaveBeenCalledOnce() + expect(registerWslPtyProvider).toHaveBeenCalledOnce() + await sessions.dispose() +}) + +it('reconnects a parsed terminal identity without persisting its session suffix', async () => { + const { store, sessions } = setup({ kind: 'daemon', ...owner, endpoint }) + const parsed = parseAppWslPtyId(toAppWslPtyId(owner, 'persisted-session')) + if (!parsed) { + throw new Error('Invalid fixture terminal identity') + } + try { + const connection = await sessions.reconnect(parsed) + expect(connection.owner).toEqual(owner) + expect(store.upsertWslDaemonRecovery).toHaveBeenCalledWith( + { + kind: 'daemon', + ...owner, + endpoint + }, + null + ) + expect(startPreparedWslDaemonOwner).not.toHaveBeenCalled() + } finally { + await sessions.dispose() + } +}) + +it('refuses a missing identity-less owner even for a fresh spawn', async () => { + const { sessions } = setup({ kind: 'daemon', ...owner, endpoint }) + await sessions.prepareFresh('Ubuntu') + lease.mockRejectedValueOnce(new Error('owner contact unverifiable')) + await expect(sessions.reconnect(owner)).rejects.toThrow('unverifiable') + expect(startPreparedWslDaemonOwner).not.toHaveBeenCalled() + lease.mockRejectedValueOnce(new Error('missing endpoint')) + await expect(sessions.prepareFresh('Ubuntu')).rejects.toThrow('unverifiable') + expect(startPreparedWslDaemonOwner).not.toHaveBeenCalled() + expect(DaemonPtyAdapter).toHaveBeenCalledOnce() + await sessions.dispose() +}) + +it('retains a cached owner when fresh startup cannot prove its endpoint absent', async () => { + const { sessions } = setup({ kind: 'daemon', ...owner, endpoint }) + await sessions.prepareFresh('Ubuntu') + lease.mockRejectedValueOnce(new Error('contact failed')) + vi.mocked(startPreparedWslDaemonOwner).mockRejectedValueOnce(new Error('owner unverifiable')) + await expect(sessions.prepareFresh('Ubuntu')).rejects.toThrow('unverifiable') + expect(detach).not.toHaveBeenCalled() + expect(unregister).not.toHaveBeenCalled() + await sessions.reconnect(owner) + expect(DaemonPtyAdapter).toHaveBeenCalledOnce() + await sessions.dispose() +}) + +it('does not bypass missing identity proof when fresh spawn joins failed reconnect', async () => { + const { sessions } = setup({ kind: 'daemon', ...owner, endpoint }) + let fail!: (error: Error) => void + lease.mockImplementationOnce( + () => + new Promise((_resolve, reject) => { + fail = reject + }) + ) + const restore = sessions.reconnect(owner) + const rejected = expect(restore).rejects.toThrow('unverifiable') + await vi.waitFor(() => expect(lease).toHaveBeenCalledOnce()) + const fresh = expect(sessions.prepareFresh('Ubuntu')).rejects.toThrow('unverifiable') + await vi.waitFor(() => expect(prepareWslDaemonEndpoint).toHaveBeenCalledOnce()) + await Promise.resolve() + lease.mockRejectedValueOnce(new Error('missing endpoint')) + fail(new Error('missing endpoint')) + await rejected + await fresh + expect(startPreparedWslDaemonOwner).not.toHaveBeenCalled() + expect(registerWslPtyProvider).not.toHaveBeenCalled() + await sessions.dispose() +}) diff --git a/src/main/wsl/wsl-daemon-sessions.ts b/src/main/wsl/wsl-daemon-sessions.ts new file mode 100644 index 000000000000..0c84d047b322 --- /dev/null +++ b/src/main/wsl/wsl-daemon-sessions.ts @@ -0,0 +1,262 @@ +import { retainedWslDaemonProtocolVersion } from './wsl-daemon-protocol' +import { WslDaemonOwnerAdmission } from './wsl-daemon-owner-admission' +import { bunOwnedRuntimeArgs } from '../../shared/bun-owned-runtime-args' +import { createHash } from 'node:crypto' +import { join } from 'node:path' +import { waitForPromiseWithSignal } from '../../shared/abort-signal-reason' +import { + normalizeWslDaemonRecovery, + sameWslDaemonEndpoint, + type PersistedWslDaemonEndpoint +} from '../../shared/wsl-daemon-recovery' +import { wslPtyOwnerKey } from '../../shared/wsl-pty-consumer-recovery' +import type { WslPtyOwner } from '../../shared/wsl-pty-id' +import type { Store } from '../persistence' +import { DaemonPtyAdapter } from '../daemon/daemon-pty-adapter' +import { registerWslPtyProvider } from '../ipc/pty/provider/registry' +import { createRunningWslRuntimeRunner } from './wsl-bun-runtime' +import { createWslDaemonTransport } from './wsl-daemon-transport' +import { WslDaemonPtyProvider } from './wsl-daemon-pty-provider' +import { prepareWslDaemonEndpoint, type PreparedWslDaemonEndpoint } from './wsl-daemon-endpoint' +import type { WslAccountExecutionContext } from './wsl-account-execution-context' +import { readWslDistributionIdentity } from './wsl-distribution-identity' + +export type WslDaemonConnection = Readonly<{ + owner: Readonly + endpoint: Readonly + provider: WslDaemonPtyProvider +}> +export type PreparedWslDaemonTerminalOwner = Readonly<{ + prepared: PreparedWslDaemonEndpoint + execution: WslAccountExecutionContext + connection: WslDaemonConnection +}> +type AdmittedConnection = WslDaemonConnection & { establishLease: () => Promise } +type Entry = { + endpoint: Readonly + pending: Promise + close?: () => Promise +} + +/** Profile-scoped composition only; the existing daemon owns terminal state and reconnection. */ +export class WslDaemonSessions { + private readonly entries = new Map() + private readonly preparing = new Map>() + private readonly lifetime = new AbortController() + private closing: Promise | undefined + + constructor( + private readonly options: { + store: Pick + profileScope: string + historyRoot: string + } + ) { + if (!options.profileScope || !options.historyRoot) { + throw new Error('WSL daemon sessions require profile and history scopes') + } + } + + prepareFresh(distro: string, signal?: AbortSignal): Promise { + signal?.throwIfAborted() + this.lifetime.signal.throwIfAborted() + const key = distro.trim().toLowerCase() + if (!key) { + throw new Error('WSL terminal distro is required') + } + let pending = this.preparing.get(key) + if (!pending) { + pending = this.prepare(distro).finally(() => this.preparing.delete(key)) + this.preparing.set(key, pending) + } + return waitForPromiseWithSignal(pending, signal) + } + + private async prepare(distro: string): Promise { + const prepared = await prepareWslDaemonEndpoint( + distro, + this.options.profileScope, + this.lifetime.signal + ) + this.lifetime.signal.throwIfAborted() + const connection = await this.connect(prepared.owner, prepared.endpoint, undefined, prepared) + return Object.freeze({ + prepared, + connection, + execution: Object.freeze({ + distro: prepared.endpoint.distro, + userName: prepared.endpoint.userName, + userId: prepared.endpoint.userId, + home: prepared.endpoint.home + }) + }) + } + + reconnect(owner: WslPtyOwner, signal?: AbortSignal): Promise { + signal?.throwIfAborted() + this.lifetime.signal.throwIfAborted() + const record = this.options.store.getWslDaemonRecovery(owner) + if (!record) { + return Promise.reject(new Error('WSL daemon owner endpoint is unverifiable')) + } + const reconnecting = this.connect(owner, record.endpoint).then(async (connection) => { + this.lifetime.signal.throwIfAborted() + await connection.establishLease() + this.lifetime.signal.throwIfAborted() + return connection + }) + return waitForPromiseWithSignal(reconnecting, signal) + } + + private connect( + owner: WslPtyOwner, + endpoint: PersistedWslDaemonEndpoint, + signal?: AbortSignal, + prepared?: PreparedWslDaemonEndpoint + ): Promise { + this.lifetime.signal.throwIfAborted() + const identity = Object.freeze({ distro: owner.distro, relayBuildId: owner.relayBuildId }) + const record = normalizeWslDaemonRecovery({ kind: 'daemon', ...identity, endpoint }) + if (!record) { + return Promise.reject(new Error('WSL daemon owner endpoint is incomplete')) + } + const key = wslPtyOwnerKey(owner) + let entry = this.entries.get(key) + if (entry && !sameWslDaemonEndpoint(entry.endpoint, record.endpoint)) { + return Promise.reject(new Error('WSL daemon owner endpoint cannot change')) + } + if (!entry) { + const captured = Object.freeze({ ...record.endpoint }) + const pending = this.open(identity, captured, prepared).catch((error) => { + this.entries.delete(key) + throw error + }) + entry = { endpoint: captured, pending } + this.entries.set(key, entry) + return waitForPromiseWithSignal(entry.pending, signal) + } + const connection = entry.pending + return waitForPromiseWithSignal( + prepared + ? connection.then( + async (connected) => { + await connected.establishLease() + return connected + }, + () => { + this.lifetime.signal.throwIfAborted() + return this.connect(owner, endpoint, undefined, prepared) + } + ) + : connection, + signal + ) + } + + private async open( + owner: Readonly, + endpoint: Readonly, + prepared?: PreparedWslDaemonEndpoint + ): Promise { + const signal = this.lifetime.signal + const protocolVersion = retainedWslDaemonProtocolVersion(endpoint) + const shell = await readGuestShell(endpoint, signal) + signal.throwIfAborted() + const historyId = createHash('sha256') + .update(JSON.stringify([this.options.profileScope, wslPtyOwnerKey(owner)])) + .digest('hex') + const admission = new WslDaemonOwnerAdmission( + this.options.store, + owner, + endpoint, + signal, + prepared + ) + const adapter = new DaemonPtyAdapter({ + respawn: admission.recover, + protocolVersion, + profileScope: this.options.profileScope, + historyPath: join(this.options.historyRoot, historyId), + guest: { + distro: owner.distro, + admitIdentity: admission.admitIdentity, + transport: createWslDaemonTransport(endpoint), + defaultShell: shell, + defaultCwd: endpoint.home, + profiles: [{ name: shell.slice(shell.lastIndexOf('/') + 1), path: shell }] + } + }) + const provider = new WslDaemonPtyProvider(owner, adapter) + const establishLease = () => admission.establishLease(() => provider.establishLifecycleLease()) + let unregister: (() => void) | undefined + try { + await establishLease() + signal.throwIfAborted() + unregister = registerWslPtyProvider(owner, provider) + const entry = this.entries.get(wslPtyOwnerKey(owner)) + if (!entry) { + throw new Error('WSL daemon session disappeared before publication') + } + entry.close = async () => { + unregister?.() + await provider.disconnectOnly() + } + return Object.freeze({ owner, endpoint, provider, establishLease }) + } catch (error) { + unregister?.() + await provider.disconnectOnly() + throw error + } + } + + dispose(): Promise { + if (!this.closing) { + this.lifetime.abort(new Error('WSL daemon profile was disposed')) + const entries = [...this.entries.values()] + const closing = entries.map(async (entry) => entry.close?.()) + this.closing = (async () => { + await Promise.all([ + Promise.allSettled([ + ...this.preparing.values(), + ...entries.map((entry) => entry.pending) + ]), + Promise.all(closing) + ]) + this.entries.clear() + this.preparing.clear() + })() + } + return this.closing + } +} + +async function readGuestShell( + endpoint: PersistedWslDaemonEndpoint, + signal: AbortSignal +): Promise { + if ((await readWslDistributionIdentity(endpoint.distro)) !== endpoint.distributionId) { + throw new Error('WSL distribution was replaced; the terminal owner is unverifiable') + } + signal.throwIfAborted() + const runner = createRunningWslRuntimeRunner(endpoint.distro, signal, endpoint.userName) + const script = + 'const e=JSON.parse(process.argv[1]);if(String(process.getuid())!==e.userId||process.env.HOME!==e.home)throw new Error("WSL terminal owner changed");const s=process.env.SHELL;if(!s||!s.startsWith("/")||/[\\0\\r\\n]/.test(s))throw new Error("WSL login shell unavailable");console.log(s)' + return runner.run({ + program: endpoint.envBinary, + args: [ + ...[ + 'NODE_OPTIONS', + 'NODE_PATH', + 'BUN_OPTIONS', + 'BUN_INSPECT', + 'ELECTRON_RUN_AS_NODE' + ].flatMap((key) => ['-u', key]), + endpoint.runtime, + ...bunOwnedRuntimeArgs('linux'), + '-e', + script, + JSON.stringify({ userId: endpoint.userId, home: endpoint.home }) + ], + loginPath: 'preferred' + }) +} diff --git a/src/main/wsl/wsl-daemon-spawn-route.ts b/src/main/wsl/wsl-daemon-spawn-route.ts new file mode 100644 index 000000000000..9101cbff6b47 --- /dev/null +++ b/src/main/wsl/wsl-daemon-spawn-route.ts @@ -0,0 +1,64 @@ +import { parseAppWslPtyId } from '../../shared/wsl-pty-id' +import type { WslAccountExecutionContext } from './wsl-account-execution-context' +import type { WslDaemonConnection, WslDaemonSessions } from './wsl-daemon-sessions' +import type { PreparedWslGuestSpawnOwner } from './wsl-guest-spawn-options' + +export type WslDaemonSpawnRoute = Readonly<{ + connection: WslDaemonConnection + execution: WslAccountExecutionContext + prepared: PreparedWslGuestSpawnOwner + fresh: boolean + coldRestore?: boolean +}> + +/** Existing unencoded IDs retain their Windows owner; only fresh WSL terminals change backend. */ +export async function prepareWslDaemonSpawnRoute(args: { + sessions?: Pick + connectionId?: string | null + sessionId?: string + isNewSession?: boolean + distro?: string | null + signal?: AbortSignal + platform?: NodeJS.Platform +}): Promise { + if (args.connectionId) { + return null + } + const owner = args.sessionId ? parseAppWslPtyId(args.sessionId) : null + if (args.sessionId?.startsWith('wsl:') && !owner) { + throw new Error('Invalid WSL terminal identity') + } + if (owner) { + if (!args.sessions || (args.platform ?? process.platform) !== 'win32') { + throw new Error('WSL terminal owner is unavailable on this host') + } + const connection = await args.sessions.reconnect(owner, args.signal) + const live = await connection.provider.probePtyLiveness(args.sessionId!) + args.signal?.throwIfAborted() + if (live === null) { + throw new Error('WSL terminal liveness is unverifiable; refusing cold restore') + } + return Object.freeze({ + connection, + prepared: Object.freeze({ owner: connection.owner, endpoint: connection.endpoint }), + execution: Object.freeze({ + distro: connection.endpoint.distro, + userName: connection.endpoint.userName, + userId: connection.endpoint.userId, + home: connection.endpoint.home + }), + fresh: false, + coldRestore: live === false + }) + } + if ( + (args.sessionId && args.isNewSession !== true) || + !args.sessions || + !args.distro || + (args.platform ?? process.platform) !== 'win32' + ) { + return null + } + const fresh = await args.sessions.prepareFresh(args.distro, args.signal) + return Object.freeze({ ...fresh, fresh: true }) +} diff --git a/src/main/wsl/wsl-daemon-start-script.test.ts b/src/main/wsl/wsl-daemon-start-script.test.ts new file mode 100644 index 000000000000..42e833fd2a59 --- /dev/null +++ b/src/main/wsl/wsl-daemon-start-script.test.ts @@ -0,0 +1,114 @@ +import { createHash } from 'node:crypto' +import { EventEmitter } from 'node:events' +import { runInNewContext } from 'node:vm' +import { expect, it, vi } from 'vitest' +import { WSL_DAEMON_START_SCRIPT } from './wsl-daemon-start-script' + +const hash = (value: string) => createHash('sha256').update(value).digest('hex') +const serverBuildId = hash( + JSON.stringify({ + runtime: hash('runtime'), + files: [{ name: 'daemon-entry.js', sha256: hash('entry') }] + }) +) + +it.each(['live', 'EACCES', 'timeout', 'ENOENT', 'ECONNREFUSED', 'changed-artifact'])( + 'launches only after verified absent %s contact', + async (contact) => { + const absent = contact === 'ENOENT' || contact === 'ECONNREFUSED' + const spawn = vi.fn(() => ({ unref: vi.fn() })) + let contacts = 0 + const processState = { + argv: [ + 'bun', + JSON.stringify({ + serverBuildId, + userId: '1000', + home: '/home/u', + runtime: '/bun', + entry: '/daemon.js', + socket: '/s', + tokenPath: '/t' + }) + ], + env: { HOME: '/home/u' }, + getuid: () => 1000, + exitCode: 0 + } + const require = (module: string) => { + if (module === 'node:crypto') { + return { createHash } + } + if (module === 'node:fs') { + return { + lstatSync: () => ({ + isFile: () => true, + isSymbolicLink: () => false, + uid: 1000, + mode: 0o700 + }), + readFileSync: (file: string) => + file === '/bun' ? 'runtime' : contact === 'changed-artifact' ? 'changed' : 'entry' + } + } + return { + createConnection: () => { + const outcome = + contacts++ > 0 ? 'live' : contact === 'changed-artifact' ? 'ENOENT' : contact + const socket = new EventEmitter() + Object.assign(socket, { setTimeout: vi.fn(), destroy: vi.fn() }) + queueMicrotask(() => { + if (outcome === 'live') { + socket.emit('connect') + } else if (outcome === 'timeout') { + socket.emit('timeout') + } else { + socket.emit('error', Object.assign(new Error(outcome), { code: outcome })) + } + }) + return socket + } + } + } + await runInNewContext(WSL_DAEMON_START_SCRIPT, { + require, + process: processState, + Bun: { spawn }, + console: { log: vi.fn(), error: vi.fn() }, + setTimeout + }) + expect(spawn).toHaveBeenCalledTimes(absent ? 1 : 0) + if (absent) { + expect(spawn).toHaveBeenCalledWith( + [ + '/bun', + '--no-env-file', + '--config=/dev/null', + '--no-install', + '/daemon.js', + '--socket', + '/s', + '--token', + '/t' + ], + expect.objectContaining({ detached: true }) + ) + } + expect(processState.exitCode).toBe(contact === 'live' || absent ? 0 : 1) + } +) + +it('rejects a changed user before touching the endpoint', () => { + const require = vi.fn() + expect(() => + runInNewContext(WSL_DAEMON_START_SCRIPT, { + require, + process: { + argv: ['bun', JSON.stringify({ userId: '1000', home: '/home/u' })], + getuid: () => 1001, + env: { HOME: '/home/u' } + } + }) + ).toThrow('owner changed') + expect(require).toHaveBeenCalledTimes(1) +}) diff --git a/src/main/wsl/wsl-daemon-start-script.ts b/src/main/wsl/wsl-daemon-start-script.ts new file mode 100644 index 000000000000..b90cda541107 --- /dev/null +++ b/src/main/wsl/wsl-daemon-start-script.ts @@ -0,0 +1,45 @@ +import { bunOwnedRuntimeArgs } from '../../shared/bun-owned-runtime-args' + +/** Only confirmed absence permits startup; daemon endpoint arbitration protects racing launches. */ +export const WSL_DAEMON_START_SCRIPT = String.raw` +const net=require('node:net'); +const plan=JSON.parse(process.argv[1]); +if (String(process.getuid()) !== plan.userId || process.env.HOME !== plan.home) throw Error('WSL daemon execution owner changed'); +function contact() { + return new Promise((resolve,reject)=>{ + const socket=net.createConnection(plan.socket); + socket.setTimeout(2000); + socket.once('connect',()=>{socket.destroy();resolve('live');}); + socket.once('timeout',()=>{socket.destroy();reject(Error('Guest owner contact is unverifiable'));}); + socket.once('error',error=>{ + if (['ENOENT','ECONNREFUSED'].includes(error.code)) resolve('absent'); + else reject(Error('Guest owner contact is unverifiable: '+error.code)); + }); + }); +} +(async()=>{ + if(await contact()==='live'){console.log('existing');return;} + const fs=require('node:fs'); + const {createHash}=require('node:crypto'); + const hash=bytes=>createHash('sha256').update(bytes).digest('hex'); + const readArtifact=file=>{ + const stat=fs.lstatSync(file); + if(!stat.isFile() || stat.isSymbolicLink() || stat.uid!==process.getuid() || (stat.mode & 0o022)) throw Error('Guest daemon artifact ownership changed'); + return fs.readFileSync(file); + }; + const artifactId=hash(JSON.stringify({runtime:hash(readArtifact(plan.runtime)),files:[{name:'daemon-entry.js',sha256:hash(readArtifact(plan.entry))}]})); + if(artifactId!==plan.serverBuildId) throw Error('Retained guest daemon artifacts changed; refusing restart'); + const env={...process.env,HOME:plan.home,PATH:plan.path??process.env.PATH,ORCA_BACKGROUND_LAUNCH:'1'}; + for(const key of ['NODE_OPTIONS','NODE_PATH','BUN_OPTIONS','BUN_INSPECT','ELECTRON_RUN_AS_NODE']) delete env[key]; + const child=Bun.spawn([plan.runtime,...${JSON.stringify(bunOwnedRuntimeArgs('linux'))},plan.entry,'--socket',plan.socket,'--token',plan.tokenPath],{ + detached:true,stdin:'ignore',stdout:'ignore',stderr:'ignore',cwd:plan.home,env + }); + child.unref(); + const deadline=Date.now()+10000; + while(Date.now()setTimeout(resolve,50)); + } + throw Error('Guest daemon did not publish its endpoint'); +})().catch(error=>{console.error(error.message);process.exitCode=1;}); +` diff --git a/src/main/wsl/wsl-daemon-transport.test.ts b/src/main/wsl/wsl-daemon-transport.test.ts new file mode 100644 index 000000000000..6fb71a306dc6 --- /dev/null +++ b/src/main/wsl/wsl-daemon-transport.test.ts @@ -0,0 +1,100 @@ +import { beforeEach, expect, it, vi } from 'vitest' +import { PassThrough } from 'node:stream' +import { createWslDaemonTransport } from './wsl-daemon-transport' +import { runProcess } from '../../shared/child-process/run-process' +import { assertWslRuntimeDistroRunning } from './wsl-bun-runtime' +import { readWslDistributionIdentity } from './wsl-distribution-identity' +import { openWslDaemonConnectorStream } from './wsl-daemon-connector-stream' + +vi.mock('../../shared/child-process/run-process', () => ({ runProcess: vi.fn() })) +vi.mock('./wsl-bun-runtime', () => ({ assertWslRuntimeDistroRunning: vi.fn() })) +vi.mock('./wsl-distribution-identity', () => ({ readWslDistributionIdentity: vi.fn() })) +vi.mock('./wsl-executable-path', () => ({ + resolveWslExecutablePath: () => 'C:\\Windows\\System32\\wsl.exe' +})) +vi.mock('../wsl-interop-spawn-directory', () => ({ + resolveWslInteropSpawnCwd: () => 'C:\\Windows' +})) +vi.mock('./wsl-daemon-connector-stream', () => ({ openWslDaemonConnectorStream: vi.fn() })) +const endpoint = { + distro: 'Ubuntu', + distributionId: 'registered-owner', + userName: 'alice', + userId: '1001', + home: '/home/alice', + envBinary: '/usr/bin/env', + runtime: '/home/alice/runtime/bun', + socket: '/home/alice/owner/d.sock', + tokenPath: '/home/alice/owner/token' +} +const operation = () => ({ timeoutMs: 5000, signal: new AbortController().signal }) +beforeEach(() => { + vi.resetAllMocks() + vi.mocked(readWslDistributionIdentity).mockResolvedValue(endpoint.distributionId) + vi.mocked(runProcess).mockResolvedValue({ + code: 0, + signal: null, + stdout: 'token', + stderr: '', + timedOut: false + }) + vi.mocked(openWslDaemonConnectorStream).mockImplementation(async () => new PassThrough()) +}) + +it('pins both channels and token reads to the captured guest user without shell interpretation', async () => { + const mutable = { ...endpoint } + const transport = createWslDaemonTransport(mutable) + mutable.userName = 'bob' + expect(await transport.readToken(operation())).toBe('token') + const control = await transport.connect('control', operation()) + const stream = await transport.connect('stream', operation()) + const specs = [ + vi.mocked(runProcess).mock.calls[0][0], + ...vi.mocked(openWslDaemonConnectorStream).mock.calls.map(([spec]) => spec) + ] + for (const spec of specs) { + expect(spec.args).toEqual(expect.arrayContaining(['-d', 'Ubuntu', '-u', 'alice', '--exec'])) + expect(spec.args).toEqual( + expect.arrayContaining(['--no-env-file', '--config=/dev/null', '--no-install']) + ) + expect(spec.args).toContain(JSON.stringify(endpoint)) + expect(spec.args).not.toContain('--') + } + expect(assertWslRuntimeDistroRunning).toHaveBeenCalledTimes(3) + control.destroy() + stream.destroy() +}) + +it('refuses a re-registered or stopped distro before spawning a connector or reading a token', async () => { + const transport = createWslDaemonTransport(endpoint) + vi.mocked(readWslDistributionIdentity).mockResolvedValue('replacement') + await expect(transport.connect('control', operation())).rejects.toThrow('unverifiable') + await expect(transport.readToken(operation())).rejects.toThrow('unverifiable') + vi.mocked(assertWslRuntimeDistroRunning).mockRejectedValue(new Error('not running')) + await expect(transport.connect('stream', operation())).rejects.toThrow('not running') + expect(openWslDaemonConnectorStream).not.toHaveBeenCalled() + expect(runProcess).not.toHaveBeenCalled() +}) + +it('does not include arbitrary guest stderr in token failure messages', async () => { + vi.mocked(runProcess).mockResolvedValue({ + code: 1, + signal: null, + stdout: '', + stderr: 'private credential content', + timedOut: false + }) + await expect(createWslDaemonTransport(endpoint).readToken(operation())).rejects.toThrow( + 'authentication token is unavailable' + ) +}) + +it('refuses malformed guest identity before any execution', () => { + expect(() => createWslDaemonTransport({ ...endpoint, tokenPath: 'C:\\host-token' })).toThrow( + 'absolute guest paths' + ) + expect(() => createWslDaemonTransport({ ...endpoint, userId: '' })).toThrow( + 'captured execution owner' + ) + expect(runProcess).not.toHaveBeenCalled() +}) diff --git a/src/main/wsl/wsl-daemon-transport.ts b/src/main/wsl/wsl-daemon-transport.ts new file mode 100644 index 000000000000..459ba1fe55f3 --- /dev/null +++ b/src/main/wsl/wsl-daemon-transport.ts @@ -0,0 +1,111 @@ +import { bunOwnedRuntimeArgs } from '../../shared/bun-owned-runtime-args' +import type { + DaemonClientTransport, + DaemonTransportOperation +} from '../daemon/daemon-client-transport' +import { runProcess, type ProcessSpec } from '../../shared/child-process/run-process' +import { buildWslExecArgs } from '../../shared/wsl-login-shell-command' +import { waitForPromiseWithSignal } from '../../shared/abort-signal-reason' +import { + assertWslAccountExecutionTarget, + type WslAccountExecutionContext +} from './wsl-account-execution-context' +import { assertWslRuntimeDistroRunning } from './wsl-bun-runtime' +import { readWslDistributionIdentity } from './wsl-distribution-identity' +import { resolveWslExecutablePath } from './wsl-executable-path' +import { resolveWslInteropSpawnCwd } from '../wsl-interop-spawn-directory' +import { openWslDaemonConnectorStream } from './wsl-daemon-connector-stream' +import { + WSL_DAEMON_CONNECTOR_SCRIPT, + WSL_DAEMON_READ_TOKEN_SCRIPT +} from './wsl-daemon-connector-script' + +export type WslDaemonEndpoint = WslAccountExecutionContext & + Readonly<{ + distributionId: string + envBinary: string + runtime: string + socket: string + tokenPath: string + }> + +/** Guest paths and process identities never pass through the Windows filesystem/process probes. */ +export function createWslDaemonTransport(value: WslDaemonEndpoint): DaemonClientTransport { + const endpoint = Object.freeze({ ...value }) + assertWslAccountExecutionTarget(endpoint, { runtime: 'wsl', wslDistro: endpoint.distro }) + if (!endpoint.distributionId || /[\0\r\n]/.test(endpoint.distro)) { + throw new Error('Invalid WSL daemon identity') + } + for (const path of [ + endpoint.home, + endpoint.envBinary, + endpoint.runtime, + endpoint.socket, + endpoint.tokenPath + ]) { + if (!path.startsWith('/') || /[\0\r\n]/.test(path)) { + throw new Error('WSL daemon requires absolute guest paths') + } + } + const verifyOwner = async ({ signal }: DaemonTransportOperation) => { + await assertWslRuntimeDistroRunning(endpoint.distro, signal) + const registration = await waitForPromiseWithSignal( + readWslDistributionIdentity(endpoint.distro), + signal + ) + if (registration !== endpoint.distributionId) { + throw new Error('WSL distribution was replaced; the terminal owner is unverifiable') + } + signal.throwIfAborted() + } + const command = (script: string): ProcessSpec => ({ + program: resolveWslExecutablePath(), + args: buildWslExecArgs( + endpoint.distro, + [ + endpoint.envBinary, + ...[ + 'NODE_OPTIONS', + 'NODE_PATH', + 'BUN_OPTIONS', + 'BUN_INSPECT', + 'ELECTRON_RUN_AS_NODE' + ].flatMap((key) => ['-u', key]), + endpoint.runtime, + ...bunOwnedRuntimeArgs('linux'), + '-e', + script, + JSON.stringify(endpoint) + ], + endpoint.userName + ), + cwd: resolveWslInteropSpawnCwd() + }) + return { + async readToken(operation) { + await verifyOwner(operation) + const result = await runProcess({ + ...command(WSL_DAEMON_READ_TOKEN_SCRIPT), + signal: operation.signal, + timeoutMs: operation.timeoutMs, + maxOutputBytes: 4096 + }) + operation.signal.throwIfAborted() + const token = result.stdout.trim() + if ( + result.code !== 0 || + result.timedOut || + result.outputTruncated || + !token || + /[\0\r\n]/.test(token) + ) { + throw new Error('WSL daemon authentication token is unavailable') + } + return token + }, + async connect(_role, operation) { + await verifyOwner(operation) + return openWslDaemonConnectorStream(command(WSL_DAEMON_CONNECTOR_SCRIPT), operation.signal) + } + } +} diff --git a/src/main/wsl/wsl-distribution-identity.test.ts b/src/main/wsl/wsl-distribution-identity.test.ts new file mode 100644 index 000000000000..dffe339c5f64 --- /dev/null +++ b/src/main/wsl/wsl-distribution-identity.test.ts @@ -0,0 +1,24 @@ +import { expect, it } from 'vitest' +import { parseWslDistributionIdentity } from './wsl-distribution-identity' +const key = 'HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Lxss' +const guid = '{11111111-2222-3333-4444-555555555555}' +it('reads exact distro registration and refuses ambiguous or absent registrations', () => { + const row = `${key}\\${guid}\r\n DistributionName REG_SZ Ubuntu-24.04\r\n` + expect(parseWslDistributionIdentity(row, 'Ubuntu-24.04')).toBe(guid) + expect(parseWslDistributionIdentity(row, 'ubuntu-24.04')).toBe(guid) + expect(parseWslDistributionIdentity(row, 'Ubuntu')).toBeNull() + expect( + parseWslDistributionIdentity( + row + row.replace(guid, '{aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee}'), + 'Ubuntu-24.04' + ) + ).toBeNull() +}) +it('does not accept a same-named registry value outside the distribution key', () => { + expect( + parseWslDistributionIdentity( + `HKEY_CURRENT_USER\\Elsewhere\\${guid}\n DistributionName REG_SZ Ubuntu`, + 'Ubuntu' + ) + ).toBeNull() +}) diff --git a/src/main/wsl/wsl-distribution-identity.ts b/src/main/wsl/wsl-distribution-identity.ts new file mode 100644 index 000000000000..a1aa33f0abf1 --- /dev/null +++ b/src/main/wsl/wsl-distribution-identity.ts @@ -0,0 +1,47 @@ +import { join } from 'node:path' +import { runProcess } from '../../shared/child-process/run-process' + +const DISTRIBUTIONS_KEY = 'HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Lxss' + +/** The registration GUID changes when a distro is removed and recreated under the same name. */ +export function parseWslDistributionIdentity(output: string, distro: string): string | null { + let identity: string | null = null + const matches = new Set() + for (const line of output.split(/\r?\n/)) { + const key = line.trim() + if (key.startsWith('HKEY_')) { + const suffix = key.slice(DISTRIBUTIONS_KEY.length) + identity = + key.toLowerCase().startsWith(DISTRIBUTIONS_KEY.toLowerCase()) && + /^\\\{[0-9a-f-]{36}\}$/i.test(suffix) + ? suffix.slice(1).toLowerCase() + : null + continue + } + const value = /^\s*DistributionName\s+REG_SZ\s+(.+?)\s*$/.exec(line) + if (identity && value?.[1]?.toLowerCase() === distro.toLowerCase()) { + matches.add(identity) + } + } + return matches.size === 1 ? [...matches][0] : null +} + +export async function readWslDistributionIdentity(distro: string): Promise { + if (process.platform !== 'win32' || !process.env.SystemRoot) { + throw new Error('WSL distribution identity is unavailable on this host') + } + const result = await runProcess({ + program: join(process.env.SystemRoot, 'System32', 'reg.exe'), + args: ['query', DISTRIBUTIONS_KEY, '/s', '/v', 'DistributionName'], + timeoutMs: 5000, + maxOutputBytes: 256 * 1024 + }) + const identity = + result.code === 0 && !result.timedOut + ? parseWslDistributionIdentity(result.stdout, distro) + : null + if (!identity) { + throw new Error('WSL distribution registration could not be verified') + } + return identity +} diff --git a/src/main/wsl/wsl-guest-artifact-install-script.ts b/src/main/wsl/wsl-guest-artifact-install-script.ts new file mode 100644 index 000000000000..2845c8d0ffc4 --- /dev/null +++ b/src/main/wsl/wsl-guest-artifact-install-script.ts @@ -0,0 +1,57 @@ +/** Runs under the already verified guest runtime; argv carries data without shell interpolation. */ +export const WSL_GUEST_ARTIFACT_INSTALL_SCRIPT = String.raw` +const fs = require('node:fs'); +const path = require('node:path'); +const {createHash, randomUUID} = require('node:crypto'); +const plan = JSON.parse(process.argv[1]); +if (plan.userId !== undefined && (String(process.getuid()) !== plan.userId || process.env.HOME !== plan.home)) throw Error('WSL artifact execution owner changed'); +function privateDirectory(directory) { + if (!path.isAbsolute(directory) || path.resolve(directory)!==directory) throw Error('Invalid guest terminal directory'); + let ancestor=path.parse(directory).root; + for(const segment of directory.slice(ancestor.length).split(path.sep).filter(Boolean)) { + ancestor=path.join(ancestor,segment); + try {fs.mkdirSync(ancestor,{mode:0o700});} catch(error) {if(error.code!=='EEXIST')throw error;} + const checked=fs.lstatSync(ancestor); + if(!checked.isDirectory() || checked.isSymbolicLink() || ![0,process.getuid()].includes(checked.uid)) throw Error('Unowned guest terminal ancestor'); + if((checked.mode & 0o022) && !(checked.uid===0 && (checked.mode & 0o1000))) throw Error('Writable guest terminal ancestor'); + } + const stat=fs.lstatSync(directory); + if (!stat.isDirectory() || stat.isSymbolicLink() || stat.uid !== process.getuid()) throw Error('Unowned guest terminal directory'); + fs.chmodSync(directory,0o700); +} +function matches(directory) { + try { + const directoryStat=fs.lstatSync(directory); + if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink() || directoryStat.uid!==process.getuid()) return false; + return plan.files.every(file=> { + const entry=path.join(directory,file.name); + const stat=fs.lstatSync(entry); + return stat.isFile() && !stat.isSymbolicLink() && (!file.executable || (stat.mode & 0o100)!==0) && createHash('sha256').update(fs.readFileSync(entry)).digest('hex')===file.sha256; + }); + } catch { return false; } +} +privateDirectory(plan.artifactRoot); +privateDirectory(plan.ownerDirectory); +if (fs.existsSync(plan.directory)) { + if (!matches(plan.directory)) throw Error('Existing guest terminal artifacts changed; refusing to replace a possible live owner'); +} else { + const stage=path.join(plan.artifactRoot,'.stage-'+randomUUID()); + privateDirectory(stage); + try { + for (const file of plan.files) { + const destination=path.join(stage,file.name); + fs.mkdirSync(path.dirname(destination),{recursive:true,mode:0o700}); + fs.copyFileSync(file.source || path.join(plan.source,file.sourceName || file.name),destination,fs.constants.COPYFILE_EXCL); + fs.chmodSync(destination,file.executable ? 0o700 : 0o600); + const fd=fs.openSync(destination,'r'); + try { fs.fsyncSync(fd); } finally { fs.closeSync(fd); } + } + if (!matches(stage)) throw Error('Guest terminal artifact checksum mismatch'); + try { fs.renameSync(stage,plan.directory); } + catch(error) { if (!['EEXIST','ENOTEMPTY'].includes(error.code) || !matches(plan.directory)) throw error; } + const fd=fs.openSync(plan.artifactRoot,'r'); + try { fs.fsyncSync(fd); } finally { fs.closeSync(fd); } + } finally { fs.rmSync(stage,{recursive:true,force:true}); } +} +console.log('ready'); +` diff --git a/src/main/wsl/wsl-guest-environment.ts b/src/main/wsl/wsl-guest-environment.ts index 4edfb862236e..15c6af95eb82 100644 --- a/src/main/wsl/wsl-guest-environment.ts +++ b/src/main/wsl/wsl-guest-environment.ts @@ -74,7 +74,8 @@ function parseProbePayload(payload: string | null): WslGuestEnvironment | null { async function probeGuestEnvironment( distro: string | undefined, - budgetMs: number + budgetMs: number, + user?: string ): Promise { // Resolve `env` rather than assume /usr/bin/env: a distro that moved it would // otherwise fail every later call. @@ -86,7 +87,7 @@ async function probeGuestEnvironment( const captured = buildWslCapturedLoginShellCommand(script) const result = await runProcess({ program: resolveWslExecutablePath(), - args: buildWslExecArgs(distro, ['sh', '-c', captured.command]), + args: buildWslExecArgs(distro, ['sh', '-c', captured.command], user), // The runner sets this for every command it launches (#9010); the probe // spawns wsl.exe itself, so without it wsl.exe's own errors arrive UTF-16LE // and the NUL-separated payload below is read through NUL-riddled text. @@ -110,8 +111,8 @@ async function probeGuestEnvironment( return environment ? { kind: 'resolved', environment } : { kind: 'transient' } } -function cacheKey(distro: string | undefined): string { - return distro ?? '' +function cacheKey(distro: string | undefined, user?: string): string { + return `${distro ?? ''}${user ? `\0${user}` : ''}` } /** Null means "could not ask", never "has no PATH" -- callers fall back. */ @@ -122,9 +123,10 @@ export function getWslGuestEnvironment( * timer, so a 5s caller could reach `runProcess` with 1ms left and report a * timeout for a command that would have taken milliseconds. */ - budgetMs = PROBE_TIMEOUT_MS + budgetMs = PROBE_TIMEOUT_MS, + user?: string ): Promise { - const key = cacheKey(distro) + const key = cacheKey(distro, user) const cached = resolved.get(key) if (cached) { return Promise.resolve(cached) @@ -172,7 +174,7 @@ export function getWslGuestEnvironment( // host without System32\wsl.exe, EAGAIN under memory pressure). Uncaught, the // rejected promise stays in `inFlight` and every later call re-throws it for // the process lifetime -- all WSL features wedged until restart. - const probe = probeGuestEnvironment(distro, budgetMs) + const probe = probeGuestEnvironment(distro, budgetMs, user) .catch((): ProbeOutcome => ({ kind: 'transient' })) .then((outcome) => { if (inFlight.get(key) !== probe) { @@ -213,11 +215,14 @@ export function invalidateWslGuestEnvironment(distro?: string, all = false): voi probedWithBudget.clear() return } - const key = cacheKey(distro) - inFlight.delete(key) - resolved.delete(key) - retryAfter.delete(key) - probedWithBudget.delete(key) + const base = cacheKey(distro) + for (const cache of [inFlight, resolved, retryAfter, probedWithBudget]) { + for (const key of cache.keys()) { + if (key === base || key.startsWith(`${base}\0`)) { + cache.delete(key) + } + } + } } /** Test-only: the cached value without probing. */ diff --git a/src/main/wsl/wsl-guest-owner-preparation.ts b/src/main/wsl/wsl-guest-owner-preparation.ts new file mode 100644 index 000000000000..218787010084 --- /dev/null +++ b/src/main/wsl/wsl-guest-owner-preparation.ts @@ -0,0 +1,48 @@ +import { readWslDistributionIdentity } from './wsl-distribution-identity' +import { parseOrcadLinuxLibc } from '../ssh/orcad-deployment-target' +import { + assertWslRuntimeDistroRunning, + createRunningWslRuntimeRunner, + ensureWslBunRuntime +} from './wsl-bun-runtime' +import { getWslGuestEnvironment, invalidateWslGuestEnvironment } from './wsl-guest-environment' + +/** Capture the default user once; every later probe and install runs as that owner. */ +export async function prepareWslGuestOwner(distro: string, signal?: AbortSignal) { + const distributionId = await readWslDistributionIdentity(distro) + const initial = createRunningWslRuntimeRunner(distro, signal) + const userName = await initial.run({ program: 'id', args: ['-un'], loginPath: 'none' }) + if (!userName || /[\0\r\n]/.test(userName)) { + throw new Error('Invalid WSL guest user') + } + const execution = createRunningWslRuntimeRunner(distro, signal, userName) + const architecture = await execution.run({ program: 'uname', args: ['-m'], loginPath: 'none' }) + if (!['x86_64', 'aarch64', 'arm64'].includes(architecture)) { + throw new Error(`Unsupported WSL architecture: ${architecture}`) + } + const platform: 'linux-x64' | 'linux-arm64' = + architecture === 'x86_64' ? 'linux-x64' : 'linux-arm64' + const libc = parseOrcadLinuxLibc( + await execution.run({ + script: + 'getconf GNU_LIBC_VERSION 2>/dev/null || ldd --version 2>&1 || for loader in /lib/ld-musl-*.so.1; do [ ! -e "$loader" ] || { echo musl; break; }; done', + loginPath: 'none' + }) + ) + const runtime = await ensureWslBunRuntime(execution) + await assertWslRuntimeDistroRunning(distro, execution.signal) + invalidateWslGuestEnvironment(distro) + const environment = await getWslGuestEnvironment(distro, undefined, userName) + execution.signal.throwIfAborted() + if (!environment) { + throw new Error('WSL login environment is unavailable') + } + const userId = await execution.run({ program: 'id', args: ['-u'], loginPath: 'none' }) + if (!/^\d+$/.test(userId)) { + throw new Error('WSL guest user could not be verified') + } + if ((await readWslDistributionIdentity(distro)) !== distributionId) { + throw new Error('WSL distribution was replaced during owner preparation') + } + return { distributionId, userName, userId, platform, libc, runtime, environment, execution } +} diff --git a/src/main/wsl/wsl-guest-spawn-options.test.ts b/src/main/wsl/wsl-guest-spawn-options.test.ts new file mode 100644 index 000000000000..03a3cfc6325f --- /dev/null +++ b/src/main/wsl/wsl-guest-spawn-options.test.ts @@ -0,0 +1,309 @@ +import { runInNewContext } from 'node:vm' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { prepareWslGuestSpawnOptions as prepareOptions } from './wsl-guest-spawn-options' +import { runWslProcess } from './wsl-runner' +import { assertWslRuntimeDistroRunning, createRunningWslRuntimeRunner } from './wsl-bun-runtime' +import { toAppWslPtyId } from '../../shared/wsl-pty-id' +import type { PtySpawnOptions } from '../providers/types' +function prepareWslGuestSpawnOptions( + prepared: Parameters[0], + options: Partial +) { + return prepareOptions(prepared, { cols: 80, rows: 24, ...options }) +} + +vi.mock('./wsl-runner', () => ({ runWslProcess: vi.fn() })) +vi.mock('./wsl-bun-runtime', () => ({ + assertWslRuntimeDistroRunning: vi.fn(), + createRunningWslRuntimeRunner: vi.fn() +})) +vi.mock('../pty/wsl-orca-env', () => ({ addOrcaWslInteropEnv: vi.fn() })) +const prepared = { + endpoint: { + distro: 'Ubuntu', + userName: 'user', + userId: '1000', + runtime: '/runtime/bun', + entry: '/relay/relay.js', + socket: '/socket', + credentialFile: '/credential' + }, + owner: { distro: 'Ubuntu', relayBuildId: 'build+hash' }, + home: '/home/user', + path: '/usr/bin', + envBinary: '/usr/bin/env', + ripgrepPath: '/relay/rg' +} +const run = vi.fn() +beforeEach(() => { + vi.clearAllMocks() + vi.mocked(createRunningWslRuntimeRunner).mockReturnValue({ + run, + signal: new AbortController().signal + }) + run.mockResolvedValue('/mnt/c/work') + vi.mocked(runWslProcess).mockResolvedValue({ + code: 0, + stdout: JSON.stringify({ + PATH: '/usr/bin', + HOME: '/home/user', + SHELL: '/bin/bash', + ORCA_TOKEN: 'token' + }), + stderr: '', + timedOut: false, + environmentResolved: true + }) +}) + +describe('guest terminal spawn preparation', () => { + it('fences existing identities without probing or spawning', async () => { + const sessionId = toAppWslPtyId(prepared.owner, 'pty2:owner:1') + expect(await prepareWslGuestSpawnOptions(prepared, { sessionId })).toEqual({ + cols: 80, + rows: 24, + sessionId, + isNewSession: false + }) + await expect( + prepareWslGuestSpawnOptions(prepared, { sessionId: 'pty2:windows:1' }) + ).rejects.toThrow('WSL-owned') + await expect( + prepareWslGuestSpawnOptions(prepared, { + sessionId: toAppWslPtyId({ ...prepared.owner, distro: 'Debian' }, 'pty2:owner:1') + }) + ).rejects.toThrow('different distro') + expect(runWslProcess).not.toHaveBeenCalled() + }) + it('translates host cwd and replaces inherited host shell environment', async () => { + const result = await prepareWslGuestSpawnOptions(prepared, { + sessionId: 'desktop-new', + isNewSession: true, + cwd: 'C:\\work', + env: { + PATH: 'C:\\Windows', + HOME: 'C:\\Users\\user', + NODE_OPTIONS: '--require bad', + ORCA_TOKEN: 'token' + }, + shellOverride: 'wsl.exe' + }) + expect(result).toMatchObject({ + cwd: '/mnt/c/work', + env: { PATH: '/usr/bin', HOME: '/home/user', ORCA_TOKEN: 'token' } + }) + expect(result.sessionId).toBeUndefined() + expect(result.shellOverride).toBeUndefined() + expect(run).toHaveBeenCalledWith({ + program: 'wslpath', + args: ['-a', '-u', 'C:\\work'], + loginPath: 'none' + }) + const spec = vi.mocked(runWslProcess).mock.calls[0][0] + expect(spec.env).not.toHaveProperty('PATH') + expect(spec.env).not.toHaveProperty('NODE_OPTIONS') + expect(spec.args).toEqual( + expect.arrayContaining(['--no-env-file', '--config=/dev/null', '--no-install']) + ) + expect(spec.args).toContain('-u') + expect(assertWslRuntimeDistroRunning).toHaveBeenCalled() + }) + it('rejects foreign UNC cwd, reminted guest IDs and fresh attach-only requests', async () => { + await expect( + prepareWslGuestSpawnOptions(prepared, { cwd: '\\\\wsl.localhost\\Debian\\home' }) + ).rejects.toThrow('different WSL distro') + await expect( + prepareWslGuestSpawnOptions(prepared, { + sessionId: toAppWslPtyId(prepared.owner, 'pty2:owner:1'), + isNewSession: true + }) + ).rejects.toThrow('reminted') + await expect(prepareWslGuestSpawnOptions(prepared, { attachOnly: true })).rejects.toThrow( + 'cannot create' + ) + }) + it('applies deletions and refuses unavailable guest environment', async () => { + expect( + (await prepareWslGuestSpawnOptions(prepared, { envToDelete: ['ORCA_TOKEN'] })).env + ).not.toHaveProperty('ORCA_TOKEN') + vi.mocked(runWslProcess).mockResolvedValue({ + code: 0, + stdout: '{}', + stderr: '', + timedOut: false, + environmentResolved: false + }) + await expect(prepareWslGuestSpawnOptions(prepared, {})).rejects.toThrow('could not be resolved') + }) +}) + +it('refuses a changed login HOME before returning prepared options', async () => { + vi.mocked(runWslProcess).mockResolvedValue({ + code: 0, + stdout: JSON.stringify({ HOME: '/home/other' }), + stderr: '', + timedOut: false, + environmentResolved: true + }) + await expect(prepareWslGuestSpawnOptions(prepared, {})).rejects.toThrow('captured owner') +}) + +it('checks guest UID and HOME before the environment probe emits credentials', async () => { + await prepareWslGuestSpawnOptions(prepared, {}) + const args = vi.mocked(runWslProcess).mock.calls[0][0].args ?? [] + const scriptIndex = args.indexOf('-e') + 1 + const log = vi.fn() + for (const [uid, home] of [ + [1001, prepared.home], + [1000, '/home/other'] + ] as const) { + expect(() => + runInNewContext(args[scriptIndex], { + process: { + argv: ['bun', ...args.slice(scriptIndex + 1)], + getuid: () => uid, + env: { HOME: home, ORCA_TOKEN: 'private' } + }, + console: { log } + }) + ).toThrow('execution owner changed') + } + expect(log).not.toHaveBeenCalled() + runInNewContext(args[scriptIndex], { + process: { + argv: ['bun', ...args.slice(scriptIndex + 1)], + getuid: () => 1000, + env: { HOME: prepared.home } + }, + console: { log } + }) + expect(log).toHaveBeenCalledOnce() +}) + +const daemon = { + owner: prepared.owner, + endpoint: { + distro: 'Ubuntu', + distributionId: 'registration', + userName: 'user', + userId: '1000', + home: '/home/user', + runtime: '/runtime/bun', + entry: '/daemon/daemon.js', + envBinary: '/usr/bin/env', + socket: '/socket', + tokenPath: '/token', + serverBuildId: 'daemon-build' + } +} + +it('preserves a fresh stable daemon identity while legacy relay spawn still mints its own', async () => { + const options = { + sessionId: 'fresh-desktop', + isNewSession: true, + cwd: '/work', + env: { ORCA_PANE_KEY: 'pane' } + } + const guest = await prepareWslGuestSpawnOptions(daemon, options) + expect(guest.sessionId).toBe('fresh-desktop') + expect(guest.isNewSession).toBe(true) + expect(guest.shellOverride).toBe('/bin/bash') + expect(vi.mocked(runWslProcess).mock.calls[0][0]).toMatchObject({ + distro: 'Ubuntu', + user: 'user', + program: '/usr/bin/env' + }) + const relay = await prepareWslGuestSpawnOptions(prepared, options) + expect(relay.sessionId).toBeUndefined() + expect(relay.isNewSession).toBeUndefined() +}) + +it('keeps daemon attaches bound to the encoded owner without auth/environment preparation', async () => { + const id = toAppWslPtyId(daemon.owner, 'stable-session') + expect( + await prepareWslGuestSpawnOptions(daemon, { + sessionId: id, + isNewSession: false, + attachOnly: true + }) + ).toMatchObject({ sessionId: id, isNewSession: false, attachOnly: true }) + await expect( + prepareWslGuestSpawnOptions(daemon, { + sessionId: toAppWslPtyId({ ...daemon.owner, relayBuildId: 'other-user' }, 'stable-session'), + isNewSession: false + }) + ).rejects.toThrow('different distro or relay build') + expect(runWslProcess).not.toHaveBeenCalled() +}) + +it('injects guest-owned history after login environment resolution for a fresh daemon spawn', async () => { + run.mockResolvedValue('ready') + const options = await prepareWslGuestSpawnOptions(daemon, { + cwd: '/work', + sessionId: 'fresh', + isNewSession: true, + worktreeId: 'folder', + historyIsolationEnabled: true, + env: { ORCA_USER_DATA_PATH: 'C:\\Orca\\profile' } + }) + expect(options.env?.HISTFILE).toMatch( + /^\/home\/user\/.orca-wsl\/[a-f0-9]+\/terminal-history\/[a-f0-9]+\/bash_history$/ + ) + expect(options.env?.ORCA_HISTFILE).toBe(options.env?.HISTFILE) + expect(run).toHaveBeenCalledWith( + expect.objectContaining({ program: '/usr/bin/env', loginPath: 'none' }) + ) + expect(options.sessionId).toBe('fresh') +}) + +it('accepts UNC paths with registered casing for a canonical fresh owner', async () => { + const owner = { + ...prepared, + owner: { ...prepared.owner, distro: 'ubuntu' }, + endpoint: { ...prepared.endpoint, distro: 'ubuntu' } + } + const result = await prepareWslGuestSpawnOptions(owner, { + cwd: String.raw`\\wsl$\Ubuntu\home\user`, + env: { CODEX_HOME: String.raw`\\wsl$\Ubuntu\home\user\.codex` } + }) + expect(result.cwd).toBe('/home/user') + expect(runWslProcess).toHaveBeenCalledWith( + expect.objectContaining({ + env: expect.objectContaining({ CODEX_HOME: '/home/user/.codex' }) + }) + ) +}) + +it('translates a positively absent daemon session without reminting its ID or inheriting desktop paths', async () => { + run.mockResolvedValue('ready') + const id = toAppWslPtyId(daemon.owner, 'retained') + const result = await prepareOptions( + daemon, + { + cols: 80, + rows: 24, + sessionId: id, + isNewSession: false, + cwd: '\\\\wsl$\\Ubuntu\\work', + worktreeId: 'folder', + historyIsolationEnabled: true, + env: { PATH: 'C:\\bin', HOME: 'C:\\Users\\bob', ORCA_USER_DATA_PATH: 'C:\\profile' } + }, + undefined, + 'confirmed-exited' + ) + expect(result).toMatchObject({ + sessionId: id, + isNewSession: false, + cwd: '/work', + shellOverride: '/bin/bash', + env: { HOME: '/home/user', PATH: '/usr/bin' } + }) + expect(result.env?.HISTFILE).toContain('/home/user/.orca-wsl/') + expect(runWslProcess).toHaveBeenCalledWith( + expect.objectContaining({ + user: 'user', + env: expect.not.objectContaining({ PATH: 'C:\\bin', HOME: 'C:\\Users\\bob' }) + }) + ) +}) diff --git a/src/main/wsl/wsl-guest-spawn-options.ts b/src/main/wsl/wsl-guest-spawn-options.ts new file mode 100644 index 000000000000..d27c52e9c5ff --- /dev/null +++ b/src/main/wsl/wsl-guest-spawn-options.ts @@ -0,0 +1,213 @@ +import { bunOwnedRuntimeArgs } from '../../shared/bun-owned-runtime-args' +import { prepareWslGuestTerminalHistory } from './wsl-guest-terminal-history' +import { parseWslUncPath } from '../../shared/wsl-paths' +import { isWslShellName } from '../../shared/local-windows-terminal-runtime' +import { parseAppWslPtyId, toRelayWslPtyId } from '../../shared/wsl-pty-id' +import { addOrcaWslInteropEnv } from '../pty/wsl-orca-env' +import { isHostCodexHomeForWsl } from '../pty/codex-home-wsl-env' +import type { PtySpawnOptions } from '../providers/types' +import { createRunningWslRuntimeRunner, assertWslRuntimeDistroRunning } from './wsl-bun-runtime' +import { runWslProcess } from './wsl-runner' +import type { WslPtyOwner } from '../../shared/wsl-pty-id' + +export type PreparedWslGuestSpawnOwner = Readonly<{ + owner: Readonly + endpoint: Readonly<{ + distro: string + userName?: string + userId?: string + runtime: string + home?: string + envBinary?: string + tokenPath?: string + }> + home?: string + envBinary?: string +}> + +export function capturedWslGuestSpawnOwner(prepared: PreparedWslGuestSpawnOwner) { + const daemon = prepared.endpoint.tokenPath !== undefined + const home = prepared.endpoint.home ?? prepared.home ?? '' + const envBinary = prepared.endpoint.envBinary ?? prepared.envBinary ?? '' + if ( + prepared.endpoint.distro !== prepared.owner.distro || + !home.startsWith('/') || + !envBinary.startsWith('/') + ) { + throw new Error('Guest spawn requires captured distro, home and environment executable') + } + return { + owner: Object.freeze({ ...prepared.owner }), + endpoint: Object.freeze({ ...prepared.endpoint }), + home, + envBinary, + daemon + } +} +import { assertWslAccountExecutionTarget } from './wsl-account-execution-context' + +const READ_ENVIRONMENT = + 'const owner=JSON.parse(process.argv[2]);if(String(process.getuid())!==owner.userId||process.env.HOME!==owner.home)throw new Error("WSL terminal execution owner changed");const keys=JSON.parse(process.argv[1]);console.log(JSON.stringify(Object.fromEntries(keys.filter(k=>typeof process.env[k]==="string").map(k=>[k,process.env[k]]))))' +const RUNTIME_ENV_KEYS = [ + 'NODE_OPTIONS', + 'NODE_PATH', + 'BUN_OPTIONS', + 'BUN_INSPECT', + 'ELECTRON_RUN_AS_NODE' +] + +function parseEnvironment(value: string): Record { + const parsed: unknown = JSON.parse(value) + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { + throw new Error('Invalid guest terminal environment') + } + const result: Record = {} + for (const [key, entry] of Object.entries(parsed)) { + if (typeof entry !== 'string') { + throw new Error('Invalid guest terminal environment value') + } + result[key] = entry + } + return result +} + +/** Let WSL apply its own WSLENV/path rules before sending a guest-native spawn request. */ +export async function prepareWslGuestSpawnOptions( + captured: PreparedWslGuestSpawnOwner, + options: PtySpawnOptions, + signal?: AbortSignal, + mode?: 'confirmed-exited' +): Promise { + const prepared = capturedWslGuestSpawnOwner(captured) + const { owner } = prepared + const execution = createRunningWslRuntimeRunner(owner.distro, signal, prepared.endpoint.userName) + if ( + mode === 'confirmed-exited' && + (!prepared.daemon || !options.sessionId || options.isNewSession) + ) { + throw new Error('Cold restore requires an existing guest daemon terminal identity') + } + if (options.sessionId && !options.isNewSession) { + toRelayWslPtyId(owner, options.sessionId) + if (mode !== 'confirmed-exited') { + return { ...options, isNewSession: false } + } + } + if (options.attachOnly) { + throw new Error('A WSL attach cannot create a new terminal') + } + if (mode !== 'confirmed-exited' && options.sessionId && parseAppWslPtyId(options.sessionId)) { + throw new Error('A WSL terminal identity cannot be reminted as a fresh spawn') + } + const accountExecution = { + distro: owner.distro, + userName: prepared.endpoint.userName ?? '', + userId: prepared.endpoint.userId ?? '', + home: prepared.home + } + assertWslAccountExecutionTarget(accountExecution, { runtime: 'wsl', wslDistro: owner.distro }) + let cwd = options.cwd ?? prepared.home + const unc = parseWslUncPath(cwd) + if (unc) { + if (unc.distro.toLowerCase() !== owner.distro.toLowerCase()) { + throw new Error('Terminal cwd belongs to a different WSL distro') + } + cwd = unc.linuxPath + } else if (!cwd.startsWith('/')) { + cwd = await execution.run({ program: 'wslpath', args: ['-a', '-u', cwd], loginPath: 'none' }) + } + if (!cwd.startsWith('/')) { + throw new Error('Terminal cwd did not resolve to a guest path') + } + const env = { ...options.env } + addOrcaWslInteropEnv(env) + for (const key of ['PATH', 'Path', 'HOME', 'SHELL', ...RUNTIME_ENV_KEYS]) { + delete env[key] + } + if (env.CODEX_HOME) { + const codex = parseWslUncPath(env.CODEX_HOME) + if (codex?.distro.toLowerCase() === owner.distro.toLowerCase()) { + env.CODEX_HOME = codex.linuxPath + env.ORCA_CODEX_HOME = codex.linuxPath + } else if (codex || isHostCodexHomeForWsl(env.CODEX_HOME)) { + delete env.CODEX_HOME + delete env.ORCA_CODEX_HOME + } + } + const keys = [ + ...new Set([ + ...Object.keys(env), + 'PATH', + 'HOME', + 'SHELL', + 'HISTFILE', + 'fish_history', + 'XDG_DATA_HOME' + ]) + ].filter((key) => key !== 'WSLENV') + await assertWslRuntimeDistroRunning(owner.distro, execution.signal) + const result = await runWslProcess({ + distro: owner.distro, + user: prepared.endpoint.userName, + program: prepared.envBinary, + args: [ + ...RUNTIME_ENV_KEYS.flatMap((key) => ['-u', key]), + prepared.endpoint.runtime, + ...bunOwnedRuntimeArgs('linux'), + '-e', + READ_ENVIRONMENT, + JSON.stringify(keys), + JSON.stringify(accountExecution) + ], + env, + loginPath: 'preferred', + timeoutMs: 15_000, + maxOutputBytes: 256 * 1024 + }) + execution.signal.throwIfAborted() + if (result.code !== 0 || result.timedOut || !result.environmentResolved) { + throw new Error('WSL terminal environment could not be resolved') + } + const guestEnv = parseEnvironment(result.stdout) + if (guestEnv.HOME !== prepared.home) { + throw new Error('WSL terminal environment does not match its captured owner') + } + const guestShell = + options.shellOverride && !isWslShellName(options.shellOverride) + ? options.shellOverride + : guestEnv.SHELL || '/bin/bash' + if (prepared.daemon) { + await prepareWslGuestTerminalHistory( + execution, + accountExecution, + prepared.endpoint.runtime, + prepared.envBinary, + options, + guestEnv, + guestShell + ) + } + for (const key of options.envToDelete ?? []) { + delete guestEnv[key] + } + const { + sessionId: _freshDesktopId, + isNewSession: _fresh, + terminalWindowsWslDistro: _distro, + terminalWindowsPowerShellImplementation: _powershell, + codexHomePathOverride: _codex, + ...guestOptions + } = options + return { + ...guestOptions, + ...(prepared.daemon ? { sessionId: _freshDesktopId, isNewSession: _fresh } : {}), + cwd, + env: guestEnv, + shellOverride: + options.shellOverride && !isWslShellName(options.shellOverride) + ? options.shellOverride + : prepared.daemon + ? guestShell + : undefined + } +} diff --git a/src/main/wsl/wsl-guest-terminal-history-script.ts b/src/main/wsl/wsl-guest-terminal-history-script.ts new file mode 100644 index 000000000000..6d5f0049c16f --- /dev/null +++ b/src/main/wsl/wsl-guest-terminal-history-script.ts @@ -0,0 +1,36 @@ +/** Runs only under the captured guest user, before publishing the history environment. */ +export const WSL_GUEST_HISTORY_PREPARE_SCRIPT = String.raw` +const fs = require('node:fs'), path = require('node:path'); +const p = JSON.parse(process.argv[1]); +if (String(process.getuid()) !== p.owner.userId || process.env.HOME !== p.owner.home) + throw new Error('WSL history owner changed'); +const relative = path.relative(p.owner.home, p.directory); +if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) + throw new Error('WSL history path escaped its owner'); +let directory = p.owner.home; +for (const part of relative.split(path.sep)) { + directory = path.join(directory, part); + try { fs.mkdirSync(directory, { mode: 0o700 }); } + catch (error) { if (error.code !== 'EEXIST') throw error; } + const stat = fs.lstatSync(directory); + if (!stat.isDirectory() || stat.isSymbolicLink() || stat.uid !== process.getuid()) + throw new Error('WSL history directory is not owned by its captured user'); +} +const meta = path.join(directory, 'meta.json'); +let existing; +try { + const stat = fs.lstatSync(meta); + if (stat.isFile() && stat.size <= 32768) existing = JSON.parse(fs.readFileSync(meta, 'utf8')); +} catch {} +if (!existing || (p.fishSession && (existing.fishSession !== p.fishSession || existing.fishHistoryDir !== p.fishHistoryDir))) { + const temporary = path.join(directory, '.meta-' + process.pid + '-' + require('node:crypto').randomUUID()); + try { + fs.writeFileSync(temporary, JSON.stringify({ worktreeId: p.worktreeId, + createdAt: existing?.createdAt || new Date().toISOString(), + ...(p.fishSession ? { fishSession: p.fishSession, fishHistoryDir: p.fishHistoryDir } : {}) + }), { flag: 'wx', mode: 0o600 }); + fs.renameSync(temporary, meta); + } finally { try { fs.unlinkSync(temporary); } catch {} } +} +console.log('ready'); +` diff --git a/src/main/wsl/wsl-guest-terminal-history.test.ts b/src/main/wsl/wsl-guest-terminal-history.test.ts new file mode 100644 index 000000000000..c4cf1d43bd14 --- /dev/null +++ b/src/main/wsl/wsl-guest-terminal-history.test.ts @@ -0,0 +1,189 @@ +import { + mkdtempSync, + readFileSync, + existsSync, + statSync, + rmSync, + symlinkSync, + mkdirSync +} from 'node:fs' +import { tmpdir } from 'node:os' +import { join, posix } from 'node:path' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { runProcess } from '../../shared/child-process/run-process' +import { hashWorktreeId } from '../terminal-history-id' +import { prepareWslGuestTerminalHistory } from './wsl-guest-terminal-history' +import type { createRunningWslRuntimeRunner } from './wsl-bun-runtime' +import type { PtySpawnOptions } from '../providers/types' + +const directories: string[] = [] +afterEach(() => { + for (const path of directories.splice(0)) { + rmSync(path, { recursive: true, force: true }) + } +}) +function fixture() { + const home = mkdtempSync(join(tmpdir(), 'orca-guest-history-')) + directories.push(home) + const owner = { + distro: 'Ubuntu', + userName: 'captured', + userId: String(process.getuid?.() ?? 0), + home + } + const run = vi.fn['run']>(async (spec) => { + const args = spec.args ?? [] + expect(args).toEqual( + expect.arrayContaining(['--no-env-file', '--config=/dev/null', '--no-install']) + ) + const script = args.indexOf('-e') + const result = await runProcess({ + program: process.execPath, + args: args.slice(script), + env: { ...process.env, HOME: home } + }) + if (result.code !== 0) { + throw new Error(result.stderr) + } + return result.stdout.trim() + }) + const options: PtySpawnOptions = { + cols: 80, + rows: 24, + worktreeId: 'folder-workspace', + historyIsolationEnabled: true, + env: { ORCA_USER_DATA_PATH: 'C:\\Orca\\profile-a' } + } + const invoke = ( + env: Record, + shell: string, + override: Partial = {} + ) => + prepareWslGuestTerminalHistory( + { run }, + owner, + '/bun', + '/usr/bin/env', + { ...options, ...override }, + env, + shell + ) + return { home, owner, run, options, invoke } +} + +describe.skipIf(process.platform === 'win32')('guest history scripts on a real filesystem', () => { + it.each(['bash', 'zsh'])( + 'isolates %s history and writes owner-private metadata', + async (shell) => { + const { invoke, home, options } = fixture() + const env: Record = { HOME: home } + await invoke(env, `/bin/${shell}`) + const directory = posix.join( + home, + '.orca-wsl', + hashWorktreeId(options.env!.ORCA_USER_DATA_PATH), + 'terminal-history', + hashWorktreeId('folder-workspace') + ) + expect(env.HISTFILE).toBe(`${directory}/${shell}_history`) + expect(env.ORCA_HISTFILE).toBe(env.HISTFILE) + const meta = JSON.parse(readFileSync(join(directory, 'meta.json'), 'utf8')) + expect(meta.worktreeId).toBe('folder-workspace') + expect(statSync(directory).mode & 0o777).toBe(0o700) + expect(statSync(join(directory, 'meta.json')).mode & 0o777).toBe(0o600) + await invoke({}, `/bin/${shell}`) + expect(JSON.parse(readFileSync(join(directory, 'meta.json'), 'utf8')).createdAt).toBe( + meta.createdAt + ) + } + ) + + it('uses fish session names and its real guest XDG data directory', async () => { + const { invoke, home, options } = fixture() + const env: Record = { HOME: home, XDG_DATA_HOME: `${home}/custom-data` } + await invoke(env, '/usr/bin/fish') + expect(env.fish_history).toMatch(/^orca_relay_[0-9a-f]{16}$/) + expect(env).not.toHaveProperty('HISTFILE') + expect(env.XDG_DATA_HOME).toBe(`${home}/custom-data`) + const metaPath = join( + home, + '.orca-wsl', + hashWorktreeId(options.env!.ORCA_USER_DATA_PATH), + 'terminal-history', + hashWorktreeId('folder-workspace'), + 'meta.json' + ) + expect(JSON.parse(readFileSync(metaPath, 'utf8'))).toMatchObject({ + fishSession: env.fish_history, + fishHistoryDir: `${home}/custom-data/fish` + }) + }) + + it('keeps profiles and worktree/folder IDs separate', async () => { + const { invoke } = fixture() + const a: Record = {}, + b: Record = {}, + c: Record = {} + await invoke(a, '/bin/bash') + await invoke(b, '/bin/bash', { worktreeId: 'git-worktree' }) + await invoke(c, '/bin/bash', { env: { ORCA_USER_DATA_PATH: 'C:\\Orca\\profile-b' } }) + expect(new Set([a.HISTFILE, b.HISTFILE, c.HISTFILE]).size).toBe(3) + }) + + it('checks UID before creating any guest history directory', async () => { + const { run, owner, home, options } = fixture() + await expect( + prepareWslGuestTerminalHistory( + { run }, + { ...owner, userId: String(Number(owner.userId) + 1) }, + '/bun', + '/usr/bin/env', + options, + {}, + '/bin/bash' + ) + ).rejects.toThrow('owner changed') + expect(existsSync(join(home, '.orca-wsl'))).toBe(false) + }) + + it('refuses symlinked history roots instead of writing outside its owner tree', async () => { + const { invoke, home } = fixture() + const outside = join(home, 'outside') + mkdirSync(outside) + symlinkSync(outside, join(home, '.orca-wsl')) + await expect(invoke({}, '/bin/bash')).rejects.toThrow('not owned') + expect(existsSync(join(outside, 'terminal-history'))).toBe(false) + }) +}) + +it('leaves explicit user history untouched and skips disabled/unsupported cases', async () => { + const { invoke, run } = fixture() + const bash = { HISTFILE: '/custom/history', ORCA_HISTFILE: '/old/orca' } + await invoke(bash, '/bin/bash') + expect(bash).toEqual({ HISTFILE: '/custom/history' }) + const fish = { fish_history: 'my-session' } + await invoke(fish, '/bin/fish') + expect(fish).toEqual({ fish_history: 'my-session' }) + const off = { HISTFILE: '/inherited', ORCA_HISTFILE: '/unchanged' } + await invoke(off, '/bin/bash', { historyIsolationEnabled: false }) + expect(off.ORCA_HISTFILE).toBe('/unchanged') + await invoke({}, '/bin/bash', { worktreeId: undefined }) + await invoke({}, '/bin/sh') + expect(run).not.toHaveBeenCalled() +}) + +it.skipIf(process.platform === 'win32')( + 'replaces only inherited Orca-owned history settings', + async () => { + const { invoke } = fixture() + const env = { + HISTFILE: '/old/terminal-history/0123456789abcdef/bash_history', + ORCA_HISTFILE: '/old', + fish_history: 'orca_0123456789abcdef' + } + await invoke(env, '/bin/bash') + expect(env.HISTFILE).not.toContain('/old/') + expect(env.ORCA_HISTFILE).toBe(env.HISTFILE) + expect(env).not.toHaveProperty('fish_history') + } +) diff --git a/src/main/wsl/wsl-guest-terminal-history.ts b/src/main/wsl/wsl-guest-terminal-history.ts new file mode 100644 index 000000000000..fba26c318132 --- /dev/null +++ b/src/main/wsl/wsl-guest-terminal-history.ts @@ -0,0 +1,88 @@ +import { bunOwnedRuntimeArgs } from '../../shared/bun-owned-runtime-args' +import { posix } from 'node:path' +import { resolveShellKind } from '../terminal-history' +import { hashWorktreeId } from '../terminal-history-id' +import { dropInheritedOrcaFishHistory, relayFishHistorySessionName } from '../fish-history-session' +import { dropInheritedOrcaHistFile } from '../worktree-history-file-path' +import type { PtySpawnOptions } from '../providers/types' +import type { WslAccountExecutionContext } from './wsl-account-execution-context' +import type { createRunningWslRuntimeRunner } from './wsl-bun-runtime' +import { WSL_GUEST_HISTORY_PREPARE_SCRIPT } from './wsl-guest-terminal-history-script' + +/** Plans on the desktop; the captured guest alone creates its history files. */ +export async function prepareWslGuestTerminalHistory( + execution: Pick, 'run'>, + owner: WslAccountExecutionContext, + runtime: string, + envBinary: string, + options: PtySpawnOptions, + env: Record, + shell: string +): Promise { + if (!options.worktreeId || options.historyIsolationEnabled !== true) { + return + } + delete env.ORCA_HISTFILE + dropInheritedOrcaFishHistory(env) + dropInheritedOrcaHistFile(env) + const kind = resolveShellKind(shell) + if (kind !== 'bash' && kind !== 'zsh' && kind !== 'fish') { + return + } + if (kind === 'fish' ? env.fish_history : env.HISTFILE) { + return + } + const profile = options.env?.ORCA_USER_DATA_PATH + if (!profile) { + throw new Error('Guest history isolation requires the captured Orca profile') + } + const worktreeHash = hashWorktreeId(options.worktreeId) + const directory = posix.join( + owner.home, + '.orca-wsl', + hashWorktreeId(profile), + 'terminal-history', + worktreeHash + ) + // Guest desktops must not sweep history owned by an independently updating Windows client. + const fishSession = + kind === 'fish' + ? relayFishHistorySessionName(hashWorktreeId(JSON.stringify([profile, options.worktreeId]))) + : undefined + const fishHistoryDir = fishSession + ? posix.join( + env.XDG_DATA_HOME?.startsWith('/') + ? env.XDG_DATA_HOME + : posix.join(owner.home, '.local/share'), + 'fish' + ) + : undefined + const payload = { owner, directory, worktreeId: options.worktreeId, fishSession, fishHistoryDir } + const result = await execution.run({ + program: envBinary, + args: [ + ...[ + 'NODE_OPTIONS', + 'NODE_PATH', + 'BUN_OPTIONS', + 'BUN_INSPECT', + 'ELECTRON_RUN_AS_NODE' + ].flatMap((key) => ['-u', key]), + runtime, + ...bunOwnedRuntimeArgs('linux'), + '-e', + WSL_GUEST_HISTORY_PREPARE_SCRIPT, + JSON.stringify(payload) + ], + loginPath: 'none' + }) + if (result !== 'ready') { + throw new Error('Guest terminal history preparation failed') + } + if (fishSession) { + env.fish_history = fishSession + } else { + env.HISTFILE = posix.join(directory, `${kind}_history`) + env.ORCA_HISTFILE = env.HISTFILE + } +} diff --git a/src/main/wsl/wsl-guest-terminal-preparation.test.ts b/src/main/wsl/wsl-guest-terminal-preparation.test.ts new file mode 100644 index 000000000000..abbc149ae1f1 --- /dev/null +++ b/src/main/wsl/wsl-guest-terminal-preparation.test.ts @@ -0,0 +1,314 @@ +import type * as GuestSpawnOptions from './wsl-guest-spawn-options' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { prepareWslGuestTerminalSpawn } from './wsl-guest-terminal-preparation' +import { prepareWslGuestSpawnOptions } from './wsl-guest-spawn-options' +import { buildPtyHostEnv } from '../ipc/pty/host-env/assembly' +import { wslHookRelayManager } from '../agent-hooks/wsl-hook-relay-manager' +import { toAppWslPtyId } from '../../shared/wsl-pty-id' + +vi.mock('./wsl-guest-spawn-options', async (importOriginal) => ({ + ...(await importOriginal()), + prepareWslGuestSpawnOptions: vi.fn() +})) +vi.mock('../ipc/pty/host-env/assembly', () => ({ buildPtyHostEnv: vi.fn() })) +vi.mock('../agent-hooks/wsl-hook-relay-manager', () => ({ + wslHookRelayManager: { ensureForDistro: vi.fn(), getGuestEndpointFilePath: vi.fn() } +})) +const prepared = { + owner: { distro: 'Ubuntu', relayBuildId: 'build' }, + endpoint: { + distro: 'Ubuntu', + userName: 'alice', + runtime: '/bun', + entry: '/relay', + socket: '/socket', + credentialFile: '/credential' + }, + home: '/home/alice', + path: '/usr/bin', + envBinary: '/usr/bin/env', + ripgrepPath: '/rg' +} +const options = { + cols: 80, + rows: 24, + sessionId: 'fresh-desktop', + isNewSession: true, + env: { ORCA_PANE_KEY: 'pane' } +} +const policy = { + isPackaged: true, + userDataPath: 'C:\\Orca', + selectedCodexHomePath: '/home/alice/.codex', + agentStatusHooksEnabled: true +} +beforeEach(() => { + vi.clearAllMocks() + vi.mocked(wslHookRelayManager.ensureForDistro).mockResolvedValue() + vi.mocked(wslHookRelayManager.getGuestEndpointFilePath).mockReturnValue( + '/home/alice/.orca-wsl/agent-hooks/instance-a/endpoint' + ) + vi.mocked(buildPtyHostEnv).mockImplementation((_id, env) => ({ + ...env, + ORCA_WSL_CLI_DIR: 'C:\\cli' + })) + vi.mocked(prepareWslGuestSpawnOptions).mockImplementation(async (_owner, value) => value) +}) +describe('guest terminal environment preparation', () => { + it('awaits the exact owner and reuses host policy before guest translation', async () => { + const result = await prepareWslGuestTerminalSpawn(prepared, options, { + ...policy, + launchAgent: 'omp' + }) + expect(wslHookRelayManager.ensureForDistro).toHaveBeenCalledWith( + 'Ubuntu', + policy.selectedCodexHomePath, + 'omp', + 'alice' + ) + expect(buildPtyHostEnv).toHaveBeenCalledOnce() + expect(buildPtyHostEnv).toHaveBeenCalledWith( + 'fresh-desktop', + options.env, + expect.objectContaining({ isWsl: true, wslDistro: 'Ubuntu', wslUser: 'alice' }) + ) + expect(result.env).toMatchObject({ ORCA_PANE_KEY: 'pane', ORCA_WSL_CLI_DIR: 'C:\\cli' }) + }) + it('refuses foreign-home hook endpoints before building spawn state', async () => { + for (const endpoint of ['/home/bob/.orca-wsl/agent-hooks/instance-a/endpoint']) { + vi.mocked(wslHookRelayManager.getGuestEndpointFilePath).mockReturnValue(endpoint) + await expect(prepareWslGuestTerminalSpawn(prepared, options, policy)).rejects.toThrow( + 'terminal owner' + ) + } + expect(buildPtyHostEnv).not.toHaveBeenCalled() + expect(prepareWslGuestSpawnOptions).not.toHaveBeenCalled() + }) + it('does not reconfigure an attach or adopt a legacy Windows owner', async () => { + const sessionId = toAppWslPtyId(prepared.owner, 'pty2:owner:1') + await prepareWslGuestTerminalSpawn( + prepared, + { ...options, sessionId, isNewSession: false }, + policy + ) + await expect( + prepareWslGuestTerminalSpawn( + prepared, + { ...options, sessionId: 'pty2:windows:1', isNewSession: false }, + policy + ) + ).rejects.toThrow('WSL-owned') + expect(wslHookRelayManager.ensureForDistro).not.toHaveBeenCalled() + expect(buildPtyHostEnv).not.toHaveBeenCalled() + }) + it('cancels a waiting spawn without aborting the shared hook relay setup', async () => { + let finish: (() => void) | undefined + vi.mocked(wslHookRelayManager.ensureForDistro).mockImplementation( + () => + new Promise((resolve) => { + finish = resolve + }) + ) + const controller = new AbortController() + const pending = prepareWslGuestTerminalSpawn(prepared, options, policy, controller.signal) + controller.abort(new Error('spawn cancelled')) + await expect(pending).rejects.toThrow('spawn cancelled') + finish?.() + expect(buildPtyHostEnv).not.toHaveBeenCalled() + }) + it('refuses a Windows account home before launching a guest hook relay', async () => { + await expect( + prepareWslGuestTerminalSpawn(prepared, options, { + ...policy, + selectedCodexHomePath: 'C:\\Users\\alice' + }) + ).rejects.toThrow('account home') + expect(wslHookRelayManager.ensureForDistro).not.toHaveBeenCalled() + }) + it('honors disabled hooks without requiring a receiver', async () => { + await prepareWslGuestTerminalSpawn(prepared, options, { + ...policy, + agentStatusHooksEnabled: false + }) + expect(wslHookRelayManager.ensureForDistro).not.toHaveBeenCalled() + expect(buildPtyHostEnv).toHaveBeenCalledOnce() + }) +}) + +it.each([true, false])( + 'prepares daemon hooks for its captured user, enabled=%s', + async (enabled) => { + const daemon = { + owner: prepared.owner, + endpoint: { + distro: 'Ubuntu', + distributionId: 'registration', + userName: 'alice', + userId: '1000', + home: '/home/alice', + runtime: '/bun', + entry: '/daemon', + envBinary: '/usr/bin/env', + socket: '/socket', + tokenPath: '/token', + serverBuildId: 'daemon-build' + } + } + const result = await prepareWslGuestTerminalSpawn(daemon, options, { + ...policy, + agentStatusHooksEnabled: enabled + }) + expect(result).toMatchObject({ sessionId: 'fresh-desktop', isNewSession: true }) + if (enabled) { + expect(wslHookRelayManager.ensureForDistro).toHaveBeenCalledWith( + 'Ubuntu', + policy.selectedCodexHomePath, + undefined, + 'alice' + ) + } else { + expect(wslHookRelayManager.ensureForDistro).not.toHaveBeenCalled() + } + expect(prepareWslGuestSpawnOptions).toHaveBeenCalledWith( + expect.objectContaining({ + home: '/home/alice', + envBinary: '/usr/bin/env', + endpoint: daemon.endpoint + }), + expect.anything(), + undefined, + undefined + ) + } +) + +it('keeps the daemon owner captured while hook readiness awaits', async () => { + const daemon = { + owner: { ...prepared.owner }, + endpoint: { + distro: 'Ubuntu', + userName: 'alice', + userId: '1000', + home: '/home/alice', + runtime: '/bun', + envBinary: '/usr/bin/env', + tokenPath: '/token' + } + } + vi.mocked(wslHookRelayManager.ensureForDistro).mockImplementation(async () => { + daemon.endpoint.userName = 'bob' + daemon.endpoint.home = '/home/bob' + daemon.owner.relayBuildId = 'other-owner' + await Promise.resolve() + }) + await prepareWslGuestTerminalSpawn(daemon, options, policy) + expect(prepareWslGuestSpawnOptions).toHaveBeenCalledWith( + expect.objectContaining({ + owner: prepared.owner, + endpoint: expect.objectContaining({ userName: 'alice', home: '/home/alice' }) + }), + expect.anything(), + undefined, + undefined + ) +}) + +it('derives stale-hook deletion only after the guest hook coordinates are built', async () => { + vi.mocked(buildPtyHostEnv).mockImplementation((_id, env) => ({ + ...env, + ORCA_AGENT_HOOK_ENDPOINT: '/home/alice/.orca-wsl/agent-hooks/endpoint' + })) + const result = await prepareWslGuestTerminalSpawn(prepared, options, policy) + expect(result.envToDelete).not.toContain('ORCA_AGENT_HOOK_ENDPOINT') + expect(result.envToDelete).toContain('ORCA_AGENT_HOOK_PORT') +}) + +it('degrades missing hook endpoints without blocking fresh terminals', async () => { + vi.mocked(wslHookRelayManager.getGuestEndpointFilePath).mockReturnValue(null) + await prepareWslGuestTerminalSpawn(prepared, options, policy) + expect(buildPtyHostEnv).toHaveBeenCalledWith( + 'fresh-desktop', + options.env, + expect.objectContaining({ agentStatusHooksEnabled: false }) + ) + expect(prepareWslGuestSpawnOptions).toHaveBeenCalledOnce() +}) + +it('degrades failed optional hook setup without using a stale endpoint', async () => { + vi.mocked(wslHookRelayManager.ensureForDistro).mockRejectedValueOnce(new Error('unavailable')) + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) + try { + await prepareWslGuestTerminalSpawn(prepared, options, policy) + expect(buildPtyHostEnv).toHaveBeenCalledWith( + 'fresh-desktop', + options.env, + expect.objectContaining({ agentStatusHooksEnabled: false }) + ) + expect(wslHookRelayManager.getGuestEndpointFilePath).not.toHaveBeenCalled() + } finally { + warn.mockRestore() + } +}) + +it('accepts a selected account UNC path using registered distro casing', async () => { + await prepareWslGuestTerminalSpawn( + { + ...prepared, + owner: { ...prepared.owner, distro: 'ubuntu' }, + endpoint: { ...prepared.endpoint, distro: 'ubuntu' } + }, + options, + { ...policy, selectedCodexHomePath: '\\\\wsl$\\Ubuntu\\home\\alice\\.codex' } + ) + expect(buildPtyHostEnv).toHaveBeenCalledWith( + 'fresh-desktop', + options.env, + expect.objectContaining({ selectedCodexHomePath: '/home/alice/.codex', wslDistro: 'ubuntu' }) + ) +}) + +it('prepares a confirmed exited terminal with its raw guest identity and current account', async () => { + const daemon = { + owner: prepared.owner, + endpoint: { + ...prepared.endpoint, + userId: '1000', + home: '/home/alice', + envBinary: '/usr/bin/env', + tokenPath: '/token' + } + } + const sessionId = toAppWslPtyId(prepared.owner, 'pty2:owner:1') + const result = await prepareWslGuestTerminalSpawn( + daemon, + { ...options, sessionId, isNewSession: false, command: 'claude --resume current' }, + policy, + undefined, + 'confirmed-exited' + ) + expect(result).toMatchObject({ + sessionId, + isNewSession: false, + command: 'claude --resume current' + }) + expect(buildPtyHostEnv).toHaveBeenCalledWith( + 'pty2:owner:1', + options.env, + expect.objectContaining({ wslUser: 'alice', selectedCodexHomePath: '/home/alice/.codex' }) + ) + vi.clearAllMocks() + await expect( + prepareWslGuestTerminalSpawn( + daemon, + { + ...options, + sessionId: toAppWslPtyId({ ...prepared.owner, relayBuildId: 'foreign' }, 'pty2:owner:1'), + isNewSession: false + }, + policy, + undefined, + 'confirmed-exited' + ) + ).rejects.toThrow() + expect(buildPtyHostEnv).not.toHaveBeenCalled() +}) diff --git a/src/main/wsl/wsl-guest-terminal-preparation.ts b/src/main/wsl/wsl-guest-terminal-preparation.ts new file mode 100644 index 000000000000..a9977cd94c5b --- /dev/null +++ b/src/main/wsl/wsl-guest-terminal-preparation.ts @@ -0,0 +1,130 @@ +import { + getInheritedAgentHookEnvKeysToDelete, + mergePtyEnvDeletions +} from '../ipc/pty/host-env/pi-agent' +import { + detectExplicitPiAgentKindFromCommand, + isPiCompatibleAgentType +} from '../../shared/pi-agent-kind' +import { resolveSetupAgentSequenceLaunchCommand } from '../../shared/setup-agent-sequencing' +import { parseWslUncPath } from '../../shared/wsl-paths' +import { posix } from 'node:path' +import { waitForPromiseWithSignal, throwIfSignalAborted } from '../../shared/abort-signal-reason' +import { toRelayWslPtyId, parseAppWslPtyId } from '../../shared/wsl-pty-id' +import { buildPtyHostEnv } from '../ipc/pty/host-env/assembly' +import type { BuildPtyHostEnvOptions } from '../ipc/pty/host-env/types' +import type { PtySpawnOptions } from '../providers/types' +import { wslHookRelayManager } from '../agent-hooks/wsl-hook-relay-manager' +import { + prepareWslGuestSpawnOptions, + capturedWslGuestSpawnOwner, + type PreparedWslGuestSpawnOwner +} from './wsl-guest-spawn-options' + +/** Account selection/auth are prepared by the existing spawn preflight before this boundary. */ +export async function prepareWslGuestTerminalSpawn( + captured: PreparedWslGuestSpawnOwner, + options: PtySpawnOptions, + policy: BuildPtyHostEnvOptions, + signal?: AbortSignal, + mode?: 'confirmed-exited' +): Promise { + const prepared = capturedWslGuestSpawnOwner(captured) + throwIfSignalAborted(signal) + if ( + mode === 'confirmed-exited' && + (!prepared.daemon || !options.sessionId || options.isNewSession) + ) { + throw new Error('Cold restore requires an existing guest daemon terminal identity') + } + if (options.sessionId && !options.isNewSession) { + toRelayWslPtyId(prepared.owner, options.sessionId) + if (mode !== 'confirmed-exited') { + return prepareWslGuestSpawnOptions(prepared, options, signal) + } + } + if ( + options.attachOnly || + (mode !== 'confirmed-exited' && options.sessionId && parseAppWslPtyId(options.sessionId)) + ) { + throw new Error('Guest spawn preparation cannot replace an existing terminal owner') + } + const { userName: user, distro } = prepared.endpoint + if (!user || distro !== prepared.owner.distro || !options.sessionId) { + throw new Error('Guest spawn preparation requires an explicit owner and fresh desktop identity') + } + const selectedHome = policy.selectedCodexHomePath + const uncHome = selectedHome ? parseWslUncPath(selectedHome) : null + if ( + selectedHome && + (uncHome + ? uncHome.distro.toLowerCase() !== distro.toLowerCase() + : !selectedHome.startsWith('/')) + ) { + throw new Error('Selected account home does not belong to the guest owner') + } + const ownedPolicy = { + ...policy, + selectedCodexHomePath: uncHome?.linuxPath ?? selectedHome, + isWsl: true, + wslDistro: distro, + wslUser: user + } + if (policy.agentStatusHooksEnabled) { + const kind = isPiCompatibleAgentType(policy.launchAgent) + ? policy.launchAgent + : policy.launchAgent === undefined + ? detectExplicitPiAgentKindFromCommand( + resolveSetupAgentSequenceLaunchCommand(options.env ?? {}, policy.launchCommand) + ) + : null + let ready = true + try { + await waitForPromiseWithSignal( + wslHookRelayManager.ensureForDistro( + distro, + ownedPolicy.selectedCodexHomePath, + kind === 'pi' || kind === 'omp' ? kind : undefined, + user + ), + signal + ? AbortSignal.any([signal, AbortSignal.timeout(15_000)]) + : AbortSignal.timeout(15_000) + ) + } catch (error) { + throwIfSignalAborted(signal) + ready = false + console.warn('[wsl] Terminal hooks unavailable; continuing without hooks', error) + } + const endpoint = ready ? wslHookRelayManager.getGuestEndpointFilePath(distro, user) : null + const root = posix.join(prepared.home, '.orca-wsl', 'agent-hooks') + if (endpoint && (!endpoint.startsWith(`${root}/`) || posix.normalize(endpoint) !== endpoint)) { + throw new Error('Guest hook endpoint is unavailable for the terminal owner') + } + ownedPolicy.agentStatusHooksEnabled = Boolean(endpoint) + if (!endpoint) { + ownedPolicy.codexStatusHooksEnabled = false + } + } + throwIfSignalAborted(signal) + const env = buildPtyHostEnv( + mode === 'confirmed-exited' + ? toRelayWslPtyId(prepared.owner, options.sessionId) + : options.sessionId, + { ...options.env }, + ownedPolicy + ) + return prepareWslGuestSpawnOptions( + prepared, + { + ...options, + env, + envToDelete: mergePtyEnvDeletions( + options.envToDelete, + getInheritedAgentHookEnvKeysToDelete(env) + ) + }, + signal, + mode + ) +} diff --git a/src/main/wsl/wsl-invocation-boundary.test.ts b/src/main/wsl/wsl-invocation-boundary.test.ts index 29906362a47c..e8c137a3c032 100644 --- a/src/main/wsl/wsl-invocation-boundary.test.ts +++ b/src/main/wsl/wsl-invocation-boundary.test.ts @@ -365,7 +365,7 @@ describe('wsl.exe is spawned through one runner', () => { // so it passed even for a scanner that found nothing. This fails the moment // detection stops seeing a call that is definitely there. expect(offenders).toContain('main/git/command-runner/wsl-command-resolution.ts') - expect(offenders).toContain('main/providers/local-pty-spawn.ts') + expect(offenders).not.toContain('main/providers/local-pty-spawn.ts') }) it('adds no new direct wsl.exe spawn', () => { diff --git a/src/main/wsl/wsl-runner.ts b/src/main/wsl/wsl-runner.ts index ac7676c25db9..d549b7a2b0b2 100644 --- a/src/main/wsl/wsl-runner.ts +++ b/src/main/wsl/wsl-runner.ts @@ -64,6 +64,8 @@ export type WslCommand = export type WslSpec = WslCommand & { /** Undefined selects the distro's default. */ distro?: string + /** Preserve an existing execution owner when the distro default changes. */ + user?: string /** Required, with no default: the wrong answer here is the defect this file exists to prevent. */ loginPath: WslLoginPath /** Guest (POSIX) path. */ @@ -210,7 +212,7 @@ export async function runWslProcess(spec: WslSpec): Promise { const remainingForProbe = deadline - Date.now() const probeBudgetMs = Math.max(1, Math.min(4_000, Math.floor(remainingForProbe / 2))) const environment = wantsEnvironment - ? await getWslGuestEnvironment(spec.distro, probeBudgetMs) + ? await getWslGuestEnvironment(spec.distro, probeBudgetMs, spec.user) : null // Probe failure must NOT fall back to the login shell. That lane sources @@ -224,7 +226,10 @@ export async function runWslProcess(spec: WslSpec): Promise { const argvForm = buildGuestArgv(environment, spec, 'argv') // Measure what is actually spawned: `wsl.exe` and `-d --exec` are // prepended after this point and are part of the same budget. - const fullLine = [resolveWslExecutablePath(), ...buildWslExecArgs(spec.distro, argvForm)] + const fullLine = [ + resolveWslExecutablePath(), + ...buildWslExecArgs(spec.distro, argvForm, spec.user) + ] // Argv is the default, but it has a hard ceiling that stdin does not. A user's // `orca.yaml` hook is the one unbounded script Orca runs, so past the cap the // choice is between failing to spawn at all and accepting the stdin caveat. @@ -239,7 +244,7 @@ export async function runWslProcess(spec: WslSpec): Promise { const remainingMs = Math.max(1, deadline - Date.now()) const result = await runProcess({ program: resolveWslExecutablePath(), - args: buildWslExecArgs(spec.distro, argv), + args: buildWslExecArgs(spec.distro, argv, spec.user), // Name a Windows directory rather than inheriting one: an inherited cwd that // is later deleted (the worktree Orca launched from) fails every later spawn // (#16463). Never the guest cwd -- withGuestCwd still cds inside. diff --git a/src/main/zsh-startup-hook-pty-harness.ts b/src/main/zsh-startup-hook-pty-harness.ts index a4de4e3bba9d..2c14738e8266 100644 --- a/src/main/zsh-startup-hook-pty-harness.ts +++ b/src/main/zsh-startup-hook-pty-harness.ts @@ -11,16 +11,19 @@ * command being typed, so matching on stdout matches the echo of the probe as * readily as its output. */ -import { spawnSync } from 'node:child_process' +import { runProcessSync } from '../shared/child-process/run-process' +import { runBundledBunFixture } from './bundled-bun-test-execution' +import { spawnBunPty } from './daemon/pty-subprocess/bun-pty-process' +import { z } from 'zod' import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' -import * as pty from 'node-pty' -export const hasZsh = process.platform !== 'win32' && spawnSync('zsh', ['--version']).status === 0 +export const hasZsh = + process.platform !== 'win32' && runProcessSync({ program: 'zsh', args: ['--version'] }).code === 0 export const ZSH_PATH = hasZsh - ? (spawnSync('sh', ['-c', 'command -v zsh'], { encoding: 'utf8' }).stdout || '').trim() + ? (runProcessSync({ program: 'sh', args: ['-c', 'command -v zsh'] }).stdout || '').trim() : '' /** OSC sequences the wrapper emits, as the terminal would receive them. */ @@ -89,13 +92,30 @@ function parseValues(resultPath: string): Record { * so a slow prompt framework makes the run slower, never flaky. */ export async function runZshPty(options: ZshPtyOptions): Promise { + if (!process.versions.bun) { + return z + .object({ + output: z.string(), + values: z.record(z.string(), z.string()), + exitedBeforePrompt: z.boolean() + }) + .parse( + await runBundledBunFixture( + __filename, + 'runZshPty', + options, + (options.timeoutMs ?? 20_000) + 5_000 + ) + ) + } const sentinel = '@@ORCA-PTY-READY@@' const workDir = mkdtempSync(join(tmpdir(), 'orca-zsh-pty-')) const resultPath = join(workDir, 'probe.txt') const timeoutMs = options.timeoutMs ?? 20_000 - const proc = pty.spawn(ZSH_PATH, ['-l', '-i'], { - name: 'xterm-256color', + const proc = spawnBunPty({ + file: ZSH_PATH, + args: ['-l', '-i'], cols: 200, rows: 40, cwd: options.cwd ?? workDir, diff --git a/src/main/zsh-startup-wrapper-builder.ts b/src/main/zsh-startup-wrapper-builder.ts index 49f706e57417..affa22b15eb2 100644 --- a/src/main/zsh-startup-wrapper-builder.ts +++ b/src/main/zsh-startup-wrapper-builder.ts @@ -39,7 +39,7 @@ import { /** Runtime values the hook re-exports after the user's own startup files ran. */ export type ZshWrapperRestoreSpec = { - /** The managed WSL CLI dir onto PATH — local wrappers only; a no-op outside WSL. */ + /** The managed WSL CLI dir onto PATH; a no-op outside WSL. */ managedWslCli: boolean /** Orca's agent-teams shim dir back onto PATH. */ agentTeamsPath: boolean diff --git a/src/preload/api/pty-bridge-stream-and-serialization.ts b/src/preload/api/pty-bridge-stream-and-serialization.ts index 49e2189fd537..5e9c0cf150e2 100644 --- a/src/preload/api/pty-bridge-stream-and-serialization.ts +++ b/src/preload/api/pty-bridge-stream-and-serialization.ts @@ -146,6 +146,7 @@ export const ptyStreamAndSerializationApi = { killAll: () => ipcRenderer.invoke('pty:management:killAll'), killOne: (args: { sessionId: string }) => ipcRenderer.invoke('pty:management:killOne', args), restart: () => ipcRenderer.invoke('pty:management:restart'), + retry: () => ipcRenderer.invoke('pty:management:retry'), macTccAttribution: () => ipcRenderer.invoke('pty:management:macTccAttribution'), resetFolderAccess: () => ipcRenderer.invoke('pty:management:resetFolderAccess') } diff --git a/src/preload/api/pty-management-api.ts b/src/preload/api/pty-management-api.ts index ddc94c77f2ce..0bb36f934f9e 100644 --- a/src/preload/api/pty-management-api.ts +++ b/src/preload/api/pty-management-api.ts @@ -41,8 +41,12 @@ export type PtyManagementFolderAccessResetResult = | { outcome: 'probed'; mismatch: PtyManagementFolderAccessMismatch | null } export type PtyManagementApi = { - // `degraded`: daemon is alive but can't spawn fresh PTYs, so new terminals run locally without daemon persistence. - listSessions: () => Promise<{ sessions: PtyManagementSession[]; degraded: boolean }> + // Existing sessions remain available while fresh terminal creation is unavailable. + listSessions: () => Promise<{ + sessions: PtyManagementSession[] + degraded: boolean + unavailable?: boolean + }> killAll: () => Promise<{ killedCount: number remainingCount: number @@ -50,6 +54,7 @@ export type PtyManagementApi = { }> killOne: (args: { sessionId: string }) => Promise<{ success: boolean }> restart: () => Promise<{ success: boolean }> + retry: () => Promise<{ success: boolean }> macTccAttribution: () => Promise<{ health: PtyManagementMacTccAttributionHealth folderAccessMismatch: PtyManagementFolderAccessMismatch | null diff --git a/src/relay/pty-handler-dispose-lifecycle.test.ts b/src/relay/pty-handler-dispose-lifecycle.test.ts index 838921fee857..e10e3ced196c 100644 --- a/src/relay/pty-handler-dispose-lifecycle.test.ts +++ b/src/relay/pty-handler-dispose-lifecycle.test.ts @@ -54,6 +54,25 @@ describe('PtyHandler', () => { await endPtyHandlerTest(handler, originalPlatform) }) + it('kills and removes a native PTY when buffered output rejects listener admission', async () => { + const kill = vi.fn() + const destroy = vi.fn() + mockPtySpawn.mockReturnValue({ + ...mockPtyInstance, + kill, + destroy, + onData: vi.fn(() => { + throw new Error('startup output overflow') + }) + }) + await expect(dispatcher.callRequest('pty.spawn', { cols: 80, rows: 24 })).rejects.toThrow( + 'startup output overflow' + ) + expect(kill).toHaveBeenCalledWith('SIGKILL') + expect(destroy).toHaveBeenCalledOnce() + expect(await dispatcher.callRequest('pty.listProcesses')).toEqual([]) + }) + it('invokes the exit listener with the spawn-time paneKey', async () => { let onExitCb: ((evt: { exitCode: number }) => void) | undefined mockPtySpawn.mockReturnValue({ diff --git a/src/relay/pty-handler-spawn-admission.test.ts b/src/relay/pty-handler-spawn-admission.test.ts index 0beaca3cfbac..2e745c56f145 100644 --- a/src/relay/pty-handler-spawn-admission.test.ts +++ b/src/relay/pty-handler-spawn-admission.test.ts @@ -208,7 +208,7 @@ describe('PtyHandler', () => { expect(handler.activePtyCount).toBe(1) }) - it('retains an operation fence when publication fails after native spawn', async () => { + it('retains an operation fence after cleaning up failed native publication', async () => { const operationId = 'f'.repeat(43) mockPtySpawn.mockReturnValue({ ...mockPtyInstance, @@ -229,7 +229,8 @@ describe('PtyHandler', () => { 'listener publication failed' ) expect(mockPtySpawn).toHaveBeenCalledOnce() - expect(handler.activePtyCount).toBe(1) + expect(mockPtyInstance.kill).toHaveBeenCalledWith('SIGKILL') + expect(handler.activePtyCount).toBe(0) }) it('releases a canceled operation before Bun spawn', async () => { diff --git a/src/relay/pty-handler.ts b/src/relay/pty-handler.ts index 08c5f824f7fc..4d85450ea096 100644 --- a/src/relay/pty-handler.ts +++ b/src/relay/pty-handler.ts @@ -1,5 +1,5 @@ /* oxlint-disable max-lines */ -import type { IPty } from 'node-pty' +import type { TerminalProcess } from '../shared/terminal-process' import { killWithDescendantSweep } from '../main/pty-descendant-termination' import { bunRelayPtyModule } from './relay-pty-runtime' import { @@ -100,7 +100,7 @@ import { } from '../shared/claimed-agent-pty-owner' import type { RelayPtySourceOutput } from './relay-pty-source-output' import { signalPosixPtyForegroundGroup } from '../main/pty/posix-pty-foreground-group' -import { readPtsName } from '../main/pty/node-pty-pts-name' +import { readPtsName } from '../main/pty/terminal-slave-device' import type { RelayPtySourcePublication } from './relay-pty-source-publication' import type { PtySourceRecoveryRequest, @@ -197,7 +197,7 @@ function parseSourceRecoveryRequest(value: unknown): PtySourceRecoveryRequest | type ManagedPty = { id: string incarnationId: string - pty: IPty + pty: TerminalProcess initialCwd: string /** Why a chunk deque: rebuilding a rolling 100KB string per PTY chunk copied the * whole window on every write once saturated. Readers are attach/adopt/revive only. */ @@ -284,7 +284,7 @@ type ManagedStartupCommand = { } // Why: Windows ConPTY rejects signals; forward them only on POSIX. -function killPtyProcess(pty: IPty, signal: string): void { +function killPtyProcess(pty: TerminalProcess, signal: string): void { if (process.platform === 'win32') { pty.kill() return @@ -842,7 +842,21 @@ export class PtyHandler { } /** Wire onData/onExit listeners for a managed PTY and store it. */ - private wireAndStore(managed: ManagedPty): void { + private async wireAndStore(managed: ManagedPty): Promise { + try { + this.wireManagedPty(managed) + } catch (error) { + // Failed listener admission must not orphan the already-spawned native owner. + try { + await this.disposePtyForRelayShutdown(managed, false) + } catch (cleanupError) { + throw new AggregateError([error, cleanupError], 'PTY listener admission and cleanup failed') + } + throw error + } + } + + private wireManagedPty(managed: ManagedPty): void { managed.physicalExit = new PhysicalExitTracker() this.ptys.set(managed.id, managed) // Why: a PTY joining the pool under this paneKey means the surface exists again (reopened pane @@ -1966,7 +1980,7 @@ export class PtyHandler { this.sourcePublication?.activate(id, managed.incarnationId, context) const sourceActivation = context && this.sourcePublication?.receivingActivation?.(id, context.clientId) - this.wireAndStore(managed) + await this.wireAndStore(managed) if (context?.isStale() && !params.agentSessionEnsure && !params.agentSessionCreateOperationId) { // Why: if the client reconnected while pty.spawn was in flight, the // response is discarded and no renderer can own this PTY. Shut it down @@ -2149,13 +2163,7 @@ export class PtyHandler { if (this.reapPtyProvenExited(managed)) { return } - // The patched node-pty retires `_fd` in the same block that gives up the - // master (config/patches/node-pty@1.1.0.patch), which makes a resize past - // that point a no-op rather than a TIOCSWINSZ aimed at a reused descriptor. - // That covers only part of the window and does not cover this process at - // all: libuv closes the fd synchronously inside `uv_close`, before the JS - // `'close'` that runs `_close()`, and a relay host installs node-pty from - // npm, where the patch is not applied. So the catch below stays. + // Backend resize can race native terminal disposal; keep teardown errors local to this PTY. try { managed.pty.resize(cols, rows) } catch (err) { @@ -2999,7 +3007,7 @@ export class PtyHandler { const shellLaunch = getRelayShellLaunchConfig(shell, spawnEnv, process.platform, { terminalWindowsWslDistro }) - let term: IPty + let term: TerminalProcess try { term = await ptyMod.spawn(shell, shellLaunch.args, { name: spawnEnv.TERM ?? 'xterm-256color', @@ -3027,7 +3035,7 @@ export class PtyHandler { } return } - this.wireAndStore({ + await this.wireAndStore({ id: entry.id, incarnationId: randomUUID(), pty: term, diff --git a/src/relay/pty-shell-overlay-wrappers.ts b/src/relay/pty-shell-overlay-wrappers.ts index 2231fc2b29b9..0951ca46f20f 100644 --- a/src/relay/pty-shell-overlay-wrappers.ts +++ b/src/relay/pty-shell-overlay-wrappers.ts @@ -1,5 +1,6 @@ import { readFileSync, statSync } from 'node:fs' import { join } from 'node:path' +import { WSL_MANAGED_CLI_PATH_RESTORE } from '../main/wsl-managed-cli-path-restore' import { getPosixOmpShellWrapper } from '../main/pty/omp-shell-wrapper' import { BASH_FEATURE_CHANNEL_BLOCK, @@ -32,7 +33,7 @@ function getRelayZshWrapperSpec(): ZshStartupHookSpec { overlayRestoreComment: '# Why: remote startup files can re-export user defaults after relay spawn.', restores: { - managedWslCli: false, + managedWslCli: true, agentTeamsPath: false, remoteCliBinDir: true, codexHome: false, @@ -73,6 +74,7 @@ fi [[ -n "\${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="\${ORCA_OPENCODE_CONFIG_DIR}" [[ -n "\${ORCA_MIMOCODE_HOME:-}" ]] && export MIMOCODE_HOME="\${ORCA_MIMOCODE_HOME}" [[ -n "\${ORCA_REMOTE_CLI_BIN_DIR:-}" ]] && case ":$PATH:" in *:"\${ORCA_REMOTE_CLI_BIN_DIR}":*) ;; *) export PATH="\${ORCA_REMOTE_CLI_BIN_DIR}:$PATH" ;; esac +${WSL_MANAGED_CLI_PATH_RESTORE} ${getPosixOmpShellWrapper()} ${BASH_HISTFILE_RESTORE_BLOCK} # Why: SSH bash sessions need the same command lifecycle markers as local diff --git a/src/relay/pty-shell-wsl-cli.test.ts b/src/relay/pty-shell-wsl-cli.test.ts new file mode 100644 index 000000000000..59854e1d65aa --- /dev/null +++ b/src/relay/pty-shell-wsl-cli.test.ts @@ -0,0 +1,56 @@ +import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { runProcess, runProcessSync } from '../shared/child-process/run-process' +import { ensureOverlayRestoreWrappers } from './pty-shell-overlay-wrappers' + +const directories: string[] = [] +afterEach(() => { + for (const directory of directories.splice(0)) { + rmSync(directory, { recursive: true, force: true }) + } +}) + +describe.each(['bash', 'zsh'])('guest relay %s CLI environment', (shell) => { + const available = + process.platform !== 'win32' && + runProcessSync({ program: 'sh', args: ['-c', 'command -v "$1"', 'orca-shell-probe', shell] }) + .code === 0 + + it.skipIf(!available)('restores the managed CLI after user startup replaces PATH', async () => { + const root = mkdtempSync(join(tmpdir(), 'orca guest cli ')) + directories.push(root) + const home = join(root, 'home') + const cli = join(root, 'managed cli') + const wrappers = join(root, 'wrappers') + mkdirSync(home) + mkdirSync(cli) + writeFileSync(join(cli, 'orca-ide'), '#!/bin/sh\nprintf "guest-cli-ok\\n"\n') + chmodSync(join(cli, 'orca-ide'), 0o755) + const startup = 'export PATH=/usr/bin:/bin\n' + writeFileSync(join(home, '.bash_profile'), startup) + writeFileSync(join(home, '.zshrc'), startup) + expect(ensureOverlayRestoreWrappers(wrappers)).toBe(true) + const result = await runProcess({ + program: shell, + args: + shell === 'bash' + ? ['--noprofile', '--rcfile', join(wrappers, 'bash', 'rcfile'), '-i'] + : ['-i'], + env: { + HOME: home, + PATH: '/usr/bin:/bin', + TERM: 'xterm', + ZDOTDIR: join(wrappers, 'zsh'), + ORCA_WSL_CLI_DIR: cli, + ORCA_CLI_COMMAND: 'orca-ide' + }, + input: 'orca-ide\nexit 0\n', + timeoutMs: 5_000 + }) + expect(result.code).toBe(0) + expect(result.stdout).toContain('guest-cli-ok') + expect(result.stderr).not.toContain('command not found') + }) +}) diff --git a/src/relay/relay-daemon-fatal-reap.test.ts b/src/relay/relay-daemon-fatal-reap.test.ts index b8b268547eaa..8900208a8175 100644 --- a/src/relay/relay-daemon-fatal-reap.test.ts +++ b/src/relay/relay-daemon-fatal-reap.test.ts @@ -96,7 +96,6 @@ vi.mock('./relay-grace-lifecycle', () => ({ import { createMockDispatcher } from './pty-handler-test-harness' import { runRelayDaemon } from './relay-daemon' -import { __setConptyJobNativeForTests } from '../main/windows/windows-pty-job' describe('relay daemon fatal PTY reap', () => { let originalPlatform: PropertyDescriptor | undefined @@ -137,7 +136,6 @@ describe('relay daemon fatal PTY reap', () => { } const handler = daemonMocks.runtimePtyHandler as PtyHandler | null await handler?.dispose({ waitForPhysicalExit: false }).catch(() => {}) - __setConptyJobNativeForTests() vi.restoreAllMocks() if (originalPlatform) { Object.defineProperty(process, 'platform', originalPlatform) @@ -150,15 +148,18 @@ describe('relay daemon fatal PTY reap', () => { throw new Error('ConPTY close failed') }) const secondKill = vi.fn() + const terminateJob = vi.fn((id: number) => + id === 22 ? ('terminated' as const) : ('unavailable' as const) + ) daemonMocks.mockPtySpawn - .mockReturnValueOnce(createMockPty(11, 101, firstKill)) - .mockReturnValueOnce(createMockPty(22, 202, secondKill)) - const terminateJob = vi.fn((id: number) => id === 22) - __setConptyJobNativeForTests(() => ({ - terminateJob, - listJobProcessIds: vi.fn(), - assignCurrentProcessToJob: vi.fn() - })) + .mockReturnValueOnce({ + ...createMockPty(11, 101, firstKill), + terminateOwnedTree: () => terminateJob(11) + }) + .mockReturnValueOnce({ + ...createMockPty(22, 202, secondKill), + terminateOwnedTree: () => terminateJob(22) + }) const exit = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as never) await runRelayDaemon({ @@ -178,10 +179,7 @@ describe('relay daemon fatal PTY reap', () => { expect(fatalListener).toBeDefined() fatalListener!(new Error('relay crashed'), 'uncaughtException') - expect(terminateJob.mock.calls).toEqual([ - [11, 101], - [22, 202] - ]) + expect(terminateJob.mock.calls).toEqual([[11], [22]]) expect(firstKill).toHaveBeenCalledOnce() expect(firstKill.mock.calls[0]).toEqual([]) expect(secondKill).not.toHaveBeenCalled() diff --git a/src/relay/relay-pty-runtime.ts b/src/relay/relay-pty-runtime.ts index be85dc6663a1..45a641def26c 100644 --- a/src/relay/relay-pty-runtime.ts +++ b/src/relay/relay-pty-runtime.ts @@ -1,4 +1,4 @@ -import type { IPty } from 'node-pty' +import type { TerminalProcess } from '../shared/terminal-process' import { waitForPromiseWithSignal } from '../shared/abort-signal-reason' import { spawnBunPty } from '../main/daemon/pty-subprocess/bun-pty-process' @@ -16,7 +16,7 @@ export type RelayPtyModule = { args: string[], options: RelayPtySpawnOptions, signal?: AbortSignal - ): IPty | Promise + ): TerminalProcess | Promise } export const bunRelayPtyModule: RelayPtyModule = { diff --git a/src/renderer/src/components/settings/ManageSessionsSection.tsx b/src/renderer/src/components/settings/ManageSessionsSection.tsx index 732566877928..87f8d7531189 100644 --- a/src/renderer/src/components/settings/ManageSessionsSection.tsx +++ b/src/renderer/src/components/settings/ManageSessionsSection.tsx @@ -1,3 +1,4 @@ +import { TerminalServiceRetryNotice } from './TerminalServiceRetryNotice' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { toast } from 'sonner' import type { PtyManagementSession } from '../../../../preload/api-types' @@ -19,6 +20,7 @@ import { translate } from '@/i18n/i18n' type ConfirmKind = 'killOne' export function ManageSessionsSection(): React.JSX.Element { + const [serviceUnavailable, setServiceUnavailable] = useState(false) const [sessions, setSessions] = useState([]) const [isRefreshing, setIsRefreshing] = useState(true) const [hasLoadedOnce, setHasLoadedOnce] = useState(false) @@ -82,6 +84,7 @@ export function ManageSessionsSection(): React.JSX.Element { return result.sessions } setSessions(result.sessions) + setServiceUnavailable(result.degraded || result.unavailable === true) return result.sessions } catch (err) { console.error('[manage-sessions] listSessions failed', err) @@ -218,6 +221,7 @@ export function ManageSessionsSection(): React.JSX.Element { showManageSessionsButton={false} refreshRevision={attributionRefreshRevision} /> + {serviceUnavailable && void refresh()} />} ({ translate: (_key: string, fallback: string) => fallback })) +const container = document.createElement('div') +const root = createRoot(container) +afterEach(async () => { + await act(async () => root.render(null)) +}) + +it('keeps retries nondestructive and disables the action until settlement', async () => { + let finish!: (value: { success: boolean }) => void + const retry = vi.fn( + () => + new Promise<{ success: boolean }>((resolve) => { + finish = resolve + }) + ) + const restart = vi.fn() + Object.assign(window, { api: { pty: { management: { retry, restart } } } }) + const recovered = vi.fn() + await act(async () => root.render()) + const button = container.querySelector('button')! + await act(async () => button.click()) + expect(button.disabled).toBe(true) + expect(retry).toHaveBeenCalledOnce() + expect(restart).not.toHaveBeenCalled() + await act(async () => finish({ success: true })) + expect(recovered).toHaveBeenCalledOnce() + expect(button.disabled).toBe(false) +}) + +it('retains an inline error and permits another attempt', async () => { + const retry = vi.fn(async () => ({ success: false })) + Object.assign(window, { api: { pty: { management: { retry } } } }) + const recovered = vi.fn() + await act(async () => root.render()) + await act(async () => container.querySelector('button')!.click()) + expect(container.textContent).toContain('still unavailable') + expect(container.querySelector('button')!.disabled).toBe(false) + expect(recovered).not.toHaveBeenCalled() +}) diff --git a/src/renderer/src/components/settings/TerminalServiceRetryNotice.tsx b/src/renderer/src/components/settings/TerminalServiceRetryNotice.tsx new file mode 100644 index 000000000000..412ce945d0ea --- /dev/null +++ b/src/renderer/src/components/settings/TerminalServiceRetryNotice.tsx @@ -0,0 +1,57 @@ +import { useState } from 'react' +import { Button } from '../ui/button' +import { translate } from '@/i18n/i18n' + +export function TerminalServiceRetryNotice({ + onRecovered +}: { + onRecovered: () => void +}): React.JSX.Element { + const [busy, setBusy] = useState(false) + const [failed, setFailed] = useState(false) + + const retry = async (): Promise => { + if (busy) { + return + } + setBusy(true) + setFailed(false) + try { + const result = await window.api.pty.management.retry() + if (result.success) { + onRecovered() + } else { + setFailed(true) + } + } catch { + setFailed(true) + } finally { + setBusy(false) + } + } + + return ( +
+

+ {failed + ? translate( + 'terminal.service.retryFailed', + 'The terminal service is still unavailable. Retry preserves existing sessions. You can also use Restart daemon below; restarting closes its running sessions.' + ) + : translate( + 'terminal.service.unavailable', + 'New terminals are unavailable. Retry the terminal service without stopping existing sessions.' + )} +

+ +
+ ) +} diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index a722f8540823..5a9c6f8b5c91 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -18427,5 +18427,12 @@ }, "common": { "retry": "Retry" + }, + "terminal": { + "service": { + "retryFailed": "The terminal service is still unavailable. Retry preserves existing sessions. You can also use Restart daemon below; restarting closes its running sessions.", + "unavailable": "New terminals are unavailable. Retry the terminal service without stopping existing sessions.", + "retry": "Retry terminal service" + } } } diff --git a/src/renderer/src/web/preload-api/web-terminal-api.ts b/src/renderer/src/web/preload-api/web-terminal-api.ts index 535c34544312..e80177d83f00 100644 --- a/src/renderer/src/web/preload-api/web-terminal-api.ts +++ b/src/renderer/src/web/preload-api/web-terminal-api.ts @@ -94,6 +94,7 @@ export function createPtyApi(): NonNullable['pty']> { killAll: () => Promise.resolve({ killedCount: 0, remainingCount: 0, killedSessionIds: [] }), killOne: () => Promise.resolve({ success: false }), restart: () => Promise.resolve({ success: false }), + retry: () => Promise.resolve({ success: false }), // Why: web clients can't inspect the host daemon's pid record; 'unknown' keeps the banner hidden. macTccAttribution: () => Promise.resolve({ health: 'unknown' as const, folderAccessMismatch: null }), diff --git a/src/shared/child-process/__fixtures__/child-process-import-allowlist.txt b/src/shared/child-process/__fixtures__/child-process-import-allowlist.txt index 7741c9907dde..0d5b9a85ea2d 100644 --- a/src/shared/child-process/__fixtures__/child-process-import-allowlist.txt +++ b/src/shared/child-process/__fixtures__/child-process-import-allowlist.txt @@ -156,7 +156,6 @@ src/main/workspace-space-analysis.ts src/main/wsl-availability.ts src/main/wsl-unc-delete.ts src/main/wsl.ts -src/main/zsh-startup-hook-pty-harness.ts src/relay/agent-exec-handler.ts src/relay/ai-vault-service-client-state.ts src/relay/ai-vault-service-client.ts diff --git a/src/shared/child-process/__fixtures__/windows-console-visibility-allowlist.txt b/src/shared/child-process/__fixtures__/windows-console-visibility-allowlist.txt index 6de7701b81ba..ecc24994f1a4 100644 --- a/src/shared/child-process/__fixtures__/windows-console-visibility-allowlist.txt +++ b/src/shared/child-process/__fixtures__/windows-console-visibility-allowlist.txt @@ -49,7 +49,6 @@ main/workspace-space-analysis.ts main/wsl-availability.ts main/wsl-unc-delete.ts main/wsl.ts -main/zsh-startup-hook-pty-harness.ts relay/agent-exec-handler.ts relay/external-automations-handler.ts relay/fs-handler-git-fallback.ts diff --git a/src/shared/child-process/child-process-import-boundary.test.ts b/src/shared/child-process/child-process-import-boundary.test.ts index e5c1d3621126..6d88a6de993d 100644 --- a/src/shared/child-process/child-process-import-boundary.test.ts +++ b/src/shared/child-process/child-process-import-boundary.test.ts @@ -29,7 +29,7 @@ const CHILD_PROCESS_IMPORT_ALLOWLIST: readonly string[] = readFileSync( * May only ever be DECREASED, and only by migrating a file off * `node:child_process`. Raising it is never the fix. */ -const DIRECT_IMPORTER_PIN = 149 +const DIRECT_IMPORTER_PIN = 148 const IMPORT_PATTERN = /(?:from\s+['"]node:child_process['"]|from\s+['"]child_process['"]|require\(\s*['"]node:child_process['"]|require\(\s*['"]child_process['"])/ diff --git a/src/shared/child-process/windows-console-visibility.test.ts b/src/shared/child-process/windows-console-visibility.test.ts index 0f92d943d298..bc3e4b60b4b6 100644 --- a/src/shared/child-process/windows-console-visibility.test.ts +++ b/src/shared/child-process/windows-console-visibility.test.ts @@ -34,7 +34,7 @@ const ALLOWLIST: readonly string[] = readAllowlist( * the allowlist does not bound this: a swap (one file fixed and delisted, one * new file added with its entry) satisfies both membership assertions. */ -const UNHIDDEN_SPAWNER_PIN = 61 +const UNHIDDEN_SPAWNER_PIN = 60 const CHILD_PROCESS_IMPORT = /from\s+['"](?:node:)?child_process['"]|require\(\s*['"](?:node:)?child_process['"]/ diff --git a/src/shared/fish-query-reply-child-stdin.node-pty.test.ts b/src/shared/fish-query-reply-child-stdin.node-pty.test.ts deleted file mode 100644 index 2bb3925328b2..000000000000 --- a/src/shared/fish-query-reply-child-stdin.node-pty.test.ts +++ /dev/null @@ -1,225 +0,0 @@ -/** - * Real-fish regression for #13892: a terminal query reply Orca held back is overtaken - * by the DA1 answer written later in the same turn, so fish's read sentinel hands the - * tty to the child while the OSC 11 reply is still queued — and the CHILD READS IT. - * - * What is real here: node-pty running fish, `PtyStartupIngress`, `PtyStartupReplyDelivery` - * and both echo probes, plus the host's own write gate - * (`answerLiveQueryReply` — copied from local-pty-provider.write, which - * is what decides whether a reply is deferred at all). Only the renderer is modelled: it - * answers queries strictly in the order they appear in the stream, so any inversion the - * child sees was produced by the delivery split and nothing else. - * - * The assertion is about a CHILD PROCESS'S STDIN, not the screen: a rendered-output check - * passes while the bytes are still being eaten by the next `npx` / `brew` confirm prompt. - * The child reads a full LINE because a leaked reply carries no newline, so a - * once('data') child would report it alone whenever it happened to land in its own read. - */ -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' -import { tmpdir } from 'node:os' -import path from 'node:path' -import { afterEach, describe, expect, it } from 'vitest' -import { fishRequirementViolation, resolveFishBinary } from './fish-binary-requirement' -import { PtyStartupIngress } from './pty-startup-ingress' - -// Why fish 4: the DA1-sentinel handoff this measures lives in the 4.0 Rust tty_handoff. -const FISH = resolveFishBinary(4) -const itWithFish = FISH.available ? it : it.skip - -const PROMPT_MARK = 'ORCA13892> ' - -/* oxlint-disable no-control-regex -- terminal query grammars are control sequences */ -/** Anchored at an ESC, first match wins; reply values match xterm.js's. */ -const QUERY_GRAMMARS = [ - { - re: /^\x1b\]1[012];\?(\x07|\x1b\\)/, - reply: (m: RegExpExecArray) => `\x1b]11;rgb:1e1e/1e1e/1e1e${m[1]}` - }, - { re: /^\x1b\[\?6n/, reply: () => '\x1b[?1;1;1R' }, - { re: /^\x1b\[6n/, reply: () => '\x1b[1;1R' }, - { re: /^\x1b\[\?996n/, reply: () => '\x1b[?997;1n' }, - { re: /^\x1b\[>0?c/, reply: () => '\x1b[>0;276;0c' }, - { re: /^\x1b\[0?c/, reply: () => '\x1b[?1;2c' }, - { re: /^\x1b\[>0?q/, reply: () => '\x1bP>|Orca\x1b\\' }, - { re: /^\x1b\[\?u/, reply: () => '\x1b[?0u' } -] as const -/** Still accumulating: no CSI final byte and no OSC/DCS terminator yet. */ -const PARTIAL_QUERY_RE = - /^(?:\x1b|\x1b\[[?>=]?[0-9;]*|\x1b\][0-9]*(?:;[^\x07\x1b]*)?\x1b?|\x1bP[^\x1b]*\x1b?)$/ -/* oxlint-enable no-control-regex */ - -const sleep = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)) - -async function waitUntil(predicate: () => boolean, timeoutMs: number): Promise { - const deadline = Date.now() + timeoutMs - while (Date.now() < deadline) { - if (predicate()) { - return true - } - await sleep(10) - } - return false -} - -describe('a held query reply never reaches the next child process (#13892)', () => { - let configHome: string | null = null - - // Always runs, so the CI lane cannot report green with the regression below skipped. - it('has the fish this suite needs when CI requires one', () => { - expect(fishRequirementViolation(FISH)).toBeNull() - }) - - // Same contract for the other half of the setup: a prebuilt node-pty has no echoState, - // so the fix under test would be off and the regression below would run vacuously. - - afterEach(() => { - if (configHome) { - rmSync(configHome, { recursive: true, force: true }) - configHome = null - } - }) - - itWithFish( - 'answers OSC 11 in the query turn so the reply cannot land in the child’s stdin', - async () => { - configHome = mkdtempSync(path.join(tmpdir(), 'orca-fish-13892-')) - mkdirSync(path.join(configHome, 'fish'), { recursive: true }) - writeFileSync( - path.join(configHome, 'fish/config.fish'), - [ - 'set -g fish_greeting ""', - `function fish_prompt; printf '${PROMPT_MARK}'; end`, - 'function fish_right_prompt; end', - '' - ].join('\n') - ) - const childScript = path.join(configHome, 'read-stdin.mjs') - writeFileSync( - childScript, - "let buffered = ''\n" + - "process.stdin.on('data', (d) => {\n" + - " buffered += d.toString('utf8')\n" + - " if (!buffered.includes('\\n')) return\n" + - " process.stdout.write('CHILD-READ:' + JSON.stringify(buffered) + '\\n')\n" + - ' process.exit(0)\n' + - '})\n' - ) - - const nodePty = await import('node-pty') - const term = nodePty.spawn(FISH.path as string, ['-l', '-i'], { - name: 'xterm-256color', - cols: 120, - rows: 30, - cwd: configHome, - env: { - PATH: process.env.PATH ?? '/usr/bin:/bin', - HOME: configHome, - TERM: 'xterm-256color', - COLORTERM: 'truecolor', - LANG: 'en_US.UTF-8', - XDG_CONFIG_HOME: configHome, - XDG_DATA_HOME: path.join(configHome, 'data'), - ORCA_NODE_BIN: process.execPath, - ORCA_CHILD_SCRIPT: childScript - } - }) - - let rendered = '' - const ingress = new PtyStartupIngress({ - ownerBackend: 'posix-pty', - write: (data) => term.write(data), - onEmission: (emission) => { - rendered += emission.data - answerQueriesInOrder(emission.data) - } - }) - - // The host gate: cooked-echo-risk replies are written by the ingress with their - // echo shapes armed; DA1/CPR stay on the host's own path, in call order. - const hostWrite = (data: string): void => { - if (ingress.answerLiveQueryReply(data)) { - return - } - term.write(data) - } - - let tail = '' - let oscQueryCount = 0 - function answerQueriesInOrder(chunk: string): void { - let buffer = tail + chunk - tail = '' - let index = 0 - while (index < buffer.length) { - const at = buffer.indexOf('\x1b', index) - if (at === -1) { - return - } - const rest = buffer.slice(at) - const grammar = QUERY_GRAMMARS.map((candidate) => ({ - candidate, - match: candidate.re.exec(rest) - })).find((entry) => entry.match) - if (grammar?.match) { - if (grammar.candidate === QUERY_GRAMMARS[0]) { - oscQueryCount += 1 - } - hostWrite(grammar.candidate.reply(grammar.match)) - index = at + grammar.match[0].length - continue - } - if (PARTIAL_QUERY_RE.test(rest)) { - tail = rest - return - } - index = at + 1 - } - } - - let exited = false - term.onExit(() => { - exited = true - }) - term.onData((data) => ingress.accept(data)) - - try { - expect(await waitUntil(() => rendered.includes(PROMPT_MARK), 15_000)).toBe(true) - await sleep(500) - const oscQueriesBeforeHandoff = oscQueryCount - - // Type-ahead is the deterministic shape: queue the child's command while an - // external command still owns the tty, so fish repaints its prompt (re-querying - // OSC 11) and hands the tty over in the same breath. - term.write('sleep 0.4\r') - await sleep(150) - term.write('"$ORCA_NODE_BIN" "$ORCA_CHILD_SCRIPT"\r') - await sleep(1_500) - expect(oscQueryCount).toBeGreaterThan(oscQueriesBeforeHandoff) - - const renderedBeforeChildInput = rendered.length - term.write('hello\r') - expect( - await waitUntil( - () => rendered.slice(renderedBeforeChildInput).includes('CHILD-READ:'), - 10_000 - ) - ).toBe(true) - - const childRead = - rendered.slice(renderedBeforeChildInput).match(/CHILD-READ:[^\r\n]*/)?.[0] ?? '' - // The merge-blocking assertion: the child's first LINE is what the user typed, - // with no escape byte in front of it. Pre-fix this reads - // `\u001b]11;rgb:1e1e/1e1e/1e1e\u001b\\hello\n`. - expect(childRead).toBe('CHILD-READ:"hello\\n"') - } finally { - term.write('exit\r') - await waitUntil(() => exited, 3_000) - try { - term.kill() - } catch { - // already gone - } - } - }, - 45_000 - ) -}) diff --git a/src/shared/node-pty-spawn-helper.test.ts b/src/shared/node-pty-spawn-helper.test.ts deleted file mode 100644 index 4562c2bc9b7b..000000000000 --- a/src/shared/node-pty-spawn-helper.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { usesNodePtySpawnHelper } from './node-pty-spawn-helper' - -describe('usesNodePtySpawnHelper', () => { - it('is macOS only', () => { - // The predicate this file exists for: node-pty's binding.gyp declares the - // spawn-helper target inside OS=="mac". Reading it as "every non-Windows platform" - // is what reported spawn_helper_missing on healthy Linux hosts (#17844). - expect(usesNodePtySpawnHelper('darwin')).toBe(true) - for (const platform of ['linux', 'win32', 'freebsd', 'openbsd', 'sunos', 'aix']) { - expect(usesNodePtySpawnHelper(platform)).toBe(false) - } - }) -}) diff --git a/src/shared/node-pty-spawn-helper.ts b/src/shared/node-pty-spawn-helper.ts deleted file mode 100644 index c3e40df101a8..000000000000 --- a/src/shared/node-pty-spawn-helper.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Whether node-pty execs its `spawn-helper` binary on a platform. - * - * Only macOS: binding.gyp declares the `spawn-helper` target inside `OS=="mac"`, and - * `src/unix/pty.cc` reads the helper path only under `#if defined(__APPLE__)`. Every - * other platform forks directly, so requiring the helper there calls a working host - * broken. - */ -export function usesNodePtySpawnHelper(platform: string): boolean { - return platform === 'darwin' -} diff --git a/src/shared/orcad-artifacts.test.ts b/src/shared/orcad-artifacts.test.ts index 61d57f385516..638885750b86 100644 --- a/src/shared/orcad-artifacts.test.ts +++ b/src/shared/orcad-artifacts.test.ts @@ -3,12 +3,34 @@ import { join } from 'node:path' import { describe, expect, it } from 'vitest' import { BUNDLED_RIPGREP_PLATFORMS, bundledRipgrepBinaryName } from './bundled-ripgrep' import { + ORCAD_ARTIFACTS, + ORCAD_WINDOWS_CONPTY_ARTIFACTS, ORCAD_RIPGREP_ARTIFACTS, ORCAD_RIPGREP_LICENSE_ARTIFACTS, orcadArtifactFilenames } from './orcad-artifacts' describe('standalone runtime artifacts', () => { + it.each(['win32', 'win32-x64', 'win32-arm64'])( + 'requires the full ConPTY closure on %s', + (target) => { + expect(orcadArtifactFilenames(target)).toEqual( + expect.arrayContaining([...ORCAD_WINDOWS_CONPTY_ARTIFACTS]) + ) + } + ) + + it.each(['', 'linux-x64-glibc', 'linux-arm64-musl', 'darwin-arm64'])( + 'preserves the POSIX artifact sequence for %s', + (target) => { + expect(orcadArtifactFilenames(target)).toEqual( + ORCAD_ARTIFACTS.filter((artifact) => !artifact.optional).map( + (artifact) => artifact.filename + ) + ) + } + ) + it('ships search binaries for every SSH host and includes them in the install identity', () => { const expected = BUNDLED_RIPGREP_PLATFORMS.map( (platform) => `ripgrep/${platform}/${bundledRipgrepBinaryName(platform)}` diff --git a/src/shared/orcad-artifacts.ts b/src/shared/orcad-artifacts.ts index 98688026a159..46949d240a85 100644 --- a/src/shared/orcad-artifacts.ts +++ b/src/shared/orcad-artifacts.ts @@ -11,6 +11,13 @@ export const ORCAD_BUN_RUNTIME_FILENAME = 'bun-runtime' export const ORCAD_WINDOWS_BUN_RUNTIME_FILENAME = 'bun-runtime.exe' export const ORCAD_WINDOWS_PROCESS_TREE_FILENAME = 'windows-process-tree.node' +export const ORCAD_WINDOWS_CONPTY_ARTIFACTS = [ + 'conpty/conpty.dll', + 'conpty/OpenConsole.exe', + 'conpty/LICENSE.txt', + 'conpty/Microsoft.Windows.Console.ConPTY.nuspec', + 'conpty/conpty.json' +] as const export function orcadBunRuntimeFilename(target: string): string { return target === 'win32' || target.startsWith('win32-') @@ -106,7 +113,7 @@ export function orcadArtifactFilenames(target = ''): string[] { : artifact.filename ) if (target === 'win32' || target.startsWith('win32-')) { - filenames.push(ORCAD_WINDOWS_PROCESS_TREE_FILENAME) + filenames.push(ORCAD_WINDOWS_PROCESS_TREE_FILENAME, ...ORCAD_WINDOWS_CONPTY_ARTIFACTS) } return filenames } diff --git a/src/shared/orcad-bun-runtime.ts b/src/shared/orcad-bun-runtime.ts index 76fc03258c86..385303db1a02 100644 --- a/src/shared/orcad-bun-runtime.ts +++ b/src/shared/orcad-bun-runtime.ts @@ -1,5 +1,16 @@ export const ORCAD_BUN_VERSION = '1.4.2' +// Custom builds may add a CLI banner suffix without changing their runtime API version. +export function orcadBunVersionProbeArgs(platform: NodeJS.Platform = process.platform): string[] { + return [ + '--no-env-file', + platform === 'win32' ? '--config=NUL' : '--config=/dev/null', + '--no-install', + '--print', + 'process.versions.bun' + ] +} + export const ORCAD_BUN_TARGETS = [ 'darwin-arm64', 'darwin-x64', diff --git a/src/shared/orcad-profile-preflight.ts b/src/shared/orcad-profile-preflight.ts index 2541a09ebdec..cb34d68cba34 100644 --- a/src/shared/orcad-profile-preflight.ts +++ b/src/shared/orcad-profile-preflight.ts @@ -1,6 +1,7 @@ import { z } from 'zod' export const ORCAD_PROFILE_PREFLIGHT_FLAG = '--orcad-profile-state-preflight' +export const ORCAD_NATIVE_PREFLIGHT_CHILD_FLAG = '--orcad-native-profile-preflight-child' export const ORCAD_STARTUP_PREFLIGHT_FLAG = '--orcad-startup-preflight' export const ORCAD_PROFILE_PREFLIGHT_TIMEOUT_MS = 90_000 // Server startup follows the disposable native/SQLite probe on every bundled launch. diff --git a/src/shared/pane-agent-identity-inventory.test.ts b/src/shared/pane-agent-identity-inventory.test.ts index 8cb5a88f702c..f7ef3889fd6b 100644 --- a/src/shared/pane-agent-identity-inventory.test.ts +++ b/src/shared/pane-agent-identity-inventory.test.ts @@ -406,7 +406,7 @@ const DIRECT_SINGLE_SOURCE_SURFACES: readonly { marker: 'isCursorAgentTitle' }, { - path: 'src/main/providers/local-pty-session-activation.ts', + path: 'src/main/daemon/pty-subprocess.ts', classification: 'action-consumer', marker: 'launchAgent' }, diff --git a/src/shared/persisted-state-types.ts b/src/shared/persisted-state-types.ts index a081eee76ab3..8c819b11c7ea 100644 --- a/src/shared/persisted-state-types.ts +++ b/src/shared/persisted-state-types.ts @@ -1,3 +1,4 @@ +import type { WslPtyRecoveryRecord } from './wsl-pty-consumer-recovery' import type { ExecutionHostId } from './execution-host' import type { RemovedSshTargetTombstone, @@ -103,6 +104,7 @@ export type PersistedState = { sshRemotePtyLeases: SshRemotePtyLease[] /** Main-owned authenticated relay recovery records; never expose through renderer settings APIs. */ sshPtyConsumerRecoveries?: SshPtyConsumerRecovery[] + wslPtyConsumerRecoveries?: WslPtyRecoveryRecord[] /** Live local Claude daemon session ids; seeds the live-PTY gate so early OAuth refresh can't rotate the single-use refresh token out from under a running daemon. */ claudeLivePtySessionIds?: string[] migrationUnsupportedPtyEntries: MigrationUnsupportedPtyEntry[] diff --git a/src/shared/terminal-process.ts b/src/shared/terminal-process.ts new file mode 100644 index 000000000000..3d290959b9ea --- /dev/null +++ b/src/shared/terminal-process.ts @@ -0,0 +1,17 @@ +export type TerminalProcessExit = { exitCode: number; signal?: number } + +/** The terminal engine contract consumed by the daemon and relay. */ +export type TerminalProcess = { + readonly pid: number + readonly cols: number + readonly rows: number + readonly process: string + onData(listener: (data: string) => void): { dispose(): void } + onExit(listener: (event: TerminalProcessExit) => void): { dispose(): void } + write(data: string | Buffer): void + resize(cols: number, rows: number): void + clear(): void + kill(signal?: string): void + pause(): void + resume(): void +} diff --git a/src/shared/terminal-unicode-provider.test.ts b/src/shared/terminal-unicode-provider.test.ts new file mode 100644 index 000000000000..251553d850d2 --- /dev/null +++ b/src/shared/terminal-unicode-provider.test.ts @@ -0,0 +1,83 @@ +import { readFileSync } from 'node:fs' +import { join } from 'node:path' +import { Terminal } from '@xterm/headless' +import { Unicode11Addon } from '@xterm/addon-unicode11' +import type { IUnicodeVersionProvider } from '@xterm/xterm' +import { afterEach, expect, it, vi } from 'vitest' +import { activateOrcaTerminalUnicodeProvider } from './terminal-unicode-provider' + +const terminals: Terminal[] = [] +afterEach(() => { + vi.unstubAllGlobals() + terminals.splice(0).forEach((terminal) => terminal.dispose()) +}) + +function provider(bun: boolean): IUnicodeVersionProvider { + vi.stubGlobal('process', { + ...process, + versions: { ...process.versions, bun: bun ? '1.4.2' : undefined } + }) + const terminal = new Terminal({ allowProposedApi: true }) + terminals.push(terminal) + terminal.loadAddon(new Unicode11Addon()) + const unicode = terminal.unicode + const register = vi.spyOn(unicode, 'register') + const unicodeCore: { + unicode: typeof unicode + _core?: { unicodeService?: { _providers?: Record } } + } = terminal + activateOrcaTerminalUnicodeProvider({ unicode, _core: unicodeCore._core }) + const registered = register.mock.calls[0]?.[0] + if (!registered) { + throw new Error('Unicode provider was not registered') + } + return registered +} + +it('preserves Unicode11 and ZWJ properties across the Bun ASCII specialization', () => { + const original = provider(false) + const optimized = provider(true) + const preceding = new Set([0, 1, 2, 3, 4, 5]) + for (const codepoint of [0x200d, 0x301, 0x1f469, 0x1f4bb, 0xac00, 0xfe0f]) { + // Snapshot the states because this loop extends the set. + for (const state of Array.from(preceding)) { + preceding.add(original.charProperties(codepoint, state)) + } + } + for (const state of preceding) { + for (let codepoint = 0; codepoint <= 0xff; codepoint++) { + expect(optimized.charProperties(codepoint, state)).toBe( + original.charProperties(codepoint, state) + ) + } + } + for (const text of ['a\u200db', '👩‍💻 ascii', '한글 abc', 'a\u0301b', '🏳️‍🌈 x']) { + let originalState = 0 + let optimizedState = 0 + for (const char of text) { + const codepoint = char.codePointAt(0) + if (codepoint === undefined) { + throw new Error('Missing codepoint') + } + originalState = original.charProperties(codepoint, originalState) + optimizedState = optimized.charProperties(codepoint, optimizedState) + expect(optimizedState).toBe(originalState) + } + } +}) + +it('pins the headless fast-path identity and ASCII invariant for both providers', () => { + const source = readFileSync( + join(__dirname, '../../config/patches/xterm-src/@xterm__headless@6.1.0-beta.302.src.patch'), + 'utf8' + ) + const literal = /activeVersion === '([^']+)'/.exec(source)?.[1] + expect(literal).toBeDefined() + for (const bun of [false, true]) { + const active = provider(bun) + expect(active.version).toBe(literal) + for (let codepoint = 0x20; codepoint < 0x7f; codepoint++) { + expect(active.charProperties(codepoint, 2)).toBe(2) + } + } +}) diff --git a/src/shared/terminal-unicode-provider.ts b/src/shared/terminal-unicode-provider.ts index ecd0fe7e7823..178e0ba9e363 100644 --- a/src/shared/terminal-unicode-provider.ts +++ b/src/shared/terminal-unicode-provider.ts @@ -9,6 +9,7 @@ type XtermTerminalWithUnicodeCore = { } } +// The headless ASCII source patch relies on this identity and charProperties(ASCII, 2) === 2. const ORCA_UNICODE_VERSION = 'orca-11-zwj' const UNICODE11_VERSION = '11' const ZERO_WIDTH_JOINER = 0x200d @@ -52,6 +53,16 @@ class OrcaUnicodeProvider implements IUnicodeVersionProvider { } } +class BunOrcaUnicodeProvider extends OrcaUnicodeProvider { + public override charProperties(codepoint: number, preceding: number): number { + // Unicode11 printable ASCII after an ordinary width-one cell has unchanged properties. + if (preceding === 2 && codepoint >= 0x20 && codepoint < 0x7f) { + return 2 + } + return super.charProperties(codepoint, preceding) + } +} + export function activateOrcaTerminalUnicodeProvider(terminal: XtermTerminalWithUnicodeCore): void { const { unicode } = terminal if (unicode.activeVersion === ORCA_UNICODE_VERSION) { @@ -65,7 +76,12 @@ export function activateOrcaTerminalUnicodeProvider(terminal: XtermTerminalWithU } if (!unicode.versions.includes(ORCA_UNICODE_VERSION)) { - unicode.register(new OrcaUnicodeProvider(baseProvider)) + // Keep V8's measured fast path unchanged; JSC benefits from bypassing the ASCII call chain. + const Provider = + typeof process !== 'undefined' && process.versions?.bun + ? BunOrcaUnicodeProvider + : OrcaUnicodeProvider + unicode.register(new Provider(baseProvider)) } unicode.activeVersion = ORCA_UNICODE_VERSION } diff --git a/src/shared/tui-agent-config.ts b/src/shared/tui-agent-config.ts index 55082bc24704..6c930e8f1358 100644 --- a/src/shared/tui-agent-config.ts +++ b/src/shared/tui-agent-config.ts @@ -135,7 +135,7 @@ const TUI_AGENT_CONFIG_SOURCE: Record = { // Why: opencode enables bracketed paste before its composer mounts; wait for the post-\x1b[?2004h show-cursor so paste lands. draftPasteReadySignal: 'render-cursor-after-bracketed-paste', // Why 20s: measured on two Windows hosts (ConPTY dll backend, as pinned by - // local-pty-utils), opencode does not enable bracketed paste until ~4.8s and its + // pty-spawn-validation), opencode does not enable bracketed paste until ~4.8s and its // composer is not ready until ~10s — so the 8s default expired first and the draft // was pasted blind, mid-startup (#22479). The signal itself fired every time in // those runs, so the budget was the problem, not a dropped escape. diff --git a/src/shared/windows-conpty-release.ts b/src/shared/windows-conpty-release.ts new file mode 100644 index 000000000000..7e807cf21a35 --- /dev/null +++ b/src/shared/windows-conpty-release.ts @@ -0,0 +1,18 @@ +/** Microsoft Terminal v1.23.12811.0 assets, identical to the qualified ConPTY provider. */ +export const WINDOWS_CONPTY_VERSION = '1.23.251008001' +export const WINDOWS_CONPTY_ARCHIVE = { + url: 'https://github.com/microsoft/terminal/releases/download/v1.23.12811.0/Microsoft.Windows.Console.ConPTY.1.23.251008001.nupkg', + sha256: 'f7e142a99f5dfee573ace98676b3c7c2fcdb870bc90b238086a4d80a36fcf099', + nuspecSha256: '845e41cf307cf71a59c25a7555fa05a91087930048f8a7918dc614f1255241af' +} as const + +export const WINDOWS_CONPTY_FILES = { + x64: { + 'conpty.dll': '7c7430632052ff703540b68371ec43821820aa1335d8e11dfbcd9ff00e9daaed', + 'OpenConsole.exe': 'd1fe7faa62f9e955e2ac2371f95d7e5513df4d496255097158f979c94782c5fc' + }, + arm64: { + 'conpty.dll': 'b6cca5f1081111f59e5255eec043d924ebd11a86d09986c0d121b680540b3380', + 'OpenConsole.exe': 'b7763ba27e80d5e84f9e60d1368ec2ddb25dfd10283336c0c9f3f252d35cb2f8' + } +} as const diff --git a/src/shared/wsl-daemon-recovery.ts b/src/shared/wsl-daemon-recovery.ts new file mode 100644 index 000000000000..07b51d75ca93 --- /dev/null +++ b/src/shared/wsl-daemon-recovery.ts @@ -0,0 +1,81 @@ +import { z } from 'zod' + +const identity = z + .string() + .min(1) + .max(512) + .refine((value) => value.trim() === value && !/[\0\r\n]/.test(value)) +const guestPath = identity.refine( + (value) => + value.startsWith('/') && + !value.includes('\\') && + !value + .split('/') + .slice(1) + .some((part) => part === '.' || part === '..' || part === '') +) +const endpoint = z + .object({ + distro: identity, + distributionId: identity, + userName: identity, + userId: z.string().regex(/^\d+$/), + home: guestPath, + runtime: guestPath, + entry: guestPath, + envBinary: guestPath, + socket: guestPath, + tokenPath: guestPath, + serverBuildId: identity, + protocolVersion: z.number().int().positive().safe().optional() + }) + .strict() +export const wslDaemonIncarnationSchema = z + .object({ + pid: z.number().int().positive().safe(), + startedAtMs: z.number().positive().finite(), + launchNonce: identity, + linuxStartTicks: z.string().regex(/^\d+$/), + bootId: identity + }) + .strict() +export type WslDaemonIncarnation = z.infer + +const recovery = z + .object({ + kind: z.literal('daemon'), + distro: identity, + relayBuildId: identity, + endpoint, + incarnation: wslDaemonIncarnationSchema.optional() + }) + .strict() + .refine((value) => value.distro === value.endpoint.distro) + +export type WslDaemonRecovery = z.infer +export type PersistedWslDaemonEndpoint = z.infer + +/** Only endpoint identity is persisted; authentication remains in the guest-private token file. */ +export function normalizeWslDaemonRecovery(value: unknown): WslDaemonRecovery | null { + const parsed = recovery.safeParse(value) + return parsed.success ? parsed.data : null +} + +// Guest-owned terminals first shipped at v36; absent metadata has a fixed meaning. +export const INITIAL_WSL_DAEMON_PROTOCOL_VERSION = 36 + +export function sameWslDaemonEndpoint( + left: PersistedWslDaemonEndpoint, + right: PersistedWslDaemonEndpoint +): boolean { + return ( + JSON.stringify({ + ...left, + protocolVersion: left.protocolVersion ?? INITIAL_WSL_DAEMON_PROTOCOL_VERSION + }) === + JSON.stringify({ + ...right, + protocolVersion: right.protocolVersion ?? INITIAL_WSL_DAEMON_PROTOCOL_VERSION + }) + ) +} diff --git a/src/shared/wsl-login-shell-command.ts b/src/shared/wsl-login-shell-command.ts index fd0f0b505c98..2cc131fc3565 100644 --- a/src/shared/wsl-login-shell-command.ts +++ b/src/shared/wsl-login-shell-command.ts @@ -14,9 +14,15 @@ export function quotePosixShell(value: string): string { */ export function buildWslExecArgs( distro: string | undefined, - shellArgs: readonly string[] + shellArgs: readonly string[], + user?: string ): string[] { - return [...(distro ? ['-d', distro] : []), '--exec', ...shellArgs] + return [ + ...(distro ? ['-d', distro] : []), + ...(user ? ['--user', user] : []), + '--exec', + ...shellArgs + ] } export function buildWslLoginShellCommand(command: string): string { diff --git a/src/shared/wsl-pty-consumer-recovery.ts b/src/shared/wsl-pty-consumer-recovery.ts new file mode 100644 index 000000000000..c06609c1335b --- /dev/null +++ b/src/shared/wsl-pty-consumer-recovery.ts @@ -0,0 +1,67 @@ +import type { WslDaemonRecovery } from './wsl-daemon-recovery' +import { z } from 'zod' +import type { WslPtyOwner } from './wsl-pty-id' + +const identity = z + .string() + .min(1) + .max(512) + .refine((value) => !/[\0\r\n]/.test(value)) +const guestPath = identity.refine((value) => value.startsWith('/')) +const endpointSchema = z.object({ + distro: identity, + distributionId: identity, + userId: z.string().regex(/^\d+$/), + userName: identity, + serverBuildId: identity, + envBinary: guestPath, + runtime: guestPath, + entry: guestPath, + socket: guestPath, + credentialFile: guestPath +}) + +const recoverySchema = z.object({ + distro: identity, + relayBuildId: identity, + clientInstanceId: identity, + clientGeneration: z.number().int().positive().max(Number.MAX_SAFE_INTEGER), + ownerGeneration: z.number().int().positive().max(Number.MAX_SAFE_INTEGER), + ownerLease: z.string().min(1).max(4096), + endpoint: endpointSchema.optional(), + outputFlowControl: z + .object({ + version: z.literal(1), + windowSu: z.number().int().positive().max(Number.MAX_SAFE_INTEGER) + }) + .optional() +}) + +export type PersistedWslRelayEndpoint = z.infer + +export function normalizePersistedWslRelayEndpoint( + value: unknown +): PersistedWslRelayEndpoint | null { + const parsed = endpointSchema.safeParse(value) + return parsed.success ? parsed.data : null +} + +export type WslPtyConsumerRecovery = z.infer + +export function normalizeWslPtyConsumerRecovery( + value: unknown, + encrypted = false +): WslPtyConsumerRecovery | null { + const parsed = recoverySchema.safeParse(value) + return parsed.success && (encrypted || parsed.data.ownerLease.length <= 512) ? parsed.data : null +} + +export function wslPtyOwnerKey(owner: WslPtyOwner): string { + return `${encodeURIComponent(owner.distro)}@@${encodeURIComponent(owner.relayBuildId)}` +} + +export type WslPtyRecoveryRecord = WslPtyConsumerRecovery | WslDaemonRecovery + +export function isWslDaemonRecovery(record: WslPtyRecoveryRecord): record is WslDaemonRecovery { + return 'kind' in record && record.kind === 'daemon' +} diff --git a/src/shared/wsl-pty-id.test.ts b/src/shared/wsl-pty-id.test.ts new file mode 100644 index 000000000000..bf9131698ab8 --- /dev/null +++ b/src/shared/wsl-pty-id.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from 'vitest' +import { parseAppWslPtyId, toAppWslPtyId, toRelayWslPtyId } from './wsl-pty-id' +import { parseAppSshPtyId } from './ssh-pty-id' + +const owner = { distro: 'Ubuntu @ home', relayBuildId: '0.1.0+abc' } + +describe('WSL terminal owner identity', () => { + it('round trips distro, exact build and opaque guest terminal without an SSH identity', () => { + const id = toAppWslPtyId(owner, 'pty2:epoch:1') + expect(parseAppWslPtyId(id)).toEqual({ ...owner, relayPtyId: 'pty2:epoch:1' }) + expect(toRelayWslPtyId(owner, id)).toBe('pty2:epoch:1') + expect(toAppWslPtyId(owner, id)).toBe(id) + expect(parseAppSshPtyId(id)).toBeNull() + }) + + it('fences both a different distro and a replaced relay build', () => { + const id = toAppWslPtyId(owner, 'pty2:epoch:1') + for (const other of [ + { ...owner, distro: 'Debian' }, + { ...owner, relayBuildId: 'new-build' } + ]) { + expect(() => toRelayWslPtyId(other, id)).toThrow('different distro or relay build') + expect(() => toAppWslPtyId(other, id)).toThrow('different distro or relay build') + } + }) + + it.each([ + 'daemon:legacy', + 'ssh:host@@pty-1', + 'wsl:broken', + 'wsl:%XX@@build@@id', + 'wsl:Ubuntu@@build@@' + ])('never interprets foreign or malformed IDs as guest-owned: %s', (id) => { + expect(parseAppWslPtyId(id)).toBeNull() + expect(() => toRelayWslPtyId(owner, id)).toThrow() + }) + + it('encodes separators inside owner names rather than changing the owner', () => { + const special = { distro: 'Ubuntu@@other', relayBuildId: 'build@@other' } + expect(parseAppWslPtyId(toAppWslPtyId(special, 'pty@@opaque'))).toEqual({ + ...special, + relayPtyId: 'pty@@opaque' + }) + }) +}) diff --git a/src/shared/wsl-pty-id.ts b/src/shared/wsl-pty-id.ts new file mode 100644 index 000000000000..e407f2b425d6 --- /dev/null +++ b/src/shared/wsl-pty-id.ts @@ -0,0 +1,64 @@ +const PREFIX = 'wsl:' +const SEPARATOR = '@@' + +export type WslPtyOwner = { + distro: string + relayBuildId: string +} + +export type ParsedWslPtyId = WslPtyOwner & { relayPtyId: string } + +function validPart(value: string): boolean { + return value.trim().length > 0 && !/[\0\r\n]/.test(value) +} + +export function parseAppWslPtyId(id: string): ParsedWslPtyId | null { + if (!id.startsWith(PREFIX)) { + return null + } + const first = id.indexOf(SEPARATOR, PREFIX.length) + const second = id.indexOf(SEPARATOR, first + SEPARATOR.length) + if (first === -1 || second === -1) { + return null + } + try { + const distro = decodeURIComponent(id.slice(PREFIX.length, first)) + const relayBuildId = decodeURIComponent(id.slice(first + SEPARATOR.length, second)) + const relayPtyId = id.slice(second + SEPARATOR.length) + return validPart(distro) && validPart(relayBuildId) && validPart(relayPtyId) + ? { distro, relayBuildId, relayPtyId } + : null + } catch { + return null + } +} + +function assertOwner(owner: WslPtyOwner, parsed: ParsedWslPtyId): void { + if (parsed.distro !== owner.distro || parsed.relayBuildId !== owner.relayBuildId) { + throw new Error('WSL terminal belongs to a different distro or relay build') + } +} + +export function toAppWslPtyId(owner: WslPtyOwner, relayPtyId: string): string { + if (!validPart(owner.distro) || !validPart(owner.relayBuildId) || !validPart(relayPtyId)) { + throw new Error('Invalid WSL terminal identity') + } + const parsed = parseAppWslPtyId(relayPtyId) + if (parsed) { + assertOwner(owner, parsed) + return relayPtyId + } + if (relayPtyId.startsWith(PREFIX) || relayPtyId.startsWith('ssh:')) { + throw new Error('Expected a guest-local terminal identity') + } + return `${PREFIX}${encodeURIComponent(owner.distro)}${SEPARATOR}${encodeURIComponent(owner.relayBuildId)}${SEPARATOR}${relayPtyId}` +} + +export function toRelayWslPtyId(owner: WslPtyOwner, id: string): string { + const parsed = parseAppWslPtyId(id) + if (!parsed) { + throw new Error('Expected a WSL-owned terminal identity') + } + assertOwner(owner, parsed) + return parsed.relayPtyId +} diff --git a/tests/e2e/capture-opencode-tui-repro.mjs b/tests/e2e/capture-opencode-tui-repro.mjs index fb4b3969209f..aad4036a4cf3 100644 --- a/tests/e2e/capture-opencode-tui-repro.mjs +++ b/tests/e2e/capture-opencode-tui-repro.mjs @@ -4,15 +4,20 @@ // node tests/e2e/capture-opencode-tui-repro.mjs // Then run the captured replay test documented in terminal-foreground-redraw-freeze.spec.ts. -import { createRequire } from 'node:module' import { createWriteStream, existsSync, mkdirSync, writeFileSync } from 'node:fs' import path from 'node:path' -const require = createRequire(import.meta.url) -const pty = require('node-pty') +import { + relaunchTranscriptWithBun, + spawnTranscriptPty +} from '../../config/scripts/pty-transcript-bun-runtime.mjs' const scriptDir = import.meta.dirname const repoRoot = path.resolve(scriptDir, '..', '..') +const relaunched = await relaunchTranscriptWithBun(repoRoot, process.argv.slice(2), process.argv[1]) +if (relaunched !== null) { + process.exit(relaunched) +} function readOption(name, fallback) { const index = process.argv.indexOf(name) @@ -121,22 +126,21 @@ mkdirSync(path.dirname(outputPath), { recursive: true }) writeFileSync(harnessPath, harnessSource) const out = createWriteStream(outputPath) -const command = process.platform === 'win32' ? 'bun.exe' : 'bun' -const child = pty.spawn(command, ['run', './orca-opencode-tui-repro.tsx'], { - cwd: opencodePackagePath, - cols: 120, - rows: 40, - name: 'xterm-256color', - env: { - ...process.env, - FORCE_COLOR: '1', - TERM: 'xterm-256color' - } -}) - -child.onData((data) => { - out.write(data) -}) +const child = await spawnTranscriptPty( + { + file: process.execPath, + args: ['run', './orca-opencode-tui-repro.tsx'], + cwd: opencodePackagePath, + cols: 120, + rows: 40, + env: { + ...process.env, + FORCE_COLOR: '1', + TERM: 'xterm-256color' + } + }, + (bytes) => out.write(bytes) +) child.onExit(({ exitCode }) => { out.end(() => { diff --git a/tests/e2e/daemon-generation-bun-runtime.unit.test.ts b/tests/e2e/daemon-generation-bun-runtime.unit.test.ts new file mode 100644 index 000000000000..4237a85ce6ce --- /dev/null +++ b/tests/e2e/daemon-generation-bun-runtime.unit.test.ts @@ -0,0 +1,49 @@ +import { expect, it } from 'vitest' +import { join } from 'node:path' +import { PROTOCOL_VERSION } from '../../src/main/daemon/types' +import { + createDaemonGenerationRuntime, + launchDaemonGeneration, + spawnGenerationCanary, + pingGenerationCanary, + cleanupDaemonGenerationFixtures, + type DaemonGeneration, + type GenerationCanary +} from './helpers/daemon-generation-safety-fixtures' +import { processIdentityIsAlive } from './helpers/daemon-generation-processes' + +it.skipIf(process.env.ORCA_DAEMON_GENERATION_SMOKE !== '1')( + 'runs current and legacy-protocol real PTY canaries under verified Bun without Electron UI', + async () => { + const runtime = await createDaemonGenerationRuntime({ + outputDir: join(process.cwd(), '.build', 'daemon-generation-bun-smoke'), + outputPath: (...parts: string[]) => + join(process.cwd(), '.build', 'daemon-generation-bun-smoke', ...parts) + }) + const generations: DaemonGeneration[] = [] + const canaries: GenerationCanary[] = [] + try { + for (const protocolVersion of [PROTOCOL_VERSION, PROTOCOL_VERSION - 1]) { + const generation = await launchDaemonGeneration({ + runtime, + label: `protocol-${protocolVersion}`, + protocolVersion + }) + generations.push(generation) + const canary = await spawnGenerationCanary({ runtime, generation, role: 'live' }) + canaries.push(canary) + await pingGenerationCanary(canary, 'first') + } + for (const canary of canaries) { + expect(await processIdentityIsAlive(canary.rootIdentity)).toBe(true) + expect(await processIdentityIsAlive(canary.descendantIdentity)).toBe(true) + await pingGenerationCanary(canary, 'still-live') + } + } finally { + runtime.retainDiagnostics(generations) + await cleanupDaemonGenerationFixtures({ generations, canaries }) + runtime.remove() + } + }, + 90_000 +) diff --git a/tests/e2e/daemon-generation-legacy-close-safety.spec.ts b/tests/e2e/daemon-generation-legacy-close-safety.spec.ts index c46cdb6d2792..609ea8fe9f32 100644 --- a/tests/e2e/daemon-generation-legacy-close-safety.spec.ts +++ b/tests/e2e/daemon-generation-legacy-close-safety.spec.ts @@ -1,4 +1,4 @@ -import { fork, type ChildProcess } from 'node:child_process' +import { fork, type ChildProcess, type ForkOptions } from 'node:child_process' import { writeFileSync } from 'node:fs' import path from 'node:path' import { expect, test, type TestInfo } from '@playwright/test' @@ -97,18 +97,24 @@ function launchLegacyCloseClient(options: { })}\n` ) let output = '' - const child = fork(runtime.legacyCloseClientEntryPath, ['--config', configPath], { - cwd: runtime.userDataDir, - execPath: runtime.electronPath, - windowsHide: true, - env: { - ...process.env, - ELECTRON_RUN_AS_NODE: '1', - NODE_PATH: path.join(process.cwd(), 'node_modules'), - ORCA_USER_DATA_PATH: runtime.userDataDir - }, - stdio: ['ignore', 'ignore', 'pipe', 'ipc'] - }) + const child = fork( + runtime.legacyCloseClientEntryPath, + ['--config', configPath], + Object.assign( + { + cwd: runtime.userDataDir, + execPath: runtime.electronPath, + env: { + ...process.env, + ELECTRON_RUN_AS_NODE: '1', + NODE_PATH: path.join(process.cwd(), 'node_modules'), + ORCA_USER_DATA_PATH: runtime.userDataDir + }, + stdio: ['ignore', 'ignore', 'pipe', 'ipc'] + } satisfies ForkOptions, + { windowsHide: true } + ) + ) child.stderr?.on('data', (chunk: Buffer) => { output = `${output}${chunk.toString('utf8')}`.slice(-32_768) }) diff --git a/tests/e2e/daemon-generation-reconnect-safety.spec.ts b/tests/e2e/daemon-generation-reconnect-safety.spec.ts index 6e57865bbfa9..e39e5a213091 100644 --- a/tests/e2e/daemon-generation-reconnect-safety.spec.ts +++ b/tests/e2e/daemon-generation-reconnect-safety.spec.ts @@ -1,4 +1,4 @@ -import { fork, type ChildProcess } from 'node:child_process' +import { fork, type ChildProcess, type ForkOptions } from 'node:child_process' import { writeFileSync } from 'node:fs' import path from 'node:path' import { expect, test, type TestInfo } from '@playwright/test' @@ -131,18 +131,24 @@ function launchReconnectClient(options: { })}\n` ) let output = '' - const child = fork(runtime.reconnectClientEntryPath, ['--config', configPath], { - cwd: runtime.userDataDir, - execPath: runtime.electronPath, - windowsHide: true, - env: { - ...process.env, - ELECTRON_RUN_AS_NODE: '1', - NODE_PATH: path.join(process.cwd(), 'node_modules'), - ORCA_USER_DATA_PATH: runtime.userDataDir - }, - stdio: ['ignore', 'ignore', 'pipe', 'ipc'] - }) + const child = fork( + runtime.reconnectClientEntryPath, + ['--config', configPath], + Object.assign( + { + cwd: runtime.userDataDir, + execPath: runtime.electronPath, + env: { + ...process.env, + ELECTRON_RUN_AS_NODE: '1', + NODE_PATH: path.join(process.cwd(), 'node_modules'), + ORCA_USER_DATA_PATH: runtime.userDataDir + }, + stdio: ['ignore', 'ignore', 'pipe', 'ipc'] + } satisfies ForkOptions, + { windowsHide: true } + ) + ) child.stderr?.on('data', (chunk: Buffer) => { output = `${output}${chunk.toString('utf8')}`.slice(-32_768) }) diff --git a/tests/e2e/fixtures/daemon-generation-entry.ts b/tests/e2e/fixtures/daemon-generation-entry.ts index c3899e6f6ba3..f41451103831 100644 --- a/tests/e2e/fixtures/daemon-generation-entry.ts +++ b/tests/e2e/fixtures/daemon-generation-entry.ts @@ -1,4 +1,5 @@ import process from 'node:process' +import { ORCAD_BUN_VERSION } from '../../../src/shared/orcad-bun-runtime' import { startDaemon, type DaemonHandle } from '../../../src/main/daemon/daemon-main' import { createPtySubprocess } from '../../../src/main/daemon/pty-subprocess' import { createDaemonFileLog } from '../../../src/main/daemon/daemon-file-log' @@ -42,6 +43,9 @@ function parseFixtureArgs(argv: string[]): FixtureArgs { } async function main(): Promise { + if (process.versions.bun !== ORCAD_BUN_VERSION) { + throw new Error('Daemon-generation PTY fixtures require the pinned Bun runtime') + } const { protocolVersion, socketPath, tokenPath, logPath, refuseDispose } = parseFixtureArgs( process.argv.slice(2) ) diff --git a/tests/e2e/fixtures/daemon-generation-legacy-close-client.ts b/tests/e2e/fixtures/daemon-generation-legacy-close-client.ts index 730ebbeffc9a..64c580063875 100644 --- a/tests/e2e/fixtures/daemon-generation-legacy-close-client.ts +++ b/tests/e2e/fixtures/daemon-generation-legacy-close-client.ts @@ -158,7 +158,12 @@ async function main(): Promise { return true }, kill: () => false, - listProcesses: (options) => router.listProcesses(options), + listProcesses: (connectionId, options) => { + if (connectionId) { + throw new Error('Generation fixture only owns local terminals') + } + return router.listProcesses(options) + }, hasPty: (ptyId) => router.hasPty(ptyId), getForegroundProcess: (ptyId) => router.getForegroundProcess(ptyId) }) @@ -183,7 +188,7 @@ async function main(): Promise { } as never) runtime.attachWindow(1) - const snapshots: RuntimeMobileSessionTabsSnapshot[] = config.sessions.map((session, index) => { + const snapshots = config.sessions.map((session, index) => { const leafId = `00000000-0000-4000-8000-${String(index + 1).padStart(12, '0')}` return { worktree: session.worktreeId, @@ -203,7 +208,7 @@ async function main(): Promise { isActive: true } ] - } + } satisfies RuntimeMobileSessionTabsSnapshot }) runtime.syncWindowGraph(1, { tabs: snapshots.map((snapshot) => ({ diff --git a/tests/e2e/fixtures/daemon-generation-reconnect-client.ts b/tests/e2e/fixtures/daemon-generation-reconnect-client.ts index 6bd163cc474b..31bdbb80548a 100644 --- a/tests/e2e/fixtures/daemon-generation-reconnect-client.ts +++ b/tests/e2e/fixtures/daemon-generation-reconnect-client.ts @@ -159,7 +159,12 @@ function createRuntimeClosePath( pendingShutdowns.push(shutdown) return true }, - listProcesses: (options) => router.listProcesses(options), + listProcesses: (connectionId, options) => { + if (connectionId) { + throw new Error('Generation fixture only owns local terminals') + } + return router.listProcesses(options) + }, hasPty: (ptyId) => router.hasPty(ptyId), getForegroundProcess: (ptyId) => router.getForegroundProcess(ptyId) }) @@ -271,6 +276,9 @@ async function main(): Promise { } router = await connectThroughDesktopDiscovery(config, burst) } + if (!router) { + throw new Error('Reconnect fixture requires at least one discovery burst') + } await connectParallelRuntimeClients(config) const { dispatcher, targets, pendingShutdowns } = await createRuntimeClosePath(config, router) await dispatchFixtureCloseBursts({ dispatcher, worktreeId: WORKTREE_ID, targets }) diff --git a/tests/e2e/fixtures/daemon-lifecycle-entry.ts b/tests/e2e/fixtures/daemon-lifecycle-entry.ts index 0e09897b0c06..6cec9fec97d3 100644 --- a/tests/e2e/fixtures/daemon-lifecycle-entry.ts +++ b/tests/e2e/fixtures/daemon-lifecycle-entry.ts @@ -2,7 +2,7 @@ import process from 'node:process' import { writeFileSync } from 'node:fs' import { startDaemon, type DaemonHandle } from '../../../src/main/daemon/daemon-main' import { serializeDaemonPidFile } from '../../../src/main/daemon/daemon-spawner' -import type { SubprocessHandle } from '../../../src/main/daemon/session' +import type { SubprocessHandle } from '../../../src/main/daemon/session-subprocess-handle' type FixtureArgs = { protocolVersion: number @@ -62,6 +62,7 @@ function createFixtureSubprocess(): SubprocessHandle { resize: () => {}, kill: () => exit(0), forceKill: () => exit(137), + terminateOwnedTree: () => 'unavailable', signal: () => {}, onData: (callback) => { onData = callback diff --git a/tests/e2e/global-setup.ts b/tests/e2e/global-setup.ts index cd5d9b3c4e0b..91b42a9861a4 100644 --- a/tests/e2e/global-setup.ts +++ b/tests/e2e/global-setup.ts @@ -10,7 +10,7 @@ * temp file so the worker fixture can pick it up at runtime. */ -import { execSync } from 'node:child_process' +import { execFileSync, execSync } from 'node:child_process' import { randomUUID } from 'node:crypto' import { existsSync, mkdirSync, mkdtempSync, realpathSync, writeFileSync } from 'node:fs' import path from 'node:path' @@ -63,6 +63,37 @@ export default function globalSetup(): void { }) console.error('[e2e] CLI build complete.') } + const terminalEntry = path.join(root, 'out', 'terminal-daemon', 'daemon-entry.js') + const terminalGate = path.join(root, 'out', 'terminal-daemon', 'windows-bun-pty-gate-entry.js') + const runtimeDirectory = path.join( + root, + 'out', + 'cli-runtime', + `${process.platform}-${process.arch}` + ) + const runtimeManifest = path.join(runtimeDirectory, 'runtime.json') + const runtimeBinary = path.join( + runtimeDirectory, + process.platform === 'win32' ? 'bun-runtime.exe' : 'bun-runtime' + ) + if (!existsSync(runtimeManifest) || !existsSync(runtimeBinary)) { + execFileSync( + process.execPath, + ['config/scripts/build-cli-runtime.mjs', '--arch', process.arch], + { + cwd: root, + stdio: 'inherit', + timeout: CLI_E2E_BUILD_TIMEOUT_MS + } + ) + } + if (!process.env.SKIP_BUILD || !existsSync(terminalEntry) || !existsSync(terminalGate)) { + execSync('node config/scripts/build-terminal-daemon.mjs', { + cwd: root, + stdio: 'inherit', + timeout: CLI_E2E_BUILD_TIMEOUT_MS + }) + } if (process.env.ORCA_E2E_WEB_CLIENT === '1') { if (process.env.SKIP_BUILD && existsSync(outWeb)) { console.error('[e2e] SKIP_BUILD set and web client exists — skipping web build') diff --git a/tests/e2e/helpers/daemon-generation-runtime-fixture.ts b/tests/e2e/helpers/daemon-generation-runtime-fixture.ts index 88111024368d..ca9aecc2d24c 100644 --- a/tests/e2e/helpers/daemon-generation-runtime-fixture.ts +++ b/tests/e2e/helpers/daemon-generation-runtime-fixture.ts @@ -2,6 +2,10 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync import { homedir, tmpdir } from 'node:os' import path from 'node:path' import { build } from 'esbuild' +import { + resolveDaemonBunRuntime, + type DaemonBunRuntime +} from '../../../src/main/daemon/daemon-bun-runtime' import type { TestInfo } from '@playwright/test' const TEMP_PREFIX = 'orca-9749-dg-' @@ -24,6 +28,7 @@ export type DaemonGenerationRuntime = { legacyCloseClientEntryPath: string canaryPath: string electronPath: string + bunRuntime: DaemonBunRuntime retainDiagnostics(generations: readonly DiagnosticGeneration[]): void remove(): void } @@ -91,13 +96,13 @@ async function buildFixtureEntry(entryPoint: string, outfile: string): Promise ): Promise { const tempRoot = process.platform === 'darwin' ? path.join(path.sep, 'tmp') : tmpdir() const rootDir = mkdtempSync(path.join(tempRoot, TEMP_PREFIX)) @@ -105,7 +110,7 @@ export async function createDaemonGenerationRuntime( const userDataDir = path.join(rootDir, 'user-data') const daemonDir = path.join(userDataDir, 'daemon') mkdirSync(daemonDir, { recursive: true }) - const entryPath = path.join(rootDir, 'daemon-generation-entry.cjs') + const entryPath = path.join(rootDir, 'daemon-entry.js') const reconnectClientEntryPath = path.join(rootDir, 'daemon-generation-reconnect-client.cjs') const legacyCloseClientEntryPath = path.join(rootDir, 'daemon-generation-legacy-close-client.cjs') const repoRoot = process.cwd() @@ -121,7 +126,28 @@ export async function createDaemonGenerationRuntime( path.join(repoRoot, 'tests/e2e/fixtures/daemon-generation-legacy-close-client.ts'), legacyCloseClientEntryPath ) + await buildFixtureEntry( + path.join(repoRoot, 'src/main/daemon/pty-subprocess/windows-bun-pty-gate-entry.ts'), + path.join(rootDir, 'windows-bun-pty-gate-entry.js') + ) + const bunRuntime = await resolveDaemonBunRuntime({ + bundleDir: rootDir, + runtimeDir: path.join(repoRoot, 'out', 'cli-runtime', `${process.platform}-${process.arch}`), + platform: process.platform, + arch: process.arch, + ...(process.platform === 'win32' + ? { + windowsProcessTreeDir: path.join( + repoRoot, + 'node_modules', + '@vscode', + 'windows-process-tree' + ) + } + : {}) + }) return { + bunRuntime, rootDir, userDataDir, daemonDir, diff --git a/tests/e2e/helpers/daemon-generation-safety-fixtures.ts b/tests/e2e/helpers/daemon-generation-safety-fixtures.ts index 3bbaa94ed571..d49dbefb9139 100644 --- a/tests/e2e/helpers/daemon-generation-safety-fixtures.ts +++ b/tests/e2e/helpers/daemon-generation-safety-fixtures.ts @@ -1,4 +1,4 @@ -import { fork, type ChildProcess } from 'node:child_process' +import { fork, type ChildProcess, type ForkOptions } from 'node:child_process' import { randomUUID } from 'node:crypto' import { existsSync, readFileSync } from 'node:fs' import path from 'node:path' @@ -73,6 +73,26 @@ export async function launchDaemonGeneration(options: { const tokenPath = getDaemonTokenPath(runtime.daemonDir, protocolVersion) const logPath = path.join(runtime.rootDir, `${label}.daemon.log`) let startupLog = '' + const env: NodeJS.ProcessEnv = { + ...process.env, + NODE_PATH: path.join(process.cwd(), 'node_modules'), + ORCA_USER_DATA_PATH: runtime.userDataDir, + ORCA_BACKGROUND_LAUNCH: '1' + } + for (const key of [ + 'ELECTRON_RUN_AS_NODE', + 'NODE_OPTIONS', + 'BUN_OPTIONS', + 'BUN_INSPECT', + 'BUN_INSPECT_BRK', + 'BUN_INSPECT_WAIT', + 'BUN_CONPTY_LIBRARY' + ]) { + delete env[key] + } + if (runtime.bunRuntime.conptyLibraryPath) { + env.BUN_CONPTY_LIBRARY = runtime.bunRuntime.conptyLibraryPath + } const child = fork( runtime.entryPath, [ @@ -87,18 +107,16 @@ export async function launchDaemonGeneration(options: { '--refuse-dispose', String(refuseDispose) ], - { - cwd: runtime.userDataDir, - execPath: runtime.electronPath, - windowsHide: true, - env: { - ...process.env, - ELECTRON_RUN_AS_NODE: '1', - NODE_PATH: path.join(process.cwd(), 'node_modules'), - ORCA_USER_DATA_PATH: runtime.userDataDir - }, - stdio: ['ignore', 'ignore', 'pipe', 'ipc'] - } + Object.assign( + { + cwd: runtime.userDataDir, + execPath: runtime.bunRuntime.execPath, + execArgv: ['--no-env-file'], + env, + stdio: ['ignore', 'ignore', 'pipe', 'ipc'] + } satisfies ForkOptions, + { windowsHide: true } + ) ) child.stderr?.on('data', (chunk: Buffer) => { startupLog = `${startupLog}${chunk.toString('utf8')}`.slice(-MAX_CAPTURED_CHARS) diff --git a/tests/e2e/helpers/runtime-types.ts b/tests/e2e/helpers/runtime-types.ts index 215e1ce5bcc1..308f63dc7d35 100644 --- a/tests/e2e/helpers/runtime-types.ts +++ b/tests/e2e/helpers/runtime-types.ts @@ -1,3 +1,4 @@ +import type { PaneManager } from '../../../src/renderer/src/lib/pane-manager/pane-manager' import type { AppState } from '../../../src/renderer/src/store/types' import type { OpenFile, RightSidebarTab } from '../../../src/renderer/src/store/slices/editor' import type { @@ -27,6 +28,7 @@ export type ManagedPaneHandle = ManagedPane & ManagedPaneInternal // Why not optional: window.__paneManagers only ever holds real PaneManager instances, and // marking the methods optional made every call site a possibly-undefined invocation. export type PaneManagerLike = { + movePane: PaneManager['movePane'] getActivePane(): ManagedPaneHandle | null getPanes(limit?: number): ManagedPaneHandle[] splitPane(paneId: number, direction: 'vertical' | 'horizontal'): ManagedPaneHandle | null diff --git a/tests/e2e/helpers/store.ts b/tests/e2e/helpers/store.ts index 0d262723525a..75038a650386 100644 --- a/tests/e2e/helpers/store.ts +++ b/tests/e2e/helpers/store.ts @@ -59,7 +59,8 @@ export async function getWorktreeTabs( const state = store.getState() return (state.tabsByWorktree[worktreeId] ?? []).map((tab): TerminalTabSummary => ({ id: tab.id, - title: tab.customTitle || tab.title + title: tab.customTitle || tab.title, + customTitle: tab.customTitle })) }, worktreeId) } @@ -86,7 +87,7 @@ export async function getTabBarOrder(page: Page, worktreeId: string): Promise g.id === activeGroupId) : groups[0] - if (activeGroup?.tabOrder?.length > 0) { + if (activeGroup && (activeGroup.tabOrder?.length ?? 0) > 0) { const unifiedTabs = state.unifiedTabsByWorktree?.[worktreeId] ?? [] return activeGroup.tabOrder.map((itemId: string) => { const tab = unifiedTabs.find((t: { id: string }) => t.id === itemId) diff --git a/tests/e2e/pty-input-write-queue-ssh.spec.ts b/tests/e2e/pty-input-write-queue-ssh.spec.ts index 08a18d1a2287..251ff835aeaf 100644 --- a/tests/e2e/pty-input-write-queue-ssh.spec.ts +++ b/tests/e2e/pty-input-write-queue-ssh.spec.ts @@ -1,6 +1,6 @@ import { mkdirSync } from 'node:fs' import path from 'node:path' -import { expect, test } from './helpers/orca-app' +import { test } from './helpers/orca-app' import { connectDockerSshRelayTarget } from './helpers/docker-ssh-relay-connection' import { cleanupDockerSshRelayTarget, @@ -100,7 +100,7 @@ test.describe('PTY input write queue over SSH', () => { } }) - test('keeps fish query replies out of the next child stdin on an upstream relay pty', async ({ + test('keeps fish query replies out of the next child stdin on a relay terminal', async ({ orcaPage }, testInfo) => { test.slow() @@ -147,13 +147,6 @@ test.describe('PTY input write queue over SSH', () => { await waitForSessionReady(orcaPage) await waitForActiveWorktree(orcaPage) await connectDockerSshRelayTarget(orcaPage, target) - const relayExports = execDockerSshRelayTargetCommand( - target, - 'module=$(find /root/.orca-remote -type d -path \'*/node_modules/node-pty\' | head -n 1); node -e "const p=require(process.argv[1]); console.log(Object.keys(p.native || {}).join(\',\'))" "$module"' - ) - testInfo.annotations.push({ type: 'relay-node-pty-exports', description: relayExports }) - expect(relayExports).not.toContain('echoState') - await ensureTerminalVisible(orcaPage, 45_000) await waitForActiveTerminalManager(orcaPage, 60_000) const ptyId = await waitForActivePanePtyId(orcaPage, 60_000) diff --git a/tests/e2e/terminal-foreground-confirmation.unit.test.ts b/tests/e2e/terminal-foreground-confirmation.unit.test.ts index 04873836a341..9088cb0045e8 100644 --- a/tests/e2e/terminal-foreground-confirmation.unit.test.ts +++ b/tests/e2e/terminal-foreground-confirmation.unit.test.ts @@ -5,8 +5,14 @@ const { spawnMock, resolveForegroundMock } = vi.hoisted(() => ({ resolveForegroundMock: vi.fn() })) -vi.mock('node-pty', () => ({ spawn: spawnMock })) -vi.mock('../../src/main/pwsh', () => ({ isPwshAvailable: vi.fn(() => false) })) +vi.mock('../../src/main/daemon/pty-subprocess/bun-pty-process', () => ({ + canUseBunPty: () => true, + spawnBunPty: spawnMock +})) +vi.mock('../../src/main/pwsh', () => ({ + isPwshAvailable: vi.fn(() => false), + isPwshAvailableAsync: vi.fn(async () => false) +})) vi.mock('../../src/main/providers/windows-powershell-executable', () => ({ resolveWindowsPowerShellExecutablePath: () => 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe', diff --git a/tests/tools/daemon-relocation-spike/README.md b/tests/tools/daemon-relocation-spike/README.md index 0cf861c1b453..a09ccad8b00c 100644 --- a/tests/tools/daemon-relocation-spike/README.md +++ b/tests/tools/daemon-relocation-spike/README.md @@ -1,5 +1,11 @@ # daemon-relocation-spike +> Historical probe for packaged Electron/node-pty daemon hosts. Current Bun packages +> do not contain its required files. Use this only with an archived compatible build; +> current runtime relocation is covered by `daemon-bun-runtime` tests and the +> Windows packaged Bun capability smoke. + + A throwaway probe that answers one empirical question for Phase 1 of the Windows update-survival work: diff --git a/tests/tools/fish-mode2031-repro/capture-fish-mode2031-trace.mjs b/tests/tools/fish-mode2031-repro/capture-fish-mode2031-trace.mjs index 1b2cbcae38c7..eaceb19bde3c 100644 --- a/tests/tools/fish-mode2031-repro/capture-fish-mode2031-trace.mjs +++ b/tests/tools/fish-mode2031-repro/capture-fish-mode2031-trace.mjs @@ -1,9 +1,11 @@ #!/usr/bin/env node /** - * Ground-truth repro harness for issue #9993: color-scheme (CSI ?997;1n) replies + * Historical node-pty-only repro for issue #9993: color-scheme (CSI ?997;1n) replies * leaking into a child process' stdin under fish. * - * Simulates Orca's terminal pipeline around a REAL fish shell: + * Requires the pre-Bun checkout and its patched node-pty dependency. + * Current Bun coverage lives in fish-query-reply-child-stdin.bun.test.ts. + * Simulates the historical terminal pipeline around a REAL fish shell: * node-pty -> scanMode2031ReplyDecision() (the real scanner from * src/shared/terminal-color-scheme-protocol.ts) * -> reply `CSI ?997;1n` written back to the PTY, optionally after diff --git a/tests/tools/omp-close-lifecycle-bun-fixture.mjs b/tests/tools/omp-close-lifecycle-bun-fixture.mjs new file mode 100644 index 000000000000..4955daac51ee --- /dev/null +++ b/tests/tools/omp-close-lifecycle-bun-fixture.mjs @@ -0,0 +1,181 @@ +import assert from 'node:assert/strict' +import { spawnBunPty } from '../../src/main/daemon/pty-subprocess/bun-pty-process.ts' +import { Session } from '../../src/main/daemon/session.ts' +import { TerminalSessionTeardown } from '../../src/main/daemon/terminal-session-teardown.ts' +import { createDaemonPtySubprocessHandle } from '../../src/main/daemon/pty-subprocess/subprocess-handle.ts' +import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs' +import { join } from 'node:path' +import { tmpdir } from 'node:os' +import { runProcess } from '../../src/shared/child-process/run-process.ts' +import { + captureDescendantSnapshot, + readProcessTable +} from '../../src/main/pty-descendant-termination.ts' +const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) +const quote = (value) => `'${value.replaceAll("'", "'\\''")}'` +const ownedPidRows = async (pids) => { + const result = await runProcess({ + program: 'ps', + args: ['-p', pids.join(','), '-o', 'pid=,ppid=,pgid=,stat=,args='], + maxOutputBytes: 16000 + }) + assert.equal(result.timedOut, false) + assert.equal(result.signal, null) + assert.equal(result.stderr.trim(), '') + assert.ok([0, 1].includes(result.code)) + if (result.code === 1) { + assert.equal(result.stdout.trim(), '') + } + return result.stdout.trim() +} +export async function runOmpCloseProbe({ binary, externalTool, shell: requestedShell }) { + const fixtures = join(process.cwd(), '.bench-fixtures') + mkdirSync(fixtures, { recursive: true }) + const output = mkdtempSync(join(fixtures, 'omp-close-')) + const report = [] + for (const launch of ['recognized', 'typed']) { + const close = 'explicit' + const home = mkdtempSync(join(tmpdir(), 'orca-omp-close-home-')) + const agentHome = join(home, 'agent') + mkdirSync(agentHome) + const config = join(home, 'probe.yml') + writeFileSync( + config, + 'startup:\n setupWizard: false\n showSplash: false\n checkUpdate: false\n' + ) + const id = `${launch}-${close}` + let transcript = '' + let nativeExit = null + const shell = requestedShell ?? (process.platform === 'darwin' ? '/bin/zsh' : '/bin/bash') + const shellArgs = shell.endsWith('zsh') ? ['-f', '-i'] : ['--noprofile', '--norc', '-i'] + const proc = spawnBunPty({ + file: shell, + args: shellArgs, + cols: 120, + rows: 35, + cwd: home, + env: { + ...process.env, + HOME: home, + USERPROFILE: home, + ZDOTDIR: home, + XDG_CONFIG_HOME: join(home, 'config'), + XDG_DATA_HOME: join(home, 'data'), + XDG_CACHE_HOME: join(home, 'cache'), + XDG_STATE_HOME: join(home, 'state'), + OMP_CODING_AGENT_DIR: agentHome, + PI_CODING_AGENT_DIR: agentHome, + OMP_PROFILE: '', + PI_PROFILE: '', + PI_CONFIG_DIR: '.omp', + PI_CONFIG_FILES: '', + ORCA_BACKGROUND_LAUNCH: '1' + } + }) + const daemonSession = new Session({ + sessionId: id, + cols: 120, + rows: 35, + shellReadySupported: false, + ...(launch === 'recognized' ? { launchAgent: 'omp' } : {}), + subprocess: createDaemonPtySubprocessHandle({ + process: proc, + shellPath: shell, + spawnCwd: home, + env: process.env, + startupCommandDeliveredInShellArgs: false, + reportsChildExitStatus: true, + sessionId: id, + startupAgentRecognition: null + }) + }) + proc.onData((data) => { + transcript = (transcript + data).slice(-131072) + }) + const exitSubscription = proc.onExit((event) => { + nativeExit = event + }) + let snapshot + try { + proc.write(`${quote(binary)} --no-session --config ${quote(config)}\r`) + await delay(5000) + snapshot = await captureDescendantSnapshot(proc.pid) + assert.ok(snapshot?.descendants.length > 0) + if (externalTool) { + proc.write('! /bin/sleep 120\r') + for (let attempt = 0; attempt < 25; attempt++) { + await delay(200) + snapshot = await captureDescendantSnapshot(proc.pid) + if (snapshot?.descendants.length > 1) { + break + } + } + assert.ok(snapshot?.descendants.length > 1) + } + const pids = [proc.pid, ...snapshot.descendants.map((row) => row.pid)] + const before = await ownedPidRows(pids) + assert.ok(before.includes('omp'), `Expected OMP process: ${before}`) + if (externalTool) { + assert.ok(before.includes('sleep')) + } + const started = Date.now() + let closeError = null + try { + await new TerminalSessionTeardown(new Map([[id, daemonSession]])).killSession( + id, + daemonSession, + true + ) + } catch (error) { + closeError = String(error) + } + await delay(6000) + const after = await ownedPidRows(pids) + report.push({ + launch, + close, + externalTool, + backend: 'daemon', + before, + after, + nativeExit, + tracked: daemonSession.isAlive, + elapsedMs: Date.now() - started, + closeError, + home + }) + writeFileSync(join(output, `${id}.txt`), transcript) + writeFileSync(join(output, 'report.json'), JSON.stringify(report, null, 2)) + assert.equal(closeError, null) + assert.equal(after, '') + } finally { + writeFileSync(join(output, `${id}.txt`), transcript) + if (snapshot) { + const current = await readProcessTable() + const owned = [ + ...snapshot.descendants, + ...(snapshot.root ? [{ ...snapshot.root, pgid: snapshot.rootPgid }] : []) + ] + for (const row of current.rows) { + if ( + owned.some( + (known) => + known.pid === row.pid && + known.startedAt === row.startedAt && + known.pgid === row.pgid + ) + ) { + try { + process.kill(row.pid, 'SIGKILL') + } catch {} + } + } + } + daemonSession?.dispose() + exitSubscription.dispose() + rmSync(home, { recursive: true, force: true }) + } + } + writeFileSync(join(output, 'report.json'), JSON.stringify(report, null, 2)) + return output +} diff --git a/tests/tools/omp-close-lifecycle.md b/tests/tools/omp-close-lifecycle.md index 03e2dd6fc31d..87a912ebd85c 100644 --- a/tests/tools/omp-close-lifecycle.md +++ b/tests/tools/omp-close-lifecycle.md @@ -1,12 +1,10 @@ # OMP owned-PTY close probe (#9530) -This opt-in probe launches an actual installed OMP binary in disposable local PTYs -and calls Orca's production `shutdownLocalPty` and `killAllLocalPtys` functions, -or daemon `Session`, native subprocess handle, and `TerminalSessionTeardown`. -It sets the same agent-session ownership flag that `activateLocalPtySession` sets -for `launchAgent` / recognized startup commands, then repeats without that flag -to represent OMP typed into a shell. This isolates termination policy; it does not -exercise Agent button delivery or terminal-tab/handle routing. +This opt-in probe launches an actual installed OMP binary through the bundled Bun +PTY adapter and calls Orca's production daemon `Session`, subprocess handle, and +`TerminalSessionTeardown`. It repeats recognized-agent and typed-in-shell launches. +This isolates termination policy; it does not exercise Agent button delivery or +terminal-tab/handle routing. The retired in-process provider is no longer tested. ```sh ORCA_BACKGROUND_LAUNCH=1 ORCA_OMP_PROBE_BINARY=/absolute/path/to/omp \ @@ -16,11 +14,11 @@ ORCA_BACKGROUND_LAUNCH=1 ORCA_OMP_PROBE_BINARY=/absolute/path/to/omp \ The probe defaults to zsh on macOS and bash on other POSIX hosts. Set `ORCA_OMP_PROBE_SHELL` to the absolute path of either shell to override. Windows -is skipped. It requires the existing node-pty native dependency for the current -Node runtime. The normal unit suite skips the test unless a binary is supplied. +is skipped. It requires the pinned bundled Bun runtime in `out/orcad`, +`out/cli-runtime`, or `BUN_EXECUTABLE`. The normal unit suite skips the test unless a binary is supplied. Each case waits five seconds for OMP startup, captures the owned process tree, -requests explicit close or local quit cleanup, and verifies those exact process +requests explicit daemon close, and verifies those exact process IDs are absent using host `ps` after a six-second observation window. It records raw terminal output and before/after process rows in `.bench-fixtures/omp-close-*`. The fixture contains no prompt or model request. It disables the first-run setup @@ -47,9 +45,8 @@ No stale foreground OMP was reproduced in these local termination-policy cases. ## Detached external tool reproduction and correction Set `ORCA_OMP_PROBE_EXTERNAL_TOOL=1` to run `! /bin/sleep 120` in OMP before -explicit immediate close. Add `ORCA_OMP_PROBE_BACKEND=daemon` to exercise the daemon -backend. Each mode tests both recognized and typed launches; these modes do not -run the local-quit cases. The probe makes no model requests. Both OMP/PI profiles +explicit immediate close. Both recognized and typed launches use the daemon +backend; `ORCA_OMP_PROBE_BACKEND` is no longer needed. The probe makes no model requests. Both OMP/PI profiles are cleared, and XDG data/cache/state roots are isolated alongside configuration. On macOS, installed Orca `1.4.202-hourly.202609132311` and OMP `18.1.18`, an actual diff --git a/tests/tools/omp-close-lifecycle.test.mjs b/tests/tools/omp-close-lifecycle.test.mjs index 47df8325c563..a19aa22faa86 100644 --- a/tests/tools/omp-close-lifecycle.test.mjs +++ b/tests/tools/omp-close-lifecycle.test.mjs @@ -1,224 +1,24 @@ import { it, expect } from 'vitest' -import * as pty from 'node-pty' -import { Session } from '../../src/main/daemon/session.ts' -import { TerminalSessionTeardown } from '../../src/main/daemon/terminal-session-teardown.ts' -import { createDaemonPtySubprocessHandle } from '../../src/main/daemon/pty-subprocess/subprocess-handle.ts' -import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs' import { join } from 'node:path' -import { tmpdir } from 'node:os' -import { runProcess } from '../../src/shared/child-process/run-process.ts' -import { - captureDescendantSnapshot, - readProcessTable -} from '../../src/main/pty-descendant-termination.ts' -import { - createPtyPhysicalExit, - shutdownLocalPty, - killAllLocalPtys -} from '../../src/main/providers/local-pty-termination.ts' -import { - ptyProcesses, - ptyAgentSessionIds, - ptyPhysicalExits, - ptyExitDisposables, - clearPtyState -} from '../../src/main/providers/local-pty-provider-state.ts' +import { runBundledBunFixture } from '../../src/main/bundled-bun-test-execution.ts' const binary = process.env.ORCA_OMP_PROBE_BINARY -const externalTool = process.env.ORCA_OMP_PROBE_EXTERNAL_TOOL === '1' -const daemonBackend = process.env.ORCA_OMP_PROBE_BACKEND === 'daemon' -const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) -const quote = (value) => `'${value.replaceAll("'", "'\\''")}'` -const ownedPidRows = async (pids) => { - const result = await runProcess({ - program: 'ps', - args: ['-p', pids.join(','), '-o', 'pid=,ppid=,pgid=,stat=,comm='], - maxOutputBytes: 16000 - }) - expect(result.timedOut).toBe(false) - expect(result.signal).toBeNull() - expect(result.stderr.trim()).toBe('') - expect([0, 1]).toContain(result.code) - if (result.code === 1) { - expect(result.stdout.trim()).toBe('') - } - return result.stdout.trim() -} + it.skipIf(!binary || process.platform === 'win32')( 'observes actual OMP under production owned-PTY closure policy', async () => { - const fixtures = join(process.cwd(), '.bench-fixtures') - mkdirSync(fixtures, { recursive: true }) - const output = mkdtempSync(join(fixtures, 'omp-close-')) - const report = [] - for (const launch of ['recognized', 'typed']) { - for (const close of externalTool || daemonBackend ? ['explicit'] : ['explicit', 'quit']) { - expect(ptyProcesses.size).toBe(0) - const home = mkdtempSync(join(tmpdir(), 'orca-omp-close-home-')) - const agentHome = join(home, 'agent') - mkdirSync(agentHome) - const config = join(home, 'probe.yml') - writeFileSync( - config, - 'startup:\n setupWizard: false\n showSplash: false\n checkUpdate: false\n' - ) - const id = `${launch}-${close}` - let transcript = '' - let nativeExit = null - const shell = - process.env.ORCA_OMP_PROBE_SHELL ?? - (process.platform === 'darwin' ? '/bin/zsh' : '/bin/bash') - const shellArgs = shell.endsWith('zsh') ? ['-f', '-i'] : ['--noprofile', '--norc', '-i'] - const proc = pty.spawn(shell, shellArgs, { - name: 'xterm-256color', - cols: 120, - rows: 35, - cwd: home, - env: { - ...process.env, - HOME: home, - USERPROFILE: home, - ZDOTDIR: home, - XDG_CONFIG_HOME: join(home, 'config'), - XDG_DATA_HOME: join(home, 'data'), - XDG_CACHE_HOME: join(home, 'cache'), - XDG_STATE_HOME: join(home, 'state'), - OMP_CODING_AGENT_DIR: agentHome, - PI_CODING_AGENT_DIR: agentHome, - OMP_PROFILE: '', - PI_PROFILE: '', - PI_CONFIG_DIR: '.omp', - PI_CONFIG_FILES: '', - ORCA_BACKGROUND_LAUNCH: '1' - } - }) - const daemonSession = daemonBackend - ? new Session({ - sessionId: id, - cols: 120, - rows: 35, - shellReadySupported: false, - ...(launch === 'recognized' ? { launchAgent: 'omp' } : {}), - subprocess: createDaemonPtySubprocessHandle({ - process: proc, - shellPath: shell, - spawnCwd: home, - env: process.env, - startupCommandDeliveredInShellArgs: false, - reportsChildExitStatus: true, - sessionId: id, - startupAgentRecognition: null - }) - }) - : null - proc.onData((data) => { - transcript = (transcript + data).slice(-131072) - }) - if (!daemonSession) { - ptyProcesses.set(id, proc) - createPtyPhysicalExit(id) - if (launch === 'recognized') { - ptyAgentSessionIds.add(id) - } - } - ptyExitDisposables.set( - id, - proc.onExit((event) => { - nativeExit = event - ptyPhysicalExits.get(id)?.markExited() - clearPtyState(id) - rmSync(home, { recursive: true, force: true }) - }) - ) - let snapshot - try { - proc.write(`${quote(binary)} --no-session --config ${quote(config)}\r`) - await delay(5000) - snapshot = await captureDescendantSnapshot(proc.pid) - expect(snapshot?.descendants.length).toBeGreaterThan(0) - if (externalTool) { - proc.write('! /bin/sleep 120\r') - for (let attempt = 0; attempt < 25; attempt++) { - await delay(200) - snapshot = await captureDescendantSnapshot(proc.pid) - if (snapshot?.descendants.length > 1) { - break - } - } - expect(snapshot?.descendants.length).toBeGreaterThan(1) - } - const pids = [proc.pid, ...snapshot.descendants.map((row) => row.pid)] - const before = await ownedPidRows(pids) - expect(before).toContain('omp') - if (externalTool) { - expect(before).toContain('sleep') - } - const started = Date.now() - let closeError = null - try { - if (daemonSession) { - await new TerminalSessionTeardown(new Map([[id, daemonSession]])).killSession( - id, - daemonSession, - true - ) - } else if (close === 'explicit') { - await shutdownLocalPty(id, { immediate: externalTool }) - } else { - killAllLocalPtys() - } - } catch (error) { - closeError = String(error) - } - await delay(6000) - const after = await ownedPidRows(pids) - report.push({ - launch, - close, - externalTool, - backend: daemonBackend ? 'daemon' : 'local', - before, - after, - nativeExit, - tracked: daemonSession ? daemonSession.isAlive : ptyProcesses.has(id), - elapsedMs: Date.now() - started, - closeError, - home - }) - writeFileSync(join(output, `${id}.txt`), transcript) - writeFileSync(join(output, 'report.json'), JSON.stringify(report, null, 2)) - expect(closeError).toBeNull() - expect(after).toBe('') - } finally { - if (snapshot) { - const current = await readProcessTable() - const owned = [ - ...snapshot.descendants, - ...(snapshot.root ? [{ ...snapshot.root, pgid: snapshot.rootPgid }] : []) - ] - for (const row of current.rows) { - if ( - owned.some( - (known) => - known.pid === row.pid && - known.startedAt === row.startedAt && - known.pgid === row.pgid - ) - ) { - try { - process.kill(row.pid, 'SIGKILL') - } catch {} - } - } - } - daemonSession?.dispose() - clearPtyState(id) - rmSync(home, { recursive: true, force: true }) - } - } - } - writeFileSync(join(output, 'report.json'), JSON.stringify(report, null, 2)) + const output = await runBundledBunFixture( + join(import.meta.dirname, 'omp-close-lifecycle-bun-fixture.mjs'), + 'runOmpCloseProbe', + { + binary, + externalTool: process.env.ORCA_OMP_PROBE_EXTERNAL_TOOL === '1', + shell: process.env.ORCA_OMP_PROBE_SHELL + }, + 85_000 + ) + expect(typeof output).toBe('string') console.log(output) }, - 90000 + 90_000 ) diff --git a/tests/tools/windows-pty-native-capability-smoke/README.md b/tests/tools/windows-pty-native-capability-smoke/README.md index fa18f4346873..bfa74dc003df 100644 --- a/tests/tools/windows-pty-native-capability-smoke/README.md +++ b/tests/tools/windows-pty-native-capability-smoke/README.md @@ -1,16 +1,13 @@ # Packaged Windows PTY native capability smoke -This narrow harness runs the packaged `Orca.exe` with `ELECTRON_RUN_AS_NODE=1` and loads its bundled `node-pty`. Checkout Node processes provide the target/canary shell fixtures, so the smoke proves the packaged native addon without putting a GUI-subsystem Electron executable under ConPTY. A 256-bit fixture token binds the target shell, a grandchild that remains live after its intermediate launcher exits, and an unrelated canary to one unique named-pipe channel; the oracle checks the patched exports, job PIDs, exact job-handle termination, PTY/socket exit events, and canary survival. +Runs the exact packaged `resources/cli-runtime/bun-runtime.exe` with its hash-verified ConPTY closure. The runner compiles the production `spawnBunPty` implementation into a private temporary directory and copies the packaged gate beside it. Checkout Node supplies target/canary fixture processes; no GUI process is launched and no runtime is downloaded. -Run it against an unpacked Windows package: +A unique 256-bit token binds a target shell, its exited transient WScript launcher, the surviving grandchild, and an unrelated canary to a named pipe. The oracle requires live job membership, exact retained-job termination via public owned-process APIs, PTY/socket exits, and canary survival followed by its separate teardown. It never infers ownership from the process table or accesses private terminal handles. ```text pnpm run smoke:windows-pty-native-capability -- --exe=dist/win-unpacked/Orca.exe ``` -The official `v1.4.158` package is the causal red artifact: it fails immediately because its -packaged `node-pty` lacks `assignCurrentProcessToJob`. Candidate green evidence comes from the -required Windows packaging job, which runs this smoke without retries. The one-shot probe flushes -its evidence and exits explicitly because direct native job termination intentionally bypasses -node-pty's ConPTY-worker disposal path; a 45-second outer ceiling reports bounded stdout, stderr, -and native-stage breadcrumbs if any boundary stops progressing. +The executable argument selects the package directory; the probe executes its Bun runtime. Package manifest/hash validation occurs before execution. Environment injection options are removed, the ConPTY selector is set before Bun starts, and dotenv/config loading is isolated. A 45-second ceiling reports bounded output and stage breadcrumbs; the production host job owns cleanup when the probe exits. + +This qualifies the packaged runtime/provider/gate with freshly compiled production adapter code. It does not prove the packaged daemon bundle contains identical adapter code or test rendered Electron routes. Packages predating Bun are no longer valid negative controls for this gate; mutated evidence tests preserve oracle failure coverage. diff --git a/tests/tools/windows-pty-native-capability-smoke/packaged-node-pty-capability-oracle.mjs b/tests/tools/windows-pty-native-capability-smoke/packaged-bun-capability-oracle.mjs similarity index 82% rename from tests/tools/windows-pty-native-capability-smoke/packaged-node-pty-capability-oracle.mjs rename to tests/tools/windows-pty-native-capability-smoke/packaged-bun-capability-oracle.mjs index d311bf24f28b..a8d7786cfa7b 100644 --- a/tests/tools/windows-pty-native-capability-smoke/packaged-node-pty-capability-oracle.mjs +++ b/tests/tools/windows-pty-native-capability-smoke/packaged-bun-capability-oracle.mjs @@ -1,4 +1,4 @@ -const REQUIRED_EXPORTS = ['assignCurrentProcessToJob', 'listJobProcessIds', 'terminateJob'] +const REQUIRED_APIS = ['listOwnedProcessIds', 'terminateOwnedTree', 'waitForSpawn'] const FIXTURE_TOKEN_PATTERN = /^[a-f0-9]{64}$/ function isFixtureObservation(value, expected) { @@ -23,16 +23,16 @@ function isPtyExit(value, terminalHandle) { return value?.terminalHandle === terminalHandle && Number.isInteger(value.exitCode) } -export function evaluatePackagedNodePtyCapability(evidence) { +export function evaluatePackagedBunCapability(evidence) { const failures = [] - const exports = new Set(evidence?.patchedExports ?? []) + const exports = new Set(evidence?.ownedApis ?? []) const fixtureToken = evidence?.fixtureToken const channel = evidence?.channel const target = evidence?.target const canary = evidence?.canary - if (REQUIRED_EXPORTS.some((name) => !exports.has(name))) { - failures.push('the packaged node-pty addon is missing a required patched export') + if (REQUIRED_APIS.some((name) => !exports.has(name))) { + failures.push('the packaged Bun addon is missing a required owned-process API') } if (!FIXTURE_TOKEN_PATTERN.test(fixtureToken) || !channel?.includes(fixtureToken)) { failures.push('the fixture channel is not bound to the per-run unguessable token') @@ -80,9 +80,21 @@ export function evaluatePackagedNodePtyCapability(evidence) { } else if (target.jobProcessIds.includes(target?.launcherExited?.pid)) { failures.push('the transient launcher is still live in the target job') } + if ( + typeof target?.terminalHandle !== 'string' || + !target.terminalHandle || + typeof canary?.terminalHandle !== 'string' || + !canary.terminalHandle || + target.terminalHandle === canary.terminalHandle + ) { + failures.push('target and canary need distinct retained terminal identities') + } + if (target?.jobProcessIds?.includes(canary?.process?.pid)) { + failures.push('target job contains the unrelated canary') + } const close = evidence?.close if ( - close?.method !== 'terminate-job' || + close?.method !== 'terminate-owned-tree' || close?.requestedHandle !== target?.terminalHandle || close?.completedHandle !== target?.terminalHandle ) { @@ -129,8 +141,8 @@ export function evaluatePackagedNodePtyCapability(evidence) { return { pass: failures.length === 0, failures } } -export function assertPackagedNodePtyCapability(evidence) { - const result = evaluatePackagedNodePtyCapability(evidence) +export function assertPackagedBunCapability(evidence) { + const result = evaluatePackagedBunCapability(evidence) if (!result.pass) { throw new Error( `Packaged node-pty native capability failed:\n- ${result.failures.join('\n- ')}` diff --git a/tests/tools/windows-pty-native-capability-smoke/packaged-node-pty-capability-oracle.test.mjs b/tests/tools/windows-pty-native-capability-smoke/packaged-bun-capability-oracle.test.mjs similarity index 79% rename from tests/tools/windows-pty-native-capability-smoke/packaged-node-pty-capability-oracle.test.mjs rename to tests/tools/windows-pty-native-capability-smoke/packaged-bun-capability-oracle.test.mjs index a752c6f499df..d24ed0998193 100644 --- a/tests/tools/windows-pty-native-capability-smoke/packaged-node-pty-capability-oracle.test.mjs +++ b/tests/tools/windows-pty-native-capability-smoke/packaged-bun-capability-oracle.test.mjs @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { evaluatePackagedNodePtyCapability } from './packaged-node-pty-capability-oracle.mjs' +import { evaluatePackagedBunCapability } from './packaged-bun-capability-oracle.mjs' const fixtureToken = 'a'.repeat(64) const channel = `\\\\.\\pipe\\orca-pty-native-capability-${fixtureToken}` @@ -18,7 +18,7 @@ function passingEvidence() { const grandchild = observation(4102, 'target-grandchild') const canary = observation(5100, 'canary-shell') return { - patchedExports: ['assignCurrentProcessToJob', 'listJobProcessIds', 'terminateJob'], + ownedApis: ['listOwnedProcessIds', 'terminateOwnedTree', 'waitForSpawn'], fixtureToken, channel, target: { @@ -37,7 +37,7 @@ function passingEvidence() { socketClosed: closure('canary-shell') }, close: { - method: 'terminate-job', + method: 'terminate-owned-tree', requestedHandle: 'pty-job:7:4100', completedHandle: 'pty-job:7:4100', targetExit: { terminalHandle: 'pty-job:7:4100', exitCode: 1, signal: 0 }, @@ -47,16 +47,16 @@ function passingEvidence() { } } -describe('packaged node-pty native capability oracle', () => { +describe('packaged Bun native capability oracle', () => { it('accepts exact target teardown while the unrelated canary remains live', () => { - expect(evaluatePackagedNodePtyCapability(passingEvidence())).toEqual({ + expect(evaluatePackagedBunCapability(passingEvidence())).toEqual({ pass: true, failures: [] }) }) it.each([ - ['missing patched export', (evidence) => evidence.patchedExports.pop()], + ['missing owned-process API', (evidence) => evidence.ownedApis.pop()], ['launcher exit absent', (evidence) => delete evidence.target.launcherExited], ['empty job membership', (evidence) => (evidence.target.jobProcessIds = [])], [ @@ -73,10 +73,18 @@ describe('packaged node-pty native capability oracle', () => { const evidence = passingEvidence() mutate(evidence) - expect(evaluatePackagedNodePtyCapability(evidence).pass).toBe(false) + expect(evaluatePackagedBunCapability(evidence).pass).toBe(false) }) it.each([ + [ + 'shared terminal handle', + (evidence) => (evidence.canary.terminalHandle = evidence.target.terminalHandle) + ], + [ + 'canary in target job', + (evidence) => evidence.target.jobProcessIds.push(evidence.canary.process.pid) + ], ['guessable token', (evidence) => (evidence.fixtureToken = 'run-1')], [ 'grandchild from another channel', @@ -93,6 +101,6 @@ describe('packaged node-pty native capability oracle', () => { const evidence = passingEvidence() mutate(evidence) - expect(evaluatePackagedNodePtyCapability(evidence).pass).toBe(false) + expect(evaluatePackagedBunCapability(evidence).pass).toBe(false) }) }) diff --git a/tests/tools/windows-pty-native-capability-smoke/packaged-node-pty-capability-probe.cjs b/tests/tools/windows-pty-native-capability-smoke/packaged-bun-capability-probe.cjs similarity index 83% rename from tests/tools/windows-pty-native-capability-smoke/packaged-node-pty-capability-probe.cjs rename to tests/tools/windows-pty-native-capability-smoke/packaged-bun-capability-probe.cjs index 580a2852a199..197fc54990f3 100644 --- a/tests/tools/windows-pty-native-capability-smoke/packaged-node-pty-capability-probe.cjs +++ b/tests/tools/windows-pty-native-capability-smoke/packaged-bun-capability-probe.cjs @@ -3,7 +3,7 @@ const { writeSync } = require('node:fs') const net = require('node:net') const path = require('node:path') -const EVIDENCE_PREFIX = 'ORCA_NODE_PTY_CAPABILITY_EVIDENCE=' +const EVIDENCE_PREFIX = 'ORCA_BUN_PTY_CAPABILITY_EVIDENCE=' const EXPECTED_ROLES = new Set([ 'target-shell', 'target-launcher-exited', @@ -73,10 +73,8 @@ function buildGrandchildLaunch(channel, fixtureToken) { } } -function startGrandchildAfterLauncherExit(channel, fixtureToken, resourcesDir) { - const { spawnProcess } = require( - path.join(resourcesDir, 'app.asar.unpacked', 'out', 'shared', 'child-process', 'run-process.js') - ) +function startGrandchildAfterLauncherExit(channel, fixtureToken, adapterPath) { + const { spawnProcess } = require(adapterPath) const launch = buildGrandchildLaunch(channel, fixtureToken) const child = spawnProcess({ ...launch, @@ -100,11 +98,11 @@ function startGrandchildAfterLauncherExit(channel, fixtureToken, resourcesDir) { }) } -async function runPtyShell(channel, fixtureToken, role, resourcesDir) { +async function runPtyShell(channel, fixtureToken, role, adapterPath) { const socket = connectFixture(channel, fixtureToken, `${role}-shell`) try { if (role === 'target') { - await startGrandchildAfterLauncherExit(channel, fixtureToken, resourcesDir) + await startGrandchildAfterLauncherExit(channel, fixtureToken, adapterPath) } } catch (error) { socket.destroy() @@ -206,7 +204,7 @@ function createFixtureServer(channel, fixtureToken) { } function terminalHandle(pty) { - return `pty-job:${pty._pty}:${pty.pid}` + return `bun-owned-job:${pty.pid}` } function exitEvent(pty) { @@ -227,41 +225,35 @@ function waitForBarrier(promise, label, timeoutMs = 30_000) { return Promise.race([promise, deadline]).finally(() => clearTimeout(timer)) } -async function exercise(resourcesDir, fixtureExecutable) { - const nodePtyDir = path.join(resourcesDir, 'node_modules', 'node-pty') - stage('addon-load:start') - const nodePty = require(nodePtyDir) - const { module: native } = require(path.join(nodePtyDir, 'lib', 'utils.js')).loadNativeModule( - 'conpty' - ) - stage('addon-load:done') - const patchedExports = ['assignCurrentProcessToJob', 'listJobProcessIds', 'terminateJob'] - for (const name of patchedExports) { - if (typeof native[name] !== 'function') { - throw new Error(`packaged node-pty is missing ${name}`) - } +async function exercise(resourcesDir, fixtureExecutable, adapterPath) { + if (!process.versions.bun) { + throw new Error('Probe requires the packaged Bun runtime') } - stage('host-job-assign:start') - const hostJobAssigned = native.assignCurrentProcessToJob() - stage('host-job-assign:done') - if (!hostJobAssigned) { - throw new Error('packaged probe could not establish host job ownership') + const { spawnBunPty } = require(adapterPath) + const ownedApis = ['listOwnedProcessIds', 'terminateOwnedTree', 'waitForSpawn'] + const spawn = (role, channel, fixtureToken) => { + const terminal = spawnBunPty({ + file: fixtureExecutable, + args: [__filename, '--pty-shell', channel, fixtureToken, role, adapterPath], + cols: 80, + rows: 30, + cwd: process.cwd(), + env: process.env + }) + for (const name of ownedApis) { + if (typeof terminal[name] !== 'function') { + terminal.destroy() + throw new Error(`Packaged Bun terminal lacks ${name}`) + } + } + return terminal } - const fixtureToken = randomBytes(32).toString('hex') const channel = `\\\\.\\pipe\\orca-pty-native-capability-${fixtureToken}` const fixtures = createFixtureServer(channel, fixtureToken) stage('fixture-listen:start') await waitForBarrier(fixtures.listening, 'fixture server listen') stage('fixture-listen:done') - const options = { - name: 'xterm-256color', - cols: 80, - rows: 30, - cwd: process.cwd(), - env: process.env, - useConptyDll: true - } const created = [] const closed = new Set() const exitPromises = [] @@ -269,27 +261,20 @@ async function exercise(resourcesDir, fixtureExecutable) { try { stage('target-spawn:start') - const target = nodePty.spawn( - fixtureExecutable, - [__filename, '--pty-shell', channel, fixtureToken, 'target', resourcesDir], - options - ) + const target = spawn('target', channel, fixtureToken) stage('target-spawn:done') created.push(target) const targetExited = exitEvent(target) exitPromises.push(targetExited) stage('canary-spawn:start') - const canary = nodePty.spawn( - fixtureExecutable, - [__filename, '--pty-shell', channel, fixtureToken, 'canary', resourcesDir], - options - ) + const canary = spawn('canary', channel, fixtureToken) stage('canary-spawn:done') created.push(canary) const canaryHandle = terminalHandle(canary) const canaryExited = exitEvent(canary) exitPromises.push(canaryExited) + await Promise.all([target.waitForSpawn(), canary.waitForSpawn()]) stage('fixture-readiness:start') const [shell, launcherExited, grandchild, canaryProcess] = await Promise.all([ waitForBarrier(fixtures.waitForRole('target-shell'), 'target shell readiness'), @@ -299,13 +284,13 @@ async function exercise(resourcesDir, fixtureExecutable) { ]) stage('fixture-readiness:done') stage('target-job-list:start') - const targetJobProcessIds = native.listJobProcessIds(target._pty, target.pid) + const targetJobProcessIds = target.listOwnedProcessIds() stage('target-job-list:done') const targetHandle = terminalHandle(target) stage('target-job-terminate:start') - const targetTerminated = native.terminateJob(target._pty, target.pid) + const targetTerminated = target.terminateOwnedTree() stage('target-job-terminate:done') - if (!targetTerminated) { + if (targetTerminated !== 'terminated') { throw new Error('exact target job termination was refused') } closed.add(target) @@ -321,14 +306,14 @@ async function exercise(resourcesDir, fixtureExecutable) { stage('target-exit-barriers:done') stage('canary-job-list:start') - const canaryJobProcessIdsAfterTargetClose = native.listJobProcessIds(canary._pty, canary.pid) + const canaryJobProcessIdsAfterTargetClose = canary.listOwnedProcessIds() stage('canary-job-list:done') const canarySocket = fixtures.sockets.get('canary-shell') const connectedAfterTargetClose = Boolean(canarySocket && !canarySocket.destroyed) stage('canary-job-terminate:start') - const canaryTerminated = native.terminateJob(canary._pty, canary.pid) + const canaryTerminated = canary.terminateOwnedTree() stage('canary-job-terminate:done') - if (!canaryTerminated) { + if (canaryTerminated !== 'terminated') { throw new Error('exact canary job termination was refused') } closed.add(canary) @@ -340,7 +325,7 @@ async function exercise(resourcesDir, fixtureExecutable) { stage('canary-exit-barriers:done') const evidence = { - patchedExports, + ownedApis, fixtureToken, channel, target: { @@ -359,7 +344,7 @@ async function exercise(resourcesDir, fixtureExecutable) { socketClosed: canaryClosed }, close: { - method: 'terminate-job', + method: 'terminate-owned-tree', requestedHandle: targetHandle, completedHandle: targetHandle, targetExit, @@ -380,7 +365,7 @@ async function exercise(resourcesDir, fixtureExecutable) { for (const pty of created) { if (!closed.has(pty)) { stage('cleanup-job-terminate:start') - native.terminateJob(pty._pty, pty.pid) + pty.terminateOwnedTree() stage('cleanup-job-terminate:done') } } @@ -414,7 +399,7 @@ async function main() { if (!args[1]) { throw new Error('exercise mode requires a fixture Node executable') } - await exercise(args[0], args[1]) + await exercise(args[0], args[1], args[2]) return } if (mode === '--exit-contract-fixture') { @@ -422,7 +407,7 @@ async function main() { setInterval(() => {}, 60_000) return } - throw new Error(`unknown packaged node-pty capability probe mode: ${mode}`) + throw new Error(`unknown packaged Bun capability probe mode: ${mode}`) } module.exports = { @@ -432,7 +417,7 @@ module.exports = { reportFixtureObservation } -if (require.main === module) { +if (process.argv[1] && path.resolve(process.argv[1]) === __filename) { const mode = process.argv[2] main().then( () => (isOneShotMode(mode) ? exitOneShot(0) : undefined), diff --git a/tests/tools/windows-pty-native-capability-smoke/packaged-node-pty-capability-probe.test.mjs b/tests/tools/windows-pty-native-capability-smoke/packaged-bun-capability-probe.test.mjs similarity index 95% rename from tests/tools/windows-pty-native-capability-smoke/packaged-node-pty-capability-probe.test.mjs rename to tests/tools/windows-pty-native-capability-smoke/packaged-bun-capability-probe.test.mjs index f26b5ae481e4..590178c45d65 100644 --- a/tests/tools/windows-pty-native-capability-smoke/packaged-node-pty-capability-probe.test.mjs +++ b/tests/tools/windows-pty-native-capability-smoke/packaged-bun-capability-probe.test.mjs @@ -6,7 +6,7 @@ import { afterEach, describe, expect, it } from 'vitest' import { runProcess } from '../../../src/shared/child-process/run-process' const require = createRequire(import.meta.url) -const probePath = require.resolve('./packaged-node-pty-capability-probe.cjs') +const probePath = require.resolve('./packaged-bun-capability-probe.cjs') const { buildGrandchildLaunch, createFixtureServer, @@ -23,7 +23,7 @@ afterEach(() => { } }) -describe('packaged node-pty launcher-surviving grandchild', () => { +describe('packaged Bun launcher-surviving grandchild', () => { it('flushes one-shot evidence and exits despite a referenced handle', async () => { expect(isOneShotMode('--exercise')).toBe(true) expect(isOneShotMode('--exit-contract-fixture')).toBe(true) diff --git a/tests/tools/windows-pty-native-capability-smoke/run.mjs b/tests/tools/windows-pty-native-capability-smoke/run.mjs index 491fba2642d2..b53ad5f63b9a 100644 --- a/tests/tools/windows-pty-native-capability-smoke/run.mjs +++ b/tests/tools/windows-pty-native-capability-smoke/run.mjs @@ -1,9 +1,12 @@ +import { build } from 'esbuild' +import { mkdtemp, copyFile, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' import { createRequire } from 'node:module' import path from 'node:path' import { pathToFileURL } from 'node:url' -import { assertPackagedNodePtyCapability } from './packaged-node-pty-capability-oracle.mjs' +import { assertPackagedBunCapability } from './packaged-bun-capability-oracle.mjs' -const EVIDENCE_PREFIX = 'ORCA_NODE_PTY_CAPABILITY_EVIDENCE=' +const EVIDENCE_PREFIX = 'ORCA_BUN_PTY_CAPABILITY_EVIDENCE=' const PROBE_TIMEOUT_MS = 45_000 const MAX_DIAGNOSTIC_CHARS = 8_000 @@ -19,14 +22,37 @@ export function checkoutRunProcessPath() { return path.resolve(import.meta.dirname, '../../../out/shared/child-process/run-process.js') } -export function packagedProbeInvocation(executable, environment = process.env) { +export function packagedProbeInvocation(executable, adapterPath, environment = process.env) { const resolvedExecutable = path.resolve(executable) const resourcesDir = path.join(path.dirname(resolvedExecutable), 'resources') - const probe = path.join(import.meta.dirname, 'packaged-node-pty-capability-probe.cjs') + const probe = path.join(import.meta.dirname, 'packaged-bun-capability-probe.cjs') return { - program: resolvedExecutable, - args: [probe, '--exercise', resourcesDir, process.execPath], - env: { ...environment, ELECTRON_RUN_AS_NODE: '1' }, + program: path.join(resourcesDir, 'cli-runtime', 'bun-runtime.exe'), + args: [ + '--no-env-file', + `--config=${path.join(path.dirname(adapterPath), 'bunfig.toml')}`, + probe, + '--exercise', + resourcesDir, + process.execPath, + adapterPath + ], + env: { + ...Object.fromEntries( + Object.entries(environment).filter( + ([key]) => + ![ + 'ELECTRON_RUN_AS_NODE', + 'NODE_OPTIONS', + 'NODE_PATH', + 'BUN_OPTIONS', + 'BUN_INSPECT' + ].includes(key.toUpperCase()) + ) + ), + ORCA_BACKGROUND_LAUNCH: '1', + BUN_CONPTY_LIBRARY: path.join(resourcesDir, 'cli-runtime', 'conpty', 'conpty.dll') + }, timeoutMs: PROBE_TIMEOUT_MS } } @@ -51,6 +77,32 @@ function parseEvidence(stdout) { return JSON.parse(line.slice(EVIDENCE_PREFIX.length)) } +export async function buildCapabilityAdapter(directory, resources) { + const adapterPath = path.join(directory, 'capability-adapter.cjs') + const root = path.resolve(import.meta.dirname, '../../..') + await build({ + stdin: { + contents: + "export { spawnBunPty } from './src/main/daemon/pty-subprocess/bun-pty-process'; export { spawnProcess, runProcess } from './src/shared/child-process/run-process'", + resolveDir: root, + loader: 'ts' + }, + outfile: adapterPath, + bundle: true, + platform: 'node', + format: 'cjs', + target: 'node24', + external: ['bun:ffi', 'electron'], + logLevel: 'silent' + }) + await copyFile( + path.join(resources, 'terminal-daemon', 'windows-bun-pty-gate-entry.js'), + path.join(directory, 'windows-bun-pty-gate-entry.js') + ) + await writeFile(path.join(directory, 'bunfig.toml'), '') + return adapterPath +} + async function main() { if (process.platform !== 'win32') { throw new Error('windows-pty-native-capability-smoke requires a physical Windows host') @@ -58,13 +110,21 @@ async function main() { const executable = executableArgument(process.argv.slice(2)) const require = createRequire(import.meta.url) const { runProcess } = require(checkoutRunProcessPath()) - const result = await runProcess(packagedProbeInvocation(executable)) - if (result.code !== 0 || result.timedOut) { - throw new Error(formatProbeFailure(result)) + const resources = path.join(path.dirname(executable), 'resources') + const { verifyCliRuntimeDirectory } = require('../../../config/bundled-cli-runtime.cjs') + verifyCliRuntimeDirectory(path.join(resources, 'cli-runtime'), 'win32', process.arch) + const directory = await mkdtemp(path.join(tmpdir(), 'orca-packaged-bun-capability-')) + try { + const adapterPath = await buildCapabilityAdapter(directory, resources) + const result = await runProcess(packagedProbeInvocation(executable, adapterPath)) + if (result.code !== 0 || result.timedOut || result.outputTruncated) { + throw new Error(formatProbeFailure(result)) + } + assertPackagedBunCapability(parseEvidence(result.stdout)) + process.stdout.write(`[windows-pty-native-capability-smoke] PASS ${executable}\n`) + } finally { + await rm(directory, { recursive: true, force: true }) } - - assertPackagedNodePtyCapability(parseEvidence(result.stdout)) - process.stdout.write(`[windows-pty-native-capability-smoke] PASS ${executable}\n`) } if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) { diff --git a/tests/tools/windows-pty-native-capability-smoke/run.test.mjs b/tests/tools/windows-pty-native-capability-smoke/run.test.mjs index bb8ab525da6b..66084e05b929 100644 --- a/tests/tools/windows-pty-native-capability-smoke/run.test.mjs +++ b/tests/tools/windows-pty-native-capability-smoke/run.test.mjs @@ -6,20 +6,35 @@ import { checkoutRunProcessPath, formatProbeFailure, packagedProbeInvocation } f const runnerSource = readFileSync(new URL('./run.mjs', import.meta.url), 'utf8') describe('packaged Windows native smoke runner boundary', () => { - it('uses one checkout-owned runner for current and affected package paths', () => { - const current = packagedProbeInvocation('/ci/current/dist/win-unpacked/Orca.exe') - const affected = packagedProbeInvocation('/ci/1.4.158/dist/win-unpacked/Orca.exe') - + it('runs the packaged runtime with its provider and explicit config', () => { + const current = packagedProbeInvocation( + '/ci/current/dist/win-unpacked/Orca.exe', + '/tmp/probe/capability-adapter.cjs', + { ELECTRON_RUN_AS_NODE: '1', NODE_OPTIONS: '--inspect', BUN_CONPTY_LIBRARY: 'untrusted.dll' } + ) expect(checkoutRunProcessPath()).toBe(path.resolve('out/shared/child-process/run-process.js')) - expect(current.program).not.toBe(affected.program) - expect(current.args[0]).toBe(affected.args[0]) - expect(current.args[1]).toBe('--exercise') - expect(current.args[2]).not.toBe(affected.args[2]) - expect(current.args[3]).toBe(process.execPath) - expect(affected.args[3]).toBe(process.execPath) - expect(current.env.ELECTRON_RUN_AS_NODE).toBe('1') - expect(affected.env.ELECTRON_RUN_AS_NODE).toBe('1') - expect(current.timeoutMs).toBe(affected.timeoutMs) + expect(current.program).toBe( + '/ci/current/dist/win-unpacked/resources/cli-runtime/bun-runtime.exe' + ) + expect(current.args.slice(0, 2)).toEqual([ + '--no-env-file', + `--config=${path.join(path.dirname('/tmp/probe/capability-adapter.cjs'), 'bunfig.toml')}` + ]) + expect(current.args).toContain('--exercise') + expect(current.args.at(-1)).toBe('/tmp/probe/capability-adapter.cjs') + expect(current.args.at(-2)).toBe(process.execPath) + expect(current.env.ELECTRON_RUN_AS_NODE).toBeUndefined() + expect(current.env.NODE_OPTIONS).toBeUndefined() + expect(current.env.BUN_CONPTY_LIBRARY).toBe( + path.join( + path.resolve('/ci/current/dist/win-unpacked'), + 'resources', + 'cli-runtime', + 'conpty', + 'conpty.dll' + ) + ) + expect(current.env.ORCA_BACKGROUND_LAUNCH).toBe('1') expect(current.timeoutMs).toBe(45_000) })