fix: lock-mgr release meta-lock before exiting
This commit is contained in:
120
lock-mgr/main.go
120
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 <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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user