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