From 66e6f5c9202ae98c3e1d3830972cd13559fb28f2 Mon Sep 17 00:00:00 2001 From: Michael Matloob Date: Tue, 3 Sep 2024 11:19:39 -0400 Subject: [PATCH] cmd/doc: add support for starting pkgsite instance for docs This change adds a new flag "-http" to cmd/doc which enables starting a pkgsite instance. -http will start a pkgsite instance and navigate to the page for the requested package, at the anchor for the item requested. For #68106 Change-Id: Ic1c113795cb2e1035e99c89c8e972c799342385b Reviewed-on: https://go-review.googlesource.com/c/go/+/628175 LUCI-TryBot-Result: Go LUCI Reviewed-by: Sam Thanawalla Reviewed-by: Jonathan Amsterdam Reviewed-by: Alan Donovan --- src/cmd/doc/main.go | 99 +++++++++++++++++++++++++++++++++++ src/cmd/doc/signal_notunix.go | 13 +++++ src/cmd/doc/signal_unix.go | 14 +++++ 3 files changed, 126 insertions(+) create mode 100644 src/cmd/doc/signal_notunix.go create mode 100644 src/cmd/doc/signal_unix.go diff --git a/src/cmd/doc/main.go b/src/cmd/doc/main.go index 502de097f5..a199991c21 100644 --- a/src/cmd/doc/main.go +++ b/src/cmd/doc/main.go @@ -44,17 +44,26 @@ package main 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" ) @@ -66,6 +75,7 @@ var ( 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. @@ -107,6 +117,7 @@ func do(writer io.Writer, flagSet *flag.FlagSet, args []string) (err error) { 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) @@ -152,6 +163,9 @@ func do(writer io.Writer, flagSet *flag.FlagSet, args []string) (err error) { panic(e) }() + if serveHTTP { + return doPkgsite(pkg, symbol, method) + } switch { case symbol == "": pkg.packageDoc() // The package exists, so we got some output. @@ -168,6 +182,91 @@ func do(writer io.Writer, flagSet *flag.FlagSet, args []string) (err error) { } } +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 diff --git a/src/cmd/doc/signal_notunix.go b/src/cmd/doc/signal_notunix.go new file mode 100644 index 0000000000..3b8fa9e080 --- /dev/null +++ b/src/cmd/doc/signal_notunix.go @@ -0,0 +1,13 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build plan9 || windows + +package main + +import ( + "os" +) + +var signalsToIgnore = []os.Signal{os.Interrupt} diff --git a/src/cmd/doc/signal_unix.go b/src/cmd/doc/signal_unix.go new file mode 100644 index 0000000000..52431c221b --- /dev/null +++ b/src/cmd/doc/signal_unix.go @@ -0,0 +1,14 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build unix || js || wasip1 + +package main + +import ( + "os" + "syscall" +) + +var signalsToIgnore = []os.Signal{os.Interrupt, syscall.SIGQUIT} -- 2.51.0