moonlight-relay-server/backend/internal/moonlight/launcher.go

86 lines
2.4 KiB
Go
Raw Permalink Normal View History

// 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 <host> <app>
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()
}