mesh/merge-gate pass: builds dnsmasq, docker, netcheck → ace, g14, novox, shanks; no bus step; 2 wait(s) for a person; every machine composes with the chan…
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery superseded: a newer head of the same pull request
- The dnsmasq watch restarts only a unit that runs, without waiting for its start; a unit stopped, starting or failed is held and said (C2). - dnsmasq_resolve asks only an address the resolver listens on (C3). - Tests for the watch loop keeping its budget, a watch that wrote nothing or stopped looking, and a unit not running (C4). - A restart is remembered as said only once emitted, restarts made before the bundle started are said, and whether one helped is said (C5). - docker_events reads the runtime's whole buffer: complete when it is not full or reaches past the window, never a quiet machine read as lost (C6). - netcheck: refused read with errors.Is; tests for addresses and ports, and for a container named as docker knows it.
146 lines
4.8 KiB
Go
146 lines
4.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. The names are primed silently, so the bundle starting again does not
|
|
// say every name again; the watch's restarts are not — a restart made while the bundle was down is said when
|
|
// it starts (the watch keeps the last hour's). Nothing is remembered as said until its emit succeeded.
|
|
type Announcer struct {
|
|
names map[string]string
|
|
primed bool
|
|
// said is every restart said, by when it was made; judged every one whose outcome was said.
|
|
said, judged map[time.Time]bool
|
|
}
|
|
|
|
// Event is one event to emit, and what saying it records.
|
|
type Event struct {
|
|
Type string
|
|
Body map[string]any
|
|
said func()
|
|
}
|
|
|
|
// Next is what to say 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;
|
|
// restarts the watch made (`restarted`), with why; and, once the looks after a restart decide it, whether it
|
|
// helped (`restart-judged`). read says the roster was read: an unreadable roster says nothing about names,
|
|
// rather than every name removed. An event is remembered by Said, once it was emitted.
|
|
func (a *Announcer) Next(names []Named, read bool, s State) []Event {
|
|
if a.said == nil {
|
|
a.said, a.judged, a.names = map[time.Time]bool{}, map[time.Time]bool{}, map[string]string{}
|
|
}
|
|
var out []Event
|
|
if read {
|
|
now := map[string]string{}
|
|
for _, n := range names {
|
|
now[n.Name] = n.Address
|
|
}
|
|
if !a.primed {
|
|
a.names, a.primed = now, true
|
|
} else {
|
|
for name, address := range now {
|
|
if _, had := a.names[name]; !had {
|
|
out = append(out, Event{"name.added", map[string]any{"name": name, "address": address},
|
|
func() { a.names[name] = address }})
|
|
}
|
|
}
|
|
for name := range a.names {
|
|
if _, has := now[name]; !has {
|
|
out = append(out, Event{"name.removed", map[string]any{"name": name}, func() { delete(a.names, name) }})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for _, r := range s.Restarts {
|
|
at := r.At
|
|
if !a.said[at] {
|
|
body := map[string]any{"unit": Unit, "at": at, "why": r.Why}
|
|
if r.Error != "" {
|
|
body["error"] = r.Error
|
|
}
|
|
out = append(out, Event{"restarted", body, func() { a.said[at] = true }})
|
|
}
|
|
if r.Helped != nil && !a.judged[at] {
|
|
out = append(out, Event{"restart-judged", map[string]any{"unit": Unit, "at": at, "helped": *r.Helped},
|
|
func() { a.judged[at] = true }})
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Said remembers an event as said.
|
|
func (e Event) Said() {
|
|
if e.said != nil {
|
|
e.said()
|
|
}
|
|
}
|
|
|
|
// 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 {
|
|
Tell(m, &a, emit)
|
|
time.Sleep(AnnounceEvery)
|
|
}
|
|
}
|
|
|
|
// Tell is one read and what it says: an event that could not be emitted is said again at the next read.
|
|
func Tell(m *Machine, a *Announcer, emit func(string, any, ...stdio.EmitOptions) error) {
|
|
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, said again at the next read: %v\n", e.Type, err)
|
|
continue
|
|
}
|
|
e.Said()
|
|
}
|
|
}
|