Files
mesh-catalog/modules/dnsmasq/cmd/dnsmasq-tools/main.go
T
jschoubben 2b2397abe0 dnsmasq: ask the resolver a real question, and restart it when it stops answering (hq issue 348)
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.
2026-10-09 15:04:45 +02:00

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