// svxdr — live-data daemon for SvxPortal (nodelist + proxy + astmeta + xlink) // Copyright (C) 2026 SA7AUX (Björn) // SPDX-License-Identifier: AGPL-3.0-or-later // // This program is free software: you can redistribute it and/or modify it under // the terms of the GNU Affero General Public License as published by the Free // Software Foundation, either version 3 of the License, or (at your option) any // later version. See the LICENSE file for details. // svxdr — the merged svx daemon: astmeta + nodelist + proxy + xlink in ONE // process, each a subsystem goroutine, toggled per box by /etc/svxlink/svxdr.json. // // One binary, one config. A reflector box runs all subsystems; a pure AllStar // bridge box runs only astmeta. Co-located subsystems share memory (no localhost HTTP hop); // the HTTP talker-meta ingest stays for a REMOTE astmeta feeding this box. // // Phase 1 scaffold: config + module toggles + lifecycle (start / SIGHUP reload / // graceful stop). Subsystems are stubs here and are folded in one at a time // (proxy, then astmeta, then nodelist+xlink) so the existing standalone // binaries keep running until cutover. package main import ( "context" "flag" "log" "os" "os/signal" "sync" "syscall" "svxportal/svxdr/astmeta" "svxportal/svxdr/config" "svxportal/svxdr/nodelist" "svxportal/svxdr/proxy" ) // module is one subsystem. Run blocks until ctx is cancelled; it should return // nil on clean shutdown. reload is called (best-effort) when the config file // changes so a running module can pick up new settings without a restart. type module struct { name string enabled func(*config.Config) bool run func(ctx context.Context, cfg *config.Config) error } var modules = []module{ {"proxy", func(c *config.Config) bool { return c.Modules.Proxy }, proxy.Run}, {"astmeta", func(c *config.Config) bool { return c.Modules.Astmeta }, astmeta.Run}, {"nodelist", func(c *config.Config) bool { return c.Modules.Nodelist }, nodelist.Run}, // xlink is not a separate process — it runs inside nodelist, gated by // modules.xlink in svxdr.json (see nodelist/svxdrcfg.go). } func main() { log.SetFlags(log.LstdFlags) path := flag.String("config", "", "path to svxdr.json (default "+config.DefaultPath+" or $SVXDR_CONFIG)") fAstmeta := flag.Bool("astmeta", false, "run astmeta (overrides modules in config)") fNodelist := flag.Bool("nodelist", false, "run nodelist (overrides modules in config)") fProxy := flag.Bool("proxy", false, "run proxy (overrides modules in config)") fXlink := flag.Bool("xlink", false, "run xlink inside nodelist (overrides modules in config)") flag.Parse() cfgPath := *path if cfgPath == "" { if p := os.Getenv("SVXDR_CONFIG"); p != "" { cfgPath = p } else { cfgPath = config.DefaultPath } } cfg, err := config.Load(cfgPath) if err != nil { log.Fatalf("svxdr: config %s: %v", cfgPath, err) } // If ANY module flag is passed, the flags fully define the set to run on this // box (config "modules" is overridden) — handy for per-box units and testing. if *fAstmeta || *fNodelist || *fProxy || *fXlink { cfg.Modules = config.Modules{Astmeta: *fAstmeta, Nodelist: *fNodelist, Proxy: *fProxy, Xlink: *fXlink} log.Printf("svxdr: module set overridden by flags: astmeta=%v nodelist=%v proxy=%v xlink=%v", *fAstmeta, *fNodelist, *fProxy, *fXlink) } ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() // SIGHUP -> reload config from disk and hand the new config to modules. hup := make(chan os.Signal, 1) signal.Notify(hup, syscall.SIGHUP) var wg sync.WaitGroup started := 0 for _, m := range modules { if !m.enabled(cfg) { continue } started++ wg.Add(1) go func(m module) { defer wg.Done() log.Printf("svxdr: starting module %q", m.name) if err := m.run(ctx, cfg); err != nil { log.Printf("svxdr: module %q exited: %v", m.name, err) } }(m) } if started == 0 { log.Printf("svxdr: no modules enabled in %s (set \"modules\": {...}) — idle", cfgPath) } else { log.Printf("svxdr: %d module(s) running from %s", started, cfgPath) } // lifecycle loop: reload on SIGHUP, exit on signal/context. for { select { case <-ctx.Done(): log.Printf("svxdr: shutting down") wg.Wait() return case <-hup: nc, err := config.Load(cfgPath) if err != nil { log.Printf("svxdr: reload failed, keeping current config: %v", err) continue } cfg = nc log.Printf("svxdr: config reloaded from %s (modules pick up changes on next cycle)", cfgPath) // NOTE: per-module live reload is wired as each subsystem is folded in. } } }