From 1a202986e89a000605e0678fa3c45942532a3d9f Mon Sep 17 00:00:00 2001 From: hzhang Date: Mon, 13 Apr 2026 15:46:49 +0100 Subject: [PATCH] fix: lock-mgr release meta-lock before exiting --- lock-mgr/main.go | 120 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 94 insertions(+), 26 deletions(-) diff --git a/lock-mgr/main.go b/lock-mgr/main.go index e4fa97b..018abe8 100644 --- a/lock-mgr/main.go +++ b/lock-mgr/main.go @@ -5,7 +5,10 @@ import ( "encoding/json" "fmt" "os" + "os/signal" "path/filepath" + "sync" + "syscall" "time" ) @@ -17,6 +20,51 @@ type LockEntry struct { type LockFile map[string]*LockEntry +// heldLock tracks the meta-lock currently held by this process so it can be +// released on panic, signal, or any other unexpected exit. +var ( + heldMu sync.Mutex + heldMgrPath string + heldMgrKey string +) + +func setHeldLock(path, key string) { + heldMu.Lock() + heldMgrPath = path + heldMgrKey = key + heldMu.Unlock() +} + +func clearHeldLock() { + heldMu.Lock() + heldMgrPath = "" + heldMgrKey = "" + heldMu.Unlock() +} + +// cleanupMgrLock releases the meta-lock if this process still holds it. +// Safe to call multiple times. +func cleanupMgrLock() { + heldMu.Lock() + path := heldMgrPath + key := heldMgrKey + heldMu.Unlock() + + if path == "" || key == "" { + return + } + lf, err := readLockFile(path) + if err != nil { + return + } + entry := lf[path] + if entry != nil && entry.Key == key { + delete(lf, path) + _ = writeLockFile(path, lf) + } + clearHeldLock() +} + func generateUUID() (string, error) { b := make([]byte, 16) if _, err := rand.Read(b); err != nil { @@ -105,6 +153,7 @@ func acquireMgrLock(mgrPath string) (string, error) { return "", err } if lf2[mgrPath] != nil && lf2[mgrPath].Key == mgrKey { + setHeldLock(mgrPath, mgrKey) return mgrKey, nil } // Lost the race; go back to step 4 (keep same mgrKey, reset timer) @@ -118,7 +167,11 @@ func releaseMgrLock(mgrPath string) error { return err } delete(lf, mgrPath) - return writeLockFile(mgrPath, lf) + err = writeLockFile(mgrPath, lf) + if err == nil { + clearHeldLock() + } + return err } func cmdAcquire(mgrPath, filePath, key string) error { @@ -176,7 +229,7 @@ func cmdAcquire(mgrPath, filePath, key string) error { if err := writeLockFile(mgrPath, lf); err != nil { return err } - + clearHeldLock() return nil } } @@ -213,7 +266,11 @@ func cmdRelease(mgrPath, filePath, key string) error { // Step 15: delete file lock and meta-lock, write delete(lf, filePath) delete(lf, mgrPath) - return writeLockFile(mgrPath, lf) + err = writeLockFile(mgrPath, lf) + if err == nil { + clearHeldLock() + } + return err } func cmdForceUnlock(mgrPath, filePath string) error { @@ -231,29 +288,31 @@ func cmdForceUnlock(mgrPath, filePath string) error { // Remove file lock and meta-lock unconditionally delete(lf, filePath) delete(lf, mgrPath) - return writeLockFile(mgrPath, lf) + err = writeLockFile(mgrPath, lf) + if err == nil { + clearHeldLock() + } + return err } -func main() { +func run() error { + // cleanupMgrLock runs on normal return AND on panic (defer unwinds through panics). + // os.Exit bypasses defer, so we keep os.Exit only in main() after run() returns. + defer cleanupMgrLock() + action := "" if len(os.Args) >= 2 { action = os.Args[1] } - // force-unlock only needs 2 args if action == "force-unlock" && len(os.Args) < 3 { - fmt.Fprintln(os.Stderr, "Usage: lock-mgr force-unlock ") - os.Exit(1) + return fmt.Errorf("usage: lock-mgr force-unlock ") } - // acquire and release need 3 args if (action == "acquire" || action == "release") && len(os.Args) < 4 { - fmt.Fprintf(os.Stderr, "Usage: lock-mgr %s \n", action) - os.Exit(1) + return fmt.Errorf("usage: lock-mgr %s ", action) } if action == "" || (action != "acquire" && action != "release" && action != "force-unlock") { - fmt.Fprintln(os.Stderr, "Usage: lock-mgr ") - fmt.Fprintln(os.Stderr, " lock-mgr force-unlock ") - os.Exit(1) + return fmt.Errorf("usage: lock-mgr \n lock-mgr force-unlock ") } fileArg := os.Args[2] @@ -265,8 +324,7 @@ func main() { // 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) + return fmt.Errorf("cannot determine executable path: %w", err) } toolDir := filepath.Dir(execPath) @@ -275,34 +333,44 @@ func main() { // 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) + return fmt.Errorf("cannot resolve lock file path: %w", err) } filePath, err := filepath.Abs(fileArg) if err != nil { - fmt.Fprintf(os.Stderr, "error: cannot resolve file path: %v\n", err) - os.Exit(1) + return fmt.Errorf("cannot resolve file path: %w", err) } // 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) + return fmt.Errorf("cannot create lock file: %w", writeErr) } } switch action { case "acquire": - err = cmdAcquire(mgrPath, filePath, key) + return cmdAcquire(mgrPath, filePath, key) case "release": - err = cmdRelease(mgrPath, filePath, key) + return cmdRelease(mgrPath, filePath, key) case "force-unlock": - err = cmdForceUnlock(mgrPath, filePath) + return cmdForceUnlock(mgrPath, filePath) } + return nil +} - if err != nil { +func main() { + // Signal handler: release meta-lock on SIGINT / SIGTERM before exiting. + // This covers Ctrl+C and process termination while the lock is held. + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + go func() { + <-sigCh + cleanupMgrLock() + os.Exit(130) + }() + + if err := run(); err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) }