From ffe7a2fcd70cb731712a662daf2b11d3e3aae30a Mon Sep 17 00:00:00 2001 From: Jakob Borg Date: Tue, 2 Aug 2016 11:06:45 +0000 Subject: [PATCH] cmd/syncthing, lib/config: Enable HTTP CPU/heap profile collection for users This adds a config to enable debug functions on the API server, which is by default disabled. When enabled, the /rest/debug things become available and become available without requiring a CSRF token (although authentication is required if configured). We also add a new endpoint /rest/debug/cpuprof?duration=15s (with the duration being configurable, defaulting to 30s). This runs a CPU profile for the duration and returns it as a file. It sets headers so that a browser will save the file with an informative name. The same is done for heap profiles, /rest/debug/heapprof, which does not take any parameters. The purpose of this is that any user can enable debugging under advanced, then point their browser to the endpoint above and get a file that contains a CPU or heap profile we can use, with the filename telling us what version and architecture the profile is from. On the command line, this becomes curl -O -J http://localhost:8082/rest/debug/cpuprof?duration=5s curl: Saved to filename 'syncthing-cpu-darwin-amd64-v0.14.3+4-g935bcc0-110307.pprof' GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3467 --- cmd/syncthing/gui.go | 50 ++++++++++++++++++++++++++++++++-- cmd/syncthing/gui_csrf.go | 7 +++++ cmd/syncthing/main.go | 1 - lib/config/guiconfiguration.go | 1 + test/h1/config.xml | 2 +- test/h2/config.xml | 2 +- test/h3/config.xml | 2 +- test/h4/config.xml | 2 +- 8 files changed, 60 insertions(+), 7 deletions(-) diff --git a/cmd/syncthing/gui.go b/cmd/syncthing/gui.go index d8a6271d1..34e26175c 100644 --- a/cmd/syncthing/gui.go +++ b/cmd/syncthing/gui.go @@ -17,6 +17,7 @@ import ( "path/filepath" "reflect" "runtime" + "runtime/pprof" "sort" "strconv" "strings" @@ -268,8 +269,12 @@ func (s *apiService) Serve() { postRestMux.HandleFunc("/rest/system/debug", s.postSystemDebug) // [enable] [disable] // Debug endpoints, not for general use - getRestMux.HandleFunc("/rest/debug/peerCompletion", s.getPeerCompletion) - getRestMux.HandleFunc("/rest/debug/httpmetrics", s.getSystemHTTPMetrics) + debugMux := http.NewServeMux() + debugMux.HandleFunc("/rest/debug/peerCompletion", s.getPeerCompletion) + debugMux.HandleFunc("/rest/debug/httpmetrics", s.getSystemHTTPMetrics) + debugMux.HandleFunc("/rest/debug/cpuprof", s.getCPUProf) // duration + debugMux.HandleFunc("/rest/debug/heapprof", s.getHeapProf) + getRestMux.Handle("/rest/debug/", s.whenDebugging(debugMux)) // A handler that splits requests between the two above and disables // caching @@ -364,6 +369,9 @@ func (s *apiService) VerifyConfiguration(from, to config.Configuration) error { } func (s *apiService) CommitConfiguration(from, to config.Configuration) bool { + // No action required when this changes, so mask the fact that it changed at all. + from.GUI.Debugging = to.GUI.Debugging + if to.GUI == from.GUI { return true } @@ -487,6 +495,18 @@ func withDetailsMiddleware(id protocol.DeviceID, h http.Handler) http.Handler { }) } +func (s *apiService) whenDebugging(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if s.cfg.GUI().Debugging { + h.ServeHTTP(w, r) + return + } + + http.Error(w, "Debugging disabled", http.StatusBadRequest) + return + }) +} + func (s *apiService) restPing(w http.ResponseWriter, r *http.Request) { sendJSON(w, map[string]string{"ping": "pong"}) } @@ -1166,6 +1186,32 @@ func (s *apiService) getSystemBrowse(w http.ResponseWriter, r *http.Request) { sendJSON(w, ret) } +func (s *apiService) getCPUProf(w http.ResponseWriter, r *http.Request) { + duration, err := time.ParseDuration(r.FormValue("duration")) + if err != nil { + duration = 30 * time.Second + } + + filename := fmt.Sprintf("syncthing-cpu-%s-%s-%s-%s.pprof", runtime.GOOS, runtime.GOARCH, Version, time.Now().Format("150405")) // hhmmss + + w.Header().Set("Content-Type", "application/octet-stream") + w.Header().Set("Content-Disposition", "attachment; filename="+filename) + + pprof.StartCPUProfile(w) + time.Sleep(duration) + pprof.StopCPUProfile() +} + +func (s *apiService) getHeapProf(w http.ResponseWriter, r *http.Request) { + filename := fmt.Sprintf("syncthing-heap-%s-%s-%s-%s.pprof", runtime.GOOS, runtime.GOARCH, Version, time.Now().Format("150405")) // hhmmss + + w.Header().Set("Content-Type", "application/octet-stream") + w.Header().Set("Content-Disposition", "attachment; filename="+filename) + + runtime.GC() + pprof.WriteHeapProfile(w) +} + func (s *apiService) toNeedSlice(fs []db.FileInfoTruncated) []jsonDBFileInfo { res := make([]jsonDBFileInfo, len(fs)) for i, f := range fs { diff --git a/cmd/syncthing/gui_csrf.go b/cmd/syncthing/gui_csrf.go index 30bc82328..71a9770be 100644 --- a/cmd/syncthing/gui_csrf.go +++ b/cmd/syncthing/gui_csrf.go @@ -41,6 +41,13 @@ func csrfMiddleware(unique string, prefix string, cfg config.GUIConfiguration, n return } + if strings.HasPrefix(r.URL.Path, "/rest/debug") { + // Debugging functions are only available when explicitly + // enabled, and can be accessed without a CSRF token + next.ServeHTTP(w, r) + return + } + // Allow requests for anything not under the protected path prefix, // and set a CSRF cookie if there isn't already a valid one. if !strings.HasPrefix(r.URL.Path, prefix) { diff --git a/cmd/syncthing/main.go b/cmd/syncthing/main.go index e22eb71d7..17cf12811 100644 --- a/cmd/syncthing/main.go +++ b/cmd/syncthing/main.go @@ -16,7 +16,6 @@ import ( "log" "net" "net/http" - _ "net/http/pprof" "net/url" "os" "os/signal" diff --git a/lib/config/guiconfiguration.go b/lib/config/guiconfiguration.go index fb6c3764c..6a4e7e473 100644 --- a/lib/config/guiconfiguration.go +++ b/lib/config/guiconfiguration.go @@ -21,6 +21,7 @@ type GUIConfiguration struct { APIKey string `xml:"apikey,omitempty" json:"apiKey"` InsecureAdminAccess bool `xml:"insecureAdminAccess,omitempty" json:"insecureAdminAccess"` Theme string `xml:"theme" json:"theme" default:"default"` + Debugging bool `xml:"debugging,attr" json:"debugging"` } func (c GUIConfiguration) Address() string { diff --git a/test/h1/config.xml b/test/h1/config.xml index 32edc7bf8..9a8f46661 100644 --- a/test/h1/config.xml +++ b/test/h1/config.xml @@ -50,7 +50,7 @@
tcp://127.0.0.1:22004
- +
127.0.0.1:8081
testuser $2a$10$7tKL5uvLDGn5s2VLPM2yWOK/II45az0mTel8hxAUJDRQN1Tk2QYwu diff --git a/test/h2/config.xml b/test/h2/config.xml index 81a7b76c2..9702453f7 100644 --- a/test/h2/config.xml +++ b/test/h2/config.xml @@ -62,7 +62,7 @@
tcp://127.0.0.1:22003
- +
127.0.0.1:8082
abc123 default diff --git a/test/h3/config.xml b/test/h3/config.xml index 201247f7f..93977b158 100644 --- a/test/h3/config.xml +++ b/test/h3/config.xml @@ -45,7 +45,7 @@
tcp://127.0.0.1:22003
- +
127.0.0.1:8083
abc123 default diff --git a/test/h4/config.xml b/test/h4/config.xml index b3dda5840..740537668 100644 --- a/test/h4/config.xml +++ b/test/h4/config.xml @@ -18,7 +18,7 @@
dynamic
- +
127.0.0.1:8084
PMA5yUTG-Mw98nJ0YEtWTCHlM5O4aNi0 default