// Copyright (c) 2026, Pim van Pelt package main import ( "context" "errors" "flag" "fmt" "log/slog" "net/http" "os" "os/signal" "strings" "syscall" "time" buildinfo "git.ipng.ch/ipng/vpp-maglev/cmd" ) func main() { if err := run(); err != nil { slog.Error("startup-fatal", "err", err) os.Exit(1) } } func run() error { // All env vars are prefixed with MAGLEV_FRONTEND_ so a single // /etc/default/vpp-maglev (or a Docker env file) can be shared // with maglevd without its MAGLEV_LOG_LEVEL / MAGLEV_GRPC_ADDR / // etc. leaking into this process's config. printVersion := flag.Bool("version", false, "print version and exit") servers := stringFlag("server", "", "MAGLEV_FRONTEND_SERVERS", "comma-separated maglevd gRPC addresses (required)") listen := stringFlag("listen", ":8080", "MAGLEV_FRONTEND_LISTEN", "HTTP listen address") logLevel := stringFlag("log-level", "info", "MAGLEV_FRONTEND_LOG_LEVEL", "log verbosity (debug|info|warn|error)") flag.Parse() if *printVersion { fmt.Printf("maglevd-frontend %s (commit %s, built %s)\n", buildinfo.Version(), buildinfo.Commit(), buildinfo.Date()) return nil } var level slog.Level if err := level.UnmarshalText([]byte(*logLevel)); err != nil { return fmt.Errorf("invalid log level %q: %w", *logLevel, err) } slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: level}))) slog.Info("starting", "version", buildinfo.Version(), "commit", buildinfo.Commit(), "date", buildinfo.Date()) addrs := parseServers(*servers) if len(addrs) == 0 { return errors.New("at least one -server address is required") } ctx, cancel := context.WithCancel(context.Background()) defer cancel() broker := NewBroker() clients := make([]*maglevClient, 0, len(addrs)) for _, addr := range addrs { c, err := newMaglevClient(addr, broker) if err != nil { return fmt.Errorf("connect %s: %w", addr, err) } clients = append(clients, c) c.Start(ctx) slog.Info("maglevd-configured", "name", c.name, "address", c.address) } admin := adminCreds{ User: os.Getenv("MAGLEV_FRONTEND_USER"), Password: os.Getenv("MAGLEV_FRONTEND_PASSWORD"), } admin.Enabled = admin.User != "" && admin.Password != "" if admin.Enabled { slog.Info("admin-enabled", "user", admin.User) } else { slog.Info("admin-disabled", "reason", "MAGLEV_FRONTEND_USER and MAGLEV_FRONTEND_PASSWORD must both be set and non-empty") } mux := http.NewServeMux() registerHandlers(mux, clients, broker, admin) srv := &http.Server{ Addr: *listen, Handler: mux, ReadHeaderTimeout: 10 * time.Second, } // Ignore SIGHUP so a controlling-terminal disconnect (or any // stray process-group SIGHUP) doesn't kill the daemon — the // default Go handler terminates the process with "Hangup", // which is the wrong behaviour for a long-running network // service. SIGTERM / SIGINT remain the clean-shutdown signals. signal.Ignore(syscall.SIGHUP) sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT) errCh := make(chan error, 1) go func() { slog.Info("http-listening", "addr", *listen) if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { errCh <- err } }() select { case sig := <-sigCh: slog.Info("shutdown", "signal", sig) case err := <-errCh: cancel() return err } cancel() shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second) defer shutdownCancel() _ = srv.Shutdown(shutdownCtx) for _, c := range clients { c.Close() } return nil } func parseServers(s string) []string { var out []string for _, part := range strings.Split(s, ",") { if p := strings.TrimSpace(part); p != "" { out = append(out, p) } } return out } func stringFlag(name, defaultVal, envKey, usage string) *string { val := defaultVal if v := os.Getenv(envKey); v != "" { val = v } return flag.String(name, val, fmt.Sprintf("%s (env: %s)", usage, envKey)) }