├── .gitignore ├── LICENSE ├── README.md ├── fslock.go ├── fslock_nix.go ├── fslock_test.go └── fslock_windows.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | All files in this repository are licensed as follows. If you contribute 2 | to this repository, it is assumed that you license your contribution 3 | under the same license unless you state otherwise. 4 | 5 | All files Copyright (C) 2015 Canonical Ltd. unless otherwise specified in the file. 6 | 7 | This software is licensed under the LGPLv3, included below. 8 | 9 | As a special exception to the GNU Lesser General Public License version 3 10 | ("LGPL3"), the copyright holders of this Library give you permission to 11 | convey to a third party a Combined Work that links statically or dynamically 12 | to this Library without providing any Minimal Corresponding Source or 13 | Minimal Application Code as set out in 4d or providing the installation 14 | information set out in section 4e, provided that you comply with the other 15 | provisions of LGPL3 and provided that you meet, for the Application the 16 | terms and conditions of the license(s) which apply to the Application. 17 | 18 | Except as stated in this special exception, the provisions of LGPL3 will 19 | continue to comply in full to this Library. If you modify this Library, you 20 | may apply this exception to your version of this Library, but you are not 21 | obliged to do so. If you do not wish to do so, delete this exception 22 | statement from your version. This exception does not (and cannot) modify any 23 | license terms which apply to the Application, with which you must still 24 | comply. 25 | 26 | 27 | GNU LESSER GENERAL PUBLIC LICENSE 28 | Version 3, 29 June 2007 29 | 30 | Copyright (C) 2007 Free Software Foundation, Inc. 31 | Everyone is permitted to copy and distribute verbatim copies 32 | of this license document, but changing it is not allowed. 33 | 34 | 35 | This version of the GNU Lesser General Public License incorporates 36 | the terms and conditions of version 3 of the GNU General Public 37 | License, supplemented by the additional permissions listed below. 38 | 39 | 0. Additional Definitions. 40 | 41 | As used herein, "this License" refers to version 3 of the GNU Lesser 42 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 43 | General Public License. 44 | 45 | "The Library" refers to a covered work governed by this License, 46 | other than an Application or a Combined Work as defined below. 47 | 48 | An "Application" is any work that makes use of an interface provided 49 | by the Library, but which is not otherwise based on the Library. 50 | Defining a subclass of a class defined by the Library is deemed a mode 51 | of using an interface provided by the Library. 52 | 53 | A "Combined Work" is a work produced by combining or linking an 54 | Application with the Library. The particular version of the Library 55 | with which the Combined Work was made is also called the "Linked 56 | Version". 57 | 58 | The "Minimal Corresponding Source" for a Combined Work means the 59 | Corresponding Source for the Combined Work, excluding any source code 60 | for portions of the Combined Work that, considered in isolation, are 61 | based on the Application, and not on the Linked Version. 62 | 63 | The "Corresponding Application Code" for a Combined Work means the 64 | object code and/or source code for the Application, including any data 65 | and utility programs needed for reproducing the Combined Work from the 66 | Application, but excluding the System Libraries of the Combined Work. 67 | 68 | 1. Exception to Section 3 of the GNU GPL. 69 | 70 | You may convey a covered work under sections 3 and 4 of this License 71 | without being bound by section 3 of the GNU GPL. 72 | 73 | 2. Conveying Modified Versions. 74 | 75 | If you modify a copy of the Library, and, in your modifications, a 76 | facility refers to a function or data to be supplied by an Application 77 | that uses the facility (other than as an argument passed when the 78 | facility is invoked), then you may convey a copy of the modified 79 | version: 80 | 81 | a) under this License, provided that you make a good faith effort to 82 | ensure that, in the event an Application does not supply the 83 | function or data, the facility still operates, and performs 84 | whatever part of its purpose remains meaningful, or 85 | 86 | b) under the GNU GPL, with none of the additional permissions of 87 | this License applicable to that copy. 88 | 89 | 3. Object Code Incorporating Material from Library Header Files. 90 | 91 | The object code form of an Application may incorporate material from 92 | a header file that is part of the Library. You may convey such object 93 | code under terms of your choice, provided that, if the incorporated 94 | material is not limited to numerical parameters, data structure 95 | layouts and accessors, or small macros, inline functions and templates 96 | (ten or fewer lines in length), you do both of the following: 97 | 98 | a) Give prominent notice with each copy of the object code that the 99 | Library is used in it and that the Library and its use are 100 | covered by this License. 101 | 102 | b) Accompany the object code with a copy of the GNU GPL and this license 103 | document. 104 | 105 | 4. Combined Works. 106 | 107 | You may convey a Combined Work under terms of your choice that, 108 | taken together, effectively do not restrict modification of the 109 | portions of the Library contained in the Combined Work and reverse 110 | engineering for debugging such modifications, if you also do each of 111 | the following: 112 | 113 | a) Give prominent notice with each copy of the Combined Work that 114 | the Library is used in it and that the Library and its use are 115 | covered by this License. 116 | 117 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 118 | document. 119 | 120 | c) For a Combined Work that displays copyright notices during 121 | execution, include the copyright notice for the Library among 122 | these notices, as well as a reference directing the user to the 123 | copies of the GNU GPL and this license document. 124 | 125 | d) Do one of the following: 126 | 127 | 0) Convey the Minimal Corresponding Source under the terms of this 128 | License, and the Corresponding Application Code in a form 129 | suitable for, and under terms that permit, the user to 130 | recombine or relink the Application with a modified version of 131 | the Linked Version to produce a modified Combined Work, in the 132 | manner specified by section 6 of the GNU GPL for conveying 133 | Corresponding Source. 134 | 135 | 1) Use a suitable shared library mechanism for linking with the 136 | Library. A suitable mechanism is one that (a) uses at run time 137 | a copy of the Library already present on the user's computer 138 | system, and (b) will operate properly with a modified version 139 | of the Library that is interface-compatible with the Linked 140 | Version. 141 | 142 | e) Provide Installation Information, but only if you would otherwise 143 | be required to provide such information under section 6 of the 144 | GNU GPL, and only to the extent that such information is 145 | necessary to install and execute a modified version of the 146 | Combined Work produced by recombining or relinking the 147 | Application with a modified version of the Linked Version. (If 148 | you use option 4d0, the Installation Information must accompany 149 | the Minimal Corresponding Source and Corresponding Application 150 | Code. If you use option 4d1, you must provide the Installation 151 | Information in the manner specified by section 6 of the GNU GPL 152 | for conveying Corresponding Source.) 153 | 154 | 5. Combined Libraries. 155 | 156 | You may place library facilities that are a work based on the 157 | Library side by side in a single library together with other library 158 | facilities that are not Applications and are not covered by this 159 | License, and convey such a combined library under terms of your 160 | choice, if you do both of the following: 161 | 162 | a) Accompany the combined library with a copy of the same work based 163 | on the Library, uncombined with any other library facilities, 164 | conveyed under the terms of this License. 165 | 166 | b) Give prominent notice with the combined library that part of it 167 | is a work based on the Library, and explaining where to find the 168 | accompanying uncombined form of the same work. 169 | 170 | 6. Revised Versions of the GNU Lesser General Public License. 171 | 172 | The Free Software Foundation may publish revised and/or new versions 173 | of the GNU Lesser General Public License from time to time. Such new 174 | versions will be similar in spirit to the present version, but may 175 | differ in detail to address new problems or concerns. 176 | 177 | Each version is given a distinguishing version number. If the 178 | Library as you received it specifies that a certain numbered version 179 | of the GNU Lesser General Public License "or any later version" 180 | applies to it, you have the option of following the terms and 181 | conditions either of that published version or of any later version 182 | published by the Free Software Foundation. If the Library as you 183 | received it does not specify a version number of the GNU Lesser 184 | General Public License, you may choose any version of the GNU Lesser 185 | General Public License ever published by the Free Software Foundation. 186 | 187 | If the Library as you received it specifies that a proxy can decide 188 | whether future versions of the GNU Lesser General Public License shall 189 | apply, that proxy's public statement of acceptance of any version is 190 | permanent authorization for you to choose that version for the 191 | Library. 192 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # fslock [![GoDoc](https://godoc.org/github.com/juju/fslock?status.svg)](https://godoc.org/github.com/juju/fslock) 3 | fslock provides a cross-process mutex based on file locks that works on windows and *nix platforms. 4 | 5 | 6 | ![fslock](https://cloud.githubusercontent.com/assets/3185864/15507515/f3351498-2199-11e6-9f37-bc59657a9e8c.jpg) 7 | 8 | image: [public domain](https://pixabay.com/en/encrypted-privacy-policy-445155/) 9 | (don't ask) 10 | 11 | 12 | fslock relies on LockFileEx on Windows and flock on \*nix systems. The timeout 13 | feature uses overlapped IO on Windows, but on \*nix platforms, timing out 14 | requires the use of a goroutine that will run until the lock is acquired, 15 | regardless of timeout. If you need to avoid this use of goroutines, poll 16 | TryLock in a loop. 17 | 18 | 19 | 20 | ## Variables 21 | ``` go 22 | var ErrLocked error = trylockError("fslock is already locked") 23 | ``` 24 | ErrLocked indicates TryLock failed because the lock was already locked. 25 | 26 | ``` go 27 | var ErrTimeout error = timeoutError("lock timeout exceeded") 28 | ``` 29 | ErrTimeout indicates that the lock attempt timed out. 30 | 31 | 32 | ## type Lock 33 | ``` go 34 | type Lock struct { 35 | // contains filtered or unexported fields 36 | } 37 | ``` 38 | Lock implements cross-process locks using syscalls. 39 | 40 | 41 | ### func New 42 | ``` go 43 | func New(filename string) *Lock 44 | ``` 45 | New returns a new lock around the given file. 46 | 47 | 48 | ### func (\*Lock) Lock 49 | ``` go 50 | func (l *Lock) Lock() error 51 | ``` 52 | Lock locks the lock. This call will block until the lock is available. 53 | 54 | ### func (\*Lock) LockWithTimeout 55 | ``` go 56 | func (l *Lock) LockWithTimeout(timeout time.Duration) error 57 | ``` 58 | LockWithTimeout tries to lock the lock until the timeout expires. If the 59 | timeout expires, this method will return ErrTimeout. 60 | 61 | ### func (\*Lock) TryLock 62 | ``` go 63 | func (l *Lock) TryLock() error 64 | ``` 65 | TryLock attempts to lock the lock. This method will return ErrLocked 66 | immediately if the lock cannot be acquired. 67 | 68 | ### func (\*Lock) Unlock 69 | ``` go 70 | func (l *Lock) Unlock() error 71 | ``` 72 | Unlock unlocks the lock. 73 | 74 | 75 | -------------------------------------------------------------------------------- /fslock.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Canonical Ltd. 2 | // Licensed under the LGPLv3, see LICENCE file for details. 3 | 4 | // Package fslock provides a cross-process mutex based on file locks. 5 | // 6 | // It is built on top of flock for linux and darwin, and LockFileEx on Windows. 7 | package fslock 8 | 9 | // ErrTimeout indicates that the lock attempt timed out. 10 | var ErrTimeout error = timeoutError("lock timeout exceeded") 11 | 12 | type timeoutError string 13 | 14 | func (t timeoutError) Error() string { 15 | return string(t) 16 | } 17 | func (timeoutError) Timeout() bool { 18 | return true 19 | } 20 | 21 | // ErrLocked indicates TryLock failed because the lock was already locked. 22 | var ErrLocked error = trylockError("fslock is already locked") 23 | 24 | type trylockError string 25 | 26 | func (t trylockError) Error() string { 27 | return string(t) 28 | } 29 | 30 | func (trylockError) Temporary() bool { 31 | return true 32 | } 33 | -------------------------------------------------------------------------------- /fslock_nix.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Canonical Ltd. 2 | // Licensed under the LGPLv3, see LICENCE file for details. 3 | 4 | // +build darwin dragonfly freebsd linux netbsd openbsd 5 | 6 | package fslock 7 | 8 | import ( 9 | "syscall" 10 | "time" 11 | ) 12 | 13 | // Lock implements cross-process locks using syscalls. 14 | // This implementation is based on flock syscall. 15 | type Lock struct { 16 | filename string 17 | fd int 18 | } 19 | 20 | // New returns a new lock around the given file. 21 | func New(filename string) *Lock { 22 | return &Lock{filename: filename} 23 | } 24 | 25 | // Lock locks the lock. This call will block until the lock is available. 26 | func (l *Lock) Lock() error { 27 | if err := l.open(); err != nil { 28 | return err 29 | } 30 | return syscall.Flock(l.fd, syscall.LOCK_EX) 31 | } 32 | 33 | // TryLock attempts to lock the lock. This method will return ErrLocked 34 | // immediately if the lock cannot be acquired. 35 | func (l *Lock) TryLock() error { 36 | if err := l.open(); err != nil { 37 | return err 38 | } 39 | err := syscall.Flock(l.fd, syscall.LOCK_EX|syscall.LOCK_NB) 40 | if err != nil { 41 | syscall.Close(l.fd) 42 | } 43 | if err == syscall.EWOULDBLOCK { 44 | return ErrLocked 45 | } 46 | return err 47 | } 48 | 49 | func (l *Lock) open() error { 50 | fd, err := syscall.Open(l.filename, syscall.O_CREAT|syscall.O_RDONLY, 0600) 51 | if err != nil { 52 | return err 53 | } 54 | l.fd = fd 55 | return nil 56 | } 57 | 58 | // Unlock unlocks the lock. 59 | func (l *Lock) Unlock() error { 60 | return syscall.Close(l.fd) 61 | } 62 | 63 | // LockWithTimeout tries to lock the lock until the timeout expires. If the 64 | // timeout expires, this method will return ErrTimeout. 65 | func (l *Lock) LockWithTimeout(timeout time.Duration) error { 66 | if err := l.open(); err != nil { 67 | return err 68 | } 69 | result := make(chan error) 70 | cancel := make(chan struct{}) 71 | go func() { 72 | err := syscall.Flock(l.fd, syscall.LOCK_EX) 73 | select { 74 | case <-cancel: 75 | // Timed out, cleanup if necessary. 76 | syscall.Flock(l.fd, syscall.LOCK_UN) 77 | syscall.Close(l.fd) 78 | case result <- err: 79 | } 80 | }() 81 | select { 82 | case err := <-result: 83 | return err 84 | case <-time.After(timeout): 85 | close(cancel) 86 | return ErrTimeout 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /fslock_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Canonical Ltd. 2 | // Licensed under the LGPLv3, see LICENCE file for details. 3 | 4 | package fslock_test 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "runtime" 12 | "sync" 13 | "sync/atomic" 14 | "testing" 15 | "time" 16 | 17 | gc "gopkg.in/check.v1" 18 | 19 | "github.com/juju/fslock" 20 | ) 21 | 22 | func Test(t *testing.T) { 23 | gc.TestingT(t) 24 | } 25 | 26 | const ( 27 | shortWait = 10 * time.Millisecond 28 | longWait = 10 * shortWait 29 | ) 30 | 31 | type fslockSuite struct{} 32 | 33 | var _ = gc.Suite(&fslockSuite{}) 34 | 35 | func (s *fslockSuite) TestLockNoContention(c *gc.C) { 36 | path := filepath.Join(c.MkDir(), "testing") 37 | lock := fslock.New(path) 38 | 39 | started := make(chan struct{}) 40 | acquired := make(chan struct{}) 41 | go func() { 42 | close(started) 43 | err := lock.Lock() 44 | close(acquired) 45 | c.Assert(err, gc.IsNil) 46 | }() 47 | 48 | select { 49 | case <-started: 50 | // good, goroutine started. 51 | case <-time.After(shortWait * 2): 52 | c.Fatalf("timeout waiting for goroutine to start") 53 | } 54 | 55 | select { 56 | case <-acquired: 57 | // got the lock. good. 58 | case <-time.After(shortWait * 2): 59 | c.Fatalf("Timed out waiting for lock acquisition.") 60 | } 61 | 62 | err := lock.Unlock() 63 | c.Assert(err, gc.IsNil) 64 | } 65 | 66 | func (s *fslockSuite) TestLockBlocks(c *gc.C) { 67 | path := filepath.Join(c.MkDir(), "testing") 68 | lock := fslock.New(path) 69 | 70 | kill := make(chan struct{}) 71 | 72 | // this will block until the other process has the lock. 73 | procDone := LockFromAnotherProc(c, path, kill) 74 | 75 | defer func() { 76 | close(kill) 77 | // now wait for the other process to exit so the file will be unlocked. 78 | select { 79 | case <-procDone: 80 | case <-time.After(time.Second): 81 | } 82 | }() 83 | 84 | started := make(chan struct{}) 85 | acquired := make(chan struct{}) 86 | go func() { 87 | close(started) 88 | err := lock.Lock() 89 | close(acquired) 90 | lock.Unlock() 91 | c.Assert(err, gc.IsNil) 92 | }() 93 | 94 | select { 95 | case <-started: 96 | // good, goroutine started. 97 | case <-time.After(shortWait * 2): 98 | c.Fatalf("timeout waiting for goroutine to start") 99 | } 100 | 101 | // Waiting for something not to happen is inherently hard... 102 | select { 103 | case <-acquired: 104 | c.Fatalf("Unexpected lock acquisition") 105 | case <-time.After(shortWait * 2): 106 | // all good. 107 | } 108 | } 109 | 110 | func (s *fslockSuite) TestTryLock(c *gc.C) { 111 | lock := fslock.New(filepath.Join(c.MkDir(), "testing")) 112 | 113 | err := lock.TryLock() 114 | c.Assert(err, gc.IsNil) 115 | lock.Unlock() 116 | } 117 | 118 | func (s *fslockSuite) TestTryLockNoBlock(c *gc.C) { 119 | path := filepath.Join(c.MkDir(), "testing") 120 | lock := fslock.New(path) 121 | 122 | kill := make(chan struct{}) 123 | 124 | // this will block until the other process has the lock. 125 | procDone := LockFromAnotherProc(c, path, kill) 126 | 127 | defer func() { 128 | close(kill) 129 | // now wait for the other process to exit so the file will be unlocked. 130 | select { 131 | case <-procDone: 132 | case <-time.After(time.Second): 133 | } 134 | }() 135 | 136 | started := make(chan struct{}) 137 | result := make(chan error) 138 | go func() { 139 | close(started) 140 | result <- lock.TryLock() 141 | }() 142 | 143 | select { 144 | case <-started: 145 | // good, goroutine started. 146 | case <-time.After(shortWait): 147 | c.Fatalf("timeout waiting for goroutine to start") 148 | } 149 | 150 | // Wait for trylock to fail. 151 | select { 152 | case err := <-result: 153 | // yes, I know this is redundant with the assert below, but it makes the 154 | // failed test message clearer. 155 | if err == nil { 156 | c.Fatalf("lock succeeded, but should have errored out") 157 | } 158 | // This should be the error from trylock failing. 159 | c.Assert(err, gc.Equals, fslock.ErrLocked) 160 | case <-time.After(shortWait): 161 | c.Fatalf("took too long to fail trylock") 162 | } 163 | } 164 | 165 | func (s *fslockSuite) TestUnlockedWithTimeout(c *gc.C) { 166 | lock := fslock.New(filepath.Join(c.MkDir(), "testing")) 167 | 168 | err := lock.LockWithTimeout(shortWait) 169 | c.Assert(err, gc.IsNil) 170 | lock.Unlock() 171 | } 172 | 173 | func (s *fslockSuite) TestLockWithTimeout(c *gc.C) { 174 | path := filepath.Join(c.MkDir(), "testing") 175 | lock := fslock.New(path) 176 | defer lock.Unlock() 177 | 178 | kill := make(chan struct{}) 179 | 180 | // this will block until the other process has the lock. 181 | procDone := LockFromAnotherProc(c, path, kill) 182 | 183 | defer func() { 184 | close(kill) 185 | // now wait for the other process to exit so the file will be unlocked. 186 | select { 187 | case <-procDone: 188 | case <-time.After(time.Second): 189 | } 190 | }() 191 | 192 | started := make(chan struct{}) 193 | result := make(chan error) 194 | go func() { 195 | close(started) 196 | result <- lock.LockWithTimeout(shortWait) 197 | }() 198 | 199 | select { 200 | case <-started: 201 | // good, goroutine started. 202 | case <-time.After(shortWait * 2): 203 | c.Fatalf("timeout waiting for goroutine to start") 204 | } 205 | 206 | // Wait for timeout. 207 | select { 208 | case err := <-result: 209 | // yes, I know this is redundant with the assert below, but it makes the 210 | // failed test message clearer. 211 | if err == nil { 212 | c.Fatalf("lock succeeded, but should have timed out") 213 | } 214 | // This should be the error from the lock timing out. 215 | c.Assert(err, gc.Equals, fslock.ErrTimeout) 216 | case <-time.After(shortWait * 2): 217 | c.Fatalf("lock took too long to timeout") 218 | } 219 | } 220 | 221 | func (s *fslockSuite) TestStress(c *gc.C) { 222 | const lockAttempts = 200 223 | const concurrentLocks = 10 224 | 225 | var counter = new(int64) 226 | // Use atomics to update lockState to make sure the lock isn't held by 227 | // someone else. A value of 1 means locked, 0 means unlocked. 228 | var lockState = new(int32) 229 | 230 | var wg sync.WaitGroup 231 | 232 | dir := c.MkDir() 233 | 234 | var stress = func(name string) { 235 | defer wg.Done() 236 | lock := fslock.New(filepath.Join(dir, "testing")) 237 | for i := 0; i < lockAttempts; i++ { 238 | err := lock.Lock() 239 | c.Assert(err, gc.IsNil) 240 | state := atomic.AddInt32(lockState, 1) 241 | c.Assert(state, gc.Equals, int32(1)) 242 | // Tell the go routine scheduler to give a slice to someone else 243 | // while we have this locked. 244 | runtime.Gosched() 245 | // need to decrement prior to unlock to avoid the race of someone 246 | // else grabbing the lock before we decrement the state. 247 | atomic.AddInt32(lockState, -1) 248 | err = lock.Unlock() 249 | c.Assert(err, gc.IsNil) 250 | // increment the general counter 251 | atomic.AddInt64(counter, 1) 252 | } 253 | } 254 | 255 | for i := 0; i < concurrentLocks; i++ { 256 | wg.Add(1) 257 | go stress(fmt.Sprintf("Lock %d", i)) 258 | } 259 | wg.Wait() 260 | c.Assert(*counter, gc.Equals, int64(lockAttempts*concurrentLocks)) 261 | } 262 | 263 | // LockFromAnotherProc will launch a process and block until that process has 264 | // created the lock file. If we time out waiting for the other process to take 265 | // the lock, this function will fail the current test. 266 | func LockFromAnotherProc(c *gc.C, path string, kill chan struct{}) (done chan struct{}) { 267 | cmd := exec.Command(os.Args[0], "-test.run", "TestLockFromOtherProcess") 268 | cmd.Env = append( 269 | // We must preserve os.Environ() on Windows, 270 | // or the subprocess will fail in weird and 271 | // wonderful ways. 272 | os.Environ(), 273 | "FSLOCK_TEST_HELPER_WANTED=1", 274 | "FSLOCK_TEST_HELPER_PATH="+path, 275 | ) 276 | 277 | if err := cmd.Start(); err != nil { 278 | c.Fatalf("error starting other proc: %v", err) 279 | } 280 | 281 | done = make(chan struct{}) 282 | 283 | go func() { 284 | cmd.Wait() 285 | close(done) 286 | }() 287 | 288 | go func() { 289 | select { 290 | case <-kill: 291 | // this may fail, but there's not much we can do about it 292 | _ = cmd.Process.Kill() 293 | case <-done: 294 | } 295 | }() 296 | 297 | for x := 0; x < 10; x++ { 298 | time.Sleep(shortWait) 299 | if _, err := os.Stat(path); err == nil { 300 | // file created by other process, let's continue 301 | break 302 | } 303 | if x == 9 { 304 | c.Fatalf("timed out waiting for other process to start") 305 | } 306 | } 307 | return done 308 | } 309 | 310 | func TestLockFromOtherProcess(t *testing.T) { 311 | if os.Getenv("FSLOCK_TEST_HELPER_WANTED") == "" { 312 | return 313 | } 314 | filename := os.Getenv("FSLOCK_TEST_HELPER_PATH") 315 | lock := fslock.New(filename) 316 | err := lock.Lock() 317 | if err != nil { 318 | fmt.Fprintf(os.Stderr, "error locking %q: %v", filename, err) 319 | os.Exit(1) 320 | } 321 | time.Sleep(longWait) 322 | err = lock.Unlock() 323 | if err != nil { 324 | fmt.Fprintf(os.Stderr, "error unlocking %q: %v", filename, err) 325 | os.Exit(1) 326 | } 327 | os.Exit(0) 328 | } 329 | -------------------------------------------------------------------------------- /fslock_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Canonical Ltd. 2 | // Licensed under the LGPLv3, see LICENCE file for details. 3 | 4 | package fslock 5 | 6 | import ( 7 | "log" 8 | "syscall" 9 | "time" 10 | "unsafe" 11 | ) 12 | 13 | var ( 14 | modkernel32 = syscall.NewLazyDLL("kernel32.dll") 15 | procLockFileEx = modkernel32.NewProc("LockFileEx") 16 | procCreateEventW = modkernel32.NewProc("CreateEventW") 17 | ) 18 | 19 | const ( 20 | lockfileExclusiveLock = 2 21 | fileFlagNormal = 0x00000080 22 | ) 23 | 24 | func init() { 25 | log.SetFlags(log.Lmicroseconds | log.Ldate) 26 | } 27 | 28 | // Lock implements cross-process locks using syscalls. 29 | // This implementation is based on LockFileEx syscall. 30 | type Lock struct { 31 | filename string 32 | handle syscall.Handle 33 | } 34 | 35 | // New returns a new lock around the given file. 36 | func New(filename string) *Lock { 37 | return &Lock{filename: filename} 38 | } 39 | 40 | // TryLock attempts to lock the lock. This method will return ErrLocked 41 | // immediately if the lock cannot be acquired. 42 | func (l *Lock) TryLock() error { 43 | err := l.LockWithTimeout(0) 44 | if err == ErrTimeout { 45 | // in our case, timing out immediately just means it was already locked. 46 | return ErrLocked 47 | } 48 | return err 49 | } 50 | 51 | // Lock locks the lock. This call will block until the lock is available. 52 | func (l *Lock) Lock() error { 53 | return l.LockWithTimeout(-1) 54 | } 55 | 56 | // Unlock unlocks the lock. 57 | func (l *Lock) Unlock() error { 58 | return syscall.Close(l.handle) 59 | } 60 | 61 | // LockWithTimeout tries to lock the lock until the timeout expires. If the 62 | // timeout expires, this method will return ErrTimeout. 63 | func (l *Lock) LockWithTimeout(timeout time.Duration) (err error) { 64 | name, err := syscall.UTF16PtrFromString(l.filename) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | // Open for asynchronous I/O so that we can timeout waiting for the lock. 70 | // Also open shared so that other processes can open the file (but will 71 | // still need to lock it). 72 | handle, err := syscall.CreateFile( 73 | name, 74 | syscall.GENERIC_READ, 75 | syscall.FILE_SHARE_READ, 76 | nil, 77 | syscall.OPEN_ALWAYS, 78 | syscall.FILE_FLAG_OVERLAPPED|fileFlagNormal, 79 | 0) 80 | if err != nil { 81 | return err 82 | } 83 | l.handle = handle 84 | defer func() { 85 | if err != nil { 86 | syscall.Close(handle) 87 | } 88 | }() 89 | 90 | millis := uint32(syscall.INFINITE) 91 | if timeout >= 0 { 92 | millis = uint32(timeout.Nanoseconds() / 1000000) 93 | } 94 | 95 | ol, err := newOverlapped() 96 | if err != nil { 97 | return err 98 | } 99 | defer syscall.CloseHandle(ol.HEvent) 100 | err = lockFileEx(handle, lockfileExclusiveLock, 0, 1, 0, ol) 101 | if err == nil { 102 | return nil 103 | } 104 | 105 | // ERROR_IO_PENDING is expected when we're waiting on an asychronous event 106 | // to occur. 107 | if err != syscall.ERROR_IO_PENDING { 108 | return err 109 | } 110 | s, err := syscall.WaitForSingleObject(ol.HEvent, millis) 111 | 112 | switch s { 113 | case syscall.WAIT_OBJECT_0: 114 | // success! 115 | return nil 116 | case syscall.WAIT_TIMEOUT: 117 | return ErrTimeout 118 | default: 119 | return err 120 | } 121 | } 122 | 123 | // newOverlapped creates a structure used to track asynchronous 124 | // I/O requests that have been issued. 125 | func newOverlapped() (*syscall.Overlapped, error) { 126 | event, err := createEvent(nil, true, false, nil) 127 | if err != nil { 128 | return nil, err 129 | } 130 | return &syscall.Overlapped{HEvent: event}, nil 131 | } 132 | 133 | func lockFileEx(h syscall.Handle, flags, reserved, locklow, lockhigh uint32, ol *syscall.Overlapped) (err error) { 134 | r1, _, e1 := syscall.Syscall6(procLockFileEx.Addr(), 6, uintptr(h), uintptr(flags), uintptr(reserved), uintptr(locklow), uintptr(lockhigh), uintptr(unsafe.Pointer(ol))) 135 | if r1 == 0 { 136 | if e1 != 0 { 137 | err = error(e1) 138 | } else { 139 | err = syscall.EINVAL 140 | } 141 | } 142 | return 143 | } 144 | 145 | func createEvent(sa *syscall.SecurityAttributes, manualReset bool, initialState bool, name *uint16) (handle syscall.Handle, err error) { 146 | var _p0 uint32 147 | if manualReset { 148 | _p0 = 1 149 | } 150 | var _p1 uint32 151 | if initialState { 152 | _p1 = 1 153 | } 154 | 155 | r0, _, e1 := syscall.Syscall6(procCreateEventW.Addr(), 4, uintptr(unsafe.Pointer(sa)), uintptr(_p0), uintptr(_p1), uintptr(unsafe.Pointer(name)), 0, 0) 156 | handle = syscall.Handle(r0) 157 | if handle == syscall.InvalidHandle { 158 | if e1 != 0 { 159 | err = error(e1) 160 | } else { 161 | err = syscall.EINVAL 162 | } 163 | } 164 | return 165 | } 166 | --------------------------------------------------------------------------------