Files
wehub-resource-sync bf9395e022
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:22:54 +08:00

81 lines
1.9 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package lockfile
import (
"errors"
"fmt"
"os"
"path/filepath"
"regexp"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/vfs"
)
// safeIDChars strips path-traversal chars from app IDs.
var safeIDChars = regexp.MustCompile(`[^a-zA-Z0-9._-]`)
// ErrHeld signals retryable contention; callers errors.Is to distinguish from real failures.
var ErrHeld = errors.New("lockfile: lock already held")
type LockFile struct {
path string
file *os.File
}
func New(path string) *LockFile {
return &LockFile{path: path}
}
// ForSubscribe sanitises appID against path traversal before forming the lock filename.
func ForSubscribe(appID string) (*LockFile, error) {
if appID == "" {
return nil, fmt.Errorf("app ID must not be empty")
}
dir := filepath.Join(core.GetConfigDir(), "locks")
if err := vfs.MkdirAll(dir, 0700); err != nil {
return nil, fmt.Errorf("create lock dir: %w", err)
}
safe := safeIDChars.ReplaceAllString(appID, "_")
name := filepath.Base(fmt.Sprintf("subscribe_%s.lock", safe))
path := filepath.Join(dir, name)
return New(path), nil
}
// TryLock acquires an exclusive non-blocking lock; auto-released on process exit.
func (l *LockFile) TryLock() error {
if l.file != nil {
return fmt.Errorf("%w: %s", ErrHeld, l.path)
}
f, err := vfs.OpenFile(l.path, os.O_CREATE|os.O_RDWR, 0600)
if err != nil {
return fmt.Errorf("open lock file: %w", err)
}
if err := tryLockFile(f); err != nil {
f.Close()
return err
}
l.file = f
return nil
}
// Unlock keeps the file on disk to avoid inode-reuse races between unlock and competing open+flock.
func (l *LockFile) Unlock() error {
if l.file == nil {
return nil
}
err := unlockFile(l.file)
closeErr := l.file.Close()
l.file = nil
if err != nil {
return fmt.Errorf("unlock file: %w", err)
}
return closeErr
}
func (l *LockFile) Path() string {
return l.path
}