Initial commit.

This commit is contained in:
jdl
2026-06-14 13:24:43 +02:00
parent 56911f5ee1
commit 6628f26bf1
3 changed files with 191 additions and 0 deletions

77
flock.go Normal file
View File

@@ -0,0 +1,77 @@
//go:build linux
// The flock package provides a file-system mediated locking mechanism on linux
// using the `flock` system call.
package flock
import (
"errors"
"os"
"syscall"
)
var ErrLocked = errors.New("locked")
// Lock gets an exclusive lock on the file at the given path. If the file
// doesn't exist, it's created.
func Lock(path string) (*os.File, error) {
return lock(path, syscall.LOCK_EX)
}
// TryLock attempts to lock the file at path. It will return ErrLocked if the
// file is already locked.
func TryLock(path string) (*os.File, error) {
return lock(path, syscall.LOCK_EX|syscall.LOCK_NB)
}
// LockFile is like Lock, but takes an *os.File.
func LockFile(f *os.File) error {
return lockFile(f, syscall.LOCK_EX)
}
// TryLockFile is like TryLock, but takes an *os.File.
func TryLockFile(f *os.File) error {
return lockFile(f, syscall.LOCK_EX|syscall.LOCK_NB)
}
func lockFile(f *os.File, flags int) error {
if err := flock(int(f.Fd()), flags); err != nil {
if flags&syscall.LOCK_NB != 0 && errors.Is(err, syscall.EAGAIN) {
return ErrLocked
}
return err
}
return nil
}
func flock(fd int, how int) error {
_, _, e1 := syscall.Syscall(syscall.SYS_FLOCK, uintptr(fd), uintptr(how), 0)
if e1 != 0 {
return syscall.Errno(e1)
}
return nil
}
func lock(path string, flags int) (*os.File, error) {
perm := os.O_CREATE | os.O_RDWR
f, err := os.OpenFile(path, perm, 0600)
if err != nil {
return nil, err
}
err = lockFile(f, flags)
if err != nil {
f.Close()
f = nil
}
return f, err
}
// Unlock releases the lock acquired via the Lock function.
func Unlock(f *os.File) error {
if err := flock(int(f.Fd()), syscall.LOCK_UN); err != nil {
return err
}
return f.Close()
}

111
flock_test.go Normal file
View File

@@ -0,0 +1,111 @@
package flock
import (
"errors"
"os"
"testing"
"time"
)
func TestLock_basic(t *testing.T) {
const path = "/tmp/fsutil-test-lock"
f, err := Lock(path)
if err != nil {
t.Fatal(err)
}
// locked is closed once the goroutine acquires the lock.
locked := make(chan struct{})
go func() {
f2, err := Lock(path) // blocks until f is unlocked
if err != nil {
t.Error(err)
return
}
close(locked)
Unlock(f2)
}()
// Goroutine should be blocked while we hold the lock.
select {
case <-locked:
t.Fatal("goroutine acquired lock while we hold it")
case <-time.After(50 * time.Millisecond):
}
Unlock(f)
// Now the goroutine should unblock and acquire the lock.
select {
case <-locked:
case <-time.After(time.Second):
t.Fatal("goroutine never acquired lock after release")
}
}
func TestLock_badPath(t *testing.T) {
_, err := Lock("./dne/file.lock")
if err == nil {
t.Fatal("expected error for bad path")
}
}
func TestTryLock(t *testing.T) {
const path = "/tmp/fsutil-test-lock"
f, err := TryLock(path)
if err != nil {
t.Fatal(err)
}
defer Unlock(f)
// While f holds the lock, TryLock should return ErrLocked.
_, err = TryLock(path)
if !errors.Is(err, ErrLocked) {
t.Fatalf("expected ErrLocked, got %v", err)
}
// After unlocking, TryLock should succeed.
if err := Unlock(f); err != nil {
t.Fatal(err)
}
f2, err := TryLock(path)
if err != nil {
t.Fatalf("expected lock after unlock, got %v", err)
}
Unlock(f2)
}
func TestTryLockFile(t *testing.T) {
const path = "/tmp/fsutil-test-lockfile"
f, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0600)
if err != nil {
t.Fatal(err)
}
defer f.Close()
if err := TryLockFile(f); err != nil {
t.Fatal(err)
}
f2, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0600)
if err != nil {
t.Fatal(err)
}
defer f2.Close()
if err := TryLockFile(f2); !errors.Is(err, ErrLocked) {
t.Fatalf("expected ErrLocked, got %v", err)
}
if err := Unlock(f); err != nil {
t.Fatal(err)
}
if err := TryLockFile(f2); err != nil {
t.Fatalf("expected lock after unlock, got %v", err)
}
}

3
go.mod Normal file
View File

@@ -0,0 +1,3 @@
module git.crumpington.com/lib/flock
go 1.25.1