103 lines
2.7 KiB
Go
103 lines
2.7 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
)
|
|
|
|
// hostOpen launches the platform's "open" handler for target (a URL, file path,
|
|
// or opaque string) without blocking. Errors after launch are logged, not
|
|
// returned, since the GUI handler is fire-and-forget.
|
|
//
|
|
// It resolves the opener via realOpenerPath so a host that also shadows
|
|
// `xdg-open`/`open` with this binary doesn't recurse back into client mode.
|
|
func hostOpen(target string) error {
|
|
name, args := openCommand(target)
|
|
bin, err := realOpenerPath(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cmd := exec.Command(bin, args...)
|
|
if err := cmd.Start(); err != nil {
|
|
return fmt.Errorf("start %s: %w", bin, err)
|
|
}
|
|
go func() {
|
|
if err := cmd.Wait(); err != nil {
|
|
log.Printf("open %q exited: %v", target, err)
|
|
}
|
|
}()
|
|
return nil
|
|
}
|
|
|
|
func openCommand(target string) (string, []string) {
|
|
switch runtime.GOOS {
|
|
case "darwin":
|
|
return "open", []string{target}
|
|
case "windows":
|
|
return "rundll32", []string{"url.dll,FileProtocolHandler", target}
|
|
default:
|
|
return "xdg-open", []string{target}
|
|
}
|
|
}
|
|
|
|
// defaultOpenerName is the platform's native opener, used as the fallback target
|
|
// when we were invoked as `open-proxy open` rather than under a shadowed name.
|
|
func defaultOpenerName() string {
|
|
if runtime.GOOS == "darwin" {
|
|
return "open"
|
|
}
|
|
return "xdg-open"
|
|
}
|
|
|
|
// localOpen runs the real opener (the next `name` on $PATH that isn't us) for
|
|
// arg. Used as the client-side fallback when the host is unreachable.
|
|
func localOpen(name, arg string) error {
|
|
bin, err := realOpenerPath(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cmd := exec.Command(bin, arg)
|
|
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
|
|
return cmd.Run()
|
|
}
|
|
|
|
// realOpenerPath finds an executable named `name` on $PATH that is not this
|
|
// binary (so a symlink shadowing `xdg-open`/`open` doesn't recurse into us).
|
|
func realOpenerPath(name string) (string, error) {
|
|
self := selfFileInfo()
|
|
for _, dir := range filepath.SplitList(os.Getenv("PATH")) {
|
|
if dir == "" {
|
|
continue
|
|
}
|
|
cand := filepath.Join(dir, name)
|
|
info, err := os.Stat(cand) // follows symlinks
|
|
if err != nil || info.IsDir() || info.Mode()&0o111 == 0 {
|
|
continue
|
|
}
|
|
// Compare by file identity (device+inode), not path string: a symlink or
|
|
// hardlink pointing back at this binary is recognized as us regardless of
|
|
// how its path is spelled or whether $PATH entries are relative.
|
|
if self != nil && os.SameFile(self, info) {
|
|
continue
|
|
}
|
|
return cand, nil
|
|
}
|
|
return "", fmt.Errorf("no real %q found on PATH", name)
|
|
}
|
|
|
|
func selfFileInfo() os.FileInfo {
|
|
exe, err := os.Executable()
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
info, err := os.Stat(exe)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return info
|
|
}
|