fix: retry the Windows activation move on transient access denials - #14
Merged
Conversation
On GitHub-hosted windows-latest runners, Defender's real-time scan (or Search indexing) can briefly hold a handle on the freshly extracted directory, causing Directory.Move to throw UnauthorizedAccessException or IOException during activation. Wrap the activation moves (including the rollback/restore path) in a bounded retry (5 attempts, ~250ms apart) that only catches those two exception types and rethrows after the final attempt.
There was a problem hiding this comment.
Pull request overview
Improves the PowerShell installer’s reliability on Windows by tolerating transient filesystem access denials (e.g., Defender/indexer briefly holding handles) during the atomic “activation” directory moves.
Changes:
- added
Invoke-WithFilesystemRetryto retry onlyUnauthorizedAccessException/IOExceptionwith a small bounded delay. - wrapped activation/rollback
Directory.Moveoperations ininstall.ps1with the new filesystem retry helper. - added Pester unit tests for the new retry helper and documented the fix in
CHANGELOG.md.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| install.ps1 | Adds a targeted filesystem retry helper and applies it around activation/rollback directory moves. |
| tests/Install.Tests.ps1 | Adds unit tests validating retry behavior, exhaustion/rethrow, and non-retry for unrelated exceptions. |
| CHANGELOG.md | Notes the Windows activation transient access-denied retry behavior under Unreleased fixes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Directory.Move races with antivirus and indexer handles, and the previous 1s window (5 attempts, 250ms apart) is well short of what the established implementations allow: MSBuild's Copy task retries the same two exception types 10 times 1s apart, and rustup retries renames for tens of seconds after hitting exactly this failure mode. Match MSBuild's defaults, report the attempt count when the window is exhausted, and reject attempt and delay values that would skip the action or sleep backwards. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
install.ps1's atomic activation moves the freshly extracted directory into its final versioned location using[System.IO.Directory]::Move. On GitHub-hostedwindows-latestrunners, this was observed to intermittently fail with:(
UnauthorizedAccessException, sometimesIOException)Directory.Moveon Windows fails outright if any handle is open on the source or destination path. Microsoft Defender's real-time scan (or Windows Search indexing) can briefly hold a handle on a directory that was just extracted from a zip, causing the move to fail even though nothing in the install itself is wrong — a rerun always passes.This adds a small bounded retry (
Invoke-WithFilesystemRetry: up to 5 attempts, ~250ms apart, total <1.5s) around everyDirectory.Movein the activation sequence — staged-to-temp, old-to-backup, temp-to-final, and the rollback restore path — retrying only onUnauthorizedAccessException/IOExceptionand rethrowing unchanged after the final attempt. Download retries already had their ownInvoke-WithRetryhelper with second-scale linear backoff, which is a poor fit here (too slow, and it retries on any exception), so this is a separate, narrower helper mirroring the same scriptblock-based shape.Type of change
Validation
install.ps1): pass.Invoke-ScriptAnalyzer -Path install.ps1 -Settings PSGallery -EnableExit: pass, no findings.pwsh -Command "Invoke-Pester tests/Install.Tests.ps1"on macOS: 5 passed, 0 failed, 27 skipped (Windows-tagged black-box tests self-skip on non-Windows hosts; the new activation-retry unit tests are not Windows-tagged and ran and passed locally, covering: success after N transientUnauthorizedAccessExceptions, success after a transientIOException, exhausting retries and rethrowing, and not retrying unrelated exception types).sh -n install.sh: pass, no changes toinstall.sh.Compatibility
None. No change to supported targets, options, environment variables, installation paths, or the four-line output contract. Activation now tolerates transient filesystem access errors instead of surfacing them as install failures; genuine (non-transient) access/IO errors still fail the install after 5 attempts, same as before but slightly slower to surface.
Checklist
CHANGELOG.mdwhere appropriate.