aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-08-18 03:45:53 +0300
committerGitHub <noreply@github.com>2026-08-18 03:45:53 +0300
commitd497ee14dd3f2b5325a16d0de779ed6bd62178ee (patch)
treef8f96edd0eaa0fa7641387eceb659793dc566aba
parentb177acf014d57e8deaf0ba5934b9ddf5f252e8d6 (diff)
fix: retry Windows installer activation after transient locks (#2921)
refs #2916
-rw-r--r--.github/workflows/windows-arm64.yml37
-rw-r--r--docs/next/CHANGELOG.md1
-rw-r--r--scripts/windows_arm64_installer_test.ps149
-rw-r--r--scripts/windows_install_conpty_package_test.ps158
-rw-r--r--website/install.ps136
5 files changed, 180 insertions, 1 deletions
diff --git a/.github/workflows/windows-arm64.yml b/.github/workflows/windows-arm64.yml
new file mode 100644
index 00000000..1488d1f1
--- /dev/null
+++ b/.github/workflows/windows-arm64.yml
@@ -0,0 +1,37 @@
+name: Windows ARM64 installer
+
+on:
+ pull_request:
+ types: [opened, synchronize, reopened]
+ paths:
+ - ".github/workflows/windows-arm64.yml"
+ - "scripts/windows_arm64_installer_test.ps1"
+ - "website/install.ps1"
+ push:
+ branches: [master]
+ paths:
+ - ".github/workflows/windows-arm64.yml"
+ - "scripts/windows_arm64_installer_test.ps1"
+ - "website/install.ps1"
+
+permissions:
+ contents: read
+
+concurrency:
+ group: windows-arm64-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ installer:
+ name: installer
+ runs-on: windows-11-arm
+ timeout-minutes: 10
+ steps:
+ - name: Checkout
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
+ with:
+ persist-credentials: false
+
+ - name: Test x86_64 fallback installation
+ shell: powershell
+ run: .\scripts\windows_arm64_installer_test.ps1
diff --git a/docs/next/CHANGELOG.md b/docs/next/CHANGELOG.md
index 7c8afecb..ac55d0be 100644
--- a/docs/next/CHANGELOG.md
+++ b/docs/next/CHANGELOG.md
@@ -23,6 +23,7 @@
- Experimental pane graphics now support bounded named layers, acknowledged full-RGBA primary-layer direct file frames on audited local terminals, owned BGRA fallback, exact pixel mouse input, and placement-only resize replay.
### Fixed
+- The Windows ARM64 installer now waits for x64 emulation to release the verified executable before activating the downloaded release. (#2916)
- On Unix, Ctrl-click URL openers are now reaped after they exit, preventing defunct child processes from accumulating on long-running servers. (#2903)
- Herdr no longer sends the full OSC 4 palette query burst under WSL, preventing reply fragments from leaking into the shell through ConPTY. (#2440)
- Qwen Code panes now use locale-independent terminal-title states and localized confirmation fallbacks, preventing active or blocked turns from appearing idle. (#2756)
diff --git a/scripts/windows_arm64_installer_test.ps1 b/scripts/windows_arm64_installer_test.ps1
new file mode 100644
index 00000000..7f5d7174
--- /dev/null
+++ b/scripts/windows_arm64_installer_test.ps1
@@ -0,0 +1,49 @@
+Set-StrictMode -Version Latest
+$ErrorActionPreference = "Stop"
+$ProgressPreference = "SilentlyContinue"
+
+$architecture = [System.Runtime.InteropServices.RuntimeInformation, mscorlib]::OSArchitecture.ToString()
+Write-Host "OS architecture: $architecture"
+Write-Host "Process architecture: $([System.Runtime.InteropServices.RuntimeInformation, mscorlib]::ProcessArchitecture)"
+Write-Host "PowerShell: $($PSVersionTable.PSVersion) $($PSVersionTable.PSEdition)"
+Write-Host "Windows: $([Environment]::OSVersion.VersionString)"
+if ($architecture -ne "Arm64") {
+ throw "This test requires Windows ARM64, found $architecture."
+}
+
+$root = Join-Path $env:RUNNER_TEMP "herdr-windows-arm64-installer-test"
+$env:HERDR_HOME = Join-Path $root "home"
+$env:HERDR_INSTALL_DIR = Join-Path $root "bin"
+$env:HERDR_CHANNEL = "preview"
+Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue
+New-Item -ItemType Directory -Force -Path $root | Out-Null
+$installer = (Resolve-Path (Join-Path $PSScriptRoot "..\website\install.ps1")).Path
+
+$previousErrorActionPreference = $ErrorActionPreference
+$ErrorActionPreference = "Continue"
+try {
+ $installerOutput = & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $installer 2>&1
+ $installerExitCode = $LASTEXITCODE
+} finally {
+ $ErrorActionPreference = $previousErrorActionPreference
+}
+$installerOutput | ForEach-Object { Write-Host $_ }
+if ($installerExitCode -ne 0) {
+ throw "The installer failed on Windows ARM64 with exit code $installerExitCode."
+}
+
+$installedHerdr = Join-Path $env:HERDR_INSTALL_DIR "herdr.exe"
+if (-not (Test-Path -LiteralPath $installedHerdr -PathType Leaf)) {
+ throw "The installer exited successfully without activating herdr.exe."
+}
+& $installedHerdr --version
+if ($LASTEXITCODE -ne 0) {
+ throw "The installed x86_64 Herdr executable failed with exit code $LASTEXITCODE."
+}
+
+$releases = Join-Path $env:HERDR_HOME "packages\standalone\releases"
+if (@(Get-ChildItem -LiteralPath $releases -Force -Directory -Filter ".staging.*" -ErrorAction SilentlyContinue).Count -ne 0) {
+ throw "The installer succeeded but left a staging directory behind."
+}
+
+Write-Host "Windows ARM64 installer test passed."
diff --git a/scripts/windows_install_conpty_package_test.ps1 b/scripts/windows_install_conpty_package_test.ps1
index 4b0e73f3..95b31c4a 100644
--- a/scripts/windows_install_conpty_package_test.ps1
+++ b/scripts/windows_install_conpty_package_test.ps1
@@ -212,6 +212,64 @@ try {
$manifest | Out-File -LiteralPath $manifestPath -Encoding utf8
$stagedConpty = Join-Path $releasesDir ".staging.$($releaseDir.Name).$PID\conpty\conpty.dll"
+
+ $transientLockState = @{ Handle = $null; Acquired = $false; Released = $false }
+ $transientLockTimer = New-Object System.Timers.Timer
+ $transientLockTimer.Interval = 300
+ $transientLockTimer.AutoReset = $false
+ $transientLockSource = "HerdrTransientInstallerLock-$PID"
+ $transientLockRelease = Register-ObjectEvent `
+ -InputObject $transientLockTimer `
+ -EventName Elapsed `
+ -SourceIdentifier $transientLockSource `
+ -MessageData $transientLockState `
+ -Action {
+ $state = $event.MessageData
+ if ($null -ne $state.Handle) {
+ $state.Handle.Dispose()
+ $state.Handle = $null
+ }
+ $state.Released = $true
+ }
+ $lockStagedFileTransiently = {
+ if (-not $transientLockState.Acquired) {
+ $transientLockState.Handle = [System.IO.File]::Open(
+ $stagedConpty,
+ [System.IO.FileMode]::Open,
+ [System.IO.FileAccess]::Read,
+ [System.IO.FileShare]::Read
+ )
+ $transientLockState.Acquired = $true
+ $transientLockTimer.Start()
+ }
+ }.GetNewClosure()
+ $transientLockBreakpoint = Set-PSBreakpoint -Script $installerPath -Variable "backupDir" -Mode Write -Action $lockStagedFileTransiently
+ try {
+ & "$PSScriptRoot\..\website\install.ps1" `
+ -ManifestUrl $manifestUrl `
+ -InstallDir $installDir `
+ -ExpectedBuildId "installer-test"
+ if (-not $transientLockState.Acquired) {
+ throw "installer did not acquire the transient staged-file lock"
+ }
+ if (-not $transientLockState.Released) {
+ throw "installer activated the release before the transient lock was released"
+ }
+ if (-not (Test-Path -LiteralPath (Join-Path $releaseDir.FullName "conpty\conpty.dll") -PathType Leaf)) {
+ throw "installer did not repair the release after the transient lock cleared"
+ }
+ } finally {
+ Remove-PSBreakpoint -Breakpoint $transientLockBreakpoint
+ $transientLockTimer.Stop()
+ Unregister-Event -SourceIdentifier $transientLockSource -ErrorAction SilentlyContinue
+ Remove-Job -Id $transientLockRelease.Id -Force -ErrorAction SilentlyContinue
+ if ($null -ne $transientLockState.Handle) {
+ $transientLockState.Handle.Dispose()
+ }
+ $transientLockTimer.Dispose()
+ }
+
+ Remove-Item -LiteralPath (Join-Path $releaseDir.FullName "conpty\conpty.dll") -Force
$lockState = @{ Handle = $null }
$lockStagedFile = {
if ($null -eq $lockState.Handle) {
diff --git a/website/install.ps1 b/website/install.ps1
index 55b3b381..cef4af04 100644
--- a/website/install.ps1
+++ b/website/install.ps1
@@ -372,6 +372,40 @@ function Test-HerdrReleaseComplete {
return $true
}
+function Move-DirectoryWithRetry {
+ param(
+ [string]$Source,
+ [string]$Destination,
+ [int]$TimeoutMilliseconds = 5000
+ )
+
+ $deadline = [DateTime]::UtcNow.AddMilliseconds($TimeoutMilliseconds)
+ while ($true) {
+ try {
+ [System.IO.Directory]::Move($Source, $Destination)
+ return
+ } catch {
+ $retryable = $false
+ $exception = $_.Exception
+ while ($null -ne $exception) {
+ if ($exception -is [System.IO.IOException] -or
+ $exception -is [System.UnauthorizedAccessException]) {
+ $retryable = $true
+ break
+ }
+ $exception = $exception.InnerException
+ }
+ if (-not $retryable -or
+ [DateTime]::UtcNow -ge $deadline -or
+ -not (Test-Path -LiteralPath $Source -PathType Container) -or
+ (Test-Path -LiteralPath $Destination)) {
+ throw
+ }
+ Start-Sleep -Milliseconds 100
+ }
+ }
+}
+
function Invoke-WithInstallLock {
param(
[string]$LockPath,
@@ -668,7 +702,7 @@ try {
[System.IO.Directory]::Move($releaseDir, $backupDir)
}
try {
- [System.IO.Directory]::Move($stagingDir, $releaseDir)
+ Move-DirectoryWithRetry -Source $stagingDir -Destination $releaseDir
} catch {
if ($null -ne $backupDir -and -not (Test-Path -LiteralPath $releaseDir)) {
[System.IO.Directory]::Move($backupDir, $releaseDir)