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	"go/token"
11	"html/template"
12	"log"
13	"net"
14	"net/http"
15	"net/http/pprof"
16	_ "net/http/pprof" // pull in the standard pprof handlers
17	"path"
18	"runtime"
19	"strconv"
20	"sync"
21
22	"golang.org/x/tools/internal/span"
23)
24
25type Cache interface {
26	ID() string
27	FileSet() *token.FileSet
28}
29
30type Session interface {
31	ID() string
32	Cache() Cache
33	Files() []*File
34	File(hash string) *File
35}
36
37type View interface {
38	ID() string
39	Name() string
40	Folder() span.URI
41	Session() Session
42}
43
44type File struct {
45	Session Session
46	URI     span.URI
47	Data    string
48	Error   error
49	Hash    string
50}
51
52var (
53	mu   sync.Mutex
54	data = struct {
55		Caches   []Cache
56		Sessions []Session
57		Views    []View
58	}{}
59)
60
61// AddCache adds a cache to the set being served
62func AddCache(cache Cache) {
63	mu.Lock()
64	defer mu.Unlock()
65	data.Caches = append(data.Caches, cache)
66}
67
68// DropCache drops a cache from the set being served
69func DropCache(cache Cache) {
70	mu.Lock()
71	defer mu.Unlock()
72	//find and remove the cache
73	if i, _ := findCache(cache.ID()); i >= 0 {
74		copy(data.Caches[i:], data.Caches[i+1:])
75		data.Caches[len(data.Caches)-1] = nil
76		data.Caches = data.Caches[:len(data.Caches)-1]
77	}
78}
79
80func findCache(id string) (int, Cache) {
81	for i, c := range data.Caches {
82		if c.ID() == id {
83			return i, c
84		}
85	}
86	return -1, nil
87}
88
89func getCache(r *http.Request) interface{} {
90	mu.Lock()
91	defer mu.Unlock()
92	id := path.Base(r.URL.Path)
93	result := struct {
94		Cache
95		Sessions []Session
96	}{}
97	_, result.Cache = findCache(id)
98
99	// now find all the views that belong to this session
100	for _, v := range data.Sessions {
101		if v.Cache().ID() == id {
102			result.Sessions = append(result.Sessions, v)
103		}
104	}
105	return result
106}
107
108func findSession(id string) Session {
109	for _, c := range data.Sessions {
110		if c.ID() == id {
111			return c
112		}
113	}
114	return nil
115}
116
117func getSession(r *http.Request) interface{} {
118	mu.Lock()
119	defer mu.Unlock()
120	id := path.Base(r.URL.Path)
121	result := struct {
122		Session
123		Views []View
124	}{
125		Session: findSession(id),
126	}
127	// now find all the views that belong to this session
128	for _, v := range data.Views {
129		if v.Session().ID() == id {
130			result.Views = append(result.Views, v)
131		}
132	}
133	return result
134}
135
136func findView(id string) View {
137	for _, c := range data.Views {
138		if c.ID() == id {
139			return c
140		}
141	}
142	return nil
143}
144
145func getView(r *http.Request) interface{} {
146	mu.Lock()
147	defer mu.Unlock()
148	id := path.Base(r.URL.Path)
149	return findView(id)
150}
151
152func getFile(r *http.Request) interface{} {
153	mu.Lock()
154	defer mu.Unlock()
155	hash := path.Base(r.URL.Path)
156	sid := path.Base(path.Dir(r.URL.Path))
157	session := findSession(sid)
158	return session.File(hash)
159}
160
161func getInfo(r *http.Request) interface{} {
162	buf := &bytes.Buffer{}
163	PrintVersionInfo(buf, true, HTML)
164	return template.HTML(buf.String())
165}
166
167func getMemory(r *http.Request) interface{} {
168	var m runtime.MemStats
169	runtime.ReadMemStats(&m)
170	return m
171}
172
173// AddSession adds a session to the set being served
174func AddSession(session Session) {
175	mu.Lock()
176	defer mu.Unlock()
177	data.Sessions = append(data.Sessions, session)
178}
179
180// DropSession drops a session from the set being served
181func DropSession(session Session) {
182	mu.Lock()
183	defer mu.Unlock()
184	//find and remove the session
185}
186
187// AddView adds a view to the set being served
188func AddView(view View) {
189	mu.Lock()
190	defer mu.Unlock()
191	data.Views = append(data.Views, view)
192}
193
194// DropView drops a view from the set being served
195func DropView(view View) {
196	mu.Lock()
197	defer mu.Unlock()
198	//find and remove the view
199}
200
201// Serve starts and runs a debug server in the background.
202// It also logs the port the server starts on, to allow for :0 auto assigned
203// ports.
204func Serve(ctx context.Context, addr string) error {
205	mu.Lock()
206	defer mu.Unlock()
207	if addr == "" {
208		return nil
209	}
210	listener, err := net.Listen("tcp", addr)
211	if err != nil {
212		return err
213	}
214	log.Printf("Debug serving on port: %d", listener.Addr().(*net.TCPAddr).Port)
215	go func() {
216		mux := http.NewServeMux()
217		mux.HandleFunc("/", Render(mainTmpl, func(*http.Request) interface{} { return data }))
218		mux.HandleFunc("/debug/", Render(debugTmpl, nil))
219		mux.HandleFunc("/debug/pprof/", pprof.Index)
220		mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
221		mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
222		mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
223		mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
224		mux.HandleFunc("/cache/", Render(cacheTmpl, getCache))
225		mux.HandleFunc("/session/", Render(sessionTmpl, getSession))
226		mux.HandleFunc("/view/", Render(viewTmpl, getView))
227		mux.HandleFunc("/file/", Render(fileTmpl, getFile))
228		mux.HandleFunc("/info", Render(infoTmpl, getInfo))
229		mux.HandleFunc("/memory", Render(memoryTmpl, getMemory))
230		if err := http.Serve(listener, mux); err != nil {
231			log.Printf("Debug server failed with %v", err)
232			return
233		}
234		log.Printf("Debug server finished")
235	}()
236	return nil
237}
238
239func Render(tmpl *template.Template, fun func(*http.Request) interface{}) func(http.ResponseWriter, *http.Request) {
240	return func(w http.ResponseWriter, r *http.Request) {
241		var data interface{}
242		if fun != nil {
243			data = fun(r)
244		}
245		if err := tmpl.Execute(w, data); err != nil {
246			log.Print(err)
247		}
248	}
249}
250
251func commas(s string) string {
252	for i := len(s); i > 3; {
253		i -= 3
254		s = s[:i] + "," + s[i:]
255	}
256	return s
257}
258
259func fuint64(v uint64) string {
260	return commas(strconv.FormatUint(v, 10))
261}
262
263func fuint32(v uint32) string {
264	return commas(strconv.FormatUint(uint64(v), 10))
265}
266
267var BaseTemplate = template.Must(template.New("").Parse(`
268<html>
269<head>
270<title>{{template "title" .}}</title>
271<style>
272.profile-name{
273	display:inline-block;
274	width:6rem;
275}
276td.value {
277  text-align: right;
278}
279</style>
280{{block "head" .}}{{end}}
281</head>
282<body>
283<a href="/">Main</a>
284<a href="/info">Info</a>
285<a href="/memory">Memory</a>
286<a href="/debug/">Debug</a>
287<hr>
288<h1>{{template "title" .}}</h1>
289{{block "body" .}}
290Unknown page
291{{end}}
292</body>
293</html>
294
295{{define "cachelink"}}<a href="/cache/{{.}}">Cache {{.}}</a>{{end}}
296{{define "sessionlink"}}<a href="/session/{{.}}">Session {{.}}</a>{{end}}
297{{define "viewlink"}}<a href="/view/{{.}}">View {{.}}</a>{{end}}
298{{define "filelink"}}<a href="/file/{{.Session.ID}}/{{.Hash}}">{{.URI}}</a>{{end}}
299`)).Funcs(template.FuncMap{
300	"fuint64": fuint64,
301	"fuint32": fuint32,
302})
303
304var mainTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
305{{define "title"}}GoPls server information{{end}}
306{{define "body"}}
307<h2>Caches</h2>
308<ul>{{range .Caches}}<li>{{template "cachelink" .ID}}</li>{{end}}</ul>
309<h2>Sessions</h2>
310<ul>{{range .Sessions}}<li>{{template "sessionlink" .ID}} from {{template "cachelink" .Cache.ID}}</li>{{end}}</ul>
311<h2>Views</h2>
312<ul>{{range .Views}}<li>{{.Name}} is {{template "viewlink" .ID}} from {{template "sessionlink" .Session.ID}} in {{.Folder}}</li>{{end}}</ul>
313{{end}}
314`))
315
316var infoTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
317{{define "title"}}GoPls version information{{end}}
318{{define "body"}}
319{{.}}
320{{end}}
321`))
322
323var memoryTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
324{{define "title"}}GoPls memory usage{{end}}
325{{define "head"}}<meta http-equiv="refresh" content="5">{{end}}
326{{define "body"}}
327<h2>Stats</h2>
328<table>
329<tr><td class="label">Allocated bytes</td><td class="value">{{fuint64 .HeapAlloc}}</td></tr>
330<tr><td class="label">Total allocated bytes</td><td class="value">{{fuint64 .TotalAlloc}}</td></tr>
331<tr><td class="label">System bytes</td><td class="value">{{fuint64 .Sys}}</td></tr>
332<tr><td class="label">Heap system bytes</td><td class="value">{{fuint64 .HeapSys}}</td></tr>
333<tr><td class="label">Malloc calls</td><td class="value">{{fuint64 .Mallocs}}</td></tr>
334<tr><td class="label">Frees</td><td class="value">{{fuint64 .Frees}}</td></tr>
335<tr><td class="label">Idle heap bytes</td><td class="value">{{fuint64 .HeapIdle}}</td></tr>
336<tr><td class="label">In use bytes</td><td class="value">{{fuint64 .HeapInuse}}</td></tr>
337<tr><td class="label">Released to system bytes</td><td class="value">{{fuint64 .HeapReleased}}</td></tr>
338<tr><td class="label">Heap object count</td><td class="value">{{fuint64 .HeapObjects}}</td></tr>
339<tr><td class="label">Stack in use bytes</td><td class="value">{{fuint64 .StackInuse}}</td></tr>
340<tr><td class="label">Stack from system bytes</td><td class="value">{{fuint64 .StackSys}}</td></tr>
341<tr><td class="label">Bucket hash bytes</td><td class="value">{{fuint64 .BuckHashSys}}</td></tr>
342<tr><td class="label">GC metaata bytes</td><td class="value">{{fuint64 .GCSys}}</td></tr>
343<tr><td class="label">Off heap bytes</td><td class="value">{{fuint64 .OtherSys}}</td></tr>
344</table>
345<h2>By size</h2>
346<table>
347<tr><th>Size</th><th>Mallocs</th><th>Frees</th></tr>
348{{range .BySize}}<tr><td class="value">{{fuint32 .Size}}</td><td class="value">{{fuint64 .Mallocs}}</td><td class="value">{{fuint64 .Frees}}</td></tr>{{end}}
349</table>
350{{end}}
351`))
352
353var debugTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
354{{define "title"}}GoPls Debug pages{{end}}
355{{define "body"}}
356<a href="/debug/pprof">Profiling</a>
357<a href="/debug/rpcz">RPCz</a>
358<a href="/debug/tracez">Tracez</a>
359{{end}}
360`))
361
362var cacheTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
363{{define "title"}}Cache {{.ID}}{{end}}
364{{define "body"}}
365<h2>Sessions</h2>
366<ul>{{range .Sessions}}<li>{{template "sessionlink" .ID}}</li>{{end}}</ul>
367{{end}}
368`))
369
370var sessionTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
371{{define "title"}}Session {{.ID}}{{end}}
372{{define "body"}}
373From: <b>{{template "cachelink" .Cache.ID}}</b><br>
374<h2>Views</h2>
375<ul>{{range .Views}}<li>{{.Name}} is {{template "viewlink" .ID}} in {{.Folder}}</li>{{end}}</ul>
376<h2>Files</h2>
377<ul>{{range .Files}}<li>{{template "filelink" .}}</li>{{end}}</ul>
378{{end}}
379`))
380
381var viewTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
382{{define "title"}}View {{.ID}}{{end}}
383{{define "body"}}
384Name: <b>{{.Name}}</b><br>
385Folder: <b>{{.Folder}}</b><br>
386From: <b>{{template "sessionlink" .Session.ID}}</b><br>
387<h2>Environment</h2>
388<ul>{{range .Env}}<li>{{.}}</li>{{end}}</ul>
389{{end}}
390`))
391
392var fileTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
393{{define "title"}}File {{.Hash}}{{end}}
394{{define "body"}}
395From: <b>{{template "sessionlink" .Session.ID}}</b><br>
396URI: <b>{{.URI}}</b><br>
397Hash: <b>{{.Hash}}</b><br>
398Error: <b>{{.Error}}</b><br>
399<h3>Contents</h3>
400<pre>{{.Data}}</pre>
401{{end}}
402`))
403