From 2f97c8db4b36c06605cfde245e1d32075017b20a Mon Sep 17 00:00:00 2001 From: zgaetano Date: Tue, 31 Mar 2026 15:29:57 -0400 Subject: [PATCH] Add backend/cmd/moonrelay/main.go --- backend/cmd/moonrelay/main.go | 114 ++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 backend/cmd/moonrelay/main.go diff --git a/backend/cmd/moonrelay/main.go b/backend/cmd/moonrelay/main.go new file mode 100644 index 0000000..366860c --- /dev/null +++ b/backend/cmd/moonrelay/main.go @@ -0,0 +1,114 @@ +// moonrelay — self-hosted streaming manager backend. +// +// In Docker mode the React SPA is embedded at build time via go:embed. +// Set MOONRELAY_NO_EMBED=1 to skip the SPA (API-only / dev mode). +// +// Environment variables: +// MOONRELAY_PORT - HTTP listen port (default: 8080) +// MOONRELAY_HOST - HTTP listen address (default: 0.0.0.0) +// TS_AUTHKEY - Tailscale auth key (optional; prompts login if unset) +// TS_HOSTNAME - Tailscale device hostname (default: moonrelay) +// MOONLIGHT_PATH - Override path to moonlight binary +// MOONRELAY_NO_TS - Set "1" to disable Tailscale (LAN-only) +package main + +import ( + "context" + "embed" + "fmt" + "io/fs" + "log" + "net" + "net/http" + "os" + "os/signal" + "syscall" + + "moonrelay/internal/api" + "moonrelay/internal/discovery" + "moonrelay/internal/moonlight" + tsnode "moonrelay/internal/tailscale" +) + +// ui holds the React production build, copied here by the Dockerfile +// before `go build`. The path must match where the build output lands. +// +//go:embed ui +var uiFiles embed.FS + +func main() { + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer cancel() + + port := env("MOONRELAY_PORT", "8080") + host := env("MOONRELAY_HOST", "0.0.0.0") + noTS := os.Getenv("MOONRELAY_NO_TS") == "1" + + // --- Moonlight launcher --- + launcher, err := moonlight.New(os.Getenv("MOONLIGHT_PATH")) + if err != nil { + log.Printf("Warning: %v", err) + launcher, _ = moonlight.New("") + } + + // --- mDNS discovery --- + disc := discovery.New() + go disc.Run(ctx) + + // --- Tailscale node (optional) --- + var ts *tsnode.Node + if !noTS { + log.Println("Starting embedded Tailscale node…") + ts, err = tsnode.New(ctx, tsnode.Config{ + Hostname: env("TS_HOSTNAME", "moonrelay"), + AuthKey: os.Getenv("TS_AUTHKEY"), + }) + if err != nil { + log.Printf("Warning: Tailscale failed to start (%v) — running in LAN-only mode", err) + ts = nil + } else { + if addr := ts.LocalAddr(); addr != "" { + log.Printf("Tailscale connected: %s", addr) + } else { + log.Println("Tailscale: waiting for auth (check logs for login URL)") + } + } + } + + // --- Embedded SPA --- + // Strip the "ui/" prefix so the FS root contains index.html directly. + uiRoot, err := fs.Sub(uiFiles, "ui") + if err != nil { + log.Fatalf("Failed to access embedded UI: %v", err) + } + + // --- HTTP server --- + srv := api.New(disc, launcher, ts, uiRoot) + addr := fmt.Sprintf("%s:%s", host, port) + ln, err := net.Listen("tcp", addr) + if err != nil { + log.Fatalf("Cannot listen on %s: %v", addr, err) + } + log.Printf("Moonlight Relay listening on http://%s", addr) + + httpSrv := &http.Server{Handler: srv.Handler()} + go func() { + if err := httpSrv.Serve(ln); err != nil && err != http.ErrServerClosed { + log.Printf("HTTP server error: %v", err) + } + }() + + <-ctx.Done() + log.Println("Shutting down…") + _ = httpSrv.Close() + if ts != nil { + _ = ts.Close() + } +} + +func env(key, fallback string) string { + if v := os.Getenv(key); v != "" { + return v + } + return fallback +}