package main // The mesh's one mirror list (novox/hq ADR 0585). Before it each node's reflector ranked its own list on its own weekly // timer (to-be 42 Phase 1), and on 2026-10-31 one node's first mirror lagged, so its upgrade found nothing to install // while the others moved on. Now: // // - the list is one key of the module's own state, `mirrors.list` (§1): the servers in order, when and by which node // they were ranked, and the options; // - it is ranked once per weekly round, by the node that opens it, inside the write that opens it (§2): reflector // ranks with the module's options (reflector.conf, its `--save` left out), only the mirrors whose last sync is no // older than the newest found are kept, and the round's key names the list's revision; // - each node's keeper writes /etc/pacman.d/mirrorlist whole from the key, through the root helper, when the key // changes and when the file differs (the keeper watches the file), so a hand edit comes back; a list that is // empty or cannot be read is never written, and the node keeps its last (§3); reflector stays installed as the // ranking tool, and its weekly timer is declared stopped and disabled; // - `pacman_mirrors` answers the shared list, its age, its ranker and whether this node's file matches; with // refresh it asks this node's keeper to rank again for the mesh, refused while a round runs, naming it (§4). import ( "bytes" "context" "encoding/json" "errors" "fmt" "os" "regexp" "sort" "strings" "time" stdio "git.novox.be/novox/mesh-sdk/go" ) // Where the list is and where the module's reflector options are. const ( Mirrorlist = "/etc/pacman.d/mirrorlist" ReflectorConf = "/etc/xdg/reflector/reflector.conf" ) // RankTimeout bounds one ranking: reflector downloads from every candidate mirror to rate it, which takes longer than // a tool call may (CallTimeout), so the keeper ranks, never a call. const RankTimeout = 3 * time.Minute // mostMirrors bounds a list the root helper writes: far more than a ranking keeps (the module's --latest). const mostMirrors = 100 // Mirrors is a mirror list file as found: the servers in force, those commented out, and the generator's header. type Mirrors struct { Servers []string `json:"servers"` Commented int `json:"commented_servers"` GeneratedBy string `json:"generated_by,omitempty"` When string `json:"generated_when,omitempty"` } // mirrorlistHeader is the first line of the file the mesh writes, by which it is known as the mesh's. const mirrorlistHeader = "# The mesh's mirror list (module pacman, novox/hq ADR 0585)" // ParseMirrorlist reads the servers in force, those commented out, and the generator's header. func ParseMirrorlist(text string) Mirrors { m := Mirrors{Servers: []string{}} for _, l := range lines(text) { l = strings.TrimSpace(l) switch { case strings.HasPrefix(l, "Server"): if _, v, ok := strings.Cut(l, "="); ok { m.Servers = append(m.Servers, strings.TrimSpace(v)) } case strings.HasPrefix(strings.TrimLeft(l, "# "), "Server"): m.Commented++ case l == mirrorlistHeader: m.GeneratedBy = "the mesh" case strings.Contains(l, "generated by Reflector"): m.GeneratedBy = "reflector" case strings.HasPrefix(l, "# When:"): m.When = strings.TrimSpace(strings.TrimPrefix(l, "# When:")) case strings.HasPrefix(l, "## Generated on"): m.GeneratedBy, m.When = "the distribution's mirrorlist", strings.TrimSpace(strings.TrimPrefix(l, "## Generated on")) } } return m } // mirrorServer is a server line's value pacman takes from the mesh: http(s), a host, a path, and the repository and // architecture left to pacman. Nothing else reaches the file: not a newline, not a second option. var mirrorServer = regexp.MustCompile(`^https?://[A-Za-z0-9.-]+(:[0-9]+)?(/[A-Za-z0-9._~%+@-]+)*/\$repo/os/\$arch$`) // checkServers refuses a list that is empty, too long, or holds anything but a server. func checkServers(servers []string) error { if len(servers) == 0 { return errors.New("the list is empty") } if len(servers) > mostMirrors { return fmt.Errorf("the list holds %d servers, more than %d", len(servers), mostMirrors) } for _, s := range servers { if !mirrorServer.MatchString(s) { return fmt.Errorf("%q is not a mirror's server", s) } } return nil } // RenderMirrorlist is the mirror list file whole, as every node writes it from the shared list: the same servers are // the same bytes, so a node compares its file with the list by its bytes. func RenderMirrorlist(servers []string) []byte { var b bytes.Buffer b.WriteString(mirrorlistHeader + "\n") b.WriteString("# Written whole from the bus (the state mirrors of the module pacman): a hand edit is written back.\n") b.WriteString("# pacman_mirrors says when it was ranked and by which node.\n") for _, s := range servers { b.WriteString("Server = " + s + "\n") } return b.Bytes() } // ReflectorOptions is the options reflector.conf sets, each line split into its flag and its value as reflector reads // them: a `--save` is left out, since the keeper writes the file (ADR 0585 §3). func ReflectorOptions(conf string) []string { out := []string{} for _, l := range lines(conf) { l = strings.TrimSpace(l) if l == "" || strings.HasPrefix(l, "#") { continue } flag, value, _ := strings.Cut(l, " ") if flag == "--save" { continue } out = append(out, flag) if value = strings.Trim(strings.TrimSpace(value), `"'`); value != "" { out = append(out, value) } } return out } // rankedMirror is one mirror reflector ranked, in its order, with its last sync (zero when the status says none). type rankedMirror struct { URL string LastSync time.Time } // reflectorTime is how reflector's --info writes a last sync. const reflectorTime = "2006-01-02 15:04:05 MST" // ParseReflectorInfo reads reflector's --info: one block per mirror in the order it ranked them, its server first and // then `field : value` lines, of which last_sync is read. func ParseReflectorInfo(text string) []rankedMirror { var out []rankedMirror open := false for _, l := range strings.Split(text, "\n") { l = strings.TrimSpace(l) switch { case l == "": open = false case !open && strings.Contains(l, "://") && !strings.Contains(l, " "): out, open = append(out, rankedMirror{URL: l}), true case open: if k, v, ok := strings.Cut(l, ":"); ok && strings.TrimSpace(k) == "last_sync" { if t, err := time.Parse(reflectorTime, strings.TrimSpace(v)); err == nil { out[len(out)-1].LastSync = t.UTC() } } } } return out } // TailLength is how many of the next freshest mirrors follow the freshest ones in the list (ADR 0586 §1). const TailLength = 4 // Freshest orders the mirrors by last sync, newest first, in reflector's order among equals, and keeps every mirror // whose last sync equals the newest found (the freshest, count of them) followed by up to TailLength of the next // freshest (ADR 0586 §1, which changes ADR 0585 §2); one whose last sync is unknown (zero) is left out. func Freshest(ranked []rankedMirror) (servers []string, newest time.Time, freshest int) { var known []rankedMirror for _, m := range ranked { if !m.LastSync.IsZero() { known = append(known, m) } } sort.SliceStable(known, func(i, j int) bool { return known[i].LastSync.After(known[j].LastSync) }) if len(known) == 0 { return nil, newest, 0 } newest = known[0].LastSync for _, m := range known { if m.LastSync.Equal(newest) { freshest++ } } for i, m := range known { if i >= freshest+TailLength { break } servers = append(servers, m.URL) } return servers, newest, freshest } // withoutUnknownSync zeroes the last sync of every mirror that reflector printed as the moment it ran: --info writes an // unknown last sync (None) through gmtime(None), which is the current time, so a last sync at or after start (the // second the run began) cannot be a real one, since the status reflector reads is older than the run. func withoutUnknownSync(ranked []rankedMirror, start time.Time) { start = start.UTC().Truncate(time.Second) for i := range ranked { if !ranked[i].LastSync.Before(start) { ranked[i].LastSync = time.Time{} } } } // reflectorRun runs reflector with args on this machine, within RankTimeout; its cache in the temporary directory, since // the keeper's account has no home of its own. A test replaces it. var reflectorRun = func(ctx context.Context, args ...string) Ran { return execWithin(ctx, RankTimeout, []string{"XDG_CACHE_HOME=" + os.TempDir()}, "reflector", args...) } // RankMirrors ranks the mirrors with reflector and the module's options (conf, reflector.conf's text) and keeps the // freshest: the mesh's list, ranked by node at now for round (none for a refresh). A ranking that fails or keeps no // mirror is an error, never an empty list (ADR 0585 §3). func RankMirrors(conf []byte, node string, now time.Time, round string) (MirrorList, error) { opts := ReflectorOptions(string(conf)) r := reflectorRun(context.Background(), append(append([]string(nil), opts...), "--info")...) if r.Status != 0 || r.Err != "" { return MirrorList{}, failure("reflector", "reflector", r) } ranked := ParseReflectorInfo(r.Stdout) withoutUnknownSync(ranked, now) servers, newest, freshest := Freshest(ranked) if err := checkServers(servers); err != nil { return MirrorList{}, fmt.Errorf("reflector ranked no list the mesh can use (%d mirrors read): %w", len(ranked), err) } return MirrorList{Servers: servers, RankedAt: now.UTC(), RankedBy: node, Options: opts, Newest: newest, Freshest: freshest, Round: round}, nil } // readMirrorList is the shared list and its revision; nil when there is none yet. func readMirrorList(r interface { Get(key string) (*stdio.StateEntry, error) }) (*MirrorList, uint64, error) { e, err := r.Get(MirrorsKey) if err != nil || e == nil { return nil, 0, err } var l MirrorList if err := json.Unmarshal(e.Value, &l); err != nil { return nil, e.Revision, fmt.Errorf("%s.%s is not readable: %w", StateMirrors, MirrorsKey, err) } return &l, e.Revision, nil } // ---- the keeper's part ------------------------------------------------------------------------------------------- // rankMirrors ranks the mesh's list and writes it to the shared key, answering its revision. guard, when set, is asked // right before the write and refuses it with its error: ranking takes minutes, and what held when it began may not. func (u *Upgrades) rankMirrors(round string, guard func() error) (uint64, error) { if u.Rank == nil || u.Mirrors == nil { return 0, errors.New("this keeper ranks no mirrors") } conf, err := u.Machine.ReadFile(ReflectorConf) if err != nil { return 0, fmt.Errorf("the module's reflector options cannot be read: %w", err) } list, err := u.Rank(conf, round) if err != nil { return 0, err } if guard != nil { if err := guard(); err != nil { return 0, err } } return u.Mirrors.Put(MirrorsKey, list) } // mirrorsForRound ranks the list in the write that opens round (ADR 0585 §2): the revision the round opens with, and // why it was not ranked again when it was not, the round then opening with the list there is. func (u *Upgrades) mirrorsForRound(round string) (uint64, string) { rev, err := u.rankMirrors(round, nil) if err == nil { return rev, "" } note := "the mirrors were not ranked again: " + err.Error() if u.Mirrors != nil { if _, kept, rerr := readMirrorList(u.Mirrors); rerr == nil && kept > 0 { return kept, note + fmt.Sprintf("; the round opens with the list at revision %d", kept) } } return 0, note + "; there is no shared list yet, and every node keeps its own" } // runningRound is the round that runs now, none when every node of the latest round passed: a refresh is refused // while one runs, so every node of a round upgrades from the list it opened with (ADR 0585 §4). func runningRound(rounds, runs Bucket, now time.Time) (string, error) { id, err := latestRound(rounds) if err != nil || id == "" { return "", err } v, err := readRound(rounds, runs, id) if err != nil || v == nil { return "", err } for _, n := range RoundState(v, now, 0) { if n.State != InRoundPassed { return id, nil } } return "", nil } // refreshRefused is the words of a refresh refused during round. func refreshRefused(round string) error { return fmt.Errorf("round %s runs: every node of it upgrades from the list it opened with, so the mirrors are ranked "+ "again only once every node of it passed (pacman_round says where it stands)", round) } // takeMirrorsAsk ranks the list again for the mesh on a refresh, unless a round runs: asked before the ranking and again // right before its write, so a refresh that began before a round opened never writes after the round's key exists. func (u *Upgrades) takeMirrorsAsk() error { noRound := func() error { if round, err := runningRound(u.Rounds, u.Runs, u.Machine.Now()); err != nil { return err } else if round != "" { return refreshRefused(round) } return nil } if err := noRound(); err != nil { return err } _, err := u.rankMirrors("", noRound) return err } // roundMirrorsWritten is why this node may not start its run in round yet, empty when it may: its mirror list file // must hold the list the round opened with (Round.Mirrors), written now when it does not (ADR 0585 §2: every node of // the round upgrades from that list). A round that opened with no list (none ranked yet) asks nothing of the file. // A refusal is a refused run, tried again an hour later as every refused run of a round is. func (u *Upgrades) roundMirrorsWritten(round string) string { e, err := u.Rounds.Get(round) if err != nil || e == nil { return fmt.Sprintf("round %s cannot be read to know its mirror list: %v", round, err) } var r Round if err := json.Unmarshal(e.Value, &r); err != nil { return fmt.Sprintf("round %s is not readable: %v", round, err) } if r.Mirrors == 0 { return "" } if u.Mirrors == nil { return "this keeper reads no mirror list, and round " + round + " opened with one" } list, rev, err := readMirrorList(u.Mirrors) switch { case err != nil: return "the mesh's mirror list cannot be read: " + err.Error() case list == nil || rev != r.Mirrors: return fmt.Sprintf("round %s opened with the mirror list at revision %d, and the bus holds revision %d", round, r.Mirrors, rev) } if _, err := u.WriteMirrors(); err != nil { return "the round's mirror list is not written to " + Mirrorlist + ": " + err.Error() } if have, err := u.Machine.ReadFile(Mirrorlist); err != nil || !bytes.Equal(have, RenderMirrorlist(list.Servers)) { return fmt.Sprintf("%s does not hold the mirror list round %s opened with (revision %d)", Mirrorlist, round, r.Mirrors) } return "" } // WriteMirrors writes this node's mirror list file whole from the shared list when they differ, through the root // helper, answering whether it wrote. No list yet writes nothing; a list that is empty or cannot be read is never // written, and the node keeps its last (ADR 0585 §3). func (u *Upgrades) WriteMirrors() (bool, error) { if u.Mirrors == nil { return false, nil } list, _, err := readMirrorList(u.Mirrors) if err != nil { return false, fmt.Errorf("%v; this node keeps its mirror list", err) } if list == nil { return false, nil } if err := checkServers(list.Servers); err != nil { return false, fmt.Errorf("the shared mirror list is not written: %v; this node keeps its mirror list", err) } want := RenderMirrorlist(list.Servers) if have, err := u.Machine.ReadFile(Mirrorlist); err == nil && bytes.Equal(have, want) { return false, nil } if err := u.Root.Mirrorlist(list.Servers); err != nil { return false, fmt.Errorf("writing %s: %w", Mirrorlist, err) } return true, nil } // ---- the tool ----------------------------------------------------------------------------------------------------- // MirrorsView answers pacman_mirrors (ADR 0585 §4): the shared list, its revision and age and who ranked it, and this // node's file beside it; with refresh, the ask of this node's keeper to rank again for the mesh, refused while a round // runs. func (m *Machine) MirrorsView(refresh bool) (map[string]any, error) { if err := m.states(); err != nil { return nil, err } out := map[string]any{"node": m.Node} list, rev, err := readMirrorList(m.States(StateMirrors)) if err != nil { return nil, err } out["shared"], out["revision"] = list, rev if list != nil { n := min(max(list.Freshest, 1), len(list.Servers)) out["freshest"], out["tail"] = list.Servers[:n], list.Servers[n:] } text, ferr := m.ReadFile(Mirrorlist) if ferr != nil { out["file_error"] = ferr.Error() } file := ParseMirrorlist(string(text)) if list == nil { out["note"] = "no shared list yet: it is ranked when the next round opens, or by pacman_mirrors with refresh" out["file"] = file } else { out["age"] = m.Now().Sub(list.RankedAt).Round(time.Minute).String() out["ranked_by"] = list.RankedBy matches := ferr == nil && bytes.Equal(text, RenderMirrorlist(list.Servers)) out["file_matches"] = matches if !matches { out["file"] = file } } if t, err := m.unitProps("reflector.timer", "LoadState", "UnitFileState", "ActiveState"); err == nil { out["reflector_timer"] = t } if !refresh { return out, nil } now := m.Now() round, err := runningRound(upgradeState(StateRounds), upgradeState(StateRuns), now) if err != nil { return nil, err } if round != "" { return nil, refreshRefused(round) } a, err := AskMirrors(upgradeState(StateAsks), thisNode(), now) if err != nil { return nil, err } out["asked"] = a.ID out["next"] = "the keeper of " + a.Node + " ranks the mirrors for the mesh now, which takes a minute or two; every " + "node writes the new list once it lands, and pacman_mirrors says it" return out, nil } // AskMirrors writes an ask for node's keeper to rank the mesh's mirrors again. func AskMirrors(asks Bucket, node string, now time.Time) (Ask, error) { if node == "" { return Ask{}, errors.New("no node to ask: the runtime names none (MESH_NODE)") } a := Ask{ID: AskID(now), Node: node, Act: ActMirrors, At: now.UTC()} if _, err := asks.Create(NodeKey(node, a.ID), a); err != nil { return Ask{}, fmt.Errorf("writing the ask: %w", err) } return a, nil }