import (
"bytes"
+ "context"
+ "errors"
"flag"
"fmt"
"go/build"
"go/token"
"io"
"log"
+ "net"
+ "net/http"
"os"
+ "os/exec"
+ "os/signal"
"path"
"path/filepath"
"strings"
+ "time"
+ "cmd/internal/browser"
+ "cmd/internal/quoted"
"cmd/internal/telemetry/counter"
)
showCmd bool // -cmd flag
showSrc bool // -src flag
short bool // -short flag
+ serveHTTP bool // -http flag
)
// usage is a replacement usage function for the flags package.
flagSet.BoolVar(&showCmd, "cmd", false, "show symbols with package docs even if package is a command")
flagSet.BoolVar(&showSrc, "src", false, "show source code for symbol")
flagSet.BoolVar(&short, "short", false, "one-line representation for each symbol")
+ flagSet.BoolVar(&serveHTTP, "http", false, "serve HTML docs over HTTP")
flagSet.Parse(args)
counter.Inc("doc/invocations")
counter.CountFlags("doc/flag:", *flag.CommandLine)
panic(e)
}()
+ if serveHTTP {
+ return doPkgsite(pkg, symbol, method)
+ }
switch {
case symbol == "":
pkg.packageDoc() // The package exists, so we got some output.
}
}
+func doPkgsite(pkg *Package, symbol, method string) error {
+ ctx := context.Background()
+
+ cmdline := "go run golang.org/x/pkgsite/cmd/pkgsite@latest -gorepo=" + buildCtx.GOROOT
+ words, err := quoted.Split(cmdline)
+ port, err := pickUnusedPort()
+ if err != nil {
+ return fmt.Errorf("failed to find port for documentation server: %v", err)
+ }
+ addr := fmt.Sprintf("localhost:%d", port)
+ words = append(words, fmt.Sprintf("-http=%s", addr))
+ cmd := exec.CommandContext(context.Background(), words[0], words[1:]...)
+ cmd.Stdout = os.Stderr
+ cmd.Stderr = os.Stderr
+ // Turn off the default signal handler for SIGINT (and SIGQUIT on Unix)
+ // and instead wait for the child process to handle the signal and
+ // exit before exiting ourselves.
+ signal.Ignore(signalsToIgnore...)
+
+ if err := cmd.Start(); err != nil {
+ return fmt.Errorf("starting pkgsite: %v", err)
+ }
+
+ // Wait for pkgsite to became available.
+ if !waitAvailable(ctx, addr) {
+ cmd.Cancel()
+ cmd.Wait()
+ return errors.New("could not connect to local documentation server")
+ }
+
+ // Open web browser.
+ path := path.Join("http://"+addr, pkg.build.ImportPath)
+ object := symbol
+ if symbol != "" && method != "" {
+ object = symbol + "." + method
+ }
+ if object != "" {
+ path = path + "#" + object
+ }
+ if ok := browser.Open(path); !ok {
+ cmd.Cancel()
+ cmd.Wait()
+ return errors.New("failed to open browser")
+ }
+
+ // Wait for child to terminate. We expect the child process to receive signals from
+ // this terminal and terminate in a timely manner, so this process will terminate
+ // soon after.
+ return cmd.Wait()
+}
+
+// pickUnusedPort finds an unused port by trying to listen on port 0
+// and letting the OS pick a port, then closing that connection and
+// returning that port number.
+// This is inherently racy.
+func pickUnusedPort() (int, error) {
+ l, err := net.Listen("tcp", "localhost:0")
+ if err != nil {
+ return 0, err
+ }
+ port := l.Addr().(*net.TCPAddr).Port
+ if err := l.Close(); err != nil {
+ return 0, err
+ }
+ return port, nil
+}
+
+func waitAvailable(ctx context.Context, addr string) bool {
+ ctx, cancel := context.WithTimeout(ctx, 15*time.Second)
+ defer cancel()
+ for ctx.Err() == nil {
+ req, err := http.NewRequestWithContext(ctx, "HEAD", "http://"+addr, nil)
+ if err != nil {
+ log.Println(err)
+ return false
+ }
+ resp, err := http.DefaultClient.Do(req)
+ if err == nil {
+ resp.Body.Close()
+ return true
+ }
+ }
+ return false
+}
+
// failMessage creates a nicely formatted error message when there is no result to show.
func failMessage(paths []string, symbol, method string) error {
var b bytes.Buffer