//go:build !windows
|
|
package singleinstance
|
|
import (
|
"errors"
|
"fmt"
|
"os"
|
"path/filepath"
|
"syscall"
|
|
"voicesnap/internal/paths"
|
)
|
|
type unixLock struct {
|
file *os.File
|
}
|
|
// Acquire tries to acquire a file lock. Returns error if another instance holds it.
|
func Acquire() (Lock, error) {
|
lockPath := filepath.Join(os.TempDir(), "privatevoice-input.lock")
|
if ensureErr := paths.Ensure(); ensureErr != nil {
|
return nil, fmt.Errorf("ensure app lock dir: %w", ensureErr)
|
}
|
appLockPath := paths.File("privatevoice-input.lock")
|
return acquireWithFallback(lockPath, appLockPath)
|
}
|
|
func acquireWithFallback(primaryPath, fallbackPath string) (Lock, error) {
|
lock, err := acquirePath(primaryPath)
|
if err == nil {
|
return lock, nil
|
}
|
|
var unavailable lockUnavailableError
|
if errors.As(err, &unavailable) {
|
return nil, err
|
}
|
|
lock, fallbackErr := acquirePath(fallbackPath)
|
if fallbackErr != nil {
|
return nil, fmt.Errorf("acquire temp lock: %w; acquire app lock: %v", err, fallbackErr)
|
}
|
return lock, nil
|
}
|
|
type lockOpenError struct {
|
path string
|
err error
|
}
|
|
func (e lockOpenError) Error() string {
|
return fmt.Sprintf("open lock %s: %v", e.path, e.err)
|
}
|
|
func (e lockOpenError) Unwrap() error {
|
return e.err
|
}
|
|
type lockAccessError struct {
|
path string
|
err error
|
}
|
|
func (e lockAccessError) Error() string {
|
return fmt.Sprintf("lock %s: %v", e.path, e.err)
|
}
|
|
func (e lockAccessError) Unwrap() error {
|
return e.err
|
}
|
|
type lockUnavailableError struct {
|
path string
|
err error
|
}
|
|
func (e lockUnavailableError) Error() string {
|
return fmt.Sprintf("another instance is already running: %s", e.path)
|
}
|
|
func (e lockUnavailableError) Unwrap() error {
|
return e.err
|
}
|
|
func acquirePath(lockPath string) (Lock, error) {
|
f, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0600)
|
if err != nil {
|
return nil, lockOpenError{path: lockPath, err: err}
|
}
|
|
err = syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
|
if err != nil {
|
f.Close()
|
if errors.Is(err, syscall.EWOULDBLOCK) || errors.Is(err, syscall.EAGAIN) {
|
return nil, lockUnavailableError{path: lockPath, err: err}
|
}
|
return nil, lockAccessError{path: lockPath, err: err}
|
}
|
|
return &unixLock{file: f}, nil
|
}
|
|
func (l *unixLock) Release() {
|
if l.file != nil {
|
syscall.Flock(int(l.file.Fd()), syscall.LOCK_UN)
|
l.file.Close()
|
l.file = nil
|
}
|
}
|