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