Skip to content

Commit 66e6f5c

Browse files
committed
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 <[email protected]> Reviewed-by: Sam Thanawalla <[email protected]> Reviewed-by: Jonathan Amsterdam <[email protected]> Reviewed-by: Alan Donovan <[email protected]>
1 parent 39ceaf7 commit 66e6f5c

File tree

3 files changed

+126
-0
lines changed

3 files changed

+126
-0
lines changed

src/cmd/doc/main.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,17 +44,26 @@ package main
4444

4545
import (
4646
"bytes"
47+
"context"
48+
"errors"
4749
"flag"
4850
"fmt"
4951
"go/build"
5052
"go/token"
5153
"io"
5254
"log"
55+
"net"
56+
"net/http"
5357
"os"
58+
"os/exec"
59+
"os/signal"
5460
"path"
5561
"path/filepath"
5662
"strings"
63+
"time"
5764

65+
"cmd/internal/browser"
66+
"cmd/internal/quoted"
5867
"cmd/internal/telemetry/counter"
5968
)
6069

@@ -66,6 +75,7 @@ var (
6675
showCmd bool // -cmd flag
6776
showSrc bool // -src flag
6877
short bool // -short flag
78+
serveHTTP bool // -http flag
6979
)
7080

7181
// 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) {
107117
flagSet.BoolVar(&showCmd, "cmd", false, "show symbols with package docs even if package is a command")
108118
flagSet.BoolVar(&showSrc, "src", false, "show source code for symbol")
109119
flagSet.BoolVar(&short, "short", false, "one-line representation for each symbol")
120+
flagSet.BoolVar(&serveHTTP, "http", false, "serve HTML docs over HTTP")
110121
flagSet.Parse(args)
111122
counter.Inc("doc/invocations")
112123
counter.CountFlags("doc/flag:", *flag.CommandLine)
@@ -152,6 +163,9 @@ func do(writer io.Writer, flagSet *flag.FlagSet, args []string) (err error) {
152163
panic(e)
153164
}()
154165

166+
if serveHTTP {
167+
return doPkgsite(pkg, symbol, method)
168+
}
155169
switch {
156170
case symbol == "":
157171
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) {
168182
}
169183
}
170184

185+
func doPkgsite(pkg *Package, symbol, method string) error {
186+
ctx := context.Background()
187+
188+
cmdline := "go run golang.org/x/pkgsite/cmd/pkgsite@latest -gorepo=" + buildCtx.GOROOT
189+
words, err := quoted.Split(cmdline)
190+
port, err := pickUnusedPort()
191+
if err != nil {
192+
return fmt.Errorf("failed to find port for documentation server: %v", err)
193+
}
194+
addr := fmt.Sprintf("localhost:%d", port)
195+
words = append(words, fmt.Sprintf("-http=%s", addr))
196+
cmd := exec.CommandContext(context.Background(), words[0], words[1:]...)
197+
cmd.Stdout = os.Stderr
198+
cmd.Stderr = os.Stderr
199+
// Turn off the default signal handler for SIGINT (and SIGQUIT on Unix)
200+
// and instead wait for the child process to handle the signal and
201+
// exit before exiting ourselves.
202+
signal.Ignore(signalsToIgnore...)
203+
204+
if err := cmd.Start(); err != nil {
205+
return fmt.Errorf("starting pkgsite: %v", err)
206+
}
207+
208+
// Wait for pkgsite to became available.
209+
if !waitAvailable(ctx, addr) {
210+
cmd.Cancel()
211+
cmd.Wait()
212+
return errors.New("could not connect to local documentation server")
213+
}
214+
215+
// Open web browser.
216+
path := path.Join("http://"+addr, pkg.build.ImportPath)
217+
object := symbol
218+
if symbol != "" && method != "" {
219+
object = symbol + "." + method
220+
}
221+
if object != "" {
222+
path = path + "#" + object
223+
}
224+
if ok := browser.Open(path); !ok {
225+
cmd.Cancel()
226+
cmd.Wait()
227+
return errors.New("failed to open browser")
228+
}
229+
230+
// Wait for child to terminate. We expect the child process to receive signals from
231+
// this terminal and terminate in a timely manner, so this process will terminate
232+
// soon after.
233+
return cmd.Wait()
234+
}
235+
236+
// pickUnusedPort finds an unused port by trying to listen on port 0
237+
// and letting the OS pick a port, then closing that connection and
238+
// returning that port number.
239+
// This is inherently racy.
240+
func pickUnusedPort() (int, error) {
241+
l, err := net.Listen("tcp", "localhost:0")
242+
if err != nil {
243+
return 0, err
244+
}
245+
port := l.Addr().(*net.TCPAddr).Port
246+
if err := l.Close(); err != nil {
247+
return 0, err
248+
}
249+
return port, nil
250+
}
251+
252+
func waitAvailable(ctx context.Context, addr string) bool {
253+
ctx, cancel := context.WithTimeout(ctx, 15*time.Second)
254+
defer cancel()
255+
for ctx.Err() == nil {
256+
req, err := http.NewRequestWithContext(ctx, "HEAD", "http://"+addr, nil)
257+
if err != nil {
258+
log.Println(err)
259+
return false
260+
}
261+
resp, err := http.DefaultClient.Do(req)
262+
if err == nil {
263+
resp.Body.Close()
264+
return true
265+
}
266+
}
267+
return false
268+
}
269+
171270
// failMessage creates a nicely formatted error message when there is no result to show.
172271
func failMessage(paths []string, symbol, method string) error {
173272
var b bytes.Buffer

src/cmd/doc/signal_notunix.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright 2012 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build plan9 || windows
6+
7+
package main
8+
9+
import (
10+
"os"
11+
)
12+
13+
var signalsToIgnore = []os.Signal{os.Interrupt}

src/cmd/doc/signal_unix.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright 2012 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build unix || js || wasip1
6+
7+
package main
8+
9+
import (
10+
"os"
11+
"syscall"
12+
)
13+
14+
var signalsToIgnore = []os.Signal{os.Interrupt, syscall.SIGQUIT}

0 commit comments

Comments
 (0)