diff --git a/internal/dotlk/dotlk.go b/internal/dotlk/dotlk.go new file mode 100644 index 0000000..3c8d782 --- /dev/null +++ b/internal/dotlk/dotlk.go @@ -0,0 +1,29 @@ +package dotlk + +import ( + "errors" + "io/fs" + "os" +) + +// LockShm creates a directory on disk to prevent SQLite +// from using this path for a shared memory file. +func LockShm(name string) error { + err := os.Mkdir(name, 0777) + if errors.Is(err, fs.ErrExist) { + s, err := os.Lstat(name) + if err == nil && s.IsDir() { + return nil + } + } + return err +} + +// Unlock removes the lock or shared memory file. +func Unlock(name string) error { + err := os.Remove(name) + if errors.Is(err, fs.ErrNotExist) { + return nil + } + return err +} diff --git a/internal/dotlk/dotlk_other.go b/internal/dotlk/dotlk_other.go new file mode 100644 index 0000000..5399a5f --- /dev/null +++ b/internal/dotlk/dotlk_other.go @@ -0,0 +1,13 @@ +//go:build !unix + +package dotlk + +import "os" + +// TryLock returns nil if it acquired the lock, +// fs.ErrExist if another process has the lock. +func TryLock(name string) error { + f, err := os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666) + f.Close() + return err +} diff --git a/internal/dotlk/dotlk_unix.go b/internal/dotlk/dotlk_unix.go new file mode 100644 index 0000000..177ab30 --- /dev/null +++ b/internal/dotlk/dotlk_unix.go @@ -0,0 +1,50 @@ +//go:build unix + +package dotlk + +import ( + "errors" + "io/fs" + "os" + "strconv" + + "golang.org/x/sys/unix" +) + +// TryLock returns nil if it acquired the lock, +// fs.ErrExist if another process has the lock. +func TryLock(name string) error { + for retry := true; retry; retry = false { + f, err := os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666) + if err == nil { + f.WriteString(strconv.Itoa(os.Getpid())) + f.Close() + return nil + } + if !errors.Is(err, fs.ErrExist) { + return err + } + if !removeStale(name) { + break + } + } + return fs.ErrExist +} + +func removeStale(name string) bool { + buf, err := os.ReadFile(name) + if err != nil { + return errors.Is(err, fs.ErrNotExist) + } + + pid, err := strconv.Atoi(string(buf)) + if err != nil { + return false + } + if unix.Kill(pid, 0) == nil { + return false + } + + err = os.Remove(name) + return err == nil || errors.Is(err, fs.ErrNotExist) +} diff --git a/vfs/os_dotlk.go b/vfs/os_dotlk.go index b00a186..7a9c388 100644 --- a/vfs/os_dotlk.go +++ b/vfs/os_dotlk.go @@ -7,6 +7,8 @@ import ( "io/fs" "os" "sync" + + "github.com/ncruces/go-sqlite3/internal/dotlk" ) var ( @@ -28,12 +30,10 @@ func osGetSharedLock(file *os.File) _ErrorCode { name := file.Name() locker := vfsDotLocks[name] if locker == nil { - f, err := os.OpenFile(name+".lock", os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666) - f.Close() - if errors.Is(err, fs.ErrExist) { - return _BUSY // Another process has the lock. - } - if err != nil { + if err := dotlk.TryLock(name + ".lock"); err != nil { + if errors.Is(err, fs.ErrExist) { + return _BUSY // Another process has the lock. + } return _IOERR_LOCK } locker = &vfsDotLocker{} @@ -114,8 +114,7 @@ func osReleaseLock(file *os.File, state LockLevel) _ErrorCode { } if locker.shared == 1 { - err := os.Remove(name + ".lock") - if err != nil && !errors.Is(err, fs.ErrNotExist) { + if err := dotlk.Unlock(name + ".lock"); err != nil { return _IOERR_UNLOCK } delete(vfsDotLocks, name) diff --git a/vfs/shm_dotlk.go b/vfs/shm_dotlk.go index e302db7..17fefe5 100644 --- a/vfs/shm_dotlk.go +++ b/vfs/shm_dotlk.go @@ -6,11 +6,11 @@ import ( "context" "errors" "io/fs" - "os" "sync" "github.com/tetratelabs/wazero/api" + "github.com/ncruces/go-sqlite3/internal/dotlk" "github.com/ncruces/go-sqlite3/internal/util" ) @@ -58,8 +58,7 @@ func (s *vfsShm) Close() error { return nil } - err := os.Remove(s.path) - if err != nil && !errors.Is(err, fs.ErrNotExist) { + if err := dotlk.Unlock(s.path); err != nil { return _IOERR_UNLOCK } delete(vfsShmList, s.path) @@ -82,9 +81,8 @@ func (s *vfsShm) shmOpen() _ErrorCode { return _OK } - // Create a directory on disk to ensure only this process - // uses this path to register a shared memory. - err := os.Mkdir(s.path, 0777) + // Dead man's switch. + err := dotlk.LockShm(s.path) if errors.Is(err, fs.ErrExist) { return _BUSY }