github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/internal/osutils/lockfile/pidlock.go (about) 1 package lockfile 2 3 import ( 4 "fmt" 5 "io" 6 "os" 7 "strconv" 8 "time" 9 10 "github.com/ActiveState/cli/internal/errs" 11 "github.com/ActiveState/cli/internal/osutils/stacktrace" 12 ) 13 14 // PidLock represents a lock file that can be used for exclusive access to 15 // resources that should be accessed by only one process at a time. 16 // 17 // The characteristics of the lock are: 18 // - Lockfiles are removed after use 19 // - Even if the lockfiles are not removed (because a process has been terminated prematurely), it is unlocked 20 // - On file-systems that support advisory locks via fcntl or LockFileEx, all file system operations are atomic 21 // 22 // Notes: 23 // - The implementation currently does not support a blocking wait operation that returns once the lock can be acquired. If required, it can be extended this way. 24 // - Storing the PID inside the lockfile was initially intended to be fall-back mechanism for file systems that do not support locking files. This is probably unnecessary, but could be extended to communicate with the process currently holding the lock via its PID. 25 type PidLock struct { 26 path string 27 file *os.File 28 locked bool 29 } 30 31 // NewPidLock creates a new PidLock that can be used to get exclusive access to resources between processes 32 func NewPidLock(path string) (pl *PidLock, err error) { 33 f, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0666) 34 if err != nil { 35 return nil, errs.Wrap(err, "failed to open lock file %s", path) 36 } 37 return &PidLock{ 38 path: path, 39 file: f, 40 }, nil 41 } 42 43 // TryLock attempts to lock the created lock file. 44 func (pl *PidLock) TryLock() (err error) { 45 err = LockFile(pl.file) 46 if err != nil { 47 // if lock cannot be acquired it means that another process is holding the lock 48 return NewAlreadyLockedError(err, pl.path, "cannot acquire exclusive lock") 49 } 50 51 // check if PID can be read and if so, if the process is running 52 b := make([]byte, 100) 53 n, err := pl.file.Read(b) 54 if err != nil && err != io.EOF { 55 _ = LockRelease(pl.file) 56 return errs.Wrap(err, "failed to read PID from lockfile %s", pl.path) 57 } 58 if n > 0 { 59 pid, err := strconv.ParseInt(string(b[:n]), 10, 64) 60 if err != nil { 61 _ = LockRelease(pl.file) 62 return errs.Wrap(err, "failed to parse PID from lockfile %s", pl.path) 63 } 64 if PidExists(int(pid)) { 65 _ = LockRelease(pl.file) 66 err := fmt.Errorf("pid %d exists", pid) 67 return NewAlreadyLockedError(err, pl.path, "pid parsed") 68 } 69 } 70 71 // write PID into lock file 72 _, err = pl.file.Write([]byte(fmt.Sprintf("%d", os.Getpid()))) 73 if err != nil { 74 _ = LockRelease(pl.file) 75 return errs.Wrap(err, "failed to write pid to lockfile %s", pl.path) 76 } 77 78 pl.locked = true 79 return nil 80 } 81 82 // Close removes the lock file and releases the lock 83 func (pl *PidLock) Close(keepFile ...bool) error { 84 keep := false 85 if len(keepFile) == 1 { 86 keep = keepFile[0] 87 } 88 if !pl.locked { 89 err := pl.file.Close() 90 if err != nil { 91 return errs.Wrap(err, "failed to close unlocked lock file %s", pl.path) 92 } 93 return nil 94 } 95 err := pl.cleanLockFile(keep) 96 if err != nil { 97 return errs.Wrap(err, "failed to remove lock file") 98 } 99 return nil 100 } 101 102 // WaitForLock will attempt to acquire the lock for the duration given 103 func (pl *PidLock) WaitForLock(timeout time.Duration) error { 104 expiration := time.Now().Add(timeout) 105 for { 106 err := pl.TryLock() 107 if err != nil { 108 if !errs.Matches(err, &AlreadyLockedError{}) { 109 return errs.Wrap(err, "Could not acquire lock") 110 } 111 112 if time.Now().After(expiration) { 113 return errs.Wrap(err, "Timed out trying to acquire lock") 114 } 115 time.Sleep(100 * time.Millisecond) 116 continue 117 } 118 return nil 119 } 120 } 121 122 // AlreadyLockedError manages info that clarifies why a lock has failed, but 123 // is still likely valid. 124 type AlreadyLockedError struct { 125 err error 126 file string 127 msg string 128 stack *stacktrace.Stacktrace 129 } 130 131 // NewAlreadyLockedError returns a new AlreadyLockedError. 132 func NewAlreadyLockedError(err error, file, msg string) *AlreadyLockedError { 133 return &AlreadyLockedError{err, file, msg, stacktrace.Get()} 134 } 135 136 // Error implements the error interface. 137 func (e *AlreadyLockedError) Error() string { 138 return fmt.Sprintf("file %q is already locked: %s", e.file, e.msg) 139 } 140 141 // Unwrap allows the unwrapping of a causing error. 142 func (e *AlreadyLockedError) Unwrap() error { 143 return e.err 144 } 145 146 // Stack implements the errs.WrapperError interface. 147 func (e *AlreadyLockedError) Stack() *stacktrace.Stacktrace { 148 return e.stack 149 }