blob: 015bdc179e329f42f87887aefa8bb6a37fa1c6e7 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"bytes"
"fmt"
"html"
"html/template"
"net"
"net/http"
"net/url"
"os/exec"
"runtime"
"strings"
"time"
"v.io/v23"
"v.io/v23/uniqueid"
"v.io/v23/context"
"v.io/v23/naming"
"v.io/v23/security"
"v.io/v23/services/logreader"
"v.io/v23/services/stats"
svtrace "v.io/v23/services/vtrace"
"v.io/v23/vdl"
"v.io/v23/verror"
"v.io/v23/vtrace"
"v.io/x/lib/cmdline"
"v.io/x/ref/lib/signals"
"v.io/x/ref/lib/v23cmd"
"v.io/x/ref/services/internal/pproflib"
"encoding/hex"
)
func init() {
cmdBrowse.Flags.StringVar(&flagBrowseAddr, "addr", "", "Address on which the interactive HTTP server will listen. For example, localhost:14141. If empty, defaults to localhost:<some random port>")
cmdBrowse.Flags.BoolVar(&flagBrowseLog, "log", true, "If true, log debug data obtained so that if a subsequent refresh from the browser fails, previously obtained information is available from the log file")
}
const browseProfilesPath = "/profiles"
var (
flagBrowseAddr string
flagBrowseLog bool
cmdBrowse = &cmdline.Command{
Runner: v23cmd.RunnerFunc(runBrowse),
Name: "browse",
Short: "Starts an interactive interface for debugging",
Long: `
Starts a webserver with a URL that when visited allows for inspection of a
remote process via a web browser.
This differs from browser.v.io in a few important ways:
(a) Does not require a chrome extension,
(b) Is not tied into the v.io cloud services
(c) Can be setup with alternative different credentials,
(d) The interface is more geared towards debugging a server than general purpose namespace browsing.
While (d) is easily overcome by sharing code between the two, (a), (b) & (c)
are not easy to work around. Of course, the down-side here is that this
requires explicit command-line invocation instead of being just a URL anyone
can visit (https://browser.v.io).
A dump of some possible future features:
TODO(ashankar):?
(1) Profiling: Should be able to use the webserver to profile the remote
process (via 'go tool pprof' for example). In the mean time, use the 'pprof'
command (instead of the 'browse' command) for this purpose.
(2) Trace browsing: Browse traces at the remote server, and possible force
the collection of some traces (avoiding the need to restart the remote server
with flags like --v23.vtrace.collect-regexp for example). In the mean time,
use the 'vtrace' command (instead of the 'browse' command) for this purpose.
(3) Log offsets: Log files can be large and currently the logging endpoint
of this interface downloads the full log file from the beginning. The ability
to start looking at the logs only from a specified offset might be useful
for these large files.
(4) Delegation: The 'browse' command requires the appropriate credentials to
inspect a remote process. Make delegation of these credentials to another
instance of the 'browse' command easier so that, for example, Bob can conveniently
ask Alice to debug his service without worrying about giving Alice the ability to
modify his service.
(5) Signature: Display the interfaces, types etc. defined by any suffix in the
remote process. in the mean time, use the 'vrpc signature' command for this purpose.
`,
ArgsName: "<name>",
ArgsLong: "<name> is the vanadium object name of the remote process to inspec",
}
)
func runBrowse(ctx *context.T, env *cmdline.Env, args []string) error {
if got, want := 1, len(args); got != want {
return env.UsageErrorf("interactive: must provide a single vanadium object name")
}
http.Handle("/", &resolveHandler{ctx})
http.Handle("/stats", &statsHandler{ctx})
http.Handle("/blessings", &blessingsHandler{ctx})
http.Handle("/logs", &logsHandler{ctx})
http.Handle("/glob", &globHandler{ctx})
http.Handle(browseProfilesPath, &profilesHandler{ctx})
http.Handle(browseProfilesPath+"/", &profilesHandler{ctx})
http.Handle("/vtraces", &allTracesHandler{ctx})
http.Handle("/vtrace", &vtraceHandler{ctx})
http.Handle("/favicon.ico", http.NotFoundHandler())
addr := flagBrowseAddr
if len(addr) == 0 {
addr = "127.0.0.1:0"
}
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
go http.Serve(ln, nil)
url := "http://" + ln.Addr().String() + "/?n=" + url.QueryEscape(args[0])
fmt.Printf("Visit %s and Ctrl-C to quit\n", url)
// Open the browser if we can
switch runtime.GOOS {
case "linux":
exec.Command("xdg-open", url).Start()
case "darwin":
exec.Command("open", url).Start()
}
<-signals.ShutdownOnSignals(ctx)
return nil
}
func executeTemplate(ctx *context.T, w http.ResponseWriter, r *http.Request, tmpl *template.Template, args interface{}) {
if flagBrowseLog {
ctx.Infof("DEBUG: %q -- %+v", r.URL, args)
}
if err := tmpl.Execute(w, args); err != nil {
fmt.Fprintf(w, "ERROR:%v", err)
ctx.Errorf("Error executing template %q: %v", tmpl.Name(), err)
}
}
// Tracer forces collection of a trace rooted at the call to newTracer.
type Tracer struct {
ctx *context.T
span vtrace.Span
}
func newTracer(ctx *context.T) (*context.T, *Tracer) {
ctx, span := vtrace.WithNewTrace(ctx)
vtrace.ForceCollect(ctx, 0)
return ctx, &Tracer{ctx, span}
}
func (t *Tracer) String() string {
if t == nil {
return ""
}
tr := vtrace.GetStore(t.ctx).TraceRecord(t.span.Trace())
if len(tr.Spans) == 0 {
// Do not bother with empty traces
return ""
}
var buf bytes.Buffer
// nil as the time.Location is fine because the HTTP "server" time is
// the same as that of the "client" (typically a browser on localhost).
vtrace.FormatTrace(&buf, vtrace.GetStore(t.ctx).TraceRecord(t.span.Trace()), nil)
return buf.String()
}
func withTimeout(ctx *context.T) *context.T {
ctx, _ = context.WithTimeout(ctx, timeout)
return ctx
}
type resolveHandler struct{ ctx *context.T }
func (h *resolveHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
name := r.FormValue("n")
var suffix string
ctx, tracer := newTracer(h.ctx)
m, err := v23.GetNamespace(ctx).Resolve(withTimeout(ctx), name)
if m != nil {
suffix = m.Name
}
args := struct {
ServerName string
CommandLine string
Vtrace *Tracer
MountEntry *naming.MountEntry
Error error
}{
ServerName: strings.TrimSuffix(name, suffix),
CommandLine: fmt.Sprintf("debug resolve %q", name),
Vtrace: tracer,
MountEntry: m,
Error: err,
}
executeTemplate(h.ctx, w, r, tmplBrowseResolve, args)
}
type blessingsHandler struct{ ctx *context.T }
func (h *blessingsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
name := r.FormValue("n")
ctx, tracer := newTracer(h.ctx)
call, err := v23.GetClient(ctx).StartCall(withTimeout(ctx), name, "DoNotReallyCareAboutTheMethod", nil)
args := struct {
ServerName string
CommandLine string
Vtrace *Tracer
Error error
Blessings security.Blessings
Recognized []string
CertificateChains [][]security.Certificate
}{
ServerName: name,
Vtrace: tracer,
CommandLine: fmt.Sprintf("vrpc identify %q", name),
Error: err,
}
if call != nil {
args.Recognized, args.Blessings = call.RemoteBlessings()
args.CertificateChains = security.MarshalBlessings(args.Blessings).CertificateChains
// Don't actually care about the RPC, so don't bother waiting on the Finish.
defer func() { go call.Finish() }()
}
executeTemplate(h.ctx, w, r, tmplBrowseBlessings, args)
}
type statsHandler struct{ ctx *context.T }
func (h *statsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var (
server = r.FormValue("n")
stat = r.FormValue("s")
prefix = naming.Join(server, "__debug", "stats")
name = naming.Join(prefix, stat)
ctx, tracer = newTracer(h.ctx)
)
v, err := stats.StatsClient(name).Value(withTimeout(ctx))
var children []string
var childrenErrors []error
if verror.ErrorID(err) == verror.ErrNoExist.ID {
// The stat itself isn't readable, maybe it is globable?
if glob, globErr := v23.GetNamespace(ctx).Glob(withTimeout(ctx), naming.Join(name, "*")); globErr == nil {
for e := range glob {
switch e := e.(type) {
case *naming.GlobReplyEntry:
children = append(children, strings.TrimPrefix(e.Value.Name, prefix))
case *naming.GlobReplyError:
childrenErrors = append(childrenErrors, e.Value.Error)
}
}
if len(children) == 1 {
// Single child, save an extra click
redirect, err := url.Parse(r.URL.String())
if err == nil {
q := redirect.Query()
q.Set("n", server)
q.Set("s", children[0])
redirect.RawQuery = q.Encode()
ctx.Infof("Redirecting from %v to %v", r.URL, redirect)
http.Redirect(w, r, redirect.String(), http.StatusTemporaryRedirect)
return
}
}
} else {
err = globErr
}
}
args := struct {
ServerName string
CommandLine string
Vtrace *Tracer
StatName string
Value *vdl.Value
Children []string
ChildrenErrors []error
Globbed bool
Error error
}{
ServerName: server,
Vtrace: tracer,
StatName: stat,
Value: v,
Error: err,
Children: children,
ChildrenErrors: childrenErrors,
Globbed: len(children)+len(childrenErrors) > 0,
}
if args.Globbed {
args.CommandLine = fmt.Sprintf("debug glob %q", naming.Join(name, "*"))
} else {
args.CommandLine = fmt.Sprintf("debug stats read %q", name)
}
executeTemplate(h.ctx, w, r, tmplBrowseStats, args)
}
type logsHandler struct{ ctx *context.T }
func (h *logsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var (
server = r.FormValue("n")
log = r.FormValue("l")
prefix = naming.Join(server, "__debug", "logs")
name = naming.Join(prefix, log)
path = r.URL.Path
list = func() bool {
for _, a := range r.Header[http.CanonicalHeaderKey("Accept")] {
if a == "text/event-stream" {
return true
}
}
return false
}()
ctx, _ = newTracer(h.ctx)
)
// The logs handler streams result to the web browser because there
// have been cases where there are ~1 million log files, so doing this
// streaming thing will make the UI more responsive.
//
// For the same reason, avoid setting a timeout.
if len(log) == 0 && list {
w.Header().Add("Content-Type", "text/event-stream")
glob, err := v23.GetNamespace(ctx).Glob(ctx, naming.Join(name, "*"))
if err != nil {
writeErrorEvent(w, err)
return
}
flusher, _ := w.(http.Flusher)
for e := range glob {
switch e := e.(type) {
case *naming.GlobReplyEntry:
logfile := strings.TrimPrefix(e.Value.Name, prefix+"/")
writeEvent(w, fmt.Sprintf(
`<a href="%s?n=%s&l=%s">%s</a>`,
path,
url.QueryEscape(server),
url.QueryEscape(logfile),
html.EscapeString(logfile)))
case *naming.GlobReplyError:
writeErrorEvent(w, e.Value.Error)
}
if flusher != nil {
flusher.Flush()
}
}
return
}
if len(log) == 0 {
args := struct {
ServerName string
CommandLine string
Vtrace *Tracer
}{
ServerName: server,
CommandLine: fmt.Sprintf("debug glob %q", naming.Join(name, "*")),
}
executeTemplate(h.ctx, w, r, tmplBrowseLogsList, args)
return
}
w.Header().Add("Content-Type", "text/plain")
stream, err := logreader.LogFileClient(name).ReadLog(ctx, 0, logreader.AllEntries, true)
if err != nil {
fmt.Fprintf(w, "ERROR(%v): %v\n", verror.ErrorID(err), err)
return
}
var (
entries = make(chan logreader.LogEntry)
abortRPC = make(chan bool)
abortHTTP <-chan bool
errch = make(chan error, 1) // At most one write on this channel, avoid blocking any goroutines
)
if notifier, ok := w.(http.CloseNotifier); ok {
abortHTTP = notifier.CloseNotify()
}
go func() {
// writes to: entries, errch
// reads from: abortRPC
defer stream.Finish()
defer close(entries)
iterator := stream.RecvStream()
for iterator.Advance() {
select {
case entries <- iterator.Value():
case <-abortRPC:
return
}
}
if err := iterator.Err(); err != nil {
errch <- err
}
}()
// reads from: entries, errch, abortHTTP
// writes to: abortRPC
defer close(abortRPC)
for {
select {
case e, more := <-entries:
if !more {
return
}
fmt.Fprintln(w, e.Line)
case err := <-errch:
fmt.Fprintf(w, "ERROR(%v): %v\n", verror.ErrorID(err), err)
return
case <-abortHTTP:
return
}
}
}
type globHandler struct{ ctx *context.T }
func (h *globHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
type entry struct {
Suffix string
Error error
}
var (
server = r.FormValue("n")
suffix = r.FormValue("s")
pattern = naming.Join(server, suffix, "*")
ctx, tracer = newTracer(h.ctx)
entries []entry
)
ch, err := v23.GetNamespace(ctx).Glob(withTimeout(ctx), pattern)
if err != nil {
entries = append(entries, entry{Error: err})
}
if ch != nil {
for e := range ch {
switch e := e.(type) {
case *naming.GlobReplyEntry:
entries = append(entries, entry{Suffix: strings.TrimPrefix(e.Value.Name, server)})
case *naming.GlobReplyError:
entries = append(entries, entry{Error: e.Value.Error})
}
}
}
args := struct {
ServerName string
CommandLine string
Vtrace *Tracer
Pattern string
Entries []entry
}{
ServerName: server,
CommandLine: fmt.Sprintf("debug glob %q", pattern),
Vtrace: tracer,
Pattern: pattern,
Entries: entries,
}
executeTemplate(h.ctx, w, r, tmplBrowseGlob, args)
}
type profilesHandler struct{ ctx *context.T }
func (h *profilesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var (
server = r.FormValue("n")
name = naming.Join(server, "__debug", "pprof")
)
if len(server) == 0 {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "Must specify a server with the URL query parameter 'n'")
return
}
if path := strings.TrimSuffix(r.URL.Path, "/"); path == strings.TrimSuffix(browseProfilesPath, "/") {
urlPrefix := fmt.Sprintf("http://%s%s/pprof", r.Host, path)
args := struct {
ServerName string
CommandLine string
Vtrace *Tracer
URLPrefix string
}{
ServerName: server,
CommandLine: fmt.Sprintf("debug pprof run %q", name),
URLPrefix: urlPrefix,
}
executeTemplate(h.ctx, w, r, tmplBrowseProfiles, args)
return
}
pproflib.PprofProxy(h.ctx, browseProfilesPath, name).ServeHTTP(w, r)
}
type allTracesHandler struct { ctx *context.T }
func (a *allTracesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var (
server = r.FormValue("n")
name = naming.Join(server, "__debug", "vtrace")
)
if len(server) == 0 {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "Must specify a server with the URL query parameter 'n'")
return
}
ctx, tracer := newTracer(a.ctx)
stub := svtrace.StoreClient(name)
call, err := stub.AllTraces(ctx)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, "Failed to get all trace ids on %s: %v", name, err)
return
}
stream := call.RecvStream()
var traces []string
for stream.Advance() {
id := stream.Value().Id
traces = append(traces, hex.EncodeToString(id[:]))
}
data := struct {
Ids []string
StreamError error
CallError error
ServerName string
CommandLine string
Vtrace *Tracer
}{
Ids: traces,
StreamError: stream.Err(),
CallError: call.Finish(),
CommandLine: fmt.Sprintf("debug vtraces %q", name),
ServerName: server,
Vtrace: tracer,
}
executeTemplate(a.ctx, w, r, tmplBrowseAllTraces, data)
}
type divTree struct {
Id string
// Start is a value from 0-100 which is the percentage of time into the parent span's duration
// that this span started.
Start int
// Width is a value from 0-100 which is the percentage of time in the parent span's duration this span
// took
Width int
Name string
Annotations []annotation
Children []divTree
}
type annotation struct {
// Position is a value from 0-100 which is the percentage of time into the span's duration that this
// annotation occured.
Position int
Msg string
}
func convertToTree(n *vtrace.Node, parentStart time.Time, parentEnd time.Time) *divTree {
startTime := n.Span.Start
if startTime.IsZero() {
startTime = parentStart
}
endTime := n.Span.End
if endTime.IsZero() {
endTime = parentEnd
}
parentDuration := parentEnd.Sub(parentStart).Seconds()
start := int(100 * startTime.Sub(parentStart).Seconds() / parentDuration)
end := int(100 * endTime.Sub(parentStart).Seconds() / parentDuration)
width := end - start
top := &divTree{
Id: n.Span.Id.String(),
Start: start,
Width: width,
Name: n.Span.Name,
}
top.Annotations = make([]annotation, len(n.Span.Annotations))
for i, a := range n.Span.Annotations {
top.Annotations[i].Msg = a.Message
if a.When.IsZero() {
top.Annotations[i].Position = 0
continue
}
top.Annotations[i].Position = int(100 * a.When.Sub(parentStart).Seconds() / parentDuration) - start
}
top.Children = make([]divTree, len(n.Children))
for i, c := range n.Children {
top.Children[i] = *convertToTree(c, startTime, endTime)
}
return top
}
func findStartTime(n *vtrace.Node) time.Time {
if !n.Span.Start.IsZero() {
return n.Span.Start
}
var startTime time.Time
for _, a := range n.Span.Annotations {
startTime = a.When
if !startTime.IsZero() {
break
}
}
for _, c := range n.Children {
childStartTime := findStartTime(c)
if startTime.IsZero() || (!childStartTime.IsZero() && startTime.After(childStartTime)) {
startTime = childStartTime
}
if !startTime.IsZero() {
break;
}
}
return startTime
}
func findEndTime(n *vtrace.Node) time.Time {
if !n.Span.End.IsZero() {
return n.Span.End
}
size := len(n.Span.Annotations)
var endTime time.Time
for i := range n.Span.Annotations {
endTime = n.Span.Annotations[size - 1 - i].When
if !endTime.IsZero() {
break
}
}
size = len(n.Children)
for i := range n.Children {
childEndTime := findEndTime(n.Children[size - 1 - i])
if endTime.IsZero() || (!childEndTime.IsZero() && childEndTime.After(endTime)) {
endTime = childEndTime
}
if !endTime.IsZero() {
break
}
}
return endTime
}
type vtraceHandler struct {ctx *context.T}
func (v *vtraceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var (
server = r.FormValue("n")
traceId = r.FormValue("t")
name = naming.Join(server, "__debug", "vtrace")
)
if len(server) == 0 {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "Must specify a server with the URL query parameter 'n'")
return
}
stub := svtrace.StoreClient(name)
ctx, tracer := newTracer(v.ctx)
id, err := uniqueid.FromHexString(traceId)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "Invalid trace id %s: %v", traceId, err)
return
}
trace, err := stub.Trace(ctx, id)
if err != nil {
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, "Unknown trace id: %s", traceId)
return
}
var buf bytes.Buffer
vtrace.FormatTrace(&buf, &trace, nil)
node := vtrace.BuildTree(&trace)
if node == nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, "coud not find root span for trace")
return
}
tree := convertToTree(node, findStartTime(node), findEndTime(node))
data := struct{
Id string
Root *divTree
ServerName string
CommandLine string
DebugTrace string
Vtrace *Tracer
}{
Id: traceId,
Root: tree,
ServerName: server,
CommandLine: fmt.Sprintf("debug vtraces %q", name),
Vtrace: tracer,
DebugTrace: buf.String(),
}
executeTemplate(v.ctx, w, r, tmplBrowseVtrace, data)
}
func writeEvent(w http.ResponseWriter, data string) {
fmt.Fprintf(w, "data: %s\n\n", strings.TrimSpace(data))
}
func writeErrorEvent(w http.ResponseWriter, err error) {
id := fmt.Sprintf("%v", verror.ErrorID(err))
writeEvent(w, fmt.Sprintf("ERROR(%v): %v", html.EscapeString(id), html.EscapeString(err.Error())))
}
func makeTemplate(name, content string) *template.Template {
content = "{{template `.header` .}}" + content + "{{template `.footer` .}}"
t := template.Must(tmplBrowseHeader.Clone())
t = template.Must(t.AddParseTree(tmplBrowseFooter.Name(), tmplBrowseFooter.Tree))
colors := []string{"red", "blue", "green"}
pos := 0
t = t.Funcs(template.FuncMap{
"verrorID": verror.ErrorID,
"unmarshalPublicKey": security.UnmarshalPublicKey,
"endpoint": func(n string) (naming.Endpoint, error) {
if naming.Rooted(n) {
n, _ = naming.SplitAddressName(n)
}
return v23.NewEndpoint(n)
},
"endpointName": func(ep naming.Endpoint) string { return ep.Name() },
"goValueFromVDL": func(v *vdl.Value) interface{} {
var ret interface{}
vdl.Convert(&ret, v)
return ret
},
"nextColor": func() string {
c := colors[pos]
pos = (pos + 1) % len(colors)
return c
},
})
t = template.Must(t.New(name).Parse(content))
return t
}
var (
tmplBrowseResolve = makeTemplate("resolve", `
<section class="section--center mdl-grid">
<h5>Name resolution</h5>
<div class="mdl-cell mdl-cell--12-col">
{{with .MountEntry}}
<ul>
{{with .Name}}<li>Suffix: {{.}}</li>{{end}}
{{with .ServesMountTable}}<li>This server is a mounttable</li>{{end}}
{{with .IsLeaf}}<li>This is a leaf server</li>{{end}}
</ul>
{{range .Servers}}
<div class="mdl-cell mdl-cell--12-col">
<table class="mdl-data-table mdl-js-data-table mdl-data-table--selectable mdl-shadow--2dp">
<tbody>
<tr>
<td class="mdl-data-table__cell--non-numeric">Endpoint</td>
<td class="mdl-data-table__cell--non-numeric"><a href="/?n={{endpoint .Server | endpointName | urlquery}}">{{.Server}}</a></td>
</tr>
<tr>
<td class="mdl-data-table__cell--non-numeric">Expires</td>
<td class="mdl-data-table__cell--non-numeric">{{.Deadline}}</td>
</tr>
{{with $ep := endpoint .Server}}
<tr>
<td class="mdl-data-table__cell--non-numeric">Network</td>
<td class="mdl-data-table__cell--non-numeric">{{.Addr.Network}}</td>
</tr>
<tr>
<td class="mdl-data-table__cell--non-numeric">Address</td>
<td class="mdl-data-table__cell--non-numeric">{{.Addr}}</td>
</tr>
<tr>
<td class="mdl-data-table__cell--non-numeric">RoutingID</td>
<td class="mdl-data-table__cell--non-numeric">{{.RoutingID}}</td>
</tr>
{{with .BlessingNames}}
<tr>
<td class="mdl-data-table__cell--non-numeric">Blessings ({{len .}})</td>
<td class="mdl-data-table__cell--non-numeric">{{.}}</td>
</tr>
{{end}}
{{with .Routes}}
<tr>
<td class="mdl-data-table__cell--non-numeric">Routes ({{len .}})</td>
<td class="mdl-data-table__cell--non-numeric">{{.}}</td>
</tr>
{{end}}
{{end}}
</tbody>
</table>
</div>
{{end}}
{{else}}
Name resolution came up empty
{{end}}
</div>
</section>
{{with .Error}}
<section class="section--center mdl-grid">
<h5><i class="material-icons">info</i>ERROR({{verrorID .}})</h5>
<div class="mdl-cell mdl-cell--12-col fixed-width">{{.}}</div>
</section>
{{end}}
`)
tmplBrowseBlessings = makeTemplate("blessings", `
<section class="section--center mdl-grid">
<h5>Claimed</h5>
<div class="mdl-cell mdl-cell--12-col">
{{.Blessings}}
</div>
</section>
{{with .Recognized}}
<section class="section--center mdl-grid">
<h5>Recognized</h5>
<div class="mdl-cell mdl-cell--12-col">
<ul>
{{range .}}
<li>{{.}}</li>
{{end}}
</ul>
</div>
</section>
{{end}}
<section class="section--center mdl-grid">
<h5>PublicKey</h5>
<div class="mdl-cell mdl-cell--12-col fixed-width">
{{.Blessings.PublicKey}}
</div>
</section>
{{with .CertificateChains}}
<section class="section--center mdl-grid">
<h5>Certificate Chains (Total: {{len .}})</h5>
<div class="mdl-cell mdl-cell--12-col">
{{range $chainidx, $chain := .}}
<section class="section--center mdl-grid">
<h6>Chain #{{$chainidx}}</h6>
<div class="mdl-cell mdl-cell--12-col">
<table class="mdl-data-table mdl-js-data-table mdl-data-table--selectable mdl-shadow--2dp">
<thead>
<tr>
<td class="mdl-data-table__cell--non-numeric">Certificate</td>
<td class="mdl-data-table__cell--non-numeric">Extension</td>
<td class="mdl-data-table__cell--non-numeric">Blessed Public Key</td>
<td class="mdl-data-table__cell--non-numeric">Caveats</td>
</tr>
</thead>
<tbody>
{{range $certidx, $cert := $chain}}
<tr>
<td>{{$certidx}}</td>
<td class="mdl-data-table__cell--non-numeric">{{$cert.Extension}}</td>
<td class="mdl-data-table__cell--non-numeric fixed-width">{{unmarshalPublicKey $cert.PublicKey}}</td>
<td class="mdl-data-table__cell--non-numeric">{{len $cert.Caveats}}
{{range $cavidx, $cav := $cert.Caveats}}
<br/>#{{$cavidx}}: {{$cav}}
{{end}}
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</section>
{{end}}
</div>
</section>
{{end}}
{{with .Error}}
<section class="section--center mdl-grid">
<h5><i class="material-icons">info</i>ERROR({{verrorID .}})</h5>
<div class="mdl-cell mdl-cell--12-col fixed-width">{{.}}</div>
</section>
{{end}}
`)
tmplBrowseStats = makeTemplate("stats", `
{{if .Globbed}}
<section class="section--center mdl-grid">
<h5>Glob</h5>
<div class="mdl-cell mdl-cell--12-col">
<ul>
{{range .Children}}
<li><a href="/stats?n={{urlquery $.ServerName}}&s={{urlquery .}}">{{.}}</a></li>
{{end}}
{{range .ChildrenErrors}}
<li>ERROR({{verrorID .}}): {{.}}</li>
{{end}}
</ul>
</div>
</section>
{{end}}
{{if .Value}}
<section class="section--center mdl-grid">
<div class="mdl-cell mdl-cell--12-col">
<table class="mdl-data-table mdl-js-data-table mdl-data-table--selectable mdl-shadow--2dp">
<tbody>
{{with $goVal := goValueFromVDL .Value}}
<tr>
<td class="mdl-data-table__cell--non-numeric">Value (Go)</td>
<td class="mdl-data-table__cell--non-numeric"><pre>{{$goVal}}</pre></td>
</tr>
<tr>
<td class="mdl-data-table__cell--non-numeric">Type (Go)</td>
<td class="mdl-data-table__cell--non-numeric fixed-width">{{printf "%T" $goVal}}</td>
</tr>
{{else}}
<tr>
<td class="mdl-data-table__cell--non-numeric">Value (VDL)</td>
<td class="mdl-data-table__cell--non-numeric fixed-width">{{.Value}}</td>
</tr>
{{end}}
<tr>
<td class="mdl-data-table__cell--non-numeric">Type (VDL)</td>
<td class="mdl-data-table__cell--non-numeric fixed-width">{{.Value.Type}}</td>
</tr>
</tbody>
</table>
</div>
</section>
{{end}}
{{if not .Globbed}}
{{with .Error}}
<section class="section--center mdl-grid">
<h5><i class="material-icons">info</i>ERROR({{verrorID .}})</h5>
<div class="mdl-cell mdl-cell--12-col fixed-width">{{.}}</div>
</section>
{{end}}
{{end}}
`)
tmplBrowseLogsList = makeTemplate("logs", `
<section class="section--center mdl-grid">
<h5>List of log files</h5>
<div id="parent" class="mdl-cell mdl-cell--12-col">
<script>
var source = new EventSource("/logs?n={{urlquery .ServerName}}");
source.onmessage = function(event) {
var ol = document.getElementById("logfiles");
var li = document.createElement("li");
li.innerHTML = event.data;
ol.appendChild(li);
}
source.onerror = function() {
source.close();
document.getElementById("parent").removeChild(document.getElementById("progress"));
}
</script>
<div id="progress" class="mdl-progress mdl-js-progress mdl-progress__indeterminate"></div>
<ol id="logfiles">
</ol>
</div>
</section>
`)
tmplBrowseGlob = makeTemplate("glob", `
<section class="section--center mdl-grid">
<h5>{{.Pattern}}</h5>
<div id="parent" class="mdl-cell mdl-cell--12-col">
<ol>
{{range .Entries}}
{{with .Suffix}}
<li><a href="/glob?n={{urlquery $.ServerName}}&s={{urlquery .}}">{{.}}</a></li>
{{end}}
{{with .Error}}
<li>ERROR({{verrorID .}}): {{.}}</li>
{{end}}
{{end}}
</ol>
</div>
</section>
`)
tmplBrowseProfiles = makeTemplate("profiles", `
<section class="section--center mdl-grid">
<h5>Profiling</h5>
<div id="parent" class="mdl-cell mdl-cell--12-col">
<ul>
<li>CPU
<div class="fixed-width">go tool pprof {{.URLPrefix}}/profile?n={{urlquery .ServerName}}</div>
</li>
<li><a href="{{.URLPrefix}}/heap?n={{urlquery .ServerName}}&debug=1">Heap</a>
<div class="fixed-width">go tool pprof {{.URLPrefix}}/heap?n={{urlquery .ServerName}}</div>
</li>
<li><a href="{{.URLPrefix}}/block?n={{urlquery .ServerName}}&debug=1">Block</a>
<div class="fixed-width">go tool pprof {{.URLPrefix}}/block?n={{urlquery .ServerName}}</div>
</li>
<li><a href="{{.URLPrefix}}/threadcreate?n={{urlquery .ServerName}}&debug=1">Threadcreate</a>
<div class="fixed-width">go tool pprof {{.URLPrefix}}/threadcreate?n={{urlquery .ServerName}}</div>
</li>
<li>Goroutines:
<a href="{{.URLPrefix}}/goroutine?n={{urlquery .ServerName}}&debug=1">(compact)</a>
<a href="{{.URLPrefix}}/goroutine?n={{urlquery .ServerName}}&debug=2">(full)</a>
</li>
</ul>
<div id="parent" class="mdl-cell mdl-cell--12-col">
<i class="material-icons">info</i>The commands above may not work if the
remote process isn't written in Go. Support for profiling code in other
languages is in the wishlist.
</div>
</div>
</section>
`)
tmplBrowseAllTraces = makeTemplate("alltraces", `
<section class="section-center mdl-grid">
<ul>
{{$name := .ServerName}}
{{range .Ids}}
<li><a href="/vtrace?n={{$name}}&t={{.}}">{{.}}</a></li>
{{end}}
</ul>
</section>
`)
tmplBrowseVtrace = makeTemplate("vtrace",`
{{define ".span"}}
<div style="position:relative;left:{{.Start}}%;width:{{.Width}}%;margin:0px;padding-top:2px;" id="div-{{.Id}}">
<!-- Root span -->
<div id="root" title="{{.Name}}" style="position:relative;width:100%;background:{{nextColor}};height:15px;display:block;margin:0px;padding:0px"></div>
{{range $i, $child := .Children}}
{{template ".span" $child}}
{{end}}
</div>
{{end}}
{{define ".collapse-nav"}}
<div id="tree-{{.Id}}" style="position:relative;left:5px">
<div style="position:relative;height:15px;font:10pt" onclick='javascript:toggleTrace("{{.Id}}")'>{{len .Children}}</div>
{{range .Children}}
{{template ".collapse-nav" .}}
{{end}}
</div>
{{end}}
<script language="javascript">
function toggleTrace(id) {
console.log('Calling func on ' + id);
var treeRoot = document.getElementById("tree-" + id);
var on = treeRoot.closed;
treeRoot.closed = !treeRoot.closed;
var newSetting = on ? "none" : "block";
for (var i = 1; i < treeRoot.children.length; i++) {
treeRoot.children[i].style.display = newSetting;
}
var divRoot = document.getElementById("div-" + id);
for (var i = 1; i < divRoot.children.length; i++) {
divRoot.children[i].style.display = newSetting;
}
}
</script>
<section class="section--center mdl-grid">
<h5>Vtrace for {{.Id}}</h5>
<pre>{{.DebugTrace}}</pre>
<div class="mdl-cell mdl-cell--12-col">
<div style="display:flex;flex-direction:row">
<div style="min-width:10%">
{{template ".collapse-nav" .Root}}
</div>
<div id="parent" style="width:80%">
{{template ".span" .Root}}
</div>
</div>
</div>
</section>
`)
tmplBrowseHeader = template.Must(template.New(".header").Parse(`
{{define ".header"}}
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Debugging {{.ServerName}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://storage.googleapis.com/code.getmdl.io/1.0.6/material.teal-blue.min.css">
<script src="https://storage.googleapis.com/code.getmdl.io/1.0.6/material.min.js"></script>
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<style>
.fixed-width { font-family: monospace; }
</style>
</head>
<body>
<!-- Always shows a header, even in smaller screens. -->
<div class="mdl-layout mdl-js-layout mdl-layout--fixed-header">
<header class="mdl-layout__header">
<div class="mdl-layout__header-row">
<!-- Title -->
<span class="mdl-layout-title">Debug</span>
<!-- Add spacer, to align navigation to the right -->
<div class="mdl-layout-spacer"></div>
<!-- Navigation. We hide it in small screens. -->
<nav class="mdl-navigation mdl-layout--large-screen-only">
<a class="mdl-navigation__link" href="/?n={{.ServerName}}">Name</a>
<a class="mdl-navigation__link" href="/blessings?n={{.ServerName}}">Blessings</a>
<a class="mdl-navigation__link" href="/stats?n={{.ServerName}}">Stats</a>
<a class="mdl-navigation__link" href="/logs?n={{.ServerName}}">Logs</a>
<a class="mdl-navigation__link" href="/glob?n={{.ServerName}}">Glob</a>
<a class="mdl-navigation__link" href="/profiles?n={{.ServerName}}">Profiles</a>
<a class="mdl-navigation__link" href="/vtraces?n={{.ServerName}}">Traces</a>
</nav>
</div>
</header>
<div class="mdl-layout__drawer">
<nav class="mdl-navigation">
<a class="mdl-navigation__link" href="/?n={{.ServerName}}">Name</a>
<a class="mdl-navigation__link" href="/blessings?n={{.ServerName}}">Blessings</a>
<a class="mdl-navigation__link" href="/stats?n={{.ServerName}}">Stats</a>
<a class="mdl-navigation__link" href="/logs?n={{.ServerName}}">Logs</a>
<a class="mdl-navigation__link" href="/glob?n={{.ServerName}}">Glob</a>
<a class="mdl-navigation__link" href="/profiles?n={{.ServerName}}">Profiles</a>
<a class="mdl-navigation__link" href="/vtraces?n={{.ServerName}}">Traces</a>
</nav>
</div>
<main class="mdl-layout__content">
<section class="section--center mdl-grid">
<h5 class="fixed-width">{{.ServerName}}</h5>
</section>
{{end}}
`))
tmplBrowseFooter = template.Must(template.New(".footer").Parse(`
{{define ".footer"}}
<hr/>
{{with .CommandLine}}
<section class="section--center mdl-grid">
<h5>CommandLine</h5>
<div class="mdl-cell mdl-cell--12-col fixed-width">{{.}}</div>
</section>
{{end}}
{{with printf "%v" .Vtrace}}
<section class="section--center mdl-grid">
<h5>Trace</h5>
<div class="mdl-cell mdl-cell--12-col"><pre>{{.}}</pre></div>
</section>
{{end}}
</main>
</div>
</body>
</html>
{{end}}
`))
)