| | |
| | | package singleinstance |
| | | |
| | | import ( |
| | | "errors" |
| | | "fmt" |
| | | "os" |
| | | "path/filepath" |
| | | "syscall" |
| | | |
| | | "voicesnap/internal/paths" |
| | | ) |
| | | |
| | | type unixLock struct { |
| | |
| | | // 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, err |
| | | return nil, lockOpenError{path: lockPath, err: err} |
| | | } |
| | | |
| | | err = syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB) |
| | | if err != nil { |
| | | f.Close() |
| | | return nil, fmt.Errorf("another instance is already running") |
| | | 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 |