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 @@
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 @@
The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again. The aggregated statistics are publicly available at the URL below. Anonymous Usage report format has changed. Would you like to move to the new format? The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again. The aggregated statistics are publicly available at the URL below. The aggregated statistics are publicly available at the URL below.