On 2026-10-09 the control node's dnsmasq stayed active and running, its journal silent, and refused every question on its private address for twenty minutes; nothing in the module looked past the unit. The module is now Go (its TypeScript tools were never served): a root watch asks the machine's own name on every listen address over UDP and TCP every 15s, restarts the unit after two failing looks (2 min apart, 3 an hour), and says whether that helped. dnsmasq_health is the watch process's declared check, so a restart that did not help becomes the module's unhealthy condition; the service is judged by its unit.
129 lines
3.8 KiB
Go
129 lines
3.8 KiB
Go
// 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)
|
|
}
|
|
}
|