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  }