1// Copyright 2017 Google Inc. All Rights Reserved. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package driver 16 17import ( 18 "bytes" 19 "fmt" 20 "html/template" 21 "net" 22 "net/http" 23 gourl "net/url" 24 "os" 25 "os/exec" 26 "strconv" 27 "strings" 28 "time" 29 30 "github.com/google/pprof/internal/graph" 31 "github.com/google/pprof/internal/plugin" 32 "github.com/google/pprof/internal/report" 33 "github.com/google/pprof/profile" 34) 35 36// webInterface holds the state needed for serving a browser based interface. 37type webInterface struct { 38 prof *profile.Profile 39 options *plugin.Options 40 help map[string]string 41 templates *template.Template 42 settingsFile string 43} 44 45func makeWebInterface(p *profile.Profile, opt *plugin.Options) (*webInterface, error) { 46 settingsFile, err := settingsFileName() 47 if err != nil { 48 return nil, err 49 } 50 templates := template.New("templategroup") 51 addTemplates(templates) 52 report.AddSourceTemplates(templates) 53 return &webInterface{ 54 prof: p, 55 options: opt, 56 help: make(map[string]string), 57 templates: templates, 58 settingsFile: settingsFile, 59 }, nil 60} 61 62// maxEntries is the maximum number of entries to print for text interfaces. 63const maxEntries = 50 64 65// errorCatcher is a UI that captures errors for reporting to the browser. 66type errorCatcher struct { 67 plugin.UI 68 errors []string 69} 70 71func (ec *errorCatcher) PrintErr(args ...interface{}) { 72 ec.errors = append(ec.errors, strings.TrimSuffix(fmt.Sprintln(args...), "\n")) 73 ec.UI.PrintErr(args...) 74} 75 76// webArgs contains arguments passed to templates in webhtml.go. 77type webArgs struct { 78 Title string 79 Errors []string 80 Total int64 81 SampleTypes []string 82 Legend []string 83 Help map[string]string 84 Nodes []string 85 HTMLBody template.HTML 86 TextBody string 87 Top []report.TextItem 88 FlameGraph template.JS 89 Configs []configMenuEntry 90} 91 92func serveWebInterface(hostport string, p *profile.Profile, o *plugin.Options, disableBrowser bool) error { 93 host, port, err := getHostAndPort(hostport) 94 if err != nil { 95 return err 96 } 97 interactiveMode = true 98 ui, err := makeWebInterface(p, o) 99 if err != nil { 100 return err 101 } 102 for n, c := range pprofCommands { 103 ui.help[n] = c.description 104 } 105 for n, help := range configHelp { 106 ui.help[n] = help 107 } 108 ui.help["details"] = "Show information about the profile and this view" 109 ui.help["graph"] = "Display profile as a directed graph" 110 ui.help["reset"] = "Show the entire profile" 111 ui.help["save_config"] = "Save current settings" 112 113 server := o.HTTPServer 114 if server == nil { 115 server = defaultWebServer 116 } 117 args := &plugin.HTTPServerArgs{ 118 Hostport: net.JoinHostPort(host, strconv.Itoa(port)), 119 Host: host, 120 Port: port, 121 Handlers: map[string]http.Handler{ 122 "/": http.HandlerFunc(ui.dot), 123 "/top": http.HandlerFunc(ui.top), 124 "/disasm": http.HandlerFunc(ui.disasm), 125 "/source": http.HandlerFunc(ui.source), 126 "/peek": http.HandlerFunc(ui.peek), 127 "/flamegraph": http.HandlerFunc(ui.flamegraph), 128 "/saveconfig": http.HandlerFunc(ui.saveConfig), 129 "/deleteconfig": http.HandlerFunc(ui.deleteConfig), 130 "/download": http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 131 w.Header().Set("Content-Type", "application/vnd.google.protobuf+gzip") 132 w.Header().Set("Content-Disposition", "attachment;filename=profile.pb.gz") 133 p.Write(w) 134 }), 135 }, 136 } 137 138 url := "http://" + args.Hostport 139 140 o.UI.Print("Serving web UI on ", url) 141 142 if o.UI.WantBrowser() && !disableBrowser { 143 go openBrowser(url, o) 144 } 145 return server(args) 146} 147 148func getHostAndPort(hostport string) (string, int, error) { 149 host, portStr, err := net.SplitHostPort(hostport) 150 if err != nil { 151 return "", 0, fmt.Errorf("could not split http address: %v", err) 152 } 153 if host == "" { 154 host = "localhost" 155 } 156 var port int 157 if portStr == "" { 158 ln, err := net.Listen("tcp", net.JoinHostPort(host, "0")) 159 if err != nil { 160 return "", 0, fmt.Errorf("could not generate random port: %v", err) 161 } 162 port = ln.Addr().(*net.TCPAddr).Port 163 err = ln.Close() 164 if err != nil { 165 return "", 0, fmt.Errorf("could not generate random port: %v", err) 166 } 167 } else { 168 port, err = strconv.Atoi(portStr) 169 if err != nil { 170 return "", 0, fmt.Errorf("invalid port number: %v", err) 171 } 172 } 173 return host, port, nil 174} 175func defaultWebServer(args *plugin.HTTPServerArgs) error { 176 ln, err := net.Listen("tcp", args.Hostport) 177 if err != nil { 178 return err 179 } 180 isLocal := isLocalhost(args.Host) 181 handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 182 if isLocal { 183 // Only allow local clients 184 host, _, err := net.SplitHostPort(req.RemoteAddr) 185 if err != nil || !isLocalhost(host) { 186 http.Error(w, "permission denied", http.StatusForbidden) 187 return 188 } 189 } 190 h := args.Handlers[req.URL.Path] 191 if h == nil { 192 // Fall back to default behavior 193 h = http.DefaultServeMux 194 } 195 h.ServeHTTP(w, req) 196 }) 197 198 // We serve the ui at /ui/ and redirect there from the root. This is done 199 // to surface any problems with serving the ui at a non-root early. See: 200 // 201 // https://github.com/google/pprof/pull/348 202 mux := http.NewServeMux() 203 mux.Handle("/ui/", http.StripPrefix("/ui", handler)) 204 mux.Handle("/", redirectWithQuery("/ui")) 205 s := &http.Server{Handler: mux} 206 return s.Serve(ln) 207} 208 209func redirectWithQuery(path string) http.HandlerFunc { 210 return func(w http.ResponseWriter, r *http.Request) { 211 pathWithQuery := &gourl.URL{Path: path, RawQuery: r.URL.RawQuery} 212 http.Redirect(w, r, pathWithQuery.String(), http.StatusTemporaryRedirect) 213 } 214} 215 216func isLocalhost(host string) bool { 217 for _, v := range []string{"localhost", "127.0.0.1", "[::1]", "::1"} { 218 if host == v { 219 return true 220 } 221 } 222 return false 223} 224 225func openBrowser(url string, o *plugin.Options) { 226 // Construct URL. 227 baseURL, _ := gourl.Parse(url) 228 current := currentConfig() 229 u, _ := current.makeURL(*baseURL) 230 231 // Give server a little time to get ready. 232 time.Sleep(time.Millisecond * 500) 233 234 for _, b := range browsers() { 235 args := strings.Split(b, " ") 236 if len(args) == 0 { 237 continue 238 } 239 viewer := exec.Command(args[0], append(args[1:], u.String())...) 240 viewer.Stderr = os.Stderr 241 if err := viewer.Start(); err == nil { 242 return 243 } 244 } 245 // No visualizer succeeded, so just print URL. 246 o.UI.PrintErr(u.String()) 247} 248 249// makeReport generates a report for the specified command. 250// If configEditor is not null, it is used to edit the config used for the report. 251func (ui *webInterface) makeReport(w http.ResponseWriter, req *http.Request, 252 cmd []string, configEditor func(*config)) (*report.Report, []string) { 253 cfg := currentConfig() 254 if err := cfg.applyURL(req.URL.Query()); err != nil { 255 http.Error(w, err.Error(), http.StatusBadRequest) 256 ui.options.UI.PrintErr(err) 257 return nil, nil 258 } 259 if configEditor != nil { 260 configEditor(&cfg) 261 } 262 catcher := &errorCatcher{UI: ui.options.UI} 263 options := *ui.options 264 options.UI = catcher 265 _, rpt, err := generateRawReport(ui.prof, cmd, cfg, &options) 266 if err != nil { 267 http.Error(w, err.Error(), http.StatusBadRequest) 268 ui.options.UI.PrintErr(err) 269 return nil, nil 270 } 271 return rpt, catcher.errors 272} 273 274// render generates html using the named template based on the contents of data. 275func (ui *webInterface) render(w http.ResponseWriter, req *http.Request, tmpl string, 276 rpt *report.Report, errList, legend []string, data webArgs) { 277 file := getFromLegend(legend, "File: ", "unknown") 278 profile := getFromLegend(legend, "Type: ", "unknown") 279 data.Title = file + " " + profile 280 data.Errors = errList 281 data.Total = rpt.Total() 282 data.SampleTypes = sampleTypes(ui.prof) 283 data.Legend = legend 284 data.Help = ui.help 285 data.Configs = configMenu(ui.settingsFile, *req.URL) 286 287 html := &bytes.Buffer{} 288 if err := ui.templates.ExecuteTemplate(html, tmpl, data); err != nil { 289 http.Error(w, "internal template error", http.StatusInternalServerError) 290 ui.options.UI.PrintErr(err) 291 return 292 } 293 w.Header().Set("Content-Type", "text/html") 294 w.Write(html.Bytes()) 295} 296 297// dot generates a web page containing an svg diagram. 298func (ui *webInterface) dot(w http.ResponseWriter, req *http.Request) { 299 rpt, errList := ui.makeReport(w, req, []string{"svg"}, nil) 300 if rpt == nil { 301 return // error already reported 302 } 303 304 // Generate dot graph. 305 g, config := report.GetDOT(rpt) 306 legend := config.Labels 307 config.Labels = nil 308 dot := &bytes.Buffer{} 309 graph.ComposeDot(dot, g, &graph.DotAttributes{}, config) 310 311 // Convert to svg. 312 svg, err := dotToSvg(dot.Bytes()) 313 if err != nil { 314 http.Error(w, "Could not execute dot; may need to install graphviz.", 315 http.StatusNotImplemented) 316 ui.options.UI.PrintErr("Failed to execute dot. Is Graphviz installed?\n", err) 317 return 318 } 319 320 // Get all node names into an array. 321 nodes := []string{""} // dot starts with node numbered 1 322 for _, n := range g.Nodes { 323 nodes = append(nodes, n.Info.Name) 324 } 325 326 ui.render(w, req, "graph", rpt, errList, legend, webArgs{ 327 HTMLBody: template.HTML(string(svg)), 328 Nodes: nodes, 329 }) 330} 331 332func dotToSvg(dot []byte) ([]byte, error) { 333 cmd := exec.Command("dot", "-Tsvg") 334 out := &bytes.Buffer{} 335 cmd.Stdin, cmd.Stdout, cmd.Stderr = bytes.NewBuffer(dot), out, os.Stderr 336 if err := cmd.Run(); err != nil { 337 return nil, err 338 } 339 340 // Fix dot bug related to unquoted ampersands. 341 svg := bytes.Replace(out.Bytes(), []byte("&;"), []byte("&;"), -1) 342 343 // Cleanup for embedding by dropping stuff before the <svg> start. 344 if pos := bytes.Index(svg, []byte("<svg")); pos >= 0 { 345 svg = svg[pos:] 346 } 347 return svg, nil 348} 349 350func (ui *webInterface) top(w http.ResponseWriter, req *http.Request) { 351 rpt, errList := ui.makeReport(w, req, []string{"top"}, func(cfg *config) { 352 cfg.NodeCount = 500 353 }) 354 if rpt == nil { 355 return // error already reported 356 } 357 top, legend := report.TextItems(rpt) 358 var nodes []string 359 for _, item := range top { 360 nodes = append(nodes, item.Name) 361 } 362 363 ui.render(w, req, "top", rpt, errList, legend, webArgs{ 364 Top: top, 365 Nodes: nodes, 366 }) 367} 368 369// disasm generates a web page containing disassembly. 370func (ui *webInterface) disasm(w http.ResponseWriter, req *http.Request) { 371 args := []string{"disasm", req.URL.Query().Get("f")} 372 rpt, errList := ui.makeReport(w, req, args, nil) 373 if rpt == nil { 374 return // error already reported 375 } 376 377 out := &bytes.Buffer{} 378 if err := report.PrintAssembly(out, rpt, ui.options.Obj, maxEntries); err != nil { 379 http.Error(w, err.Error(), http.StatusBadRequest) 380 ui.options.UI.PrintErr(err) 381 return 382 } 383 384 legend := report.ProfileLabels(rpt) 385 ui.render(w, req, "plaintext", rpt, errList, legend, webArgs{ 386 TextBody: out.String(), 387 }) 388 389} 390 391// source generates a web page containing source code annotated with profile 392// data. 393func (ui *webInterface) source(w http.ResponseWriter, req *http.Request) { 394 args := []string{"weblist", req.URL.Query().Get("f")} 395 rpt, errList := ui.makeReport(w, req, args, nil) 396 if rpt == nil { 397 return // error already reported 398 } 399 400 // Generate source listing. 401 var body bytes.Buffer 402 if err := report.PrintWebList(&body, rpt, ui.options.Obj, maxEntries); err != nil { 403 http.Error(w, err.Error(), http.StatusBadRequest) 404 ui.options.UI.PrintErr(err) 405 return 406 } 407 408 legend := report.ProfileLabels(rpt) 409 ui.render(w, req, "sourcelisting", rpt, errList, legend, webArgs{ 410 HTMLBody: template.HTML(body.String()), 411 }) 412} 413 414// peek generates a web page listing callers/callers. 415func (ui *webInterface) peek(w http.ResponseWriter, req *http.Request) { 416 args := []string{"peek", req.URL.Query().Get("f")} 417 rpt, errList := ui.makeReport(w, req, args, func(cfg *config) { 418 cfg.Granularity = "lines" 419 }) 420 if rpt == nil { 421 return // error already reported 422 } 423 424 out := &bytes.Buffer{} 425 if err := report.Generate(out, rpt, ui.options.Obj); err != nil { 426 http.Error(w, err.Error(), http.StatusBadRequest) 427 ui.options.UI.PrintErr(err) 428 return 429 } 430 431 legend := report.ProfileLabels(rpt) 432 ui.render(w, req, "plaintext", rpt, errList, legend, webArgs{ 433 TextBody: out.String(), 434 }) 435} 436 437// saveConfig saves URL configuration. 438func (ui *webInterface) saveConfig(w http.ResponseWriter, req *http.Request) { 439 if err := setConfig(ui.settingsFile, *req.URL); err != nil { 440 http.Error(w, err.Error(), http.StatusBadRequest) 441 ui.options.UI.PrintErr(err) 442 return 443 } 444} 445 446// deleteConfig deletes a configuration. 447func (ui *webInterface) deleteConfig(w http.ResponseWriter, req *http.Request) { 448 name := req.URL.Query().Get("config") 449 if err := removeConfig(ui.settingsFile, name); err != nil { 450 http.Error(w, err.Error(), http.StatusBadRequest) 451 ui.options.UI.PrintErr(err) 452 return 453 } 454} 455 456// getFromLegend returns the suffix of an entry in legend that starts 457// with param. It returns def if no such entry is found. 458func getFromLegend(legend []string, param, def string) string { 459 for _, s := range legend { 460 if strings.HasPrefix(s, param) { 461 return s[len(param):] 462 } 463 } 464 return def 465} 466