1// Copyright 2019 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 5package debug 6 7import ( 8 "bytes" 9 "context" 10 "fmt" 11 "go/token" 12 "html/template" 13 "io" 14 stdlog "log" 15 "net" 16 "net/http" 17 "net/http/pprof" 18 _ "net/http/pprof" // pull in the standard pprof handlers 19 "os" 20 "path" 21 "path/filepath" 22 "reflect" 23 "runtime" 24 rpprof "runtime/pprof" 25 "strconv" 26 "strings" 27 "sync" 28 "time" 29 30 "golang.org/x/tools/internal/lsp/protocol" 31 "golang.org/x/tools/internal/span" 32 "golang.org/x/tools/internal/telemetry" 33 "golang.org/x/tools/internal/telemetry/export" 34 "golang.org/x/tools/internal/telemetry/export/ocagent" 35 "golang.org/x/tools/internal/telemetry/export/prometheus" 36 "golang.org/x/tools/internal/telemetry/log" 37 "golang.org/x/tools/internal/telemetry/tag" 38) 39 40// An Instance holds all debug information associated with a gopls instance. 41type Instance struct { 42 Logfile string 43 StartTime time.Time 44 ServerAddress string 45 DebugAddress string 46 ListenedDebugAddress string 47 Workdir string 48 OCAgentConfig string 49 50 LogWriter io.Writer 51 52 ocagent *ocagent.Exporter 53 prometheus *prometheus.Exporter 54 rpcs *rpcs 55 traces *traces 56 State *State 57} 58 59// State holds debugging information related to the server state. 60type State struct { 61 mu sync.Mutex 62 caches objset 63 sessions objset 64 views objset 65 clients objset 66 servers objset 67} 68 69type ider interface { 70 ID() string 71} 72 73type objset struct { 74 objs []ider 75} 76 77func (s *objset) add(elem ider) { 78 s.objs = append(s.objs, elem) 79} 80 81func (s *objset) drop(elem ider) { 82 var newobjs []ider 83 for _, obj := range s.objs { 84 if obj.ID() != elem.ID() { 85 newobjs = append(newobjs, obj) 86 } 87 } 88 s.objs = newobjs 89} 90 91func (s *objset) find(id string) ider { 92 for _, e := range s.objs { 93 if e.ID() == id { 94 return e 95 } 96 } 97 return nil 98} 99 100// Caches returns the set of Cache objects currently being served. 101func (st *State) Caches() []Cache { 102 st.mu.Lock() 103 defer st.mu.Unlock() 104 caches := make([]Cache, len(st.caches.objs)) 105 for i, c := range st.caches.objs { 106 caches[i] = c.(Cache) 107 } 108 return caches 109} 110 111// Sessions returns the set of Session objects currently being served. 112func (st *State) Sessions() []Session { 113 st.mu.Lock() 114 defer st.mu.Unlock() 115 sessions := make([]Session, len(st.sessions.objs)) 116 for i, s := range st.sessions.objs { 117 sessions[i] = s.(Session) 118 } 119 return sessions 120} 121 122// Views returns the set of View objects currently being served. 123func (st *State) Views() []View { 124 st.mu.Lock() 125 defer st.mu.Unlock() 126 views := make([]View, len(st.views.objs)) 127 for i, v := range st.views.objs { 128 views[i] = v.(View) 129 } 130 return views 131} 132 133// Clients returns the set of Clients currently being served. 134func (st *State) Clients() []Client { 135 st.mu.Lock() 136 defer st.mu.Unlock() 137 clients := make([]Client, len(st.clients.objs)) 138 for i, c := range st.clients.objs { 139 clients[i] = c.(Client) 140 } 141 return clients 142} 143 144// Servers returns the set of Servers the instance is currently connected to. 145func (st *State) Servers() []Server { 146 st.mu.Lock() 147 defer st.mu.Unlock() 148 servers := make([]Server, len(st.servers.objs)) 149 for i, s := range st.servers.objs { 150 servers[i] = s.(Server) 151 } 152 return servers 153} 154 155// A Client is an incoming connection from a remote client. 156type Client interface { 157 ID() string 158 Session() Session 159 DebugAddress() string 160 Logfile() string 161 ServerID() string 162} 163 164// A Server is an outgoing connection to a remote LSP server. 165type Server interface { 166 ID() string 167 DebugAddress() string 168 Logfile() string 169 ClientID() string 170} 171 172// A Cache is an in-memory cache. 173type Cache interface { 174 ID() string 175 FileSet() *token.FileSet 176 MemStats() map[reflect.Type]int 177} 178 179// A Session is an LSP serving session. 180type Session interface { 181 ID() string 182 Cache() Cache 183 Files() []*File 184 File(hash string) *File 185} 186 187// A View is a root directory within a Session. 188type View interface { 189 ID() string 190 Name() string 191 Folder() span.URI 192 Session() Session 193} 194 195// A File is is a file within a session. 196type File struct { 197 Session Session 198 URI span.URI 199 Data string 200 Error error 201 Hash string 202} 203 204// AddCache adds a cache to the set being served. 205func (st *State) AddCache(cache Cache) { 206 st.mu.Lock() 207 defer st.mu.Unlock() 208 st.caches.add(cache) 209} 210 211// DropCache drops a cache from the set being served. 212func (st *State) DropCache(cache Cache) { 213 st.mu.Lock() 214 defer st.mu.Unlock() 215 st.caches.drop(cache) 216} 217 218// AddSession adds a session to the set being served. 219func (st *State) AddSession(session Session) { 220 st.mu.Lock() 221 defer st.mu.Unlock() 222 st.sessions.add(session) 223} 224 225// DropSession drops a session from the set being served. 226func (st *State) DropSession(session Session) { 227 st.mu.Lock() 228 defer st.mu.Unlock() 229 st.sessions.drop(session) 230} 231 232// AddView adds a view to the set being served. 233func (st *State) AddView(view View) { 234 st.mu.Lock() 235 defer st.mu.Unlock() 236 st.views.add(view) 237} 238 239// DropView drops a view from the set being served. 240func (st *State) DropView(view View) { 241 st.mu.Lock() 242 defer st.mu.Unlock() 243 st.views.drop(view) 244} 245 246// AddClient adds a client to the set being served. 247func (st *State) AddClient(client Client) { 248 st.mu.Lock() 249 defer st.mu.Unlock() 250 st.clients.add(client) 251} 252 253// DropClient adds a client to the set being served. 254func (st *State) DropClient(client Client) { 255 st.mu.Lock() 256 defer st.mu.Unlock() 257 st.clients.drop(client) 258} 259 260// AddServer adds a server to the set being queried. In practice, there should 261// be at most one remote server. 262func (st *State) AddServer(server Server) { 263 st.mu.Lock() 264 defer st.mu.Unlock() 265 st.servers.add(server) 266} 267 268// DropServer drops a server to the set being queried. 269func (st *State) DropServer(server Server) { 270 st.mu.Lock() 271 defer st.mu.Unlock() 272 st.servers.drop(server) 273} 274 275func (i *Instance) getCache(r *http.Request) interface{} { 276 i.State.mu.Lock() 277 defer i.State.mu.Unlock() 278 id := path.Base(r.URL.Path) 279 result := struct { 280 Cache 281 Sessions []Session 282 }{ 283 Cache: i.State.caches.find(id).(Cache), 284 } 285 286 // now find all the views that belong to this session 287 for _, vd := range i.State.sessions.objs { 288 v := vd.(Session) 289 if v.Cache().ID() == id { 290 result.Sessions = append(result.Sessions, v) 291 } 292 } 293 return result 294} 295 296func (i *Instance) getSession(r *http.Request) interface{} { 297 i.State.mu.Lock() 298 defer i.State.mu.Unlock() 299 id := path.Base(r.URL.Path) 300 result := struct { 301 Session 302 Views []View 303 }{ 304 Session: i.State.sessions.find(id).(Session), 305 } 306 // now find all the views that belong to this session 307 for _, vd := range i.State.views.objs { 308 v := vd.(View) 309 if v.Session().ID() == id { 310 result.Views = append(result.Views, v) 311 } 312 } 313 return result 314} 315 316func (i Instance) getClient(r *http.Request) interface{} { 317 i.State.mu.Lock() 318 defer i.State.mu.Unlock() 319 id := path.Base(r.URL.Path) 320 return i.State.clients.find(id).(Client) 321} 322 323func (i Instance) getServer(r *http.Request) interface{} { 324 i.State.mu.Lock() 325 defer i.State.mu.Unlock() 326 id := path.Base(r.URL.Path) 327 return i.State.servers.find(id).(Server) 328} 329 330func (i Instance) getView(r *http.Request) interface{} { 331 i.State.mu.Lock() 332 defer i.State.mu.Unlock() 333 id := path.Base(r.URL.Path) 334 return i.State.views.find(id).(View) 335} 336 337func (i *Instance) getFile(r *http.Request) interface{} { 338 i.State.mu.Lock() 339 defer i.State.mu.Unlock() 340 hash := path.Base(r.URL.Path) 341 sid := path.Base(path.Dir(r.URL.Path)) 342 return i.State.sessions.find(sid).(Session).File(hash) 343} 344 345func (i *Instance) getInfo(r *http.Request) interface{} { 346 buf := &bytes.Buffer{} 347 i.PrintServerInfo(buf) 348 return template.HTML(buf.String()) 349} 350 351func getMemory(r *http.Request) interface{} { 352 var m runtime.MemStats 353 runtime.ReadMemStats(&m) 354 return m 355} 356 357// NewInstance creates debug instance ready for use using the supplied configuration. 358func NewInstance(workdir, agent string) *Instance { 359 i := &Instance{ 360 StartTime: time.Now(), 361 Workdir: workdir, 362 OCAgentConfig: agent, 363 } 364 i.LogWriter = os.Stderr 365 ocConfig := ocagent.Discover() 366 //TODO: we should not need to adjust the discovered configuration 367 ocConfig.Address = i.OCAgentConfig 368 i.ocagent = ocagent.Connect(ocConfig) 369 i.prometheus = prometheus.New() 370 i.rpcs = &rpcs{} 371 i.traces = &traces{} 372 i.State = &State{} 373 export.SetExporter(i) 374 return i 375} 376 377// SetLogFile sets the logfile for use with this instance. 378func (i *Instance) SetLogFile(logfile string) (func(), error) { 379 // TODO: probably a better solution for deferring closure to the caller would 380 // be for the debug instance to itself be closed, but this fixes the 381 // immediate bug of logs not being captured. 382 closeLog := func() {} 383 if logfile != "" { 384 if logfile == "auto" { 385 logfile = filepath.Join(os.TempDir(), fmt.Sprintf("gopls-%d.log", os.Getpid())) 386 } 387 f, err := os.Create(logfile) 388 if err != nil { 389 return nil, fmt.Errorf("unable to create log file: %v", err) 390 } 391 closeLog = func() { 392 defer f.Close() 393 } 394 stdlog.SetOutput(io.MultiWriter(os.Stderr, f)) 395 i.LogWriter = f 396 } 397 i.Logfile = logfile 398 return closeLog, nil 399} 400 401// Serve starts and runs a debug server in the background. 402// It also logs the port the server starts on, to allow for :0 auto assigned 403// ports. 404func (i *Instance) Serve(ctx context.Context) error { 405 if i.DebugAddress == "" { 406 return nil 407 } 408 listener, err := net.Listen("tcp", i.DebugAddress) 409 if err != nil { 410 return err 411 } 412 i.ListenedDebugAddress = listener.Addr().String() 413 414 port := listener.Addr().(*net.TCPAddr).Port 415 if strings.HasSuffix(i.DebugAddress, ":0") { 416 stdlog.Printf("debug server listening on port %d", port) 417 } 418 log.Print(ctx, "Debug serving", tag.Of("Port", port)) 419 go func() { 420 mux := http.NewServeMux() 421 mux.HandleFunc("/", render(mainTmpl, func(*http.Request) interface{} { return i })) 422 mux.HandleFunc("/debug/", render(debugTmpl, nil)) 423 mux.HandleFunc("/debug/pprof/", pprof.Index) 424 mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) 425 mux.HandleFunc("/debug/pprof/profile", pprof.Profile) 426 mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) 427 mux.HandleFunc("/debug/pprof/trace", pprof.Trace) 428 if i.prometheus != nil { 429 mux.HandleFunc("/metrics/", i.prometheus.Serve) 430 } 431 if i.rpcs != nil { 432 mux.HandleFunc("/rpc/", render(rpcTmpl, i.rpcs.getData)) 433 } 434 if i.traces != nil { 435 mux.HandleFunc("/trace/", render(traceTmpl, i.traces.getData)) 436 } 437 mux.HandleFunc("/cache/", render(cacheTmpl, i.getCache)) 438 mux.HandleFunc("/session/", render(sessionTmpl, i.getSession)) 439 mux.HandleFunc("/view/", render(viewTmpl, i.getView)) 440 mux.HandleFunc("/client/", render(clientTmpl, i.getClient)) 441 mux.HandleFunc("/server/", render(serverTmpl, i.getServer)) 442 mux.HandleFunc("/file/", render(fileTmpl, i.getFile)) 443 mux.HandleFunc("/info", render(infoTmpl, i.getInfo)) 444 mux.HandleFunc("/memory", render(memoryTmpl, getMemory)) 445 if err := http.Serve(listener, mux); err != nil { 446 log.Error(ctx, "Debug server failed", err) 447 return 448 } 449 log.Print(ctx, "Debug server finished") 450 }() 451 return nil 452} 453 454// MonitorMemory starts recording memory statistics each second. 455func (i *Instance) MonitorMemory(ctx context.Context) { 456 tick := time.NewTicker(time.Second) 457 nextThresholdGiB := uint64(1) 458 go func() { 459 for { 460 <-tick.C 461 var mem runtime.MemStats 462 runtime.ReadMemStats(&mem) 463 if mem.HeapAlloc < nextThresholdGiB*1<<30 { 464 continue 465 } 466 i.writeMemoryDebug(nextThresholdGiB) 467 log.Print(ctx, fmt.Sprintf("Wrote memory usage debug info to %v", os.TempDir())) 468 nextThresholdGiB++ 469 } 470 }() 471} 472 473func (i *Instance) writeMemoryDebug(threshold uint64) error { 474 fname := func(t string) string { 475 return fmt.Sprintf("gopls.%d-%dGiB-%s", os.Getpid(), threshold, t) 476 } 477 478 f, err := os.Create(filepath.Join(os.TempDir(), fname("heap.pb.gz"))) 479 if err != nil { 480 return err 481 } 482 defer f.Close() 483 if err := rpprof.Lookup("heap").WriteTo(f, 0); err != nil { 484 return err 485 } 486 487 f, err = os.Create(filepath.Join(os.TempDir(), fname("goroutines.txt"))) 488 if err != nil { 489 return err 490 } 491 defer f.Close() 492 if err := rpprof.Lookup("goroutine").WriteTo(f, 1); err != nil { 493 return err 494 } 495 return nil 496} 497 498func (i *Instance) StartSpan(ctx context.Context, spn *telemetry.Span) { 499 if i.ocagent != nil { 500 i.ocagent.StartSpan(ctx, spn) 501 } 502 if i.traces != nil { 503 i.traces.StartSpan(ctx, spn) 504 } 505} 506 507func (i *Instance) FinishSpan(ctx context.Context, spn *telemetry.Span) { 508 if i.ocagent != nil { 509 i.ocagent.FinishSpan(ctx, spn) 510 } 511 if i.traces != nil { 512 i.traces.FinishSpan(ctx, spn) 513 } 514} 515 516//TODO: remove this hack 517// capture stderr at startup because it gets modified in a way that this 518// logger should not respect 519var stderr = os.Stderr 520 521func (i *Instance) Log(ctx context.Context, event telemetry.Event) { 522 if event.Error != nil { 523 fmt.Fprintf(stderr, "%v\n", event) 524 } 525 protocol.LogEvent(ctx, event) 526 if i.ocagent != nil { 527 i.ocagent.Log(ctx, event) 528 } 529} 530 531func (i *Instance) Metric(ctx context.Context, data telemetry.MetricData) { 532 if i.ocagent != nil { 533 i.ocagent.Metric(ctx, data) 534 } 535 if i.traces != nil { 536 i.prometheus.Metric(ctx, data) 537 } 538 if i.rpcs != nil { 539 i.rpcs.Metric(ctx, data) 540 } 541} 542 543type dataFunc func(*http.Request) interface{} 544 545func render(tmpl *template.Template, fun dataFunc) func(http.ResponseWriter, *http.Request) { 546 return func(w http.ResponseWriter, r *http.Request) { 547 var data interface{} 548 if fun != nil { 549 data = fun(r) 550 } 551 if err := tmpl.Execute(w, data); err != nil { 552 log.Error(context.Background(), "", err) 553 http.Error(w, err.Error(), http.StatusInternalServerError) 554 } 555 } 556} 557 558func commas(s string) string { 559 for i := len(s); i > 3; { 560 i -= 3 561 s = s[:i] + "," + s[i:] 562 } 563 return s 564} 565 566func fuint64(v uint64) string { 567 return commas(strconv.FormatUint(v, 10)) 568} 569 570func fuint32(v uint32) string { 571 return commas(strconv.FormatUint(uint64(v), 10)) 572} 573 574var baseTemplate = template.Must(template.New("").Parse(` 575<html> 576<head> 577<title>{{template "title" .}}</title> 578<style> 579.profile-name{ 580 display:inline-block; 581 width:6rem; 582} 583td.value { 584 text-align: right; 585} 586ul.events { 587 list-style-type: none; 588} 589 590</style> 591{{block "head" .}}{{end}} 592</head> 593<body> 594<a href="/">Main</a> 595<a href="/info">Info</a> 596<a href="/memory">Memory</a> 597<a href="/metrics">Metrics</a> 598<a href="/rpc">RPC</a> 599<a href="/trace">Trace</a> 600<hr> 601<h1>{{template "title" .}}</h1> 602{{block "body" .}} 603Unknown page 604{{end}} 605</body> 606</html> 607 608{{define "cachelink"}}<a href="/cache/{{.}}">Cache {{.}}</a>{{end}} 609{{define "clientlink"}}<a href="/client/{{.}}">Client {{.}}</a>{{end}} 610{{define "serverlink"}}<a href="/server/{{.}}">Server {{.}}</a>{{end}} 611{{define "sessionlink"}}<a href="/session/{{.}}">Session {{.}}</a>{{end}} 612{{define "viewlink"}}<a href="/view/{{.}}">View {{.}}</a>{{end}} 613{{define "filelink"}}<a href="/file/{{.Session.ID}}/{{.Hash}}">{{.URI}}</a>{{end}} 614`)).Funcs(template.FuncMap{ 615 "fuint64": fuint64, 616 "fuint32": fuint32, 617 "localAddress": func(s string) string { 618 // Try to translate loopback addresses to localhost, both for cosmetics and 619 // because unspecified ipv6 addresses can break links on Windows. 620 // 621 // TODO(rfindley): In the future, it would be better not to assume the 622 // server is running on localhost, and instead construct this address using 623 // the remote host. 624 host, port, err := net.SplitHostPort(s) 625 if err != nil { 626 return s 627 } 628 ip := net.ParseIP(host) 629 if ip == nil { 630 return s 631 } 632 if ip.IsLoopback() || ip.IsUnspecified() { 633 return "localhost:" + port 634 } 635 return s 636 }, 637}) 638 639var mainTmpl = template.Must(template.Must(baseTemplate.Clone()).Parse(` 640{{define "title"}}GoPls server information{{end}} 641{{define "body"}} 642<h2>Caches</h2> 643<ul>{{range .State.Caches}}<li>{{template "cachelink" .ID}}</li>{{end}}</ul> 644<h2>Sessions</h2> 645<ul>{{range .State.Sessions}}<li>{{template "sessionlink" .ID}} from {{template "cachelink" .Cache.ID}}</li>{{end}}</ul> 646<h2>Views</h2> 647<ul>{{range .State.Views}}<li>{{.Name}} is {{template "viewlink" .ID}} from {{template "sessionlink" .Session.ID}} in {{.Folder}}</li>{{end}}</ul> 648<h2>Clients</h2> 649<ul>{{range .State.Clients}}<li>{{template "clientlink" .ID}}</li>{{end}}</ul> 650<h2>Servers</h2> 651<ul>{{range .State.Servers}}<li>{{template "serverlink" .ID}}</li>{{end}}</ul> 652{{end}} 653`)) 654 655var infoTmpl = template.Must(template.Must(baseTemplate.Clone()).Parse(` 656{{define "title"}}GoPls version information{{end}} 657{{define "body"}} 658{{.}} 659{{end}} 660`)) 661 662var memoryTmpl = template.Must(template.Must(baseTemplate.Clone()).Parse(` 663{{define "title"}}GoPls memory usage{{end}} 664{{define "head"}}<meta http-equiv="refresh" content="5">{{end}} 665{{define "body"}} 666<h2>Stats</h2> 667<table> 668<tr><td class="label">Allocated bytes</td><td class="value">{{fuint64 .HeapAlloc}}</td></tr> 669<tr><td class="label">Total allocated bytes</td><td class="value">{{fuint64 .TotalAlloc}}</td></tr> 670<tr><td class="label">System bytes</td><td class="value">{{fuint64 .Sys}}</td></tr> 671<tr><td class="label">Heap system bytes</td><td class="value">{{fuint64 .HeapSys}}</td></tr> 672<tr><td class="label">Malloc calls</td><td class="value">{{fuint64 .Mallocs}}</td></tr> 673<tr><td class="label">Frees</td><td class="value">{{fuint64 .Frees}}</td></tr> 674<tr><td class="label">Idle heap bytes</td><td class="value">{{fuint64 .HeapIdle}}</td></tr> 675<tr><td class="label">In use bytes</td><td class="value">{{fuint64 .HeapInuse}}</td></tr> 676<tr><td class="label">Released to system bytes</td><td class="value">{{fuint64 .HeapReleased}}</td></tr> 677<tr><td class="label">Heap object count</td><td class="value">{{fuint64 .HeapObjects}}</td></tr> 678<tr><td class="label">Stack in use bytes</td><td class="value">{{fuint64 .StackInuse}}</td></tr> 679<tr><td class="label">Stack from system bytes</td><td class="value">{{fuint64 .StackSys}}</td></tr> 680<tr><td class="label">Bucket hash bytes</td><td class="value">{{fuint64 .BuckHashSys}}</td></tr> 681<tr><td class="label">GC metadata bytes</td><td class="value">{{fuint64 .GCSys}}</td></tr> 682<tr><td class="label">Off heap bytes</td><td class="value">{{fuint64 .OtherSys}}</td></tr> 683</table> 684<h2>By size</h2> 685<table> 686<tr><th>Size</th><th>Mallocs</th><th>Frees</th></tr> 687{{range .BySize}}<tr><td class="value">{{fuint32 .Size}}</td><td class="value">{{fuint64 .Mallocs}}</td><td class="value">{{fuint64 .Frees}}</td></tr>{{end}} 688</table> 689{{end}} 690`)) 691 692var debugTmpl = template.Must(template.Must(baseTemplate.Clone()).Parse(` 693{{define "title"}}GoPls Debug pages{{end}} 694{{define "body"}} 695<a href="/debug/pprof">Profiling</a> 696{{end}} 697`)) 698 699var cacheTmpl = template.Must(template.Must(baseTemplate.Clone()).Parse(` 700{{define "title"}}Cache {{.ID}}{{end}} 701{{define "body"}} 702<h2>Sessions</h2> 703<ul>{{range .Sessions}}<li>{{template "sessionlink" .ID}}</li>{{end}}</ul> 704<h2>memoize.Store entries</h2> 705<ul>{{range $k,$v := .MemStats}}<li>{{$k}} - {{$v}}</li>{{end}}</ul> 706{{end}} 707`)) 708 709var clientTmpl = template.Must(template.Must(baseTemplate.Clone()).Parse(` 710{{define "title"}}Client {{.ID}}{{end}} 711{{define "body"}} 712Using session: <b>{{template "sessionlink" .Session.ID}}</b><br> 713Debug this client at: <a href="http://{{localAddress .DebugAddress}}">{{localAddress .DebugAddress}}</a><br> 714Logfile: {{.Logfile}}<br> 715Gopls Path: {{.GoplsPath}}<br> 716{{end}} 717`)) 718 719var serverTmpl = template.Must(template.Must(baseTemplate.Clone()).Parse(` 720{{define "title"}}Server {{.ID}}{{end}} 721{{define "body"}} 722Debug this server at: <a href="http://{{localAddress .DebugAddress}}">{{localAddress .DebugAddress}}</a><br> 723Logfile: {{.Logfile}}<br> 724Gopls Path: {{.GoplsPath}}<br> 725{{end}} 726`)) 727 728var sessionTmpl = template.Must(template.Must(baseTemplate.Clone()).Parse(` 729{{define "title"}}Session {{.ID}}{{end}} 730{{define "body"}} 731From: <b>{{template "cachelink" .Cache.ID}}</b><br> 732<h2>Views</h2> 733<ul>{{range .Views}}<li>{{.Name}} is {{template "viewlink" .ID}} in {{.Folder}}</li>{{end}}</ul> 734<h2>Files</h2> 735<ul>{{range .Files}}<li>{{template "filelink" .}}</li>{{end}}</ul> 736{{end}} 737`)) 738 739var viewTmpl = template.Must(template.Must(baseTemplate.Clone()).Parse(` 740{{define "title"}}View {{.ID}}{{end}} 741{{define "body"}} 742Name: <b>{{.Name}}</b><br> 743Folder: <b>{{.Folder}}</b><br> 744From: <b>{{template "sessionlink" .Session.ID}}</b><br> 745<h2>Environment</h2> 746<ul>{{range .Env}}<li>{{.}}</li>{{end}}</ul> 747{{end}} 748`)) 749 750var fileTmpl = template.Must(template.Must(baseTemplate.Clone()).Parse(` 751{{define "title"}}File {{.Hash}}{{end}} 752{{define "body"}} 753From: <b>{{template "sessionlink" .Session.ID}}</b><br> 754URI: <b>{{.URI}}</b><br> 755Hash: <b>{{.Hash}}</b><br> 756Error: <b>{{.Error}}</b><br> 757<h3>Contents</h3> 758<pre>{{.Data}}</pre> 759{{end}} 760`)) 761