Skip to content

P1: Improve portable upgrade swap (temp + atomic replace + rollback)#39

Merged
Kinin-Code-Offical merged 1 commit intomainfrom
p1/portable-upgrade-swap
Dec 22, 2025
Merged

P1: Improve portable upgrade swap (temp + atomic replace + rollback)#39
Kinin-Code-Offical merged 1 commit intomainfrom
p1/portable-upgrade-swap

Conversation

@Kinin-Code-Offical
Copy link
Owner

@Kinin-Code-Offical Kinin-Code-Offical commented Dec 22, 2025

Closes #22

Summary:

  • Stage portable updates via temp file in target dir.
  • Add rollback path if swap fails.
  • Clean up backup on success.

Tests:

  • npm ci (PASS)
  • npm run lint (PASS)
  • npm test (PASS)
  • npm run build (PASS)
  • npm run package (PASS)
  • npm run smoke:exe (PASS)

Rollback:

  • Revert this commit to restore the previous swap behavior.

Summary by Sourcery

Improve the portable self-update swap process by staging the new binary via a temp file, performing an atomic replacement, and adding rollback behavior on failure.

Bug Fixes:

  • Add rollback logic to restore the previous binary if swapping to the new version fails.

Enhancements:

  • Stage the new executable in the target directory via a temporary file before swapping to reduce partial-upgrade risk.
  • Remove backup artifacts after a successful swap to keep the install directory clean.

Copilot AI review requested due to automatic review settings December 22, 2025 02:14
@sourcery-ai
Copy link

sourcery-ai bot commented Dec 22, 2025

Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

Implements a safer portable self-update flow for the Windows binary by staging the new executable as a temp file in the target directory, performing an atomic-style swap with backup and cleanup, and adding rollback handling if the swap fails.

Sequence diagram for portable self-update swap with staging and rollback

sequenceDiagram
    actor User
    participant SelfUpdateScript
    participant FileSystem

    User->>SelfUpdateScript: run_self_update
    SelfUpdateScript->>FileSystem: compute TargetExe NewExe BackupExe TempExe
    SelfUpdateScript->>FileSystem: copy NewExe to TempExe (same dir)
    Note right of SelfUpdateScript: Stage new binary as TempExe

    SelfUpdateScript->>FileSystem: move TargetExe to BackupExe
    SelfUpdateScript->>FileSystem: move TempExe to TargetExe
    alt swap_success
        SelfUpdateScript->>FileSystem: remove BackupExe
        SelfUpdateScript->>FileSystem: start TargetExe with --version
    else swap_failure
        SelfUpdateScript->>SelfUpdateScript: catch error
        SelfUpdateScript->>FileSystem: check BackupExe exists
        SelfUpdateScript->>FileSystem: move BackupExe to TargetExe
        SelfUpdateScript->>FileSystem: check TempExe exists
        SelfUpdateScript->>FileSystem: remove TempExe
        SelfUpdateScript-->>User: propagate error
    end
Loading

Flow diagram for updated portable binary swap and rollback

flowchart TD
    A[Start self update] --> B[Compute paths for TargetExe NewExe BackupExe TempExe]
    B --> C[Copy NewExe to TempExe in target directory]
    C --> D[Try swap]
    D --> E[Move TargetExe to BackupExe]
    E --> F[Move TempExe to TargetExe]
    F --> G{Swap succeeded}
    G -->|Yes| H[Remove BackupExe]
    H --> I[Start new TargetExe with --version]
    I --> Z[End]

    G -->|No exception| J[Log swap failed attempting rollback]
    J --> K{BackupExe exists}
    K -->|Yes| L[Move BackupExe back to TargetExe]
    K -->|No| M[Skip restore original binary]
    L --> N
    M --> N[Continue rollback]
    N --> O{TempExe exists}
    O -->|Yes| P[Remove TempExe]
    O -->|No| Q[Skip temp cleanup]
    P --> R[Throw error]
    Q --> R[Throw error]
    R --> Z[End]
Loading

File-Level Changes

Change Details Files
Stage new executable in target directory before performing the swap.
  • Introduce a $TempExe path pointing to a .tmp file alongside the target executable.
  • Copy the downloaded $NewExe to $TempExe using Copy-Item with -Force before any swap operations.
  • Log the staging step to the console via Write-Host for better observability.
src/core/selfUpdate.ts
Make the binary swap safer and reversible with try/catch, backup, and rollback logic.
  • Wrap the swap logic in a try/catch block instead of running two bare Move-Item calls.
  • First move the current $TargetExe to $BackupExe (silently continuing if it does not exist), then move $TempExe into place as $TargetExe.
  • On success, remove the backup executable using Remove-Item with -Force and -ErrorAction SilentlyContinue to avoid leaving stale backups.
  • On failure, detect and restore from $BackupExe back to $TargetExe where possible, and ensure the staged temp executable is deleted if present, then rethrow to propagate the error.
src/core/selfUpdate.ts

Assessment against linked issues

Issue Objective Addressed Explanation
#22 Stage the new portable executable via a temporary file in the target directory before swapping.
#22 Perform the swap as an atomic-style replace with rollback behavior if the swap fails.
#22 Clean up temporary/backup files after a successful swap so no leftover artifacts remain.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +329 to +332
try {
Move-Item -Path $TargetExe -Destination $BackupExe -Force -ErrorAction SilentlyContinue
Move-Item -Path $TempExe -Destination $TargetExe -Force
Remove-Item -Path $BackupExe -Force -ErrorAction SilentlyContinue

Choose a reason for hiding this comment

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

P1 Badge Enforce terminating errors for swap failures

In PowerShell, try/catch only handles terminating errors; Move-Item and Copy-Item emit non-terminating errors by default. As written, failures such as a locked target binary or permission issues will not enter the catch block, so the rollback path is skipped and the script still proceeds to Start-Process, potentially leaving the old binary in place or an inconsistent swap state. Consider setting -ErrorAction Stop on the move operations (and the copy if you want a hard fail) so swap failures actually trigger the rollback logic.

Useful? React with 👍 / 👎.

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • Consider wrapping the staging Copy-Item into the same try/catch flow (with a finally) so that failures during the temp copy also trigger consistent cleanup of $TempExe and don't leave partial temp files behind.
  • If there’s any chance of concurrent updates, using a unique temp filename (e.g., including a PID or GUID) instead of $TargetExe.tmp would avoid collisions between multiple updater processes operating in the same directory.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consider wrapping the staging `Copy-Item` into the same `try`/`catch` flow (with a `finally`) so that failures during the temp copy also trigger consistent cleanup of `$TempExe` and don't leave partial temp files behind.
- If there’s any chance of concurrent updates, using a unique temp filename (e.g., including a PID or GUID) instead of `$TargetExe.tmp` would avoid collisions between multiple updater processes operating in the same directory.

## Individual Comments

### Comment 1
<location> `src/core/selfUpdate.ts:324-333` </location>
<code_context>

+Write-Host "Staging new binary..."
+$TempExe = "$TargetExe.tmp"
+Copy-Item -Path $NewExe -Destination $TempExe -Force
+
 Write-Host "Swapping binaries..."
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Handle failures in the staging copy step consistently with the swap/rollback logic.

`Copy-Item` runs outside the `try` block and without `-ErrorAction Stop`, so failures copying to `$TempExe` (permissions, disk, locks, etc.) won’t use the same rollback/error handling as the `Move-Item` failures. Please either move this call into the `try` block and/or add `-ErrorAction Stop` so copy failures follow the same rollback path.

```suggestion
Write-Host "Staging new binary..."
$TempExe = "$TargetExe.tmp"

Write-Host "Swapping binaries..."
try {
    Copy-Item -Path $NewExe -Destination $TempExe -Force -ErrorAction Stop
    Move-Item -Path $TargetExe -Destination $BackupExe -Force -ErrorAction SilentlyContinue
    Move-Item -Path $TempExe -Destination $TargetExe -Force
    Remove-Item -Path $BackupExe -Force -ErrorAction SilentlyContinue
} catch {
```
</issue_to_address>

### Comment 2
<location> `src/core/selfUpdate.ts:333-342` </location>
<code_context>
+} catch {
+    Write-Host "Swap failed, attempting rollback..."
+    if (Test-Path $BackupExe) {
+        Move-Item -Path $BackupExe -Destination $TargetExe -Force -ErrorAction SilentlyContinue
+    }
+    if (Test-Path $TempExe) {
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Avoid fully silencing rollback failures to aid diagnosis and reliability.

`Move-Item` is using `-ErrorAction SilentlyContinue`, which can hide failures to restore the original binary (e.g., due to permissions, locks, or invalid paths). Since rollback is the final safeguard after a failed swap, consider removing `SilentlyContinue` or at least logging rollback failures so it’s clear when both the update and rollback have failed.

```suggestion
} catch {
    Write-Host "Swap failed, attempting rollback..."
    if (Test-Path $BackupExe) {
        try {
            Move-Item -Path $BackupExe -Destination $TargetExe -Force -ErrorAction Stop
        } catch {
            Write-Warning "Rollback failed: could not restore original binary from '$BackupExe' to '$TargetExe'. Error: $_"
        }
    }
    if (Test-Path $TempExe) {
        try {
            Remove-Item -Path $TempExe -Force -ErrorAction Stop
        } catch {
            Write-Warning "Rollback cleanup failed: could not remove temporary binary '$TempExe'. Error: $_"
        }
    }
    throw
}
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +324 to +333
Write-Host "Staging new binary..."
$TempExe = "$TargetExe.tmp"
Copy-Item -Path $NewExe -Destination $TempExe -Force

Write-Host "Swapping binaries..."
Move-Item -Path $TargetExe -Destination $BackupExe -Force -ErrorAction SilentlyContinue
Move-Item -Path $NewExe -Destination $TargetExe -Force
try {
Move-Item -Path $TargetExe -Destination $BackupExe -Force -ErrorAction SilentlyContinue
Move-Item -Path $TempExe -Destination $TargetExe -Force
Remove-Item -Path $BackupExe -Force -ErrorAction SilentlyContinue
} catch {
Copy link

Choose a reason for hiding this comment

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

suggestion (bug_risk): Handle failures in the staging copy step consistently with the swap/rollback logic.

Copy-Item runs outside the try block and without -ErrorAction Stop, so failures copying to $TempExe (permissions, disk, locks, etc.) won’t use the same rollback/error handling as the Move-Item failures. Please either move this call into the try block and/or add -ErrorAction Stop so copy failures follow the same rollback path.

Suggested change
Write-Host "Staging new binary..."
$TempExe = "$TargetExe.tmp"
Copy-Item -Path $NewExe -Destination $TempExe -Force
Write-Host "Swapping binaries..."
Move-Item -Path $TargetExe -Destination $BackupExe -Force -ErrorAction SilentlyContinue
Move-Item -Path $NewExe -Destination $TargetExe -Force
try {
Move-Item -Path $TargetExe -Destination $BackupExe -Force -ErrorAction SilentlyContinue
Move-Item -Path $TempExe -Destination $TargetExe -Force
Remove-Item -Path $BackupExe -Force -ErrorAction SilentlyContinue
} catch {
Write-Host "Staging new binary..."
$TempExe = "$TargetExe.tmp"
Write-Host "Swapping binaries..."
try {
Copy-Item -Path $NewExe -Destination $TempExe -Force -ErrorAction Stop
Move-Item -Path $TargetExe -Destination $BackupExe -Force -ErrorAction SilentlyContinue
Move-Item -Path $TempExe -Destination $TargetExe -Force
Remove-Item -Path $BackupExe -Force -ErrorAction SilentlyContinue
} catch {

Comment on lines +333 to +342
} catch {
Write-Host "Swap failed, attempting rollback..."
if (Test-Path $BackupExe) {
Move-Item -Path $BackupExe -Destination $TargetExe -Force -ErrorAction SilentlyContinue
}
if (Test-Path $TempExe) {
Remove-Item -Path $TempExe -Force -ErrorAction SilentlyContinue
}
throw
}
Copy link

Choose a reason for hiding this comment

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

suggestion (bug_risk): Avoid fully silencing rollback failures to aid diagnosis and reliability.

Move-Item is using -ErrorAction SilentlyContinue, which can hide failures to restore the original binary (e.g., due to permissions, locks, or invalid paths). Since rollback is the final safeguard after a failed swap, consider removing SilentlyContinue or at least logging rollback failures so it’s clear when both the update and rollback have failed.

Suggested change
} catch {
Write-Host "Swap failed, attempting rollback..."
if (Test-Path $BackupExe) {
Move-Item -Path $BackupExe -Destination $TargetExe -Force -ErrorAction SilentlyContinue
}
if (Test-Path $TempExe) {
Remove-Item -Path $TempExe -Force -ErrorAction SilentlyContinue
}
throw
}
} catch {
Write-Host "Swap failed, attempting rollback..."
if (Test-Path $BackupExe) {
try {
Move-Item -Path $BackupExe -Destination $TargetExe -Force -ErrorAction Stop
} catch {
Write-Warning "Rollback failed: could not restore original binary from '$BackupExe' to '$TargetExe'. Error: $_"
}
}
if (Test-Path $TempExe) {
try {
Remove-Item -Path $TempExe -Force -ErrorAction Stop
} catch {
Write-Warning "Rollback cleanup failed: could not remove temporary binary '$TempExe'. Error: $_"
}
}
throw
}

@Kinin-Code-Offical Kinin-Code-Offical merged commit b2f7ad8 into main Dec 22, 2025
9 checks passed
@Kinin-Code-Offical Kinin-Code-Offical deleted the p1/portable-upgrade-swap branch December 22, 2025 02:16
Copy link

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

This PR enhances the portable executable upgrade process by introducing a more robust file swap mechanism with staging, rollback capabilities, and cleanup. The changes address issue #22 by improving the reliability of self-updates for portable installations.

Key changes:

  • Introduces a temporary staging file (.tmp) to prepare the new binary before the atomic swap operation
  • Adds try-catch error handling around the swap operation with automatic rollback on failure
  • Implements cleanup of backup files upon successful swap

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

}
if (Test-Path $TempExe) {
Remove-Item -Path $TempExe -Force -ErrorAction SilentlyContinue
}
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

The rollback logic doesn't clean up the $NewExe file after a failed swap. If the swap fails and rollback occurs, the original downloaded file at $NewExe will remain on disk, potentially causing confusion or wasting space. Consider adding cleanup for $NewExe in the catch block.

Suggested change
}
}
if (Test-Path $NewExe) {
Remove-Item -Path $NewExe -Force -ErrorAction SilentlyContinue
}

Copilot uses AI. Check for mistakes.

Write-Host "Staging new binary..."
$TempExe = "$TargetExe.tmp"
Copy-Item -Path $NewExe -Destination $TempExe -Force
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

If the Copy-Item operation fails (e.g., due to insufficient disk space or permissions), $TempExe won't be created, but the subsequent Move-Item operations will proceed anyway. This could lead to data loss if $TargetExe is moved to $BackupExe but there's no $TempExe to replace it with. Consider adding error handling or verification after the Copy-Item operation.

Suggested change
Copy-Item -Path $NewExe -Destination $TempExe -Force
try {
Copy-Item -Path $NewExe -Destination $TempExe -Force -ErrorAction Stop
if (-not (Test-Path $TempExe)) {
Write-Host "Staging failed: temporary executable not found at $TempExe"
throw "Staging failed"
}
} catch {
Write-Host "Staging new binary failed, aborting update."
throw
}

Copilot uses AI. Check for mistakes.
Move-Item -Path $TargetExe -Destination $BackupExe -Force -ErrorAction SilentlyContinue
Move-Item -Path $NewExe -Destination $TargetExe -Force
try {
Move-Item -Path $TargetExe -Destination $BackupExe -Force -ErrorAction SilentlyContinue
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

Using -ErrorAction SilentlyContinue on the Move-Item that moves $TargetExe to $BackupExe can mask real errors. If this operation fails silently (e.g., due to file locks or permissions), the next Move-Item will attempt to overwrite the active $TargetExe without creating a backup, resulting in potential data loss if the operation fails. Consider removing -ErrorAction SilentlyContinue or at least verifying the backup was created before proceeding.

Suggested change
Move-Item -Path $TargetExe -Destination $BackupExe -Force -ErrorAction SilentlyContinue
Move-Item -Path $TargetExe -Destination $BackupExe -Force -ErrorAction SilentlyContinue
if ((Test-Path $TargetExe) -and -not (Test-Path $BackupExe)) {
throw "Failed to create backup of existing executable."
}

Copilot uses AI. Check for mistakes.
Comment on lines +336 to +339
Move-Item -Path $BackupExe -Destination $TargetExe -Force -ErrorAction SilentlyContinue
}
if (Test-Path $TempExe) {
Remove-Item -Path $TempExe -Force -ErrorAction SilentlyContinue
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

The rollback operations use -ErrorAction SilentlyContinue, which means if the rollback itself fails, the error will be silently suppressed. This could leave the system in a broken state without proper notification. Since this is in the catch block and already handling an error, consider allowing rollback failures to be visible or adding explicit error logging.

Suggested change
Move-Item -Path $BackupExe -Destination $TargetExe -Force -ErrorAction SilentlyContinue
}
if (Test-Path $TempExe) {
Remove-Item -Path $TempExe -Force -ErrorAction SilentlyContinue
Move-Item -Path $BackupExe -Destination $TargetExe -Force
}
if (Test-Path $TempExe) {
Remove-Item -Path $TempExe -Force

Copilot uses AI. Check for mistakes.
try {
Move-Item -Path $TargetExe -Destination $BackupExe -Force -ErrorAction SilentlyContinue
Move-Item -Path $TempExe -Destination $TargetExe -Force
Remove-Item -Path $BackupExe -Force -ErrorAction SilentlyContinue
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

The backup file is removed immediately after the swap succeeds, but this happens inside the try block before the new version is verified. If the new version fails to start or has issues (line 345), there's no way to recover the backup. Consider moving the backup cleanup outside the try-catch block or after verifying the new version starts successfully.

Copilot uses AI. Check for mistakes.
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.

P1: Improve portable upgrade swap (temp + atomic replace + rollback)

1 participant