@@ -44,17 +44,26 @@ package main
44
44
45
45
import (
46
46
"bytes"
47
+ "context"
48
+ "errors"
47
49
"flag"
48
50
"fmt"
49
51
"go/build"
50
52
"go/token"
51
53
"io"
52
54
"log"
55
+ "net"
56
+ "net/http"
53
57
"os"
58
+ "os/exec"
59
+ "os/signal"
54
60
"path"
55
61
"path/filepath"
56
62
"strings"
63
+ "time"
57
64
65
+ "cmd/internal/browser"
66
+ "cmd/internal/quoted"
58
67
"cmd/internal/telemetry/counter"
59
68
)
60
69
66
75
showCmd bool // -cmd flag
67
76
showSrc bool // -src flag
68
77
short bool // -short flag
78
+ serveHTTP bool // -http flag
69
79
)
70
80
71
81
// 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) {
107
117
flagSet .BoolVar (& showCmd , "cmd" , false , "show symbols with package docs even if package is a command" )
108
118
flagSet .BoolVar (& showSrc , "src" , false , "show source code for symbol" )
109
119
flagSet .BoolVar (& short , "short" , false , "one-line representation for each symbol" )
120
+ flagSet .BoolVar (& serveHTTP , "http" , false , "serve HTML docs over HTTP" )
110
121
flagSet .Parse (args )
111
122
counter .Inc ("doc/invocations" )
112
123
counter .CountFlags ("doc/flag:" , * flag .CommandLine )
@@ -152,6 +163,9 @@ func do(writer io.Writer, flagSet *flag.FlagSet, args []string) (err error) {
152
163
panic (e )
153
164
}()
154
165
166
+ if serveHTTP {
167
+ return doPkgsite (pkg , symbol , method )
168
+ }
155
169
switch {
156
170
case symbol == "" :
157
171
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) {
168
182
}
169
183
}
170
184
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
+
171
270
// failMessage creates a nicely formatted error message when there is no result to show.
172
271
func failMessage (paths []string , symbol , method string ) error {
173
272
var b bytes.Buffer
0 commit comments