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"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -17,6 +20,51 @@ type LockEntry struct {
|
|||||||
|
|
||||||
type LockFile map[string]*LockEntry
|
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) {
|
func generateUUID() (string, error) {
|
||||||
b := make([]byte, 16)
|
b := make([]byte, 16)
|
||||||
if _, err := rand.Read(b); err != nil {
|
if _, err := rand.Read(b); err != nil {
|
||||||
@@ -105,6 +153,7 @@ func acquireMgrLock(mgrPath string) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if lf2[mgrPath] != nil && lf2[mgrPath].Key == mgrKey {
|
if lf2[mgrPath] != nil && lf2[mgrPath].Key == mgrKey {
|
||||||
|
setHeldLock(mgrPath, mgrKey)
|
||||||
return mgrKey, nil
|
return mgrKey, nil
|
||||||
}
|
}
|
||||||
// Lost the race; go back to step 4 (keep same mgrKey, reset timer)
|
// Lost the race; go back to step 4 (keep same mgrKey, reset timer)
|
||||||
@@ -118,7 +167,11 @@ func releaseMgrLock(mgrPath string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
delete(lf, mgrPath)
|
delete(lf, mgrPath)
|
||||||
return writeLockFile(mgrPath, lf)
|
err = writeLockFile(mgrPath, lf)
|
||||||
|
if err == nil {
|
||||||
|
clearHeldLock()
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func cmdAcquire(mgrPath, filePath, key string) error {
|
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 {
|
if err := writeLockFile(mgrPath, lf); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
clearHeldLock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -213,7 +266,11 @@ func cmdRelease(mgrPath, filePath, key string) error {
|
|||||||
// Step 15: delete file lock and meta-lock, write
|
// Step 15: delete file lock and meta-lock, write
|
||||||
delete(lf, filePath)
|
delete(lf, filePath)
|
||||||
delete(lf, mgrPath)
|
delete(lf, mgrPath)
|
||||||
return writeLockFile(mgrPath, lf)
|
err = writeLockFile(mgrPath, lf)
|
||||||
|
if err == nil {
|
||||||
|
clearHeldLock()
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func cmdForceUnlock(mgrPath, filePath string) error {
|
func cmdForceUnlock(mgrPath, filePath string) error {
|
||||||
@@ -231,29 +288,31 @@ func cmdForceUnlock(mgrPath, filePath string) error {
|
|||||||
// Remove file lock and meta-lock unconditionally
|
// Remove file lock and meta-lock unconditionally
|
||||||
delete(lf, filePath)
|
delete(lf, filePath)
|
||||||
delete(lf, mgrPath)
|
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 := ""
|
action := ""
|
||||||
if len(os.Args) >= 2 {
|
if len(os.Args) >= 2 {
|
||||||
action = os.Args[1]
|
action = os.Args[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
// force-unlock only needs 2 args
|
|
||||||
if action == "force-unlock" && len(os.Args) < 3 {
|
if action == "force-unlock" && len(os.Args) < 3 {
|
||||||
fmt.Fprintln(os.Stderr, "Usage: lock-mgr force-unlock <file>")
|
return fmt.Errorf("usage: lock-mgr force-unlock <file>")
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
// acquire and release need 3 args
|
|
||||||
if (action == "acquire" || action == "release") && len(os.Args) < 4 {
|
if (action == "acquire" || action == "release") && len(os.Args) < 4 {
|
||||||
fmt.Fprintf(os.Stderr, "Usage: lock-mgr %s <file> <key>\n", action)
|
return fmt.Errorf("usage: lock-mgr %s <file> <key>", action)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
if action == "" || (action != "acquire" && action != "release" && action != "force-unlock") {
|
if action == "" || (action != "acquire" && action != "release" && action != "force-unlock") {
|
||||||
fmt.Fprintln(os.Stderr, "Usage: lock-mgr <acquire|release> <file> <key>")
|
return fmt.Errorf("usage: lock-mgr <acquire|release> <file> <key>\n lock-mgr force-unlock <file>")
|
||||||
fmt.Fprintln(os.Stderr, " lock-mgr force-unlock <file>")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fileArg := os.Args[2]
|
fileArg := os.Args[2]
|
||||||
@@ -265,8 +324,7 @@ func main() {
|
|||||||
// Step 1: resolve tool directory and locate (or create) the lock file
|
// Step 1: resolve tool directory and locate (or create) the lock file
|
||||||
execPath, err := os.Executable()
|
execPath, err := os.Executable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "error: cannot determine executable path: %v\n", err)
|
return fmt.Errorf("cannot determine executable path: %w", err)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toolDir := filepath.Dir(execPath)
|
toolDir := filepath.Dir(execPath)
|
||||||
@@ -275,34 +333,44 @@ func main() {
|
|||||||
// Step 2: get absolute paths
|
// Step 2: get absolute paths
|
||||||
mgrPath, err := filepath.Abs(rawMgrPath)
|
mgrPath, err := filepath.Abs(rawMgrPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "error: cannot resolve lock file path: %v\n", err)
|
return fmt.Errorf("cannot resolve lock file path: %w", err)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
filePath, err := filepath.Abs(fileArg)
|
filePath, err := filepath.Abs(fileArg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "error: cannot resolve file path: %v\n", err)
|
return fmt.Errorf("cannot resolve file path: %w", err)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure lock file exists
|
// Ensure lock file exists
|
||||||
if _, statErr := os.Stat(mgrPath); os.IsNotExist(statErr) {
|
if _, statErr := os.Stat(mgrPath); os.IsNotExist(statErr) {
|
||||||
if writeErr := os.WriteFile(mgrPath, []byte("{}"), 0644); writeErr != nil {
|
if writeErr := os.WriteFile(mgrPath, []byte("{}"), 0644); writeErr != nil {
|
||||||
fmt.Fprintf(os.Stderr, "error: cannot create lock file: %v\n", writeErr)
|
return fmt.Errorf("cannot create lock file: %w", writeErr)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch action {
|
switch action {
|
||||||
case "acquire":
|
case "acquire":
|
||||||
err = cmdAcquire(mgrPath, filePath, key)
|
return cmdAcquire(mgrPath, filePath, key)
|
||||||
case "release":
|
case "release":
|
||||||
err = cmdRelease(mgrPath, filePath, key)
|
return cmdRelease(mgrPath, filePath, key)
|
||||||
case "force-unlock":
|
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)
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user