larksuite/cli · error · ErrHeld

%w (lock: %s, syscall: %v)

Error message

%w (lock: %s, syscall: %v)

What it means

On Windows, tryLockFile calls LockFileEx with LK_LOCK_EXCLUSIVE|LK_FAIL_IMMEDIATELY semantics; if the syscall returns 0 the region is already locked (or the syscall failed). The error wraps the sentinel ErrHeld so callers can errors.Is it as retryable contention, and it embeds the lock file name and the underlying syscall error for diagnosis.

Source

Thrown at internal/lockfile/lock_windows.go:37

)

const (
	lockfileExclusiveLock   = 0x00000002
	lockfileFailImmediately = 0x00000001
)

func tryLockFile(f *os.File) error {
	var ol syscall.Overlapped
	handle := syscall.Handle(f.Fd())
	r1, _, err := procLockFileEx.Call(
		uintptr(handle),
		uintptr(lockfileExclusiveLock|lockfileFailImmediately),
		0,
		1, 0,
		uintptr(unsafe.Pointer(&ol)),
	)
	if r1 == 0 {
		return fmt.Errorf("%w (lock: %s, syscall: %v)", ErrHeld, f.Name(), err)
	}
	return nil
}

func unlockFile(f *os.File) error {
	var ol syscall.Overlapped
	handle := syscall.Handle(f.Fd())
	r1, _, err := procUnlockFile.Call(
		uintptr(handle),
		0,
		1, 0,
		uintptr(unsafe.Pointer(&ol)),
	)
	if r1 == 0 {
		return err
	}
	return nil
}

View on GitHub (pinned to 7fd6ef3c07)

Solutions

  1. Check errors.Is(err, lockfile.ErrHeld) and treat it as contention: inform the user another subscribe is active or retry after it exits.
  2. Read the embedded syscall error (%v) — if it is not ERROR_LOCK_VIOLATION, investigate handle/filesystem issues instead of retrying.
  3. Ensure the competing process exits cleanly so the OS auto-releases the lock (locks auto-release on process death).
  4. Run the command with a different app ID/config dir if you intentionally need parallel sessions.

Example fix

// before
if err := lf.TryLock(); err != nil {
    return err
}
// after
if err := lf.TryLock(); err != nil {
    if errors.Is(err, lockfile.ErrHeld) {
        return fmt.Errorf("another subscribe is running for this app; try again later")
    }
    return err
}
Defensive patterns

Strategy: try-catch

Try / catch

lf, err := lockfile.ForSubscribe(appID)
if err != nil { return err }
if err := lf.TryLock(); err != nil {
    if errors.Is(err, lockfile.ErrHeld) {
        return fmt.Errorf("another subscribe process holds the lock (%s)", lf.Path())
    }
    return err
}
defer lf.Unlock()

Prevention

When it happens

Trigger: Calling LockFile.TryLock() on Windows when another process (or a stale handle) already holds the OS-level byte-range lock on the lock file, or when LockFileEx itself fails (e.g. invalid handle, low resources).

Common situations: Two CLI event-subscribe sessions running concurrently for the same app ID on the same machine; a previous subscribe process that crashed without releasing while its handle lingers; antivirus or backup tools briefly holding the file.

Related errors


AI-assisted analysis of larksuite/cli@7fd6ef3c07 (2026-09-04). Data as JSON: /api/errors/4cec4cd43bc7c801. Report an issue: GitHub.