// Package instanceguard 强制同一 SQLite 数据文件只有一个活动服务。 // 锁在打开数据库(迁移发生在打开内部)之前取得。账户 CLI 不使用本包。 package instanceguard import ( "crypto/rand" "encoding/hex" "errors" "strconv" "strings" "agent-system-backend/internal/store" ) var ( ErrBusy = errors.New("instance guard: sqlite service already running") ErrHardlink = errors.New("instance guard: sqlite database has a hardlink alias") ErrDBReplaced = errors.New("instance guard: sqlite database replaced") ErrDBMissing = errors.New("instance guard: recorded sqlite database is missing") ErrLockRebuilt = errors.New("instance guard: lock file was rebuilt") ErrLockCorrupt = errors.New("instance guard: lock record is corrupt") ErrInstanceMismatch = errors.New("instance guard: instance id mismatch") ErrUnsupported = errors.New("instance guard: sqlite activity lock is not available on this platform") ErrLockAliased = errors.New("instance guard: lock file aliases the sqlite database") ) // Record 是锁文件里记下的数据库身份。没有记录表示锁文件是新建的。 type Record struct { DBDev uint64 DBIno uint64 InstanceID string LockGen string } // Lease 持有活动服务锁,直到 Release。关闭顺序由调用方保证:先关存储,再 Release。 type Lease struct { fd int dbPath string lockDev uint64 lockIno uint64 dbDev uint64 dbIno uint64 dbExists bool record *Record parseErr error release func() } // Release 放开锁并关闭锁文件描述符。锁文件留在磁盘上。 func (l *Lease) Release() { if l == nil || l.release == nil { return } l.release() l.release = nil } // DBPath 是解析后的数据库路径。符号链接指向真实文件。 func (l *Lease) DBPath() string { if l == nil { return "" } return l.dbPath } // AutomaticRefusal 在打开数据库之前调用。 // 尚无记录时返回 nil(首次建立,含旧库升级)。硬链接在 Acquire 阶段已经拒绝。 func (l *Lease) AutomaticRefusal() error { if l.parseErr != nil { return l.parseErr } if l.record == nil { return nil } if !l.dbExists { return ErrDBMissing } if l.record.DBDev != l.dbDev || l.record.DBIno != l.dbIno { return ErrDBReplaced } return nil } // BindOptions 控制守卫建立后的比对。 type BindOptions struct { Adopt bool InstanceID string } // Bind 在数据库已经打开并完成迁移之后调用。 // 锁文件 inode 与表不一致则拒绝,除非 Adopt。通过时重写锁文件内容与表。 func Bind(st *store.Store, lease *Lease, opt BindOptions) error { if lease == nil { return errors.New("instance guard: nil lease") } marker, ok, err := st.LoadInstanceGuard() if err != nil { return err } if ok && !opt.Adopt { inodeMismatch := marker.LockDev != lease.lockDev || marker.LockIno != lease.lockIno genMismatch := marker.LockGen != "" && (lease.record == nil || lease.record.LockGen != marker.LockGen) if inodeMismatch || genMismatch { return ErrLockRebuilt } } id := opt.InstanceID if ok && marker.InstanceID != "" && id != "" && marker.InstanceID != id && !opt.Adopt { return ErrInstanceMismatch } if id == "" && ok { id = marker.InstanceID } dev, ino, exists, err := lease.currentDB() if err != nil { return err } if !exists { return ErrDBMissing } lease.dbDev, lease.dbIno, lease.dbExists = dev, ino, true gen := "" if ok && marker.LockGen != "" && !opt.Adopt { gen = marker.LockGen } else { gen, err = newLockGen() if err != nil { return err } } next := store.InstanceGuardMarker{ DBDev: dev, DBIno: ino, LockDev: lease.lockDev, LockIno: lease.lockIno, InstanceID: id, LockGen: gen, } if err := lease.writeRecord(dev, ino, id, gen); err != nil { return err } if !ok || opt.Adopt || marker != next { if err := st.SaveInstanceGuard(next); err != nil { return err } } return nil } func newLockGen() (string, error) { buf := make([]byte, 16) if _, err := rand.Read(buf); err != nil { return "", err } return hex.EncodeToString(buf), nil } func parseRecord(raw string) (*Record, error) { raw = strings.TrimSpace(raw) if raw == "" { return nil, nil } parts := strings.Split(raw, "\t") if parts[0] != "v1" { return nil, ErrLockCorrupt } rec := &Record{} seen := map[string]bool{} for _, part := range parts[1:] { key, val, ok := strings.Cut(part, "=") if !ok || seen[key] || strings.ContainsAny(val, " \t") { return nil, ErrLockCorrupt } seen[key] = true switch key { case "db_dev": n, err := strconv.ParseUint(val, 10, 64) if err != nil { return nil, ErrLockCorrupt } rec.DBDev = n case "db_ino": n, err := strconv.ParseUint(val, 10, 64) if err != nil { return nil, ErrLockCorrupt } rec.DBIno = n case "instance_id": rec.InstanceID = val case "lock_gen": if val == "" { return nil, ErrLockCorrupt } rec.LockGen = val default: return nil, ErrLockCorrupt } } if !seen["db_dev"] || !seen["db_ino"] || !seen["instance_id"] { return nil, ErrLockCorrupt } return rec, nil }