Files
mesh-catalog/modules/pacman/cmd/pacman-tools/watch.go
T
jschoubben 1de9a903c8 issue 1840/S6: keep the node's packages in installed, read at the keeper's start and at every released lock
The keeper reads pacman's database into installed.<node> and writes it only when it differs from the key,
says each change as package.changed, and keeps the last value when the list is over the bus's cap, marking
the node not current until it fits again; the health names the size. The loop runs over injected states,
events, clock, watch and machine, and leaves hooks (start, after a transaction, state watches, wakes) for the
upgrade parts of ADR 0508.
2026-10-22 14:31:33 +02:00

130 lines
3.5 KiB
Go

package main
// The machine's watch (novox/hq ADR 0507 §2, §4): inotify on the directory of pacman's database lock, for the
// lock file deleted (a transaction ended), and on the mirror list's directory, for the list written or moved
// into place (reflector rewrote it). A directory is watched, not the file: the lock comes and goes, and a list
// replaced by a rename is a new file. Nothing is polled.
import (
"errors"
"fmt"
"os"
"sync"
"unsafe"
"golang.org/x/sys/unix"
)
type inotify struct {
f *os.File
lockWD int
mirWD int
lock string
mirrors string
out chan Signal
// pending holds a signal of each kind until the keeper takes it, so a burst of one kind is one signal and
// never crowds out the other.
mu sync.Mutex
pending map[Signal]bool
notify chan struct{}
}
// WatchMachine watches lockDir for lockName deleted and mirrorDir for mirrorName written or moved in.
func WatchMachine(lockDir, lockName, mirrorDir, mirrorName string) (Watcher, error) {
fd, err := unix.InotifyInit1(unix.IN_CLOEXEC | unix.IN_NONBLOCK)
if err != nil {
return nil, fmt.Errorf("inotify: %w", err)
}
w := &inotify{lock: lockName, mirrors: mirrorName, out: make(chan Signal), pending: map[Signal]bool{},
notify: make(chan struct{}, 1)}
if w.lockWD, err = unix.InotifyAddWatch(fd, lockDir, unix.IN_DELETE|unix.IN_MOVED_FROM); err != nil {
unix.Close(fd)
return nil, fmt.Errorf("watching %s: %w", lockDir, err)
}
if w.mirWD, err = unix.InotifyAddWatch(fd, mirrorDir, unix.IN_CLOSE_WRITE|unix.IN_MOVED_TO); err != nil {
unix.Close(fd)
return nil, fmt.Errorf("watching %s: %w", mirrorDir, err)
}
// a non-blocking descriptor as an os.File is read through the runtime's poller, so Close ends a read.
w.f = os.NewFile(uintptr(fd), "inotify")
go w.read()
go w.forward()
return w, nil
}
func (w *inotify) Signals() <-chan Signal { return w.out }
func (w *inotify) Close() error { return w.f.Close() }
func (w *inotify) read() {
defer close(w.notify)
buf := make([]byte, 64*(unix.SizeofInotifyEvent+unix.NAME_MAX+1))
for {
n, err := w.f.Read(buf)
if err != nil {
if !errors.Is(err, os.ErrClosed) {
fmt.Fprintf(os.Stderr, "[pacman] keeper: the machine's watch failed: %v\n", err)
}
return
}
for off := 0; off+unix.SizeofInotifyEvent <= n; {
ev := (*unix.InotifyEvent)(unsafe.Pointer(&buf[off]))
nameAt := off + unix.SizeofInotifyEvent
end := nameAt + int(ev.Len)
if end > n {
break
}
name := cstring(buf[nameAt:end])
off = end
if ev.Mask&unix.IN_Q_OVERFLOW != 0 {
// events were lost: anything may have happened, so both are read again.
w.send(LockReleased)
w.send(MirrorsRewritten)
continue
}
switch {
case int(ev.Wd) == w.lockWD && name == w.lock:
w.send(LockReleased)
case int(ev.Wd) == w.mirWD && name == w.mirrors:
w.send(MirrorsRewritten)
}
}
}
}
// send marks a signal pending; one of the same kind already waiting covers it.
func (w *inotify) send(s Signal) {
w.mu.Lock()
w.pending[s] = true
w.mu.Unlock()
select {
case w.notify <- struct{}{}:
default:
}
}
// forward hands the pending signals to the keeper, in a fixed order, until the watch ends.
func (w *inotify) forward() {
defer close(w.out)
for range w.notify {
for _, s := range []Signal{LockReleased, MirrorsRewritten} {
w.mu.Lock()
due := w.pending[s]
delete(w.pending, s)
w.mu.Unlock()
if due {
w.out <- s
}
}
}
}
func cstring(b []byte) string {
for i, c := range b {
if c == 0 {
return string(b[:i])
}
}
return string(b)
}