github.com/u-root/u-root@v7.0.1-0.20200915234505-ad7babab0a8e+incompatible/pkg/lockfile/lockfile.go (about) 1 // Copyright 2018 the u-root Authors. All rights reserved 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Package lockfile coordinates process-based file locking. 6 // 7 // This package is designed to aid concurrency issues between different 8 // processes. 9 // 10 // Sample usage: 11 // 12 // lf := lockfile.New("/var/apt/apt.lock") 13 // // Blocks and waits if /var/apt/apt.lock already exists. 14 // if err := lf.Lock(); err != nil { 15 // log.Fatal(err) 16 // } 17 // 18 // defer lf.MustUnlock() 19 // 20 // // Do something in /var/apt/?? 21 // 22 // Two concurrent invocations of this program will compete to create 23 // /var/apt/apt.lock, and then make the other wait until /var/apt/apt.lock 24 // disappears. 25 // 26 // If the lock holding process disappears without removing the lock, another 27 // process using this library will detect that and remove the lock. 28 // 29 // If some other entity removes the lockfile erroneously, the lock holder's 30 // call to Unlock() will return an ErrRogueDeletion. 31 package lockfile 32 33 import ( 34 "errors" 35 "fmt" 36 "io" 37 "io/ioutil" 38 "log" 39 "os" 40 "path/filepath" 41 "strconv" 42 "syscall" 43 ) 44 45 var ( 46 // ErrRogueDeletion means the lock file was removed by someone 47 // other than the lock holder. 48 ErrRogueDeletion = errors.New("cannot unlock lockfile owned by another process") 49 50 // ErrBusy means the lock is being held by another living process. 51 ErrBusy = errors.New("file is locked by another process") 52 53 // ErrInvalidPID means the lock file contains an incompatible syntax. 54 ErrInvalidPID = errors.New("lockfile points to file with invalid content") 55 56 // ErrProcessDead means the lock is held by a process that does not exist. 57 ErrProcessDead = errors.New("lockfile points to invalid PID") 58 ) 59 60 var ( 61 errUnlocked = errors.New("file is unlocked") 62 ) 63 64 // Lockfile is a process-based file lock. 65 type Lockfile struct { 66 // path is the file whose existense is the lock. 67 path string 68 69 // pid is our PID. 70 // 71 // This mostly exists for testing. 72 pid int 73 } 74 75 // New returns a new lock file at the given path. 76 func New(path string) *Lockfile { 77 return &Lockfile{ 78 path: path, 79 pid: os.Getpid(), 80 } 81 } 82 83 func (l *Lockfile) pidfile() (string, error) { 84 dir, base := filepath.Split(l.path) 85 pidfile, err := ioutil.TempFile(dir, fmt.Sprintf("%s-", base)) 86 if err != nil { 87 return "", err 88 } 89 defer pidfile.Close() 90 91 if _, err := io.WriteString(pidfile, fmt.Sprintf("%d", l.pid)); err != nil { 92 if err := os.Remove(pidfile.Name()); err != nil { 93 log.Fatalf("Lockfile could not remove %q: %v", pidfile.Name(), err) 94 } 95 return "", err 96 } 97 return pidfile.Name(), nil 98 } 99 100 // TryLock attempts to create the lock file, or returns ErrBusy if a valid lock 101 // file already exists. 102 // 103 // If the lock file is detected not to be valid, it is removed and replaced 104 // with our lock file. 105 func (l *Lockfile) TryLock() error { 106 pidpath, err := l.pidfile() 107 if err != nil { 108 return err 109 } 110 111 if err := l.lockWith(pidpath); err != nil { 112 if err := os.Remove(pidpath); err != nil { 113 log.Fatalf("Lockfile could not remove %q: %v", pidpath, err) 114 } 115 return err 116 } 117 return nil 118 } 119 120 // Lock blocks until it can create a valid lock file. 121 // 122 // If a valid lock file already exists, it waits for the file to be deleted or 123 // until the associated process dies. 124 // 125 // If an invalid lock file exists, it will be deleted and we'll retry locking. 126 func (l *Lockfile) Lock() error { 127 pidpath, err := l.pidfile() 128 if err != nil { 129 return err 130 } 131 132 // Spin, oh, spin. 133 if err := l.lockWith(pidpath); err == ErrBusy { 134 return l.lockWith(pidpath) 135 } else if err != nil { 136 if err := os.Remove(pidpath); err != nil { 137 log.Fatalf("Lockfile could not remove %q: %v", pidpath, err) 138 } 139 return err 140 } 141 return nil 142 } 143 144 func (l *Lockfile) checkLockfile() error { 145 owningPid, err := ioutil.ReadFile(l.path) 146 if os.IsNotExist(err) { 147 return errUnlocked 148 } else if err != nil { 149 return err 150 } 151 152 if len(owningPid) == 0 { 153 return ErrInvalidPID 154 } 155 156 pid, err := strconv.Atoi(string(owningPid)) 157 if err != nil || pid <= 0 { 158 return ErrInvalidPID 159 } 160 161 p, err := os.FindProcess(pid) 162 if err != nil { 163 return ErrProcessDead 164 } 165 166 if err := p.Signal(syscall.Signal(0)); err != nil { 167 return ErrProcessDead 168 } 169 170 if pid == l.pid { 171 return nil 172 } 173 return ErrBusy 174 } 175 176 // Unlock attempts to delete the lock file. 177 // 178 // If we are not the lock holder, and the lock holder is an existing process, 179 // ErrRogueDeletion will be returned. 180 // 181 // If we are not the lock holder, and there is no valid lock holder (process 182 // died, invalid lock file syntax), ErrRogueDeletion will be returned. 183 func (l *Lockfile) Unlock() error { 184 switch err := l.checkLockfile(); err { 185 case nil: 186 // Nuke the symlink and its target. 187 target, err := os.Readlink(l.path) 188 if os.IsNotExist(err) { 189 return ErrRogueDeletion 190 } else if err != nil { 191 // The symlink is somehow screwed up. Just nuke it. 192 if err := os.Remove(l.path); err != nil { 193 log.Fatalf("Lockfile could not remove %q: %v", l.path, err) 194 } 195 return err 196 } 197 198 absTarget := resolveSymlinkTarget(l.path, target) 199 if err := os.Remove(absTarget); os.IsNotExist(err) { 200 return ErrRogueDeletion 201 } else if err != nil { 202 return err 203 } 204 205 if err := os.Remove(l.path); os.IsNotExist(err) { 206 return ErrRogueDeletion 207 } else if err != nil { 208 return err 209 } 210 return nil 211 212 case ErrInvalidPID, ErrProcessDead, errUnlocked, ErrBusy: 213 return ErrRogueDeletion 214 215 default: 216 return err 217 } 218 } 219 220 // MustUnlock panics if the call to Unlock fails. 221 func (l *Lockfile) MustUnlock() { 222 if err := l.Unlock(); err != nil { 223 log.Fatalf("could not unlock %q: %v", l.path, err) 224 } 225 } 226 227 // resolveSymlinkTarget returns an absolute path for a given symlink target. 228 // 229 // Symlinks targets' "working directory" is the symlink's parent. 230 // 231 // Said another way, a symlink is always resolved relative to the symlink's 232 // parent. 233 // 234 // E.g. 235 // /foo/bar -> ./zoo resolves to the absolute path /foo/zoo 236 func resolveSymlinkTarget(symlink, target string) string { 237 if filepath.IsAbs(target) { 238 return target 239 } 240 241 return filepath.Join(filepath.Dir(symlink), target) 242 } 243 244 func (l *Lockfile) lockWith(pidpath string) error { 245 switch err := os.Symlink(pidpath, l.path); { 246 case err == nil: 247 return nil 248 249 case !os.IsExist(err): 250 // Some kind of system error. 251 return err 252 253 default: 254 // Symlink already exists. 255 switch err := l.checkLockfile(); err { 256 case errUnlocked: 257 return l.lockWith(pidpath) 258 259 case ErrInvalidPID, ErrProcessDead: 260 // Nuke the symlink and its target. 261 target, err := os.Readlink(l.path) 262 if os.IsNotExist(err) { 263 return l.lockWith(pidpath) 264 } else if err != nil { 265 // File might not be a symlink at all? 266 // Leave it alone, in case it's someone's 267 // legitimate file. 268 return err 269 } 270 271 absTarget := resolveSymlinkTarget(l.path, target) 272 // If it doesn't exist anymore, whatever. The symlink's 273 // existence is the actual lock. 274 if err := os.Remove(absTarget); !os.IsNotExist(err) && err != nil { 275 return err 276 } 277 278 if err := os.Remove(l.path); os.IsNotExist(err) { 279 return l.lockWith(pidpath) 280 } else if err != nil { 281 return err 282 } 283 284 // Retry making the symlink. 285 return l.lockWith(pidpath) 286 287 case ErrBusy, nil: 288 return err 289 290 default: 291 return err 292 } 293 } 294 }