//go:build linux package processlock import ( "errors" "fmt" "os" "path/filepath" "sync" "golang.org/x/sys/unix" ) // Lock 持有与文件描述符绑定的 Linux advisory flock。 // 锁文件可以永久存在;只有内核锁状态表示当前所有权。 type Lock struct { mu sync.Mutex file *os.File } // Acquire 以非阻塞方式获取排他锁,并把当前 PID 写入文件用于诊断。 func Acquire(path string) (*Lock, error) { if path == "" { return nil, errors.New("process lock path is required") } absPath, err := filepath.Abs(path) if err != nil { return nil, fmt.Errorf("resolve process lock path: %w", err) } if err := os.MkdirAll(filepath.Dir(absPath), 0o750); err != nil { return nil, fmt.Errorf("create process lock directory: %w", err) } file, err := os.OpenFile(absPath, os.O_CREATE|os.O_RDWR, 0o640) if err != nil { return nil, fmt.Errorf("open process lock: %w", err) } if err := unix.Flock(int(file.Fd()), unix.LOCK_EX|unix.LOCK_NB); err != nil { _ = file.Close() if errors.Is(err, unix.EWOULDBLOCK) || errors.Is(err, unix.EAGAIN) { return nil, ErrAlreadyLocked } return nil, fmt.Errorf("acquire process lock: %w", err) } if err := writeOwnerPID(file); err != nil { _ = unix.Flock(int(file.Fd()), unix.LOCK_UN) _ = file.Close() return nil, err } return &Lock{file: file}, nil } func writeOwnerPID(file *os.File) error { if err := file.Truncate(0); err != nil { return fmt.Errorf("truncate process lock metadata: %w", err) } if _, err := file.Seek(0, 0); err != nil { return fmt.Errorf("seek process lock metadata: %w", err) } if _, err := fmt.Fprintf(file, "%d\n", os.Getpid()); err != nil { return fmt.Errorf("write process lock metadata: %w", err) } if err := file.Sync(); err != nil { return fmt.Errorf("flush process lock metadata: %w", err) } return nil } // Close 释放内核锁并关闭文件描述符,不删除锁文件。 func (l *Lock) Close() error { if l == nil { return nil } l.mu.Lock() defer l.mu.Unlock() if l.file == nil { return nil } file := l.file l.file = nil unlockErr := unix.Flock(int(file.Fd()), unix.LOCK_UN) closeErr := file.Close() return errors.Join(unlockErr, closeErr) }