package main import ( "crypto/rand" "encoding/json" "fmt" "os" "path/filepath" "time" ) type LockEntry struct { Locked bool `json:"locked"` Key string `json:"key,omitempty"` Time string `json:"time,omitempty"` } type LockFile map[string]*LockEntry func generateUUID() (string, error) { b := make([]byte, 16) if _, err := rand.Read(b); err != nil { return "", err } b[6] = (b[6] & 0x0f) | 0x40 // version 4 b[8] = (b[8] & 0x3f) | 0x80 // variant bits return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]), nil } func readLockFile(path string) (LockFile, error) { data, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { return make(LockFile), nil } return nil, fmt.Errorf("read lock file: %w", err) } if len(data) == 0 { return make(LockFile), nil } var lf LockFile if err := json.Unmarshal(data, &lf); err != nil { return make(LockFile), nil } if lf == nil { return make(LockFile), nil } return lf, nil } func writeLockFile(path string, lf LockFile) error { data, err := json.MarshalIndent(lf, "", " ") if err != nil { return fmt.Errorf("marshal lock file: %w", err) } return os.WriteFile(path, data, 0644) } // acquireMgrLock implements steps 3-8: acquire the meta-lock on the JSON file itself. // Returns the mgr-key on success. func acquireMgrLock(mgrPath string) (string, error) { // Step 3: generate mgr-key once mgrKey, err := generateUUID() if err != nil { return "", err } for { // Step 4: record current time startTime := time.Now() // Steps 5-6: spin until the meta-lock is free or owned by us for { lf, err := readLockFile(mgrPath) if err != nil { return "", err } entry := lf[mgrPath] if entry == nil || !entry.Locked || entry.Key == mgrKey { break } if time.Since(startTime) > 5*time.Second { return "", fmt.Errorf("timeout waiting to acquire manager lock") } time.Sleep(100 * time.Millisecond) } // Step 7: write locked=true, key=mgrKey lf, err := readLockFile(mgrPath) if err != nil { return "", err } if lf[mgrPath] == nil { lf[mgrPath] = &LockEntry{} } lf[mgrPath].Locked = true lf[mgrPath].Key = mgrKey if err := writeLockFile(mgrPath, lf); err != nil { return "", err } // Step 8: verify we actually won the race lf2, err := readLockFile(mgrPath) if err != nil { return "", err } if lf2[mgrPath] != nil && lf2[mgrPath].Key == mgrKey { return mgrKey, nil } // Lost the race; go back to step 4 (keep same mgrKey, reset timer) } } // releaseMgrLock removes the meta-lock entry and writes the file. func releaseMgrLock(mgrPath string) error { lf, err := readLockFile(mgrPath) if err != nil { return err } delete(lf, mgrPath) return writeLockFile(mgrPath, lf) } func cmdAcquire(mgrPath, filePath string) error { // Steps 3-8: acquire meta-lock if _, err := acquireMgrLock(mgrPath); err != nil { return err } // Step 9: record start time for file-lock wait timeout startTime := time.Now() for { // Step 10: read current file lock state lf, err := readLockFile(mgrPath) if err != nil { _ = releaseMgrLock(mgrPath) return err } entry := lf[filePath] xLocked := entry != nil && entry.Locked xKey := "" if entry != nil { xKey = entry.Key } envKey := os.Getenv("LOCK_MGR_KEY") // Step 11: if locked by someone else, wait and retry if xLocked && xKey != envKey { if time.Since(startTime) > 30*time.Second { _ = releaseMgrLock(mgrPath) return fmt.Errorf("timeout waiting to acquire lock on %s", filePath) } // Release meta-lock while sleeping so others can proceed if err := releaseMgrLock(mgrPath); err != nil { return err } time.Sleep(1 * time.Second) // Re-acquire meta-lock before next read if _, err := acquireMgrLock(mgrPath); err != nil { return err } continue } // Step 12: not locked (or already owned by us) — acquire var newKey string if envKey != "" { newKey = envKey } else { newKey, err = generateUUID() if err != nil { _ = releaseMgrLock(mgrPath) return err } } if lf[filePath] == nil { lf[filePath] = &LockEntry{} } lf[filePath].Locked = true lf[filePath].Key = newKey lf[filePath].Time = time.Now().UTC().Format(time.RFC3339) // Step 13: delete meta-lock entry and write atomically delete(lf, mgrPath) if err := writeLockFile(mgrPath, lf); err != nil { return err } fmt.Println(newKey) return nil } } func cmdRelease(mgrPath, filePath string) error { // Steps 3-8: acquire meta-lock if _, err := acquireMgrLock(mgrPath); err != nil { return err } lf, err := readLockFile(mgrPath) if err != nil { _ = releaseMgrLock(mgrPath) return err } entry := lf[filePath] xLocked := entry != nil && entry.Locked xKey := "" if entry != nil { xKey = entry.Key } envKey := os.Getenv("LOCK_MGR_KEY") // Step 14: validate preconditions if envKey == "" { _ = releaseMgrLock(mgrPath) return fmt.Errorf("LOCK_MGR_KEY environment variable not set") } if !xLocked { _ = releaseMgrLock(mgrPath) return fmt.Errorf("file %s is not locked", filePath) } if xKey != envKey { _ = releaseMgrLock(mgrPath) return fmt.Errorf("LOCK_MGR_KEY does not match the lock key for %s", filePath) } // Step 15: delete file lock and meta-lock, write delete(lf, filePath) delete(lf, mgrPath) return writeLockFile(mgrPath, lf) } func cmdForceUnlock(mgrPath, filePath string) error { // Steps 3-8: acquire meta-lock if _, err := acquireMgrLock(mgrPath); err != nil { return err } lf, err := readLockFile(mgrPath) if err != nil { _ = releaseMgrLock(mgrPath) return err } // Remove file lock and meta-lock unconditionally delete(lf, filePath) delete(lf, mgrPath) return writeLockFile(mgrPath, lf) } func main() { if len(os.Args) < 3 { fmt.Fprintln(os.Stderr, "Usage: lock-mgr ") os.Exit(1) } action := os.Args[1] fileArg := os.Args[2] // Step 1: resolve tool directory and locate (or create) the lock file execPath, err := os.Executable() if err != nil { fmt.Fprintf(os.Stderr, "error: cannot determine executable path: %v\n", err) os.Exit(1) } toolDir := filepath.Dir(execPath) rawMgrPath := filepath.Join(toolDir, "..", ".lock-mgr.json") // Step 2: get absolute paths mgrPath, err := filepath.Abs(rawMgrPath) if err != nil { fmt.Fprintf(os.Stderr, "error: cannot resolve lock file path: %v\n", err) os.Exit(1) } filePath, err := filepath.Abs(fileArg) if err != nil { fmt.Fprintf(os.Stderr, "error: cannot resolve file path: %v\n", err) os.Exit(1) } // Ensure lock file exists if _, statErr := os.Stat(mgrPath); os.IsNotExist(statErr) { if writeErr := os.WriteFile(mgrPath, []byte("{}"), 0644); writeErr != nil { fmt.Fprintf(os.Stderr, "error: cannot create lock file: %v\n", writeErr) os.Exit(1) } } switch action { case "acquire": err = cmdAcquire(mgrPath, filePath) case "release": err = cmdRelease(mgrPath, filePath) case "force-unlock": err = cmdForceUnlock(mgrPath, filePath) default: fmt.Fprintf(os.Stderr, "error: unknown action %q\n", action) fmt.Fprintln(os.Stderr, "Usage: lock-mgr ") os.Exit(1) } if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } }