Files
agent-exec-transport 7b379d28e7
governance / governance (pull_request) Successful in 17s
ci / vet-and-test (pull_request) Successful in 1m6s
agent-exec transport d2c63b4a83821a5716997a28174233b1/d6e403e1e778
2026-10-18 09:20:03 +00:00

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
}