fix: lock-mgr release meta-lock before exiting

This commit is contained in:
h z
2026-04-13 15:46:49 +01:00
parent dcc91ead9b
commit 1a202986e8

View File

@@ -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 <file>")
os.Exit(1)
return fmt.Errorf("usage: lock-mgr force-unlock <file>")
}
// acquire and release need 3 args
if (action == "acquire" || action == "release") && len(os.Args) < 4 {
fmt.Fprintf(os.Stderr, "Usage: lock-mgr %s <file> <key>\n", action)
os.Exit(1)
return fmt.Errorf("usage: lock-mgr %s <file> <key>", action)
}
if action == "" || (action != "acquire" && action != "release" && action != "force-unlock") {
fmt.Fprintln(os.Stderr, "Usage: lock-mgr <acquire|release> <file> <key>")
fmt.Fprintln(os.Stderr, " lock-mgr force-unlock <file>")
os.Exit(1)
return fmt.Errorf("usage: lock-mgr <acquire|release> <file> <key>\n lock-mgr force-unlock <file>")
}
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)
}