package version import ( "errors" "fmt" "os" "path/filepath" "time" ) var ErrUpdateInProgress = errors.New("another MPV Manager update is already in progress") type updateLock struct { file *os.File anchor *os.File path string } func tryAcquireUpdateLock(path string) (*updateLock, error) { directory := filepath.Dir(path) if err := os.MkdirAll(directory, 0755); err != nil { return nil, fmt.Errorf("create update lock directory: %w", err) } anchor, err := openUpdateLockAnchor(directory) if err != nil { return nil, fmt.Errorf("open update lock anchor: %w", err) } if anchor != nil { if err := lockFile(anchor); err != nil { _ = anchor.Close() if isLockContended(err) { return nil, ErrUpdateInProgress } return nil, fmt.Errorf("lock update directory anchor: %w", err) } } file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0600) if err != nil { _ = releaseUpdateLockAnchor(anchor) return nil, fmt.Errorf("open update lock: %w", err) } if err := lockFile(file); err != nil { _ = file.Close() _ = releaseUpdateLockAnchor(anchor) if isLockContended(err) { return nil, ErrUpdateInProgress } return nil, fmt.Errorf("lock update transaction: %w", err) } return &updateLock{file: file, anchor: anchor, path: path}, nil } func acquireUpdateLockWithRetry(path string, timeout time.Duration) (*updateLock, error) { deadline := time.Now().Add(timeout) for { lock, err := tryAcquireUpdateLock(path) if err == nil { return lock, nil } if !errors.Is(err, ErrUpdateInProgress) || time.Now().After(deadline) { return nil, err } time.Sleep(100 * time.Millisecond) } } func (l *updateLock) release() error { if l == nil || l.file == nil { return nil } unlockErr := unlockFile(l.file) closeErr := l.file.Close() l.file = nil anchorErr := releaseUpdateLockAnchor(l.anchor) l.anchor = nil if unlockErr != nil { return fmt.Errorf("unlock update transaction: %w", unlockErr) } if closeErr != nil { return fmt.Errorf("close update lock: %w", closeErr) } if anchorErr != nil { return anchorErr } return nil }