blob: 6409626978dac8e78a25595deac1bd6e058ff24f [file] [log] [blame]
// 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
}
}
}