From 2760d032ca672435e06a1eeb7832f5fe1af14f92 Mon Sep 17 00:00:00 2001 From: Audrius Butkevicius Date: Thu, 12 Oct 2017 06:16:46 +0000 Subject: [PATCH] cmd/syncthing: Add more stats to usage reports (ref #3628) GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/4347 --- cmd/syncthing/gui.go | 21 ++- cmd/syncthing/main.go | 16 +-- cmd/syncthing/mocked_connections_test.go | 4 + cmd/syncthing/mocked_model_test.go | 4 + cmd/syncthing/usage_report.go | 122 ++++++++---------- gui/default/syncthing/core/modalDirective.js | 2 + .../syncthing/core/syncthingController.js | 51 ++++++-- .../syncthing/settings/settingsModalView.html | 12 +- .../usagereport/usageReportModalView.html | 11 +- .../usageReportPreviewModalView.html | 7 +- lib/config/config.go | 11 +- lib/config/config_test.go | 1 + lib/config/optionsconfiguration.go | 1 + lib/config/testdata/v24.xml | 16 +++ lib/connections/kcp_listen.go | 19 ++- lib/connections/relay_listen.go | 4 + lib/connections/service.go | 12 ++ lib/connections/structs.go | 19 +++ lib/connections/tcp_listen.go | 4 + lib/model/folder.go | 4 + lib/model/model.go | 101 +++++++++++++++ lib/model/model_test.go | 3 + lib/model/rwfolder.go | 30 +++++ 23 files changed, 358 insertions(+), 117 deletions(-) create mode 100644 lib/config/testdata/v24.xml diff --git a/cmd/syncthing/gui.go b/cmd/syncthing/gui.go index 57134d452..640f4778f 100644 --- a/cmd/syncthing/gui.go +++ b/cmd/syncthing/gui.go @@ -100,6 +100,7 @@ type modelIntf interface { CurrentSequence(folder string) (int64, bool) RemoteSequence(folder string) (int64, bool) State(folder string) (string, time.Time, error) + UsageReportingStats(version int) map[string]interface{} } type configIntf interface { @@ -119,6 +120,7 @@ type configIntf interface { type connectionsIntf interface { Status() map[string]interface{} + NATType() string } type rater interface { @@ -800,18 +802,6 @@ func (s *apiService) postSystemConfig(w http.ResponseWriter, r *http.Request) { } } - // Fixup usage reporting settings - - if curAcc := s.cfg.Options().URAccepted; to.Options.URAccepted > curAcc { - // UR was enabled - to.Options.URAccepted = usageReportVersion - to.Options.URUniqueID = rand.String(8) - } else if to.Options.URAccepted < curAcc { - // UR was disabled - to.Options.URAccepted = -1 - to.Options.URUniqueID = "" - } - // Activate and save if err := s.cfg.Replace(to); err != nil { @@ -903,6 +893,7 @@ func (s *apiService) getSystemStatus(w http.ResponseWriter, r *http.Request) { // gives us percent res["cpuPercent"] = s.cpu.Rate() / 10 / float64(runtime.NumCPU()) res["pathSeparator"] = string(filepath.Separator) + res["urVersionMax"] = usageReportVersion res["uptime"] = int(time.Since(startTime).Seconds()) res["startTime"] = startTime @@ -981,7 +972,11 @@ func (s *apiService) getSystemDiscovery(w http.ResponseWriter, r *http.Request) } func (s *apiService) getReport(w http.ResponseWriter, r *http.Request) { - sendJSON(w, reportData(s.cfg, s.model)) + version := usageReportVersion + if val, _ := strconv.Atoi(r.URL.Query().Get("version")); val > 0 { + version = val + } + sendJSON(w, reportData(s.cfg, s.model, s.connectionsService, version)) } func (s *apiService) getRandomString(w http.ResponseWriter, r *http.Request) { diff --git a/cmd/syncthing/main.go b/cmd/syncthing/main.go index 949ee4283..e86024686 100644 --- a/cmd/syncthing/main.go +++ b/cmd/syncthing/main.go @@ -882,24 +882,14 @@ func syncthingMain(runtimeOptions RuntimeOptions) { // Unique ID will be set and config saved below if necessary. } - if opts.URAccepted > 0 && opts.URAccepted < usageReportVersion { - l.Infoln("Anonymous usage report has changed; revoking acceptance") - opts.URAccepted = 0 - opts.URUniqueID = "" - cfg.SetOptions(opts) - } - - if opts.URAccepted >= usageReportVersion && opts.URUniqueID == "" { - // Generate and save a new unique ID if it is missing. + if opts.URUniqueID == "" { opts.URUniqueID = rand.String(8) cfg.SetOptions(opts) cfg.Save() } - // The usageReportingManager registers itself to listen to configuration - // changes, and there's nothing more we need to tell it from the outside. - // Hence we don't keep the returned pointer. - newUsageReportingManager(cfg, m) + usageReportingSvc := newUsageReportingService(cfg, m, connectionsService) + mainService.Add(usageReportingSvc) if opts.RestartOnWakeup { go standbyMonitor() diff --git a/cmd/syncthing/mocked_connections_test.go b/cmd/syncthing/mocked_connections_test.go index 82286af89..ebb90503b 100644 --- a/cmd/syncthing/mocked_connections_test.go +++ b/cmd/syncthing/mocked_connections_test.go @@ -11,3 +11,7 @@ type mockedConnections struct{} func (m *mockedConnections) Status() map[string]interface{} { return nil } + +func (m *mockedConnections) NATType() string { + return "" +} diff --git a/cmd/syncthing/mocked_model_test.go b/cmd/syncthing/mocked_model_test.go index 3ca25bd8a..8f9acd08f 100644 --- a/cmd/syncthing/mocked_model_test.go +++ b/cmd/syncthing/mocked_model_test.go @@ -114,3 +114,7 @@ func (m *mockedModel) RemoteSequence(folder string) (int64, bool) { func (m *mockedModel) State(folder string) (string, time.Time, error) { return "", time.Time{}, nil } + +func (m *mockedModel) UsageReportingStats(version int) map[string]interface{} { + return nil +} diff --git a/cmd/syncthing/usage_report.go b/cmd/syncthing/usage_report.go index c43958015..aa5ea6a0a 100644 --- a/cmd/syncthing/usage_report.go +++ b/cmd/syncthing/usage_report.go @@ -12,7 +12,6 @@ import ( "crypto/rand" "crypto/tls" "encoding/json" - "fmt" "net/http" "runtime" "sort" @@ -20,71 +19,25 @@ import ( "time" "github.com/syncthing/syncthing/lib/config" + "github.com/syncthing/syncthing/lib/connections" "github.com/syncthing/syncthing/lib/dialer" "github.com/syncthing/syncthing/lib/model" "github.com/syncthing/syncthing/lib/protocol" "github.com/syncthing/syncthing/lib/scanner" "github.com/syncthing/syncthing/lib/upgrade" - "github.com/thejerf/suture" ) // Current version number of the usage report, for acceptance purposes. If // fields are added or changed this integer must be incremented so that users // are prompted for acceptance of the new report. -const usageReportVersion = 2 - -type usageReportingManager struct { - cfg *config.Wrapper - model *model.Model - sup *suture.Supervisor -} - -func newUsageReportingManager(cfg *config.Wrapper, m *model.Model) *usageReportingManager { - mgr := &usageReportingManager{ - cfg: cfg, - model: m, - } - - // Start UR if it's enabled. - mgr.CommitConfiguration(config.Configuration{}, cfg.RawCopy()) - - // Listen to future config changes so that we can start and stop as - // appropriate. - cfg.Subscribe(mgr) - - return mgr -} - -func (m *usageReportingManager) VerifyConfiguration(from, to config.Configuration) error { - return nil -} - -func (m *usageReportingManager) CommitConfiguration(from, to config.Configuration) bool { - if to.Options.URAccepted >= usageReportVersion && m.sup == nil { - // Usage reporting was turned on; lets start it. - service := newUsageReportingService(m.cfg, m.model) - m.sup = suture.NewSimple("usageReporting") - m.sup.Add(service) - m.sup.ServeBackground() - } else if to.Options.URAccepted < usageReportVersion && m.sup != nil { - // Usage reporting was turned off - m.sup.Stop() - m.sup = nil - } - - return true -} - -func (m *usageReportingManager) String() string { - return fmt.Sprintf("usageReportingManager@%p", m) -} +const usageReportVersion = 3 // reportData returns the data to be sent in a usage report. It's used in // various places, so not part of the usageReportingManager object. -func reportData(cfg configIntf, m modelIntf) map[string]interface{} { +func reportData(cfg configIntf, m modelIntf, connectionsService connectionsIntf, version int) map[string]interface{} { opts := cfg.Options() res := make(map[string]interface{}) - res["urVersion"] = usageReportVersion + res["urVersion"] = version res["uniqueID"] = opts.URUniqueID res["version"] = Version res["longVersion"] = LongVersion @@ -227,25 +180,40 @@ func reportData(cfg configIntf, m modelIntf) map[string]interface{} { res["upgradeAllowedAuto"] = !(upgrade.DisabledByCompilation || noUpgradeFromEnv) && opts.AutoUpgradeIntervalH > 0 res["upgradeAllowedPre"] = !(upgrade.DisabledByCompilation || noUpgradeFromEnv) && opts.AutoUpgradeIntervalH > 0 && opts.UpgradeToPreReleases + if version >= 3 { + res["uptime"] = time.Now().Sub(startTime).Seconds() + res["natType"] = connectionsService.NATType() + } + + for key, value := range m.UsageReportingStats(version) { + res[key] = value + } + return res } type usageReportingService struct { - cfg *config.Wrapper - model *model.Model - stop chan struct{} + cfg *config.Wrapper + model *model.Model + connectionsService *connections.Service + forceRun chan struct{} + stop chan struct{} } -func newUsageReportingService(cfg *config.Wrapper, model *model.Model) *usageReportingService { - return &usageReportingService{ - cfg: cfg, - model: model, - stop: make(chan struct{}), +func newUsageReportingService(cfg *config.Wrapper, model *model.Model, connectionsService *connections.Service) *usageReportingService { + svc := &usageReportingService{ + cfg: cfg, + model: model, + connectionsService: connectionsService, + forceRun: make(chan struct{}), + stop: make(chan struct{}), } + cfg.Subscribe(svc) + return svc } func (s *usageReportingService) sendUsageReport() error { - d := reportData(s.cfg, s.model) + d := reportData(s.cfg, s.model, s.connectionsService, s.cfg.Options().URAccepted) var b bytes.Buffer json.NewEncoder(&b).Encode(d) @@ -264,27 +232,45 @@ func (s *usageReportingService) sendUsageReport() error { func (s *usageReportingService) Serve() { s.stop = make(chan struct{}) - - l.Infoln("Starting usage reporting") - defer l.Infoln("Stopping usage reporting") - - t := time.NewTimer(time.Duration(s.cfg.Options().URInitialDelayS) * time.Second) // time to initial report at start + t := time.NewTimer(time.Duration(s.cfg.Options().URInitialDelayS) * time.Second) for { select { case <-s.stop: return + case <-s.forceRun: + t.Reset(0) case <-t.C: - err := s.sendUsageReport() - if err != nil { - l.Infoln("Usage report:", err) + if s.cfg.Options().URAccepted >= 2 { + err := s.sendUsageReport() + if err != nil { + l.Infoln("Usage report:", err) + } else { + l.Infof("Sent usage report (version %d)", s.cfg.Options().URAccepted) + } } t.Reset(24 * time.Hour) // next report tomorrow } } } +func (s *usageReportingService) VerifyConfiguration(from, to config.Configuration) error { + return nil +} + +func (s *usageReportingService) CommitConfiguration(from, to config.Configuration) bool { + if from.Options.URAccepted != to.Options.URAccepted || from.Options.URUniqueID != to.Options.URUniqueID || from.Options.URURL != to.Options.URURL { + s.forceRun <- struct{}{} + } + return true +} + func (s *usageReportingService) Stop() { close(s.stop) + close(s.forceRun) +} + +func (usageReportingService) String() string { + return "usageReportingService" } // cpuBench returns CPU performance as a measure of single threaded SHA-256 MiB/s diff --git a/gui/default/syncthing/core/modalDirective.js b/gui/default/syncthing/core/modalDirective.js index def2ff3fa..43553ac0d 100644 --- a/gui/default/syncthing/core/modalDirective.js +++ b/gui/default/syncthing/core/modalDirective.js @@ -1,6 +1,8 @@ angular.module('syncthing.core') .directive('modal', function () { return { + // If you ever change any of the petroglyphs below, please search for $parent.$parent, + // as some templates rely on the way scope is composed in this case. restrict: 'E', templateUrl: 'modal.html', replace: true, diff --git a/gui/default/syncthing/core/syncthingController.js b/gui/default/syncthing/core/syncthingController.js index 8aad64176..710131ef8 100755 --- a/gui/default/syncthing/core/syncthingController.js +++ b/gui/default/syncthing/core/syncthingController.js @@ -33,6 +33,8 @@ angular.module('syncthing.core') $scope.folderRejections = {}; $scope.protocolChanged = false; $scope.reportData = {}; + $scope.reportDataPreview = {}; + $scope.reportDataPreviewVersion = ''; $scope.reportPreview = false; $scope.folders = {}; $scope.seenError = ''; @@ -133,7 +135,11 @@ angular.module('syncthing.core') }).error($scope.emitHTTPError); $http.get(urlbase + '/svc/report').success(function (data) { - $scope.reportData = data; + $scope.reportDataPreview = $scope.reportData = data; + if ($scope.system && $scope.config.options.urSeen < $scope.system.urVersionMax) { + // Usage reporting format has changed, prompt the user to re-accept. + $('#ur').modal(); + } }).error($scope.emitHTTPError); $http.get(urlbase + '/system/upgrade').success(function (data) { @@ -376,6 +382,7 @@ angular.module('syncthing.core') $scope.config = config; $scope.config.options._listenAddressesStr = $scope.config.options.listenAddresses.join(', '); $scope.config.options._globalAnnounceServersStr = $scope.config.options.globalAnnounceServers.join(', '); + $scope.config.options._urAcceptedStr = "" + $scope.config.options.urAccepted; $scope.devices = $scope.config.devices; $scope.devices.forEach(function (deviceCfg) { @@ -412,6 +419,10 @@ angular.module('syncthing.core') $scope.myID = data.myID; $scope.system = data; + if ($scope.reportDataPreviewVersion === '') { + $scope.reportDataPreviewVersion = $scope.system.urVersionMax; + } + var listenersFailed = []; for (var address in data.connectionServiceStatus) { if (data.connectionServiceStatus[address].error) { @@ -1058,7 +1069,6 @@ angular.module('syncthing.core') $scope.editSettings = function () { // Make a working copy $scope.tmpOptions = angular.copy($scope.config.options); - $scope.tmpOptions.urEnabled = ($scope.tmpOptions.urAccepted > 0); $scope.tmpOptions.deviceName = $scope.thisDevice().name; $scope.tmpOptions.upgrades = "none"; if ($scope.tmpOptions.autoUpgradeIntervalH > 0) { @@ -1088,18 +1098,31 @@ angular.module('syncthing.core') }).error($scope.emitHTTPError); }; + $scope.urVersions = function() { + var result = []; + if ($scope.system) { + for (var i = $scope.system.urVersionMax; i >= 2; i--) { + result.push("" + i); + } + } + return result; + }; + $scope.saveSettings = function () { // Make sure something changed var changed = !angular.equals($scope.config.options, $scope.tmpOptions) || !angular.equals($scope.config.gui, $scope.tmpGUI); var themeChanged = $scope.config.gui.theme !== $scope.tmpGUI.theme; if (changed) { + // Angular has issues with selects with numeric values, so we handle strings here. + $scope.tmpOptions.urAccepted = parseInt($scope.tmpOptions._urAcceptedStr); // Check if auto-upgrade has been enabled or disabled. This // also has an effect on usage reporting, so do the check // for that later. if ($scope.tmpOptions.upgrades == "candidate") { $scope.tmpOptions.autoUpgradeIntervalH = $scope.tmpOptions.autoUpgradeIntervalH || 12; $scope.tmpOptions.upgradeToPreReleases = true; - $scope.tmpOptions.urEnabled = true; + $scope.tmpOptions.urAccepted = $scope.system.urVersionMax; + $scope.tmpOptions.urSeen = $scope.system.urVersionMax; } else if ($scope.tmpOptions.upgrades == "stable") { $scope.tmpOptions.autoUpgradeIntervalH = $scope.tmpOptions.autoUpgradeIntervalH || 12; $scope.tmpOptions.upgradeToPreReleases = false; @@ -1107,13 +1130,6 @@ angular.module('syncthing.core') $scope.tmpOptions.autoUpgradeIntervalH = 0; } - // Check if usage reporting has been enabled or disabled - if ($scope.tmpOptions.urEnabled && $scope.tmpOptions.urAccepted <= 0) { - $scope.tmpOptions.urAccepted = 1000; - } else if (!$scope.tmpOptions.urEnabled && $scope.tmpOptions.urAccepted > 0) { - $scope.tmpOptions.urAccepted = -1; - } - // Check if protocol will need to be changed on restart if ($scope.config.gui.useTLS !== $scope.tmpGUI.useTLS) { $scope.protocolChanged = true; @@ -1691,13 +1707,17 @@ angular.module('syncthing.core') }; $scope.acceptUR = function () { - $scope.config.options.urAccepted = 1000; // Larger than the largest existing report version + $scope.config.options.urAccepted = $scope.system.urVersionMax; + $scope.config.options.urSeen = $scope.system.urVersionMax; $scope.saveConfig(); $('#ur').modal('hide'); }; $scope.declineUR = function () { - $scope.config.options.urAccepted = -1; + if ($scope.config.options.urAccepted === 0) { + $scope.config.options.urAccepted = -1; + } + $scope.config.options.urSeen = $scope.system.urVersionMax; $scope.saveConfig(); $('#ur').modal('hide'); }; @@ -1747,6 +1767,13 @@ angular.module('syncthing.core') $scope.reportPreview = true; }; + $scope.refreshReportDataPreview = function () { + $scope.reportDataPreview = ''; + $http.get(urlbase + '/svc/report?version=' + $scope.reportDataPreviewVersion).success(function (data) { + $scope.reportDataPreview = data; + }).error($scope.emitHTTPError); + }; + $scope.rescanAllFolders = function () { $http.post(urlbase + "/db/scan"); }; diff --git a/gui/default/syncthing/settings/settingsModalView.html b/gui/default/syncthing/settings/settingsModalView.html index 77540ec35..782c9f5f4 100644 --- a/gui/default/syncthing/settings/settingsModalView.html +++ b/gui/default/syncthing/settings/settingsModalView.html @@ -139,10 +139,14 @@
-
- +
+ (Preview) +

Usage reporting is always enabled for candidate releases. (Preview) diff --git a/gui/default/syncthing/usagereport/usageReportModalView.html b/gui/default/syncthing/usagereport/usageReportModalView.html index 76b1d294a..620a98ad2 100644 --- a/gui/default/syncthing/usagereport/usageReportModalView.html +++ b/gui/default/syncthing/usagereport/usageReportModalView.html @@ -1,8 +1,13 @@