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