返回 DeepSeek-Reasonix
session_lock_unix.go
根目录 / internal / agent / session_lock_unix.go
1 //go:build !windows
2
3 package agent
4
5 import (
6 "errors"
7 "os"
8
9 "reasonix/internal/store"
10
11 "golang.org/x/sys/unix"
12 )
13
14 // tryLockSessionFile attempts the compatibility save lock once without
15 // blocking. The shared wrapper in save.go supplies the bounded retry window.
16 func tryLockSessionFile(path string) (func(), error) {
17 f, err := os.OpenFile(store.SessionLockFile(path), os.O_CREATE|os.O_RDWR, 0o600)
18 if err != nil {
19 return nil, err
20 }
21 if err := unix.Flock(int(f.Fd()), unix.LOCK_EX|unix.LOCK_NB); err != nil {
22 _ = f.Close()
23 if errors.Is(err, unix.EWOULDBLOCK) || errors.Is(err, unix.EAGAIN) {
24 return nil, ErrSessionFileLockHeld
25 }
26 return nil, err
27 }
28 return func() {
29 _ = unix.Flock(int(f.Fd()), unix.LOCK_UN)
30 _ = f.Close()
31 }, nil
32 }
33
34 // sessionLockFile is a non-blocking exclusive lock on a lock file itself,
35 // used by cleanup paths that may need to delete the file they locked.
36 type sessionLockFile struct {
37 f *os.File
38 }
39
40 // tryTakeSessionLockFile opens lockPath and takes its exclusive flock without
41 // blocking. A live holder surfaces as ErrSessionFileLockHeld.
42 func tryTakeSessionLockFile(lockPath string) (*sessionLockFile, error) {
43 f, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0o600)
44 if err != nil {
45 return nil, err
46 }
47 if err := unix.Flock(int(f.Fd()), unix.LOCK_EX|unix.LOCK_NB); err != nil {
48 _ = f.Close()
49 if errors.Is(err, unix.EWOULDBLOCK) || errors.Is(err, unix.EAGAIN) {
50 return nil, ErrSessionFileLockHeld
51 }
52 return nil, err
53 }
54 return &sessionLockFile{f: f}, nil
55 }
56
57 func tryTakeSessionLeaseLockFile(lockPath string) (*sessionLockFile, error) {
58 return tryTakeSessionLockFile(lockPath)
59 }
60
61 func (l *sessionLockFile) Unlock() {
62 _ = unix.Flock(int(l.f.Fd()), unix.LOCK_UN)
63 _ = l.f.Close()
64 }
65
66 // writeOwnerInfo replaces the lock file's contents with b through the held
67 // descriptor while the flock is still held. The lease publishes its owner
68 // identity directly inside .lease.lock, so the metadata dies with the lock
69 // file on RemoveAndUnlock instead of surviving as a stale sidecar.
70 func (l *sessionLockFile) writeOwnerInfo(b []byte) error {
71 if l == nil || l.f == nil {
72 return errors.New("lease lock not held")
73 }
74 if err := l.f.Truncate(0); err != nil {
75 return err
76 }
77 if _, err := l.f.WriteAt(sessionLeaseOwnerBytes(b), 0); err != nil {
78 return err
79 }
80 return l.f.Sync()
81 }
82
83 // RemoveAndUnlock deletes the lock file atomically with the release: the
84 // unlink happens while the flock is still held, so a waiter blocked on this
85 // inode can never adopt a file that is about to disappear for everyone else.
86 func (l *sessionLockFile) RemoveAndUnlock() error {
87 removeErr := os.Remove(l.f.Name())
88 l.Unlock()
89 if removeErr != nil && !os.IsNotExist(removeErr) {
90 return removeErr
91 }
92 return nil
93 }
94
95 func tryLockSessionLeaseFile(path string) (func(), error) {
96 f, err := os.OpenFile(store.SessionLeaseLock(path), os.O_CREATE|os.O_RDWR, 0o600)
97 if err != nil {
98 return nil, err
99 }
100 if err := unix.Flock(int(f.Fd()), unix.LOCK_EX|unix.LOCK_NB); err != nil {
101 _ = f.Close()
102 if errors.Is(err, unix.EWOULDBLOCK) || errors.Is(err, unix.EAGAIN) {
103 return nil, ErrSessionLeaseHeld
104 }
105 return nil, err
106 }
107 return func() {
108 _ = unix.Flock(int(f.Fd()), unix.LOCK_UN)
109 _ = f.Close()
110 }, nil
111 }
112
113 func readSessionLeaseLockFile(path string) ([]byte, error) {
114 return os.ReadFile(path)
115 }
116
116 lines GO