broadcast/cmd/broadcast/main.go

275 lines
5.8 KiB
Go

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")
docker = flag.Bool("docker", false, "Interpret as a docker container")
)
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)
}
if *docker && *systemd {
logger.Err(map[string]any{
"msg": "Incompatible flags",
"err": "You may set `-docker` or `-systemd`",
})
os.Exit(1)
}
runnableName := flag.Arg(0)
var runnable broadcast.Runnable
var err error
if *systemd {
runnable = broadcast.NewSystemdUnit(logger, runnableName, *user)
} else if *docker {
runnable = broadcast.NewDockerContainer(logger, runnableName)
} 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, 15*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]
}