github.com/mutagen-io/mutagen@v0.18.0-rc1/pkg/filesystem/locking/locker_posix.go (about) 1 //go:build !windows && !plan9 2 3 // TODO: Figure out what to do for Plan 9. It doesn't support FcntlFlock at all, 4 // but we might be able to ~emulate it with os.O_EXCL, but that wouldn't allow 5 // us to automatically release locks if a process dies. 6 7 package locking 8 9 import ( 10 "errors" 11 "os" 12 13 "golang.org/x/sys/unix" 14 ) 15 16 // fcntlFlockRetryingOnEINTR is a wrapper around the fcntl system call that 17 // retries on EINTR errors and returns on the first successful call or non-EINTR 18 // error. 19 func fcntlFlockRetryingOnEINTR(file uintptr, command int, specification *unix.Flock_t) error { 20 for { 21 err := unix.FcntlFlock(file, command, specification) 22 if err == unix.EINTR { 23 continue 24 } 25 return err 26 } 27 } 28 29 // Lock attempts to acquire the file lock. 30 func (l *Locker) Lock(block bool) error { 31 // Verify that we don't already hold the lock. 32 if l.held { 33 return errors.New("lock already held") 34 } 35 36 // Set up the lock specification. 37 lockSpec := unix.Flock_t{ 38 Type: unix.F_WRLCK, 39 Whence: int16(os.SEEK_SET), 40 Start: 0, 41 Len: 0, 42 } 43 44 // Set up the blocking specification. 45 operation := unix.F_SETLK 46 if block { 47 operation = unix.F_SETLKW 48 } 49 50 // Attempt to perform locking, retrying if EINTR is encountered. According 51 // to the POSIX standard, EINTR should only be expected in blocking cases 52 // (i.e. when using F_SETLKW), but Linux allows it to be received when using 53 // F_SETLK if it occurs before the lock is checked or acquired. Given that 54 // Go's runtime preemption can also cause spurious interrupts, it's best to 55 // handle EINTR in all cases. 56 if err := fcntlFlockRetryingOnEINTR(l.file.Fd(), operation, &lockSpec); err != nil { 57 return err 58 } 59 60 // Mark the lock as held. 61 l.held = true 62 63 // Success. 64 return nil 65 } 66 67 // Unlock releases the file lock. 68 func (l *Locker) Unlock() error { 69 // Verify that we hold the lock. 70 if !l.held { 71 return errors.New("lock not held") 72 } 73 74 // Set up the unlock specification. 75 unlockSpec := unix.Flock_t{ 76 Type: unix.F_UNLCK, 77 Whence: int16(os.SEEK_SET), 78 Start: 0, 79 Len: 0, 80 } 81 82 // Attempt to perform unlocking. Unlike the locking case, we don't retry if 83 // EINTR is encountered because we don't have any information about the 84 // state of the lock in that case (POSIX doesn't even allow for EINTR when 85 // using F_SETLK and the Linux documentation only covers the locking case). 86 // If the lock was successfully unlocked and we retry due to EINTR, then we 87 // might end up in a race condition with other code trying to acquire the 88 // lock. This is the same issue as with calls to close returning EINTR, in 89 // which case the Go standard library and runtime (and Mutagen) don't retry 90 // the operation because it's safer to err on the side of failure. 91 // 92 // In any case, this isn't ever going to be an issue for Mutagen in practice 93 // because (a) EINTR is exceedingly unlikely here, (b) this code is only 94 // used in Mutagen right before a process exits (usually in a defer that 95 // ignores errors), and (c) Mutagen already has to be careful to avoid 96 // multiple code paths managing locks on the same file (because POSIX 97 // releases all fcntl locks for a file if any file descriptor for that lock 98 // in the process is closed). 99 if err := unix.FcntlFlock(l.file.Fd(), unix.F_SETLK, &unlockSpec); err != nil { 100 return err 101 } 102 103 // Mark the lock as no longer being held. 104 l.held = false 105 106 // Success. 107 return nil 108 }