// The dnsmasq module's Go bundle: one of the mesh's resolvers (novox/hq ADR 0194, 0223). // // Started with no arguments it is served by the node's runtime as the operator account, over MCP on stdio // (ADR 0188, ADR 0193): the module's tools and its health, and beside them the events the resolver's // answers give — a machine's name it now answers or no longer does, and a restart the watch made. Started // as `dnsmasq-tools watch` it is the module's long-running process, as root: it asks the resolver a real // question on every listen address and restarts it, within a budget, when it stops answering (watch.go, // novox/hq issue 348). stdout is the MCP channel; everything else is said on stderr. package main import ( "context" "fmt" "os" "os/signal" "syscall" "time" stdio "git.novox.be/novox/mesh-sdk/go" ) func main() { if len(os.Args) > 1 { if os.Args[1] != "watch" || len(os.Args) != 2 { fmt.Fprintln(os.Stderr, "usage: dnsmasq-tools [watch]") os.Exit(2) } ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, os.Interrupt) defer stop() m := Here() if err := m.Watch(ctx, ExecRunner, m.WriteState); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } return } m := Here() go Announce(m, stdio.Emit) if err := stdio.Serve("", Tools(m)); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } // AnnounceEvery is how often the bundle reads the roster and the watch for what to say. var AnnounceEvery = 30 * time.Second // Announcer remembers what was said, primed silently so a restart of the bundle does not say every name // and every restart again. type Announcer struct { names map[string]string restarts map[time.Time]bool // primed says the names were read once; heard that the watch's restarts were. primed, heard bool } // Event is one event to emit. type Event struct { Type string Body map[string]any } // Next is what changed since the last read: names the resolver now answers (`name.added`) or no longer // does (`name.removed`) — the mesh's names having reached this resolver, apart from the controller's own // events — and restarts the watch made (`restarted`), each with why and whether it helped once known. // // read says the roster was read: an unreadable roster says nothing about names, rather than every name // removed. func (a *Announcer) Next(names []Named, read bool, s State) []Event { now := map[string]string{} for _, n := range names { now[n.Name] = n.Address } var out []Event if a.primed && read { for name, address := range now { if _, had := a.names[name]; !had { out = append(out, Event{"name.added", map[string]any{"name": name, "address": address}}) } } for name := range a.names { if _, has := now[name]; !has { out = append(out, Event{"name.removed", map[string]any{"name": name}}) } } } if a.restarts == nil { a.restarts = map[time.Time]bool{} } for _, r := range s.Restarts { if a.restarts[r.At] { continue } a.restarts[r.At] = true if a.heard { body := map[string]any{"unit": Unit, "at": r.At, "why": r.Why} if r.Error != "" { body["error"] = r.Error } out = append(out, Event{"restarted", body}) } } a.heard = true if read { a.names, a.primed = now, true } return out } // Announce says the events for as long as the bundle runs. func Announce(m *Machine, emit func(string, any, ...stdio.EmitOptions) error) { time.Sleep(2 * time.Second) // the SDK reaches the bus once Serve runs var a Announcer for { var names []Named text, err := m.read(NodesFile) if err == nil { names = AnsweredNames(text) } s, _, _ := m.ReadState() for _, e := range a.Next(names, err == nil, s) { if err := emit(e.Type, e.Body); err != nil { fmt.Fprintf(os.Stderr, "dnsmasq: %s not said: %v\n", e.Type, err) } } time.Sleep(AnnounceEvery) } }