// Package operationlock serializes proxyctl state mutations across processes. package operationlock import ( "context" "errors" "fmt" "path/filepath" "time" "golang.org/x/sys/unix" "proxyctl/internal/securefs" ) type Locker interface { Lock(context.Context) (func(), error) } type Lock struct { path string uid int gid int owned bool } type Group struct{ locks []Locker } func NewGroup(locks ...Locker) *Group { return &Group{locks: append([]Locker(nil), locks...)} } func (g *Group) Lock(ctx context.Context) (func(), error) { var unlocks []func() for _, lock := range g.locks { unlock, err := lock.Lock(ctx) if err != nil { for i := len(unlocks) - 1; i >= 0; i-- { unlocks[i]() } return nil, err } unlocks = append(unlocks, unlock) } return func() { for i := len(unlocks) - 1; i >= 0; i-- { unlocks[i]() } }, nil } func New(path string) *Lock { return &Lock{path: path} } func NewOwned(path string, uid, gid int) *Lock { return &Lock{path: path, uid: uid, gid: gid, owned: true} } func (l *Lock) Lock(ctx context.Context) (func(), error) { if !filepath.IsAbs(l.path) { return nil, errors.New("operation lock path must be absolute") } store := securefs.Store{TrustedRoot: securefs.ExistingTrustedRoot(l.path)} if l.owned { store.Owner = &securefs.Owner{UID: l.uid, GID: l.gid} } file, err := store.OpenRegular(l.path, unix.O_CREAT|unix.O_RDWR|unix.O_NONBLOCK, 0o600) if err != nil { return nil, fmt.Errorf("open operation lock securely: %w", err) } for { err = unix.Flock(int(file.Fd()), unix.LOCK_EX|unix.LOCK_NB) if err == nil { break } if !errors.Is(err, unix.EWOULDBLOCK) && !errors.Is(err, unix.EAGAIN) { file.Close() return nil, fmt.Errorf("acquire operation lock: %w", err) } select { case <-ctx.Done(): file.Close() return nil, ctx.Err() case <-time.After(10 * time.Millisecond): } } return func() { _ = unix.Flock(int(file.Fd()), unix.LOCK_UN); _ = file.Close() }, nil }