summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKamil Trzciński <ayufan@ayufan.eu>2019-07-30 13:59:21 +0000
committerKamil Trzciński <ayufan@ayufan.eu>2019-07-30 13:59:21 +0000
commitc19e7f224070016cb61f2877b4f50fc7787a29a3 (patch)
tree6e4bc753ec674705f90ed5e60d6ca628160e6d03
parentMerge branch 'windows-sh' into 'master' (diff)
parentPrevent running multiple instances of the gitlab-runner process using the sam... (diff)
downloadgitlab-runner-c19e7f224070016cb61f2877b4f50fc7787a29a3.tar.gz
Merge branch '4407-prevent-running-multiple-runner-process-instances' into 'master'
Prevent running multiple instances of the gitlab-runner process using the same configuration file Closes #4407 See merge request gitlab-org/gitlab-runner!1496
-rw-r--r--Gopkg.lock9
-rw-r--r--Gopkg.toml4
-rw-r--r--Makefile1
-rw-r--r--commands/config.go5
-rw-r--r--commands/multi.go17
-rw-r--r--commands/multi_test.go77
-rw-r--r--helpers/fslocker/errors.go17
-rw-r--r--helpers/fslocker/fslocker.go38
-rw-r--r--helpers/fslocker/fslocker_test.go110
-rw-r--r--helpers/fslocker/mock_locker.go47
-rw-r--r--vendor/github.com/gofrs/flock/LICENSE27
-rw-r--r--vendor/github.com/gofrs/flock/flock.go127
-rw-r--r--vendor/github.com/gofrs/flock/flock_example_test.go72
-rw-r--r--vendor/github.com/gofrs/flock/flock_test.go290
-rw-r--r--vendor/github.com/gofrs/flock/flock_unix.go195
-rw-r--r--vendor/github.com/gofrs/flock/flock_winapi.go76
-rw-r--r--vendor/github.com/gofrs/flock/flock_windows.go140
17 files changed, 1250 insertions, 2 deletions
diff --git a/Gopkg.lock b/Gopkg.lock
index daaefe7c..2afd27df 100644
--- a/Gopkg.lock
+++ b/Gopkg.lock
@@ -275,6 +275,14 @@
revision = "20b96f641a5ea98f2f8619ff4f3e061cff4833bd"
[[projects]]
+ branch = "master"
+ digest = "1:e27d558706a107ae82e91ac3010bb15d3978faab98ded7435d640e28007e611c"
+ name = "github.com/gofrs/flock"
+ packages = ["."]
+ pruneopts = "N"
+ revision = "5135e617513b1e6e205a3a89b042249dee6730c8"
+
+[[projects]]
digest = "1:809e332c8b79399d55f1fd11f5e45b892bc0fb25c7c912898ad307a8d0506164"
name = "github.com/gogo/protobuf"
packages = [
@@ -1052,6 +1060,7 @@
"github.com/docker/go-units",
"github.com/docker/machine/commands/mcndirs",
"github.com/getsentry/raven-go",
+ "github.com/gofrs/flock",
"github.com/golang/mock/gomock",
"github.com/gorhill/cronexpr",
"github.com/gorilla/mux",
diff --git a/Gopkg.toml b/Gopkg.toml
index b1ae0ff2..d4939268 100644
--- a/Gopkg.toml
+++ b/Gopkg.toml
@@ -162,6 +162,10 @@ ignored = ["test", "appengine"]
name = "golang.org/x/sys"
branch = "master"
+[[constraint]]
+ name = "github.com/gofrs/flock"
+ branch = "master"
+
##
## Refrain innovations ;)
##
diff --git a/Makefile b/Makefile
index a3c5bb2b..5820417c 100644
--- a/Makefile
+++ b/Makefile
@@ -146,6 +146,7 @@ mocks: $(MOCKERY)
GOPATH=$(ORIGINAL_GOPATH) mockery $(MOCKERY_FLAGS) -dir=./vendor/github.com/ayufan/golang-kardianos-service -output=./helpers/service/mocks -name='(Interface)'
GOPATH=$(ORIGINAL_GOPATH) mockery $(MOCKERY_FLAGS) -dir=./helpers/docker -all -inpkg
GOPATH=$(ORIGINAL_GOPATH) mockery $(MOCKERY_FLAGS) -dir=./helpers/certificate -all -inpkg
+ GOPATH=$(ORIGINAL_GOPATH) mockery $(MOCKERY_FLAGS) -dir=./helpers/fslocker -all -inpkg
GOPATH=$(ORIGINAL_GOPATH) mockery $(MOCKERY_FLAGS) -dir=./executors/docker -all -inpkg
GOPATH=$(ORIGINAL_GOPATH) mockery $(MOCKERY_FLAGS) -dir=./executors/custom -all -inpkg
GOPATH=$(ORIGINAL_GOPATH) mockery $(MOCKERY_FLAGS) -dir=./cache -all -inpkg
diff --git a/commands/config.go b/commands/config.go
index 7f2eddca..dd176437 100644
--- a/commands/config.go
+++ b/commands/config.go
@@ -10,6 +10,7 @@ import (
"github.com/sirupsen/logrus"
"gitlab.com/gitlab-org/gitlab-runner/common"
+ "gitlab.com/gitlab-org/gitlab-runner/helpers/fslocker"
"gitlab.com/gitlab-org/gitlab-runner/network"
)
@@ -41,6 +42,10 @@ func (c *configOptions) loadConfig() error {
return nil
}
+func (c *configOptions) inLock(fn func()) error {
+ return fslocker.InLock(c.ConfigFile, fn)
+}
+
func (c *configOptions) RunnerByName(name string) (*common.RunnerConfig, error) {
if c.config == nil {
return nil, fmt.Errorf("config has not been loaded")
diff --git a/commands/multi.go b/commands/multi.go
index d272ab08..e67eccc4 100644
--- a/commands/multi.go
+++ b/commands/multi.go
@@ -403,7 +403,7 @@ func (mr *RunCommand) Start(s service.Service) error {
}
// Start should not block. Do the actual work async.
- go mr.Run()
+ go mr.RunWithLock()
return nil
}
@@ -575,6 +575,19 @@ func (mr *RunCommand) setupSessionServer() {
Info("Session server listening")
}
+func (mr *RunCommand) RunWithLock() {
+ log := mr.log().WithFields(logrus.Fields{
+ "file": mr.ConfigFile,
+ "pid": os.Getpid(),
+ })
+ log.Info("Locking configuration file")
+
+ err := mr.inLock(mr.Run)
+ if err != nil {
+ log.WithError(err).Fatal("Could not handle configuration file locking")
+ }
+}
+
func (mr *RunCommand) Run() {
mr.setupMetricsAndDebugServer()
mr.setupSessionServer()
@@ -609,7 +622,7 @@ func (mr *RunCommand) Run() {
mr.currentWorkers--
}
mr.log().Println("All workers stopped. Can exit now")
- mr.runFinished <- true
+ close(mr.runFinished)
}
func (mr *RunCommand) interruptRun() {
diff --git a/commands/multi_test.go b/commands/multi_test.go
index 0d635967..0f1c1a79 100644
--- a/commands/multi_test.go
+++ b/commands/multi_test.go
@@ -1,14 +1,20 @@
package commands
import (
+ "io/ioutil"
+ "os"
"sync"
"sync/atomic"
"testing"
"time"
+ "github.com/gofrs/flock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
+ "github.com/stretchr/testify/require"
+
"gitlab.com/gitlab-org/gitlab-runner/common"
+ "gitlab.com/gitlab-org/gitlab-runner/helpers"
)
func TestProcessRunner_BuildLimit(t *testing.T) {
@@ -110,3 +116,74 @@ func TestProcessRunner_BuildLimit(t *testing.T) {
// Wait for all builds to finish.
wg.Wait()
}
+
+func runFileLockingCmd(t *testing.T, wg *sync.WaitGroup, started chan bool, stop chan bool, filePath string, shouldPanic bool) {
+ cmd := &RunCommand{
+ configOptionsWithListenAddress: configOptionsWithListenAddress{
+ configOptions: configOptions{
+ ConfigFile: filePath,
+ config: &common.Config{
+ Concurrent: 5,
+ },
+ },
+ },
+ stopSignals: make(chan os.Signal),
+ reloadSignal: make(chan os.Signal, 1),
+ runFinished: make(chan bool, 1),
+ }
+
+ go func() {
+ if shouldPanic {
+ assert.Panics(t, cmd.RunWithLock, "Expected the Runner to create a new lock")
+ } else {
+ assert.NotPanics(t, cmd.RunWithLock, "Expected the Runner to reject creating a new lock")
+ }
+ wg.Done()
+ }()
+
+ close(started)
+
+ <-stop
+ cmd.stopSignal = os.Kill
+}
+
+func TestMulti_RunWithLock(t *testing.T) {
+ defer helpers.MakeFatalToPanic()()
+
+ file, err := ioutil.TempFile("", "config.toml")
+ require.NoError(t, err)
+
+ err = file.Close()
+ require.NoError(t, err)
+
+ filePath := file.Name()
+
+ wg := new(sync.WaitGroup)
+ stop := make(chan bool)
+
+ wg.Add(2)
+
+ started := make(chan bool)
+ go runFileLockingCmd(t, wg, started, stop, filePath, false)
+ <-started
+
+ time.Sleep(1 * time.Second)
+
+ started = make(chan bool)
+ go runFileLockingCmd(t, wg, started, stop, filePath, true)
+ <-started
+
+ time.Sleep(1 * time.Second)
+
+ close(stop)
+ wg.Wait()
+
+ // Try to lock the file to check if it was properly unlocked while
+ // finishing cmd.RunWithLock() call
+ fl := flock.New(filePath)
+ locked, err := fl.TryLock()
+ defer fl.Unlock()
+
+ assert.True(t, locked, "File was not unlocked!")
+ assert.NoError(t, err)
+}
diff --git a/helpers/fslocker/errors.go b/helpers/fslocker/errors.go
new file mode 100644
index 00000000..fe7b57e3
--- /dev/null
+++ b/helpers/fslocker/errors.go
@@ -0,0 +1,17 @@
+package fslocker
+
+import (
+ "github.com/pkg/errors"
+)
+
+func errFileInUse(path string) error {
+ return errors.Errorf("file %q is locked by another process", path)
+}
+
+func errCantAcquireLock(inner error, path string) error {
+ return errors.Wrapf(inner, "can't acquire file lock on %q file", path)
+}
+
+func errCantReleaseLock(inner error, path string) error {
+ return errors.Wrapf(inner, "can't release file lock on %q file", path)
+}
diff --git a/helpers/fslocker/fslocker.go b/helpers/fslocker/fslocker.go
new file mode 100644
index 00000000..f8ae41ba
--- /dev/null
+++ b/helpers/fslocker/fslocker.go
@@ -0,0 +1,38 @@
+package fslocker
+
+import (
+ "github.com/gofrs/flock"
+)
+
+type locker interface {
+ TryLock() (bool, error)
+ Unlock() error
+}
+
+var defaultLocker = func(path string) locker {
+ return flock.New(path)
+}
+
+func InLock(filePath string, fn func()) (err error) {
+ locker := defaultLocker(filePath)
+
+ lock, err := locker.TryLock()
+ if err != nil {
+ return errCantAcquireLock(err, filePath)
+ }
+
+ if !lock {
+ return errFileInUse(filePath)
+ }
+
+ defer func() {
+ unlockErr := locker.Unlock()
+ if unlockErr != nil {
+ err = errCantReleaseLock(unlockErr, filePath)
+ }
+ }()
+
+ fn()
+
+ return
+}
diff --git a/helpers/fslocker/fslocker_test.go b/helpers/fslocker/fslocker_test.go
new file mode 100644
index 00000000..91f54838
--- /dev/null
+++ b/helpers/fslocker/fslocker_test.go
@@ -0,0 +1,110 @@
+package fslocker
+
+import (
+ "errors"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/mock"
+)
+
+type mockFn struct {
+ mock.Mock
+}
+
+func (l *mockFn) Run() {
+ l.Called()
+}
+
+func mockDefaultLocker(t *testing.T, expectedPath string) (*mockLocker, func()) {
+ fsLockerMock := new(mockLocker)
+
+ oldLocker := defaultLocker
+ cleanup := func() {
+ defaultLocker = oldLocker
+ fsLockerMock.AssertExpectations(t)
+ }
+
+ defaultLocker = func(path string) locker {
+ assert.Equal(t, expectedPath, path)
+
+ return fsLockerMock
+ }
+
+ return fsLockerMock, cleanup
+}
+
+func TestInLock(t *testing.T) {
+ filePath := "/some/path/to/config/file"
+ testError := errors.New("test-error")
+
+ tests := map[string]struct {
+ fileLocker locker
+ prepareMockAssertions func(locker *mockLocker, fn *mockFn)
+ expectedError error
+ }{
+ "error on file locking": {
+ prepareMockAssertions: func(locker *mockLocker, fn *mockFn) {
+ locker.On("TryLock").
+ Return(true, testError).
+ Once()
+ },
+ expectedError: errCantAcquireLock(testError, filePath),
+ },
+ "can't lock the file": {
+ prepareMockAssertions: func(locker *mockLocker, fn *mockFn) {
+ locker.On("TryLock").
+ Return(false, nil).
+ Once()
+
+ },
+ expectedError: errFileInUse(filePath),
+ },
+ "file locked properly, fails on unlock": {
+ prepareMockAssertions: func(locker *mockLocker, fn *mockFn) {
+ locker.On("TryLock").
+ Return(true, nil).
+ Once()
+ locker.On("Unlock").
+ Return(testError).
+ Once()
+
+ fn.On("Run").Once()
+ },
+ expectedError: errCantReleaseLock(testError, filePath),
+ },
+ "file locked and unlocked properly": {
+ prepareMockAssertions: func(locker *mockLocker, fn *mockFn) {
+ locker.On("TryLock").
+ Return(true, nil).
+ Once()
+ locker.On("Unlock").
+ Return(nil).
+ Once()
+
+ fn.On("Run").Once()
+ },
+ },
+ }
+
+ for testName, testCase := range tests {
+ t.Run(testName, func(t *testing.T) {
+ fnMock := new(mockFn)
+ defer fnMock.AssertExpectations(t)
+
+ fsLockerMock, cleanup := mockDefaultLocker(t, filePath)
+ defer cleanup()
+
+ testCase.prepareMockAssertions(fsLockerMock, fnMock)
+
+ err := InLock(filePath, fnMock.Run)
+
+ if testCase.expectedError == nil {
+ assert.NoError(t, err)
+ return
+ }
+
+ assert.EqualError(t, err, testCase.expectedError.Error())
+ })
+ }
+}
diff --git a/helpers/fslocker/mock_locker.go b/helpers/fslocker/mock_locker.go
new file mode 100644
index 00000000..1e979768
--- /dev/null
+++ b/helpers/fslocker/mock_locker.go
@@ -0,0 +1,47 @@
+// Code generated by mockery v1.0.0. DO NOT EDIT.
+
+// This comment works around https://github.com/vektra/mockery/issues/155
+
+package fslocker
+
+import mock "github.com/stretchr/testify/mock"
+
+// mockLocker is an autogenerated mock type for the locker type
+type mockLocker struct {
+ mock.Mock
+}
+
+// TryLock provides a mock function with given fields:
+func (_m *mockLocker) TryLock() (bool, error) {
+ ret := _m.Called()
+
+ var r0 bool
+ if rf, ok := ret.Get(0).(func() bool); ok {
+ r0 = rf()
+ } else {
+ r0 = ret.Get(0).(bool)
+ }
+
+ var r1 error
+ if rf, ok := ret.Get(1).(func() error); ok {
+ r1 = rf()
+ } else {
+ r1 = ret.Error(1)
+ }
+
+ return r0, r1
+}
+
+// Unlock provides a mock function with given fields:
+func (_m *mockLocker) Unlock() error {
+ ret := _m.Called()
+
+ var r0 error
+ if rf, ok := ret.Get(0).(func() error); ok {
+ r0 = rf()
+ } else {
+ r0 = ret.Error(0)
+ }
+
+ return r0
+}
diff --git a/vendor/github.com/gofrs/flock/LICENSE b/vendor/github.com/gofrs/flock/LICENSE
new file mode 100644
index 00000000..aff7d358
--- /dev/null
+++ b/vendor/github.com/gofrs/flock/LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2015, Tim Heckman
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+* Neither the name of linode-netint nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/vendor/github.com/gofrs/flock/flock.go b/vendor/github.com/gofrs/flock/flock.go
new file mode 100644
index 00000000..8f109b8a
--- /dev/null
+++ b/vendor/github.com/gofrs/flock/flock.go
@@ -0,0 +1,127 @@
+// Copyright 2015 Tim Heckman. All rights reserved.
+// Use of this source code is governed by the BSD 3-Clause
+// license that can be found in the LICENSE file.
+
+// Package flock implements a thread-safe interface for file locking.
+// It also includes a non-blocking TryLock() function to allow locking
+// without blocking execution.
+//
+// Package flock is released under the BSD 3-Clause License. See the LICENSE file
+// for more details.
+//
+// While using this library, remember that the locking behaviors are not
+// guaranteed to be the same on each platform. For example, some UNIX-like
+// operating systems will transparently convert a shared lock to an exclusive
+// lock. If you Unlock() the flock from a location where you believe that you
+// have the shared lock, you may accidentally drop the exclusive lock.
+package flock
+
+import (
+ "context"
+ "os"
+ "sync"
+ "time"
+)
+
+// Flock is the struct type to handle file locking. All fields are unexported,
+// with access to some of the fields provided by getter methods (Path() and Locked()).
+type Flock struct {
+ path string
+ m sync.RWMutex
+ fh *os.File
+ l bool
+ r bool
+}
+
+// New returns a new instance of *Flock. The only parameter
+// it takes is the path to the desired lockfile.
+func New(path string) *Flock {
+ return &Flock{path: path}
+}
+
+// NewFlock returns a new instance of *Flock. The only parameter
+// it takes is the path to the desired lockfile.
+//
+// Deprecated: Use New instead.
+func NewFlock(path string) *Flock {
+ return New(path)
+}
+
+// Close is equivalent to calling Unlock.
+//
+// This will release the lock and close the underlying file descriptor.
+// It will not remove the file from disk, that's up to your application.
+func (f *Flock) Close() error {
+ return f.Unlock()
+}
+
+// Path returns the path as provided in NewFlock().
+func (f *Flock) Path() string {
+ return f.path
+}
+
+// Locked returns the lock state (locked: true, unlocked: false).
+//
+// Warning: by the time you use the returned value, the state may have changed.
+func (f *Flock) Locked() bool {
+ f.m.RLock()
+ defer f.m.RUnlock()
+ return f.l
+}
+
+// RLocked returns the read lock state (locked: true, unlocked: false).
+//
+// Warning: by the time you use the returned value, the state may have changed.
+func (f *Flock) RLocked() bool {
+ f.m.RLock()
+ defer f.m.RUnlock()
+ return f.r
+}
+
+func (f *Flock) String() string {
+ return f.path
+}
+
+// TryLockContext repeatedly tries to take an exclusive lock until one of the
+// conditions is met: TryLock succeeds, TryLock fails with error, or Context
+// Done channel is closed.
+func (f *Flock) TryLockContext(ctx context.Context, retryDelay time.Duration) (bool, error) {
+ return tryCtx(ctx, f.TryLock, retryDelay)
+}
+
+// TryRLockContext repeatedly tries to take a shared lock until one of the
+// conditions is met: TryRLock succeeds, TryRLock fails with error, or Context
+// Done channel is closed.
+func (f *Flock) TryRLockContext(ctx context.Context, retryDelay time.Duration) (bool, error) {
+ return tryCtx(ctx, f.TryRLock, retryDelay)
+}
+
+func tryCtx(ctx context.Context, fn func() (bool, error), retryDelay time.Duration) (bool, error) {
+ if ctx.Err() != nil {
+ return false, ctx.Err()
+ }
+ for {
+ if ok, err := fn(); ok || err != nil {
+ return ok, err
+ }
+ select {
+ case <-ctx.Done():
+ return false, ctx.Err()
+ case <-time.After(retryDelay):
+ // try again
+ }
+ }
+}
+
+func (f *Flock) setFh() error {
+ // open a new os.File instance
+ // create it if it doesn't exist, and open the file read-only.
+ fh, err := os.OpenFile(f.path, os.O_CREATE|os.O_RDONLY, os.FileMode(0600))
+ if err != nil {
+ return err
+ }
+
+ // set the filehandle on the struct
+ f.fh = fh
+ return nil
+}
diff --git a/vendor/github.com/gofrs/flock/flock_example_test.go b/vendor/github.com/gofrs/flock/flock_example_test.go
new file mode 100644
index 00000000..0c2afa5c
--- /dev/null
+++ b/vendor/github.com/gofrs/flock/flock_example_test.go
@@ -0,0 +1,72 @@
+// Copyright 2015 Tim Heckman. All rights reserved.
+// Copyright 2018 The Gofrs. All rights reserved.
+// Use of this source code is governed by the BSD 3-Clause
+// license that can be found in the LICENSE file.
+
+package flock_test
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "time"
+
+ "github.com/gofrs/flock"
+)
+
+func ExampleFlock_Locked() {
+ f := flock.New(os.TempDir() + "/go-lock.lock")
+ f.TryLock() // unchecked errors here
+
+ fmt.Printf("locked: %v\n", f.Locked())
+
+ f.Unlock()
+
+ fmt.Printf("locked: %v\n", f.Locked())
+ // Output: locked: true
+ // locked: false
+}
+
+func ExampleFlock_TryLock() {
+ // should probably put these in /var/lock
+ fileLock := flock.New(os.TempDir() + "/go-lock.lock")
+
+ locked, err := fileLock.TryLock()
+
+ if err != nil {
+ // handle locking error
+ }
+
+ if locked {
+ fmt.Printf("path: %s; locked: %v\n", fileLock.Path(), fileLock.Locked())
+
+ if err := fileLock.Unlock(); err != nil {
+ // handle unlock error
+ }
+ }
+
+ fmt.Printf("path: %s; locked: %v\n", fileLock.Path(), fileLock.Locked())
+}
+
+func ExampleFlock_TryLockContext() {
+ // should probably put these in /var/lock
+ fileLock := flock.New(os.TempDir() + "/go-lock.lock")
+
+ lockCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+ defer cancel()
+ locked, err := fileLock.TryLockContext(lockCtx, 678*time.Millisecond)
+
+ if err != nil {
+ // handle locking error
+ }
+
+ if locked {
+ fmt.Printf("path: %s; locked: %v\n", fileLock.Path(), fileLock.Locked())
+
+ if err := fileLock.Unlock(); err != nil {
+ // handle unlock error
+ }
+ }
+
+ fmt.Printf("path: %s; locked: %v\n", fileLock.Path(), fileLock.Locked())
+}
diff --git a/vendor/github.com/gofrs/flock/flock_test.go b/vendor/github.com/gofrs/flock/flock_test.go
new file mode 100644
index 00000000..e277e525
--- /dev/null
+++ b/vendor/github.com/gofrs/flock/flock_test.go
@@ -0,0 +1,290 @@
+// Copyright 2015 Tim Heckman. All rights reserved.
+// Copyright 2018 The Gofrs. All rights reserved.
+// Use of this source code is governed by the BSD 3-Clause
+// license that can be found in the LICENSE file.
+
+package flock_test
+
+import (
+ "context"
+ "io/ioutil"
+ "os"
+ "testing"
+ "time"
+
+ "github.com/gofrs/flock"
+
+ . "gopkg.in/check.v1"
+)
+
+type TestSuite struct {
+ path string
+ flock *flock.Flock
+}
+
+var _ = Suite(&TestSuite{})
+
+func Test(t *testing.T) { TestingT(t) }
+
+func (t *TestSuite) SetUpTest(c *C) {
+ tmpFile, err := ioutil.TempFile(os.TempDir(), "go-flock-")
+ c.Assert(err, IsNil)
+ c.Assert(tmpFile, Not(IsNil))
+
+ t.path = tmpFile.Name()
+
+ defer os.Remove(t.path)
+ tmpFile.Close()
+
+ t.flock = flock.New(t.path)
+}
+
+func (t *TestSuite) TearDownTest(c *C) {
+ t.flock.Unlock()
+ os.Remove(t.path)
+}
+
+func (t *TestSuite) TestNew(c *C) {
+ var f *flock.Flock
+
+ f = flock.New(t.path)
+ c.Assert(f, Not(IsNil))
+ c.Check(f.Path(), Equals, t.path)
+ c.Check(f.Locked(), Equals, false)
+ c.Check(f.RLocked(), Equals, false)
+}
+
+func (t *TestSuite) TestFlock_Path(c *C) {
+ var path string
+ path = t.flock.Path()
+ c.Check(path, Equals, t.path)
+}
+
+func (t *TestSuite) TestFlock_Locked(c *C) {
+ var locked bool
+ locked = t.flock.Locked()
+ c.Check(locked, Equals, false)
+}
+
+func (t *TestSuite) TestFlock_RLocked(c *C) {
+ var locked bool
+ locked = t.flock.RLocked()
+ c.Check(locked, Equals, false)
+}
+
+func (t *TestSuite) TestFlock_String(c *C) {
+ var str string
+ str = t.flock.String()
+ c.Assert(str, Equals, t.path)
+}
+
+func (t *TestSuite) TestFlock_TryLock(c *C) {
+ c.Assert(t.flock.Locked(), Equals, false)
+ c.Assert(t.flock.RLocked(), Equals, false)
+
+ var locked bool
+ var err error
+
+ locked, err = t.flock.TryLock()
+ c.Assert(err, IsNil)
+ c.Check(locked, Equals, true)
+ c.Check(t.flock.Locked(), Equals, true)
+ c.Check(t.flock.RLocked(), Equals, false)
+
+ locked, err = t.flock.TryLock()
+ c.Assert(err, IsNil)
+ c.Check(locked, Equals, true)
+
+ // make sure we just return false with no error in cases
+ // where we would have been blocked
+ locked, err = flock.New(t.path).TryLock()
+ c.Assert(err, IsNil)
+ c.Check(locked, Equals, false)
+}
+
+func (t *TestSuite) TestFlock_TryRLock(c *C) {
+ c.Assert(t.flock.Locked(), Equals, false)
+ c.Assert(t.flock.RLocked(), Equals, false)
+
+ var locked bool
+ var err error
+
+ locked, err = t.flock.TryRLock()
+ c.Assert(err, IsNil)
+ c.Check(locked, Equals, true)
+ c.Check(t.flock.Locked(), Equals, false)
+ c.Check(t.flock.RLocked(), Equals, true)
+
+ locked, err = t.flock.TryRLock()
+ c.Assert(err, IsNil)
+ c.Check(locked, Equals, true)
+
+ // shared lock should not block.
+ flock2 := flock.New(t.path)
+ locked, err = flock2.TryRLock()
+ c.Assert(err, IsNil)
+ c.Check(locked, Equals, true)
+
+ // make sure we just return false with no error in cases
+ // where we would have been blocked
+ t.flock.Unlock()
+ flock2.Unlock()
+ t.flock.Lock()
+ locked, err = flock.New(t.path).TryRLock()
+ c.Assert(err, IsNil)
+ c.Check(locked, Equals, false)
+}
+
+func (t *TestSuite) TestFlock_TryLockContext(c *C) {
+ // happy path
+ ctx, cancel := context.WithCancel(context.Background())
+ locked, err := t.flock.TryLockContext(ctx, time.Second)
+ c.Assert(err, IsNil)
+ c.Check(locked, Equals, true)
+
+ // context already canceled
+ cancel()
+ locked, err = flock.New(t.path).TryLockContext(ctx, time.Second)
+ c.Assert(err, Equals, context.Canceled)
+ c.Check(locked, Equals, false)
+
+ // timeout
+ ctx, cancel = context.WithTimeout(context.Background(), 10*time.Millisecond)
+ defer cancel()
+ locked, err = flock.New(t.path).TryLockContext(ctx, time.Second)
+ c.Assert(err, Equals, context.DeadlineExceeded)
+ c.Check(locked, Equals, false)
+}
+
+func (t *TestSuite) TestFlock_TryRLockContext(c *C) {
+ // happy path
+ ctx, cancel := context.WithCancel(context.Background())
+ locked, err := t.flock.TryRLockContext(ctx, time.Second)
+ c.Assert(err, IsNil)
+ c.Check(locked, Equals, true)
+
+ // context already canceled
+ cancel()
+ locked, err = flock.New(t.path).TryRLockContext(ctx, time.Second)
+ c.Assert(err, Equals, context.Canceled)
+ c.Check(locked, Equals, false)
+
+ // timeout
+ t.flock.Unlock()
+ t.flock.Lock()
+ ctx, cancel = context.WithTimeout(context.Background(), 10*time.Millisecond)
+ defer cancel()
+ locked, err = flock.New(t.path).TryRLockContext(ctx, time.Second)
+ c.Assert(err, Equals, context.DeadlineExceeded)
+ c.Check(locked, Equals, false)
+}
+
+func (t *TestSuite) TestFlock_Unlock(c *C) {
+ var err error
+
+ err = t.flock.Unlock()
+ c.Assert(err, IsNil)
+
+ // get a lock for us to unlock
+ locked, err := t.flock.TryLock()
+ c.Assert(err, IsNil)
+ c.Assert(locked, Equals, true)
+ c.Assert(t.flock.Locked(), Equals, true)
+ c.Check(t.flock.RLocked(), Equals, false)
+
+ _, err = os.Stat(t.path)
+ c.Assert(os.IsNotExist(err), Equals, false)
+
+ err = t.flock.Unlock()
+ c.Assert(err, IsNil)
+ c.Check(t.flock.Locked(), Equals, false)
+ c.Check(t.flock.RLocked(), Equals, false)
+}
+
+func (t *TestSuite) TestFlock_Lock(c *C) {
+ c.Assert(t.flock.Locked(), Equals, false)
+ c.Check(t.flock.RLocked(), Equals, false)
+
+ var err error
+
+ err = t.flock.Lock()
+ c.Assert(err, IsNil)
+ c.Check(t.flock.Locked(), Equals, true)
+ c.Check(t.flock.RLocked(), Equals, false)
+
+ // test that the short-circuit works
+ err = t.flock.Lock()
+ c.Assert(err, IsNil)
+
+ //
+ // Test that Lock() is a blocking call
+ //
+ ch := make(chan error, 2)
+ gf := flock.New(t.path)
+ defer gf.Unlock()
+
+ go func(ch chan<- error) {
+ ch <- nil
+ ch <- gf.Lock()
+ close(ch)
+ }(ch)
+
+ errCh, ok := <-ch
+ c.Assert(ok, Equals, true)
+ c.Assert(errCh, IsNil)
+
+ err = t.flock.Unlock()
+ c.Assert(err, IsNil)
+
+ errCh, ok = <-ch
+ c.Assert(ok, Equals, true)
+ c.Assert(errCh, IsNil)
+ c.Check(t.flock.Locked(), Equals, false)
+ c.Check(t.flock.RLocked(), Equals, false)
+ c.Check(gf.Locked(), Equals, true)
+ c.Check(gf.RLocked(), Equals, false)
+}
+
+func (t *TestSuite) TestFlock_RLock(c *C) {
+ c.Assert(t.flock.Locked(), Equals, false)
+ c.Check(t.flock.RLocked(), Equals, false)
+
+ var err error
+
+ err = t.flock.RLock()
+ c.Assert(err, IsNil)
+ c.Check(t.flock.Locked(), Equals, false)
+ c.Check(t.flock.RLocked(), Equals, true)
+
+ // test that the short-circuit works
+ err = t.flock.RLock()
+ c.Assert(err, IsNil)
+
+ //
+ // Test that RLock() is a blocking call
+ //
+ ch := make(chan error, 2)
+ gf := flock.New(t.path)
+ defer gf.Unlock()
+
+ go func(ch chan<- error) {
+ ch <- nil
+ ch <- gf.RLock()
+ close(ch)
+ }(ch)
+
+ errCh, ok := <-ch
+ c.Assert(ok, Equals, true)
+ c.Assert(errCh, IsNil)
+
+ err = t.flock.Unlock()
+ c.Assert(err, IsNil)
+
+ errCh, ok = <-ch
+ c.Assert(ok, Equals, true)
+ c.Assert(errCh, IsNil)
+ c.Check(t.flock.Locked(), Equals, false)
+ c.Check(t.flock.RLocked(), Equals, false)
+ c.Check(gf.Locked(), Equals, false)
+ c.Check(gf.RLocked(), Equals, true)
+}
diff --git a/vendor/github.com/gofrs/flock/flock_unix.go b/vendor/github.com/gofrs/flock/flock_unix.go
new file mode 100644
index 00000000..45f71a70
--- /dev/null
+++ b/vendor/github.com/gofrs/flock/flock_unix.go
@@ -0,0 +1,195 @@
+// Copyright 2015 Tim Heckman. All rights reserved.
+// Use of this source code is governed by the BSD 3-Clause
+// license that can be found in the LICENSE file.
+
+// +build !windows
+
+package flock
+
+import (
+ "os"
+ "syscall"
+)
+
+// Lock is a blocking call to try and take an exclusive file lock. It will wait
+// until it is able to obtain the exclusive file lock. It's recommended that
+// TryLock() be used over this function. This function may block the ability to
+// query the current Locked() or RLocked() status due to a RW-mutex lock.
+//
+// If we are already exclusive-locked, this function short-circuits and returns
+// immediately assuming it can take the mutex lock.
+//
+// If the *Flock has a shared lock (RLock), this may transparently replace the
+// shared lock with an exclusive lock on some UNIX-like operating systems. Be
+// careful when using exclusive locks in conjunction with shared locks
+// (RLock()), because calling Unlock() may accidentally release the exclusive
+// lock that was once a shared lock.
+func (f *Flock) Lock() error {
+ return f.lock(&f.l, syscall.LOCK_EX)
+}
+
+// RLock is a blocking call to try and take a shared file lock. It will wait
+// until it is able to obtain the shared file lock. It's recommended that
+// TryRLock() be used over this function. This function may block the ability to
+// query the current Locked() or RLocked() status due to a RW-mutex lock.
+//
+// If we are already shared-locked, this function short-circuits and returns
+// immediately assuming it can take the mutex lock.
+func (f *Flock) RLock() error {
+ return f.lock(&f.r, syscall.LOCK_SH)
+}
+
+func (f *Flock) lock(locked *bool, flag int) error {
+ f.m.Lock()
+ defer f.m.Unlock()
+
+ if *locked {
+ return nil
+ }
+
+ if f.fh == nil {
+ if err := f.setFh(); err != nil {
+ return err
+ }
+ }
+
+ if err := syscall.Flock(int(f.fh.Fd()), flag); err != nil {
+ shouldRetry, reopenErr := f.reopenFDOnError(err)
+ if reopenErr != nil {
+ return reopenErr
+ }
+
+ if !shouldRetry {
+ return err
+ }
+
+ if err = syscall.Flock(int(f.fh.Fd()), flag); err != nil {
+ return err
+ }
+ }
+
+ *locked = true
+ return nil
+}
+
+// Unlock is a function to unlock the file. This file takes a RW-mutex lock, so
+// while it is running the Locked() and RLocked() functions will be blocked.
+//
+// This function short-circuits if we are unlocked already. If not, it calls
+// syscall.LOCK_UN on the file and closes the file descriptor. It does not
+// remove the file from disk. It's up to your application to do.
+//
+// Please note, if your shared lock became an exclusive lock this may
+// unintentionally drop the exclusive lock if called by the consumer that
+// believes they have a shared lock. Please see Lock() for more details.
+func (f *Flock) Unlock() error {
+ f.m.Lock()
+ defer f.m.Unlock()
+
+ // if we aren't locked or if the lockfile instance is nil
+ // just return a nil error because we are unlocked
+ if (!f.l && !f.r) || f.fh == nil {
+ return nil
+ }
+
+ // mark the file as unlocked
+ if err := syscall.Flock(int(f.fh.Fd()), syscall.LOCK_UN); err != nil {
+ return err
+ }
+
+ f.fh.Close()
+
+ f.l = false
+ f.r = false
+ f.fh = nil
+
+ return nil
+}
+
+// TryLock is the preferred function for taking an exclusive file lock. This
+// function takes an RW-mutex lock before it tries to lock the file, so there is
+// the possibility that this function may block for a short time if another
+// goroutine is trying to take any action.
+//
+// The actual file lock is non-blocking. If we are unable to get the exclusive
+// file lock, the function will return false instead of waiting for the lock. If
+// we get the lock, we also set the *Flock instance as being exclusive-locked.
+func (f *Flock) TryLock() (bool, error) {
+ return f.try(&f.l, syscall.LOCK_EX)
+}
+
+// TryRLock is the preferred function for taking a shared file lock. This
+// function takes an RW-mutex lock before it tries to lock the file, so there is
+// the possibility that this function may block for a short time if another
+// goroutine is trying to take any action.
+//
+// The actual file lock is non-blocking. If we are unable to get the shared file
+// lock, the function will return false instead of waiting for the lock. If we
+// get the lock, we also set the *Flock instance as being share-locked.
+func (f *Flock) TryRLock() (bool, error) {
+ return f.try(&f.r, syscall.LOCK_SH)
+}
+
+func (f *Flock) try(locked *bool, flag int) (bool, error) {
+ f.m.Lock()
+ defer f.m.Unlock()
+
+ if *locked {
+ return true, nil
+ }
+
+ if f.fh == nil {
+ if err := f.setFh(); err != nil {
+ return false, err
+ }
+ }
+
+ var retried bool
+retry:
+ err := syscall.Flock(int(f.fh.Fd()), flag|syscall.LOCK_NB)
+
+ switch err {
+ case syscall.EWOULDBLOCK:
+ return false, nil
+ case nil:
+ *locked = true
+ return true, nil
+ }
+ if !retried {
+ if shouldRetry, reopenErr := f.reopenFDOnError(err); reopenErr != nil {
+ return false, reopenErr
+ } else if shouldRetry {
+ retried = true
+ goto retry
+ }
+ }
+
+ return false, err
+}
+
+// reopenFDOnError determines whether we should reopen the file handle
+// in readwrite mode and try again. This comes from util-linux/sys-utils/flock.c:
+// Since Linux 3.4 (commit 55725513)
+// Probably NFSv4 where flock() is emulated by fcntl().
+func (f *Flock) reopenFDOnError(err error) (bool, error) {
+ if err != syscall.EIO && err != syscall.EBADF {
+ return false, nil
+ }
+ if st, err := f.fh.Stat(); err == nil {
+ // if the file is able to be read and written
+ if st.Mode()&0600 == 0600 {
+ f.fh.Close()
+ f.fh = nil
+
+ // reopen in read-write mode and set the filehandle
+ fh, err := os.OpenFile(f.path, os.O_CREATE|os.O_RDWR, os.FileMode(0600))
+ if err != nil {
+ return false, err
+ }
+ f.fh = fh
+ return true, nil
+ }
+ }
+
+ return false, nil
+}
diff --git a/vendor/github.com/gofrs/flock/flock_winapi.go b/vendor/github.com/gofrs/flock/flock_winapi.go
new file mode 100644
index 00000000..fe405a25
--- /dev/null
+++ b/vendor/github.com/gofrs/flock/flock_winapi.go
@@ -0,0 +1,76 @@
+// Copyright 2015 Tim Heckman. All rights reserved.
+// Use of this source code is governed by the BSD 3-Clause
+// license that can be found in the LICENSE file.
+
+// +build windows
+
+package flock
+
+import (
+ "syscall"
+ "unsafe"
+)
+
+var (
+ kernel32, _ = syscall.LoadLibrary("kernel32.dll")
+ procLockFileEx, _ = syscall.GetProcAddress(kernel32, "LockFileEx")
+ procUnlockFileEx, _ = syscall.GetProcAddress(kernel32, "UnlockFileEx")
+)
+
+const (
+ winLockfileFailImmediately = 0x00000001
+ winLockfileExclusiveLock = 0x00000002
+ winLockfileSharedLock = 0x00000000
+)
+
+// Use of 0x00000000 for the shared lock is a guess based on some the MS Windows
+// `LockFileEX` docs, which document the `LOCKFILE_EXCLUSIVE_LOCK` flag as:
+//
+// > The function requests an exclusive lock. Otherwise, it requests a shared
+// > lock.
+//
+// https://msdn.microsoft.com/en-us/library/windows/desktop/aa365203(v=vs.85).aspx
+
+func lockFileEx(handle syscall.Handle, flags uint32, reserved uint32, numberOfBytesToLockLow uint32, numberOfBytesToLockHigh uint32, offset *syscall.Overlapped) (bool, syscall.Errno) {
+ r1, _, errNo := syscall.Syscall6(
+ uintptr(procLockFileEx),
+ 6,
+ uintptr(handle),
+ uintptr(flags),
+ uintptr(reserved),
+ uintptr(numberOfBytesToLockLow),
+ uintptr(numberOfBytesToLockHigh),
+ uintptr(unsafe.Pointer(offset)))
+
+ if r1 != 1 {
+ if errNo == 0 {
+ return false, syscall.EINVAL
+ }
+
+ return false, errNo
+ }
+
+ return true, 0
+}
+
+func unlockFileEx(handle syscall.Handle, reserved uint32, numberOfBytesToLockLow uint32, numberOfBytesToLockHigh uint32, offset *syscall.Overlapped) (bool, syscall.Errno) {
+ r1, _, errNo := syscall.Syscall6(
+ uintptr(procUnlockFileEx),
+ 5,
+ uintptr(handle),
+ uintptr(reserved),
+ uintptr(numberOfBytesToLockLow),
+ uintptr(numberOfBytesToLockHigh),
+ uintptr(unsafe.Pointer(offset)),
+ 0)
+
+ if r1 != 1 {
+ if errNo == 0 {
+ return false, syscall.EINVAL
+ }
+
+ return false, errNo
+ }
+
+ return true, 0
+}
diff --git a/vendor/github.com/gofrs/flock/flock_windows.go b/vendor/github.com/gofrs/flock/flock_windows.go
new file mode 100644
index 00000000..9f4a5f10
--- /dev/null
+++ b/vendor/github.com/gofrs/flock/flock_windows.go
@@ -0,0 +1,140 @@
+// Copyright 2015 Tim Heckman. All rights reserved.
+// Use of this source code is governed by the BSD 3-Clause
+// license that can be found in the LICENSE file.
+
+package flock
+
+import (
+ "syscall"
+)
+
+// ErrorLockViolation is the error code returned from the Windows syscall when a
+// lock would block and you ask to fail immediately.
+const ErrorLockViolation syscall.Errno = 0x21 // 33
+
+// Lock is a blocking call to try and take an exclusive file lock. It will wait
+// until it is able to obtain the exclusive file lock. It's recommended that
+// TryLock() be used over this function. This function may block the ability to
+// query the current Locked() or RLocked() status due to a RW-mutex lock.
+//
+// If we are already locked, this function short-circuits and returns
+// immediately assuming it can take the mutex lock.
+func (f *Flock) Lock() error {
+ return f.lock(&f.l, winLockfileExclusiveLock)
+}
+
+// RLock is a blocking call to try and take a shared file lock. It will wait
+// until it is able to obtain the shared file lock. It's recommended that
+// TryRLock() be used over this function. This function may block the ability to
+// query the current Locked() or RLocked() status due to a RW-mutex lock.
+//
+// If we are already locked, this function short-circuits and returns
+// immediately assuming it can take the mutex lock.
+func (f *Flock) RLock() error {
+ return f.lock(&f.r, winLockfileSharedLock)
+}
+
+func (f *Flock) lock(locked *bool, flag uint32) error {
+ f.m.Lock()
+ defer f.m.Unlock()
+
+ if *locked {
+ return nil
+ }
+
+ if f.fh == nil {
+ if err := f.setFh(); err != nil {
+ return err
+ }
+ }
+
+ if _, errNo := lockFileEx(syscall.Handle(f.fh.Fd()), flag, 0, 1, 0, &syscall.Overlapped{}); errNo > 0 {
+ return errNo
+ }
+
+ *locked = true
+ return nil
+}
+
+// Unlock is a function to unlock the file. This file takes a RW-mutex lock, so
+// while it is running the Locked() and RLocked() functions will be blocked.
+//
+// This function short-circuits if we are unlocked already. If not, it calls
+// UnlockFileEx() on the file and closes the file descriptor. It does not remove
+// the file from disk. It's up to your application to do.
+func (f *Flock) Unlock() error {
+ f.m.Lock()
+ defer f.m.Unlock()
+
+ // if we aren't locked or if the lockfile instance is nil
+ // just return a nil error because we are unlocked
+ if (!f.l && !f.r) || f.fh == nil {
+ return nil
+ }
+
+ // mark the file as unlocked
+ if _, errNo := unlockFileEx(syscall.Handle(f.fh.Fd()), 0, 1, 0, &syscall.Overlapped{}); errNo > 0 {
+ return errNo
+ }
+
+ f.fh.Close()
+
+ f.l = false
+ f.r = false
+ f.fh = nil
+
+ return nil
+}
+
+// TryLock is the preferred function for taking an exclusive file lock. This
+// function does take a RW-mutex lock before it tries to lock the file, so there
+// is the possibility that this function may block for a short time if another
+// goroutine is trying to take any action.
+//
+// The actual file lock is non-blocking. If we are unable to get the exclusive
+// file lock, the function will return false instead of waiting for the lock. If
+// we get the lock, we also set the *Flock instance as being exclusive-locked.
+func (f *Flock) TryLock() (bool, error) {
+ return f.try(&f.l, winLockfileExclusiveLock)
+}
+
+// TryRLock is the preferred function for taking a shared file lock. This
+// function does take a RW-mutex lock before it tries to lock the file, so there
+// is the possibility that this function may block for a short time if another
+// goroutine is trying to take any action.
+//
+// The actual file lock is non-blocking. If we are unable to get the shared file
+// lock, the function will return false instead of waiting for the lock. If we
+// get the lock, we also set the *Flock instance as being shared-locked.
+func (f *Flock) TryRLock() (bool, error) {
+ return f.try(&f.r, winLockfileSharedLock)
+}
+
+func (f *Flock) try(locked *bool, flag uint32) (bool, error) {
+ f.m.Lock()
+ defer f.m.Unlock()
+
+ if *locked {
+ return true, nil
+ }
+
+ if f.fh == nil {
+ if err := f.setFh(); err != nil {
+ return false, err
+ }
+ }
+
+ _, errNo := lockFileEx(syscall.Handle(f.fh.Fd()), flag|winLockfileFailImmediately, 0, 1, 0, &syscall.Overlapped{})
+
+ if errNo > 0 {
+ if errNo == ErrorLockViolation || errNo == syscall.ERROR_IO_PENDING {
+ return false, nil
+ }
+
+ return false, errNo
+ }
+
+ *locked = true
+
+ return true, nil
+}