flock_windows.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 windows
   7  
   8  package flock
   9  
  10  import (
  11  	"errors"
  12  
  13  	"golang.org/x/sys/windows"
  14  )
  15  
  16  // Use of 0x00000000 for the shared lock is a guess based on some the MS Windows `LockFileEX` docs,
  17  // which document the `LOCKFILE_EXCLUSIVE_LOCK` flag as:
  18  //
  19  // > The function requests an exclusive lock. Otherwise, it requests a shared lock.
  20  //
  21  // https://msdn.microsoft.com/en-us/library/windows/desktop/aa365203(v=vs.85).aspx
  22  const winLockfileSharedLock = 0x00000000
  23  
  24  // ErrorLockViolation is the error code returned from the Windows syscall when a lock would block,
  25  // and you ask to fail immediately.
  26  //
  27  //nolint:errname // It should be renamed to `ErrLockViolation`.
  28  const ErrorLockViolation windows.Errno = 0x21 // 33
  29  
  30  // Lock is a blocking call to try and take an exclusive file lock.
  31  // It will wait until it is able to obtain the exclusive file lock.
  32  // It's recommended that TryLock() be used over this function.
  33  // This function may block the ability to query the current Locked() or RLocked() status due to a RW-mutex lock.
  34  //
  35  // If we are already locked, this function short-circuits and
  36  // returns immediately assuming it can take the mutex lock.
  37  func (f *Flock) Lock() error {
  38  	return f.lock(&f.l, windows.LOCKFILE_EXCLUSIVE_LOCK)
  39  }
  40  
  41  // RLock is a blocking call to try and take a shared file lock.
  42  // It will wait until it is able to obtain the shared file lock.
  43  // It's recommended that TryRLock() be used over this function.
  44  // This function may block the ability to query the current Locked() or RLocked() status due to a RW-mutex lock.
  45  //
  46  // If we are already locked, this function short-circuits and
  47  // returns immediately assuming it can take the mutex lock.
  48  func (f *Flock) RLock() error {
  49  	return f.lock(&f.r, winLockfileSharedLock)
  50  }
  51  
  52  func (f *Flock) lock(locked *bool, flag uint32) error {
  53  	f.m.Lock()
  54  	defer f.m.Unlock()
  55  
  56  	if *locked {
  57  		return nil
  58  	}
  59  
  60  	if f.fh == nil {
  61  		if err := f.setFh(f.flag); err != nil {
  62  			return err
  63  		}
  64  
  65  		defer f.ensureFhState()
  66  	}
  67  
  68  	err := windows.LockFileEx(windows.Handle(f.fh.Fd()), flag, 0, 1, 0, &windows.Overlapped{})
  69  	if err != nil && !errors.Is(err, windows.Errno(0)) {
  70  		return err
  71  	}
  72  
  73  	*locked = true
  74  
  75  	return nil
  76  }
  77  
  78  // Unlock is a function to unlock the file.
  79  // This file takes a RW-mutex lock,
  80  // so while it is running the Locked() and RLocked() functions will be blocked.
  81  //
  82  // This function short-circuits if we are unlocked already.
  83  // If not, it calls UnlockFileEx() on the file and closes the file descriptor.
  84  // It does not remove the file from disk.
  85  // It's up to your application to do.
  86  func (f *Flock) Unlock() error {
  87  	f.m.Lock()
  88  	defer f.m.Unlock()
  89  
  90  	// if we aren't locked or if the lockfile instance is nil
  91  	// just return a nil error because we are unlocked
  92  	if (!f.l && !f.r) || f.fh == nil {
  93  		return nil
  94  	}
  95  
  96  	// mark the file as unlocked
  97  	err := windows.UnlockFileEx(windows.Handle(f.fh.Fd()), 0, 1, 0, &windows.Overlapped{})
  98  	if err != nil && !errors.Is(err, windows.Errno(0)) {
  99  		return err
 100  	}
 101  
 102  	f.reset()
 103  
 104  	return nil
 105  }
 106  
 107  // TryLock is the preferred function for taking an exclusive file lock.
 108  // This function does take a RW-mutex lock before it tries to lock the file,
 109  // so there is the possibility that this function may block for a short time
 110  // if another goroutine is trying to take any action.
 111  //
 112  // The actual file lock is non-blocking.
 113  // If we are unable to get the exclusive file lock,
 114  // the function will return false instead of waiting for the lock.
 115  // If we get the lock, we also set the *Flock instance as being exclusive-locked.
 116  func (f *Flock) TryLock() (bool, error) {
 117  	return f.try(&f.l, windows.LOCKFILE_EXCLUSIVE_LOCK)
 118  }
 119  
 120  // TryRLock is the preferred function for taking a shared file lock.
 121  // This function does take a RW-mutex lock before it tries to lock the file,
 122  // so there is the possibility that this function may block for a short time if another goroutine is trying to take any action.
 123  //
 124  // The actual file lock is non-blocking.
 125  // If we are unable to get the shared file lock,
 126  // the function will return false instead of waiting for the lock.
 127  // If we get the lock, we also set the *Flock instance as being shared-locked.
 128  func (f *Flock) TryRLock() (bool, error) {
 129  	return f.try(&f.r, winLockfileSharedLock)
 130  }
 131  
 132  func (f *Flock) try(locked *bool, flag uint32) (bool, error) {
 133  	f.m.Lock()
 134  	defer f.m.Unlock()
 135  
 136  	if *locked {
 137  		return true, nil
 138  	}
 139  
 140  	if f.fh == nil {
 141  		if err := f.setFh(f.flag); err != nil {
 142  			return false, err
 143  		}
 144  
 145  		defer f.ensureFhState()
 146  	}
 147  
 148  	err := windows.LockFileEx(windows.Handle(f.fh.Fd()), flag|windows.LOCKFILE_FAIL_IMMEDIATELY, 0, 1, 0, &windows.Overlapped{})
 149  	if err != nil && !errors.Is(err, windows.Errno(0)) {
 150  		if errors.Is(err, ErrorLockViolation) || errors.Is(err, windows.ERROR_IO_PENDING) {
 151  			return false, nil
 152  		}
 153  
 154  		return false, err
 155  	}
 156  
 157  	*locked = true
 158  
 159  	return true, nil
 160  }
 161