├── .github └── workflows │ └── go.yml ├── LICENSE ├── README.md ├── filemutex.go ├── filemutex_flock.go ├── filemutex_test.go ├── filemutex_windows.go ├── go.mod └── go.sum /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | linux_build_and_test: 11 | name: Build and test (Linux) 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | go: ['1.19', '1.20', '1.21'] 17 | os: ['ubuntu-latest', 'windows-latest', 'macos-latest'] 18 | steps: 19 | - id: go 20 | name: Set up Go 21 | uses: actions/setup-go@v1 22 | with: 23 | go-version: ${{ matrix.go }} 24 | - name: Checkout 25 | uses: actions/checkout@v2 26 | - name: Build 27 | run: go build -v . 28 | - name: Test 29 | run: go test -v . 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2010-2017 Alex Flint. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FileMutex 2 | 3 | FileMutex is similar to `sync.RWMutex`, but also synchronizes across processes. 4 | On Linux, OSX, and other POSIX systems it uses the flock system call. On windows 5 | it uses the LockFileEx and UnlockFileEx system calls. 6 | 7 | ```go 8 | import ( 9 | "log" 10 | "github.com/alexflint/go-filemutex" 11 | ) 12 | 13 | func main() { 14 | m, err := filemutex.New("/tmp/foo.lock") 15 | if err != nil { 16 | log.Fatalln("Directory did not exist or file could not created") 17 | } 18 | 19 | m.Lock() // Will block until lock can be acquired 20 | 21 | // Code here is protected by the mutex 22 | 23 | m.Unlock() 24 | } 25 | ``` 26 | 27 | ### Installation 28 | 29 | go get github.com/alexflint/go-filemutex 30 | 31 | Forked from https://github.com/golang/build/tree/master/cmd/builder/filemutex_*.go 32 | -------------------------------------------------------------------------------- /filemutex.go: -------------------------------------------------------------------------------- 1 | package filemutex 2 | 3 | import "errors" 4 | 5 | var AlreadyLocked = errors.New("lock already acquired") 6 | -------------------------------------------------------------------------------- /filemutex_flock.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build darwin dragonfly freebsd linux netbsd openbsd solaris 6 | 7 | package filemutex 8 | 9 | import "golang.org/x/sys/unix" 10 | 11 | const ( 12 | mkdirPerm = 0750 13 | ) 14 | 15 | // FileMutex is similar to sync.RWMutex, but also synchronizes across processes. 16 | // This implementation is based on flock syscall. 17 | type FileMutex struct { 18 | fd int 19 | } 20 | 21 | func New(filename string) (*FileMutex, error) { 22 | fd, err := unix.Open(filename, unix.O_CREAT|unix.O_RDONLY, mkdirPerm) 23 | if err != nil { 24 | return nil, err 25 | } 26 | return &FileMutex{fd: fd}, nil 27 | } 28 | 29 | func (m *FileMutex) Lock() error { 30 | return unix.Flock(m.fd, unix.LOCK_EX) 31 | } 32 | 33 | func (m *FileMutex) TryLock() error { 34 | if err := unix.Flock(m.fd, unix.LOCK_EX|unix.LOCK_NB); err != nil { 35 | if errno, ok := err.(unix.Errno); ok { 36 | if errno == unix.EWOULDBLOCK { 37 | return AlreadyLocked 38 | } 39 | } 40 | return err 41 | } 42 | return nil 43 | } 44 | 45 | func (m *FileMutex) Unlock() error { 46 | return unix.Flock(m.fd, unix.LOCK_UN) 47 | } 48 | 49 | func (m *FileMutex) RLock() error { 50 | return unix.Flock(m.fd, unix.LOCK_SH) 51 | } 52 | 53 | func (m *FileMutex) RUnlock() error { 54 | return unix.Flock(m.fd, unix.LOCK_UN) 55 | } 56 | 57 | // Close unlocks the lock and closes the underlying file descriptor. 58 | func (m *FileMutex) Close() error { 59 | if err := unix.Flock(m.fd, unix.LOCK_UN); err != nil { 60 | return err 61 | } 62 | return unix.Close(m.fd) 63 | } 64 | -------------------------------------------------------------------------------- /filemutex_test.go: -------------------------------------------------------------------------------- 1 | package filemutex 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestLockUnlock(t *testing.T) { 13 | dir, err := ioutil.TempDir("", "") 14 | require.NoError(t, err) 15 | defer os.RemoveAll(dir) 16 | 17 | path := filepath.Join(dir, "x") 18 | m, err := New(path) 19 | require.NoError(t, err) 20 | 21 | err = m.Lock() 22 | require.NoError(t, err) 23 | err = m.Unlock() 24 | require.NoError(t, err) 25 | } 26 | 27 | func TestTryLockUnlock(t *testing.T) { 28 | dir, err := ioutil.TempDir("", "") 29 | require.NoError(t, err) 30 | defer os.RemoveAll(dir) 31 | 32 | path := filepath.Join(dir, "x") 33 | m, err := New(path) 34 | require.NoError(t, err) 35 | m2, err := New(path) 36 | require.NoError(t, err) 37 | 38 | err = m.Lock() 39 | require.NoError(t, err) 40 | err = m2.TryLock() 41 | require.Equal(t, AlreadyLocked, err) 42 | err = m.Unlock() 43 | require.NoError(t, err) 44 | err = m2.TryLock() 45 | require.NoError(t, err) 46 | } 47 | 48 | func TestRLockUnlock(t *testing.T) { 49 | dir, err := ioutil.TempDir("", "") 50 | require.NoError(t, err) 51 | defer os.RemoveAll(dir) 52 | 53 | path := filepath.Join(dir, "x") 54 | m, err := New(path) 55 | require.NoError(t, err) 56 | 57 | err = m.RLock() 58 | require.NoError(t, err) 59 | err = m.RUnlock() 60 | require.NoError(t, err) 61 | } 62 | 63 | func TestClose(t *testing.T) { 64 | dir, err := ioutil.TempDir("", "") 65 | require.NoError(t, err) 66 | defer os.RemoveAll(dir) 67 | 68 | path := filepath.Join(dir, "x") 69 | m, err := New(path) 70 | require.NoError(t, err) 71 | 72 | m.Lock() 73 | m.Close() 74 | } 75 | 76 | func TestOnlyClose(t *testing.T) { 77 | dir, err := ioutil.TempDir("", "") 78 | require.NoError(t, err) 79 | defer os.RemoveAll(dir) 80 | 81 | path := filepath.Join(dir, "x") 82 | m, err := New(path) 83 | require.NoError(t, err) 84 | 85 | require.NoError(t, m.Close()) 86 | } 87 | 88 | func TestLockErrorsAreRecoverable(t *testing.T) { 89 | dir, err := ioutil.TempDir("", "") 90 | require.NoError(t, err) 91 | defer os.RemoveAll(dir) 92 | 93 | path := filepath.Join(dir, "x") 94 | m, err := New(path) 95 | require.NoError(t, err) 96 | 97 | // muck with the internal state in order to cause an error 98 | oldfd := m.fd 99 | m.fd = 99999 100 | 101 | // trigger an error 102 | err = m.Lock() 103 | require.Error(t, err) 104 | 105 | // restore a sane internal state 106 | m.fd = oldfd 107 | 108 | // this would hang if we hadn't Unlock()ed in the Lock error branch 109 | err = m.Lock() 110 | require.NoError(t, err) 111 | 112 | // clean up 113 | err = m.Unlock() 114 | require.NoError(t, err) 115 | } 116 | 117 | func TestUnlockErrorsAreRecoverable(t *testing.T) { 118 | dir, err := ioutil.TempDir("", "") 119 | require.NoError(t, err) 120 | defer os.RemoveAll(dir) 121 | 122 | path := filepath.Join(dir, "x") 123 | m, err := New(path) 124 | require.NoError(t, err) 125 | 126 | err = m.Lock() 127 | require.NoError(t, err) 128 | 129 | // muck with the internal state in order to cause an error 130 | oldfd := m.fd 131 | m.fd = 99999 132 | 133 | // trigger an error 134 | err = m.Unlock() 135 | require.Error(t, err) 136 | 137 | // restore a sane internal state 138 | m.fd = oldfd 139 | 140 | // this would crash if we the mutex were unlocked in the error branch 141 | err = m.Unlock() 142 | require.NoError(t, err) 143 | } 144 | 145 | func TestRLockErrorsAreRecoverable(t *testing.T) { 146 | dir, err := ioutil.TempDir("", "") 147 | require.NoError(t, err) 148 | defer os.RemoveAll(dir) 149 | 150 | path := filepath.Join(dir, "x") 151 | m, err := New(path) 152 | require.NoError(t, err) 153 | 154 | // muck with the internal state in order to cause an error 155 | oldfd := m.fd 156 | m.fd = 99999 157 | 158 | // trigger an error 159 | err = m.RLock() 160 | require.Error(t, err) 161 | 162 | // restore a sane internal state 163 | m.fd = oldfd 164 | 165 | // this would hang if we hadn't Unlock()ed in the RLock error branch 166 | err = m.Lock() 167 | require.NoError(t, err) 168 | 169 | // clean up 170 | err = m.Unlock() 171 | require.NoError(t, err) 172 | } 173 | 174 | func TestRUnlockErrorsAreRecoverable(t *testing.T) { 175 | dir, err := ioutil.TempDir("", "") 176 | require.NoError(t, err) 177 | defer os.RemoveAll(dir) 178 | 179 | path := filepath.Join(dir, "x") 180 | m, err := New(path) 181 | require.NoError(t, err) 182 | 183 | err = m.RLock() 184 | require.NoError(t, err) 185 | 186 | // muck with the internal state in order to cause an error 187 | oldfd := m.fd 188 | m.fd = 99999 189 | 190 | // trigger an error 191 | err = m.RUnlock() 192 | require.Error(t, err) 193 | 194 | // restore a sane internal state 195 | m.fd = oldfd 196 | 197 | // this would crash if we the mutex were unlocked in the error branch 198 | err = m.RUnlock() 199 | require.NoError(t, err) 200 | } 201 | -------------------------------------------------------------------------------- /filemutex_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package filemutex 6 | 7 | import ( 8 | "golang.org/x/sys/windows" 9 | ) 10 | 11 | // FileMutex is similar to sync.RWMutex, but also synchronizes across processes. 12 | // This implementation is based on flock syscall. 13 | type FileMutex struct { 14 | fd windows.Handle 15 | } 16 | 17 | func New(filename string) (*FileMutex, error) { 18 | fd, err := windows.CreateFile(&(windows.StringToUTF16(filename)[0]), windows.GENERIC_READ|windows.GENERIC_WRITE, 19 | windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE, nil, windows.OPEN_ALWAYS, windows.FILE_ATTRIBUTE_NORMAL, 0) 20 | if err != nil { 21 | return nil, err 22 | } 23 | return &FileMutex{fd: fd}, nil 24 | } 25 | 26 | func (m *FileMutex) TryLock() error { 27 | if err := windows.LockFileEx(m.fd, windows.LOCKFILE_FAIL_IMMEDIATELY|windows.LOCKFILE_EXCLUSIVE_LOCK, 0, 1, 0, &windows.Overlapped{}); err != nil { 28 | if errno, ok := err.(windows.Errno); ok { 29 | if errno == windows.ERROR_LOCK_VIOLATION { 30 | return AlreadyLocked 31 | } 32 | } 33 | return err 34 | } 35 | return nil 36 | } 37 | 38 | func (m *FileMutex) Lock() error { 39 | return windows.LockFileEx(m.fd, windows.LOCKFILE_EXCLUSIVE_LOCK, 0, 1, 0, &windows.Overlapped{}) 40 | } 41 | 42 | func (m *FileMutex) Unlock() error { 43 | return windows.UnlockFileEx(m.fd, 0, 1, 0, &windows.Overlapped{}) 44 | } 45 | 46 | func (m *FileMutex) RLock() error { 47 | return windows.LockFileEx(m.fd, 0, 0, 1, 0, &windows.Overlapped{}) 48 | } 49 | 50 | func (m *FileMutex) RUnlock() error { 51 | return windows.UnlockFileEx(m.fd, 0, 1, 0, &windows.Overlapped{}) 52 | } 53 | 54 | // Close unlocks the lock and closes the underlying file descriptor. 55 | func (m *FileMutex) Close() error { 56 | // See comment section of https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-lockfileex 57 | // It's recommended to unlock a file explicitly before closing in order to 58 | // avoid delays, but all locks are definitly unlocked when closing a file. 59 | // So any unlocking error can be ignored. 60 | _ = windows.UnlockFileEx(m.fd, 0, 1, 0, &windows.Overlapped{}) 61 | 62 | return windows.Close(m.fd) 63 | } 64 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/alexflint/go-filemutex 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/stretchr/testify v1.4.0 7 | golang.org/x/sys v0.16.0 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 6 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 7 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 8 | golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio= 9 | golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 10 | golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= 11 | golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 12 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 13 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 14 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 15 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 16 | --------------------------------------------------------------------------------