Skip to content

fix: retry self-upgrade with binary rename on Linux/WSL to avoid ETXTBSY#21793

Merged
pelikhan merged 6 commits intomainfrom
copilot/fix-gh-aw-upgrade-wsl-error
Mar 19, 2026
Merged

fix: retry self-upgrade with binary rename on Linux/WSL to avoid ETXTBSY#21793
pelikhan merged 6 commits intomainfrom
copilot/fix-gh-aw-upgrade-wsl-error

Conversation

Copy link
Contributor

Copilot AI commented Mar 19, 2026

gh aw upgrade fails on Linux/WSL because gh extension upgrade cannot overwrite the gh-aw binary while it is the currently-executing process — the kernel returns ETXTBSY when any process attempts to open() a memory-mapped executable for writing.

Change

The upgrade now uses a try-first, rename-retry strategy to minimize the risk of leaving the installation in a broken state:

  1. First attempt: gh extension upgrade is run directly, without touching the filesystem. On most systems — and on Linux when there is no in-use binary conflict — this succeeds with zero file manipulation.
  2. Rename + retry (Linux only): if the first attempt fails (likely ETXTBSY), the current executable is atomically renamed to a .bak backup path, freeing the original path for gh to write the newly-downloaded binary. The inode remains alive for the running process.
  3. Restore on retry failure: if the retry also fails (network error, permissions, etc.), the backup is automatically renamed back so the user is never left without a working gh-aw installation.
  4. Correct relaunch path: after the rename, os.Executable() returns a "(deleted)"-suffixed path on Linux. upgradeExtensionIfOutdated returns the pre-rename install path, passed directly to relaunchWithSameArgs via a new exeOverride parameter, ensuring the freshly-installed binary is exec'd correctly.

On Linux, the first attempt's output is buffered and suppressed when the rename+retry succeeds, so users do not see a confusing error followed by success.

// renamePathForUpgrade renames exe → exe.bak, freeing the path for the new binary.
// restoreExecutableBackup renames exe.bak → exe if the upgrade fails.
// cleanupExecutableBackup removes exe.bak after a successful upgrade.
  • No behaviour change on macOS or Windows — the first attempt is the only attempt and no files are renamed.
  • Rename failure is non-fatal: logged at debug level and the retry proceeds (it may still fail with ETXTBSY, but with an informative error from gh).
  • If backup restoration itself fails, a user-visible error message is printed with instructions to recover manually.
  • Helper functions (firstAttemptWriter, renamePathForUpgrade, restoreExecutableBackup, cleanupExecutableBackup) are extracted and covered by unit tests.

💬 Send tasks to Copilot coding agent from Slack and Teams to turn conversations into code. Copilot posts an update in your thread when it's finished.

Co-authored-by: dsyme <7204669+dsyme@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix gh aw upgrade failure on WSL due to file in use fix: unlink binary before self-upgrade on Linux/WSL to avoid ETXTBSY Mar 19, 2026
Copilot AI requested a review from dsyme March 19, 2026 13:17
@dsyme dsyme marked this pull request as ready for review March 19, 2026 13:57
Copilot AI review requested due to automatic review settings March 19, 2026 13:57
updateExtensionCheckLog.Printf("Upgrading extension from %s to %s", currentVersion, latestVersion)
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Upgrading gh-aw extension from %s to %s...", currentVersion, latestVersion)))

// On Linux (including WSL), the kernel returns ETXTBSY when any process
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pelikhan wdyt, could you look at this? Seems kind of dodgy to delete outselves befor we have a clear understanding the upgrade will succeed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replaced the os.Remove approach with os.Rename to a .bak backup in commit 87759d2. If gh extension upgrade succeeds, the backup is cleaned up; if it fails, the backup is automatically restored so the user is never left without gh-aw. The upgrade is only attempted after the rename — we don't proceed blindly without knowing the path is free.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes gh aw upgrade failures on Linux/WSL where gh extension upgrade cannot overwrite the currently-running gh-aw executable due to ETXTBSY (“text file busy”).

Changes:

  • Add Linux-only pre-upgrade logic to resolve the current executable path and os.Remove() it before invoking gh extension upgrade.
  • Add debug logging around the pre-removal attempt to aid diagnosis.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +83 to +96
// tries to open a currently-executing binary for writing. Removing the
// directory entry (unlink) first avoids this: the inode stays alive while
// this process is running, but the path is now free for gh to write the
// newly-downloaded binary.
if runtime.GOOS == "linux" {
if exe, err := os.Executable(); err == nil {
// Resolve any symlink so we remove the real file, not just a link.
if resolved, resolveErr := filepath.EvalSymlinks(exe); resolveErr == nil {
exe = resolved
}
if rmErr := os.Remove(exe); rmErr != nil {
updateExtensionCheckLog.Printf("Could not pre-remove executable before upgrade (upgrade may still fail): %v", rmErr)
} else {
updateExtensionCheckLog.Printf("Pre-removed executable to avoid ETXTBSY on Linux: %s", exe)
Comment on lines +93 to +97
if rmErr := os.Remove(exe); rmErr != nil {
updateExtensionCheckLog.Printf("Could not pre-remove executable before upgrade (upgrade may still fail): %v", rmErr)
} else {
updateExtensionCheckLog.Printf("Pre-removed executable to avoid ETXTBSY on Linux: %s", exe)
}
Comment on lines +82 to +99
// On Linux (including WSL), the kernel returns ETXTBSY when any process
// tries to open a currently-executing binary for writing. Removing the
// directory entry (unlink) first avoids this: the inode stays alive while
// this process is running, but the path is now free for gh to write the
// newly-downloaded binary.
if runtime.GOOS == "linux" {
if exe, err := os.Executable(); err == nil {
// Resolve any symlink so we remove the real file, not just a link.
if resolved, resolveErr := filepath.EvalSymlinks(exe); resolveErr == nil {
exe = resolved
}
if rmErr := os.Remove(exe); rmErr != nil {
updateExtensionCheckLog.Printf("Could not pre-remove executable before upgrade (upgrade may still fail): %v", rmErr)
} else {
updateExtensionCheckLog.Printf("Pre-removed executable to avoid ETXTBSY on Linux: %s", exe)
}
}
}
@pelikhan
Copy link
Contributor

@copilot review comments

…e failure

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title fix: unlink binary before self-upgrade on Linux/WSL to avoid ETXTBSY fix: rename binary before self-upgrade on Linux/WSL to avoid ETXTBSY Mar 19, 2026
Copilot AI requested a review from pelikhan March 19, 2026 17:16
@pelikhan
Copy link
Contributor

@copilot review the risk of bricking the extension and make sure that the copy/swap is done when the new binary is ready and minize potential failure that would break the installation.

…fails

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 19, 2026

@copilot review the risk of bricking the extension and make sure that the copy/swap is done when the new binary is ready and minize potential failure that would break the installatio...

Reworked in f37859c. The swap now only happens after the upgrade has already been attempted:

  1. First, try gh extension upgrade directly — no filesystem changes at all (zero bricking risk). On macOS/Windows this is the only path.
  2. Only if the first attempt fails on Linux (likely ETXTBSY): rename the binary to .bak, then retry.
  3. On retry success → clean up .bak.
  4. On retry failure → restore .bak so the user still has a working gh-aw.

The first attempt's error output is buffered on Linux and suppressed when the retry succeeds, so users see a clean experience. The binary is never touched on systems where the upgrade works first time.

Copilot AI changed the title fix: rename binary before self-upgrade on Linux/WSL to avoid ETXTBSY fix: retry self-upgrade with binary rename on Linux/WSL to avoid ETXTBSY Mar 19, 2026
@pelikhan pelikhan merged commit 534d92f into main Mar 19, 2026
102 of 103 checks passed
@pelikhan pelikhan deleted the copilot/fix-gh-aw-upgrade-wsl-error branch March 19, 2026 18:18
Copilot stopped work on behalf of pelikhan due to an error March 19, 2026 18:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

gh aw upgrade fails on WSL because it can't update itself while running

4 participants