Skip to content

Windows: daemon start always fails — update.lock owner is re-read through a second handle while byte-range locked (ERROR_LOCK_VIOLATION) #717

Closed

Description

@Matheusalexandre-01

CompozyOS version

compozy 0.3.0-beta.31 (direct binary). I didn't run beta.32, but the code path below is unchanged on v0.3.0-beta.32 and main @ 88534944a7.

Operating system

Windows (native) — Windows 11 (build 26200)

Area

Core / daemon · CLI · Install / update

ACP agent

Not agent-specific

Model / provider

—

Steps to reproduce

  1. Install the Windows binary (direct-binary install) and run compozy install.
  2. From a normal, non-elevated terminal, run compozy daemon start.

What happened, and what did you expect?

daemon start fails before the daemon is launched:

error: cli: acquire daemon start mutation lock: daemon: read update lock "C:\Users\<user>\.compozy\update.lock": read C:\Users\<user>\.compozy\update.lock: The process cannot access the file because another process has locked a portion of the file.

(The original message was localized: "O processo não pode acessar o arquivo porque outro processo bloqueou parte do arquivo", which is ERROR_LOCK_VIOLATION.)

No update is running: compozy update --cancel reports nothing active. The only way we found to start the daemon was compozy daemon start --foreground --internal-child, which skips the mutation lock.

Expected: daemon start acquires the lock and starts the daemon.

Cause. AcquireUpdateLock (internal/daemon/update_lock.go) locks update.lock and then calls readUpdateLockOwner(cleanPath) and writeUpdateLockOwner(cleanPath, …), which open the same file again through a second handle. On Windows the lock is a mandatory byte-range lock (LockFileEx), and it blocks other handles even in the same process, so that second open fails with ERROR_LOCK_VIOLATION. On Linux and macOS flock is advisory, so the same code works there.

Patch we are running locally. It keeps the owner metadata in a sidecar file on Windows. Reading and writing through the already-locked handle would work too, if you'd rather keep a single file:

--- a/internal/daemon/update_lock.go
+++ b/internal/daemon/update_lock.go
@@ -6,6 +6,7 @@ import (
 	"fmt"
 	"os"
 	"path/filepath"
+	"runtime"
 	"strings"
 	"time"
 
@@ -53,7 +54,13 @@ func AcquireUpdateLock(path string, owner UpdateLockOwner) (*UpdateLock, error)
 		}
 		return nil, ErrRuntimeMutationInProgress
 	}
-	priorOwner, err := readUpdateLockOwner(cleanPath)
+	ownerPath := cleanPath
+	if runtime.GOOS == "windows" {
+		// Windows byte-range locks also block a second handle in this process.
+		// Keep ownership metadata beside the locked file instead of reopening it.
+		ownerPath += ".owner"
+	}
+	priorOwner, err := readUpdateLockOwner(ownerPath)
 	if err != nil {
 		unlockErr := fileLock.Unlock()
 		closeErr := fileLock.Close()
@@ -63,12 +70,12 @@ func AcquireUpdateLock(path string, owner UpdateLockOwner) (*UpdateLock, error)
 	if priorOwner != nil && !procutil.MatchesStartTime(priorOwner.PID, priorOwner.StartedAt) {
 		staleOwner = priorOwner
 	}
-	if err := writeUpdateLockOwner(cleanPath, owner); err != nil {
+	if err := writeUpdateLockOwner(ownerPath, owner); err != nil {
 		unlockErr := fileLock.Unlock()
 		closeErr := fileLock.Close()
 		return nil, errors.Join(err, unlockErr, closeErr)
 	}
-	return &UpdateLock{flock: fileLock, path: cleanPath, staleOwner: staleOwner}, nil
+	return &UpdateLock{flock: fileLock, path: ownerPath, staleOwner: staleOwner}, nil
 }

With this patch, compozy daemon start works normally on Windows.

Logs

error: cli: acquire daemon start mutation lock: daemon: read update lock "C:\Users\<user>\.compozy\update.lock": read C:\Users\<user>\.compozy\update.lock: The process cannot access the file because another process has locked a portion of the file.

Final checks

  • I searched existing issues and this is not a duplicate.
  • I can reproduce this on the latest release. (Reproduced on beta.31; on beta.32 and main I only confirmed by reading the source.)

Diagnosed with Claude Code on my machine. The diff is the patch I'm running locally, offered as a starting point.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions