Add backend/cmd/moonrelay/main.go

This commit is contained in:
Zac Gaetano 2026-03-31 15:29:57 -04:00
parent a513a64b54
commit 2f97c8db4b

View file

@ -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
}