From c890bfd1c3bf520bb59365fc95de8ff37f8563ff Mon Sep 17 00:00:00 2001 From: zgaetano Date: Tue, 31 Mar 2026 15:29:58 -0400 Subject: [PATCH] Add backend/internal/moonlight/launcher.go --- backend/internal/moonlight/launcher.go | 85 ++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 backend/internal/moonlight/launcher.go diff --git a/backend/internal/moonlight/launcher.go b/backend/internal/moonlight/launcher.go new file mode 100644 index 0000000..f7e47c3 --- /dev/null +++ b/backend/internal/moonlight/launcher.go @@ -0,0 +1,85 @@ +// Package moonlight handles launching the Moonlight client. +// It locates the Moonlight executable and launches a stream session +// to a specific host and app. +package moonlight + +import ( + "fmt" + "os/exec" + "runtime" +) + +// DefaultPaths are the well-known install locations per platform. +var DefaultPaths = map[string][]string{ + "windows": { + `C:\Program Files\Moonlight Game Streaming\Moonlight.exe`, + `C:\Program Files (x86)\Moonlight Game Streaming\Moonlight.exe`, + }, + "darwin": { + "/Applications/Moonlight.app/Contents/MacOS/Moonlight", + }, + "linux": { + "/usr/bin/moonlight", + "/usr/local/bin/moonlight", + }, +} + +// Launcher knows how to start Moonlight. +type Launcher struct { + execPath string +} + +// New creates a Launcher, auto-detecting the Moonlight binary. +// Pass an empty string to use auto-detection. +func New(overridePath string) (*Launcher, error) { + if overridePath != "" { + if path, err := exec.LookPath(overridePath); err == nil { + return &Launcher{execPath: path}, nil + } + return nil, fmt.Errorf("moonlight not found at %q", overridePath) + } + + // Try PATH first + if path, err := exec.LookPath("moonlight"); err == nil { + return &Launcher{execPath: path}, nil + } + + // Try well-known locations + for _, candidate := range DefaultPaths[runtime.GOOS] { + if path, err := exec.LookPath(candidate); err == nil { + return &Launcher{execPath: path}, nil + } + } + + // Return a launcher even if not found — we'll surface the error at connect time + return &Launcher{}, nil +} + +// ExecPath returns the resolved path to the Moonlight binary. +func (l *Launcher) ExecPath() string { + return l.execPath +} + +// Stream launches a Moonlight stream to the given host + app. +// app is typically "Desktop" for a full desktop stream. +func (l *Launcher) Stream(hostIP, app string) error { + if l.execPath == "" { + return fmt.Errorf("Moonlight executable not found — please install Moonlight or set MOONLIGHT_PATH") + } + // moonlight stream + cmd := exec.Command(l.execPath, "stream", hostIP, app) + cmd.Stdout = nil + cmd.Stderr = nil + return cmd.Start() // non-blocking — let Moonlight own its window +} + +// Pair runs the Moonlight pairing flow for a host. +func (l *Launcher) Pair(hostIP string) error { + if l.execPath == "" { + return fmt.Errorf("Moonlight executable not found") + } + cmd := exec.Command(l.execPath, "pair", hostIP) + cmd.Stdout = nil + cmd.Stderr = nil + return cmd.Start() +}