]> Cypherpunks repositories - gostls13.git/commitdiff
cmd/doc: add support for starting pkgsite instance for docs
authorMichael Matloob <matloob@golang.org>
Tue, 3 Sep 2024 15:19:39 +0000 (11:19 -0400)
committerMichael Matloob <matloob@golang.org>
Mon, 3 Feb 2025 17:26:03 +0000 (09:26 -0800)
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 <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Sam Thanawalla <samthanawalla@google.com>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
src/cmd/doc/main.go
src/cmd/doc/signal_notunix.go [new file with mode: 0644]
src/cmd/doc/signal_unix.go [new file with mode: 0644]

index 502de097f50688e2326b35c0660327334c4fb4de..a199991c21d42e1b75d9eeacddc8ffb7fdea48bd 100644 (file)
@@ -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 (file)
index 0000000..3b8fa9e
--- /dev/null
@@ -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 (file)
index 0000000..52431c2
--- /dev/null
@@ -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}