flock_unix.go raw

   1  // Copyright 2015 Tim Heckman. All rights reserved.
   2  // Copyright 2018-2025 The Gofrs. All rights reserved.
   3  // Use of this source code is governed by the BSD 3-Clause
   4  // license that can be found in the LICENSE file.
   5  
   6  //go:build darwin || dragonfly || freebsd || illumos || linux || netbsd || openbsd
   7  
   8  package flock
   9  
  10  import (
  11  	"errors"
  12  	"os"
  13  
  14  	"golang.org/x/sys/unix"
  15  )
  16  
  17  // Lock is a blocking call to try and take an exclusive file lock.
  18  // It will wait until it is able to obtain the exclusive file lock.
  19  // It's recommended that TryLock() be used over this function.
  20  // This function may block the ability to query the current Locked() or RLocked() status due to a RW-mutex lock.
  21  //
  22  // If we are already exclusive-locked,
  23  // this function short-circuits and returns immediately assuming it can take the mutex lock.
  24  //
  25  // If the *Flock has a shared lock (RLock),
  26  // this may transparently replace the shared lock with an exclusive lock on some UNIX-like operating systems.
  27  // Be careful when using exclusive locks in conjunction with shared locks (RLock()),
  28  // because calling Unlock() may accidentally release the exclusive lock that was once a shared lock.
  29  func (f *Flock) Lock() error {
  30  	return f.lock(&f.l, unix.LOCK_EX)
  31  }
  32  
  33  // RLock is a blocking call to try and take a shared file lock.
  34  // It will wait until it is able to obtain the shared file lock.
  35  // It's recommended that TryRLock() be used over this function.
  36  // This function may block the ability to query the current Locked() or RLocked() status due to a RW-mutex lock.
  37  //
  38  // If we are already shared-locked,
  39  // this function short-circuits and returns immediately assuming it can take the mutex lock.
  40  func (f *Flock) RLock() error {
  41  	return f.lock(&f.r, unix.LOCK_SH)
  42  }
  43  
  44  func (f *Flock) lock(locked *bool, flag int) error {
  45  	f.m.Lock()
  46  	defer f.m.Unlock()
  47  
  48  	if *locked {
  49  		return nil
  50  	}
  51  
  52  	if f.fh == nil {
  53  		if err := f.setFh(f.flag); err != nil {
  54  			return err
  55  		}
  56  
  57  		defer f.ensureFhState()
  58  	}
  59  
  60  	err := unix.Flock(int(f.fh.Fd()), flag)
  61  	if err != nil {
  62  		shouldRetry, reopenErr := f.reopenFDOnError(err)
  63  		if reopenErr != nil {
  64  			return reopenErr
  65  		}
  66  
  67  		if !shouldRetry {
  68  			return err
  69  		}
  70  
  71  		err = unix.Flock(int(f.fh.Fd()), flag)
  72  		if err != nil {
  73  			return err
  74  		}
  75  	}
  76  
  77  	*locked = true
  78  
  79  	return nil
  80  }
  81  
  82  // Unlock is a function to unlock the file.
  83  // This file takes a RW-mutex lock,
  84  // so while it is running the Locked() and RLocked() functions will be blocked.
  85  //
  86  // This function short-circuits if we are unlocked already.
  87  // If not, it calls unix.LOCK_UN on the file and closes the file descriptor.
  88  // It does not remove the file from disk. It's up to your application to do.
  89  //
  90  // Please note,
  91  // if your shared lock became an exclusive lock,
  92  // this may unintentionally drop the exclusive lock if called by the consumer that believes they have a shared lock.
  93  // Please see Lock() for more details.
  94  func (f *Flock) Unlock() error {
  95  	f.m.Lock()
  96  	defer f.m.Unlock()
  97  
  98  	// If we aren't locked or if the lockfile instance is nil
  99  	// just return a nil error because we are unlocked.
 100  	if (!f.l && !f.r) || f.fh == nil {
 101  		return nil
 102  	}
 103  
 104  	// Mark the file as unlocked.
 105  	err := unix.Flock(int(f.fh.Fd()), unix.LOCK_UN)
 106  	if err != nil {
 107  		return err
 108  	}
 109  
 110  	f.reset()
 111  
 112  	return nil
 113  }
 114  
 115  // TryLock is the preferred function for taking an exclusive file lock.
 116  // This function takes an RW-mutex lock before it tries to lock the file,
 117  // so there is the possibility that this function may block for a short time
 118  // if another goroutine is trying to take any action.
 119  //
 120  // The actual file lock is non-blocking.
 121  // If we are unable to get the exclusive file lock,
 122  // the function will return false instead of waiting for the lock.
 123  // If we get the lock, we also set the *Flock instance as being exclusive-locked.
 124  func (f *Flock) TryLock() (bool, error) {
 125  	return f.try(&f.l, unix.LOCK_EX)
 126  }
 127  
 128  // TryRLock is the preferred function for taking a shared file lock.
 129  // This function takes an RW-mutex lock before it tries to lock the file,
 130  // so there is the possibility that this function may block for a short time
 131  // if another goroutine is trying to take any action.
 132  //
 133  // The actual file lock is non-blocking.
 134  // If we are unable to get the shared file lock,
 135  // the function will return false instead of waiting for the lock.
 136  // If we get the lock, we also set the *Flock instance as being share-locked.
 137  func (f *Flock) TryRLock() (bool, error) {
 138  	return f.try(&f.r, unix.LOCK_SH)
 139  }
 140  
 141  func (f *Flock) try(locked *bool, flag int) (bool, error) {
 142  	f.m.Lock()
 143  	defer f.m.Unlock()
 144  
 145  	if *locked {
 146  		return true, nil
 147  	}
 148  
 149  	if f.fh == nil {
 150  		if err := f.setFh(f.flag); err != nil {
 151  			return false, err
 152  		}
 153  
 154  		defer f.ensureFhState()
 155  	}
 156  
 157  	var retried bool
 158  
 159  retry:
 160  	err := unix.Flock(int(f.fh.Fd()), flag|unix.LOCK_NB)
 161  
 162  	switch {
 163  	case errors.Is(err, unix.EWOULDBLOCK):
 164  		return false, nil
 165  	case err == nil:
 166  		*locked = true
 167  		return true, nil
 168  	}
 169  
 170  	if !retried {
 171  		shouldRetry, reopenErr := f.reopenFDOnError(err)
 172  		if reopenErr != nil {
 173  			return false, reopenErr
 174  		} else if shouldRetry {
 175  			retried = true
 176  			goto retry
 177  		}
 178  	}
 179  
 180  	return false, err
 181  }
 182  
 183  // reopenFDOnError determines whether we should reopen the file handle in readwrite mode and try again.
 184  // This comes from `util-linux/sys-utils/flock.c`:
 185  // > Since Linux 3.4 (commit 55725513)
 186  // > Probably NFSv4 where flock() is emulated by fcntl().
 187  // > https://github.com/util-linux/util-linux/blob/198e920aa24743ef6ace4e07cf6237de527f9261/sys-utils/flock.c#L374-L390
 188  func (f *Flock) reopenFDOnError(err error) (bool, error) {
 189  	if !errors.Is(err, unix.EIO) && !errors.Is(err, unix.EBADF) {
 190  		return false, nil
 191  	}
 192  
 193  	st, err := f.fh.Stat()
 194  	if err != nil {
 195  		return false, nil
 196  	}
 197  
 198  	if st.Mode()&f.perm != f.perm {
 199  		return false, nil
 200  	}
 201  
 202  	f.resetFh()
 203  
 204  	// reopen in read-write mode and set the file handle
 205  	err = f.setFh(f.flag | os.O_RDWR)
 206  	if err != nil {
 207  		return false, err
 208  	}
 209  
 210  	return true, nil
 211  }
 212