mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-09 18:08:31 -10:00
* fix(daemon): publish status files atomically * fix(daemon): bind status publication to directory handle * fix(daemon): validate status directory access * fix(daemon): accept current token directory owner * fix(daemon): migrate owned runtime directories safely * fix(daemon): open Windows security handle relatively * fix(daemon): use current NT directory object * fix(observability): harden existing crash directories * fix(security): bind crash report creation to private root * fix(observability): atomically publish crash reports * fix(observability): preserve committed crash reports * fix(observability): revalidate committed crash path * fix(daemon): secure runtime lifecycle boundaries * fix(daemon): bind status ownership through cleanup * fix(daemon): preserve Windows status security * fix(daemon): retain trusted runtime root for lifecycle * fix(daemon): roll back redirected socket binds * fix(daemon): address status publication review
114 lines
3.4 KiB
Go
114 lines
3.4 KiB
Go
package daemon
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/Gitlawb/zero/internal/lockutil"
|
|
)
|
|
|
|
// Single-instance lock. The stable PID file carries a kernel advisory lock for
|
|
// the daemon's full lifetime. Process exit releases the lock automatically, so
|
|
// stale metadata needs no rename/remove recovery protocol.
|
|
|
|
// ErrAlreadyRunning is returned when a live daemon already holds the lock.
|
|
var ErrAlreadyRunning = errors.New("daemon: another instance is already running")
|
|
|
|
// fileLock is an acquired single-instance lock.
|
|
type fileLock struct {
|
|
lock *lockutil.FileLock
|
|
}
|
|
|
|
// processAlive reports whether pid is a live process. Implemented per-platform
|
|
// (lock_posix.go / lock_windows.go). It is a package var so tests can stub it.
|
|
var processAlive = osProcessAlive
|
|
|
|
// acquireLock takes the single-instance advisory lock at path. isAlive is used
|
|
// only to enrich a contention error with the current holder's PID; the kernel
|
|
// lock, not PID metadata, is authoritative.
|
|
func acquireLock(path string, isAlive func(pid int) bool) (*fileLock, error) {
|
|
return acquireLockWith(isAlive, func() (*lockutil.FileLock, error) {
|
|
return lockutil.TryAcquireFileLock(path)
|
|
}, func() (int, error) {
|
|
return readPidFile(path)
|
|
})
|
|
}
|
|
|
|
func acquireLockRoot(root *os.Root, name, displayPath string, isAlive func(pid int) bool) (*fileLock, error) {
|
|
return acquireLockWith(isAlive, func() (*lockutil.FileLock, error) {
|
|
return lockutil.TryAcquireFileLockRoot(root, name, displayPath)
|
|
}, func() (int, error) {
|
|
return readPidFileRoot(root, name)
|
|
})
|
|
}
|
|
|
|
func acquireLockWith(
|
|
isAlive func(pid int) bool,
|
|
acquire func() (*lockutil.FileLock, error),
|
|
readPID func() (int, error),
|
|
) (*fileLock, error) {
|
|
if isAlive == nil {
|
|
isAlive = processAlive
|
|
}
|
|
lock, err := acquire()
|
|
if err != nil {
|
|
if !errors.Is(err, lockutil.ErrLockHeld) {
|
|
return nil, err
|
|
}
|
|
pid, perr := readPID()
|
|
if perr == nil && pid > 0 && isAlive(pid) {
|
|
return nil, fmt.Errorf("%w (pid %d)", ErrAlreadyRunning, pid)
|
|
}
|
|
return nil, ErrAlreadyRunning
|
|
}
|
|
if err := lock.WriteMetadata([]byte(fmt.Sprintf("%d\n", os.Getpid()))); err != nil {
|
|
return nil, errors.Join(err, lock.Release())
|
|
}
|
|
return &fileLock{lock: lock}, nil
|
|
}
|
|
|
|
// release drops the kernel lock and closes its handle. The stable PID file is
|
|
// deliberately retained so contenders always address the same inode.
|
|
func (l *fileLock) release() error {
|
|
if l == nil || l.lock == nil {
|
|
return nil
|
|
}
|
|
return l.lock.Release()
|
|
}
|
|
|
|
// readPidFile reads and parses the PID recorded in a lock file.
|
|
func readPidFile(path string) (int, error) {
|
|
data, err := os.ReadFile(path)
|
|
return parsePidFile(data, err)
|
|
}
|
|
|
|
func readPidFileRoot(root *os.Root, name string) (int, error) {
|
|
data, err := root.ReadFile(name)
|
|
return parsePidFile(data, err)
|
|
}
|
|
|
|
func parsePidFile(data []byte, err error) (int, error) {
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
metadata := strings.TrimSpace(string(data))
|
|
if pid, err := strconv.Atoi(metadata); err == nil {
|
|
return pid, nil
|
|
}
|
|
pidText, sequenceText, ok := strings.Cut(metadata, "-")
|
|
if !ok || pidText == "" || sequenceText == "" || strings.Contains(sequenceText, "-") {
|
|
return 0, fmt.Errorf("daemon: parse lock PID metadata %q", metadata)
|
|
}
|
|
if _, err := strconv.ParseUint(sequenceText, 10, 64); err != nil {
|
|
return 0, fmt.Errorf("daemon: parse lock holder sequence: %w", err)
|
|
}
|
|
pid, err := strconv.Atoi(pidText)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("daemon: parse lock PID: %w", err)
|
|
}
|
|
return pid, nil
|
|
}
|