Files

88 lines
2.4 KiB
Go
Raw Permalink Normal View History

2026-08-15 02:30:36 +08:00
//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 保证释放操作与状态访问的并发安全。
mu sync.Mutex
// file 与内核锁绑定的锁文件描述符。
2026-08-15 02:30:36 +08:00
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
}
// writeOwnerPID 将当前进程 PID 写入锁文件并落盘,供诊断排查。
2026-08-15 02:30:36 +08:00
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)
}