202 lines
4.8 KiB
Go
202 lines
4.8 KiB
Go
//go:build unix
|
|
|
|
package instanceguard
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"syscall"
|
|
)
|
|
|
|
// Acquire 取得 SQLite 活动服务锁。失败时不返回 Lease。
|
|
// 硬链接别名直接拒绝。锁文件被并发替换时重试数次后拒绝。
|
|
func Acquire(dataDir string) (*Lease, error) {
|
|
dir, err := normalizeDataDir(dataDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
return nil, err
|
|
}
|
|
_, dbPath, err := resolveDB(dir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rejectHardlink(dbPath); err != nil {
|
|
return nil, err
|
|
}
|
|
lockPath := dbPath + ".lock"
|
|
var last error
|
|
for attempt := 0; attempt < 5; attempt++ {
|
|
lease, retry, err := acquireOnce(dbPath, lockPath)
|
|
if err == nil && !retry {
|
|
return lease, nil
|
|
}
|
|
if retry {
|
|
last = err
|
|
continue
|
|
}
|
|
return nil, err
|
|
}
|
|
if last == nil {
|
|
last = errorsRetry()
|
|
}
|
|
return nil, last
|
|
}
|
|
|
|
func errorsRetry() error {
|
|
return fmt.Errorf("instance guard: lock file inode changed while locking")
|
|
}
|
|
|
|
func acquireOnce(dbPath, lockPath string) (*Lease, bool, error) {
|
|
fd, err := syscall.Open(lockPath, syscall.O_RDWR|syscall.O_CREAT|syscall.O_CLOEXEC|syscall.O_NOFOLLOW, 0o600)
|
|
if err != nil {
|
|
if err == syscall.ELOOP {
|
|
return nil, false, ErrLockAliased
|
|
}
|
|
return nil, false, err
|
|
}
|
|
fail := func(err error) (*Lease, bool, error) {
|
|
syscall.Close(fd)
|
|
return nil, false, err
|
|
}
|
|
if err := syscall.Flock(fd, syscall.LOCK_EX|syscall.LOCK_NB); err != nil {
|
|
syscall.Close(fd)
|
|
if err == syscall.EWOULDBLOCK || err == syscall.EAGAIN {
|
|
return nil, false, ErrBusy
|
|
}
|
|
return nil, false, err
|
|
}
|
|
var held syscall.Stat_t
|
|
if err := syscall.Fstat(fd, &held); err != nil {
|
|
return fail(err)
|
|
}
|
|
var named syscall.Stat_t
|
|
if err := syscall.Stat(lockPath, &named); err != nil {
|
|
return fail(err)
|
|
}
|
|
if held.Ino != named.Ino || held.Dev != named.Dev {
|
|
syscall.Flock(fd, syscall.LOCK_UN)
|
|
syscall.Close(fd)
|
|
return nil, true, errorsRetry()
|
|
}
|
|
buf := make([]byte, 512)
|
|
n, err := syscall.Read(fd, buf)
|
|
if err != nil {
|
|
return fail(err)
|
|
}
|
|
rec, perr := parseRecord(string(buf[:n]))
|
|
dbDev, dbIno, dbExists, err := statFile(dbPath)
|
|
if err != nil {
|
|
return fail(err)
|
|
}
|
|
if dbExists && dbDev == uint64(held.Dev) && dbIno == uint64(held.Ino) {
|
|
syscall.Flock(fd, syscall.LOCK_UN)
|
|
syscall.Close(fd)
|
|
return nil, false, ErrLockAliased
|
|
}
|
|
lease := &Lease{
|
|
fd: fd,
|
|
dbPath: dbPath,
|
|
lockDev: uint64(held.Dev),
|
|
lockIno: uint64(held.Ino),
|
|
dbDev: dbDev,
|
|
dbIno: dbIno,
|
|
dbExists: dbExists,
|
|
record: rec,
|
|
parseErr: perr,
|
|
release: func() {
|
|
syscall.Flock(fd, syscall.LOCK_UN)
|
|
syscall.Close(fd)
|
|
},
|
|
}
|
|
return lease, false, nil
|
|
}
|
|
|
|
func (l *Lease) writeRecord(dev, ino uint64, instanceID, lockGen string) error {
|
|
if strings.ContainsAny(instanceID, " \t\r\n") {
|
|
return fmt.Errorf("instance guard: instance id contains whitespace")
|
|
}
|
|
line := fmt.Sprintf("v1\tdb_dev=%d\tdb_ino=%d\tinstance_id=%s\tlock_gen=%s\n", dev, ino, instanceID, lockGen)
|
|
if _, err := syscall.Seek(l.fd, 0, 0); err != nil {
|
|
return err
|
|
}
|
|
if _, err := syscall.Write(l.fd, []byte(line)); err != nil {
|
|
return err
|
|
}
|
|
return syscall.Ftruncate(l.fd, int64(len(line)))
|
|
}
|
|
|
|
func (l *Lease) currentDB() (dev, ino uint64, exists bool, err error) {
|
|
return statFile(l.dbPath)
|
|
}
|
|
|
|
func normalizeDataDir(dataDir string) (string, error) {
|
|
if strings.TrimSpace(dataDir) == "" {
|
|
dataDir = "."
|
|
}
|
|
abs, err := filepath.Abs(dataDir)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if _, err := os.Lstat(abs); err == nil {
|
|
return filepath.EvalSymlinks(abs)
|
|
} else if !os.IsNotExist(err) {
|
|
return "", err
|
|
}
|
|
parent, base := filepath.Split(abs)
|
|
parent = filepath.Clean(parent)
|
|
if _, err := os.Lstat(parent); err == nil {
|
|
resolved, err := filepath.EvalSymlinks(parent)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Join(resolved, base), nil
|
|
}
|
|
return abs, nil
|
|
}
|
|
|
|
func resolveDB(dataDir string) (logical, resolved string, err error) {
|
|
logical = filepath.Join(dataDir, "agent-system.db")
|
|
fi, err := os.Lstat(logical)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return logical, logical, nil
|
|
}
|
|
return "", "", err
|
|
}
|
|
if fi.Mode()&os.ModeSymlink != 0 {
|
|
resolved, err = filepath.EvalSymlinks(logical)
|
|
return logical, resolved, err
|
|
}
|
|
return logical, logical, nil
|
|
}
|
|
|
|
func rejectHardlink(dbPath string) error {
|
|
_, _, exists, err := statFile(dbPath)
|
|
if err != nil || !exists {
|
|
return err
|
|
}
|
|
var st syscall.Stat_t
|
|
if err := syscall.Stat(dbPath, &st); err != nil {
|
|
return err
|
|
}
|
|
if st.Nlink > 1 {
|
|
return ErrHardlink
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func statFile(path string) (dev, ino uint64, exists bool, err error) {
|
|
var st syscall.Stat_t
|
|
if err = syscall.Stat(path, &st); err != nil {
|
|
if os.IsNotExist(err) {
|
|
return 0, 0, false, nil
|
|
}
|
|
return 0, 0, false, err
|
|
}
|
|
return uint64(st.Dev), uint64(st.Ino), true, nil
|
|
}
|