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 // Package flock implements a thread-safe interface for file locking.
7 // It also includes a non-blocking TryLock() function to allow locking
8 // without blocking execution.
9 //
10 // Package flock is released under the BSD 3-Clause License. See the LICENSE file
11 // for more details.
12 //
13 // While using this library, remember that the locking behaviors are not
14 // guaranteed to be the same on each platform. For example, some UNIX-like
15 // operating systems will transparently convert a shared lock to an exclusive
16 // lock. If you Unlock() the flock from a location where you believe that you
17 // have the shared lock, you may accidentally drop the exclusive lock.
18 package flock
19 20 import (
21 "context"
22 "io/fs"
23 "os"
24 "runtime"
25 "sync"
26 "time"
27 )
28 29 type Option func(f *Flock)
30 31 // SetFlag sets the flag used to create/open the file.
32 func SetFlag(flag int) Option {
33 return func(f *Flock) {
34 f.flag = flag
35 }
36 }
37 38 // SetPermissions sets the OS permissions to set on the file.
39 func SetPermissions(perm fs.FileMode) Option {
40 return func(f *Flock) {
41 f.perm = perm
42 }
43 }
44 45 // Flock is the struct type to handle file locking. All fields are unexported,
46 // with access to some of the fields provided by getter methods (Path() and Locked()).
47 type Flock struct {
48 path string
49 m sync.RWMutex
50 fh *os.File
51 l bool
52 r bool
53 54 // flag is the flag used to create/open the file.
55 flag int
56 // perm is the OS permissions to set on the file.
57 perm fs.FileMode
58 }
59 60 // New returns a new instance of *Flock. The only parameter
61 // it takes is the path to the desired lockfile.
62 func New(path string, opts ...Option) *Flock {
63 // create it if it doesn't exist, and open the file read-only.
64 flags := os.O_CREATE
65 66 switch runtime.GOOS {
67 case "aix", "solaris", "illumos":
68 // AIX cannot preform write-lock (i.e. exclusive) on a read-only file.
69 flags |= os.O_RDWR
70 default:
71 flags |= os.O_RDONLY
72 }
73 74 f := &Flock{
75 path: path,
76 flag: flags,
77 perm: fs.FileMode(0o600),
78 }
79 80 for _, opt := range opts {
81 opt(f)
82 }
83 84 return f
85 }
86 87 // NewFlock returns a new instance of *Flock. The only parameter
88 // it takes is the path to the desired lockfile.
89 //
90 // Deprecated: Use New instead.
91 func NewFlock(path string) *Flock {
92 return New(path)
93 }
94 95 // Close is equivalent to calling Unlock.
96 //
97 // This will release the lock and close the underlying file descriptor.
98 // It will not remove the file from disk, that's up to your application.
99 func (f *Flock) Close() error {
100 return f.Unlock()
101 }
102 103 // Path returns the path as provided in NewFlock().
104 func (f *Flock) Path() string {
105 return f.path
106 }
107 108 // Locked returns the lock state (locked: true, unlocked: false).
109 //
110 // Warning: by the time you use the returned value, the state may have changed.
111 func (f *Flock) Locked() bool {
112 f.m.RLock()
113 defer f.m.RUnlock()
114 115 return f.l
116 }
117 118 // RLocked returns the read lock state (locked: true, unlocked: false).
119 //
120 // Warning: by the time you use the returned value, the state may have changed.
121 func (f *Flock) RLocked() bool {
122 f.m.RLock()
123 defer f.m.RUnlock()
124 125 return f.r
126 }
127 128 // Stat returns the FileInfo structure describing the lock file.
129 // If the lock file does not exist or cannot be accessed, an error is returned.
130 //
131 // This can be used to check the modification time of the lock file,
132 // which is useful for detecting stale locks.
133 func (f *Flock) Stat() (fs.FileInfo, error) {
134 f.m.RLock()
135 defer f.m.RUnlock()
136 137 if f.fh != nil {
138 return f.fh.Stat()
139 }
140 141 return os.Stat(f.path)
142 }
143 144 func (f *Flock) String() string {
145 return f.path
146 }
147 148 // TryLockContext repeatedly tries to take an exclusive lock until one of the conditions is met:
149 // - TryLock succeeds
150 // - TryLock fails with error
151 // - Context Done channel is closed.
152 func (f *Flock) TryLockContext(ctx context.Context, retryDelay time.Duration) (bool, error) {
153 return tryCtx(ctx, f.TryLock, retryDelay)
154 }
155 156 // TryRLockContext repeatedly tries to take a shared lock until one of the conditions is met:
157 // - TryRLock succeeds
158 // - TryRLock fails with error
159 // - Context Done channel is closed.
160 func (f *Flock) TryRLockContext(ctx context.Context, retryDelay time.Duration) (bool, error) {
161 return tryCtx(ctx, f.TryRLock, retryDelay)
162 }
163 164 func tryCtx(ctx context.Context, fn func() (bool, error), retryDelay time.Duration) (bool, error) {
165 if ctx.Err() != nil {
166 return false, ctx.Err()
167 }
168 169 for {
170 if ok, err := fn(); ok || err != nil {
171 return ok, err
172 }
173 174 select {
175 case <-ctx.Done():
176 return false, ctx.Err()
177 case <-time.After(retryDelay):
178 }
179 }
180 }
181 182 func (f *Flock) setFh(flag int) error {
183 // open a new os.File instance
184 fh, err := os.OpenFile(f.path, flag, f.perm)
185 if err != nil {
186 return err
187 }
188 189 // set the file handle on the struct
190 f.fh = fh
191 192 return nil
193 }
194 195 // resetFh resets file handle:
196 // - tries to close the file (ignore errors)
197 // - sets fh to nil.
198 func (f *Flock) resetFh() {
199 if f.fh == nil {
200 return
201 }
202 203 _ = f.fh.Close()
204 205 f.fh = nil
206 }
207 208 // ensure the file handle is closed if no lock is held.
209 func (f *Flock) ensureFhState() {
210 if f.l || f.r || f.fh == nil {
211 return
212 }
213 214 f.resetFh()
215 }
216 217 func (f *Flock) reset() {
218 f.l = false
219 f.r = false
220 221 f.resetFh()
222 }
223