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 }, 131 } 132 133 url := "http://" + args.Hostport 134 135 o.UI.Print("Serving web UI on ", url) 136 137 if o.UI.WantBrowser() && !disableBrowser { 138 go openBrowser(url, o) 139 } 140 return server(args) 141} 142 143func getHostAndPort(hostport string) (string, int, error) { 144 host, portStr, err := net.SplitHostPort(hostport) 145 if err != nil { 146 return "", 0, fmt.Errorf("could not split http address: %v", err) 147 } 148 if host == "" { 149 host = "localhost" 150 } 151 var port int 152 if portStr == "" { 153 ln, err := net.Listen("tcp", net.JoinHostPort(host, "0")) 154 if err != nil { 155 return "", 0, fmt.Errorf("could not generate random port: %v", err) 156 } 157 port = ln.Addr().(*net.TCPAddr).Port 158 err = ln.Close() 159 if err != nil { 160 return "", 0, fmt.Errorf("could not generate random port: %v", err) 161 } 162 } else { 163 port, err = strconv.Atoi(portStr) 164 if err != nil { 165 return "", 0, fmt.Errorf("invalid port number: %v", err) 166 } 167 } 168 return host, port, nil 169} 170func defaultWebServer(args *plugin.HTTPServerArgs) error { 171 ln, err := net.Listen("tcp", args.Hostport) 172 if err != nil { 173 return err 174 } 175 isLocal := isLocalhost(args.Host) 176 handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 177 if isLocal { 178 // Only allow local clients 179 host, _, err := net.SplitHostPort(req.RemoteAddr) 180 if err != nil || !isLocalhost(host) { 181 http.Error(w, "permission denied", http.StatusForbidden) 182 return 183 } 184 } 185 h := args.Handlers[req.URL.Path] 186 if h == nil { 187 // Fall back to default behavior 188 h = http.DefaultServeMux 189 } 190 h.ServeHTTP(w, req) 191 }) 192 193 // We serve the ui at /ui/ and redirect there from the root. This is done 194 // to surface any problems with serving the ui at a non-root early. See: 195 // 196 // https://github.com/google/pprof/pull/348 197 mux := http.NewServeMux() 198 mux.Handle("/ui/", http.StripPrefix("/ui", handler)) 199 mux.Handle("/", redirectWithQuery("/ui")) 200 s := &http.Server{Handler: mux} 201 return s.Serve(ln) 202} 203 204func redirectWithQuery(path string) http.HandlerFunc { 205 return func(w http.ResponseWriter, r *http.Request) { 206 pathWithQuery := &gourl.URL{Path: path, RawQuery: r.URL.RawQuery} 207 http.Redirect(w, r, pathWithQuery.String(), http.StatusTemporaryRedirect) 208 } 209} 210 211func isLocalhost(host string) bool { 212 for _, v := range []string{"localhost", "127.0.0.1", "[::1]", "::1"} { 213 if host == v { 214 return true 215 } 216 } 217 return false 218} 219 220func openBrowser(url string, o *plugin.Options) { 221 // Construct URL. 222 baseURL, _ := gourl.Parse(url) 223 current := currentConfig() 224 u, _ := current.makeURL(*baseURL) 225 226 // Give server a little time to get ready. 227 time.Sleep(time.Millisecond * 500) 228 229 for _, b := range browsers() { 230 args := strings.Split(b, " ") 231 if len(args) == 0 { 232 continue 233 } 234 viewer := exec.Command(args[0], append(args[1:], u.String())...) 235 viewer.Stderr = os.Stderr 236 if err := viewer.Start(); err == nil { 237 return 238 } 239 } 240 // No visualizer succeeded, so just print URL. 241 o.UI.PrintErr(u.String()) 242} 243 244// makeReport generates a report for the specified command. 245// If configEditor is not null, it is used to edit the config used for the report. 246func (ui *webInterface) makeReport(w http.ResponseWriter, req *http.Request, 247 cmd []string, configEditor func(*config)) (*report.Report, []string) { 248 cfg := currentConfig() 249 if err := cfg.applyURL(req.URL.Query()); err != nil { 250 http.Error(w, err.Error(), http.StatusBadRequest) 251 ui.options.UI.PrintErr(err) 252 return nil, nil 253 } 254 if configEditor != nil { 255 configEditor(&cfg) 256 } 257 catcher := &errorCatcher{UI: ui.options.UI} 258 options := *ui.options 259 options.UI = catcher 260 _, rpt, err := generateRawReport(ui.prof, cmd, cfg, &options) 261 if err != nil { 262 http.Error(w, err.Error(), http.StatusBadRequest) 263 ui.options.UI.PrintErr(err) 264 return nil, nil 265 } 266 return rpt, catcher.errors 267} 268 269// render generates html using the named template based on the contents of data. 270func (ui *webInterface) render(w http.ResponseWriter, req *http.Request, tmpl string, 271 rpt *report.Report, errList, legend []string, data webArgs) { 272 file := getFromLegend(legend, "File: ", "unknown") 273 profile := getFromLegend(legend, "Type: ", "unknown") 274 data.Title = file + " " + profile 275 data.Errors = errList 276 data.Total = rpt.Total() 277 data.SampleTypes = sampleTypes(ui.prof) 278 data.Legend = legend 279 data.Help = ui.help 280 data.Configs = configMenu(ui.settingsFile, *req.URL) 281 282 html := &bytes.Buffer{} 283 if err := ui.templates.ExecuteTemplate(html, tmpl, data); err != nil { 284 http.Error(w, "internal template error", http.StatusInternalServerError) 285 ui.options.UI.PrintErr(err) 286 return 287 } 288 w.Header().Set("Content-Type", "text/html") 289 w.Write(html.Bytes()) 290} 291 292// dot generates a web page containing an svg diagram. 293func (ui *webInterface) dot(w http.ResponseWriter, req *http.Request) { 294 rpt, errList := ui.makeReport(w, req, []string{"svg"}, nil) 295 if rpt == nil { 296 return // error already reported 297 } 298 299 // Generate dot graph. 300 g, config := report.GetDOT(rpt) 301 legend := config.Labels 302 config.Labels = nil 303 dot := &bytes.Buffer{} 304 graph.ComposeDot(dot, g, &graph.DotAttributes{}, config) 305 306 // Convert to svg. 307 svg, err := dotToSvg(dot.Bytes()) 308 if err != nil { 309 http.Error(w, "Could not execute dot; may need to install graphviz.", 310 http.StatusNotImplemented) 311 ui.options.UI.PrintErr("Failed to execute dot. Is Graphviz installed?\n", err) 312 return 313 } 314 315 // Get all node names into an array. 316 nodes := []string{""} // dot starts with node numbered 1 317 for _, n := range g.Nodes { 318 nodes = append(nodes, n.Info.Name) 319 } 320 321 ui.render(w, req, "graph", rpt, errList, legend, webArgs{ 322 HTMLBody: template.HTML(string(svg)), 323 Nodes: nodes, 324 }) 325} 326 327func dotToSvg(dot []byte) ([]byte, error) { 328 cmd := exec.Command("dot", "-Tsvg") 329 out := &bytes.Buffer{} 330 cmd.Stdin, cmd.Stdout, cmd.Stderr = bytes.NewBuffer(dot), out, os.Stderr 331 if err := cmd.Run(); err != nil { 332 return nil, err 333 } 334 335 // Fix dot bug related to unquoted ampersands. 336 svg := bytes.Replace(out.Bytes(), []byte("&;"), []byte("&;"), -1) 337 338 // Cleanup for embedding by dropping stuff before the <svg> start. 339 if pos := bytes.Index(svg, []byte("<svg")); pos >= 0 { 340 svg = svg[pos:] 341 } 342 return svg, nil 343} 344 345func (ui *webInterface) top(w http.ResponseWriter, req *http.Request) { 346 rpt, errList := ui.makeReport(w, req, []string{"top"}, func(cfg *config) { 347 cfg.NodeCount = 500 348 }) 349 if rpt == nil { 350 return // error already reported 351 } 352 top, legend := report.TextItems(rpt) 353 var nodes []string 354 for _, item := range top { 355 nodes = append(nodes, item.Name) 356 } 357 358 ui.render(w, req, "top", rpt, errList, legend, webArgs{ 359 Top: top, 360 Nodes: nodes, 361 }) 362} 363 364// disasm generates a web page containing disassembly. 365func (ui *webInterface) disasm(w http.ResponseWriter, req *http.Request) { 366 args := []string{"disasm", req.URL.Query().Get("f")} 367 rpt, errList := ui.makeReport(w, req, args, nil) 368 if rpt == nil { 369 return // error already reported 370 } 371 372 out := &bytes.Buffer{} 373 if err := report.PrintAssembly(out, rpt, ui.options.Obj, maxEntries); err != nil { 374 http.Error(w, err.Error(), http.StatusBadRequest) 375 ui.options.UI.PrintErr(err) 376 return 377 } 378 379 legend := report.ProfileLabels(rpt) 380 ui.render(w, req, "plaintext", rpt, errList, legend, webArgs{ 381 TextBody: out.String(), 382 }) 383 384} 385 386// source generates a web page containing source code annotated with profile 387// data. 388func (ui *webInterface) source(w http.ResponseWriter, req *http.Request) { 389 args := []string{"weblist", req.URL.Query().Get("f")} 390 rpt, errList := ui.makeReport(w, req, args, nil) 391 if rpt == nil { 392 return // error already reported 393 } 394 395 // Generate source listing. 396 var body bytes.Buffer 397 if err := report.PrintWebList(&body, rpt, ui.options.Obj, maxEntries); err != nil { 398 http.Error(w, err.Error(), http.StatusBadRequest) 399 ui.options.UI.PrintErr(err) 400 return 401 } 402 403 legend := report.ProfileLabels(rpt) 404 ui.render(w, req, "sourcelisting", rpt, errList, legend, webArgs{ 405 HTMLBody: template.HTML(body.String()), 406 }) 407} 408 409// peek generates a web page listing callers/callers. 410func (ui *webInterface) peek(w http.ResponseWriter, req *http.Request) { 411 args := []string{"peek", req.URL.Query().Get("f")} 412 rpt, errList := ui.makeReport(w, req, args, func(cfg *config) { 413 cfg.Granularity = "lines" 414 }) 415 if rpt == nil { 416 return // error already reported 417 } 418 419 out := &bytes.Buffer{} 420 if err := report.Generate(out, rpt, ui.options.Obj); err != nil { 421 http.Error(w, err.Error(), http.StatusBadRequest) 422 ui.options.UI.PrintErr(err) 423 return 424 } 425 426 legend := report.ProfileLabels(rpt) 427 ui.render(w, req, "plaintext", rpt, errList, legend, webArgs{ 428 TextBody: out.String(), 429 }) 430} 431 432// saveConfig saves URL configuration. 433func (ui *webInterface) saveConfig(w http.ResponseWriter, req *http.Request) { 434 if err := setConfig(ui.settingsFile, *req.URL); err != nil { 435 http.Error(w, err.Error(), http.StatusBadRequest) 436 ui.options.UI.PrintErr(err) 437 return 438 } 439} 440 441// deleteConfig deletes a configuration. 442func (ui *webInterface) deleteConfig(w http.ResponseWriter, req *http.Request) { 443 name := req.URL.Query().Get("config") 444 if err := removeConfig(ui.settingsFile, name); err != nil { 445 http.Error(w, err.Error(), http.StatusBadRequest) 446 ui.options.UI.PrintErr(err) 447 return 448 } 449} 450 451// getFromLegend returns the suffix of an entry in legend that starts 452// with param. It returns def if no such entry is found. 453func getFromLegend(legend []string, param, def string) string { 454 for _, s := range legend { 455 if strings.HasPrefix(s, param) { 456 return s[len(param):] 457 } 458 } 459 return def 460} 461