diff --git a/cmd/syncthing/gui.go b/cmd/syncthing/gui.go index 3b6d0be1..de2f22d5 100644 --- a/cmd/syncthing/gui.go +++ b/cmd/syncthing/gui.go @@ -7,9 +7,11 @@ package main import ( + "bytes" "crypto/tls" "encoding/json" "fmt" + "io" "io/ioutil" "net" "net/http" @@ -323,6 +325,7 @@ func (s *apiService) Serve() { debugMux.HandleFunc("/rest/debug/httpmetrics", s.getSystemHTTPMetrics) debugMux.HandleFunc("/rest/debug/cpuprof", s.getCPUProf) // duration debugMux.HandleFunc("/rest/debug/heapprof", s.getHeapProf) + debugMux.HandleFunc("/rest/debug/support", s.getSupportBundle) getRestMux.Handle("/rest/debug/", s.whenDebugging(debugMux)) // A handler that splits requests between the two above and disables @@ -1034,6 +1037,106 @@ func (s *apiService) getSystemLogTxt(w http.ResponseWriter, r *http.Request) { } } +type fileEntry struct { + name string + data []byte +} + +func (s *apiService) getSupportBundle(w http.ResponseWriter, r *http.Request) { + var files []fileEntry + + // Redacted configuration as a JSON + if jsonConfig, err := json.MarshalIndent(getRedactedConfig(s), "", " "); err == nil { + files = append(files, fileEntry{name: "config.json.txt", data: jsonConfig}) + } else { + l.Warnln("Support bundle: failed to create config.json:", err) + } + + // Log as a text + var buflog bytes.Buffer + for _, line := range s.systemLog.Since(time.Time{}) { + fmt.Fprintf(&buflog, "%s: %s\n", line.When.Format(time.RFC3339), line.Message) + } + files = append(files, fileEntry{name: "log.txt", data: buflog.Bytes()}) + + // Errors as a JSON + if errs := s.guiErrors.Since(time.Time{}); len(errs) > 0 { + if jsonError, err := json.MarshalIndent(errs, "", " "); err != nil { + files = append(files, fileEntry{name: "errors.json.txt", data: jsonError}) + } else { + l.Warnln("Support bundle: failed to create errors.json:", err) + } + } + + // Panic files as a JSON + if panicFiles, err := filepath.Glob(filepath.Join(baseDirs["config"], "panic*")); err == nil { + for _, f := range panicFiles { + if panicFile, err := ioutil.ReadFile(f); err != nil { + l.Warnf("Support bundle: failed to load %s: %s", filepath.Base(f), err) + } else { + files = append(files, fileEntry{name: filepath.Base(f), data: panicFile}) + } + } + } + + // Version and platform information as a JSON + if versionPlatform, err := json.MarshalIndent(map[string]string{ + "now": time.Now().Format(time.RFC3339), + "version": Version, + "codename": Codename, + "longVersion": LongVersion, + "os": runtime.GOOS, + "arch": runtime.GOARCH, + }, "", " "); err == nil { + files = append(files, fileEntry{name: "version-platform.json.txt", data: versionPlatform}) + } else { + l.Warnln("Failed to create versionPlatform.json: ", err) + } + + // Report Data as a JSON + if usageReportingData, err := json.MarshalIndent(reportData(s.cfg, s.model, s.connectionsService, usageReportVersion, true), "", " "); err != nil { + l.Warnln("Support bundle: failed to create versionPlatform.json:", err) + } else { + files = append(files, fileEntry{name: "usage-reporting.json.txt", data: usageReportingData}) + } + + // Heap and CPU Proofs as a pprof extension + var heapBuffer, cpuBuffer bytes.Buffer + filename := fmt.Sprintf("syncthing-heap-%s-%s-%s-%s.pprof", runtime.GOOS, runtime.GOARCH, Version, time.Now().Format("150405")) // hhmmss + runtime.GC() + pprof.WriteHeapProfile(&heapBuffer) + files = append(files, fileEntry{name: filename, data: heapBuffer.Bytes()}) + + const duration = 4 * time.Second + filename = fmt.Sprintf("syncthing-cpu-%s-%s-%s-%s.pprof", runtime.GOOS, runtime.GOARCH, Version, time.Now().Format("150405")) // hhmmss + pprof.StartCPUProfile(&cpuBuffer) + time.Sleep(duration) + pprof.StopCPUProfile() + files = append(files, fileEntry{name: filename, data: cpuBuffer.Bytes()}) + + // Add buffer files to buffer zip + var zipFilesBuffer bytes.Buffer + if err := writeZip(&zipFilesBuffer, files); err != nil { + l.Warnln("Support bundle: failed to create support bundle zip:", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Set zip file name and path + zipFileName := fmt.Sprintf("support-bundle-%s.zip", time.Now().Format("2018-01-02T15.04.05")) + zipFilePath := filepath.Join(baseDirs["config"], zipFileName) + + // Write buffer zip to local zip file (back up) + if err := ioutil.WriteFile(zipFilePath, zipFilesBuffer.Bytes(), 0600); err != nil { + l.Warnln("Support bundle: support bundle zip could not be created:", err) + } + + // Serve the buffer zip to client for download + w.Header().Set("Content-Type", "application/zip") + w.Header().Set("Content-Disposition", "attachment; filename="+zipFileName) + io.Copy(w, &zipFilesBuffer) +} + func (s *apiService) getSystemHTTPMetrics(w http.ResponseWriter, r *http.Request) { stats := make(map[string]interface{}) metrics.Each(func(name string, intf interface{}) { diff --git a/cmd/syncthing/main.go b/cmd/syncthing/main.go index 66323b28..59d92dff 100644 --- a/cmd/syncthing/main.go +++ b/cmd/syncthing/main.go @@ -1261,6 +1261,7 @@ func cleanConfigDirectory() { "*.idx.gz": 30 * 24 * time.Hour, // these should for sure no longer exist "backup-of-v0.8": 30 * 24 * time.Hour, // these neither "tmp-index-sorter.*": time.Minute, // these should never exist on startup + "support-bundle-*": 30 * 24 * time.Hour, // keep old support bundle zip or folder for a month } for pat, dur := range patterns { diff --git a/cmd/syncthing/support_bundle.go b/cmd/syncthing/support_bundle.go new file mode 100644 index 00000000..78b4aa98 --- /dev/null +++ b/cmd/syncthing/support_bundle.go @@ -0,0 +1,47 @@ +// Copyright (C) 2018 The Syncthing Authors. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +package main + +import ( + "archive/zip" + "io" + + "github.com/syncthing/syncthing/lib/config" +) + +// getRedactedConfig redacting some parts of config +func getRedactedConfig(s *apiService) config.Configuration { + rawConf := s.cfg.RawCopy() + rawConf.GUI.APIKey = "REDACTED" + if rawConf.GUI.Password != "" { + rawConf.GUI.Password = "REDACTED" + } + if rawConf.GUI.User != "" { + rawConf.GUI.User = "REDACTED" + } + return rawConf +} + +// writeZip writes a zip file containing the given entries +func writeZip(writer io.Writer, files []fileEntry) error { + zipWriter := zip.NewWriter(writer) + defer zipWriter.Close() + + for _, file := range files { + zipFile, err := zipWriter.Create(file.name) + if err != nil { + return err + } + + _, err = zipFile.Write(file.data) + if err != nil { + return err + } + } + + return zipWriter.Close() +} diff --git a/gui/default/index.html b/gui/default/index.html index 38f0225d..124cd346 100644 --- a/gui/default/index.html +++ b/gui/default/index.html @@ -81,6 +81,8 @@