package main import ( "context" "flag" "fmt" "net/http" "os" "os/signal" "strings" "syscall" "time" "github.com/go-chi/chi/v5" "git.abbiamoundominio.org/blallo/broadcast" "git.sr.ht/~blallo/logz/interface" "git.sr.ht/~blallo/logz/zlog" ) var ( debug = flag.Bool("debug", false, "Enable debug logging") addr = flag.String("addr", ":8080", "Address to bind to, in the 'ipaddress:port' format") startNow = flag.Bool("start", false, "If set, try to start the process now") tlsCert = flag.String("tls-cert", "", "Path to certificate file for TLS connection") tlsKey = flag.String("tls-key", "", "Path to key file for TLS connection") systemd = flag.Bool("systemd", false, "Interpret as systemd unit") user = flag.Bool("systemd-as-user", false, "Connect to systemd via DBus as current user") ) func main() { logger := zlog.NewConsoleLogger() flag.Parse() if flag.NArg() < 1 { logger.Err(map[string]any{ "msg": "Wrong number of arguments", }) os.Exit(1) } if *debug { logger.SetLevel(logz.LogDebug) } runnableName := flag.Arg(0) var runnable broadcast.Runnable var err error if *systemd { runnable = broadcast.NewSystemdUnit(logger, runnableName, *user) } else { cmdLine := flag.Args()[1:] runnable, err = broadcast.NewProcess(logger, runnableName, cmdLine...) } if err != nil { logger.Err(map[string]any{ "msg": "Failed to create runnable process", "err": err.Error(), }) os.Exit(2) } radio, err := broadcast.NewRadio(logger, runnable) if err != nil { logger.Err(map[string]any{ "msg": "Failed to start", "err": err, }) os.Exit(2) } ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) defer cancel() start := make(chan os.Signal) stop := make(chan os.Signal) status := make(chan os.Signal) signal.Notify(start, syscall.SIGUSR1) signal.Notify(stop, syscall.SIGTSTP) signal.Notify(status, syscall.SIGUSR2) go func() { for { select { case <-ctx.Done(): return case <-start: resp := <-radio.Start() if resp != nil { logger.Warn(map[string]any{ "msg": "Failed to start", "context": "os", "err": resp.(error).Error(), }) } else { logger.Info(map[string]any{ "msg": "Started", "context": "os", }) } case <-stop: resp := <-radio.Stop() if resp != nil { logger.Warn(map[string]any{ "msg": "Failed to stop", "context": "os", "err": resp.(error).Error(), }) } else { logger.Info(map[string]any{ "msg": "Stopped", "context": "os", }) } case <-status: resp := <-radio.Status() for i, line := range resp.([]string) { logger.Info(map[string]any{ "msg": line, "context": "os", "lineNum": i, }) } } } }() if *startNow { go func() { resp, err := withTimeout(ctx, radio.Start()) if err != nil { logger.Err(map[string]any{ "msg": "Cannot start", "context": "os", "err": err.Error(), }) } else { if resp != nil { logger.Info(map[string]any{ "msg": "Started", "context": "os", "resp": resp.(error).Error(), }) } } }() } handler, err := setupHandler(radio, logger, *addr) if err != nil { logger.Err(map[string]any{ "msg": "Cannot create handler", "err": err.Error(), }) os.Exit(2) } if isValidTLSConf(logger) { go func() { if err := http.ListenAndServeTLS(*addr, *tlsCert, *tlsKey, handler); err != nil { logger.Err(map[string]any{ "msg": "Could not bind", "err": err.Error(), }) } }() } else { go func() { if err := http.ListenAndServe(*addr, handler); err != nil { logger.Err(map[string]any{ "msg": "Could not bind", "err": err.Error(), }) } }() } if err := radio.Run(ctx); err != nil { logger.Err(map[string]any{ "msg": "Execution failed", "err": err.Error(), }) os.Exit(2) } } func withTimeout[T any](ctx context.Context, respCh <-chan T) (zero T, err error) { shortCtx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() select { case <-shortCtx.Done(): return zero, shortCtx.Err() case resp := <-respCh: return resp, nil } } func isValidTLSConf(logger logz.Logger) bool { if *tlsCert == "" && *tlsKey == "" { return false } if (*tlsCert != "" && *tlsKey == "") || (*tlsCert == "" && *tlsKey != "") { logger.Err(map[string]any{ "msg": "You must specify both the path to the certificate and to the key to use TLS", }) return false } return true } func setupHandler(radio *broadcast.Radio, logger logz.Logger, addr string) (http.Handler, error) { handler := &radioHandler{ radio: radio, logger: logger, } wsHandler := &livenessHandler{ radio: radio, logger: logger, } testUI := &testUIHandler{ baseAddr: getBaseAddr(addr), logger: logger, } assetsHandler, err := newAssetsHandler(logger) if err != nil { return nil, err } router := chi.NewRouter() router.Handle("/", assetsHandler) router.Handle("/{elem}", assetsHandler) router.Handle("/static/js/{elem}", assetsHandler) router.Handle("/static/css/{elem}", assetsHandler) router.Post("/start", handler.Start) router.Post("/stop", handler.Stop) router.Get("/status", handler.Status) router.Handle("/liveness", wsHandler) router.Get("/test_ui", testUI.TestUI) return router, nil } func getBaseAddr(addr string) string { parts := strings.Split(addr, ":") if parts[0] == "" { parts[0] = "localhost" } if len(parts) == 2 { return fmt.Sprintf("%s:%s", parts[0], parts[1]) } return parts[0] }