fix: retry Windows installer activation after transient locks (#2921)

refs #2916
This commit is contained in:
Can Celik
2026-08-18 03:45:53 +03:00
committed by GitHub
parent b177acf014
commit d497ee14dd
5 changed files with 180 additions and 1 deletions
+37
View File
@@ -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
+1
View File
@@ -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)
+49
View File
@@ -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."
@@ -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) {
+35 -1
View File
@@ -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)