Initial implementation, supports POSIX and Windows
Initial locking implementation with POSIX and Windows support. Tests
work on both POSIX and Windows (2016 Server) systems.
Added CI primitives.
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..7908de8
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,22 @@
+# Copyright 2017 by Dan Jacques. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+language: go
+
+go:
+ - 1.2
+ - 1.3
+ - 1.5
+ - 1.6
+ - 1.7
+ - tip
+
+before_install:
+ - go get github.com/maruel/pre-commit-go/cmd/pcg
+
+install:
+ - go get -t ./...
+
+script:
+ - pcg
diff --git a/README.md b/README.md
index 593fcc5..89477e0 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,32 @@
# gofslock
Go implementation of filesystem-level locking.
+
+[![GoDoc](https://godoc.org/github.com/danjacques/gofslock?status.svg)](http://godoc.org/github.com/danjacques/gofslock)
+[![Build Status](https://travis-ci.org/danjacques/gofslock.svg?branch=master)](https://travis-ci.org/danjacques/gofslock)
+
+Feedback
+--------
+
+Request features and report bugs using the
+[GitHub Issue Tracker](https://github.com/danjacques/gofslock/issues/new).
+
+Contributions
+-------------
+
+Contributions to this project are welcome, though please
+[file an issue](https://github.com/danjacques/gofslock/issues/new).
+before starting work on anything major.
+
+To get started contributing to this project,
+clone the repository:
+
+ git clone https://github.com/danjacques/gofslock
+
+This repository uses [pre-commit-go](https://github.com/maruel/pre-commit-go) to
+validate itself. Please install this prior to working on the project:
+
+ * Make sure your `user.email` and `user.name` are configured in `git config`.
+ * Install test-only packages:
+ `go get -u -t github.com/danjacques/gofslock/...`
+ * Install the [pcg](https://github.com/maruel/pre-commit-go) git hook:
+ `go get -u github.com/maruel/pre-commit-go/cmd/... && pcg`
diff --git a/fslock/interface.go b/fslock/interface.go
new file mode 100644
index 0000000..0e5eb5b
--- /dev/null
+++ b/fslock/interface.go
@@ -0,0 +1,65 @@
+// Copyright 2017 by Dan Jacques. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package fslock is a cross-platform filesystem locking implementation.
+//
+// fslock aims to implement workable filesystem-based locking semantics. Each
+// implementation offers its own nuances, and not all of those nuances are
+// addressed by this package.
+//
+// fslock will work as long as you don't do anything particularly weird, such
+// as:
+//
+// - Manually forking your Go process.
+// - Circumventing the fslock package and opening and/or manipulating the lock
+// file directly.
+//
+// An attempt to take a filesystem lock is non-blocking, and will return
+// ErrLockHeld if the lock is already held elsewhere.
+package fslock
+
+// Lock acquires a filesystem lock for the given path.
+//
+// If the lock could not be acquired because it is held by another entity,
+// ErrLockHeld will be returned. If an error is encountered while locking,
+// that error will be returned.
+//
+// Lock is a convenience method for L's Lock.
+func Lock(path string) (Handle, error) { return LockBlocking(path, nil) }
+
+// LockBlocking acquires a filesystem lock for the given path. If the lock is
+// already held, LockBlocking will repeatedly attempt to acquire it using
+// the supplied Blocker in between attempts.
+//
+// If the lock could not be acquired because it is held by another entity,
+// ErrLockHeld will be returned. If an error is encountered while locking,
+// or an error is returned by b, that error will be returned.
+//
+// Lock is a convenience method for L's Lock.
+func LockBlocking(path string, b Blocker) (Handle, error) {
+ l := L{
+ Path: path,
+ Block: b,
+ }
+ return l.Lock()
+}
+
+// With is a convenience function to create a lock, execute a function while
+// holding that lock, and then release the lock on completion.
+//
+// See L's With method for details.
+func With(path string, fn func() error) error { return WithBlocking(path, nil, fn) }
+
+// WithBlocking is a convenience function to create a lock, execute a function
+// while holding that lock, and then release the lock on completion. The
+// supplied block function is used to retry (see L's Block field).
+//
+// See L's With method for details.
+func WithBlocking(path string, b Blocker, fn func() error) error {
+ l := L{
+ Path: path,
+ Block: b,
+ }
+ return l.With(fn)
+}
diff --git a/fslock/lock.go b/fslock/lock.go
new file mode 100644
index 0000000..d29011f
--- /dev/null
+++ b/fslock/lock.go
@@ -0,0 +1,102 @@
+// Copyright 2017 by Dan Jacques. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package fslock
+
+import (
+ "errors"
+)
+
+// ErrLockHeld is a sentinel error returned when the lock could not be
+// acquired.
+var ErrLockHeld = errors.New("fslock: lock is held")
+
+// Handle is a reference to a held lock. It must be released via Unlock when
+// finished.
+type Handle interface {
+ // Unlock releases the held lock.
+ //
+ // This can error if the underlying filesystem operations fail. This should
+ // not happen unless something has gone externally wrong, or the lock was
+ // mishandled.
+ Unlock() error
+}
+
+// DelayFunc is used for the Delay field in a Lock.
+type Blocker func() error
+
+// L describes a filesystem lock.
+type L struct {
+ // Path is the path of the file to lock.
+ Path string
+
+ // Content, if populated, is the lock file content. Content is written to the
+ // file when the lock call creates it, and only if the lock call actually
+ // creates the file. Failure to write Content is non-fatal.
+ //
+ // Content should be used only as a convenience hint for users who want to
+ // know what the lock file is, and not for actual programmatic management.
+ // Several code paths can result in successful file locking and still fail to
+ // write Content to that file.
+ //
+ // Content is not synchronized with the actual locking. Failure to write
+ // Content to the lock file is considered non-fatal.
+ Content []byte
+
+ // Block is the configured blocking function.
+ //
+ // If not nil, an attempt to acquire the lock will loop indefinitely until an
+ // error is encountered or the lock is acquired. Block will be called in
+ // between each acquire attempt, and should delay and/or cancel the
+ // acquisition.
+ //
+ // If Block returns an error, it will be propagated as the error result of the
+ // locking attempt.
+ Block Blocker
+}
+
+// Lock attempts to acquire the configured lock.
+func (l *L) Lock() (Handle, error) {
+ // Loop repeatedly until the lock is held or an error is encountered.
+ for {
+ switch h, err := lockImpl(l); err {
+ case nil:
+ // Acquired the lock.
+ return h, nil
+
+ case ErrLockHeld:
+ // If we have a Block function configured, invoke it, then try again.
+ // Otherwise, propagate ErrLockHeld.
+ if l.Block != nil {
+ if err := l.Block(); err != nil {
+ return nil, err
+ }
+ continue
+ }
+ fallthrough
+
+ default:
+ return nil, err
+ }
+ }
+}
+
+// With is a convenience method to acquire a lock via Lock, call fn, and release
+// the lock on completion (via defer).
+//
+// If an error is encountered, it will be returned. Otherwise, the return value
+// from fn will be returned.
+func (l *L) With(fn func() error) (err error) {
+ h, err := l.Lock()
+ if err != nil {
+ return
+ }
+ defer func() {
+ uerr := h.Unlock()
+ if uerr != nil && err == nil {
+ err = uerr
+ }
+ }()
+ return fn()
+}
diff --git a/fslock/lock_posix.go b/fslock/lock_posix.go
new file mode 100644
index 0000000..6409626
--- /dev/null
+++ b/fslock/lock_posix.go
@@ -0,0 +1,162 @@
+// Copyright 2017 by Dan Jacques. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build darwin linux freebsd netbsd openbsd android
+
+package fslock
+
+import (
+ "fmt"
+ "os"
+ "sync"
+ "syscall"
+)
+
+var globalPosixLockState posixLockState
+
+// lockImpl is an implementation of lock using POSIX locks via flock().
+//
+// flock() locks are released when *any* file handle to the locked file is
+// closed. To address this, we hold actual file handles globally. Attempts to
+// acquire a file lock first check to see if there is already a global entity
+// holding the lock (fail), then attempt to acquire the lock at a filesystem
+// level.
+func lockImpl(l *L) (Handle, error) {
+ return globalPosixLockState.lockImpl(l)
+}
+
+// posixLockState maintains an internal state of filesystem locks.
+//
+// For runtime usage, this is maintained in the global variable,
+// globalPosixLockState.
+type posixLockState struct {
+ sync.RWMutex
+ held map[uint64]*os.File
+}
+
+func (pls *posixLockState) lockImpl(l *L) (Handle, error) {
+ fd, err := getOrCreateLockFile(l.Path, l.Content)
+ if err != nil {
+ return nil, err
+ }
+ defer func() {
+ // Close "fd". On success, we'll clear "fd", so this will become a no-op.
+ if fd != nil {
+ fd.Close()
+ }
+ }()
+
+ st, err := fd.Stat()
+ if err != nil {
+ return nil, err
+ }
+ stat := st.Sys().(*syscall.Stat_t)
+
+ // Do we already have a lock on this file?
+ pls.RLock()
+ has := pls.held[stat.Ino]
+ pls.RUnlock()
+
+ if has != nil {
+ // Some other code path within our process already holds the lock.
+ return nil, ErrLockHeld
+ }
+
+ // Attempt to register the lock.
+ pls.Lock()
+ defer pls.Unlock()
+
+ // Check again, with write lock held.
+ if has := pls.held[stat.Ino]; has != nil {
+ return nil, ErrLockHeld
+ }
+
+ // Use "flock()" to get a lock on the file.
+ //
+ // LOCK_EX: Exclusive lock
+ // LOCK_NB: Non-blocking.
+ if err := syscall.Flock(int(fd.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err != nil {
+ if errno, ok := err.(syscall.Errno); ok {
+ switch errno {
+ case syscall.EWOULDBLOCK:
+ // Someone else holds the lock on this file.
+ return nil, ErrLockHeld
+ default:
+ return nil, err
+ }
+ }
+ return nil, err
+ }
+
+ if pls.held == nil {
+ pls.held = make(map[uint64]*os.File)
+ }
+ pls.held[stat.Ino] = fd
+ fd = nil // Don't Close in defer().
+ return &posixLockHandle{pls, stat.Ino}, nil
+}
+
+type posixLockHandle struct {
+ pls *posixLockState
+ ino uint64
+}
+
+func (l *posixLockHandle) Unlock() error {
+ if l.pls == nil {
+ panic("lock is not held")
+ }
+
+ l.pls.Lock()
+ defer l.pls.Unlock()
+
+ fd := l.pls.held[l.ino]
+ if fd == nil {
+ panic(fmt.Errorf("lock for inode %d is not held", l.ino))
+ }
+ if err := fd.Close(); err != nil {
+ return err
+ }
+ delete(l.pls.held, l.ino)
+ return nil
+}
+
+func getOrCreateLockFile(path string, content []byte) (*os.File, error) {
+ const mode = 0640 | os.ModeTemporary
+
+ // Loop until we've either created or opened the file.
+ for {
+ // Attempt to open the file. This will succeed if the file already exists.
+ fd, err := os.OpenFile(path, os.O_RDONLY, mode)
+ switch {
+ case err == nil:
+ // Successfully opened the file, return handle.
+ return fd, nil
+
+ case os.IsNotExist(err):
+ // The file doesn't exist. Attempt to exclusively create it.
+ //
+ // If this fails, the file exists, so we will try opening it again.
+ fd, err := os.OpenFile(path, (os.O_CREATE | os.O_EXCL | os.O_RDWR), mode)
+ switch {
+ case err == nil:
+ // Successfully created the new file. If we have content to write, try
+ // and write it.
+ if len(content) > 0 {
+ // Failure to write content is non-fatal.
+ _, _ = fd.Write(content)
+ }
+ return fd, err
+
+ case os.IsExist(err):
+ // Loop, we will try to open the file.
+
+ default:
+ return nil, err
+ }
+
+ default:
+ return nil, err
+ }
+ }
+}
diff --git a/fslock/lock_test.go b/fslock/lock_test.go
new file mode 100644
index 0000000..39f5b44
--- /dev/null
+++ b/fslock/lock_test.go
@@ -0,0 +1,370 @@
+// Copyright 2017 by Dan Jacques. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package fslock
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "testing"
+ "time"
+)
+
+func withTempDir(t *testing.T, prefix string, fn func(string)) {
+ wd, err := os.Getwd()
+ if err != nil {
+ t.Fatalf("failed to get working directory: %v", err)
+ }
+ tdir, err := ioutil.TempDir(wd, prefix)
+ if err != nil {
+ t.Fatalf("failed to create temporary directory in [%s]: %v", wd, err)
+ }
+ defer func() {
+ if err := os.RemoveAll(tdir); err != nil {
+ t.Logf("failed to clean up temporary directory [%s]: %v", tdir, err)
+ }
+ }()
+ fn(tdir)
+}
+
+// TestConcurrent tests file locking within the same process using concurrency
+// (via goroutines).
+//
+// For this to really be effective, the test should be run with "-race", since
+// it's *possible* that all of the goroutines end up cooperating in spite of a
+// bug.
+func TestConcurrent(t *testing.T) {
+ t.Parallel()
+
+ withTempDir(t, "concurrent", func(tdir string) {
+ value := 0
+ lock := filepath.Join(tdir, "lock")
+
+ const count = 1024
+ startC := make(chan struct{})
+ doneC := make(chan error, count)
+
+ // Individual test function, run per goroutine.
+ blocker := func() error {
+ time.Sleep(time.Millisecond)
+ return nil
+ }
+ doTest := func() error {
+ return WithBlocking(lock, blocker, func() error {
+ value++
+ return nil
+ })
+ }
+
+ for i := 0; i < count; i++ {
+ go func() {
+ var err error
+ defer func() {
+ doneC <- err
+ }()
+
+ // Wait for the start signal, then run the test.
+ <-startC
+ err = doTest()
+ }()
+ }
+
+ // Start our test.
+ close(startC)
+
+ // Reap errors.
+ errs := make([]error, 0, count)
+ for i := 0; i < count; i++ {
+ if err := <-doneC; err != nil {
+ errs = append(errs, err)
+ }
+ }
+ if len(errs) > 0 {
+ errList := make([]string, len(errs))
+ for i, err := range errs {
+ errList[i] = err.Error()
+ }
+ t.Fatalf("encountered %d error(s):\n%s", len(errs), strings.Join(errList, "\n"))
+ }
+ if value != count {
+ t.Fatalf("value doesn't match expected (%d != %d)", value, count)
+ }
+ })
+}
+
+// TestMultiProcessing tests access from multiple separate processes.
+//
+// The main process creates an output file, seeded with the value "0". It then
+// spawns a number of subprocesses (re-executions of this test program with
+// a "you're a subprocess" enviornment variable set). Each subprocess acquires
+// the lock, reads the output file, increments its value by 1, and writes the
+// result.
+//
+// To maximize contention, we spawn all of our subprocesses first, having each
+// block on a signal. When each spawns, it will signal that it's ready. Then,
+// the main process will signal that it should start.
+//
+// Success is if all of the subprocesses succeeded and the output file has the
+// correct value.
+func TestMultiProcessing(t *testing.T) {
+ t.Parallel()
+
+ getFiles := func(tdir string) (lock, out string) {
+ lock = filepath.Join(tdir, "lock")
+ out = filepath.Join(tdir, "out")
+ return
+ }
+
+ // Are we a testing process instance, or the main process?
+ const envSentinel = "_FSLOCK_TEST_WORKDIR"
+ if path := os.Getenv(envSentinel); path != "" {
+ // Resolve our signal files.
+ signalR := os.Stdin
+ respW := os.Stdout
+
+ lock, out := getFiles(path)
+ rv := testMultiProcessingSubprocess(lock, out, respW, signalR)
+ if _, err := respW.Write([]byte{rv}); err != nil {
+ // Raise an error in the parent process on Wait().
+ fmt.Printf("failed to write result (%d): %v\n", rv, err)
+ os.Exit(1)
+ }
+ os.Exit(0)
+ return
+ }
+
+ // This pipe will be used to signal that the processes should start the test.
+ signalR, signalW, err := os.Pipe()
+ if err != nil {
+ t.Fatalf("failed to create signal IPC pipe: %v", err)
+ }
+ defer signalR.Close()
+ defer signalW.Close()
+
+ respR, respW, err := os.Pipe()
+ if err != nil {
+ t.Fatalf("failed to create response IPC pipe: %v", err)
+ }
+ defer respR.Close()
+ defer respW.Close()
+
+ withTempDir(t, "multiprocessing", func(tdir string) {
+ // Seed our initial file.
+ _, out := getFiles(tdir)
+ if err := ioutil.WriteFile(out, []byte("0"), 0664); err != nil {
+ t.Fatalf("failed to write initial output file value: %v", err)
+ }
+ t.Logf("wrote initial output file to [%s]", out)
+
+ // TODO: Replace with os.Executable for Go 1.8.
+ executable := os.Args[0]
+
+ const count = 256
+ cmds := make([]*exec.Cmd, count)
+
+ // Kill all of our processes on cleanup, regardless of success/failure.
+ defer func() {
+ for _, cmd := range cmds {
+ _ = cmd.Process.Kill()
+ _ = cmd.Wait()
+ }
+ }()
+
+ for i := range cmds {
+ cmd := exec.Command(executable, "-test.run", "^TestMultiProcessing$")
+ cmd.Env = append(os.Environ(), fmt.Sprintf("%s=%s", envSentinel, tdir))
+ cmd.Stdin = signalR
+ cmd.Stdout = respW
+ cmd.Stderr = os.Stderr
+ if err := cmd.Start(); err != nil {
+ t.Fatalf("failed to start subprocess: %v", err)
+ }
+ cmds[i] = cmd
+ }
+
+ // Close our child-side pipe ends.
+ signalR.Close()
+ respW.Close()
+
+ // Wait for all of thr processes to signal that they're ready.
+ for i := 0; i < count; i++ {
+ buf := []byte{0}
+ switch n, err := respR.Read(buf[:]); {
+ case err != nil:
+ t.Fatalf("failed to read ready signal: %v", err)
+ case n != 1:
+ t.Fatal("failed to read ready signal byte")
+ }
+ }
+
+ // Signal our subprocesses to start!
+ if err := signalW.Close(); err != nil {
+ t.Fatalf("failed to signal processes to start: %v", err)
+ }
+
+ // Consume our responses. Each subprocess will write one byte to "respW"
+ // when they finish. That byte will be zero for success, non-zero for
+ // failure.
+ failures := 0
+ for i := 0; i < count; i++ {
+ buf := []byte{0}
+ switch n, err := respR.Read(buf[:]); {
+ case err != nil:
+ t.Fatalf("failed to read response: %v", err)
+ case n != 1:
+ t.Fatal("failed to read response byte")
+
+ default:
+ if buf[0] != 0 {
+ failures++
+ }
+ }
+ }
+
+ // Wait for our processes to actually exit.
+ for _, cmd := range cmds {
+ if err := cmd.Wait(); err != nil {
+ t.Fatalf("failed to wait for process: %v", err)
+ }
+ }
+
+ // Report the failure.
+ if failures > 0 {
+ t.Fatalf("subprocesses reported %d failure(s)", failures)
+ }
+
+ // Our "out" file should be "count".
+ buf, err := ioutil.ReadFile(out)
+ if err != nil {
+ t.Fatalf("failed to read output file: %v", err)
+ }
+ if exp := strconv.Itoa(count); string(buf) != exp {
+ t.Fatalf("output file doesn't match expected (%s != %s)", buf, exp)
+ }
+ })
+}
+
+func testMultiProcessingSubprocess(lock, out string, respW io.Writer, signalR io.Reader) byte {
+ // Signal that we're ready to start.
+ if _, err := respW.Write([]byte{0}); err != nil {
+ fmt.Fprintf(os.Stderr, "failed to send ready signal: %v", err)
+ return 1
+ }
+
+ // Wait for our signal (signalR closing).
+ if _, err := ioutil.ReadAll(signalR); err != nil {
+ fmt.Fprintf(os.Stderr, "failed to wait for signal: %v", err)
+ return 2
+ }
+
+ blocker := func() error {
+ time.Sleep(time.Millisecond)
+ return nil
+ }
+
+ var rc byte = 255
+ err := WithBlocking(lock, blocker, func() error {
+ // We hold the lock. Update our "out" file value by reading/writing a new
+ // number.
+ d, err := ioutil.ReadFile(out)
+ if err != nil {
+ rc = 4
+ return fmt.Errorf("failed to read output file: %v\n", err)
+ }
+ v, err := strconv.Atoi(string(d))
+ if err != nil {
+ rc = 5
+ return fmt.Errorf("invalid number value (%s): %v\n", d, err)
+ }
+ if err := ioutil.WriteFile(out, []byte(strconv.Itoa(v+1)), 0664); err != nil {
+ rc = 6
+ return fmt.Errorf("failed to write updated value: %v\n", err)
+ }
+ return nil
+ })
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err.Error())
+ return rc
+ }
+ return 0
+}
+
+// TestBlockingAndContent tests L's Block and Content fields.
+//
+// It does this by creating one lock goroutine, writing Content to it, then
+func TestBlockingAndContent(t *testing.T) {
+ t.Parallel()
+
+ withTempDir(t, "content", func(tdir string) {
+ lock := filepath.Join(tdir, "lock")
+ heldC := make(chan struct{})
+ blockedC := make(chan struct{})
+ errC := make(chan error)
+
+ // Blocking goroutine: test blocking, try and write content, should not
+ // write because first has already done it.
+ go func(blockedC chan<- struct{}) {
+ // Wait for the first to signal that it has the lock.
+ <-heldC
+
+ l := L{
+ Path: lock,
+ Content: []byte("Second"),
+ Block: func() error {
+ // Notify that we've tried and failed to acquire the lock.
+ if blockedC != nil {
+ close(blockedC)
+ blockedC = nil
+ }
+ time.Sleep(time.Millisecond)
+ return nil
+ },
+ }
+ errC <- l.With(func() error { return nil })
+ }(blockedC)
+
+ // Acquire lock, write content.
+ const expected = "First"
+ l := L{
+ Path: lock,
+ Content: []byte(expected),
+ }
+ err := l.With(func() error {
+ // Signal that we're holding the lock.
+ close(heldC)
+
+ // Wait for our other goroutine to signal that it has tried and failed
+ // to acquire the lock.
+ <-blockedC
+
+ // Release the lock.
+ return nil
+ })
+ if err != nil {
+ t.Fatalf("failed to create lock: %v", err)
+ }
+
+ // Wait for our blocker goroutine to finish.
+ if err := <-errC; err != nil {
+ t.Fatalf("goroutine error: %v", err)
+ }
+
+ // Confirm that the content is written, and that it is the first
+ // goroutine's content.
+ content, err := ioutil.ReadFile(lock)
+ if err != nil {
+ t.Fatalf("failed to read content: %v", err)
+ }
+ if !bytes.Equal(content, []byte(expected)) {
+ t.Fatalf("content does not match expected (%s != %s)", content, expected)
+ }
+ })
+}
diff --git a/fslock/lock_windows.go b/fslock/lock_windows.go
new file mode 100644
index 0000000..eacaef5
--- /dev/null
+++ b/fslock/lock_windows.go
@@ -0,0 +1,70 @@
+// Copyright 2017 by Dan Jacques. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package fslock
+
+import (
+ "os"
+ "syscall"
+ "unsafe"
+)
+
+const errno_ERROR_SHARING_VIOLATION syscall.Errno = 32
+
+func lockImpl(l *L) (Handle, error) {
+ fd, created, err := exclusiveGetOrCreateFile(l.Path)
+ if err != nil {
+ return nil, err
+ }
+
+ // If we just created the file, write Contents to it. If this fails, it is
+ // non-fatal.
+ if created && len(l.Content) > 0 {
+ _, _ = fd.Write(l.Content)
+ }
+
+ // We own the lock on virtue of having accessed the file exclusively.
+ return winLockHandle{fd}, nil
+}
+
+type winLockHandle struct {
+ fd *os.File
+}
+
+func (h winLockHandle) Unlock() error { return h.fd.Close() }
+
+func exclusiveGetOrCreateFile(path string) (*os.File, bool, error) {
+ mod := syscall.NewLazyDLL("kernel32.dll")
+ proc := mod.NewProc("CreateFileW")
+
+ pathp, err := syscall.UTF16PtrFromString(path)
+ if err != nil {
+ return nil, false, err
+ }
+
+ a, _, err := proc.Call(
+ uintptr(unsafe.Pointer(pathp)),
+ uintptr(syscall.GENERIC_READ|syscall.GENERIC_WRITE),
+ 0, // No sharing.
+ uintptr(0), // No security attributes.
+ uintptr(syscall.OPEN_ALWAYS),
+ uintptr(syscall.FILE_ATTRIBUTE_NORMAL),
+ 0, // No template file.
+ )
+ fd := syscall.Handle(a)
+ errno := err.(syscall.Errno)
+ switch errno {
+ case 0:
+ return os.NewFile(uintptr(fd), path), true, nil
+ case syscall.ERROR_ALREADY_EXISTS:
+ // Opened the file, but did not create it.
+ return os.NewFile(uintptr(fd), path), false, nil
+ case errno_ERROR_SHARING_VIOLATION:
+ // We could not open the file because someone else is holding it.
+ return nil, false, ErrLockHeld
+ default:
+ syscall.CloseHandle(fd)
+ return nil, false, err
+ }
+}
diff --git a/pre-commit-go.yml b/pre-commit-go.yml
new file mode 100644
index 0000000..e6e7d43
--- /dev/null
+++ b/pre-commit-go.yml
@@ -0,0 +1,68 @@
+# https://github.com/maruel/pre-commit-go configuration file to run checks
+# automatically on commit, on push and on continuous integration service after
+# a push or on merge of a pull request.
+#
+# See https://godoc.org/github.com/maruel/pre-commit-go/checks for more
+# information.
+
+min_version: 0.4.7
+modes:
+ continuous-integration:
+ checks:
+ build:
+ - build_all: false
+ extra_args: []
+ coverage:
+ - use_global_inference: false
+ use_coveralls: true
+ global:
+ min_coverage: 50
+ max_coverage: 100
+ per_dir_default:
+ min_coverage: 1
+ max_coverage: 100
+ per_dir: {}
+ gofmt:
+ - {}
+ test:
+ - extra_args:
+ - -v
+ - -race
+ max_duration: 1200
+ lint:
+ checks:
+ golint:
+ - blacklist: []
+ govet:
+ - blacklist: []
+ max_duration: 15
+ pre-commit:
+ checks:
+ build:
+ - build_all: false
+ extra_args: []
+ gofmt:
+ - {}
+ test:
+ - extra_args:
+ - -short
+ max_duration: 60
+ pre-push:
+ checks:
+ coverage:
+ - use_global_inference: false
+ use_coveralls: false
+ global:
+ min_coverage: 50
+ max_coverage: 100
+ per_dir_default:
+ min_coverage: 1
+ max_coverage: 100
+ per_dir: {}
+ goimports:
+ - {}
+ test:
+ - extra_args:
+ - -v
+ - -race
+ max_duration: 60