133 lines
4.6 KiB
Go
133 lines
4.6 KiB
Go
// svxdr — live-data daemon for Reflector Dashboard (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.
|
|
}
|
|
}
|
|
}
|