//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 } }