// Package vncspawner discovers and launches a VNC server as a child process // so the gateway can run with a single command. package vncspawner import ( "fmt" "log" "os/exec" "runtime" "strings" "time" ) // Candidate is a discovered VNC server program + args to launch it. type Candidate struct { Path string Args []string Desc string } // Process is a launched VNC server child process. type Process struct { cmd *exec.Cmd } // Stop terminates the child process. func (p *Process) Stop() { if p == nil || p.cmd == nil || p.cmd.Process == nil { return } log.Printf("vncspawner: stopping %s", p.cmd.String()) _ = p.cmd.Process.Kill() } // Detect searches for an installed VNC server and returns a launch candidate. func Detect() (Candidate, error) { cands := candidates() for _, c := range cands { if c.Path != "" { return c, nil } } return Candidate{}, fmt.Errorf("no supported VNC server found; install one of: %s", strings.Join(detectionHints(), ", ")) } // detectionHints returns human-readable installation hints per OS. func detectionHints() []string { switch runtime.GOOS { case "windows": return []string{"UltraVNC (winvnc.exe)", "TightVNC (tvnserver.exe)"} case "darwin": return []string{"macOS Screen Sharing"} default: return []string{"x11vnc", "TigerVNC (Xvnc/tigervncserver)"} } } // Launch starts a VNC server from a candidate (or explicit command line) as a child. // cmdLine is non-empty it takes precedence (split by spaces, simple shlex). func Launch(c Candidate, cmdLine string) (*Process, error) { var path string var args []string var desc string if cmdLine != "" { parts := splitArgs(cmdLine) if len(parts) == 0 { return nil, fmt.Errorf("empty spawn command") } path = parts[0] args = parts[1:] desc = cmdLine } else { if c.Path == "" { return nil, fmt.Errorf("no VNC server candidate available") } path = c.Path args = c.Args desc = c.Desc } cmd := exec.Command(path, args...) hideWindow(cmd) log.Printf("vncspawner: launching %s -> %s", desc, cmd.String()) if err := cmd.Start(); err != nil { return nil, fmt.Errorf("launch %s: %w", path, err) } p := &Process{cmd: cmd} // Reap/wait the child in background; log exit. go func() { err := cmd.Wait() if err != nil { log.Printf("vncspawner: %s exited: %v", desc, err) } else { log.Printf("vncspawner: %s exited cleanly", desc) } }() // Give the server a moment to start listening. time.Sleep(1200 * time.Millisecond) return p, nil } // splitArgs is a minimal whitespace splitter (no quote handling). For complex // commands prefer an explicit binary path via configuration instead. func splitArgs(s string) []string { var out []string for _, f := range strings.Fields(s) { out = append(out, f) } return out }