fix(cua-driver-rs)(install): rename-out-of-way locked binary before Copy-Item in install-local.ps1 - #1687
Conversation
…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.
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
|
Caution Review failedPull request was closed or merged during review 📝 WalkthroughWalkthroughThe Windows local installer script enhances binary staging to handle locked executables. When the destination contains an existing ChangesGuarded Binary Staging
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
…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>
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
🤖 Generated with Claude Code
Summary by CodeRabbit