@@ -0,0 +1 @@
|
||||
packaging/windows/licenses/* binary
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 复制。
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
```
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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"))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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! {
|
||||
Vendored
+24
-13
@@ -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
|
||||
|
||||
|
||||
Vendored
+3
-2
@@ -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
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user