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 <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>
This commit is contained in:
Michael Matloob 2024-09-03 11:19:39 -04:00
parent 39ceaf7961
commit 66e6f5c920
3 changed files with 126 additions and 0 deletions

View 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

View File

@ -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}

View File

@ -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}