Skip to content

cli: Windows curl upgrade cannot replace the running opencode.exe (locked by the TUI and background service) #52025

Closed as duplicate

Description

@angeloruggieridj

Description

On Windows, the V2 curl upgrade path must replace %USERPROFILE%\.opencode\bin\opencode.exe, but that file is the running executable and is write-locked by every live opencode process. Even with a correct MINGW/Git Bash (i.e. after the shell-selection fix in #51038), the install script's final mv cannot overwrite it, so the upgrade still fails unless the user stops every opencode process first.

While the TUI and a long-lived background service were running, a direct write-lock probe on the installed binary failed:

> [System.IO.File]::Open("C:\Users\<user>\.opencode\bin\opencode.exe",'Open','ReadWrite','None')
The process cannot access the file '...\.opencode\bin\opencode.exe' because it is being used by another process.

Three opencode.exe processes were running from that same path: the TUI, a second TUI, and a background service process started days earlier. Windows allows renaming a running executable but not overwriting or deleting it, so a coreutils mv that replaces the destination fails.

This is independent of the shell-choice bug: C:\Program Files\Git\bin\bash.exe runs the downloaded installer successfully (uname -s = MINGW64_NT-10.0-26200, uname -m = x86_64), so with #51038 the spawn would succeed and the replacement would still hit the lock.

Related: #51755 (V2 in-place upgrade blocked by the locked executable) and #37055 (V1, same class, scheduled-replace fix proposed in PR #37051). Filed separately to record the direct lock evidence and the fact that it persists after the shell-selection fix.

Workaround that worked: stop every opencode process (TUI plus background service), then run the installer from Git Bash. Because Windows permits renaming the running binary, the updater could adopt the same rename-then-swap-after-exit strategy to update in place.

Plugins

None found in global or project config.

OpenCode version

2.0.16, channel latest (attempting to upgrade to 2.0.19).

Steps to reproduce

  1. On Windows, install OpenCode V2 with the curl installer so the binary is %USERPROFILE%\.opencode\bin\opencode.exe.
  2. Start the TUI. A separate background service opencode.exe process also stays resident and runs from the same path.
  3. Attempt to replace the binary the way the install script does: mv tmp/package/bin/opencode.exe "$INSTALL_DIR/opencode.exe".
  4. The replacement fails; the running version is unchanged.

Screenshot and/or share link

No share link. The write-lock probe output is in Description.

Operating System

Windows 11 Pro build 26200 (Windows_NT 10.0.26200, x64).

Terminal

cmd.exe / PowerShell 7 (launched from C:\WINDOWS\system32\cmd.exe).

Activity

  1. added
    needs:complianceThis means the issue will auto-close after 2 hours.
    and removed
    needs:complianceThis means the issue will auto-close after 2 hours.
    on Sep 29, 2026
  2. github-actions commented on Sep 29, 2026

    @github-actions
    Contributor

    Thanks for updating the issue with the required details and reproduction steps!

  3. loisekk commented on Oct 11, 2026

    @loisekk

    Went looking for the mechanism behind this and think the premise needs re-testing, because mv may not hit the lock you expect.

    Your probe is accurate for a native Win32 caller:

    [System.IO.File]::Open('...opencode.exe','Open','ReadWrite','None')
    → being used by another process
    

    But the installer's final step isn't a Win32 open. install.sh:343 is mv "$tmp_dir/package/bin/$binary_name" "$INSTALL_DIR/$installed_binary", and under MINGW64 coreutils mv that succeeds against a running executable. Ran the real installer against a sandboxed HOME with the install-dir binary live:

    running install-dir binary: pid=1185 nlink=1
    hash before: e4224d18c3c96826
    $ HOME=$SANDBOX bash install.sh --version 2.0.19 --no-modify-path
    installer exit status : 0
    hash after            : 100c90202ec358bd
    VERDICT: BINARY WAS REPLACED
    

    Same-fs and cross-fs both replace, verified by hash change rather than exit code:

    [same-fs ] mv -> exit 0, hash CHANGED (e4224d18c3c96826 -> dafaac0690b4eb16)
    [cross-fs] mv -> exit 0, hash CHANGED (e4224d18c3c96826 -> dafaac0690b4eb16)
    

    The reason your probe and the installer disagree is sharing mode, not locking. Native Win32 opens don't request FILE_SHARE_DELETE, so the probe is refused; Cygwin/MSYS does request it, so mv is allowed. Both results are correct for their own caller — the probe measures .NET, the failure you predicted comes from mv, which isn't .NET.

    The claim I'd push back on is "Windows allows renaming a running executable but not overwriting or deleting it, so a coreutils mv that replaces the destination fails." Renaming a running exe does work, and mv is a rename — MSYS2 gets to rename because of FILE_SHARE_DELETE, so the lock you're reasoning about doesn't apply to it.

    Your rename-then-swap suggestion works, and it's the right shape for the path that genuinely is broken — but for mv it isn't needed.

    Where a real failure does happen is the cp path, install.sh:354, used by --binary and effectively by anyone following the PowerShell route in #51755:

    $ bash install.sh --binary /tmp/payload.exe
    cp: cannot create regular file '.../opencode.exe': Device or resource busy
    exit=1
    

    cp must open the destination for writing, which is refused even with FILE_SHARE_DELETE on the caller. That needs rename-aside, and it explains #51755's .old workaround exactly.

    Also relevant to the retained-image.ts question: the hard link it creates does allow unlink of a running image (I measured EPERM at nlink=1, OK at nlink=2), but the curl installer never unlinks the destination, so retaining() isn't what rescues that path.

    Two caveats on all of this. I used ping.exe as the stand-in running binary rather than opencode.exe itself — same Windows semantics, but it's not the same image. And it was a single process; I didn't try to reproduce your multi-process TUI-plus-service setup, so if the number of concurrent holders is what tips it over, my run wouldn't have caught that.

    If you still see it fail with a real Git Bash bash.exe, the installer's stderr would settle it — opencode upgrade currently discards it, and --print-logs --log-level DEBUG didn't surface it for me either. Surfacing installer stderr on failure looks like the more useful fix than the locking theory.

  4. opencodejr commented on Oct 28, 2026

    @opencodejr
    Contributor

    Thanks for the detailed report and the lock-probe output. I think this is a duplicate of #51755, which reports the same problem on v2: the Windows in-place upgrade can't replace the running %USERPROFILE%\.opencode\bin\opencode.exe. It also uses the same rename-aside workaround.

    Possible duplicates and related issues, most likely first:

    It would help to add your probe results and a note on whether the real installer mv failed under Git Bash (with its exact error) to #51755. The bot will close this issue as a duplicate in 1 day unless you reply explaining how your problem is different.

  5. opencodejr commented on Oct 29, 2026

    @opencodejr
    Contributor

    We haven't heard back in the last day, so I'm closing this as a duplicate of #51755. That issue tracks the same v2 problem: the Windows in-place upgrade can't replace the running %USERPROFILE%\.opencode\bin\opencode.exe.

    Your write-lock probe, and the note that the lock remains after the #51038 shell-selection fix, would be useful on #51755. There is a discussion there about whether the installer's mv actually fails under Git Bash. If your problem turns out to be different, reply here and we can reopen it. Thanks for the detailed report!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions