1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
|
// Copyright 2009 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.
// godoc: Go Documentation Server
// Web server tree:
//
// http://godoc/ main landing page
// http://godoc/doc/ serve from $GOROOT/doc - spec, mem, tutorial, etc.
// http://godoc/src/ serve files from $GOROOT/src; .go gets pretty-printed
// http://godoc/cmd/ serve documentation about commands (TODO)
// http://godoc/pkg/ serve documentation about packages
// (idea is if you say import "compress/zlib", you go to
// http://godoc/pkg/compress/zlib)
//
// Command-line interface:
//
// godoc packagepath [name ...]
//
// godoc compress/zlib
// - prints doc for package compress/zlib
// godoc crypto/block Cipher NewCMAC
// - prints doc for Cipher and NewCMAC in package crypto/block
package main
import (
"bytes";
"container/vector";
"flag";
"fmt";
"go/ast";
"go/doc";
"go/parser";
"go/printer";
"go/scanner";
"go/token";
"http";
"io";
"log";
"os";
pathutil "path";
"sort";
"strings";
"sync";
"template";
"time";
)
const Pkg = "/pkg/"; // name for auto-generated package documentation tree
type delayTime struct {
mutex sync.RWMutex;
minutes int;
}
func (dt *delayTime) set(minutes int) {
dt.mutex.Lock();
dt.minutes = minutes;
dt.mutex.Unlock();
}
func (dt *delayTime) backoff(max int) {
dt.mutex.Lock();
dt.minutes *= 2;
if dt.minutes > max {
dt.minutes = max
}
dt.mutex.Unlock();
}
func (dt *delayTime) get() int {
dt.mutex.RLock();
defer dt.mutex.RUnlock();
return dt.minutes;
}
type timeStamp struct {
mutex sync.RWMutex;
seconds int64;
}
func (ts *timeStamp) set() {
ts.mutex.Lock();
ts.seconds = time.Seconds();
ts.mutex.Unlock();
}
func (ts *timeStamp) get() int64 {
ts.mutex.RLock();
defer ts.mutex.RUnlock();
return ts.seconds;
}
var (
verbose = flag.Bool("v", false, "verbose mode");
// file system roots
goroot string;
pkgroot = flag.String("pkgroot", "src/pkg", "root package source directory (if unrooted, relative to goroot)");
tmplroot = flag.String("tmplroot", "lib/godoc", "root template directory (if unrooted, relative to goroot)");
// periodic sync
syncCmd = flag.String("sync", "", "sync command; disabled if empty");
syncMin = flag.Int("sync_minutes", 0, "sync interval in minutes; disabled if <= 0");
syncDelay delayTime; // actual sync delay in minutes; usually syncDelay == syncMin, but delay may back off exponentially
syncTime timeStamp; // time of last p4 sync
// layout control
tabwidth = flag.Int("tabwidth", 4, "tab width");
html = flag.Bool("html", false, "print HTML in command-line mode");
// server control
httpaddr = flag.String("http", "", "HTTP service address (e.g., ':6060')");
)
func init() {
goroot = os.Getenv("GOROOT");
if goroot == "" {
goroot = "/home/r/go-release/go";
}
flag.StringVar(&goroot, "goroot", goroot, "Go root directory");
syncTime.set(); // have a reasonable initial value
}
// ----------------------------------------------------------------------------
// Support
func isGoFile(dir *os.Dir) bool {
return
dir.IsRegular() &&
!strings.HasPrefix(dir.Name, ".") && // ignore .files
pathutil.Ext(dir.Name) == ".go" &&
!strings.HasSuffix(dir.Name, "_test.go"); // ignore test files
}
func isPkgDir(dir *os.Dir) bool {
return dir.IsDirectory() && len(dir.Name) > 0 && dir.Name[0] != '_';
}
// ----------------------------------------------------------------------------
// Parsing
// A single error in the parsed file.
type parseError struct {
src []byte; // source before error
line int; // line number of error
msg string; // error message
}
// All the errors in the parsed file, plus surrounding source code.
// Each error has a slice giving the source text preceding it
// (starting where the last error occurred). The final element in list[]
// has msg = "", to give the remainder of the source code.
// This data structure is handed to the templates parseerror.txt and parseerror.html.
//
type parseErrors struct {
filename string; // path to file
list []parseError; // the errors
src []byte; // the file's entire source code
}
// Parses a file (path) and returns the corresponding AST and
// a sorted list (by file position) of errors, if any.
//
func parse(path string, mode uint) (*ast.File, *parseErrors) {
src, err := io.ReadFile(path);
if err != nil {
log.Stderrf("%v", err);
errs := []parseError{parseError{nil, 0, err.String()}};
return nil, &parseErrors{path, errs, nil};
}
prog, err := parser.ParseFile(path, src, mode);
if err != nil {
var errs []parseError;
if errors, ok := err.(scanner.ErrorList); ok {
// convert error list (already sorted)
// TODO(gri) If the file contains //line comments, the errors
// may not be sorted in increasing file offset value
// which will lead to incorrect output.
errs = make([]parseError, len(errors) + 1); // +1 for final fragment of source
offs := 0;
for i, r := range errors {
// Should always be true, but check for robustness.
if 0 <= r.Pos.Offset && r.Pos.Offset <= len(src) {
errs[i].src = src[offs : r.Pos.Offset];
offs = r.Pos.Offset;
}
errs[i].line = r.Pos.Line;
errs[i].msg = r.Msg;
}
errs[len(errors)].src = src[offs : len(src)];
} else {
// single error of unspecified type
errs = make([]parseError, 2);
errs[0] = parseError{[]byte{}, 0, err.String()};
errs[1].src = src;
}
return nil, &parseErrors{path, errs, src};
}
return prog, nil;
}
// ----------------------------------------------------------------------------
// Templates
// Write an AST-node to w; optionally html-escaped.
func writeNode(w io.Writer, node interface{}, html bool) {
mode := printer.UseSpaces;
if html {
mode |= printer.GenHTML;
}
printer.Fprint(w, node, mode, *tabwidth);
}
// Write text to w; optionally html-escaped.
func writeText(w io.Writer, text []byte, html bool) {
if html {
template.HtmlEscape(w, text);
return;
}
w.Write(text);
}
// Write anything to w; optionally html-escaped.
func writeAny(w io.Writer, x interface{}, html bool) {
switch v := x.(type) {
case []byte:
writeText(w, v, html);
case string:
writeText(w, strings.Bytes(v), html);
case ast.Decl:
writeNode(w, v, html);
case ast.Expr:
writeNode(w, v, html);
default:
if html {
var buf bytes.Buffer;
fmt.Fprint(&buf, x);
writeText(w, buf.Bytes(), true);
} else {
fmt.Fprint(w, x);
}
}
}
// Template formatter for "html" format.
func htmlFmt(w io.Writer, x interface{}, format string) {
writeAny(w, x, true);
}
// Template formatter for "html-comment" format.
func htmlCommentFmt(w io.Writer, x interface{}, format string) {
var buf bytes.Buffer;
writeAny(&buf, x, false);
doc.ToHtml(w, buf.Bytes());
}
// Template formatter for "" (default) format.
func textFmt(w io.Writer, x interface{}, format string) {
writeAny(w, x, false);
}
// Template formatter for "link" format.
func linkFmt(w io.Writer, x interface{}, format string) {
type Positioner interface { Pos() token.Position }
if node, ok := x.(Positioner); ok {
pos := node.Pos();
if pos.IsValid() {
// line id's in html-printed source are of the
// form "L%d" where %d stands for the line number
fmt.Fprintf(w, "/%s#L%d", pos.Filename, pos.Line);
}
}
}
var fmap = template.FormatterMap{
"": textFmt,
"html": htmlFmt,
"html-comment": htmlCommentFmt,
"link": linkFmt,
}
func readTemplate(name string) *template.Template {
path := pathutil.Join(*tmplroot, name);
data, err := io.ReadFile(path);
if err != nil {
log.Exitf("ReadFile %s: %v", path, err);
}
t, err1 := template.Parse(string(data), fmap);
if err1 != nil {
log.Exitf("%s: %v", name, err);
}
return t;
}
var godocHtml *template.Template
var packageHtml *template.Template
var packageText *template.Template
var parseerrorHtml *template.Template;
var parseerrorText *template.Template;
func readTemplates() {
// have to delay until after flags processing,
// so that main has chdir'ed to goroot.
godocHtml = readTemplate("godoc.html");
packageHtml = readTemplate("package.html");
packageText = readTemplate("package.txt");
parseerrorHtml = readTemplate("parseerror.html");
parseerrorText = readTemplate("parseerror.txt");
}
// ----------------------------------------------------------------------------
// Generic HTML wrapper
func servePage(c *http.Conn, title, content interface{}) {
type Data struct {
title interface{};
timestamp string;
content interface{};
}
d := Data{
title: title,
timestamp: time.SecondsToLocalTime(syncTime.get()).String(),
content: content,
};
if err := godocHtml.Execute(&d, c); err != nil {
log.Stderrf("godocHtml.Execute: %s", err);
}
}
func serveText(c *http.Conn, text []byte) {
c.SetHeader("content-type", "text/plain; charset=utf-8");
c.Write(text);
}
// ----------------------------------------------------------------------------
// Files
var (
tagBegin = strings.Bytes("<!--");
tagEnd = strings.Bytes("-->");
)
// commentText returns the text of the first HTML comment in src.
func commentText(src []byte) (text string) {
i := bytes.Index(src, tagBegin);
j := bytes.Index(src, tagEnd);
if i >= 0 && j >= i+len(tagBegin) {
text = string(bytes.TrimSpace(src[i+len(tagBegin) : j]));
}
return;
}
func serveHtmlDoc(c *http.Conn, r *http.Request, filename string) {
// get HTML body contents
path := pathutil.Join(goroot, filename);
src, err := io.ReadFile(path);
if err != nil {
log.Stderrf("%v", err);
http.NotFound(c, r);
return;
}
// if it's the language spec, add tags to EBNF productions
if strings.HasSuffix(path, "go_spec.html") {
var buf bytes.Buffer;
linkify(&buf, src);
src = buf.Bytes();
}
title := commentText(src);
servePage(c, title, src);
}
func serveParseErrors(c *http.Conn, errors *parseErrors) {
// format errors
var buf bytes.Buffer;
if err := parseerrorHtml.Execute(errors, &buf); err != nil {
log.Stderrf("parseerrorHtml.Execute: %s", err);
}
servePage(c, "Parse errors in source file " + errors.filename, buf.Bytes());
}
func serveGoSource(c *http.Conn, filename string) {
path := pathutil.Join(goroot, filename);
prog, errors := parse(path, parser.ParseComments);
if errors != nil {
serveParseErrors(c, errors);
return;
}
var buf bytes.Buffer;
fmt.Fprintln(&buf, "<pre>");
writeNode(&buf, prog, true);
fmt.Fprintln(&buf, "</pre>");
servePage(c, "Source file " + filename, buf.Bytes());
}
var fileServer = http.FileServer(".", "");
func serveFile(c *http.Conn, r *http.Request) {
path := r.Url.Path;
// pick off special cases and hand the rest to the standard file server
switch ext := pathutil.Ext(path); {
case path == "/":
serveHtmlDoc(c, r, "doc/root.html");
case r.Url.Path == "/doc/root.html":
// hide landing page from its real name
http.NotFound(c, r);
case ext == ".html":
serveHtmlDoc(c, r, path);
case ext == ".go":
serveGoSource(c, path);
default:
// TODO:
// - need to decide what to serve and what not to serve
// - don't want to download files, want to see them
fileServer.ServeHTTP(c, r);
}
}
// ----------------------------------------------------------------------------
// Packages
// TODO if we don't plan to use the directory information, simplify to []string
type dirList []*os.Dir
func (d dirList) Len() int { return len(d) }
func (d dirList) Less(i, j int) bool { return d[i].Name < d[j].Name }
func (d dirList) Swap(i, j int) { d[i], d[j] = d[j], d[i] }
func pkgName(filename string) string {
file, err := parse(filename, parser.PackageClauseOnly);
if err != nil || file == nil {
return "";
}
return file.Name.Value;
}
type PageInfo struct {
PDoc *doc.PackageDoc; // nil if no package found
Dirs dirList; // nil if no subdirectories found
}
// getPageInfo returns the PageInfo for a given package directory.
// If there is no corresponding package in the directory,
// PageInfo.PDoc is nil. If there are no subdirectories,
// PageInfo.Dirs is nil.
//
func getPageInfo(path string) PageInfo {
// the path is relative to *pkgroot
dirname := pathutil.Join(*pkgroot, path);
// the package name is the directory name within its parent
_, pkgname := pathutil.Split(dirname);
// filter function to select the desired .go files and
// collect subdirectories
var subdirlist vector.Vector;
subdirlist.Init(0);
filter := func(d *os.Dir) bool {
if isGoFile(d) {
// Some directories contain main packages: Only accept
// files that belong to the expected package so that
// parser.ParsePackage doesn't return "multiple packages
// found" errors.
return pkgName(dirname + "/" + d.Name) == pkgname;
}
if isPkgDir(d) {
subdirlist.Push(d);
}
return false;
};
// get package AST
pkg, err := parser.ParsePackage(dirname, filter, parser.ParseComments);
if err != nil {
// TODO: parse errors should be shown instead of an empty directory
log.Stderr(err);
}
// convert and sort subdirectory list, if any
var subdirs dirList;
if subdirlist.Len() > 0 {
subdirs = make(dirList, subdirlist.Len());
for i := 0; i < subdirlist.Len(); i++ {
subdirs[i] = subdirlist.At(i).(*os.Dir);
}
sort.Sort(subdirs);
}
// compute package documentation
var pdoc *doc.PackageDoc;
if pkg != nil {
ast.PackageExports(pkg);
pdoc = doc.NewPackageDoc(pkg, pathutil.Clean(path)); // no trailing '/' in importpath
}
return PageInfo{pdoc, subdirs};
}
func servePkg(c *http.Conn, r *http.Request) {
path := r.Url.Path;
path = path[len(Pkg) : len(path)];
// canonicalize URL path and redirect if necessary
if canonical := pathutil.Clean(Pkg + path) + "/"; r.Url.Path != canonical {
http.Redirect(c, canonical, http.StatusMovedPermanently);
return;
}
info := getPageInfo(path);
var buf bytes.Buffer;
if false { // TODO req.Params["format"] == "text"
if err := packageText.Execute(info, &buf); err != nil {
log.Stderrf("packageText.Execute: %s", err);
}
serveText(c, buf.Bytes());
return;
}
if err := packageHtml.Execute(info, &buf); err != nil {
log.Stderrf("packageHtml.Execute: %s", err);
}
if path == "" {
path = "."; // don't display an empty path
}
title := "Directory " + path;
if info.PDoc != nil {
title = "Package " + info.PDoc.PackageName;
}
servePage(c, title, buf.Bytes());
}
// ----------------------------------------------------------------------------
// Server
func loggingHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(c *http.Conn, req *http.Request) {
log.Stderrf("%s\t%s", c.RemoteAddr, req.Url);
h.ServeHTTP(c, req);
})
}
func exec(c *http.Conn, args []string) bool {
r, w, err := os.Pipe();
if err != nil {
log.Stderrf("os.Pipe(): %v\n", err);
return false;
}
bin := args[0];
fds := []*os.File{nil, w, w};
if *verbose {
log.Stderrf("executing %v", args);
}
pid, err := os.ForkExec(bin, args, os.Environ(), goroot, fds);
defer r.Close();
w.Close();
if err != nil {
log.Stderrf("os.ForkExec(%q): %v\n", bin, err);
return false;
}
var buf bytes.Buffer;
io.Copy(r, &buf);
wait, err := os.Wait(pid, 0);
if err != nil {
os.Stderr.Write(buf.Bytes());
log.Stderrf("os.Wait(%d, 0): %v\n", pid, err);
return false;
}
if !wait.Exited() || wait.ExitStatus() != 0 {
os.Stderr.Write(buf.Bytes());
log.Stderrf("executing %v failed (exit status = %d)", args, wait.ExitStatus());
return false;
}
if *verbose {
os.Stderr.Write(buf.Bytes());
}
if c != nil {
c.SetHeader("content-type", "text/plain; charset=utf-8");
c.Write(buf.Bytes());
}
return true;
}
func dosync(c *http.Conn, r *http.Request) {
args := []string{"/bin/sh", "-c", *syncCmd};
if exec(c, args) {
// sync succeeded
syncTime.set();
syncDelay.set(*syncMin); // revert to regular sync schedule
} else {
// sync failed - back off exponentially, but try at least once a day
syncDelay.backoff(24*60);
}
}
func usage() {
fmt.Fprintf(os.Stderr,
"usage: godoc package [name ...]\n"
" godoc -http=:6060\n"
);
flag.PrintDefaults();
os.Exit(2);
}
func main() {
flag.Usage = usage;
flag.Parse();
// Check usage first; get usage message out early.
switch {
case *httpaddr != "":
if flag.NArg() != 0 {
usage();
}
default:
if flag.NArg() == 0 {
usage();
}
}
if err := os.Chdir(goroot); err != nil {
log.Exitf("chdir %s: %v", goroot, err);
}
readTemplates();
if *httpaddr != "" {
var handler http.Handler = http.DefaultServeMux;
if *verbose {
log.Stderrf("Go Documentation Server\n");
log.Stderrf("address = %s\n", *httpaddr);
log.Stderrf("goroot = %s\n", goroot);
log.Stderrf("pkgroot = %s\n", *pkgroot);
log.Stderrf("tmplroot = %s\n", *tmplroot);
handler = loggingHandler(handler);
}
http.Handle(Pkg, http.HandlerFunc(servePkg));
if *syncCmd != "" {
http.Handle("/debug/sync", http.HandlerFunc(dosync));
}
http.Handle("/", http.HandlerFunc(serveFile));
// The server may have been restarted; always wait 1sec to
// give the forking server a chance to shut down and release
// the http port.
time.Sleep(1e9);
// Start sync goroutine, if enabled.
if *syncCmd != "" && *syncMin > 0 {
syncDelay.set(*syncMin); // initial sync delay
go func() {
for {
dosync(nil, nil);
if *verbose {
log.Stderrf("next sync in %dmin", syncDelay.get());
}
time.Sleep(int64(syncDelay.get()) * (60 * 1e9));
}
}();
}
if err := http.ListenAndServe(*httpaddr, handler); err != nil {
log.Exitf("ListenAndServe %s: %v", *httpaddr, err)
}
return;
}
if *html {
packageText = packageHtml;
parseerrorText = parseerrorHtml;
}
info := getPageInfo(flag.Arg(0));
if info.PDoc != nil && flag.NArg() > 1 {
args := flag.Args();
info.PDoc.Filter(args[1 : len(args)]);
}
if err := packageText.Execute(info, os.Stdout); err != nil {
log.Stderrf("packageText.Execute: %s", err);
}
}
|