fix: bundle modern ConPTY on Windows (#1828)

refs #1533
refs #1644
This commit is contained in:
Can Celik
2026-07-25 02:40:14 +03:00
committed by GitHub
parent 5f1957c27b
commit 8afd52ae57
32 changed files with 2694 additions and 133 deletions
+1
View File
@@ -0,0 +1 @@
packaging/windows/licenses/* binary
+11 -4
View File
@@ -224,19 +224,26 @@ jobs:
- name: Package artifact
shell: pwsh
run: |
Copy-Item target\x86_64-pc-windows-msvc\release\herdr.exe herdr-windows-x86_64.exe
$ErrorActionPreference = "Stop"
$package = Join-Path $env:RUNNER_TEMP "Microsoft.Windows.Console.ConPTY.nupkg"
$stage = Join-Path $env:RUNNER_TEMP "herdr-windows-x86_64"
.\scripts\package_windows_conpty.ps1 `
-HerdrExe target\x86_64-pc-windows-msvc\release\herdr.exe `
-PackagePath $package `
-StageDir $stage `
-OutputPath herdr-windows-x86_64.zip
"commit=$env:GITHUB_SHA" | Out-File -Encoding utf8 BUILD_INFO.txt
"target=x86_64-pc-windows-msvc" | Out-File -Encoding utf8 -Append BUILD_INFO.txt
"libghostty_vt_optimize=$env:LIBGHOSTTY_VT_OPTIMIZE" | Out-File -Encoding utf8 -Append BUILD_INFO.txt
"libghostty_vt_simd=$env:LIBGHOSTTY_VT_SIMD" | Out-File -Encoding utf8 -Append BUILD_INFO.txt
$hash = (Get-FileHash -Algorithm SHA256 herdr-windows-x86_64.exe).Hash.ToLowerInvariant()
"sha256=$hash herdr-windows-x86_64.exe" | Out-File -Encoding utf8 -Append BUILD_INFO.txt
$hash = (Get-FileHash -Algorithm SHA256 herdr-windows-x86_64.zip).Hash.ToLowerInvariant()
"sha256=$hash herdr-windows-x86_64.zip" | Out-File -Encoding utf8 -Append BUILD_INFO.txt
- name: Upload artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: herdr-windows-x86_64-${{ inputs.libghostty_optimize }}-simd-${{ inputs.libghostty_simd }}
path: |
herdr-windows-x86_64.exe
herdr-windows-x86_64.zip
BUILD_INFO.txt
retention-days: 7
+103
View File
@@ -173,3 +173,106 @@ jobs:
$ErrorActionPreference = "Stop"
$exe = Join-Path $PWD "target\x86_64-pc-windows-msvc\debug\herdr.exe"
.\scripts\windows_smoke_conpty_path.ps1 -ExePath $exe -Session "ci-windows-$env:GITHUB_RUN_ID-$env:GITHUB_RUN_ATTEMPT"
windows-conpty-package:
name: Windows ConPTY package
runs-on: windows-2022
timeout-minutes: 25
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
- name: Install Rust
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # v1
with:
toolchain: ${{ env.RUST_TOOLCHAIN_VERSION }}
targets: x86_64-pc-windows-msvc
- name: Install Zig
uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1
with:
version: 0.15.2
use-cache: false
- name: Restore Cargo cache
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
cache-bin: false
key: conpty-package-windows-2022
- name: Build Herdr
run: cargo build --locked --target x86_64-pc-windows-msvc
- name: Verify invalid bundle is rejected and system override recovers
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
Remove-Item Env:HERDR_SOCKET_PATH, Env:HERDR_CLIENT_SOCKET_PATH -ErrorAction SilentlyContinue
$exe = Join-Path $PWD "target\x86_64-pc-windows-msvc\debug\herdr.exe"
$bundle = Join-Path (Split-Path -Parent $exe) "conpty"
New-Item -ItemType Directory -Force -Path (Join-Path $bundle "x64"), (Join-Path $bundle "arm64") | Out-Null
Set-Content -LiteralPath (Join-Path $bundle "herdr-conpty.json") -Value "{}" -Encoding ascii
[System.IO.File]::WriteAllBytes((Join-Path $bundle "conpty.dll"), [byte[]](0x48, 0x45, 0x52, 0x44, 0x52))
[System.IO.File]::WriteAllBytes((Join-Path $bundle "x64\OpenConsole.exe"), [byte[]](0x48, 0x45, 0x52, 0x44, 0x52))
[System.IO.File]::WriteAllBytes((Join-Path $bundle "arm64\OpenConsole.exe"), [byte[]](0x48, 0x45, 0x52, 0x44, 0x52))
try {
$rejected = $false
try {
.\scripts\windows_smoke_conpty_path.ps1 `
-ExePath $exe `
-Session "ci-conpty-invalid-windows-2022-$env:GITHUB_RUN_ID-$env:GITHUB_RUN_ATTEMPT"
} catch {
if ($_.Exception.Message -notlike "workspace create failed with exit code*") {
throw
}
$rejected = $true
}
if (-not $rejected) {
throw "Herdr accepted a tampered app-local ConPTY bundle"
}
$env:HERDR_WINDOWS_CONPTY = "system"
.\scripts\windows_smoke_conpty_path.ps1 `
-ExePath $exe `
-Session "ci-conpty-system-windows-2022-$env:GITHUB_RUN_ID-$env:GITHUB_RUN_ATTEMPT"
} finally {
Remove-Item Env:HERDR_WINDOWS_CONPTY -ErrorAction SilentlyContinue
Remove-Item -LiteralPath $bundle -Recurse -Force -ErrorAction SilentlyContinue
}
- name: Build and verify official ConPTY package
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$package = Join-Path $env:RUNNER_TEMP "Microsoft.Windows.Console.ConPTY.nupkg"
$stage = Join-Path $env:RUNNER_TEMP "herdr-windows-x86_64"
New-Item -ItemType Directory -Force -Path artifacts | Out-Null
.\scripts\package_windows_conpty.ps1 `
-HerdrExe target\x86_64-pc-windows-msvc\debug\herdr.exe `
-PackagePath $package `
-StageDir $stage `
-OutputPath artifacts\herdr-windows-x86_64.zip
"HERDR_CONPTY_PACKAGE_DIR=$stage" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
- name: Probe enhanced pane input with bundled ConPTY
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$exe = Join-Path $env:HERDR_CONPTY_PACKAGE_DIR "herdr.exe"
$consoleHost = Join-Path $env:HERDR_CONPTY_PACKAGE_DIR "conpty\x64\OpenConsole.exe"
.\scripts\windows_conpty_enhanced_input_probe.ps1 `
-ExePath $exe `
-Session "ci-conpty-bundled-windows-2022-$env:GITHUB_RUN_ID-$env:GITHUB_RUN_ATTEMPT" `
-ExpectedConsoleHostPath $consoleHost
- name: Test packaged installer and repair with Windows PowerShell 5.1
shell: pwsh
run: |
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File `
.\scripts\windows_install_conpty_package_test.ps1 `
-ArchivePath artifacts\herdr-windows-x86_64.zip
if ($LASTEXITCODE -ne 0) {
throw "Windows PowerShell installer test failed with exit code $LASTEXITCODE"
}
+11 -4
View File
@@ -139,7 +139,7 @@ jobs:
name: herdr-macos-aarch64
- target: x86_64-pc-windows-msvc
os: windows-latest
name: herdr-windows-x86_64.exe
name: herdr-windows-x86_64.zip
runs-on: ${{ matrix.os }}
env:
LIBGHOSTTY_VT_OPTIMIZE: ReleaseFast
@@ -258,7 +258,14 @@ jobs:
if: runner.os == 'Windows'
shell: pwsh
run: |
Copy-Item target\${{ matrix.target }}\release\herdr.exe ${{ matrix.name }}
$ErrorActionPreference = "Stop"
$package = Join-Path $env:RUNNER_TEMP "Microsoft.Windows.Console.ConPTY.nupkg"
$stage = Join-Path $env:RUNNER_TEMP "herdr-windows-x86_64"
.\scripts\package_windows_conpty.ps1 `
-HerdrExe target\${{ matrix.target }}\release\herdr.exe `
-PackagePath $package `
-StageDir $stage `
-OutputPath ${{ matrix.name }}
$hash = (Get-FileHash -Algorithm SHA256 ${{ matrix.name }}).Hash.ToLowerInvariant()
"$hash ${{ matrix.name }}" | Out-File -Encoding ascii ${{ matrix.name }}.sha256
"commit=${{ needs.preflight.outputs.commit }}" | Out-File -Encoding utf8 BUILD_INFO.txt
@@ -324,7 +331,7 @@ jobs:
result = {}
for path in pathlib.Path('artifacts').glob('herdr-*/*.sha256'):
digest, name = path.read_text().split()[:2]
target = name.removeprefix('herdr-').removesuffix('.exe')
target = name.removeprefix('herdr-').removesuffix('.exe').removesuffix('.zip')
result[target] = digest
pathlib.Path('preview-sha256.json').write_text(json.dumps(result, indent=2) + '\n')
PY
@@ -344,7 +351,7 @@ jobs:
artifacts/herdr-linux-aarch64/herdr-linux-aarch64
artifacts/herdr-macos-x86_64/herdr-macos-x86_64
artifacts/herdr-macos-aarch64/herdr-macos-aarch64
artifacts/herdr-windows-x86_64.exe/herdr-windows-x86_64.exe
artifacts/herdr-windows-x86_64.zip/herdr-windows-x86_64.zip
- name: Update preview manifest
run: |
Generated
+1 -11
View File
@@ -1126,7 +1126,7 @@ dependencies = [
"log",
"nix 0.28.0",
"serial2",
"shared_library",
"sha2",
"shell-words",
"winapi",
"winreg",
@@ -1517,16 +1517,6 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "shared_library"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11"
dependencies = [
"lazy_static",
"libc",
]
[[package]]
name = "shell-words"
version = "1.1.1"
@@ -96,7 +96,9 @@ Windows binaries are published only on preview releases while native Windows sup
| System | Asset |
| --- | --- |
| Windows x86_64 beta | `herdr-windows-x86_64.exe` |
| Windows x86_64 beta | `herdr-windows-x86_64.zip` |
The archive contains `herdr.exe` and its app-local ConPTY runtime. Keep the extracted directory together; do not copy only `herdr.exe`.
## Verify
@@ -96,7 +96,9 @@ mv herdr-linux-x86_64 ~/.local/bin/herdr
| システム | アセット |
| --- | --- |
| Windows x86_64 ベータ | `herdr-windows-x86_64.exe` |
| Windows x86_64 ベータ | `herdr-windows-x86_64.zip` |
アーカイブには `herdr.exe` とアプリローカルの ConPTY ランタイムが含まれます。展開したディレクトリをまとめて保持し、`herdr.exe` だけをコピーしないでください。
## 動作確認
@@ -79,6 +79,8 @@ host_cursor = "native"
Windows のターミナルは、修飾キー付きのキーをすべて同じ形で報告するわけではありません。Herdr は Windows Terminal と Windows 上の Alacritty ではマウス報告と `ctrl+j` を保持しますが、`shift+enter` は外側ターミナルが修飾付き Enter として区別して報告する場合にだけ動作します。Windows またはターミナルがそれを通常の Enter として報告する場合、Herdr は通常の Enter を転送します。
古い Windows 10 ビルドのシステム ConPTY は、Kimi や Pi などのエージェントが使用する Kitty キーボードプロトコルのシーケンスを失うため、プレビューパッケージには Microsoft の最新のアプリローカル ConPTY ランタイムが含まれます。バンドル版ランタイムとの互換性問題を診断する場合に限り、Herdr を起動する前に `HERDR_WINDOWS_CONPTY=system` を設定してください。
## コピーと貼り付け
Herdr のペインテキストコピーは Windows ベータで動作します。ペイン内でテキストをドラッグ選択すると Herdr 経由でコピーされます。
@@ -79,6 +79,8 @@ Native mode restores the IME anchor, but it can reintroduce occasional cursor fl
Windows terminals do not all report modified keys in the same shape. Herdr preserves mouse reporting and `ctrl+j` in Windows Terminal and Alacritty on Windows, but `shift+enter` only works when the outer terminal reports it as a distinct modified Enter key. If Windows or the terminal reports it as plain Enter, Herdr forwards plain Enter.
Preview packages include Microsoft's current app-local ConPTY runtime because the system ConPTY on older Windows 10 builds drops Kitty keyboard protocol sequences used by agents such as Kimi and Pi. Set `HERDR_WINDOWS_CONPTY=system` before starting Herdr only when diagnosing a compatibility problem with the bundled runtime.
## Copy and paste
Herdr's pane text copy works on Windows beta. Drag-select text inside a pane to copy through Herdr.
@@ -96,7 +96,9 @@ mv herdr-linux-x86_64 ~/.local/bin/herdr
| 系统 | 产物 |
| --- | --- |
| Windows x86_64 测试版 | `herdr-windows-x86_64.exe` |
| Windows x86_64 测试版 | `herdr-windows-x86_64.zip` |
压缩包包含 `herdr.exe` 及其应用本地 ConPTY 运行时。请保留完整的解压目录,不要只复制 `herdr.exe`。
## 验证
@@ -79,6 +79,8 @@ host_cursor = "native"
Windows 终端并不都以相同形式报告带修饰键的按键。Herdr 在 Windows Terminal 和 Windows 上的 Alacritty 中保留鼠标报告和 `ctrl+j`,但 `shift+enter` 只有在外层终端把它报告为独立的修饰 Enter 键时才有效。如果 Windows 或终端把它报告为普通 Enter,Herdr 会转发普通 Enter。
旧版 Windows 10 的系统 ConPTY 会丢弃 Kimi 和 Pi 等智能体使用的 Kitty 键盘协议序列,因此预览包包含 Microsoft 当前的应用本地 ConPTY 运行时。仅在诊断捆绑运行时的兼容性问题时,才应在启动 Herdr 前设置 `HERDR_WINDOWS_CONPTY=system`。
## 复制与粘贴
Herdr 的窗格文本复制在 Windows 测试版上可用。在窗格内拖选文本即可通过 Herdr 复制。
+2 -2
View File
@@ -3,7 +3,7 @@
# Run tests
test:
cargo nextest run --locked --status-level fail --final-status-level fail --failure-output final --success-output never
python3 -m unittest scripts.test_agent_detection_manifest_check scripts.test_changelog scripts.test_config_reference_check scripts.test_docs_translation_parity scripts.test_hermes_integration_asset scripts.test_preview scripts.test_vendor_libghostty_vt scripts.test_vendor_portable_pty
python3 -m unittest scripts.test_agent_detection_manifest_check scripts.test_changelog scripts.test_config_reference_check scripts.test_docs_translation_parity scripts.test_hermes_integration_asset scripts.test_package_windows_conpty scripts.test_preview scripts.test_vendor_libghostty_vt scripts.test_vendor_portable_pty
just integration-assets-test
just plugin-marketplace-test
@@ -29,7 +29,7 @@ windows-lint:
# Check formatting + run unit tests + Windows target lint + maintenance script tests
check: ci windows-lint
python3 -m unittest scripts.test_agent_detection_manifest_check scripts.test_changelog scripts.test_config_reference_check scripts.test_docs_translation_parity scripts.test_hermes_integration_asset scripts.test_preview scripts.test_vendor_libghostty_vt scripts.test_vendor_portable_pty
python3 -m unittest scripts.test_agent_detection_manifest_check scripts.test_changelog scripts.test_config_reference_check scripts.test_docs_translation_parity scripts.test_hermes_integration_asset scripts.test_package_windows_conpty scripts.test_preview scripts.test_vendor_libghostty_vt scripts.test_vendor_portable_pty
@echo "docs reminder: if this changes user-facing behavior, make sure the relevant release docs are updated or called out before release."
# Install repo-local git hooks
+48
View File
@@ -0,0 +1,48 @@
{
"schema_version": 1,
"package": {
"id": "Microsoft.Windows.Console.ConPTY",
"version": "1.24.260710001",
"release_tag": "v1.24.11911.0",
"url": "https://github.com/microsoft/terminal/releases/download/v1.24.11911.0/Microsoft.Windows.Console.ConPTY.1.24.260710001.nupkg",
"sha256": "9382ad7becb7e4d84e300578d8e4f4df28f43d979d9055d978c42913c47e0e9d",
"license": "MIT"
},
"bundles": {
"x86_64": {
"target_triple": "x86_64-pc-windows-msvc",
"files": [
{
"source": "runtimes/win-x64/native/conpty.dll",
"destination": "conpty/conpty.dll",
"sha256": "39fba2713e2495117b1591ae8c32a3b904bea7aa66069cf7815e2844c76d75d8",
"pe_machine": "0x8664"
},
{
"source": "build/native/runtimes/x64/OpenConsole.exe",
"destination": "conpty/x64/OpenConsole.exe",
"sha256": "b7fd936c2668b87b9ecf7b3366dc6568afc1c6f981874cba3e955a1c35cf8160",
"pe_machine": "0x8664"
},
{
"source": "build/native/runtimes/arm64/OpenConsole.exe",
"destination": "conpty/arm64/OpenConsole.exe",
"sha256": "ed7622fd0d3bedc9ab9f122f5e58edf0def9e7999224f52dd395ba9f54edbe09",
"pe_machine": "0xaa64"
}
]
}
},
"notices": [
{
"source": "packaging/windows/licenses/Microsoft.Windows.Console.ConPTY-LICENSE.txt",
"destination": "THIRD-PARTY-NOTICES/Microsoft.Windows.Console.ConPTY-LICENSE.txt",
"sha256": "5d177f23ecfeb0ea8e050b6a5a16355e1ae9a0b286436ca8f83ed08b3795be6b"
},
{
"source": "packaging/windows/licenses/Microsoft.Windows.Console.ConPTY-NOTICE.md",
"destination": "THIRD-PARTY-NOTICES/Microsoft.Windows.Console.ConPTY-NOTICE.md",
"sha256": "e7fbaadee6ab20c28b87730a510ee5f5815d8fb4bd88d1d54d282dc2a74c0726"
}
]
}
@@ -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.
@@ -0,0 +1,535 @@
# NOTICES AND INFORMATION
Do Not Translate or Localize
This software incorporates material from third parties. Microsoft makes certain
open source code available at [http://3rdpartysource.microsoft.com](http://3rdpartysource.microsoft.com), or you may
send a check or money order for US $5.00, including the product name, the open
source component name, and version number, to:
```
Source Code Compliance Team
Microsoft Corporation
One Microsoft Way
Redmond, WA 98052
USA
```
Notwithstanding any other terms, you may reverse engineer this software to the
extent required to debug changes to any libraries licensed under the GNU Lesser
General Public License.
## jsoncpp
**Source**: [https://github.com/open-source-parsers/jsoncpp](https://github.com/open-source-parsers/jsoncpp)
### License
```
Copyright (c) 2007-2010 Baptiste Lepilleur and The JsonCpp Authors
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.
```
## chromium/base/numerics
**Source**: [https://github.com/chromium/chromium/tree/master/base/numerics](https://github.com/chromium/chromium/tree/master/base/numerics)
### License
```
Copyright 2015 The Chromium Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
```
## \{fmt\}
**Source**: [https://github.com/fmtlib/fmt](https://github.com/fmtlib/fmt)
### License
```
MIT License
Copyright (c) 2012 - present, Victor Zverovich
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.
--- Optional exception to the license ---
As an exception, if, as a result of your compiling your source code, portions
of this Software are embedded into a machine-executable object form of such
source code, you may redistribute such embedded portions in such object form
without including the above copyright and permission notices.
```
## interval_tree
**Source**: [https://github.com/ekg/IntervalTree](https://github.com/ekg/IntervalTree)
### License
```
Copyright (c) 2011 Erik Garrison
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.
```
## PCG Random Number Generation
**Source**: [https://github.com/imneme/pcg-cpp](https://github.com/imneme/pcg-cpp)
### License
```
Copyright (c) 2014-2017 Melissa O'Neill and PCG Project contributors
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.
```
## wyhash
**Source**: [https://github.com/wangyi-fudan/wyhash](https://github.com/wangyi-fudan/wyhash)
### License
```
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
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 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.
For more information, please refer to <http://unlicense.org/>
```
## stb
**Source**: [https://github.com/nothings/stb](https://github.com/nothings/stb)
### License
```
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
software, either in source code form or as a compiled binary, for any purpose,
commercial or non-commercial, and by any means.
In jurisdictions that recognize copyright laws, the author or authors of this
software dedicate any and all copyright interest in the software to the public
domain. We make this dedication for the benefit of the public at large and to
the detriment of our heirs and successors. We intend this dedication to be an
overt act of relinquishment in perpetuity of all present and future rights to
this software under copyright law.
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 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.
```
## Oklab
**Source**: [https://bottosson.github.io/posts/oklab/](https://bottosson.github.io/posts/oklab/)
### License
```
Copyright (c) 2020 Björn Ottosson
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.
```
## ColorBrewer
**Source**: [https://colorbrewer2.org/](https://colorbrewer2.org/)
### License
```
Apache-Style Software License for ColorBrewer software and ColorBrewer Color Schemes
Copyright (c) 2002 Cynthia Brewer, Mark Harrower, and The Pennsylvania State University.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied. See the License for the
specific language governing permissions and limitations under the License.
```
## cmark
**Source**: [https://github.com/commonmark/cmark](https://github.com/commonmark/cmark)
### License
```
Copyright (c) 2014, John MacFarlane
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-----
houdini.h, houdini_href_e.c, houdini_html_e.c, houdini_html_u.c
derive from https://github.com/vmg/houdini (with some modifications)
Copyright (C) 2012 Vicent Martí
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.
-----
buffer.h, buffer.c, chunk.h
are derived from code (C) 2012 Github, Inc.
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.
-----
utf8.c and utf8.c
are derived from utf8proc
(<http://www.public-software-group.org/utf8proc>),
(C) 2009 Public Software Group e. V., Berlin, Germany.
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.
-----
The normalization code in normalize.py was derived from the
markdowntest project, Copyright 2013 Karl Dubost:
The MIT License (MIT)
Copyright (c) 2013 Karl Dubost
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.
-----
The CommonMark spec (test/spec.txt) is
Copyright (C) 2014-15 John MacFarlane
Released under the Creative Commons CC-BY-SA 4.0 license:
<http://creativecommons.org/licenses/by-sa/4.0/>.
-----
The test software in test/ is
Copyright (c) 2014, John MacFarlane
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
```
## fzf
### License
```
The MIT License (MIT)
Copyright (c) 2013-2024 Junegunn Choi
Copyright (c) 2021-2025 Simon Hauser
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.
```
# Microsoft Open Source
This product also incorporates source code from other Microsoft open source projects, all licensed under the MIT license.
## `GSL`
**Source**: [https://github.com/microsoft/GSL](https://github.com/microsoft/GSL)
## `Microsoft-UI-XAML`
**Source**: [https://github.com/microsoft/Microsoft-UI-XAML](https://github.com/microsoft/Microsoft-UI-XAML)
## `VirtualDesktopUtils`
**Source**: [https://github.com/microsoft/PowerToys](https://github.com/microsoft/PowerToys)
## `wil`
**Source**: [https://github.com/microsoft/wil](https://github.com/microsoft/wil)
### License
```
The MIT License
Copyright (c) Microsoft Corporation. All rights reserved.
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.
```
+53
View File
@@ -0,0 +1,53 @@
param(
[Parameter(Mandatory = $true)]
[string]$HerdrExe,
[Parameter(Mandatory = $true)]
[string]$PackagePath,
[Parameter(Mandatory = $true)]
[string]$StageDir,
[Parameter(Mandatory = $true)]
[string]$OutputPath
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
function Invoke-NativeChecked {
param(
[string]$Command,
[string[]]$Arguments
)
& $Command @Arguments
if ($LASTEXITCODE -ne 0) {
throw "$Command failed with exit code $LASTEXITCODE"
}
}
$packager = Join-Path $PSScriptRoot "package_windows_conpty.py"
Invoke-NativeChecked python @(
$packager,
"stage",
"--package", $PackagePath,
"--herdr-exe", $HerdrExe,
"--output-dir", $StageDir
)
Invoke-NativeChecked dotnet @("nuget", "verify", "--all", $PackagePath)
foreach ($relative in @("conpty\conpty.dll", "conpty\x64\OpenConsole.exe", "conpty\arm64\OpenConsole.exe")) {
$signature = Get-AuthenticodeSignature (Join-Path $StageDir $relative)
$subject = if ($null -eq $signature.SignerCertificate) { "" } else { $signature.SignerCertificate.Subject }
if ($signature.Status -ne "Valid" -or $subject -notlike "*Microsoft Corporation*") {
throw "Invalid Microsoft signature for $relative`: $($signature.Status) $subject"
}
}
Invoke-NativeChecked python @(
$packager,
"archive",
"--stage-dir", $StageDir,
"--output", $OutputPath
)
+245
View File
@@ -0,0 +1,245 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import hashlib
import json
import shutil
import struct
import tempfile
import urllib.request
import xml.etree.ElementTree as ET
import zipfile
from pathlib import Path, PurePosixPath
from typing import Any
PROJECT_ROOT = Path(__file__).resolve().parent.parent
DEFAULT_METADATA = PROJECT_ROOT / "packaging" / "windows" / "conpty.json"
MARKER_PATH = PurePosixPath("conpty/herdr-conpty.json")
DOWNLOAD_TIMEOUT_SECONDS = 60
def sha256_bytes(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
def sha256_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as source:
for chunk in iter(lambda: source.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def load_metadata(path: Path) -> dict[str, Any]:
data = json.loads(path.read_text(encoding="utf-8"))
if data.get("schema_version") != 1:
raise ValueError("unsupported ConPTY metadata schema")
return data
def pe_machine(data: bytes) -> int:
if len(data) < 0x40 or data[:2] != b"MZ":
raise ValueError("file is not a PE image")
pe_offset = struct.unpack_from("<I", data, 0x3C)[0]
if pe_offset + 6 > len(data) or data[pe_offset : pe_offset + 4] != b"PE\0\0":
raise ValueError("file has an invalid PE header")
return struct.unpack_from("<H", data, pe_offset + 4)[0]
def validate_nuspec(archive: zipfile.ZipFile, package: dict[str, Any]) -> None:
nuspec_name = f"{package['id']}.nuspec"
try:
root = ET.fromstring(archive.read(nuspec_name).decode("utf-8-sig"))
except KeyError as error:
raise ValueError(f"package is missing {nuspec_name}") from error
namespace = {"n": "http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd"}
metadata = root.find("n:metadata", namespace)
if metadata is None:
raise ValueError("package NuSpec is missing metadata")
expected = {
"id": package["id"],
"version": package["version"],
"license": package["license"],
}
for field, expected_value in expected.items():
element = metadata.find(f"n:{field}", namespace)
actual = element.text.strip() if element is not None and element.text else None
if actual != expected_value:
raise ValueError(
f"package NuSpec {field} mismatch: expected {expected_value!r}, got {actual!r}"
)
def acquire_package(package: dict[str, Any], package_path: Path) -> None:
package_path.parent.mkdir(parents=True, exist_ok=True)
if not package_path.exists():
with urllib.request.urlopen(
package["url"], timeout=DOWNLOAD_TIMEOUT_SECONDS
) as response, package_path.open("wb") as output:
shutil.copyfileobj(response, output)
actual = sha256_file(package_path)
if actual != package["sha256"]:
raise ValueError(
f"ConPTY package hash mismatch: expected {package['sha256']}, got {actual}"
)
def marker_data(metadata: dict[str, Any], architecture: str) -> bytes:
bundle = metadata["bundles"][architecture]
marker = {
"schema_version": 1,
"package": metadata["package"]["id"],
"version": metadata["package"]["version"],
"architecture": architecture,
"files": {
item["destination"]: item["sha256"] for item in bundle["files"]
},
}
return (json.dumps(marker, indent=2, sort_keys=True) + "\n").encode()
def stage_bundle(
metadata_path: Path,
architecture: str,
package_path: Path,
herdr_exe: Path,
output_dir: Path,
) -> None:
metadata = load_metadata(metadata_path)
if architecture not in metadata["bundles"]:
raise ValueError(f"unsupported Windows architecture: {architecture}")
if output_dir.exists():
raise ValueError(f"output directory already exists: {output_dir}")
if not herdr_exe.is_file():
raise ValueError(f"Herdr executable does not exist: {herdr_exe}")
acquire_package(metadata["package"], package_path)
bundle = metadata["bundles"][architecture]
metadata_root = metadata_path.resolve().parent
with zipfile.ZipFile(package_path) as archive, tempfile.TemporaryDirectory(
prefix="herdr-conpty-stage-", dir=output_dir.parent
) as temporary:
validate_nuspec(archive, metadata["package"])
staging = Path(temporary) / "bundle"
staging.mkdir()
shutil.copy2(herdr_exe, staging / "herdr.exe")
for item in bundle["files"]:
try:
payload = archive.read(item["source"])
except KeyError as error:
raise ValueError(f"package is missing {item['source']}") from error
actual_hash = sha256_bytes(payload)
if actual_hash != item["sha256"]:
raise ValueError(
f"hash mismatch for {item['source']}: expected {item['sha256']}, got {actual_hash}"
)
actual_machine = pe_machine(payload)
expected_machine = int(item["pe_machine"], 16)
if actual_machine != expected_machine:
raise ValueError(
f"PE machine mismatch for {item['source']}: "
f"expected {item['pe_machine']}, got 0x{actual_machine:04x}"
)
destination = staging / PurePosixPath(item["destination"])
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_bytes(payload)
marker = staging / MARKER_PATH
marker.parent.mkdir(parents=True, exist_ok=True)
marker.write_bytes(marker_data(metadata, architecture))
for notice in metadata["notices"]:
source = (metadata_root.parent.parent / notice["source"]).resolve()
actual_hash = sha256_file(source)
if actual_hash != notice["sha256"]:
raise ValueError(
f"notice hash mismatch for {source}: expected {notice['sha256']}, got {actual_hash}"
)
destination = staging / PurePosixPath(notice["destination"])
destination.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(source, destination)
staging.rename(output_dir)
def expected_stage_files(metadata: dict[str, Any], architecture: str) -> set[str]:
files = {"herdr.exe", MARKER_PATH.as_posix()}
files.update(item["destination"] for item in metadata["bundles"][architecture]["files"])
files.update(item["destination"] for item in metadata["notices"])
return files
def validate_stage(metadata_path: Path, architecture: str, stage_dir: Path) -> None:
metadata = load_metadata(metadata_path)
actual = {
path.relative_to(stage_dir).as_posix()
for path in stage_dir.rglob("*")
if path.is_file()
}
expected = expected_stage_files(metadata, architecture)
if actual != expected:
raise ValueError(
f"bundle layout mismatch; missing={sorted(expected - actual)}, unexpected={sorted(actual - expected)}"
)
if (stage_dir / MARKER_PATH).read_bytes() != marker_data(metadata, architecture):
raise ValueError("bundle marker does not match pinned ConPTY metadata")
for item in metadata["bundles"][architecture]["files"]:
path = stage_dir / PurePosixPath(item["destination"])
actual_hash = sha256_file(path)
if actual_hash != item["sha256"]:
raise ValueError(f"staged file hash mismatch for {path}")
for notice in metadata["notices"]:
path = stage_dir / PurePosixPath(notice["destination"])
actual_hash = sha256_file(path)
if actual_hash != notice["sha256"]:
raise ValueError(f"staged notice hash mismatch for {path}")
def archive_bundle(
metadata_path: Path, architecture: str, stage_dir: Path, output_path: Path
) -> None:
validate_stage(metadata_path, architecture, stage_dir)
output_path.parent.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(output_path, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive:
for path in sorted(stage_dir.rglob("*")):
if path.is_file():
archive.write(path, path.relative_to(stage_dir).as_posix())
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Package Herdr with Microsoft's pinned ConPTY runtime")
parser.add_argument("--metadata", type=Path, default=DEFAULT_METADATA)
subparsers = parser.add_subparsers(dest="command", required=True)
stage = subparsers.add_parser("stage")
stage.add_argument("--architecture", choices=("x86_64",), default="x86_64")
stage.add_argument("--package", type=Path, required=True)
stage.add_argument("--herdr-exe", type=Path, required=True)
stage.add_argument("--output-dir", type=Path, required=True)
archive = subparsers.add_parser("archive")
archive.add_argument("--architecture", choices=("x86_64",), default="x86_64")
archive.add_argument("--stage-dir", type=Path, required=True)
archive.add_argument("--output", type=Path, required=True)
return parser.parse_args()
def main() -> None:
args = parse_args()
if args.command == "stage":
stage_bundle(
args.metadata,
args.architecture,
args.package,
args.herdr_exe,
args.output_dir,
)
else:
archive_bundle(args.metadata, args.architecture, args.stage_dir, args.output)
if __name__ == "__main__":
main()
+5 -1
View File
@@ -17,7 +17,7 @@ ASSET_TARGETS = (
)
EXPECTED_ASSET_NAMES = {
**{target: f"herdr-{target}" for target in ASSET_TARGETS},
"windows-x86_64": "herdr-windows-x86_64.exe",
"windows-x86_64": "herdr-windows-x86_64.zip",
}
HIDDEN_SUBJECTS = (
"docs: update website manifest",
@@ -188,6 +188,10 @@ def asset_objects(urls: dict[str, str], shas: dict[str, str]) -> dict[str, dict[
sha = shas.get(target)
if sha:
entry["sha256"] = sha
if target.startswith("windows-"):
if not sha or not re.fullmatch(r"[0-9a-fA-F]{64}", sha):
raise ValueError(f"{target} requires a SHA-256 digest")
entry["format"] = "zip"
assets[target] = entry
return assets
+171
View File
@@ -0,0 +1,171 @@
from __future__ import annotations
import hashlib
import io
import json
import struct
import tempfile
import unittest
import zipfile
from pathlib import Path
from unittest import mock
from scripts import package_windows_conpty as package
class WindowsConptyPackageTests(unittest.TestCase):
def test_pinned_metadata_and_notices_are_consistent(self) -> None:
metadata = package.load_metadata(package.DEFAULT_METADATA)
self.assertEqual(metadata["package"]["id"], "Microsoft.Windows.Console.ConPTY")
self.assertEqual(metadata["package"]["version"], "1.24.260710001")
self.assertEqual(
{item["destination"] for item in metadata["bundles"]["x86_64"]["files"]},
{
"conpty/conpty.dll",
"conpty/x64/OpenConsole.exe",
"conpty/arm64/OpenConsole.exe",
},
)
loader = (
package.PROJECT_ROOT / "vendor/portable-pty/src/win/psuedocon.rs"
).read_text(encoding="utf-8")
installer = (package.PROJECT_ROOT / "website/install.ps1").read_text(
encoding="utf-8"
)
for item in metadata["bundles"]["x86_64"]["files"]:
self.assertIn(item["sha256"], loader)
self.assertNotIn(item["sha256"], installer)
self.assertIn('Get-Content -LiteralPath $markerPath -Raw', installer)
self.assertIn('$filesProperty.Value.PSObject.Properties[$relative]', installer)
for notice in metadata["notices"]:
source = package.PROJECT_ROOT / notice["source"]
self.assertEqual(package.sha256_file(source), notice["sha256"])
def test_powershell_wrapper_verifies_package_and_signatures(self) -> None:
wrapper = (package.PROJECT_ROOT / "scripts/package_windows_conpty.ps1").read_text(
encoding="utf-8"
)
self.assertIn('"nuget", "verify", "--all"', wrapper)
self.assertIn("Get-AuthenticodeSignature", wrapper)
self.assertIn('conpty\\arm64\\OpenConsole.exe', wrapper)
self.assertIn('conpty\\x64\\OpenConsole.exe', wrapper)
self.assertIn('conpty\\conpty.dll', wrapper)
self.assertIn('"*Microsoft Corporation*"', wrapper)
def test_package_download_has_a_finite_timeout(self) -> None:
payload = b"package"
with tempfile.TemporaryDirectory() as temporary:
destination = Path(temporary) / "conpty.nupkg"
metadata = {
"url": "https://example.invalid/conpty.nupkg",
"sha256": hashlib.sha256(payload).hexdigest(),
}
with mock.patch.object(
package.urllib.request, "urlopen", return_value=io.BytesIO(payload)
) as urlopen:
package.acquire_package(metadata, destination)
urlopen.assert_called_once_with(
metadata["url"], timeout=package.DOWNLOAD_TIMEOUT_SECONDS
)
def test_stage_and_archive_validate_exact_package(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
dll = self._pe(0x8664, b"dll")
x64_host = self._pe(0x8664, b"x64")
arm64_host = self._pe(0xAA64, b"arm64")
files = [
self._file("runtimes/win-x64/native/conpty.dll", "conpty/conpty.dll", dll, "0x8664"),
self._file(
"build/native/runtimes/x64/OpenConsole.exe",
"conpty/x64/OpenConsole.exe",
x64_host,
"0x8664",
),
self._file(
"build/native/runtimes/arm64/OpenConsole.exe",
"conpty/arm64/OpenConsole.exe",
arm64_host,
"0xaa64",
),
]
nupkg = root / "conpty.nupkg"
self._write_package(nupkg, files, {item["source"]: data for item, data in zip(files, (dll, x64_host, arm64_host))})
metadata_path = root / "conpty.json"
metadata_path.write_text(
json.dumps(
{
"schema_version": 1,
"package": {
"id": "Microsoft.Windows.Console.ConPTY",
"version": "1.24.260710001",
"url": nupkg.as_uri(),
"sha256": package.sha256_file(nupkg),
"license": "MIT",
},
"bundles": {"x86_64": {"files": files}},
"notices": [],
}
),
encoding="utf-8",
)
herdr = root / "input-herdr.exe"
herdr.write_bytes(b"herdr")
stage = root / "stage"
package.stage_bundle(metadata_path, "x86_64", nupkg, herdr, stage)
package.validate_stage(metadata_path, "x86_64", stage)
output = root / "herdr.zip"
package.archive_bundle(metadata_path, "x86_64", stage, output)
with zipfile.ZipFile(output) as archive:
self.assertEqual(
set(archive.namelist()),
package.expected_stage_files(
package.load_metadata(metadata_path), "x86_64"
),
)
(stage / "conpty" / "conpty.dll").write_bytes(b"tampered")
with self.assertRaisesRegex(ValueError, "staged file hash mismatch"):
package.validate_stage(metadata_path, "x86_64", stage)
@staticmethod
def _pe(machine: int, payload: bytes) -> bytes:
data = bytearray(0x80)
data[:2] = b"MZ"
struct.pack_into("<I", data, 0x3C, 0x40)
data[0x40:0x44] = b"PE\0\0"
struct.pack_into("<H", data, 0x44, machine)
return bytes(data) + payload
@staticmethod
def _file(source: str, destination: str, data: bytes, machine: str) -> dict[str, str]:
return {
"source": source,
"destination": destination,
"sha256": hashlib.sha256(data).hexdigest(),
"pe_machine": machine,
}
@staticmethod
def _write_package(
path: Path, files: list[dict[str, str]], payloads: dict[str, bytes]
) -> None:
nuspec = """<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
<metadata>
<id>Microsoft.Windows.Console.ConPTY</id>
<version>1.24.260710001</version>
<license type="expression">MIT</license>
</metadata>
</package>
"""
with zipfile.ZipFile(path, "w") as archive:
archive.writestr("Microsoft.Windows.Console.ConPTY.nuspec", nuspec)
for item in files:
archive.writestr(item["source"], payloads[item["source"]])
if __name__ == "__main__":
unittest.main()
+27 -2
View File
@@ -39,7 +39,10 @@ class PreviewNotesTests(unittest.TestCase):
base_version="0.6.6",
protocol=12,
notes=notes,
shas={"linux-x86_64": "deadbeef"},
shas={
"linux-x86_64": "deadbeef",
"windows-x86_64": "a" * 64,
},
retain=30,
)
data = json.loads(content)
@@ -51,10 +54,32 @@ class PreviewNotesTests(unittest.TestCase):
)
self.assertEqual(
data["assets"]["windows-x86_64"]["url"],
"https://github.com/ogulcancelik/herdr/releases/download/preview-2026-06-02-abcdef123456/herdr-windows-x86_64.exe",
"https://github.com/ogulcancelik/herdr/releases/download/preview-2026-06-02-abcdef123456/herdr-windows-x86_64.zip",
)
self.assertEqual(
data["assets"]["windows-x86_64"]["sha256"],
"a" * 64,
)
self.assertEqual(data["assets"]["windows-x86_64"]["format"], "zip")
self.assertIn("2026-06-02-abcdef123456", data["builds"])
def test_windows_preview_asset_requires_sha256(self):
with tempfile.TemporaryDirectory() as tmp:
with self.assertRaisesRegex(ValueError, "windows-x86_64 requires"):
preview.build_manifest(
output=Path(tmp) / "preview.json",
repo="ogulcancelik/herdr",
tag="preview-test",
build_id="test",
commit="abcdef",
built_at="2026-06-02T03:00:00Z",
base_version="0.6.6",
protocol=12,
notes="test",
shas={},
retain=1,
)
def test_hidden_subjects_include_preview_manifest_commits(self):
self.assertTrue(preview.hidden_subject("docs: update preview manifest"))
self.assertTrue(preview.hidden_subject("docs: update website manifest"))
+11 -2
View File
@@ -103,13 +103,22 @@ class VendorPortablePtyTests(unittest.TestCase):
f"stderr:\n{result.stderr}",
)
def test_windows_conpty_loader_does_not_probe_path_conpty_dll(self) -> None:
def test_windows_conpty_loader_uses_only_controlled_sources(self) -> None:
project_root = Path(__file__).resolve().parent.parent
source = project_root / "vendor" / "portable-pty" / "src" / "win" / "psuedocon.rs"
text = source.read_text()
self.assertIn('ConPtyFuncs::open(Path::new("kernel32.dll"))', text)
self.assertIn("std::env::current_exe()", text)
self.assertIn('.join("conpty")', text)
self.assertIn('"x64/OpenConsole.exe"', text)
self.assertIn('"arm64/OpenConsole.exe"', text)
self.assertIn("LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR", text)
self.assertIn("LOAD_LIBRARY_SEARCH_SYSTEM32", text)
self.assertIn("GetModuleHandleW", text)
self.assertIn("HERDR_WINDOWS_CONPTY", text)
self.assertIn("Sha256::new()", text)
self.assertNotIn('Path::new("conpty.dll")', text)
self.assertNotIn("shared_library", text)
if __name__ == "__main__":
@@ -0,0 +1,418 @@
param(
[Parameter(Mandatory = $true)]
[string] $ExePath,
[string] $Session = "conpty-input-$([guid]::NewGuid().ToString('N'))",
[string] $ExpectedConsoleHostPath = ""
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
function Invoke-Checked {
param([string] $Command, [string[]] $Arguments)
& $Command @Arguments
if ($LASTEXITCODE -ne 0) {
throw "command failed with exit code $LASTEXITCODE`: $Command $($Arguments -join ' ')"
}
}
function Read-Pane {
param([string] $PaneId)
$output = & $script:Exe pane read $PaneId --source recent-unwrapped --lines 200 --format text 2>&1
if ($LASTEXITCODE -ne 0) {
throw "pane read failed with exit code $LASTEXITCODE`: $($output -join "`n")"
}
return $output -join "`n"
}
function Wait-PaneText {
param(
[string] $PaneId,
[string] $Needle,
[int] $TimeoutSeconds = 10
)
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
do {
$text = Read-Pane -PaneId $PaneId
if ($text.Contains($Needle)) {
return $text
}
Start-Sleep -Milliseconds 200
} while ((Get-Date) -lt $deadline)
return $text
}
function New-ProbePane {
param([string] $Mode)
$created = & $script:Exe workspace create --cwd $PWD.Path 2>&1
if ($LASTEXITCODE -ne 0) {
throw "workspace create failed with exit code $LASTEXITCODE`: $($created -join "`n")"
}
$paneId = (($created -join "`n") | ConvertFrom-Json).result.root_pane.pane_id
if ([string]::IsNullOrWhiteSpace($paneId)) {
throw "workspace create did not return a root pane id: $($created -join "`n")"
}
$command = "& '$script:ProbeExe' $Mode"
Invoke-Checked $script:Exe @("pane", "run", $paneId, $command)
$ready = "PROBE_READY_$($Mode.ToUpperInvariant())"
$text = Wait-PaneText -PaneId $paneId -Needle $ready
if (-not $text.Contains($ready)) {
throw "$Mode probe did not become ready: $text"
}
return $paneId
}
function Get-LatestProbeHex {
param([string] $PaneText)
$hexMatches = [regex]::Matches($PaneText, "PROBE_ALL:([0-9a-f]+)")
if ($hexMatches.Count -eq 0) {
return ""
}
return $hexMatches[$hexMatches.Count - 1].Groups[1].Value
}
function Wait-PaneHexAppend {
param(
[string] $PaneId,
[string] $PreviousHex,
[string] $ExpectedHex,
[int] $TimeoutSeconds = 4
)
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
do {
$paneText = Read-Pane -PaneId $PaneId
$latestHex = Get-LatestProbeHex -PaneText $paneText
if ($latestHex.StartsWith($PreviousHex)) {
$appendedHex = $latestHex.Substring($PreviousHex.Length)
if ($appendedHex -eq $ExpectedHex) {
return [ordered]@{ delivered = $true; pane = $paneText }
}
}
Start-Sleep -Milliseconds 200
} while ((Get-Date) -lt $deadline)
return [ordered]@{ delivered = $false; pane = $paneText }
}
function Send-KeyAndObserve {
param(
[string] $PaneId,
[string] $Key,
[string] $ExpectedHex
)
$before = Get-LatestProbeHex -PaneText (Read-Pane -PaneId $PaneId)
Invoke-Checked $script:Exe @("pane", "send-keys", $PaneId, $Key)
$observed = Wait-PaneHexAppend -PaneId $PaneId -PreviousHex $before -ExpectedHex $ExpectedHex
return [ordered]@{
key = $Key
expected_hex = $ExpectedHex
delivered = $observed.delivered
pane = $observed.pane
}
}
function Send-RawAndObserve {
param(
[string] $PaneId,
[string] $Text,
[string] $ExpectedHex
)
$before = Get-LatestProbeHex -PaneText (Read-Pane -PaneId $PaneId)
& $script:Exe pane send-text $PaneId $Text | Out-Null
if ($LASTEXITCODE -ne 0) {
throw "pane send-text failed with exit code $LASTEXITCODE"
}
$observed = Wait-PaneHexAppend -PaneId $PaneId -PreviousHex $before -ExpectedHex $ExpectedHex
return [ordered]@{
expected_hex = $ExpectedHex
delivered = $observed.delivered
pane = $observed.pane
}
}
$script:Exe = (Resolve-Path $ExePath).Path
$workDir = Join-Path ([System.IO.Path]::GetTempPath()) "herdr-conpty-input-$([guid]::NewGuid().ToString('N'))"
$probeSource = Join-Path $workDir "probe.rs"
$script:ProbeExe = Join-Path $workDir "probe.exe"
$oldSession = $env:HERDR_SESSION
$oldSocket = $env:HERDR_SOCKET_PATH
$oldClientSocket = $env:HERDR_CLIENT_SOCKET_PATH
$server = $null
$report = [ordered]@{}
$failed = $false
$initialConsoleHostIds = @(Get-Process -Name conhost, OpenConsole -ErrorAction SilentlyContinue | ForEach-Object { $_.Id })
$expectedConsoleHost = if ([string]::IsNullOrWhiteSpace($ExpectedConsoleHostPath)) {
$null
} else {
(Resolve-Path $ExpectedConsoleHostPath).Path
}
try {
New-Item -ItemType Directory -Force -Path $workDir | Out-Null
@'
use std::ffi::c_void;
const STD_INPUT_HANDLE: u32 = -10i32 as u32;
const STD_OUTPUT_HANDLE: u32 = -11i32 as u32;
const ENABLE_PROCESSED_INPUT: u32 = 0x0001;
const ENABLE_LINE_INPUT: u32 = 0x0002;
const ENABLE_ECHO_INPUT: u32 = 0x0004;
const ENABLE_VIRTUAL_TERMINAL_INPUT: u32 = 0x0200;
type Handle = *mut c_void;
#[link(name = "Kernel32")]
extern "system" {
fn GetStdHandle(kind: u32) -> Handle;
fn GetConsoleMode(handle: Handle, mode: *mut u32) -> i32;
fn SetConsoleMode(handle: Handle, mode: u32) -> i32;
fn ReadFile(
handle: Handle,
buffer: *mut c_void,
bytes_to_read: u32,
bytes_read: *mut u32,
overlapped: *mut c_void,
) -> i32;
fn WriteFile(
handle: Handle,
buffer: *const c_void,
bytes_to_write: u32,
bytes_written: *mut u32,
overlapped: *mut c_void,
) -> i32;
}
fn write_all(handle: Handle, mut bytes: &[u8]) {
while !bytes.is_empty() {
let mut written = 0;
let ok = unsafe {
WriteFile(
handle,
bytes.as_ptr().cast(),
bytes.len() as u32,
&mut written,
std::ptr::null_mut(),
)
};
if ok == 0 || written == 0 {
std::process::exit(3);
}
bytes = &bytes[written as usize..];
}
}
fn main() {
let mode = std::env::args().nth(1).unwrap_or_else(|| "legacy".to_string());
let input = unsafe { GetStdHandle(STD_INPUT_HANDLE) };
let output = unsafe { GetStdHandle(STD_OUTPUT_HANDLE) };
let mut console_mode = 0;
if unsafe { GetConsoleMode(input, &mut console_mode) } == 0 {
write_all(output, b"PROBE_ERROR_GET_MODE\r\n");
std::process::exit(1);
}
let raw_vt_mode = (console_mode | ENABLE_VIRTUAL_TERMINAL_INPUT)
& !(ENABLE_PROCESSED_INPUT | ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT);
if unsafe { SetConsoleMode(input, raw_vt_mode) } == 0 {
write_all(output, b"PROBE_ERROR_SET_MODE\r\n");
std::process::exit(2);
}
if mode == "kitty" {
write_all(output, b"\x1b[>7u\x1b[?u\x1b[cPROBE_READY_KITTY\r\n");
} else {
write_all(output, b"PROBE_READY_LEGACY\r\n");
}
let mut all = Vec::new();
let mut buffer = [0u8; 256];
loop {
let mut read = 0;
let ok = unsafe {
ReadFile(
input,
buffer.as_mut_ptr().cast(),
buffer.len() as u32,
&mut read,
std::ptr::null_mut(),
)
};
if ok == 0 || read == 0 {
break;
}
all.extend_from_slice(&buffer[..read as usize]);
let mut line = String::from("PROBE_ALL:");
for byte in &all {
use std::fmt::Write as _;
let _ = write!(&mut line, "{byte:02x}");
}
line.push_str("\r\n");
write_all(output, line.as_bytes());
}
}
'@ | Set-Content -NoNewline -Encoding utf8 $probeSource
Invoke-Checked rustc @("--edition", "2021", $probeSource, "-o", $script:ProbeExe)
$env:HERDR_SESSION = $Session
Remove-Item Env:HERDR_SOCKET_PATH -ErrorAction SilentlyContinue
Remove-Item Env:HERDR_CLIENT_SOCKET_PATH -ErrorAction SilentlyContinue
$os = Get-CimInstance Win32_OperatingSystem
$report.os = [ordered]@{
caption = $os.Caption
version = $os.Version
build = $os.BuildNumber
}
Invoke-Checked $script:Exe @("--version")
& $script:Exe --default-config | Out-Null
if ($LASTEXITCODE -ne 0) {
throw "command failed with exit code $LASTEXITCODE`: $script:Exe --default-config"
}
$server = Start-Process -FilePath $script:Exe -ArgumentList "server" -PassThru -WindowStyle Hidden
$deadline = (Get-Date).AddSeconds(10)
$serverReady = $false
do {
Start-Sleep -Milliseconds 250
$status = & $script:Exe status server 2>&1
if ($LASTEXITCODE -eq 0 -and (($status -join "`n") -match "status: running")) {
$serverReady = $true
break
}
} while ((Get-Date) -lt $deadline)
if (-not $serverReady) {
throw "server did not become ready: $($status -join "`n")"
}
$legacyPane = New-ProbePane -Mode "legacy"
$report.legacy_alt_v = Send-KeyAndObserve -PaneId $legacyPane -Key "alt+v" -ExpectedHex "1b76"
$kittyPane = New-ProbePane -Mode "kitty"
$report.kitty_initial = Wait-PaneText -PaneId $kittyPane -Needle "1b5b3f3775"
$kittyInitialHex = Get-LatestProbeHex -PaneText $report.kitty_initial
$report.device_attributes_response = $kittyInitialHex -match "1b5b3f(?:3[0-9]|3b)+63"
$report.kitty_query_response = $kittyInitialHex.Contains("1b5b3f3775")
$report.kitty_alt_v = Send-KeyAndObserve -PaneId $kittyPane -Key "alt+v" -ExpectedHex "1b5b3131383b333a3175"
$report.kitty_ctrl_u = Send-KeyAndObserve -PaneId $kittyPane -Key "ctrl+u" -ExpectedHex "1b5b3131373b353a3175"
$report.kitty_ctrl_v = Send-KeyAndObserve -PaneId $kittyPane -Key "ctrl+v" -ExpectedHex "1b5b3131383b353a3175"
$report.kitty_shift_enter = Send-KeyAndObserve -PaneId $kittyPane -Key "shift+enter" -ExpectedHex "1b5b31333b3275"
$report.kitty_ctrl_backspace = Send-KeyAndObserve -PaneId $kittyPane -Key "ctrl+backspace" -ExpectedHex "1b5b3132373b3575"
$report.kitty_up = Send-KeyAndObserve -PaneId $kittyPane -Key "up" -ExpectedHex "1b5b313b313a3141"
$report.kitty_escape = Send-KeyAndObserve -PaneId $kittyPane -Key "esc" -ExpectedHex "1b5b323775"
$report.raw_kitty_alt_v = Send-RawAndObserve -PaneId $kittyPane -Text ([char]27 + "[118;3:1u") -ExpectedHex "1b5b3131383b333a3175"
$report.raw_kitty_ctrl_u = Send-RawAndObserve -PaneId $kittyPane -Text ([char]27 + "[117;5:1u") -ExpectedHex "1b5b3131373b353a3175"
$report.raw_kitty_ctrl_v = Send-RawAndObserve -PaneId $kittyPane -Text ([char]27 + "[118;5:1u") -ExpectedHex "1b5b3131383b353a3175"
$report.raw_kitty_shift_enter = Send-RawAndObserve -PaneId $kittyPane -Text ([char]27 + "[13;2u") -ExpectedHex "1b5b31333b3275"
$report.raw_kitty_ctrl_backspace = Send-RawAndObserve -PaneId $kittyPane -Text ([char]27 + "[127;5u") -ExpectedHex "1b5b3132373b3575"
$report.raw_kitty_ctrl_delete = Send-RawAndObserve -PaneId $kittyPane -Text ([char]27 + "[57426;5u") -ExpectedHex "1b5b35373432363b3575"
$report.raw_alt_v = Send-RawAndObserve -PaneId $kittyPane -Text ([char]27 + "v") -ExpectedHex "1b76"
$report.raw_ctrl_u = Send-RawAndObserve -PaneId $kittyPane -Text ([string][char]0x15) -ExpectedHex "15"
$consoleHosts = @(Get-Process -Name conhost, OpenConsole -ErrorAction SilentlyContinue |
Where-Object { $initialConsoleHostIds -notcontains $_.Id } |
ForEach-Object {
$path = ""
$version = ""
try { $path = $_.Path } catch {}
try { $version = $_.MainModule.FileVersionInfo.FileVersion } catch {}
[ordered]@{
id = $_.Id
name = $_.ProcessName
path = $path
version = $version
}
})
$report.new_console_hosts = $consoleHosts
$appLocalHostRequired = $null -ne $expectedConsoleHost
if ($appLocalHostRequired) {
$report.expected_console_host = $expectedConsoleHost
$report.app_local_console_host = @($consoleHosts | Where-Object {
$_.name -ieq "OpenConsole" -and $_.path -ieq $expectedConsoleHost
}).Count -gt 0
} else {
$report.app_local_console_host = $null
}
$failed = -not $report.legacy_alt_v.delivered `
-or -not $report.device_attributes_response `
-or -not $report.kitty_query_response `
-or -not $report.kitty_alt_v.delivered `
-or -not $report.kitty_ctrl_u.delivered `
-or -not $report.kitty_ctrl_v.delivered `
-or -not $report.kitty_shift_enter.delivered `
-or -not $report.kitty_ctrl_backspace.delivered `
-or -not $report.kitty_up.delivered `
-or -not $report.kitty_escape.delivered `
-or -not $report.raw_kitty_alt_v.delivered `
-or -not $report.raw_kitty_ctrl_u.delivered `
-or -not $report.raw_kitty_ctrl_v.delivered `
-or -not $report.raw_kitty_shift_enter.delivered `
-or -not $report.raw_kitty_ctrl_backspace.delivered `
-or -not $report.raw_kitty_ctrl_delete.delivered `
-or -not $report.raw_alt_v.delivered `
-or -not $report.raw_ctrl_u.delivered `
-or ($appLocalHostRequired -and -not $report.app_local_console_host)
} finally {
if ($null -ne $server) {
try {
$stopOutput = & $script:Exe server stop 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Host "server stop during cleanup failed: $($stopOutput -join "`n")"
}
} catch {
Write-Host "server stop during cleanup failed: $($_.Exception.Message)"
}
Wait-Process -Id $server.Id -Timeout 10 -ErrorAction SilentlyContinue
$server.Refresh()
if (-not $server.HasExited) {
& taskkill.exe /PID $server.Id /T /F 2>&1 | Out-Null
}
}
if ($null -ne $expectedConsoleHost) {
$hostExitDeadline = (Get-Date).AddSeconds(5)
do {
$remainingHosts = @(Get-Process -Name OpenConsole -ErrorAction SilentlyContinue |
Where-Object { $initialConsoleHostIds -notcontains $_.Id } |
Where-Object {
$path = ""
try { $path = $_.Path } catch {}
$path -ieq $expectedConsoleHost
})
if ($remainingHosts.Count -eq 0) {
break
}
Start-Sleep -Milliseconds 100
} while ((Get-Date) -lt $hostExitDeadline)
$report.app_local_console_host_exited = $remainingHosts.Count -eq 0
if (-not $report.app_local_console_host_exited) {
$failed = $true
}
}
$global:LASTEXITCODE = 0
if ($null -eq $oldSession) {
Remove-Item Env:HERDR_SESSION -ErrorAction SilentlyContinue
} else {
$env:HERDR_SESSION = $oldSession
}
if ($null -eq $oldSocket) {
Remove-Item Env:HERDR_SOCKET_PATH -ErrorAction SilentlyContinue
} else {
$env:HERDR_SOCKET_PATH = $oldSocket
}
if ($null -eq $oldClientSocket) {
Remove-Item Env:HERDR_CLIENT_SOCKET_PATH -ErrorAction SilentlyContinue
} else {
$env:HERDR_CLIENT_SOCKET_PATH = $oldClientSocket
}
$json = $report | ConvertTo-Json -Depth 8
Write-Host $json
Remove-Item -Recurse -Force $workDir -ErrorAction SilentlyContinue
}
if ($failed) {
throw "enhanced Windows ConPTY input probe failed; see the JSON report above"
}
@@ -0,0 +1,146 @@
param(
[Parameter(Mandatory = $true)]
[string]$ArchivePath
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$archive = (Resolve-Path -LiteralPath $ArchivePath).Path
$root = Join-Path $env:RUNNER_TEMP ("herdr-installer-test-" + [Guid]::NewGuid().ToString("N"))
$webRoot = Join-Path $root "web"
$herdrHome = Join-Path $root "home"
$installDir = Join-Path $root "bin"
New-Item -ItemType Directory -Force -Path $webRoot | Out-Null
Copy-Item -LiteralPath $archive -Destination (Join-Path $webRoot "herdr-windows-x86_64.zip")
$hash = (Get-FileHash -Algorithm SHA256 $archive).Hash.ToLowerInvariant()
$listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 0)
$listener.Start()
$port = ([System.Net.IPEndPoint]$listener.LocalEndpoint).Port
$listener.Stop()
$manifest = @{
channel = "preview"
base_version = "0.0.0"
build_id = "installer-test"
assets = @{
"windows-x86_64" = @{
url = "http://127.0.0.1:$port/herdr-windows-x86_64.zip"
sha256 = $hash
format = "zip"
}
}
} | ConvertTo-Json -Depth 5
$manifestPath = Join-Path $webRoot "preview.json"
$manifest | Out-File -LiteralPath $manifestPath -Encoding utf8
$server = $null
$oldHerdrHome = $env:HERDR_HOME
try {
$server = Start-Process python -ArgumentList @("-m", "http.server", "$port", "--bind", "127.0.0.1", "--directory", $webRoot) -PassThru -WindowStyle Hidden
$env:HERDR_HOME = Join-Path $root "unused\..\home"
$manifestUrl = "http://127.0.0.1:$port/preview.json"
for ($attempt = 0; $attempt -lt 20; $attempt++) {
try {
Invoke-WebRequest -Uri $manifestUrl -UseBasicParsing | Out-Null
break
} catch {
if ($attempt -eq 19) { throw }
Start-Sleep -Milliseconds 100
}
}
& "$PSScriptRoot\..\website\install.ps1" `
-ManifestUrl $manifestUrl `
-InstallDir $installDir `
-ExpectedBuildId "installer-test"
$required = @(
"herdr.exe",
"conpty\herdr-conpty.json",
"conpty\conpty.dll",
"conpty\x64\OpenConsole.exe",
"conpty\arm64\OpenConsole.exe",
"THIRD-PARTY-NOTICES\Microsoft.Windows.Console.ConPTY-LICENSE.txt",
"THIRD-PARTY-NOTICES\Microsoft.Windows.Console.ConPTY-NOTICE.md"
)
foreach ($relative in $required) {
if (-not (Test-Path -LiteralPath (Join-Path $installDir $relative) -PathType Leaf)) {
throw "installer did not activate required file $relative"
}
}
$releaseDir = Get-ChildItem -LiteralPath (Join-Path $herdrHome "packages\standalone\releases") -Directory |
Where-Object { -not $_.Name.StartsWith(".staging.") } |
Select-Object -First 1
if ($null -eq $releaseDir) {
throw "installer did not create a versioned release"
}
Remove-Item -LiteralPath (Join-Path $releaseDir.FullName "conpty\conpty.dll") -Force
$badManifest = $manifest | ConvertFrom-Json
$badManifest.assets."windows-x86_64".url = "http://127.0.0.1:$port/missing.zip"
$badManifest | ConvertTo-Json -Depth 5 | Out-File -LiteralPath $manifestPath -Encoding utf8
$downloadFailed = $false
try {
& "$PSScriptRoot\..\website\install.ps1" `
-ManifestUrl $manifestUrl `
-InstallDir $installDir `
-ExpectedBuildId "installer-test"
} catch {
if ($_.Exception.Message -notmatch "(?i)(404|not found)") {
throw
}
$downloadFailed = $true
}
if (-not $downloadFailed) {
throw "installer repair unexpectedly accepted a missing archive"
}
if (-not (Test-Path -LiteralPath (Join-Path $releaseDir.FullName "herdr.exe") -PathType Leaf)) {
throw "failed repair removed the existing release"
}
$manifest | Out-File -LiteralPath $manifestPath -Encoding utf8
& "$PSScriptRoot\..\website\install.ps1" `
-ManifestUrl $manifestUrl `
-InstallDir $installDir `
-ExpectedBuildId "installer-test"
if (-not (Test-Path -LiteralPath (Join-Path $installDir "conpty\conpty.dll") -PathType Leaf)) {
throw "installer did not repair an incomplete release"
}
$x64HostDir = Join-Path $releaseDir.FullName "conpty\x64"
$junctionTarget = Join-Path $root "junction-target"
Move-Item -LiteralPath $x64HostDir -Destination $junctionTarget
New-Item -ItemType Junction -Path $x64HostDir -Target $junctionTarget | Out-Null
& "$PSScriptRoot\..\website\install.ps1" `
-ManifestUrl $manifestUrl `
-InstallDir $installDir `
-ExpectedBuildId "installer-test"
$repairedHostDir = Get-Item -LiteralPath (Join-Path $installDir "conpty\x64") -Force
if ($repairedHostDir.Attributes -band [IO.FileAttributes]::ReparsePoint) {
throw "installer accepted a reparse-point ConPTY directory"
}
$rejected = $false
try {
& "$PSScriptRoot\..\website\install.ps1" `
-ManifestUrl $manifestUrl `
-InstallDir $installDir `
-ExpectedBuildId "different-build"
} catch {
if ($_.Exception.Message -notlike "Preview manifest changed while updating.*") {
throw
}
$rejected = $true
}
if (-not $rejected) {
throw "installer accepted a manifest that did not match the updater-selected build"
}
} finally {
$env:HERDR_HOME = $oldHerdrHome
if ($null -ne $server -and -not $server.HasExited) {
Stop-Process -Id $server.Id -Force -ErrorAction SilentlyContinue
}
Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue
}
+32 -4
View File
@@ -59,8 +59,11 @@ Invoke-Checked rustc @("--crate-type", "cdylib", "--edition", "2021", $fakeSourc
$oldPath = $env:PATH
$oldSession = $env:HERDR_SESSION
$oldSocket = $env:HERDR_SOCKET_PATH
$oldClientSocket = $env:HERDR_CLIENT_SOCKET_PATH
$env:PATH = "$fakeDir;$oldPath"
$env:HERDR_SESSION = $Session
Remove-Item Env:HERDR_SOCKET_PATH, Env:HERDR_CLIENT_SOCKET_PATH -ErrorAction SilentlyContinue
$server = $null
try {
@@ -84,9 +87,16 @@ try {
throw "server did not become ready"
}
$created = & $exe workspace create --cwd $PWD.Path 2>&1
if ($LASTEXITCODE -ne 0) {
throw "workspace create failed with exit code $LASTEXITCODE`: $($created -join "`n")"
$savedErrorActionPreference = $ErrorActionPreference
try {
$ErrorActionPreference = "Continue"
$created = & $exe workspace create --cwd $PWD.Path 2>&1
$createdExitCode = $LASTEXITCODE
} finally {
$ErrorActionPreference = $savedErrorActionPreference
}
if ($createdExitCode -ne 0) {
throw "workspace create failed with exit code $createdExitCode`: $($created -join "`n")"
}
$paneId = (($created -join "`n") | ConvertFrom-Json).result.root_pane.pane_id
if ([string]::IsNullOrWhiteSpace($paneId)) {
@@ -126,9 +136,27 @@ try {
Write-Host "server stop during cleanup failed: $($_.Exception.Message)"
}
Wait-Process -Id $server.Id -Timeout 10 -ErrorAction SilentlyContinue
$server.Refresh()
if (-not $server.HasExited) {
& taskkill.exe /PID $server.Id /T /F 2>&1 | Out-Null
}
}
$global:LASTEXITCODE = 0
$env:PATH = $oldPath
$env:HERDR_SESSION = $oldSession
if ($null -eq $oldSession) {
Remove-Item Env:HERDR_SESSION -ErrorAction SilentlyContinue
} else {
$env:HERDR_SESSION = $oldSession
}
if ($null -eq $oldSocket) {
Remove-Item Env:HERDR_SOCKET_PATH -ErrorAction SilentlyContinue
} else {
$env:HERDR_SOCKET_PATH = $oldSocket
}
if ($null -eq $oldClientSocket) {
Remove-Item Env:HERDR_CLIENT_SOCKET_PATH -ErrorAction SilentlyContinue
} else {
$env:HERDR_CLIENT_SOCKET_PATH = $oldClientSocket
}
Remove-Item -Recurse -Force $fakeDir -ErrorAction SilentlyContinue
}
+12 -4
View File
@@ -628,8 +628,12 @@ fn install_downloaded_update(mut update: DownloadedUpdate) -> Result<(), String>
}
#[cfg(windows)]
fn install_windows_update_with_installer(channel: UpdateChannel) -> Result<(), String> {
let status = Command::new("powershell")
fn install_windows_update_with_installer(
channel: UpdateChannel,
expected_build_id: Option<&str>,
) -> Result<(), String> {
let mut command = Command::new("powershell");
command
.args([
"-NoProfile",
"-ExecutionPolicy",
@@ -643,7 +647,11 @@ fn install_windows_update_with_installer(channel: UpdateChannel) -> Result<(), S
// PowerShell 5.1 (this `powershell`) fails to autoload cmdlets like
// Get-FileHash. Removing it lets 5.1 compute its own default path.
// See PowerShell/PowerShell#8635.
.env_remove("PSModulePath")
.env_remove("PSModulePath");
if let Some(build_id) = expected_build_id {
command.env("HERDR_EXPECTED_BUILD_ID", build_id);
}
let status = command
.status()
.map_err(|err| format!("failed to run Windows installer: {err}"))?;
@@ -2019,7 +2027,7 @@ pub fn self_update(options: SelfUpdateOptions) -> Result<Version, String> {
if let Some(sha256) = &release.sha256 {
tracing::debug!(sha256 = %sha256, "selected Windows update asset has checksum");
}
install_windows_update_with_installer(channel)?;
install_windows_update_with_installer(channel, release.build_id.as_deref())?;
let updated_exe = windows_installed_herdr_exe_path()?;
eprintln!("installed {}", release.label());
print_outdated_integration_notice_with_updated_binary(&updated_exe);
@@ -0,0 +1,357 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: herdr maintainers <maintainers@herdr.dev>
Date: Tue, 23 Jun 2026 00:00:00 +0000
Subject: [PATCH] control ConPTY loading
Load Herdr's hash-verified app-local ConPTY with a constrained DLL search,
otherwise use the exports from the already loaded kernel32 module. Never
probe for a bare conpty.dll through the DLL search path.
Herdr issues: https://github.com/ogulcancelik/herdr/issues/761
https://github.com/ogulcancelik/herdr/issues/1533
Vendored base: portable-pty 0.9.0
---
diff --git a/vendor/portable-pty/Cargo.toml b/vendor/portable-pty/Cargo.toml
index b8d11230..8a33abf9 100644
--- a/vendor/portable-pty/Cargo.toml
+++ b/vendor/portable-pty/Cargo.toml
@@ -102,8 +102,8 @@ version = "1.3"
[target."cfg(windows)".dependencies.lazy_static]
version = "1.4"
-[target."cfg(windows)".dependencies.shared_library]
-version = "0.1"
+[target."cfg(windows)".dependencies.sha2]
+version = "0.10"
[target."cfg(windows)".dependencies.winapi]
version = "0.3"
@@ -112,6 +112,7 @@ features = [
"consoleapi",
"handleapi",
"fileapi",
+ "libloaderapi",
"namedpipeapi",
"synchapi",
]
diff --git a/vendor/portable-pty/Cargo.toml.orig b/vendor/portable-pty/Cargo.toml.orig
index 0c3d6b8d..21abe3b6 100644
--- a/vendor/portable-pty/Cargo.toml.orig
+++ b/vendor/portable-pty/Cargo.toml.orig
@@ -27,12 +27,13 @@ serde_support = ["serde", "serde_derive"]
[target."cfg(windows)".dependencies]
bitflags = "1.3"
lazy_static = "1.4"
-shared_library = "0.1"
+sha2 = "0.10"
winapi = { version = "0.3", features = [
"winuser",
"consoleapi",
"handleapi",
"fileapi",
+ "libloaderapi",
"namedpipeapi",
"synchapi",
]}
diff --git a/vendor/portable-pty/src/win/psuedocon.rs b/vendor/portable-pty/src/win/psuedocon.rs
index 133eab52..d4f111bb 100644
--- a/vendor/portable-pty/src/win/psuedocon.rs
+++ b/vendor/portable-pty/src/win/psuedocon.rs
@@ -4,17 +4,24 @@ use crate::win::procthreadattr::ProcThreadAttributeList;
use anyhow::{bail, ensure, Error};
use filedescriptor::{FileDescriptor, OwnedHandle};
use lazy_static::lazy_static;
-use shared_library::shared_library;
-use std::ffi::OsString;
-use std::io::Error as IoError;
-use std::os::windows::ffi::OsStringExt;
+use sha2::{Digest, Sha256};
+use std::collections::BTreeSet;
+use std::ffi::{OsStr, OsString};
+use std::fs::File;
+use std::io::{Error as IoError, Read};
+use std::os::windows::ffi::{OsStrExt, OsStringExt};
+use std::os::windows::fs::MetadataExt;
use std::os::windows::io::{AsRawHandle, FromRawHandle};
-use std::path::Path;
+use std::path::{Path, PathBuf};
use std::sync::Mutex;
use std::{mem, ptr};
-use winapi::shared::minwindef::DWORD;
+use winapi::shared::minwindef::{DWORD, HMODULE};
use winapi::shared::winerror::{HRESULT, S_OK};
use winapi::um::handleapi::*;
+use winapi::um::libloaderapi::{
+ FreeLibrary, GetModuleHandleW, GetProcAddress, LoadLibraryExW,
+ LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR, LOAD_LIBRARY_SEARCH_SYSTEM32,
+};
use winapi::um::processthreadsapi::*;
use winapi::um::winbase::{
CREATE_UNICODE_ENVIRONMENT, EXTENDED_STARTUPINFO_PRESENT, STARTF_USESTDHANDLES, STARTUPINFOEXW,
@@ -30,26 +37,249 @@ pub const PSEUDOCONSOLE_WIN32_INPUT_MODE: DWORD = 0x4;
#[allow(dead_code)]
pub const PSEUDOCONSOLE_PASSTHROUGH_MODE: DWORD = 0x8;
-shared_library!(ConPtyFuncs,
- pub fn CreatePseudoConsole(
- size: COORD,
- hInput: HANDLE,
- hOutput: HANDLE,
- flags: DWORD,
- hpc: *mut HPCON
- ) -> HRESULT,
- pub fn ResizePseudoConsole(hpc: HPCON, size: COORD) -> HRESULT,
- pub fn ClosePseudoConsole(hpc: HPCON),
-);
+type CreatePseudoConsoleFn = unsafe extern "system" fn(
+ size: COORD,
+ h_input: HANDLE,
+ h_output: HANDLE,
+ flags: DWORD,
+ hpc: *mut HPCON,
+) -> HRESULT;
+type ResizePseudoConsoleFn = unsafe extern "system" fn(hpc: HPCON, size: COORD) -> HRESULT;
+type ClosePseudoConsoleFn = unsafe extern "system" fn(hpc: HPCON);
+
+// These fields intentionally mirror the exported Win32 symbol names.
+#[allow(non_snake_case)]
+struct ConPtyFuncs {
+ module: HMODULE,
+ owned: bool,
+ CreatePseudoConsole: CreatePseudoConsoleFn,
+ ResizePseudoConsole: ResizePseudoConsoleFn,
+ ClosePseudoConsole: ClosePseudoConsoleFn,
+}
+
+unsafe impl Send for ConPtyFuncs {}
+unsafe impl Sync for ConPtyFuncs {}
+
+impl Drop for ConPtyFuncs {
+ fn drop(&mut self) {
+ if self.owned {
+ unsafe { FreeLibrary(self.module) };
+ }
+ }
+}
+
+impl ConPtyFuncs {
+ unsafe fn from_module(module: HMODULE, owned: bool) -> Result<Self, String> {
+ if module.is_null() {
+ return Err(IoError::last_os_error().to_string());
+ }
+ Ok(Self {
+ module,
+ owned,
+ CreatePseudoConsole: load_symbol(module, b"CreatePseudoConsole\0")?,
+ ResizePseudoConsole: load_symbol(module, b"ResizePseudoConsole\0")?,
+ ClosePseudoConsole: load_symbol(module, b"ClosePseudoConsole\0")?,
+ })
+ }
+}
+
+unsafe fn load_symbol<T: Copy>(module: HMODULE, name: &'static [u8]) -> Result<T, String> {
+ let symbol = GetProcAddress(module, name.as_ptr().cast());
+ if symbol.is_null() {
+ return Err(format!(
+ "missing symbol {}",
+ String::from_utf8_lossy(&name[..name.len() - 1])
+ ));
+ }
+ Ok(mem::transmute_copy(&symbol))
+}
+
+fn load_system_conpty() -> Result<ConPtyFuncs, String> {
+ let kernel_name = wide_string(OsStr::new("kernel32.dll"));
+ let module = unsafe { GetModuleHandleW(kernel_name.as_ptr()) };
+ unsafe { ConPtyFuncs::from_module(module, false) }
+}
+
+fn load_app_local_conpty(path: &Path) -> Result<ConPtyFuncs, String> {
+ let wide_path = wide_string(path.as_os_str());
+ let module = unsafe {
+ LoadLibraryExW(
+ wide_path.as_ptr(),
+ ptr::null_mut(),
+ LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32,
+ )
+ };
+ match unsafe { ConPtyFuncs::from_module(module, true) } {
+ Ok(functions) => Ok(functions),
+ Err(error) => {
+ if !module.is_null() {
+ unsafe { FreeLibrary(module) };
+ }
+ Err(error)
+ }
+ }
+}
+
+fn wide_string(value: &OsStr) -> Vec<u16> {
+ value.encode_wide().chain(Some(0)).collect()
+}
fn load_conpty() -> ConPtyFuncs {
// If the kernel doesn't export these functions then their system is
// too old and we cannot run.
- let kernel = ConPtyFuncs::open(Path::new("kernel32.dll")).expect(
- "this system does not support conpty. Windows 10 October 2018 or newer is required",
- );
+ let system = load_system_conpty().unwrap_or_else(|error| {
+ panic!(
+ "this system does not support conpty. Windows 10 October 2018 or newer is required: {}",
+ error
+ )
+ });
+
+ if std::env::var("HERDR_WINDOWS_CONPTY").as_deref() == Ok("system") {
+ log::warn!("HERDR_WINDOWS_CONPTY=system disables Herdr's bundled ConPTY");
+ return system;
+ }
+
+ match app_local_conpty_path() {
+ Ok(Some(path)) => load_app_local_conpty(&path).unwrap_or_else(|error| {
+ panic!(
+ "failed to load verified app-local ConPTY from {}: {error}; set HERDR_WINDOWS_CONPTY=system to use the Windows system ConPTY",
+ path.display()
+ )
+ }),
+ Ok(None) => system,
+ Err(error) => panic!(
+ "Herdr's app-local ConPTY bundle is invalid: {}; reinstall Herdr or set HERDR_WINDOWS_CONPTY=system to use the Windows system ConPTY",
+ error
+ ),
+ }
+}
+
+const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x400;
+const CONPTY_FILES: &[(&str, &str)] = &[
+ (
+ "conpty.dll",
+ "39fba2713e2495117b1591ae8c32a3b904bea7aa66069cf7815e2844c76d75d8",
+ ),
+ (
+ "x64/OpenConsole.exe",
+ "b7fd936c2668b87b9ecf7b3366dc6568afc1c6f981874cba3e955a1c35cf8160",
+ ),
+ (
+ "arm64/OpenConsole.exe",
+ "ed7622fd0d3bedc9ab9f122f5e58edf0def9e7999224f52dd395ba9f54edbe09",
+ ),
+];
+
+fn app_local_conpty_path() -> Result<Option<PathBuf>, String> {
+ if std::env::consts::ARCH != "x86_64" {
+ return Ok(None);
+ }
+ let executable = std::env::current_exe()
+ .map_err(|error| format!("cannot locate the Herdr executable: {error}"))?;
+ let executable_dir = executable
+ .parent()
+ .ok_or_else(|| "Herdr executable has no parent directory".to_string())?;
+ let bundle = executable_dir.join("conpty");
+ if !bundle.exists() {
+ return Ok(None);
+ }
+ reject_reparse_point(&bundle)?;
+
+ let mut expected = BTreeSet::from(["herdr-conpty.json".to_string()]);
+ for (relative, expected_hash) in CONPTY_FILES {
+ expected.insert((*relative).to_string());
+ verify_bundle_file(&bundle, relative, expected_hash)?;
+ }
+ let marker = bundle.join("herdr-conpty.json");
+ verify_regular_file(&marker)?;
+
+ let actual = bundle_files(&bundle)?;
+ if actual != expected {
+ return Err(format!(
+ "unexpected bundle layout; expected {expected:?}, found {actual:?}"
+ ));
+ }
+ Ok(Some(bundle.join("conpty.dll")))
+}
+
+fn verify_bundle_file(root: &Path, relative: &str, expected_hash: &str) -> Result<(), String> {
+ let path = root.join(relative);
+ verify_regular_file(&path)?;
+ let mut file = File::open(&path).map_err(|error| format!("cannot open {}: {error}", path.display()))?;
+ let mut digest = Sha256::new();
+ let mut buffer = [0; 64 * 1024];
+ loop {
+ let read = file
+ .read(&mut buffer)
+ .map_err(|error| format!("cannot read {}: {error}", path.display()))?;
+ if read == 0 {
+ break;
+ }
+ digest.update(&buffer[..read]);
+ }
+ let actual_hash = format!("{:x}", digest.finalize());
+ if actual_hash != expected_hash {
+ return Err(format!(
+ "{} has hash {actual_hash}, expected {expected_hash}",
+ path.display()
+ ));
+ }
+ Ok(())
+}
+
+fn verify_regular_file(path: &Path) -> Result<(), String> {
+ let metadata = path
+ .symlink_metadata()
+ .map_err(|error| format!("cannot inspect {}: {error}", path.display()))?;
+ if !metadata.is_file() || metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
+ return Err(format!("{} is not a regular file", path.display()));
+ }
+ Ok(())
+}
+
+fn reject_reparse_point(path: &Path) -> Result<(), String> {
+ let metadata = path
+ .symlink_metadata()
+ .map_err(|error| format!("cannot inspect {}: {error}", path.display()))?;
+ if !metadata.is_dir() || metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
+ return Err(format!("{} is not a regular directory", path.display()));
+ }
+ Ok(())
+}
+
+fn bundle_files(root: &Path) -> Result<BTreeSet<String>, String> {
+ fn visit(root: &Path, directory: &Path, files: &mut BTreeSet<String>) -> Result<(), String> {
+ reject_reparse_point(directory)?;
+ for entry in std::fs::read_dir(directory)
+ .map_err(|error| format!("cannot read {}: {error}", directory.display()))?
+ {
+ let entry = entry.map_err(|error| error.to_string())?;
+ let path = entry.path();
+ let metadata = path
+ .symlink_metadata()
+ .map_err(|error| format!("cannot inspect {}: {error}", path.display()))?;
+ if metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
+ return Err(format!("{} is a reparse point", path.display()));
+ }
+ if metadata.is_dir() {
+ visit(root, &path, files)?;
+ } else if metadata.is_file() {
+ let relative = path
+ .strip_prefix(root)
+ .map_err(|error| error.to_string())?
+ .to_string_lossy()
+ .replace('\\', "/");
+ files.insert(relative);
+ } else {
+ return Err(format!("{} is not a regular file", path.display()));
+ }
+ }
+ Ok(())
+ }
- kernel
+ let mut files = BTreeSet::new();
+ visit(root, root, &mut files)?;
+ Ok(files)
}
lazy_static! {
--
2.50.1
@@ -1,37 +0,0 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: herdr maintainers <maintainers@herdr.dev>
Date: Tue, 23 Jun 2026 00:00:00 +0000
Subject: [PATCH] force system ConPTY
Herdr should use the ConPTY functions exported by kernel32.dll and must not
load a bare conpty.dll from PATH. portable-pty's sideload probe is intentional
for WezTerm's bundled OpenConsole.exe/conpty.dll pair, but Herdr does not ship
that pair.
Herdr issue: https://github.com/ogulcancelik/herdr/issues/761
Vendored base: portable-pty 0.9.0
---
vendor/portable-pty/src/win/psuedocon.rs | 9 +--------
1 file changed, 1 insertion(+), 8 deletions(-)
diff --git a/vendor/portable-pty/src/win/psuedocon.rs b/vendor/portable-pty/src/win/psuedocon.rs
index 0000000..0000000 100644
--- a/vendor/portable-pty/src/win/psuedocon.rs
+++ b/vendor/portable-pty/src/win/psuedocon.rs
@@ -49,14 +49,7 @@ fn load_conpty() -> ConPtyFuncs {
"this system does not support conpty. Windows 10 October 2018 or newer is required",
);
-
- // We prefer to use a sideloaded conpty.dll and openconsole.exe host deployed
- // alongside the application. We check for this after checking for kernel
- // support so that we don't try to proceed and do something crazy.
- if let Ok(sideloaded) = ConPtyFuncs::open(Path::new("conpty.dll")) {
- sideloaded
- } else {
- kernel
- }
+
+ kernel
}
lazy_static! {
+24 -13
View File
@@ -4,15 +4,18 @@ This file tracks intentional local changes applied on top of the vendored
`portable-pty` source. Remove a patch only when the upstream crate contains an
equivalent fix or exposes an option that lets Herdr keep the same behavior.
## 0001 force system ConPTY
## 0001 control ConPTY loading
status: active
patch: `vendor/patches/portable-pty/0001-force-system-conpty.patch`
patch: `vendor/patches/portable-pty/0001-control-conpty-loading.patch`
herdr issue: https://github.com/ogulcancelik/herdr/issues/761
herdr issues:
upstream discussion: none found
- https://github.com/ogulcancelik/herdr/issues/761
- https://github.com/ogulcancelik/herdr/issues/1533
upstream discussion: https://github.com/microsoft/terminal/issues/17452
upstream pr: none
@@ -20,16 +23,23 @@ vendored base: `portable-pty 0.9.0`
local files:
- `vendor/portable-pty/Cargo.toml`
- `vendor/portable-pty/Cargo.toml.orig`
- `vendor/portable-pty/src/win/psuedocon.rs`
reason: `portable-pty` intentionally probes a bare `conpty.dll` after verifying
that `kernel32.dll` exports the ConPTY API. That is useful for WezTerm's bundled
`OpenConsole.exe` and `conpty.dll` pair, but Herdr does not ship that pair and
must not load another application's `conpty.dll` from `PATH`.
reason: `portable-pty` intentionally probes a bare `conpty.dll` through the DLL
search path. Herdr must never load another application's DLL from `PATH`. The
Windows package ships a pinned Microsoft ConPTY runtime under `conpty/`; Herdr
verifies the exact DLL and x64/ARM64 host hashes, rejects reparse points and
unexpected files, then loads the DLL by absolute path with its dependency search
limited to that directory and System32. Installations without a bundle continue
using the ConPTY exports from the already loaded `kernel32.dll`. Set
`HERDR_WINDOWS_CONPTY=system` to bypass the bundle during compatibility
recovery.
remove when: upstream `portable-pty` no longer loads bare `conpty.dll` from the
DLL search path, upstream exposes a way for consumers to force system ConPTY, or
Herdr replaces the Windows PTY backend.
remove when: upstream `portable-pty` exposes hash-verified app-local and system
ConPTY selection with constrained DLL loading and no bare DLL search, or Herdr
replaces the Windows PTY backend.
verification:
@@ -37,8 +47,9 @@ verification:
python3 -m unittest scripts.test_vendor_portable_pty
```
On Windows, also verify that pane creation succeeds when `PATH` contains a
directory with `conpty.dll`.
On Windows, run the enhanced-input CI probe against the packaged `conpty/`
bundle. It verifies the exact OpenConsole process path on Server 2022 and 2025,
rejects a tampered bundle, and verifies the explicit system fallback.
## 0002 expose Windows raw command tails
+3 -2
View File
@@ -102,8 +102,8 @@ version = "1.3"
[target."cfg(windows)".dependencies.lazy_static]
version = "1.4"
[target."cfg(windows)".dependencies.shared_library]
version = "0.1"
[target."cfg(windows)".dependencies.sha2]
version = "0.10"
[target."cfg(windows)".dependencies.winapi]
version = "0.3"
@@ -112,6 +112,7 @@ features = [
"consoleapi",
"handleapi",
"fileapi",
"libloaderapi",
"namedpipeapi",
"synchapi",
]
+2 -1
View File
@@ -27,12 +27,13 @@ serde_support = ["serde", "serde_derive"]
[target."cfg(windows)".dependencies]
bitflags = "1.3"
lazy_static = "1.4"
shared_library = "0.1"
sha2 = "0.10"
winapi = { version = "0.3", features = [
"winuser",
"consoleapi",
"handleapi",
"fileapi",
"libloaderapi",
"namedpipeapi",
"synchapi",
]}
+251 -21
View File
@@ -4,17 +4,24 @@ use crate::win::procthreadattr::ProcThreadAttributeList;
use anyhow::{bail, ensure, Error};
use filedescriptor::{FileDescriptor, OwnedHandle};
use lazy_static::lazy_static;
use shared_library::shared_library;
use std::ffi::OsString;
use std::io::Error as IoError;
use std::os::windows::ffi::OsStringExt;
use sha2::{Digest, Sha256};
use std::collections::BTreeSet;
use std::ffi::{OsStr, OsString};
use std::fs::File;
use std::io::{Error as IoError, Read};
use std::os::windows::ffi::{OsStrExt, OsStringExt};
use std::os::windows::fs::MetadataExt;
use std::os::windows::io::{AsRawHandle, FromRawHandle};
use std::path::Path;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use std::{mem, ptr};
use winapi::shared::minwindef::DWORD;
use winapi::shared::minwindef::{DWORD, HMODULE};
use winapi::shared::winerror::{HRESULT, S_OK};
use winapi::um::handleapi::*;
use winapi::um::libloaderapi::{
FreeLibrary, GetModuleHandleW, GetProcAddress, LoadLibraryExW,
LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR, LOAD_LIBRARY_SEARCH_SYSTEM32,
};
use winapi::um::processthreadsapi::*;
use winapi::um::winbase::{
CREATE_UNICODE_ENVIRONMENT, EXTENDED_STARTUPINFO_PRESENT, STARTF_USESTDHANDLES, STARTUPINFOEXW,
@@ -30,26 +37,249 @@ pub const PSEUDOCONSOLE_WIN32_INPUT_MODE: DWORD = 0x4;
#[allow(dead_code)]
pub const PSEUDOCONSOLE_PASSTHROUGH_MODE: DWORD = 0x8;
shared_library!(ConPtyFuncs,
pub fn CreatePseudoConsole(
size: COORD,
hInput: HANDLE,
hOutput: HANDLE,
flags: DWORD,
hpc: *mut HPCON
) -> HRESULT,
pub fn ResizePseudoConsole(hpc: HPCON, size: COORD) -> HRESULT,
pub fn ClosePseudoConsole(hpc: HPCON),
);
type CreatePseudoConsoleFn = unsafe extern "system" fn(
size: COORD,
h_input: HANDLE,
h_output: HANDLE,
flags: DWORD,
hpc: *mut HPCON,
) -> HRESULT;
type ResizePseudoConsoleFn = unsafe extern "system" fn(hpc: HPCON, size: COORD) -> HRESULT;
type ClosePseudoConsoleFn = unsafe extern "system" fn(hpc: HPCON);
// These fields intentionally mirror the exported Win32 symbol names.
#[allow(non_snake_case)]
struct ConPtyFuncs {
module: HMODULE,
owned: bool,
CreatePseudoConsole: CreatePseudoConsoleFn,
ResizePseudoConsole: ResizePseudoConsoleFn,
ClosePseudoConsole: ClosePseudoConsoleFn,
}
unsafe impl Send for ConPtyFuncs {}
unsafe impl Sync for ConPtyFuncs {}
impl Drop for ConPtyFuncs {
fn drop(&mut self) {
if self.owned {
unsafe { FreeLibrary(self.module) };
}
}
}
impl ConPtyFuncs {
unsafe fn from_module(module: HMODULE, owned: bool) -> Result<Self, String> {
if module.is_null() {
return Err(IoError::last_os_error().to_string());
}
Ok(Self {
module,
owned,
CreatePseudoConsole: load_symbol(module, b"CreatePseudoConsole\0")?,
ResizePseudoConsole: load_symbol(module, b"ResizePseudoConsole\0")?,
ClosePseudoConsole: load_symbol(module, b"ClosePseudoConsole\0")?,
})
}
}
unsafe fn load_symbol<T: Copy>(module: HMODULE, name: &'static [u8]) -> Result<T, String> {
let symbol = GetProcAddress(module, name.as_ptr().cast());
if symbol.is_null() {
return Err(format!(
"missing symbol {}",
String::from_utf8_lossy(&name[..name.len() - 1])
));
}
Ok(mem::transmute_copy(&symbol))
}
fn load_system_conpty() -> Result<ConPtyFuncs, String> {
let kernel_name = wide_string(OsStr::new("kernel32.dll"));
let module = unsafe { GetModuleHandleW(kernel_name.as_ptr()) };
unsafe { ConPtyFuncs::from_module(module, false) }
}
fn load_app_local_conpty(path: &Path) -> Result<ConPtyFuncs, String> {
let wide_path = wide_string(path.as_os_str());
let module = unsafe {
LoadLibraryExW(
wide_path.as_ptr(),
ptr::null_mut(),
LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32,
)
};
match unsafe { ConPtyFuncs::from_module(module, true) } {
Ok(functions) => Ok(functions),
Err(error) => {
if !module.is_null() {
unsafe { FreeLibrary(module) };
}
Err(error)
}
}
}
fn wide_string(value: &OsStr) -> Vec<u16> {
value.encode_wide().chain(Some(0)).collect()
}
fn load_conpty() -> ConPtyFuncs {
// If the kernel doesn't export these functions then their system is
// too old and we cannot run.
let kernel = ConPtyFuncs::open(Path::new("kernel32.dll")).expect(
"this system does not support conpty. Windows 10 October 2018 or newer is required",
);
let system = load_system_conpty().unwrap_or_else(|error| {
panic!(
"this system does not support conpty. Windows 10 October 2018 or newer is required: {}",
error
)
});
kernel
if std::env::var("HERDR_WINDOWS_CONPTY").as_deref() == Ok("system") {
log::warn!("HERDR_WINDOWS_CONPTY=system disables Herdr's bundled ConPTY");
return system;
}
match app_local_conpty_path() {
Ok(Some(path)) => load_app_local_conpty(&path).unwrap_or_else(|error| {
panic!(
"failed to load verified app-local ConPTY from {}: {error}; set HERDR_WINDOWS_CONPTY=system to use the Windows system ConPTY",
path.display()
)
}),
Ok(None) => system,
Err(error) => panic!(
"Herdr's app-local ConPTY bundle is invalid: {}; reinstall Herdr or set HERDR_WINDOWS_CONPTY=system to use the Windows system ConPTY",
error
),
}
}
const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x400;
const CONPTY_FILES: &[(&str, &str)] = &[
(
"conpty.dll",
"39fba2713e2495117b1591ae8c32a3b904bea7aa66069cf7815e2844c76d75d8",
),
(
"x64/OpenConsole.exe",
"b7fd936c2668b87b9ecf7b3366dc6568afc1c6f981874cba3e955a1c35cf8160",
),
(
"arm64/OpenConsole.exe",
"ed7622fd0d3bedc9ab9f122f5e58edf0def9e7999224f52dd395ba9f54edbe09",
),
];
fn app_local_conpty_path() -> Result<Option<PathBuf>, String> {
if std::env::consts::ARCH != "x86_64" {
return Ok(None);
}
let executable = std::env::current_exe()
.map_err(|error| format!("cannot locate the Herdr executable: {error}"))?;
let executable_dir = executable
.parent()
.ok_or_else(|| "Herdr executable has no parent directory".to_string())?;
let bundle = executable_dir.join("conpty");
if !bundle.exists() {
return Ok(None);
}
reject_reparse_point(&bundle)?;
let mut expected = BTreeSet::from(["herdr-conpty.json".to_string()]);
for (relative, expected_hash) in CONPTY_FILES {
expected.insert((*relative).to_string());
verify_bundle_file(&bundle, relative, expected_hash)?;
}
let marker = bundle.join("herdr-conpty.json");
verify_regular_file(&marker)?;
let actual = bundle_files(&bundle)?;
if actual != expected {
return Err(format!(
"unexpected bundle layout; expected {expected:?}, found {actual:?}"
));
}
Ok(Some(bundle.join("conpty.dll")))
}
fn verify_bundle_file(root: &Path, relative: &str, expected_hash: &str) -> Result<(), String> {
let path = root.join(relative);
verify_regular_file(&path)?;
let mut file = File::open(&path).map_err(|error| format!("cannot open {}: {error}", path.display()))?;
let mut digest = Sha256::new();
let mut buffer = [0; 64 * 1024];
loop {
let read = file
.read(&mut buffer)
.map_err(|error| format!("cannot read {}: {error}", path.display()))?;
if read == 0 {
break;
}
digest.update(&buffer[..read]);
}
let actual_hash = format!("{:x}", digest.finalize());
if actual_hash != expected_hash {
return Err(format!(
"{} has hash {actual_hash}, expected {expected_hash}",
path.display()
));
}
Ok(())
}
fn verify_regular_file(path: &Path) -> Result<(), String> {
let metadata = path
.symlink_metadata()
.map_err(|error| format!("cannot inspect {}: {error}", path.display()))?;
if !metadata.is_file() || metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
return Err(format!("{} is not a regular file", path.display()));
}
Ok(())
}
fn reject_reparse_point(path: &Path) -> Result<(), String> {
let metadata = path
.symlink_metadata()
.map_err(|error| format!("cannot inspect {}: {error}", path.display()))?;
if !metadata.is_dir() || metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
return Err(format!("{} is not a regular directory", path.display()));
}
Ok(())
}
fn bundle_files(root: &Path) -> Result<BTreeSet<String>, String> {
fn visit(root: &Path, directory: &Path, files: &mut BTreeSet<String>) -> Result<(), String> {
reject_reparse_point(directory)?;
for entry in std::fs::read_dir(directory)
.map_err(|error| format!("cannot read {}: {error}", directory.display()))?
{
let entry = entry.map_err(|error| error.to_string())?;
let path = entry.path();
let metadata = path
.symlink_metadata()
.map_err(|error| format!("cannot inspect {}: {error}", path.display()))?;
if metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
return Err(format!("{} is a reparse point", path.display()));
}
if metadata.is_dir() {
visit(root, &path, files)?;
} else if metadata.is_file() {
let relative = path
.strip_prefix(root)
.map_err(|error| error.to_string())?
.to_string_lossy()
.replace('\\', "/");
files.insert(relative);
} else {
return Err(format!("{} is not a regular file", path.display()));
}
}
Ok(())
}
let mut files = BTreeSet::new();
visit(root, root, &mut files)?;
Ok(files)
}
lazy_static! {
+189 -22
View File
@@ -3,6 +3,7 @@ param(
[string]$Channel = $env:HERDR_CHANNEL,
[string]$ManifestUrl = $env:HERDR_MANIFEST_URL,
[string]$InstallDir = $env:HERDR_INSTALL_DIR,
[string]$ExpectedBuildId = $env:HERDR_EXPECTED_BUILD_ID,
[int]$Retain = 3
)
@@ -106,19 +107,40 @@ function Get-ManifestAsset {
$asset = $property.Value
if ($asset -is [string]) {
$url = [string]$asset
return [PSCustomObject]@{
Url = $asset
Url = $url
Sha256 = $null
Format = if ($url.EndsWith(".zip", [System.StringComparison]::OrdinalIgnoreCase)) { "zip" } else { "exe" }
}
}
if ([string]::IsNullOrWhiteSpace([string]$asset.url)) {
$urlProperty = $asset.PSObject.Properties["url"]
if ($null -eq $urlProperty -or [string]::IsNullOrWhiteSpace([string]$urlProperty.Value)) {
throw "Release manifest asset $Target is missing a URL."
}
$url = [string]$urlProperty.Value
$formatProperty = $asset.PSObject.Properties["format"]
$format = if ($null -eq $formatProperty -or [string]::IsNullOrWhiteSpace([string]$formatProperty.Value)) {
if ($url.EndsWith(".zip", [System.StringComparison]::OrdinalIgnoreCase)) { "zip" } else { "exe" }
} else {
[string]$formatProperty.Value
}
if ($format -notin @("zip", "exe")) {
throw "Release manifest asset $Target has unsupported format '$format'."
}
$shaProperty = $asset.PSObject.Properties["sha256"]
$sha256 = if ($null -eq $shaProperty -or [string]::IsNullOrWhiteSpace([string]$shaProperty.Value)) {
$null
} else {
[string]$shaProperty.Value
}
return [PSCustomObject]@{
Url = [string]$asset.url
Sha256 = if ([string]::IsNullOrWhiteSpace([string]$asset.sha256)) { $null } else { [string]$asset.sha256 }
Url = $url
Sha256 = $sha256
Format = $format
}
}
@@ -130,7 +152,8 @@ function ConvertTo-ManifestObject {
}
$json = $Manifest.TrimStart([char]0xFEFF)
if ($json.StartsWith("")) {
$utf8BomDecodedAsLatin1 = [string]::Concat([char]0x00EF, [char]0x00BB, [char]0x00BF)
if ($json.StartsWith($utf8BomDecodedAsLatin1)) {
$json = $json.Substring(3)
}
@@ -144,7 +167,10 @@ function Test-FileDigest {
)
if ([string]::IsNullOrWhiteSpace($ExpectedDigest)) {
return
throw "A SHA-256 checksum is required for $Path."
}
if ($ExpectedDigest -notmatch '^[0-9a-fA-F]{64}$') {
throw "Invalid SHA-256 checksum for $Path."
}
$sha256 = [System.Security.Cryptography.SHA256]::Create()
@@ -159,6 +185,118 @@ function Test-FileDigest {
}
}
function Test-RegularFile {
param([string]$Path)
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
return $false
}
$item = Get-Item -LiteralPath $Path -Force
return -not ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)
}
function Test-RegularDirectory {
param([string]$Path)
if (-not (Test-Path -LiteralPath $Path -PathType Container)) {
return $false
}
$item = Get-Item -LiteralPath $Path -Force
return -not ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)
}
function Test-HerdrReleaseComplete {
param(
[string]$ReleaseDir,
[string]$Format
)
if (-not (Test-RegularDirectory -Path $ReleaseDir)) {
return $false
}
$herdrExe = Join-Path $ReleaseDir "herdr.exe"
if (-not (Test-RegularFile -Path $herdrExe)) {
return $false
}
if ($Format -eq "exe") {
return $true
}
$conptyRoot = Join-Path $ReleaseDir "conpty"
if (-not (Test-RegularDirectory -Path $conptyRoot) -or
-not (Test-RegularDirectory -Path (Join-Path $conptyRoot "x64")) -or
-not (Test-RegularDirectory -Path (Join-Path $conptyRoot "arm64"))) {
return $false
}
$markerPath = Join-Path $conptyRoot "herdr-conpty.json"
$required = @(
"conpty/conpty.dll",
"conpty/x64/OpenConsole.exe",
"conpty/arm64/OpenConsole.exe",
"THIRD-PARTY-NOTICES/Microsoft.Windows.Console.ConPTY-LICENSE.txt",
"THIRD-PARTY-NOTICES/Microsoft.Windows.Console.ConPTY-NOTICE.md"
)
foreach ($relative in $required) {
if (-not (Test-RegularFile -Path (Join-Path $ReleaseDir ($relative -replace '/', '\')))) {
return $false
}
}
if (-not (Test-RegularFile -Path $markerPath)) {
return $false
}
try {
$marker = ConvertTo-ManifestObject -Manifest (Get-Content -LiteralPath $markerPath -Raw)
$schemaProperty = $marker.PSObject.Properties["schema_version"]
$packageProperty = $marker.PSObject.Properties["package"]
$versionProperty = $marker.PSObject.Properties["version"]
$architectureProperty = $marker.PSObject.Properties["architecture"]
$filesProperty = $marker.PSObject.Properties["files"]
if ($null -eq $schemaProperty -or [int]$schemaProperty.Value -ne 1 -or
$null -eq $packageProperty -or [string]$packageProperty.Value -ne "Microsoft.Windows.Console.ConPTY" -or
$null -eq $versionProperty -or [string]::IsNullOrWhiteSpace([string]$versionProperty.Value) -or
$null -eq $architectureProperty -or [string]$architectureProperty.Value -ne "x86_64" -or
$null -eq $filesProperty) {
return $false
}
$expectedConptyFiles = @(
"conpty/conpty.dll",
"conpty/x64/OpenConsole.exe",
"conpty/arm64/OpenConsole.exe"
)
$markerFileNames = @($filesProperty.Value.PSObject.Properties | ForEach-Object { $_.Name })
if (@(Compare-Object $expectedConptyFiles $markerFileNames).Count -ne 0) {
return $false
}
$bundleEntries = @(Get-ChildItem -LiteralPath $conptyRoot -Force -Recurse)
if (@($bundleEntries | Where-Object {
$_.Attributes -band [IO.FileAttributes]::ReparsePoint
}).Count -ne 0) {
return $false
}
$releaseRoot = [System.IO.Path]::GetFullPath($ReleaseDir).TrimEnd('\')
$actualBundleFiles = @($bundleEntries | Where-Object { -not $_.PSIsContainer } | ForEach-Object {
$_.FullName.Substring($releaseRoot.Length + 1).Replace('\', '/')
})
$expectedBundleFiles = @($expectedConptyFiles) + "conpty/herdr-conpty.json"
if (@(Compare-Object $expectedBundleFiles $actualBundleFiles).Count -ne 0) {
return $false
}
foreach ($relative in $expectedConptyFiles) {
$digestProperty = $filesProperty.Value.PSObject.Properties[$relative]
if ($null -eq $digestProperty) {
return $false
}
Test-FileDigest -Path (Join-Path $ReleaseDir ($relative -replace '/', '\')) -ExpectedDigest ([string]$digestProperty.Value)
}
} catch {
return $false
}
return $true
}
function Invoke-WithInstallLock {
param(
[string]$LockPath,
@@ -286,7 +424,7 @@ function Remove-OldReleases {
$currentFullPath = [System.IO.Path]::GetFullPath($CurrentReleaseDir)
$releaseDirs = Get-ChildItem -LiteralPath $ReleasesDir -Force -Directory -ErrorAction SilentlyContinue |
Where-Object { -not $_.Name.StartsWith(".staging.") } |
Where-Object { -not $_.Name.StartsWith(".staging.") -and -not $_.Name.StartsWith(".backup.") } |
Sort-Object LastWriteTimeUtc -Descending
$kept = 0
foreach ($dir in $releaseDirs) {
@@ -367,6 +505,7 @@ $herdrHome = if ([string]::IsNullOrWhiteSpace($env:HERDR_HOME)) {
} else {
$env:HERDR_HOME
}
$herdrHome = [System.IO.Path]::GetFullPath($herdrHome)
$standaloneRoot = Join-Path $herdrHome "packages\standalone"
$releasesDir = Join-Path $standaloneRoot "releases"
$currentDir = Join-Path $standaloneRoot "current"
@@ -396,6 +535,9 @@ if (-not [string]::IsNullOrWhiteSpace($existingHerdr) -and -not (Test-PathStarts
Write-Step "Fetching Herdr $Channel manifest"
$manifest = ConvertTo-ManifestObject -Manifest (Invoke-RestMethod -Uri $ManifestUrl)
if (-not [string]::IsNullOrWhiteSpace($ExpectedBuildId) -and [string]$manifest.build_id -ne $ExpectedBuildId) {
throw "Preview manifest changed while updating. Expected build $ExpectedBuildId but found $($manifest.build_id). Run herdr update again."
}
$versionIdentity = Resolve-HerdrVersion -Manifest $manifest -SelectedChannel $Channel
$asset = Get-ManifestAsset -Manifest $manifest -Target $target
$safeVersionIdentity = $versionIdentity -replace '[^0-9A-Za-z._-]', '-'
@@ -410,31 +552,56 @@ try {
Invoke-WithInstallLock -LockPath $lockPath -Script {
Remove-StaleInstallArtifacts -ReleasesDir $releasesDir
if (-not (Test-Path -LiteralPath (Join-Path $releaseDir "herdr.exe") -PathType Leaf)) {
if (Test-Path -LiteralPath $releaseDir) {
Remove-Item -LiteralPath $releaseDir -Recurse -Force
}
$downloadPath = Join-Path $tempDir "herdr.exe"
if (-not (Test-HerdrReleaseComplete -ReleaseDir $releaseDir -Format $asset.Format)) {
$downloadPath = Join-Path $tempDir "herdr-download.$($asset.Format)"
$stagingDir = Join-Path $releasesDir ".staging.$releaseName.$PID"
Write-Step "Downloading Herdr"
Invoke-WebRequest -Uri $asset.Url -OutFile $downloadPath
Test-FileDigest -Path $downloadPath -ExpectedDigest $asset.Sha256
New-Item -ItemType Directory -Force -Path $stagingDir | Out-Null
Copy-Item -LiteralPath $downloadPath -Destination (Join-Path $stagingDir "herdr.exe")
Move-Item -LiteralPath $stagingDir -Destination $releaseDir
if ($asset.Format -eq "zip") {
Expand-Archive -LiteralPath $downloadPath -DestinationPath $stagingDir
} else {
New-Item -ItemType Directory -Force -Path $stagingDir | Out-Null
Copy-Item -LiteralPath $downloadPath -Destination (Join-Path $stagingDir "herdr.exe")
}
if (-not (Test-HerdrReleaseComplete -ReleaseDir $stagingDir -Format $asset.Format)) {
throw "Downloaded Herdr package is incomplete or failed ConPTY verification."
}
$stagedHerdr = Join-Path $stagingDir "herdr.exe"
& $stagedHerdr --version *> $null
if ($LASTEXITCODE -ne 0) {
throw "Downloaded Herdr command failed verification: $stagedHerdr --version"
}
$backupDir = $null
if (Test-Path -LiteralPath $releaseDir) {
$backupDir = Join-Path $releasesDir ".backup.$releaseName.$([System.Guid]::NewGuid().ToString('N'))"
Move-Item -LiteralPath $releaseDir -Destination $backupDir
}
try {
Move-Item -LiteralPath $stagingDir -Destination $releaseDir
} catch {
if ($null -ne $backupDir -and -not (Test-Path -LiteralPath $releaseDir)) {
Move-Item -LiteralPath $backupDir -Destination $releaseDir
}
throw
}
if ($null -ne $backupDir) {
Remove-Item -LiteralPath $backupDir -Recurse -Force -ErrorAction SilentlyContinue
}
}
$releaseHerdr = Join-Path $releaseDir "herdr.exe"
& $releaseHerdr --version *> $null
if ($LASTEXITCODE -ne 0) {
throw "Installed Herdr command failed verification: $releaseHerdr --version"
}
Get-ChildItem -LiteralPath $releasesDir -Force -Directory -Filter ".backup.$releaseName.*" -ErrorAction SilentlyContinue |
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
Set-ManagedJunction -LinkPath $currentDir -TargetPath $releaseDir -ManagedTargetPrefix $releasesDir
Set-ManagedJunction -LinkPath $visibleBinDir -TargetPath $releaseDir -ManagedTargetPrefix $standaloneRoot -AllowLegacyHerdrBinMigration $allowLegacyVisibleBinMigration
$herdrCommand = Join-Path $visibleBinDir "herdr.exe"
& $herdrCommand --version *> $null
if ($LASTEXITCODE -ne 0) {
throw "Installed Herdr command failed verification: $herdrCommand --version"
}
Remove-OldReleases -ReleasesDir $releasesDir -CurrentReleaseDir $releaseDir -Keep $Retain
}
} finally {