package lockfile import ( "fmt" "os" "path/filepath" "time" ) const ( lockFileName = ".ekdns.lock" staleTimeout = 2 * time.Minute retryInterval = 200 * time.Millisecond maxWait = 2 * time.Second ) // Lock represents an acquired lock file that can be released. type Lock interface { Release() error } type fileLock struct { path string } // Release deletes the lock file. It is safe to call more than once. func (l *fileLock) Release() error { err := os.Remove(l.path) if err != nil && os.IsNotExist(err) { return nil } return err } // Acquire creates a lock file in dir and returns a Lock that can release it. // It waits up to maxWait for a stale or otherwise held lock to clear. func Acquire(dir string) (Lock, error) { lockPath := filepath.Join(dir, lockFileName) deadline := time.Now().Add(maxWait) for { f, err := os.OpenFile(lockPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644) if err == nil { // Write PID for debugging (not used for staleness detection) fmt.Fprintf(f, "%d\n", os.Getpid()) f.Close() return &fileLock{path: lockPath}, nil } if !os.IsExist(err) { return nil, fmt.Errorf("creating lock file %s: %w", lockPath, err) } // Lock file exists — check staleness info, statErr := os.Stat(lockPath) if statErr == nil && time.Since(info.ModTime()) > staleTimeout { os.Remove(lockPath) continue // Retry after breaking stale lock } if time.Now().After(deadline) { return nil, fmt.Errorf("could not acquire lock file %s: another instance may be running", lockPath) } time.Sleep(retryInterval) } }