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) }