Skip to content

fix(cua-driver-rs)(install): rename-out-of-way locked binary before Copy-Item in install-local.ps1 - #1687

Merged
f-trycua merged 1 commit into
mainfrom
install-local-stop-daemon-first
May 24, 2026
Merged

fix(cua-driver-rs)(install): rename-out-of-way locked binary before Copy-Item in install-local.ps1#1687
f-trycua merged 1 commit into
mainfrom
install-local-stop-daemon-first

Conversation

@f-trycua

@f-trycua f-trycua commented May 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

Repro from real user session: re-running `install-local.ps1` while the autostart task has a daemon running fails with:

```
Copy-Item : The process cannot access the file '...cua-driver.exe' because it is being used by another process.
```

Why kill-the-daemon doesn't fix it

The autostart task is `RunLevel=Highest`, so the spawned `cua-driver.exe` runs at High IL. A Medium-IL PowerShell shell cannot terminate a High-IL process — `taskkill /F` and `Stop-Process` both return Access Denied. `schtasks /End` ends the task INSTANCE but the daemon was detached via `Start-Process -WindowStyle Hidden` so it survives.

The only kill paths that work need UAC elevation. Adding a UAC prompt to install-local.ps1 is ugly — interrupts the dev loop every re-build.

Fix: rename-out-of-way

Windows loads .exe images with `FILE_SHARE_DELETE` set, so the directory entry can be renamed while the content stays mapped. `Move-Item` succeeds against a locked .exe; `Copy-Item` then lands at the (now-free) destination path. The renamed-aside file is unlinked at next reboot or when the daemon process exits.

```powershell
if (Test-Path -LiteralPath $DestBinary) {
$stale = "$DestBinary.stale-$((Get-Date).ToString('yyyyMMdd-HHmmss'))"
Move-Item $DestBinary $stale -Force
}
Copy-Item $BuiltBinary $DestBinary -Force
```

Plus a best-effort GC: `.stale-*` siblings older than 1 day get cleaned up so the dir doesn't grow unbounded across many re-builds.

End-to-end verified on Windows

Daemon PID 14152 running (High IL, from `cua-driver autostart kick`):

```
==> renamed locked previous binary to cua-driver.exe.stale-20260524-144036
==> staging into C:\Users\cuademo.cua-driver\packages\releases\0.0.0-local-release-...
```

Post-install:

```
$ ls .cua-driver/packages/releases/0.0.0-local-release-.../
cua-driver.exe ← May 24 14:39 — fresh build
cua-driver.exe.stale-20260524-144036 ← old, still locked by PID 14152
```

Daemon keeps running off the renamed file. New `cua-driver autostart enable` will re-register the task pointing at the fresh `cua-driver.exe`.

If `Move-Item` fails (rare — would require AV/EDR with `FILE_SHARE_NONE`), the script prints a clear hint instead of just dying:

```
Note: could not rename previous binary at .
()
Most likely a running cua-driver daemon is holding it.
Stop it first (e.g. `schtasks /End /TN cua-driver-serve` then re-run).
```

Test plan

  • PowerShell parser clean
  • End-to-end: install-local.ps1 with a High-IL daemon running succeeds (renames + copies)
  • Reviewer: same on a host with no daemon — should still work (the rename branch is gated on `Test-Path`)
  • Reviewer: rapid re-builds don't accumulate stale-* files unboundedly (GC older than 1 day)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Enhanced the local Windows installer's reliability during updates. The installer now safely handles cases where the previous version's binary is still executing by renaming existing locked executable files and automatically cleaning up older installation artifacts from prior updates. This enables more robust and reliable updates even when the prior version is still running.

Review Change Stack

…opy-Item in install-local.ps1

Repro: user re-runs install-local.ps1 while cua-driver-serve task has
spawned a High-IL daemon at logon. Copy-Item fails with "The process
cannot access the file ... because it is being used by another
process."

Killing the daemon via taskkill / Stop-Process doesn't work — Medium-
IL shell can't terminate the High-IL process spawned from a
RunLevel=Highest task. schtasks /End ends the task INSTANCE but the
spawned cua-driver.exe was detached via Start-Process and survives,
still holding the .exe locked.

Workaround that does work: rename the locked file out of the way.
Windows loads .exe images with FILE_SHARE_DELETE, so the directory
entry can be renamed while the content stays mapped. After rename,
the destination path is free for Copy-Item; the old file is unlinked
at the next reboot (or when the daemon exits).

Timestamp suffix on the rename so multiple re-builds stack cleanly:
  cua-driver.exe.stale-20260524-144036
Best-effort GC of stale-* siblings >1 day old, so the dir doesn't
grow unbounded across many re-builds.

Verified on Windows with a running High-IL daemon — Copy-Item now
succeeds, daemon keeps running off the renamed file until restart.

If rename fails (rare — would require an antivirus / EDR holding the
file with FILE_SHARE_NONE), the script prints a clear "stop the
daemon and retry" hint instead of just dying on the IOException.
@vercel

vercel Bot commented May 24, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Ignored Ignored May 24, 2026 2:42pm

Request Review

@coderabbitai

coderabbitai Bot commented May 24, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

The Windows local installer script enhances binary staging to handle locked executables. When the destination contains an existing cua-driver.exe, the script now renames it to a timestamped .stale-* filename, cleans up older stale binaries (older than 1 day), and then stages the new binary.

Changes

Guarded Binary Staging

Layer / File(s) Summary
Locked binary detection, rename, and stale cleanup
libs/cua-driver/scripts/install-local.ps1
Script detects a pre-existing DestBinary in the versioned directory, renames it to a timestamped .stale-* name with warning output on rename failure, removes older stale binaries (>1 day old), and then stages the newly built executable to the validated path.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • trycua/cua#1658: Both PRs modify the Windows local installer script to adjust destination binary handling during installation staging.

Poem

🐰 A rabbit once dreamed of an install so clean,
Where locked binaries wouldn't muck up the scene—
Rename to stale, sweep the old ones away,
Then stage the new binary, fresh as the day!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically describes the main change: renaming a locked binary before copying in the Windows installer script, addressing the core problem of file-in-use errors.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch install-local-stop-daemon-first

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@f-trycua
f-trycua merged commit 798fb30 into main May 24, 2026
5 of 7 checks passed
@f-trycua
f-trycua deleted the install-local-stop-daemon-first branch May 24, 2026 14:43
f-trycua added a commit that referenced this pull request May 24, 2026
…ary swap (install.ps1 + install-local.ps1) (#1689)

* fix(cua-driver-rs)(install): kill orphan cua-driver daemons after binary swap (install.ps1 + install-local.ps1)

User report after PR #1687 (rename-out-of-way): re-installing leaves
the previous in-memory daemon running. That daemon keeps serving
requests + drawing its overlay window from the OLD code until reboot.
Surfaces as "the bug I just fixed is still there" because the binary
on disk is new but the running process is pre-fix. Repro on the
reporter's machine had THREE orphan cua-driver.exe processes
accumulated from successive install-local.ps1 + autostart kick runs.

Two scripts get the same fix, shared via a duplicated function pair
(install.ps1 is fetched via `irm | iex` so it can't dot-source — the
duplication is intentional, marked with KEEP IN SYNC markers):

- Stop-CuaDriverDaemons: schtasks /End → taskkill /F /IM → returns
  any process that survived (those need elevation to kill).
- Show-CuaDriverDaemonSurvivors: prints a hint pointing the user at
  an elevated `taskkill` one-liner.

install.ps1 runs the pair right before the autostart-registration
block. install-local.ps1 runs it after the binary rename, replacing
the inline block that PR #1687 added (functionally identical, just
factored out for the share-the-logic story).

Knock-on: because `cua-driver update --apply` invokes install.ps1
(production curl-installer) under the hood, the update path inherits
this behaviour for free.

Verified end-to-end on Windows with 3 orphan daemons + 1 High-IL
daemon from the autostart task: 2 of 3 die cleanly (Medium-IL kill
works), 1 survives + the Note hint fires pointing the user at
`taskkill /IM cua-driver.exe /F` from an elevated shell.

* refactor(cua-driver-rs)(install): extract daemon-cleanup helpers to CuaDriverInstall.psm1 (true single source)

PR feedback on #1689: "use a psm to share" instead of the
KEEP-IN-SYNC duplication.

New module at libs/cua-driver/scripts/CuaDriverInstall.psm1 owns:
  - Stop-CuaDriverDaemons (schtasks /End -> taskkill /F /IM -> return
    survivors)
  - Show-CuaDriverDaemonSurvivors (warn + hint on Medium-IL kill that
    couldn't reach a High-IL daemon)
  - Import-CuaDriverInstallModule (helper for picking on-disk vs
    network load; used by install.ps1's bootstrap)

Two load paths:

  install-local.ps1 - checked-out tree, .psm1 is right there on disk
                      next to it. One liner: Import-Module
                      $ScriptDir/CuaDriverInstall.psm1 -Force.

  install.ps1       - fetched via `irm | iex`, no file on disk.
                      Inline bootstrap function tries on-disk first
                      ($PSScriptRoot, populated when run from a
                      checked-out tree / .ps1 file on disk), then
                      falls back to Invoke-RestMethod against the
                      same .psm1 on GitHub raw + Import-Module from
                      a temp file. Module stays in memory after the
                      temp file is removed.

End-to-end verified on Windows:

  ==> killing previous cua-driver processes (best-effort; High-IL needs admin)
  Note: 1 cua-driver process(es) still running after best-effort kill (pid: 14152).
        They are likely High-IL (spawned by RunLevel=Highest autostart task).
        From an elevated PowerShell:
          taskkill /IM cua-driver.exe /F
        Or just reboot. Until they exit, the OLD binary keeps running.

Two-on-three orphan daemons cleaned (Medium-IL); the High-IL
autostart-spawned survivor correctly reported. Same behaviour as the
duplicate-in-both-files version from PR #1689 but now there's a
single canonical definition.

* refactor(cua-driver-rs)(install): extract daemon-cleanup helpers to _install-common.sh (matches PowerShell .psm1 pattern)

PR #1689 just extracted the equivalent Windows logic into
CuaDriverInstall.psm1. Same single-source-of-truth treatment for the
sh installers — and rename the PowerShell module to _install-common.psm1
so the two halves share the underscore-prefixed `internal helper`
naming convention used by the other _install-*.{sh,ps1} files.

New shared file: libs/cua-driver/scripts/_install-common.sh
Functions: stop_cua_driver_daemons, show_cua_driver_daemon_survivors.
Loaded from on-disk first (when run from a checked-out tree) or from
GitHub raw via curl when install.sh runs as `curl ... | bash`.

Consumers updated to source it:
  - _install-rust.sh           (production Rust delegate; on-disk +
                                network fallback)
  - _install-local-rust.sh     (dev-build Rust installer; on-disk only)
  - _install-local-swift.sh    (dev-build Swift installer; same kill
                                covers both since both binaries bake
                                the `cua-driver` name)

macOS LaunchAgent plist names (verified from _install-local-*.sh):
  - Rust:  ~/Library/LaunchAgents/com.trycua.cua-driver-rs.plist
  - Swift: ~/Library/LaunchAgents/com.trycua.cua_driver_daemon.plist
Linux systemd unit (verified from _install-local-rust.sh):
  - cua-driver-rs.service

One pkill -x cua-driver covers both Rust and Swift binaries since both
exec under the same product name; no need for a per-backend kill
variant. cua-driver-uia is Windows-only so it never appears on Unix
hosts and isn't probed here.

bash -n passes on all touched files.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
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.

1 participant